chore: extract molecule-medo plugin to standalone repo
molecule-medo now lives at Molecule-AI/molecule-ai-plugin-molecule-medo (same pattern as all other plugins). Removed the gitignore exception that kept it in the monorepo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
595aa3681d
commit
cbee9a7237
4
.gitignore
vendored
4
.gitignore
vendored
@ -133,7 +133,5 @@ org-templates/**/.auth-token
|
||||
!/org-templates/molecule-dev
|
||||
/org-templates/molecule-dev/*
|
||||
!/org-templates/molecule-dev/system-prompt.md
|
||||
/plugins/*
|
||||
# Exception: molecule-medo lives here until it gets its own standalone repo.
|
||||
!/plugins/molecule-medo/
|
||||
/plugins/
|
||||
/workspace-configs-templates/
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
name: molecule-medo
|
||||
version: 0.1.0
|
||||
description: Baidu MeDo no-code AI platform integration (hackathon / China-region)
|
||||
author: Molecule AI
|
||||
tags: [hackathon, baidu, medo, china]
|
||||
runtimes: [claude_code, deepagents, langgraph]
|
||||
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: MeDo Tools
|
||||
description: >
|
||||
Create, update, and publish applications on Baidu MeDo (摩搭), a no-code AI
|
||||
application builder. Used in the Molecule AI hackathon integration (May 2026).
|
||||
tags: [hackathon, baidu, medo, china, no-code]
|
||||
examples:
|
||||
- "Create a chatbot app on MeDo called 'Customer Support'"
|
||||
- "Update the content of my MeDo app abc123"
|
||||
- "Publish my MeDo app to production"
|
||||
---
|
||||
|
||||
# MeDo Tools
|
||||
|
||||
Provides three tools for interacting with the Baidu MeDo no-code platform:
|
||||
|
||||
- **create_medo_app** — Scaffold a new application from a template (blank, chatbot, form, dashboard).
|
||||
- **update_medo_app** — Push content or configuration changes to an existing application.
|
||||
- **publish_medo_app** — Publish a draft application to production or staging.
|
||||
|
||||
## Setup
|
||||
|
||||
Set `MEDO_API_KEY` as a workspace secret. Optionally override the base URL via `MEDO_BASE_URL`
|
||||
(default: `https://api.moda.baidu.com/v1`).
|
||||
|
||||
When `MEDO_API_KEY` is absent the tools run in mock mode and return stub responses — safe for
|
||||
local development and testing.
|
||||
@ -1,106 +0,0 @@
|
||||
"""MeDo tools — Baidu MeDo no-code AI platform integration.
|
||||
|
||||
MeDo (摩搭, moda.baidu.com) is Baidu's no-code AI application builder used in
|
||||
the Molecule AI hackathon integration (May 2026). Three core operations:
|
||||
create_medo_app — scaffold a new application from a template
|
||||
update_medo_app — push content / config changes to an existing app
|
||||
publish_medo_app — publish a draft app to a target environment
|
||||
|
||||
Authentication: set MEDO_API_KEY as a workspace secret.
|
||||
Override base URL via MEDO_BASE_URL (default: https://api.moda.baidu.com/v1).
|
||||
|
||||
Mock backend: when MEDO_API_KEY is absent the tools return a predictable stub
|
||||
response — safe for unit tests and local development.
|
||||
TODO: swap _mock_http_post for a real httpx.AsyncClient call once keys are live.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MEDO_BASE_URL = os.environ.get("MEDO_BASE_URL", "https://api.moda.baidu.com/v1")
|
||||
MEDO_API_KEY = os.environ.get("MEDO_API_KEY", "")
|
||||
|
||||
_VALID_TEMPLATES = ("blank", "chatbot", "form", "dashboard")
|
||||
_VALID_ENVS = ("production", "staging")
|
||||
|
||||
|
||||
async def _mock_http_post(path: str, payload: dict) -> dict:
|
||||
"""Stub HTTP call. TODO: replace with real httpx.AsyncClient once MEDO_API_KEY is live."""
|
||||
return {"status": "ok", "mock": True, "path": path, "payload_keys": list(payload.keys())}
|
||||
|
||||
|
||||
@tool
|
||||
async def create_medo_app(name: str, template: str = "blank", description: str = "") -> dict:
|
||||
"""Create a new MeDo application.
|
||||
|
||||
Args:
|
||||
name: Application name (required).
|
||||
template: Starting template — blank | chatbot | form | dashboard (default: blank).
|
||||
description: Short description of the application.
|
||||
|
||||
Returns:
|
||||
dict with 'app_id' and 'status' on success, 'error' key on failure.
|
||||
"""
|
||||
if not name:
|
||||
return {"error": "name is required"}
|
||||
if template not in _VALID_TEMPLATES:
|
||||
return {"error": f"template must be one of: {', '.join(_VALID_TEMPLATES)}"}
|
||||
try:
|
||||
result = await _mock_http_post("/apps", {"name": name, "template": template, "description": description})
|
||||
logger.info("MeDo create_app: name=%s template=%s → %s", name, template, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
logger.exception("MeDo create_app failed")
|
||||
return {"error": str(exc)}
|
||||
|
||||
|
||||
@tool
|
||||
async def update_medo_app(app_id: str, content: dict) -> dict:
|
||||
"""Push content or configuration changes to an existing MeDo application.
|
||||
|
||||
Args:
|
||||
app_id: The MeDo application ID returned by create_medo_app.
|
||||
content: Dict of fields to update (e.g. {"title": "...", "nodes": [...]}).
|
||||
|
||||
Returns:
|
||||
dict with 'status' on success, 'error' key on failure.
|
||||
"""
|
||||
if not app_id:
|
||||
return {"error": "app_id is required"}
|
||||
if not content:
|
||||
return {"error": "content must be a non-empty dict"}
|
||||
try:
|
||||
result = await _mock_http_post(f"/apps/{app_id}", content)
|
||||
logger.info("MeDo update_app: app_id=%s keys=%s → %s", app_id, list(content.keys()), result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
logger.exception("MeDo update_app failed")
|
||||
return {"error": str(exc)}
|
||||
|
||||
|
||||
@tool
|
||||
async def publish_medo_app(app_id: str, environment: str = "production") -> dict:
|
||||
"""Publish a MeDo application to a target environment.
|
||||
|
||||
Args:
|
||||
app_id: The MeDo application ID to publish.
|
||||
environment: Target — production | staging (default: production).
|
||||
|
||||
Returns:
|
||||
dict with 'status' on success, 'error' key on failure.
|
||||
"""
|
||||
if not app_id:
|
||||
return {"error": "app_id is required"}
|
||||
if environment not in _VALID_ENVS:
|
||||
return {"error": f"environment must be one of: {', '.join(_VALID_ENVS)}"}
|
||||
try:
|
||||
result = await _mock_http_post(f"/apps/{app_id}/publish", {"environment": environment})
|
||||
logger.info("MeDo publish_app: app_id=%s env=%s → %s", app_id, environment, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
logger.exception("MeDo publish_app failed")
|
||||
return {"error": str(exc)}
|
||||
@ -1,21 +0,0 @@
|
||||
"""Minimal conftest for molecule-medo plugin tests.
|
||||
|
||||
langchain_core is a declared dependency of workspace-template (>=0.3.0) and
|
||||
is expected to be present in the test environment. If it is absent, mock it
|
||||
so the @tool decorator in medo.py is a no-op and the tests can still run.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
def _mock_langchain_if_missing():
|
||||
if "langchain_core" not in sys.modules:
|
||||
lc_mod = ModuleType("langchain_core")
|
||||
lc_tools_mod = ModuleType("langchain_core.tools")
|
||||
lc_tools_mod.tool = lambda f: f # @tool becomes identity decorator
|
||||
sys.modules["langchain_core"] = lc_mod
|
||||
sys.modules["langchain_core.tools"] = lc_tools_mod
|
||||
|
||||
|
||||
_mock_langchain_if_missing()
|
||||
@ -1,85 +0,0 @@
|
||||
"""Tests for plugins/molecule-medo/skills/medo-tools/scripts/medo.py.
|
||||
|
||||
All tests exercise the mock backend (no MEDO_API_KEY required).
|
||||
|
||||
NOTE: @tool is a LangChain decorator that returns a StructuredTool rather than
|
||||
the raw async function. conftest.py mocks langchain_core.tools.tool as an
|
||||
identity decorator so that calling the functions directly (without .ainvoke())
|
||||
works in tests — matching the original test approach.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# plugin root: plugins/molecule-medo/
|
||||
_PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
||||
_MEDO_PATH = _PLUGIN_ROOT / "skills" / "medo-tools" / "scripts" / "medo.py"
|
||||
|
||||
|
||||
def _load_medo():
|
||||
spec = importlib.util.spec_from_file_location("medo_plugin_tools", _MEDO_PATH)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules["medo_plugin_tools"] = mod # register before exec to handle self-refs
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def medo(monkeypatch):
|
||||
monkeypatch.delenv("MEDO_API_KEY", raising=False)
|
||||
monkeypatch.delenv("MEDO_BASE_URL", raising=False)
|
||||
return _load_medo()
|
||||
|
||||
|
||||
class TestCreateMedoApp:
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_name(self, medo):
|
||||
result = await medo.create_medo_app(name="")
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_unknown_template(self, medo):
|
||||
result = await medo.create_medo_app(name="app", template="unknown")
|
||||
assert "error" in result and "template" in result["error"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_success(self, medo):
|
||||
result = await medo.create_medo_app(name="my-app", template="chatbot")
|
||||
assert result.get("mock") is True and result.get("status") == "ok"
|
||||
|
||||
|
||||
class TestUpdateMedoApp:
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_app_id(self, medo):
|
||||
result = await medo.update_medo_app(app_id="", content={"title": "x"})
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_non_empty_content(self, medo):
|
||||
result = await medo.update_medo_app(app_id="abc", content={})
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_success(self, medo):
|
||||
result = await medo.update_medo_app(app_id="abc", content={"title": "v2"})
|
||||
assert result.get("mock") is True and "abc" in result.get("path", "")
|
||||
|
||||
|
||||
class TestPublishMedoApp:
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_app_id(self, medo):
|
||||
result = await medo.publish_medo_app(app_id="")
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_invalid_environment(self, medo):
|
||||
result = await medo.publish_medo_app(app_id="abc", environment="dev")
|
||||
assert "error" in result and "environment" in result["error"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_success(self, medo):
|
||||
result = await medo.publish_medo_app(app_id="abc")
|
||||
assert result.get("mock") is True and result.get("status") == "ok"
|
||||
Loading…
Reference in New Issue
Block a user