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:
rabbitblood 2026-04-17 16:11:50 -07:00
parent 595aa3681d
commit cbee9a7237
6 changed files with 1 additions and 248 deletions

4
.gitignore vendored
View File

@ -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/

View File

@ -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]

View File

@ -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.

View File

@ -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)}

View File

@ -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()

View File

@ -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"