diff --git a/protocol/a2a/__init__.py b/protocol/a2a/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/protocol/a2a/app/README.md b/protocol/a2a/app/README.md new file mode 100644 index 0000000..c5ee04a --- /dev/null +++ b/protocol/a2a/app/README.md @@ -0,0 +1,195 @@ +# Manus Agent with A2A Protocol + +This is an experimental integration of the A2A protocol (https://google.github.io/A2A/#/documentation) with OpenManus, currently supporting only non-streaming mode. + +## Prerequisites +- conda activate 'Your OpenManus python env' +- pip install a2a-sdk==0.2.5 + + + +## Setup & Running + +1. Run A2A Server: + + ```bash + cd OpenManus + python -m protocol.a2a.app.main + ``` + +2. Clone A2A official repository and run A2A Client,there are two ways to use A2AClient——CLI and Register A2A Agent Server in UI.(details at https://github.com/google/A2A): + + ```bash + git clone https://github.com/google-a2a/a2a-samples.git + cd a2a-samples + echo "GOOGLE_API_KEY=your_api_key_here" > .env + cd samples/python/hosts/cli + uv run . + ``` + +3. Send tasks to OpenManus via A2A Client CLI or Register A2A Agent Server in UI + + +## Examples + +**Get Agent Card** + +Request: + +``` +curl http://localhost:10000/.well-known/agent.json + +``` + + +``` +Response: + +{ + "capabilities": { + "pushNotifications": true, + "streaming": false + }, + "defaultInputModes": [ + "text", + "text/plain" + ], + "defaultOutputModes": [ + "text", + "text/plain" + ], + "description": "A versatile agent that can solve various tasks using multiple tools including MCP-based tools", + "name": "Manus Agent", + "skills": [ + { + "description": "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results.", + "examples": [ + "Execute Python code:'''python \n Print('Hello World') \n '''" + ], + "id": "Python Execute", + "name": "Python Execute Tool", + "tags": [ + "Execute Python Code" + ] + }, + { + "description": "A powerful browser automation tool that allows interaction with web pages through various actions.\n* This tool provides commands for controlling a browser session, navigating web pages, and extracting information\n* It maintains state across calls, keeping the browser session alive until explicitly closed\n* Use this when you need to browse websites, fill forms, click buttons, extract content, or perform web searches\n* Each action requires specific parameters as defined in the tool's dependencies\n\nKey capabilities include:\n* Navigation: Go to specific URLs, go back, search the web, or refresh pages\n* Interaction: Click elements, input text, select from dropdowns, send keyboard commands\n* Scrolling: Scroll up/down by pixel amount or scroll to specific text\n* Content extraction: Extract and analyze content from web pages based on specific goals\n* Tab management: Switch between tabs, open new tabs, or close tabs\n\nNote: When using element indices, refer to the numbered elements shown in the current browser state.\n", + "examples": [ + "go_to 'https://www.google.com'" + ], + "id": "Browser use", + "name": "Browser use Tool", + "tags": [ + "Use Browser" + ] + }, + { + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* The `undo_edit` command will revert the last edit made to the file at `path`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`\n", + "examples": [ + "Replace 'old' with 'new' in 'file.txt'" + ], + "id": "Replace String", + "name": "Str_replace Tool", + "tags": [ + "Operate Files" + ] + }, + { + "description": "Use this tool to ask human for help.", + "examples": [ + "Ask human: 'What time is it?'" + ], + "id": "Ask human", + "name": "Ask human Tool", + "tags": [ + "Ask human for help" + ] + }, + { + "description": "Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.\nWhen you have finished all the tasks, call this tool to end the work.", + "examples": [ + "terminate" + ], + "id": "terminate", + "name": "terminate Tool", + "tags": [ + "terminate task" + ] + } + ], + "url": "http://localhost:10000/", + "version": "1.0.0" +} +``` + +**Send Task** + +Request: + +``` +curl --location 'http://localhost:10000' \ +--header 'Content-Type: application/json' \ +--data '{ + "id":130, + "jsonrpc":"2.0", + "method": "message/send", + "params": { + "message": { + "messageId": "", + "role": "user", + "parts": [{"text":"什么是快乐星球"}] + } + } +}' +``` + +Response: + +``` +{ + "id": 130, + "jsonrpc": "2.0", + "result": { + "artifacts": [ + { + "artifactId": "2f9d0af8-c7da-4f88-9c8c-3033836322b8", + "description": "", + "name": "task_cf64d3c9-1e08-4948-a620-76900aa204cf", + "parts": [ + { + "kind": "text", + "text": "Step 1: “快乐星球”是一个流行的网络用语,源自中国儿童科幻电视剧《快乐星球》。这部剧讲述了一群孩子在一个虚构的“快乐星球”上经历的冒险故事,主题围绕着友谊、成长和科学幻想。后来,“快乐星球”逐渐成为一种网络梗,用来形容一种无忧无虑、充满快乐的理想状态。\n\n如果你对这个词的具体含义、出处或者相关的文化背景有更多兴趣,可以告诉我,我可以为你提供更详细的信息!\nStep 2: Observed output of cmd `terminate` executed:\nThe interaction has been completed with status: success" + } + ] + } + ], + "contextId": "44d16c16-9ccf-49c2-9a99-5c9513969b5f", + "history": [ + { + "contextId": "44d16c16-9ccf-49c2-9a99-5c9513969b5f", + "kind": "message", + "messageId": "", + "parts": [ + { + "kind": "text", + "text": "什么是快乐星球" + } + ], + "role": "user", + "taskId": "cf64d3c9-1e08-4948-a620-76900aa204cf" + } + ], + "id": "cf64d3c9-1e08-4948-a620-76900aa204cf", + "kind": "task", + "status": { + "state": "completed" + } + } +} +``` + + +## Learn More + +- [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation) + diff --git a/protocol/a2a/app/README_zh.md b/protocol/a2a/app/README_zh.md new file mode 100644 index 0000000..afa6fac --- /dev/null +++ b/protocol/a2a/app/README_zh.md @@ -0,0 +1,195 @@ +# Manus Agent with A2A Protocol + +这是一个将A2A协议(https://google.github.io/A2A/#/documentation)与OpenManus结合的一个尝试,当前仅支持非流式 + +## Prerequisites +- conda activate 'Your OpenManus python env' +- pip install a2a-sdk==0.2.5 + + + +## Setup & Running + +1. 运行A2A Server: + + ```bash + cd OpenManus + python -m protocol.a2a.app.main + ``` + +2. 拉取A2A官方库并运行A2A Client,有两种使用A2A客户端的方式——CLI以及在前端页面注册Agent服务。(详情参考https://github.com/google/A2A): + + ```bash + git clone https://github.com/google-a2a/a2a-samples.git + cd a2a-samples + echo "GOOGLE_API_KEY=your_api_key_here" > .env + cd samples/python/hosts/cli + uv run . + ``` + +3. 通过A2A Client的命令行向OpenManus发送任务或者在A2A前端页面上将其注册 + + +## Examples + +**获得Agent Card** + +Request: + +``` +curl http://localhost:10000/.well-known/agent.json + +``` + + +``` +Response: + +{ + "capabilities": { + "pushNotifications": true, + "streaming": false + }, + "defaultInputModes": [ + "text", + "text/plain" + ], + "defaultOutputModes": [ + "text", + "text/plain" + ], + "description": "A versatile agent that can solve various tasks using multiple tools including MCP-based tools", + "name": "Manus Agent", + "skills": [ + { + "description": "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results.", + "examples": [ + "Execute Python code:'''python \n Print('Hello World') \n '''" + ], + "id": "Python Execute", + "name": "Python Execute Tool", + "tags": [ + "Execute Python Code" + ] + }, + { + "description": "A powerful browser automation tool that allows interaction with web pages through various actions.\n* This tool provides commands for controlling a browser session, navigating web pages, and extracting information\n* It maintains state across calls, keeping the browser session alive until explicitly closed\n* Use this when you need to browse websites, fill forms, click buttons, extract content, or perform web searches\n* Each action requires specific parameters as defined in the tool's dependencies\n\nKey capabilities include:\n* Navigation: Go to specific URLs, go back, search the web, or refresh pages\n* Interaction: Click elements, input text, select from dropdowns, send keyboard commands\n* Scrolling: Scroll up/down by pixel amount or scroll to specific text\n* Content extraction: Extract and analyze content from web pages based on specific goals\n* Tab management: Switch between tabs, open new tabs, or close tabs\n\nNote: When using element indices, refer to the numbered elements shown in the current browser state.\n", + "examples": [ + "go_to 'https://www.google.com'" + ], + "id": "Browser use", + "name": "Browser use Tool", + "tags": [ + "Use Browser" + ] + }, + { + "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* The `undo_edit` command will revert the last edit made to the file at `path`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`\n", + "examples": [ + "Replace 'old' with 'new' in 'file.txt'" + ], + "id": "Replace String", + "name": "Str_replace Tool", + "tags": [ + "Operate Files" + ] + }, + { + "description": "Use this tool to ask human for help.", + "examples": [ + "Ask human: 'What time is it?'" + ], + "id": "Ask human", + "name": "Ask human Tool", + "tags": [ + "Ask human for help" + ] + }, + { + "description": "Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.\nWhen you have finished all the tasks, call this tool to end the work.", + "examples": [ + "terminate" + ], + "id": "terminate", + "name": "terminate Tool", + "tags": [ + "terminate task" + ] + } + ], + "url": "http://localhost:10000/", + "version": "1.0.0" +} +``` + +**发送任务** + +Request: + +``` +curl --location 'http://localhost:10000' \ +--header 'Content-Type: application/json' \ +--data '{ + "id":130, + "jsonrpc":"2.0", + "method": "message/send", + "params": { + "message": { + "messageId": "", + "role": "user", + "parts": [{"text":"什么是快乐星球"}] + } + } +}' +``` + +Response: + +``` +{ + "id": 130, + "jsonrpc": "2.0", + "result": { + "artifacts": [ + { + "artifactId": "2f9d0af8-c7da-4f88-9c8c-3033836322b8", + "description": "", + "name": "task_cf64d3c9-1e08-4948-a620-76900aa204cf", + "parts": [ + { + "kind": "text", + "text": "Step 1: “快乐星球”是一个流行的网络用语,源自中国儿童科幻电视剧《快乐星球》。这部剧讲述了一群孩子在一个虚构的“快乐星球”上经历的冒险故事,主题围绕着友谊、成长和科学幻想。后来,“快乐星球”逐渐成为一种网络梗,用来形容一种无忧无虑、充满快乐的理想状态。\n\n如果你对这个词的具体含义、出处或者相关的文化背景有更多兴趣,可以告诉我,我可以为你提供更详细的信息!\nStep 2: Observed output of cmd `terminate` executed:\nThe interaction has been completed with status: success" + } + ] + } + ], + "contextId": "44d16c16-9ccf-49c2-9a99-5c9513969b5f", + "history": [ + { + "contextId": "44d16c16-9ccf-49c2-9a99-5c9513969b5f", + "kind": "message", + "messageId": "", + "parts": [ + { + "kind": "text", + "text": "什么是快乐星球" + } + ], + "role": "user", + "taskId": "cf64d3c9-1e08-4948-a620-76900aa204cf" + } + ], + "id": "cf64d3c9-1e08-4948-a620-76900aa204cf", + "kind": "task", + "status": { + "state": "completed" + } + } +} +``` + + +## Learn More + +- [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation) + diff --git a/protocol/a2a/app/__init__.py b/protocol/a2a/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/protocol/a2a/app/agent.py b/protocol/a2a/app/agent.py new file mode 100644 index 0000000..9559ceb --- /dev/null +++ b/protocol/a2a/app/agent.py @@ -0,0 +1,32 @@ +import httpx +from typing import Any, Dict, AsyncIterable, Literal, List, ClassVar +from pydantic import BaseModel +from app.agent.manus import Manus + + +class ResponseFormat(BaseModel): + """Respond to the user in this format.""" + + status: Literal["input_required", "completed", "error"] = "input_required" + message: str + + +class A2AManus(Manus): + + async def invoke(self, query, sessionId) -> str: + config = {"configurable": {"thread_id": sessionId}} + response = await self.run(query) + return self.get_agent_response(config, response) + + async def stream(self, query: str) -> AsyncIterable[Dict[str, Any]]: + """Streaming is not supported by Manus.""" + raise NotImplementedError("Streaming is not supported by Manus yet.") + + def get_agent_response(self, config, agent_response): + return { + "is_task_complete": True, + "require_user_input": False, + "content": agent_response, + } + + SUPPORTED_CONTENT_TYPES: ClassVar[List[str]] = ["text", "text/plain"] diff --git a/protocol/a2a/app/agent_executor.py b/protocol/a2a/app/agent_executor.py new file mode 100644 index 0000000..bce0b1d --- /dev/null +++ b/protocol/a2a/app/agent_executor.py @@ -0,0 +1,74 @@ +import logging + +from a2a.server.agent_execution import AgentExecutor, RequestContext +from a2a.server.events import Event, EventQueue +from a2a.server.tasks import TaskUpdater +from a2a.types import ( + InvalidParamsError, + Part, + Task, + TextPart, + UnsupportedOperationError, +) +from a2a.utils import ( + completed_task, + new_artifact, +) +from .agent import A2AManus +from a2a.utils.errors import ServerError +from typing import Callable, Awaitable + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class ManusExecutor(AgentExecutor): + """Currency Conversion AgentExecutor Example.""" + + def __init__(self, agent_factory: Callable[[], Awaitable[A2AManus]]): + self.agent_factory = agent_factory + + async def execute( + self, + context: RequestContext, + event_queue: EventQueue, + ) -> None: + error = self._validate_request(context) + if error: + raise ServerError(error=InvalidParamsError()) + + query = context.get_user_input() + try: + self.agent = await self.agent_factory() + result = await self.agent.invoke(query, context.context_id) + print(f"Final Result ===> {result}") + except Exception as e: + print("Error invoking agent: %s", e) + raise ServerError(error=ValueError(f"Error invoking agent: {e}")) from e + parts = [ + Part( + root=TextPart( + text=( + result["content"] + if result["content"] + else "failed to generate response" + ) + ), + ) + ] + event_queue.enqueue_event( + completed_task( + context.task_id, + context.context_id, + [new_artifact(parts, f"task_{context.task_id}")], + [context.message], + ) + ) + + def _validate_request(self, context: RequestContext) -> bool: + return False + + async def cancel( + self, request: RequestContext, event_queue: EventQueue + ) -> Task | None: + raise ServerError(error=UnsupportedOperationError()) diff --git a/protocol/a2a/app/main.py b/protocol/a2a/app/main.py new file mode 100644 index 0000000..dd09f23 --- /dev/null +++ b/protocol/a2a/app/main.py @@ -0,0 +1,134 @@ +import httpx +import argparse + +from a2a.server.apps import A2AStarletteApplication +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.tasks import InMemoryTaskStore, InMemoryPushNotifier +from a2a.types import ( + AgentCapabilities, + AgentCard, + AgentSkill, +) + +from .agent_executor import ManusExecutor + +from .agent import A2AManus +from app.tool.browser_use_tool import _BROWSER_DESCRIPTION +from app.tool.str_replace_editor import _STR_REPLACE_EDITOR_DESCRIPTION +from app.tool.terminate import _TERMINATE_DESCRIPTION +import logging +from dotenv import load_dotenv +import asyncio +from typing import Optional + +load_dotenv() + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(host: str = "localhost", port: int = 10000): + """Starts the Manus Agent server.""" + try: + capabilities = AgentCapabilities(streaming=False, pushNotifications=True) + skills = [ + AgentSkill( + id="Python Execute", + name="Python Execute Tool", + description="Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results.", + tags=["Execute Python Code"], + examples=[ + "Execute Python code:'''python \n Print('Hello World') \n '''" + ], + ), + AgentSkill( + id="Browser use", + name="Browser use Tool", + description=_BROWSER_DESCRIPTION, + tags=["Use Browser"], + examples=["go_to 'https://www.google.com'"], + ), + AgentSkill( + id="Replace String", + name="Str_replace Tool", + description=_STR_REPLACE_EDITOR_DESCRIPTION, + tags=["Operate Files"], + examples=["Replace 'old' with 'new' in 'file.txt'"], + ), + AgentSkill( + id="Ask human", + name="Ask human Tool", + description="Use this tool to ask human for help.", + tags=["Ask human for help"], + examples=["Ask human: 'What time is it?'"], + ), + AgentSkill( + id="terminate", + name="terminate Tool", + description=_TERMINATE_DESCRIPTION, + tags=["terminate task"], + examples=["terminate"], + ), + # Add more skills as needed + ] + + agent_card = AgentCard( + name="Manus Agent", + description="A versatile agent that can solve various tasks using multiple tools including MCP-based tools", + url=f"http://{host}:{port}/", + version="1.0.0", + defaultInputModes=A2AManus.SUPPORTED_CONTENT_TYPES, + defaultOutputModes=A2AManus.SUPPORTED_CONTENT_TYPES, + capabilities=capabilities, + skills=skills, + ) + + httpx_client = httpx.AsyncClient() + request_handler = DefaultRequestHandler( + agent_executor=ManusExecutor( + agent_factory=lambda: A2AManus.create(max_steps=3) + ), + task_store=InMemoryTaskStore(), + push_notifier=InMemoryPushNotifier(httpx_client), + ) + + server = A2AStarletteApplication( + agent_card=agent_card, http_handler=request_handler + ) + + logger.info(f"Starting server on {host}:{port}") + return server.build() + except Exception as e: + logger.error(f"An error occurred during server startup: {e}") + exit(1) + + +def run_server(host: Optional[str] = "localhost", port: Optional[int] = 10000): + try: + import uvicorn + + app = asyncio.run(main(host, port)) + config = uvicorn.Config( + app=app, host=host, port=port, loop="asyncio", proxy_headers=True + ) + uvicorn.Server(config=config).run() + logger.info(f"Server started on {host}:{port}") + except Exception as e: + logger.error(f"An error occurred while starting the server: {e}") + + +if __name__ == "__main__": + # Parse command line arguments for host and port, with default values + parser = argparse.ArgumentParser(description="Start Manus Agent service") + parser.add_argument( + "--host", + type=str, + default="localhost", + help="Server host address, default is localhost", + ) + parser.add_argument( + "--port", type=int, default=10000, help="Server port, default is 10000" + ) + args = parser.parse_args() + # Start the server with the specified or default host and port + run_server(args.host, args.port)