From 7214b52f23d31086008d2389ed72eda009e35dd2 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 16 Apr 2026 04:27:22 -0700 Subject: [PATCH] feat: add adapter code + Dockerfile for standalone deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapters extracted from molecule-monorepo/workspace-template. Uses molecule-ai-workspace-runtime PyPI package for shared infrastructure. - adapter.py — runtime-specific adapter class - requirements.txt — runtime-specific deps + molecule-ai-workspace-runtime - Dockerfile — FROM python:3.11-slim, pip install, COPY adapter, molecule-runtime entrypoint - ADAPTER_MODULE=adapter tells the runtime to load this repo's Adapter class Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 18 ++++++ __init__.py | 3 + adapter.py | 144 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++ 4 files changed, 170 insertions(+) create mode 100644 Dockerfile create mode 100644 __init__.py create mode 100644 adapter.py create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1c461ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl gosu ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -u 1000 -m -s /bin/bash agent +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY adapter.py . +COPY __init__.py . + +ENV ADAPTER_MODULE=adapter + +ENTRYPOINT ["molecule-runtime"] diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..8f464b9 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .adapter import CrewAIAdapter + +Adapter = CrewAIAdapter diff --git a/adapter.py b/adapter.py new file mode 100644 index 0000000..ac5b3a4 --- /dev/null +++ b/adapter.py @@ -0,0 +1,144 @@ +"""CrewAI adapter — role-based multi-agent framework with full platform integration. + +Creates a CrewAI Agent + Task + Crew with all platform tools (delegation, memory, +sandbox, approval), skills, plugins, and coordinator support. + +Requires: pip install crewai +""" + +import asyncio +import logging + +from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig +from a2a.server.agent_execution import AgentExecutor + +logger = logging.getLogger(__name__) + + +def _langchain_to_crewai(lc_tool): + """Wrap a LangChain BaseTool as a sync CrewAI @tool. + + CrewAI's @tool decorator requires the function to have a docstring + at decoration time, so we set __doc__ before applying the decorator. + """ + from crewai.tools import tool as crewai_tool + + def wrapper(**kwargs) -> str: + """Placeholder.""" + result = asyncio.get_event_loop().run_until_complete(lc_tool.ainvoke(kwargs)) + return str(result) + + wrapper.__name__ = lc_tool.name + wrapper.__doc__ = lc_tool.description or f"Tool: {lc_tool.name}" + return crewai_tool(lc_tool.name)(wrapper) + + +class CrewAIAdapter(BaseAdapter): + + def __init__(self): + self.system_prompt = None + self.crewai_tools = [] + + @staticmethod + def name() -> str: + return "crewai" + + @staticmethod + def display_name() -> str: + return "CrewAI" + + @staticmethod + def description() -> str: + return "CrewAI — role-based agent with task delegation and crew orchestration" + + @staticmethod + def get_config_schema() -> dict: + return { + "model": {"type": "string", "description": "LLM model (e.g. openai:gpt-4.1-mini)"}, + "skills": {"type": "array", "items": {"type": "string"}, "description": "Skill folder names to load"}, + "tools": {"type": "array", "items": {"type": "string"}, "description": "Built-in tools"}, + } + + async def setup(self, config: AdapterConfig) -> None: + try: + import crewai # noqa: F401 + logger.info(f"CrewAI version: {crewai.__version__}") + except ImportError: + raise RuntimeError("crewai not installed.") + + result = await self._common_setup(config) + self.system_prompt = result.system_prompt + self.crewai_tools = [_langchain_to_crewai(t) for t in result.langchain_tools] + logger.info(f"CrewAI tools: {[t.name for t in result.langchain_tools]}") + + async def create_executor(self, config: AdapterConfig) -> AgentExecutor: + return CrewAIA2AExecutor( + model=config.model, + system_prompt=self.system_prompt, + crewai_tools=self.crewai_tools, + heartbeat=config.heartbeat, + ) + + +class CrewAIA2AExecutor(AgentExecutor): + """Wraps CrewAI's Agent + Crew.kickoff() with full platform tools.""" + + def __init__(self, model: str, system_prompt: str | None, crewai_tools: list, heartbeat=None): + self.model = model + self.system_prompt = system_prompt + self.crewai_tools = crewai_tools + self._heartbeat = heartbeat + + async def execute(self, context, event_queue): + from a2a.utils import new_agent_text_message + from molecule_runtime.adapters.shared_runtime import extract_history, build_task_text, brief_task, set_current_task + + from molecule_runtime.adapters.shared_runtime import extract_message_text + user_message = extract_message_text(context) + + if not user_message: + await event_queue.enqueue_event(new_agent_text_message("No message provided")) + return + + await set_current_task(self._heartbeat, brief_task(user_message)) + + try: + from crewai import Agent, Task, Crew + + model_str = self.model + if model_str.startswith("openai:"): + model_str = model_str.replace("openai:", "openai/") + + backstory = self.system_prompt or "You are a helpful AI agent." + + history = extract_history(context) + task_desc = build_task_text(user_message, history) + + agent = Agent( + role=backstory.split("\n")[0][:100], + goal="Help the user and coordinate with peer agents when needed", + backstory=backstory, + llm=model_str, + tools=self.crewai_tools, + verbose=False, + ) + + task = Task( + description=task_desc, + expected_output="A helpful response", + agent=agent, + ) + + crew = Crew(agents=[agent], tasks=[task], verbose=False) + result = await asyncio.to_thread(crew.kickoff) + reply = str(result) + + except Exception as e: + reply = f"CrewAI error: {e}" + finally: + await set_current_task(self._heartbeat, "") + + await event_queue.enqueue_event(new_agent_text_message(reply)) + + async def cancel(self, context, event_queue): # pragma: no cover + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..254acbc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# Molecule AI workspace runtime — shared infrastructure +molecule-ai-workspace-runtime>=0.1.0 + +# CrewAI adapter specific deps +crewai>=0.100.0