├── img ├── .gitkeep ├── a2a_mcp.png └── a2a_mcp_readme.png ├── agent_api_template └── python_a2a_template │ ├── __init__.py │ ├── .python-version │ ├── utils │ ├── logging_util.py │ ├── setting_util.py │ ├── util.py │ ├── jwt_util.py │ └── middleware.py │ ├── requirements.txt │ ├── static │ └── agent.json │ ├── router │ ├── __init__.py │ ├── agent_card_router.py │ └── task_router.py │ ├── app.py │ ├── README-zh.md │ ├── service │ ├── custom_task_manager.py │ ├── server.py │ └── task_manager.py │ ├── README-en.md │ └── models │ └── types.py ├── LICENSE ├── CONTRIBUTING.md ├── .gitignore └── README.md /img/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 -------------------------------------------------------------------------------- /img/a2a_mcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetMindAI-Open/Awesome-Agent2Agent/HEAD/img/a2a_mcp.png -------------------------------------------------------------------------------- /img/a2a_mcp_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetMindAI-Open/Awesome-Agent2Agent/HEAD/img/a2a_mcp_readme.png -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/utils/logging_util.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.115.12 2 | pydantic==2.11.3 3 | python-dotenv==1.1.0 4 | python-jose==3.4.0 5 | sse-starlette==2.2.1 6 | starlette==0.46.2 7 | uvicorn==0.34.1 -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/static/agent.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xxx", 3 | "description": "xxx", 4 | "url": "http://host:port", 5 | "version": "0.0.1", 6 | "capabilities": { 7 | "streaming": false 8 | }, 9 | "skills": [ 10 | { 11 | "id": "xxxx", 12 | "name": "xxx" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/router/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import fastapi 3 | 4 | from router.agent_card_router import agent_card_router 5 | from router.task_router import task_router 6 | 7 | 8 | api_endpoint_router = fastapi.APIRouter() 9 | 10 | api_endpoint_router.include_router(router=agent_card_router) 11 | api_endpoint_router.include_router(router=task_router) 12 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/app.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | import click 4 | 5 | from utils.setting_util import setting 6 | from service.custom_task_manager import CustomTaskManager 7 | from service.server import A2AServer 8 | 9 | 10 | @click.command() 11 | @click.option("--host", default="localhost") 12 | @click.option("--port", default=10001) 13 | def main(host, port): 14 | 15 | server = A2AServer( 16 | host=host, 17 | port=port, 18 | ) 19 | server.start() 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | 25 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/README-zh.md: -------------------------------------------------------------------------------- 1 | ### 项目来源说明 2 | 3 | 本项目基于 [google-A2A](https://github.com/google/A2A) 开发。 4 | 5 | 许可证:本项目采用 [Apache 许可证 2.0](https://opensource.org/licenses/Apache-2.0) - 详细内容请查看 [LICENSE](https://github.com/google/A2A/blob/main/LICENSE) 文件。 6 | 7 | ### 开发说明 8 | 1. 必须根据 models/types.py:AgentCard 完善 static/agent.json 9 | 2. 必须在 service/custom_task_manager.py 中完善你的 agent 功能 10 | 3. 如果你的 api 需要 auth 验证,可以在 utils/jwt_util.py:verify_jwt 中完善逻辑 11 | 4. 另外你还可以在 router 中自定义 api 12 | 13 | ### 运行说明 14 | 1. pip3 install -r requirements.txt 15 | 2. python3 app.py 16 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/service/custom_task_manager.py: -------------------------------------------------------------------------------- 1 | 2 | from models.types import ( 3 | SendTaskRequest, 4 | SendTaskStreamingRequest, 5 | ) 6 | from service.task_manager import InMemoryTaskManager 7 | 8 | 9 | class CustomTaskManager(InMemoryTaskManager): 10 | """ TODO 11 | 自定义任务管理器 12 | 13 | custom task manager 14 | """ 15 | 16 | async def on_send_task(self, request: SendTaskRequest): 17 | pass 18 | 19 | async def on_send_task_subscribe( 20 | self, request: SendTaskStreamingRequest 21 | ): 22 | pass 23 | 24 | 25 | custom_task_manager = CustomTaskManager() 26 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/router/agent_card_router.py: -------------------------------------------------------------------------------- 1 | 2 | import fastapi 3 | from fastapi.requests import Request 4 | from fastapi.responses import JSONResponse 5 | from typing import Annotated, Any 6 | 7 | from utils.setting_util import setting 8 | from utils.jwt_util import verify_jwt 9 | 10 | agent_card_router = fastapi.APIRouter(prefix="/.well-known") 11 | 12 | 13 | @agent_card_router.get( 14 | path="/agent.json" 15 | ) 16 | async def get_agent_card( 17 | request: Request, 18 | _: Annotated[Any, fastapi.Depends(verify_jwt)] 19 | ) -> JSONResponse: 20 | return JSONResponse(setting.AgentCARD.model_dump(exclude_none=True)) 21 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/utils/setting_util.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | from dotenv import load_dotenv 5 | 6 | from models.types import AgentCard 7 | 8 | load_dotenv() 9 | BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 10 | 11 | 12 | def load_agent_json(): 13 | agent_json_path = os.path.join(BASE_PATH, "static/agent.json") 14 | with open(agent_json_path, "r", encoding="utf-8") as f: 15 | agent_json = json.load(f) 16 | return agent_json 17 | 18 | 19 | class Setting: 20 | 21 | APISecretKey: str = os.getenv("APISecretKey", "your_key") 22 | 23 | AgentCARD: AgentCard = AgentCard(**load_agent_json()) 24 | 25 | 26 | setting = Setting() 27 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/README-en.md: -------------------------------------------------------------------------------- 1 | ## Project Source 2 | 3 | This project is based on [google-A2A](https://github.com/google/A2A). 4 | 5 | License: This project is licensed under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0) - see the [LICENSE](https://github.com/google/A2A/blob/main/LICENSE) file for details. 6 | 7 | ### Development instructions 8 | 1. You must complete static/agent.json according to models/types.py:AgentCard 9 | 2. You must complete your agent function in service/custom_task_manager.py 10 | 3. If your api requires auth verification, you can complete the logic in utils/jwt_util.py:verify_jwt 11 | 4. You can also customize the api in the router 12 | 13 | ### Running instructions 14 | 1. pip3 install -r requirements.txt 15 | 2. python3 app.py 16 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/utils/util.py: -------------------------------------------------------------------------------- 1 | 2 | from models.types import ( 3 | JSONRPCResponse, 4 | ContentTypeNotSupportedError, 5 | UnsupportedOperationError, 6 | ) 7 | from typing import List 8 | 9 | 10 | def are_modalities_compatible( 11 | server_output_modes: List[str], client_output_modes: List[str] 12 | ): 13 | """Modalities are compatible if they are both non-empty 14 | and there is at least one common element.""" 15 | if client_output_modes is None or len(client_output_modes) == 0: 16 | return True 17 | 18 | if server_output_modes is None or len(server_output_modes) == 0: 19 | return True 20 | 21 | return any(x in server_output_modes for x in client_output_modes) 22 | 23 | 24 | def new_incompatible_types_error(request_id): 25 | return JSONRPCResponse(id=request_id, error=ContentTypeNotSupportedError()) 26 | 27 | 28 | def new_not_implemented_error(request_id): 29 | return JSONRPCResponse(id=request_id, error=UnsupportedOperationError()) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 NetMind.AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/service/server.py: -------------------------------------------------------------------------------- 1 | 2 | import fastapi 3 | from fastapi.responses import ORJSONResponse 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from utils.logging_util import logger 7 | from utils.setting_util import setting 8 | from utils.middleware import CustomMiddleware 9 | from router import api_endpoint_router 10 | from service.task_manager import TaskManager 11 | 12 | 13 | class A2AServer: 14 | def __init__( 15 | self, 16 | host="0.0.0.0", 17 | port=5000, 18 | endpoint="/", 19 | ): 20 | self.host = host 21 | self.port = port 22 | self.endpoint = endpoint 23 | self.app = fastapi.FastAPI(default_response_class=ORJSONResponse) 24 | self.app.add_middleware(CustomMiddleware) 25 | self.app.add_middleware( 26 | CORSMiddleware, 27 | allow_origins=["*"], 28 | allow_credentials=True, 29 | allow_methods=["*"], 30 | allow_headers=["*"], 31 | ) 32 | 33 | self.app.include_router(router=api_endpoint_router) 34 | 35 | def start(self): 36 | import uvicorn 37 | 38 | uvicorn.run(self.app, host=self.host, port=self.port) 39 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/utils/jwt_util.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | from jose import jwt as jose_jwt 4 | from jose.exceptions import ExpiredSignatureError, JWTError 5 | from typing import Annotated, Any 6 | from collections.abc import AsyncGenerator 7 | from fastapi import Depends, Header, HTTPException 8 | 9 | import sys 10 | sys.path.append('/root/liss_package/Awesome-Agent2Agent/agent_api_template/python_a2a_template') 11 | from utils.setting_util import setting 12 | 13 | 14 | def create_jwt_token() -> str: 15 | data = { 16 | "current_time": int(time.time()), 17 | } 18 | token = jose_jwt.encode( 19 | data, 20 | setting.APISecretKey, 21 | algorithm="HS256", 22 | ) 23 | return token 24 | 25 | 26 | def decode_jwt_token(token: str) -> dict: 27 | data = jose_jwt.decode( 28 | token, 29 | setting.APISecretKey, 30 | algorithms=["HS256"], 31 | ) 32 | return data 33 | 34 | 35 | async def extract_token(token: str | None = Header(default=None)) -> AsyncGenerator[str, Any]: 36 | if not setting.AgentCARD.authentication: 37 | yield None 38 | return 39 | 40 | if not token: 41 | raise HTTPException(status_code=401, detail="Unauthorized, token is required") 42 | 43 | if not token.startswith("Bearer "): 44 | raise HTTPException(status_code=403, detail="Login status is abnormal. Please log in again before proceeding.") 45 | 46 | token = token.split(" ")[1] 47 | yield token 48 | 49 | 50 | async def verify_jwt( 51 | token: Annotated[str, Depends(extract_token)], 52 | ) -> AsyncGenerator[Any]: 53 | if not setting.AgentCARD.authentication: 54 | yield None 55 | return 56 | try: 57 | data = decode_jwt_token(token) 58 | yield data 59 | except JWTError as e: 60 | raise HTTPException(status_code=401, detail="Invalid token") from e 61 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/utils/middleware.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | from typing import AsyncIterable, Any 4 | from pydantic import ValidationError 5 | from starlette.requests import Request 6 | from starlette.responses import Response, JSONResponse 7 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint, _StreamingResponse 8 | from sse_starlette.sse import EventSourceResponse 9 | 10 | from models.types import ( 11 | JSONRPCResponse, 12 | InvalidRequestError, 13 | JSONParseError, 14 | InternalError, 15 | ) 16 | from utils.logging_util import logger 17 | from utils.setting_util import setting 18 | 19 | 20 | def _handle_exception(e: Exception) -> JSONResponse: 21 | if isinstance(e, json.decoder.JSONDecodeError): 22 | json_rpc_error = JSONParseError() 23 | elif isinstance(e, ValidationError): 24 | json_rpc_error = InvalidRequestError(data=json.loads(e.json())) 25 | else: 26 | logger.error(f"Unhandled exception: {e}") 27 | json_rpc_error = InternalError() 28 | 29 | response = JSONRPCResponse(id=None, error=json_rpc_error) 30 | return JSONResponse(response.model_dump(exclude_none=True), status_code=400) 31 | 32 | 33 | def _create_response(result: Any) -> JSONResponse | EventSourceResponse: 34 | if isinstance(result, AsyncIterable): 35 | 36 | async def event_generator(result) -> AsyncIterable[dict[str, str]]: 37 | async for item in result: 38 | yield {"data": item.model_dump_json(exclude_none=True)} 39 | 40 | return EventSourceResponse(event_generator(result)) 41 | elif isinstance(result, JSONRPCResponse): 42 | return JSONResponse(result.model_dump(exclude_none=True)) 43 | elif isinstance(result, _StreamingResponse): 44 | return result 45 | else: 46 | logger.error(f"Unexpected result type: {type(result)}") 47 | raise ValueError(f"Unexpected result type: {type(result)}") 48 | 49 | 50 | class CustomMiddleware(BaseHTTPMiddleware): 51 | 52 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: 53 | # Pre-processing logic 54 | 55 | try: 56 | result = await call_next(request) 57 | return _create_response(result) 58 | except Exception as e: 59 | return _handle_exception(e) 60 | # Post-processing logic 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for your interest in contributing to Awesome-Agent2Agent! This document provides guidelines and instructions for adding to this curated list. 4 | 5 | ## Adding to the List 6 | 7 | Please ensure your pull request adheres to the following guidelines: 8 | 9 | - Make sure the project/resource is related to the Agent2Agent (A2A) protocol 10 | - Add a single item per pull request 11 | - Check your spelling and grammar 12 | - Make sure your text editor is set to remove trailing whitespace 13 | - The pull request and commit should have a useful title 14 | 15 | ## Project Requirements 16 | 17 | For projects to be included in the list, they should: 18 | 19 | 1. Use or implement the Agent2Agent protocol 20 | 2. Have clear documentation or examples showing how A2A is implemented 21 | 3. Be active or maintained (no abandoned projects) 22 | 23 | ## Format 24 | 25 | When adding a new entry, please use the following format: 26 | 27 | ``` 28 | - [Project Name](URL) - Brief description of the project. ![Language Badge](language-badge-url) ![Feature Badge](feature-badge-url) 29 | ``` 30 | 31 | For badges, you can use shields.io to create appropriate badges for: 32 | - Programming language 33 | - A2A features (client/server) 34 | - Open/closed source status 35 | - Current version 36 | 37 | ## Templates 38 | 39 | ### Open Source Project 40 | ``` 41 | - [Project Name](https://github.com/username/project) - Brief description of the project. ![Language](https://img.shields.io/badge/-Language-color?style=flat-square&logo=language&logoColor=white) ![A2A Feature](https://img.shields.io/badge/-Feature-4285F4?style=flat-square) 42 | ``` 43 | 44 | ### Closed Source Product 45 | ``` 46 | - [Product Name](https://product-website.com) - Brief description of how this product implements A2A. 47 | ``` 48 | 49 | ### Resource 50 | ``` 51 | - [Resource Name](URL) - Brief description of the resource. 52 | ``` 53 | 54 | ## Pull Request Process 55 | 56 | 1. Fork the repository 57 | 2. Add your entry in the appropriate section, maintaining alphabetical order within each section 58 | 3. Commit changes to your fork 59 | 4. Submit a pull request with a clear title and description 60 | 5. Address any feedback or changes requested by maintainers 61 | 62 | ## Updating Your Pull Request 63 | 64 | Sometimes a maintainer will ask you to edit your pull request before it is included. This is normally due to spelling errors or because your PR didn't match the list guidelines. Here's what you should do: 65 | 66 | 1. Make the changes directly to your fork of the repository 67 | 2. Push the changes to your fork 68 | 3. The pull request will automatically update 69 | 70 | Thank you for your contributions! -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/router/task_router.py: -------------------------------------------------------------------------------- 1 | 2 | import fastapi 3 | from fastapi.requests import Request 4 | from typing import Annotated, Any 5 | 6 | from models.types import ( 7 | A2ARequest, 8 | GetTaskRequest, 9 | SendTaskRequest, 10 | SendTaskStreamingRequest, 11 | CancelTaskRequest, 12 | SetTaskPushNotificationRequest, 13 | GetTaskPushNotificationRequest, 14 | TaskResubscriptionRequest, 15 | ) 16 | from utils.logging_util import logger 17 | from utils.jwt_util import verify_jwt 18 | from service.custom_task_manager import custom_task_manager 19 | 20 | 21 | task_router = fastapi.APIRouter(prefix="/tasks") 22 | 23 | 24 | async def _process_request(request: Request): 25 | body = await request.json() 26 | json_rpc_request = A2ARequest.validate_python(body) 27 | 28 | if isinstance(json_rpc_request, GetTaskRequest): 29 | result = await custom_task_manager.on_get_task(json_rpc_request) 30 | elif isinstance(json_rpc_request, SendTaskRequest): 31 | result = await custom_task_manager.on_send_task(json_rpc_request) 32 | elif isinstance(json_rpc_request, SendTaskStreamingRequest): 33 | result = await custom_task_manager.on_send_task_subscribe( 34 | json_rpc_request 35 | ) 36 | elif isinstance(json_rpc_request, CancelTaskRequest): 37 | result = await custom_task_manager.on_cancel_task(json_rpc_request) 38 | elif isinstance(json_rpc_request, SetTaskPushNotificationRequest): 39 | result = await custom_task_manager.on_set_task_push_notification(json_rpc_request) 40 | elif isinstance(json_rpc_request, GetTaskPushNotificationRequest): 41 | result = await custom_task_manager.on_get_task_push_notification(json_rpc_request) 42 | elif isinstance(json_rpc_request, TaskResubscriptionRequest): 43 | result = await custom_task_manager.on_resubscribe_to_task( 44 | json_rpc_request 45 | ) 46 | else: 47 | logger.warning(f"Unexpected request type: {type(json_rpc_request)}") 48 | raise ValueError(f"Unexpected request type: {type(request)}") 49 | 50 | return result 51 | 52 | 53 | @task_router.post( 54 | path="/get" 55 | ) 56 | async def get_task( 57 | request: Request, 58 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 59 | ) -> fastapi.Response: 60 | result = await _process_request(request) 61 | return result 62 | 63 | 64 | @task_router.post( 65 | path="/send" 66 | ) 67 | async def send_task( 68 | request: Request, 69 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 70 | ) -> fastapi.Response: 71 | result = await _process_request(request) 72 | return result 73 | 74 | 75 | @task_router.post( 76 | path="/sendSubscribe" 77 | ) 78 | async def send_task_subscribe( 79 | request: Request, 80 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 81 | ) -> fastapi.Response: 82 | result = await _process_request(request) 83 | return result 84 | 85 | 86 | @task_router.post( 87 | path="/cancel" 88 | ) 89 | async def cancel_task( 90 | request: Request, 91 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 92 | ) -> fastapi.Response: 93 | result = await _process_request(request) 94 | return result 95 | 96 | 97 | @task_router.post( 98 | path="/pushNotification/set" 99 | ) 100 | async def set_task_push_notification( 101 | request: Request, 102 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 103 | ) -> fastapi.Response: 104 | result = await _process_request(request) 105 | return result 106 | 107 | 108 | @task_router.post( 109 | path="/pushNotification/get" 110 | ) 111 | async def get_task_push_notification( 112 | request: Request, 113 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 114 | ) -> fastapi.Response: 115 | result = await _process_request(request) 116 | return result 117 | 118 | 119 | @task_router.post( 120 | path="/resubscribe" 121 | ) 122 | async def resubscribe_to_task( 123 | request: Request, 124 | _: Annotated[Any, fastapi.Depends(verify_jwt)], 125 | ) -> fastapi.Response: 126 | result = await _process_request(request) 127 | return result 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | resource/ 3 | 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # UV 102 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | #uv.lock 106 | 107 | # poetry 108 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 109 | # This is especially recommended for binary packages to ensure reproducibility, and is more 110 | # commonly ignored for libraries. 111 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 112 | #poetry.lock 113 | 114 | # pdm 115 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 116 | #pdm.lock 117 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 118 | # in version control. 119 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 120 | .pdm.toml 121 | .pdm-python 122 | .pdm-build/ 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | # Ruff stuff: 175 | .ruff_cache/ 176 | 177 | # PyPI configuration file 178 | .pypirc 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awesome-Agent2Agent [![Awesome](https://awesome.re/badge.svg)](https://awesome.re) 2 | 3 |
4 |
5 |

The curated list of Agent2Agent (A2A) protocol projects, templates, tools, and resources — enabling a future of interoperable AI agents.

6 |
7 | 8 | --- 9 | 10 | ## What is Agent2Agent (A2A)? 11 | 12 | Agent2Agent (A2A) is an open communication protocol developed by Google to enable secure and standardized interaction between AI agents built by different teams, across different platforms. 13 | 14 | It defines a common language and interaction model for agents to: 15 | 16 | - Discover each other 17 | - Negotiate capabilities 18 | - Collaborate on tasks 19 | - Communicate securely 20 | 21 | ![A2A Protocol Overview](./img/a2a_mcp_readme.png) 22 | 23 | > Learn more in the [Official A2A Documentation](https://github.com/google/A2A) 24 | 25 | --- 26 | 27 | ## Why Awesome-Agent2Agent? 28 | 29 | This repo aims to become the central hub for the Agent2Agent ecosystem. 30 | 31 | We collect, organize, and showcase: 32 | 33 | - Open-source A2A-compatible agent projects 34 | - Ready-to-use Agent2Agent API Templates 35 | - MCP-based A2A Server demos 36 | - Web-based Agents Hub interface 37 | 38 | By following the A2A standard, we believe the future of AI will be collaborative, modular, and decentralized. 39 | 40 | --- 41 | 42 | ## Contents 43 | 44 | - [Official Resources](#official-resources) 45 | - [Templates & Demos](#templates--demos) 46 | - [Agents Hub](#agents-hub) 47 | - [Open Source Agents](#open-source-agents) 48 | - [Commercial Agents](#commercial-agents) 49 | - [Tutorials & Guides](#tutorials--guides) 50 | - [Research Papers](#research-papers) 51 | - [Roadmap](#roadmap) 52 | - [Contribute](#contribute) 53 | 54 | --- 55 | 56 | ## Official Resources 57 | 58 | - [A2A Protocol GitHub Repository](https://github.com/google/A2A) 59 | - [A2A Technical Documentation](https://github.com/google/A2A/blob/main/README.md) 60 | - [A2A JSON Specification](https://github.com/google/A2A/blob/main/specification/json/a2a.json) 61 | 62 | --- 63 | 64 | ## Templates & Demos 65 | 66 | | Name | Description | Language | Link | 67 | |------|-------------|----------|------| 68 | | FastAPI A2A Template | Starter project for building A2A-compatible agents using FastAPI. | Python | [View Template](https://github.com/NetMindAI-Open/Awesome-Agent2Agent/tree/main/agent_api_template/python/fastapi_a2a_template) | 69 | 70 | --- 71 | 72 | ## Agents Hub (Coming Soon) 73 | 74 | A web-based tool for discovering, testing, and interacting with A2A-compliant agents. 75 | 76 | Key Features: 77 | 78 | - Agent discovery & management 79 | - Session handling 80 | - Interactive testing 81 | - Pre-configured agent gallery 82 | - Conversation history 83 | 84 | --- 85 | 86 | ## Open Source Agents 87 | 88 | Curated list of public A2A-compatible agents. 89 | 90 | 91 | --- 92 | 93 | ## Commercial Agents 94 | 95 | List of commercial / closed-source agents implementing the A2A protocol. 96 | 97 | --- 98 | 99 | 100 | ## Tutorials & Guides 101 | 102 | Coming soon. 103 | 104 | --- 105 | 106 | ## Research Papers 107 | 108 | Coming soon. 109 | 110 | --- 111 | 112 | ## Roadmap 113 | 114 | We are actively building the following projects to accelerate Agent2Agent (A2A) protocol adoption: 115 | 116 | --- 117 | 118 | ### A2A API Templates 119 | 120 | Reference implementation templates to help developers quickly build Agent2Agent (A2A) protocol-compliant services. 121 | 122 | Features: 123 | 124 | - Fully aligned with the official A2A protocol specification 125 | - Built-in handling of HTTP request/response formats 126 | - Session management for conversation context 127 | - Structured response and error handling 128 | - Ready-to-use FastAPI implementation with OpenAPI documentation 129 | - Extension points for custom agent logic 130 | 131 | --- 132 | 133 | ### MCP-based A2A Server Implementation 134 | 135 | We are exploring the integration of Google's Multimodal Conversational Platform (MCP) with the A2A protocol. 136 | 137 | This server template wraps A2A-compliant agents as MCP servers to maximize interoperability while maintaining protocol compliance. 138 | 139 | Key features: 140 | 141 | - Full implementation of A2A HTTP specifications 142 | - Session management for multi-turn conversations 143 | - Structured task handling and artifact generation 144 | - FastAPI-based implementation with OpenAPI documentation 145 | - Customizable agent capability extension points 146 | 147 | --- 148 | 149 | ### Agents Hub Web Interface 150 | 151 | A web-based tool to help developers discover, test, and interact with A2A-compliant agents. 152 | 153 | Key features: 154 | 155 | - Agent discovery and management (connect to any A2A-compatible agent via Base URL) 156 | - Interactive API testing with automatic session handling 157 | - Pre-configured gallery of ready-to-use agents 158 | - Conversation history tracking 159 | - Capability exploration within the A2A protocol specification 160 | 161 | --- 162 | 163 | ### Curated Gallery of A2A-Compatible Agents 164 | 165 | A growing collection of agents that implement the A2A protocol — including both our own projects and contributions from the open-source community. 166 | 167 | The gallery will cover agents from multiple domains: 168 | 169 | - Productivity & Work 170 | - Developer Tools 171 | - Healthcare 172 | - Finance 173 | - IoT & Smart Home 174 | - Research 175 | - And more 176 | 177 | 178 | --- 179 | 180 | ## Contribute 181 | 182 | We welcome contributions from the community! 183 | 184 | ### Ways to Contribute: 185 | 186 | - Submit your A2A-compatible agent project 187 | - Add tutorials, tools, libraries 188 | - Share research papers or use cases 189 | - Improve templates or demos 190 | 191 | > Check the [Contribution Guidelines](CONTRIBUTING.md) to get started. 192 | 193 | --- 194 | 195 | ## Stargazers over time 196 | 197 | [![Stargazers over time](https://starchart.cc/NetMindAI-Open/Awesome-Agent2Agent.svg)](https://starchart.cc/NetMindAI-Open/Awesome-Agent2Agent) 198 | 199 | --- 200 | 201 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/models/types.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Union, Any 3 | from pydantic import BaseModel, Field, TypeAdapter 4 | from typing import Literal, List, Annotated, Optional 5 | from datetime import datetime 6 | from pydantic import model_validator, ConfigDict, field_serializer 7 | from uuid import uuid4 8 | from enum import Enum 9 | from typing_extensions import Self 10 | 11 | 12 | class TaskState(str, Enum): 13 | SUBMITTED = "submitted" 14 | WORKING = "working" 15 | INPUT_REQUIRED = "input-required" 16 | COMPLETED = "completed" 17 | CANCELED = "canceled" 18 | FAILED = "failed" 19 | UNKNOWN = "unknown" 20 | 21 | 22 | class TextPart(BaseModel): 23 | type: Literal["text"] = "text" 24 | text: str 25 | metadata: dict[str, Any] | None = None 26 | 27 | 28 | class FileContent(BaseModel): 29 | name: str | None = None 30 | mimeType: str | None = None 31 | bytes: str | None = None 32 | uri: str | None = None 33 | 34 | @model_validator(mode="after") 35 | def check_content(self) -> Self: 36 | if not (self.bytes or self.uri): 37 | raise ValueError("Either 'bytes' or 'uri' must be present in the file data") 38 | if self.bytes and self.uri: 39 | raise ValueError( 40 | "Only one of 'bytes' or 'uri' can be present in the file data" 41 | ) 42 | return self 43 | 44 | 45 | class FilePart(BaseModel): 46 | type: Literal["file"] = "file" 47 | file: FileContent 48 | metadata: dict[str, Any] | None = None 49 | 50 | 51 | class DataPart(BaseModel): 52 | type: Literal["data"] = "data" 53 | data: dict[str, Any] 54 | metadata: dict[str, Any] | None = None 55 | 56 | 57 | Part = Annotated[Union[TextPart, FilePart, DataPart], Field(discriminator="type")] 58 | 59 | 60 | class Message(BaseModel): 61 | role: Literal["user", "agent"] 62 | parts: List[Part] 63 | metadata: dict[str, Any] | None = None 64 | 65 | 66 | class TaskStatus(BaseModel): 67 | state: TaskState 68 | message: Message | None = None 69 | timestamp: datetime = Field(default_factory=datetime.now) 70 | 71 | @field_serializer("timestamp") 72 | def serialize_dt(self, dt: datetime, _info): 73 | return dt.isoformat() 74 | 75 | 76 | class Artifact(BaseModel): 77 | name: str | None = None 78 | description: str | None = None 79 | parts: List[Part] 80 | metadata: dict[str, Any] | None = None 81 | index: int = 0 82 | append: bool | None = None 83 | lastChunk: bool | None = None 84 | 85 | 86 | class Task(BaseModel): 87 | id: str 88 | sessionId: str | None = None 89 | status: TaskStatus 90 | artifacts: List[Artifact] | None = None 91 | history: List[Message] | None = None 92 | metadata: dict[str, Any] | None = None 93 | 94 | 95 | class TaskStatusUpdateEvent(BaseModel): 96 | id: str 97 | status: TaskStatus 98 | final: bool = False 99 | metadata: dict[str, Any] | None = None 100 | 101 | 102 | class TaskArtifactUpdateEvent(BaseModel): 103 | id: str 104 | artifact: Artifact 105 | metadata: dict[str, Any] | None = None 106 | 107 | 108 | class AuthenticationInfo(BaseModel): 109 | model_config = ConfigDict(extra="allow") 110 | 111 | schemes: List[str] 112 | credentials: str | None = None 113 | 114 | 115 | class PushNotificationConfig(BaseModel): 116 | url: str 117 | token: str | None = None 118 | authentication: AuthenticationInfo | None = None 119 | 120 | 121 | class TaskIdParams(BaseModel): 122 | id: str 123 | metadata: dict[str, Any] | None = None 124 | 125 | 126 | class TaskQueryParams(TaskIdParams): 127 | historyLength: int | None = None 128 | 129 | 130 | class TaskSendParams(BaseModel): 131 | id: str 132 | sessionId: str = Field(default_factory=lambda: uuid4().hex) 133 | message: Message 134 | acceptedOutputModes: Optional[List[str]] = None 135 | pushNotification: PushNotificationConfig | None = None 136 | historyLength: int | None = None 137 | metadata: dict[str, Any] | None = None 138 | 139 | 140 | class TaskPushNotificationConfig(BaseModel): 141 | id: str 142 | pushNotificationConfig: PushNotificationConfig 143 | 144 | 145 | ## RPC Messages 146 | 147 | 148 | class JSONRPCMessage(BaseModel): 149 | jsonrpc: Literal["2.0"] = "2.0" 150 | id: int | str | None = Field(default_factory=lambda: uuid4().hex) 151 | 152 | 153 | class JSONRPCRequest(JSONRPCMessage): 154 | method: str 155 | params: dict[str, Any] | None = None 156 | 157 | 158 | class JSONRPCError(BaseModel): 159 | code: int 160 | message: str 161 | data: Any | None = None 162 | 163 | 164 | class JSONRPCResponse(JSONRPCMessage): 165 | result: Any | None = None 166 | error: JSONRPCError | None = None 167 | 168 | 169 | class SendTaskRequest(JSONRPCRequest): 170 | method: Literal["tasks/send"] = "tasks/send" 171 | params: TaskSendParams 172 | 173 | 174 | class SendTaskResponse(JSONRPCResponse): 175 | result: Task | None = None 176 | 177 | 178 | class SendTaskStreamingRequest(JSONRPCRequest): 179 | method: Literal["tasks/sendSubscribe"] = "tasks/sendSubscribe" 180 | params: TaskSendParams 181 | 182 | 183 | class SendTaskStreamingResponse(JSONRPCResponse): 184 | result: TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None = None 185 | 186 | 187 | class GetTaskRequest(JSONRPCRequest): 188 | method: Literal["tasks/get"] = "tasks/get" 189 | params: TaskQueryParams 190 | 191 | 192 | class GetTaskResponse(JSONRPCResponse): 193 | result: Task | None = None 194 | 195 | 196 | class CancelTaskRequest(JSONRPCRequest): 197 | method: Literal["tasks/cancel",] = "tasks/cancel" 198 | params: TaskIdParams 199 | 200 | 201 | class CancelTaskResponse(JSONRPCResponse): 202 | result: Task | None = None 203 | 204 | 205 | class SetTaskPushNotificationRequest(JSONRPCRequest): 206 | method: Literal["tasks/pushNotification/set",] = "tasks/pushNotification/set" 207 | params: TaskPushNotificationConfig 208 | 209 | 210 | class SetTaskPushNotificationResponse(JSONRPCResponse): 211 | result: TaskPushNotificationConfig | None = None 212 | 213 | 214 | class GetTaskPushNotificationRequest(JSONRPCRequest): 215 | method: Literal["tasks/pushNotification/get",] = "tasks/pushNotification/get" 216 | params: TaskIdParams 217 | 218 | 219 | class GetTaskPushNotificationResponse(JSONRPCResponse): 220 | result: TaskPushNotificationConfig | None = None 221 | 222 | 223 | class TaskResubscriptionRequest(JSONRPCRequest): 224 | method: Literal["tasks/resubscribe",] = "tasks/resubscribe" 225 | params: TaskIdParams 226 | 227 | 228 | A2ARequest = TypeAdapter( 229 | Annotated[ 230 | Union[ 231 | SendTaskRequest, 232 | GetTaskRequest, 233 | CancelTaskRequest, 234 | SetTaskPushNotificationRequest, 235 | GetTaskPushNotificationRequest, 236 | TaskResubscriptionRequest, 237 | SendTaskStreamingRequest, 238 | ], 239 | Field(discriminator="method"), 240 | ] 241 | ) 242 | 243 | ## Error types 244 | 245 | 246 | class JSONParseError(JSONRPCError): 247 | code: int = -32700 248 | message: str = "Invalid JSON payload" 249 | data: Any | None = None 250 | 251 | 252 | class InvalidRequestError(JSONRPCError): 253 | code: int = -32600 254 | message: str = "Request payload validation error" 255 | data: Any | None = None 256 | 257 | 258 | class MethodNotFoundError(JSONRPCError): 259 | code: int = -32601 260 | message: str = "Method not found" 261 | data: None = None 262 | 263 | 264 | class InvalidParamsError(JSONRPCError): 265 | code: int = -32602 266 | message: str = "Invalid parameters" 267 | data: Any | None = None 268 | 269 | 270 | class InternalError(JSONRPCError): 271 | code: int = -32603 272 | message: str = "Internal error" 273 | data: Any | None = None 274 | 275 | 276 | class TaskNotFoundError(JSONRPCError): 277 | code: int = -32001 278 | message: str = "Task not found" 279 | data: None = None 280 | 281 | 282 | class TaskNotCancelableError(JSONRPCError): 283 | code: int = -32002 284 | message: str = "Task cannot be canceled" 285 | data: None = None 286 | 287 | 288 | class PushNotificationNotSupportedError(JSONRPCError): 289 | code: int = -32003 290 | message: str = "Push Notification is not supported" 291 | data: None = None 292 | 293 | 294 | class UnsupportedOperationError(JSONRPCError): 295 | code: int = -32004 296 | message: str = "This operation is not supported" 297 | data: None = None 298 | 299 | 300 | class ContentTypeNotSupportedError(JSONRPCError): 301 | code: int = -32005 302 | message: str = "Incompatible content types" 303 | data: None = None 304 | 305 | 306 | class AgentProvider(BaseModel): 307 | organization: str 308 | url: str | None = None 309 | 310 | 311 | class AgentCapabilities(BaseModel): 312 | streaming: bool = False 313 | pushNotifications: bool = False 314 | stateTransitionHistory: bool = False 315 | 316 | 317 | class AgentAuthentication(BaseModel): 318 | schemes: List[str] 319 | credentials: str | None = None 320 | 321 | 322 | class AgentSkill(BaseModel): 323 | id: str 324 | name: str 325 | description: str | None = None 326 | tags: List[str] | None = None 327 | examples: List[str] | None = None 328 | inputModes: List[str] | None = None 329 | outputModes: List[str] | None = None 330 | 331 | 332 | class AgentCard(BaseModel): 333 | name: str 334 | description: str | None = None 335 | url: str 336 | provider: AgentProvider | None = None 337 | version: str 338 | documentationUrl: str | None = None 339 | capabilities: AgentCapabilities 340 | authentication: AgentAuthentication | None = None 341 | defaultInputModes: List[str] = ["text"] 342 | defaultOutputModes: List[str] = ["text"] 343 | skills: List[AgentSkill] 344 | 345 | 346 | class A2AClientError(Exception): 347 | pass 348 | 349 | 350 | class A2AClientHTTPError(A2AClientError): 351 | def __init__(self, status_code: int, message: str): 352 | self.status_code = status_code 353 | self.message = message 354 | super().__init__(f"HTTP Error {status_code}: {message}") 355 | 356 | 357 | class A2AClientJSONError(A2AClientError): 358 | def __init__(self, message: str): 359 | self.message = message 360 | super().__init__(f"JSON Error: {message}") 361 | 362 | 363 | class MissingAPIKeyError(Exception): 364 | """Exception for missing API key.""" 365 | 366 | pass 367 | -------------------------------------------------------------------------------- /agent_api_template/python_a2a_template/service/task_manager.py: -------------------------------------------------------------------------------- 1 | 2 | from abc import ABC, abstractmethod 3 | from typing import Union, AsyncIterable, List 4 | from models.types import Task 5 | from models.types import ( 6 | JSONRPCResponse, 7 | TaskIdParams, 8 | TaskQueryParams, 9 | GetTaskRequest, 10 | TaskNotFoundError, 11 | SendTaskRequest, 12 | CancelTaskRequest, 13 | TaskNotCancelableError, 14 | SetTaskPushNotificationRequest, 15 | GetTaskPushNotificationRequest, 16 | GetTaskResponse, 17 | CancelTaskResponse, 18 | SendTaskResponse, 19 | SetTaskPushNotificationResponse, 20 | GetTaskPushNotificationResponse, 21 | PushNotificationNotSupportedError, 22 | TaskSendParams, 23 | TaskStatus, 24 | TaskState, 25 | TaskResubscriptionRequest, 26 | SendTaskStreamingRequest, 27 | SendTaskStreamingResponse, 28 | Artifact, 29 | PushNotificationConfig, 30 | TaskStatusUpdateEvent, 31 | JSONRPCError, 32 | TaskPushNotificationConfig, 33 | InternalError, 34 | ) 35 | from utils.util import new_not_implemented_error 36 | import asyncio 37 | import logging 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | class TaskManager(ABC): 42 | @abstractmethod 43 | async def on_get_task(self, request: GetTaskRequest) -> GetTaskResponse: 44 | pass 45 | 46 | @abstractmethod 47 | async def on_cancel_task(self, request: CancelTaskRequest) -> CancelTaskResponse: 48 | pass 49 | 50 | @abstractmethod 51 | async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse: 52 | pass 53 | 54 | @abstractmethod 55 | async def on_send_task_subscribe( 56 | self, request: SendTaskStreamingRequest 57 | ) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]: 58 | pass 59 | 60 | @abstractmethod 61 | async def on_set_task_push_notification( 62 | self, request: SetTaskPushNotificationRequest 63 | ) -> SetTaskPushNotificationResponse: 64 | pass 65 | 66 | @abstractmethod 67 | async def on_get_task_push_notification( 68 | self, request: GetTaskPushNotificationRequest 69 | ) -> GetTaskPushNotificationResponse: 70 | pass 71 | 72 | @abstractmethod 73 | async def on_resubscribe_to_task( 74 | self, request: TaskResubscriptionRequest 75 | ) -> Union[AsyncIterable[SendTaskResponse], JSONRPCResponse]: 76 | pass 77 | 78 | 79 | class InMemoryTaskManager(TaskManager): 80 | def __init__(self): 81 | self.tasks: dict[str, Task] = {} 82 | self.push_notification_infos: dict[str, PushNotificationConfig] = {} 83 | self.lock = asyncio.Lock() 84 | self.task_sse_subscribers: dict[str, List[asyncio.Queue]] = {} 85 | self.subscriber_lock = asyncio.Lock() 86 | 87 | async def on_get_task(self, request: GetTaskRequest) -> GetTaskResponse: 88 | logger.info(f"Getting task {request.params.id}") 89 | task_query_params: TaskQueryParams = request.params 90 | 91 | async with self.lock: 92 | task = self.tasks.get(task_query_params.id) 93 | if task is None: 94 | return GetTaskResponse(id=request.id, error=TaskNotFoundError()) 95 | 96 | task_result = self.append_task_history( 97 | task, task_query_params.historyLength 98 | ) 99 | 100 | return GetTaskResponse(id=request.id, result=task_result) 101 | 102 | async def on_cancel_task(self, request: CancelTaskRequest) -> CancelTaskResponse: 103 | logger.info(f"Cancelling task {request.params.id}") 104 | task_id_params: TaskIdParams = request.params 105 | 106 | async with self.lock: 107 | task = self.tasks.get(task_id_params.id) 108 | if task is None: 109 | return CancelTaskResponse(id=request.id, error=TaskNotFoundError()) 110 | 111 | return CancelTaskResponse(id=request.id, error=TaskNotCancelableError()) 112 | 113 | @abstractmethod 114 | async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse: 115 | pass 116 | 117 | @abstractmethod 118 | async def on_send_task_subscribe( 119 | self, request: SendTaskStreamingRequest 120 | ) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]: 121 | pass 122 | 123 | async def set_push_notification_info(self, task_id: str, notification_config: PushNotificationConfig): 124 | async with self.lock: 125 | task = self.tasks.get(task_id) 126 | if task is None: 127 | raise ValueError(f"Task not found for {task_id}") 128 | 129 | self.push_notification_infos[task_id] = notification_config 130 | 131 | return 132 | 133 | async def get_push_notification_info(self, task_id: str) -> PushNotificationConfig: 134 | async with self.lock: 135 | task = self.tasks.get(task_id) 136 | if task is None: 137 | raise ValueError(f"Task not found for {task_id}") 138 | 139 | return self.push_notification_infos[task_id] 140 | 141 | return 142 | 143 | async def has_push_notification_info(self, task_id: str) -> bool: 144 | async with self.lock: 145 | return task_id in self.push_notification_infos 146 | 147 | 148 | async def on_set_task_push_notification( 149 | self, request: SetTaskPushNotificationRequest 150 | ) -> SetTaskPushNotificationResponse: 151 | logger.info(f"Setting task push notification {request.params.id}") 152 | task_notification_params: TaskPushNotificationConfig = request.params 153 | 154 | try: 155 | await self.set_push_notification_info(task_notification_params.id, task_notification_params.pushNotificationConfig) 156 | except Exception as e: 157 | logger.error(f"Error while setting push notification info: {e}") 158 | return JSONRPCResponse( 159 | id=request.id, 160 | error=InternalError( 161 | message="An error occurred while setting push notification info" 162 | ), 163 | ) 164 | 165 | return SetTaskPushNotificationResponse(id=request.id, result=task_notification_params) 166 | 167 | async def on_get_task_push_notification( 168 | self, request: GetTaskPushNotificationRequest 169 | ) -> GetTaskPushNotificationResponse: 170 | logger.info(f"Getting task push notification {request.params.id}") 171 | task_params: TaskIdParams = request.params 172 | 173 | try: 174 | notification_info = await self.get_push_notification_info(task_params.id) 175 | except Exception as e: 176 | logger.error(f"Error while getting push notification info: {e}") 177 | return GetTaskPushNotificationResponse( 178 | id=request.id, 179 | error=InternalError( 180 | message="An error occurred while getting push notification info" 181 | ), 182 | ) 183 | 184 | return GetTaskPushNotificationResponse(id=request.id, result=TaskPushNotificationConfig(id=task_params.id, pushNotificationConfig=notification_info)) 185 | 186 | async def upsert_task(self, task_send_params: TaskSendParams) -> Task: 187 | logger.info(f"Upserting task {task_send_params.id}") 188 | async with self.lock: 189 | task = self.tasks.get(task_send_params.id) 190 | if task is None: 191 | task = Task( 192 | id=task_send_params.id, 193 | sessionId = task_send_params.sessionId, 194 | messages=[task_send_params.message], 195 | status=TaskStatus(state=TaskState.SUBMITTED), 196 | history=[task_send_params.message], 197 | ) 198 | self.tasks[task_send_params.id] = task 199 | else: 200 | task.history.append(task_send_params.message) 201 | 202 | return task 203 | 204 | async def on_resubscribe_to_task( 205 | self, request: TaskResubscriptionRequest 206 | ) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]: 207 | return new_not_implemented_error(request.id) 208 | 209 | async def update_store( 210 | self, task_id: str, status: TaskStatus, artifacts: list[Artifact] 211 | ) -> Task: 212 | async with self.lock: 213 | try: 214 | task = self.tasks[task_id] 215 | except KeyError: 216 | logger.error(f"Task {task_id} not found for updating the task") 217 | raise ValueError(f"Task {task_id} not found") 218 | 219 | task.status = status 220 | 221 | if status.message is not None: 222 | task.history.append(status.message) 223 | 224 | if artifacts is not None: 225 | if task.artifacts is None: 226 | task.artifacts = [] 227 | task.artifacts.extend(artifacts) 228 | 229 | return task 230 | 231 | def append_task_history(self, task: Task, historyLength: int | None): 232 | new_task = task.model_copy() 233 | if historyLength is not None and historyLength > 0: 234 | new_task.history = new_task.history[-historyLength:] 235 | else: 236 | new_task.history = [] 237 | 238 | return new_task 239 | 240 | async def setup_sse_consumer(self, task_id: str, is_resubscribe: bool = False): 241 | async with self.subscriber_lock: 242 | if task_id not in self.task_sse_subscribers: 243 | if is_resubscribe: 244 | raise ValueError("Task not found for resubscription") 245 | else: 246 | self.task_sse_subscribers[task_id] = [] 247 | 248 | sse_event_queue = asyncio.Queue(maxsize=0) # <=0 is unlimited 249 | self.task_sse_subscribers[task_id].append(sse_event_queue) 250 | return sse_event_queue 251 | 252 | async def enqueue_events_for_sse(self, task_id, task_update_event): 253 | async with self.subscriber_lock: 254 | if task_id not in self.task_sse_subscribers: 255 | return 256 | 257 | current_subscribers = self.task_sse_subscribers[task_id] 258 | for subscriber in current_subscribers: 259 | await subscriber.put(task_update_event) 260 | 261 | async def dequeue_events_for_sse( 262 | self, request_id, task_id, sse_event_queue: asyncio.Queue 263 | ) -> AsyncIterable[SendTaskStreamingResponse] | JSONRPCResponse: 264 | try: 265 | while True: 266 | event = await sse_event_queue.get() 267 | if isinstance(event, JSONRPCError): 268 | yield SendTaskStreamingResponse(id=request_id, error=event) 269 | break 270 | 271 | yield SendTaskStreamingResponse(id=request_id, result=event) 272 | if isinstance(event, TaskStatusUpdateEvent) and event.final: 273 | break 274 | finally: 275 | async with self.subscriber_lock: 276 | if task_id in self.task_sse_subscribers: 277 | self.task_sse_subscribers[task_id].remove(sse_event_queue) 278 | --------------------------------------------------------------------------------