{"id":28186739,"url":"https://github.com/sing1ee/python-a2a-tutorial","last_synced_at":"2026-07-07T03:31:14.373Z","repository":{"id":291346594,"uuid":"977145532","full_name":"sing1ee/python-a2a-tutorial","owner":"sing1ee","description":"Python A2A Tutorial","archived":false,"fork":false,"pushed_at":"2025-05-04T02:03:28.000Z","size":46,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-04T03:18:38.440Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://a2aprotocol.ai/blog/python-a2a-tutorial-with-source-code","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sing1ee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-03T14:28:25.000Z","updated_at":"2025-05-04T02:37:59.000Z","dependencies_parsed_at":"2025-05-04T03:18:43.986Z","dependency_job_id":"9be5e0fd-f20e-49ed-adbe-e3c06495ec07","html_url":"https://github.com/sing1ee/python-a2a-tutorial","commit_stats":null,"previous_names":["sing1ee/python-a2a-tutorial"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sing1ee%2Fpython-a2a-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sing1ee%2Fpython-a2a-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sing1ee%2Fpython-a2a-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sing1ee%2Fpython-a2a-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sing1ee","download_url":"https://codeload.github.com/sing1ee/python-a2a-tutorial/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254485042,"owners_count":22078767,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-16T07:10:44.740Z","updated_at":"2025-10-27T19:09:51.852Z","avatar_url":"https://github.com/sing1ee.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Python A2A Tutorial Source Code\n\n[Python A2A Tutorial Source Code](https://github.com/sing1ee/python-a2a-tutorial)\n\n## Table of Contents\n- [Introduction](#introduction)\n- [Set up Your Environment](#set-up-your-environment)\n- [Creating A Project](#creating-a-project)\n- [Agent Skills](#agent-skills)\n- [Agent Card](#agent-card)\n- [A2A Server](#a2a-server)\n- [Interacting With Your A2A Server](#interacting-with-your-a2a-server)\n- [Adding Agent Capabilities](#adding-agent-capabilities)\n- [Using a Local Ollama Model](#using-a-local-ollama-model)\n\n## Introduction\n\nIn this tutorial, you will build a simple echo A2A server using Python. This barebones implementation will show you all the features A2A has to offer. Following this tutorial, you will be able to add agent functionality using Ollama or Google's Agent Development Kit.\n\n**What you'll learn:**\n- The basic concepts behind A2A\n- How to create an A2A server in Python\n- Interacting with an A2A server\n- Add a trained model to act as the agent\n\n## Set up Your Environment\n\n### What You'll Need\n\n- A code editor such as Cursor/VsCode\n- A command prompt such as Terminal (Linux), iTerm/Warp (Mac) or just the Terminal in Cursor\n\n\n### Python Environment\n\nWe'll be using [uv](https://docs.astral.sh/uv/getting-started/installation/) as our package manager and to set up our project.\n\nThe A2A libraries we'll be using require `python \u003e= 3.12` which [uv can install](https://docs.astral.sh/uv/guides/install-python/) if you don't already have a matching version. We'll be using python 3.12.\n\n### Check\n\nRun the following command to make sure you're ready for the next step:\n\n```bash\necho 'import sys; print(sys.version)' | uv run -\n```\n\nIf you see something similar to the following, you are ready to proceed!\n\n```bash\n3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0]\n```\n\n### My Environment\n- Python 3.13\n- uv: uv 0.7.2 (Homebrew 2025-04-30)\n- Warp\n- Ollama 0.6.7 (with Qwen3 support)\n- macOs Sequoia 15.4.1\n\n## Creating A Project\n\nLet's first create a project using `uv`. We'll add the `--package` flag in case you want to add tests, or publish your project later:\n\n```bash\nuv init --package my-project\ncd my-project\n```\n\n### Using a Virtual Env\n\nWe'll create a venv for this project. This only needs to be done once:\n\n```bash\nuv venv .venv\n```\n\nFor this and any future terminal windows you open, you'll need to source this venv:\n\n```bash\nsource .venv/bin/activate\n```\n\nIf you're using a code editor such as VS Code, you'll want to set the Python Interpreter for code completions. In VS Code, press `Ctrl-Shift-P` and select `Python: Select Interpreter`. Then select your project `my-project` followed by the correct python interpreter `Python 3.12.3 ('.venv':venv) ./.venv/bin/python`\n\nThe source code should now look similar to this:\n\n```\n# my-project\ntree\n.\n|____pyproject.toml\n|____README.md\n|____.venv\n| |____bin\n| | |____activate.bat\n| | |____activate.ps1\n| | |____python3\n| | |____python\n| | |____activate.fish\n| | |____pydoc.bat\n| | |____activate_this.py\n| | |____activate\n| | |____activate.nu\n| | |____deactivate.bat\n| | |____python3.13\n| | |____activate.csh\n| |____pyvenv.cfg\n| |____CACHEDIR.TAG\n| |____.gitignore\n| |____lib\n| | |____python3.13\n| | | |____site-packages\n| | | | |_____virtualenv.py\n| | | | |_____virtualenv.pth\n|____.python-version\n|____src\n| |____my_project\n| | |______init__.py\n```\n\n### Adding the Google-A2A Python Libraries\n\nNext we'll add the sample A2A python libraries from Google:\n\n```bash\nuv add git+https://github.com/google/A2A#subdirectory=samples/python\n```\n\npyproject.toml：\n```shell\n[project]\nname = \"my-project\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nauthors = [\n    { name = \"zhangcheng\", email = \"zh.milo@gmail.com\" }\n]\nrequires-python = \"\u003e=3.13\"\ndependencies = [\n    \"a2a-samples\",\n]\n\n[project.scripts]\nmy-project = \"my_project:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.uv.sources]\na2a-samples = { git = \"https://github.com/google/A2A\", subdirectory = \"samples/python\" }\n```\n\n### Setting up the project structure\n\nLet's now create some files we'll later be using:\n\n```bash\ntouch src/my_project/agent.py\ntouch src/my_project/task_manager.py\n```\n\n### Test Run\n\nIf everything is setup correctly, you should now be able to run your application:\n\n```bash\nuv run my-project\n```\n\nThe output should look something like this:\n\n```bash\nHello from my-project!\n```\n\n## Agent Skills\n\nAn agent skill is a set of capabilities the agent can perform. Here's an example of what it would look like for our echo agent:\n\n```ts\n{\n  id: \"my-project-echo-skill\"\n  name: \"Echo Tool\",\n  description: \"Echos the input given\",\n  tags: [\"echo\", \"repeater\"],\n  examples: [\"I will see this echoed back to me\"],\n  inputModes: [\"text\"],\n  outputModes: [\"text\"]\n}\n```\n\nThis conforms to the skills section of the Agent Card:\n\n```ts\n{\n  id: string; // unique identifier for the agent's skill\n  name: string; //human readable name of the skill\n  // description of the skill - will be used by the client or a human\n  // as a hint to understand what the skill does.\n  description: string;\n  // Set of tag words describing classes of capabilities for this specific\n  // skill (e.g. \"cooking\", \"customer support\", \"billing\")\n  tags: string[];\n  // The set of example scenarios that the skill can perform.\n  // Will be used by the client as a hint to understand how the skill can be\n  // used. (e.g. \"I need a recipe for bread\")\n  examples?: string[]; // example prompts for tasks\n  // The set of interaction modes that the skill supports\n  // (if different than the default)\n  inputModes?: string[]; // supported mime types for input\n  outputModes?: string[]; // supported mime types for output\n}\n```\n\n### Implementation\n\nLet's create this Agent Skill in code. Open up `src/my-project/__init__.py` and replace the contents with the following code:\n\n```python\nimport google_a2a\nfrom google_a2a.common.types import AgentSkill\n\ndef main():\n  skill = AgentSkill(\n    id=\"my-project-echo-skill\",\n    name=\"Echo Tool\",\n    description=\"Echos the input given\",\n    tags=[\"echo\", \"repeater\"],\n    examples=[\"I will see this echoed back to me\"],\n    inputModes=[\"text\"],\n    outputModes=[\"text\"],\n  )\n  print(skill)\n\nif __name__ == \"__main__\":\n  main()\n```\n\nif you got errors about modules, try this:\n\n```python\nfrom common.types import AgentSkill\n\n# same code\n```\n\n### Test Run\n\nLet's give this a run:\n\n```bash\nuv run my-project\n```\n\nThe output should look something like this:\n\n```bash\nid='my-project-echo-skill' name='Echo Tool' description='Echos the input given' tags=['echo', 'repeater'] examples=['I will see this echoed back to me'] inputModes=['text'] outputModes=['text']\n```\n\n## Agent Card\n\nNow that we have defined our skills, we can create an Agent Card.\n\nRemote Agents are required to publish an Agent Card in JSON format describing the agent's capabilities and skills in addition to authentication mechanisms. In other words, this lets the world know about your agent and how to interact with it.\n\n### Implementation\n\nFirst lets add some helpers for parsing command line arguments. This will be helpful later for starting our server:\n\n```bash\nuv add click\n```\n\nAnd update our code:\n\n```python\nimport logging\n\nimport click\nimport google_a2a\nfrom google_a2a.common.types import AgentSkill, AgentCapabilities, AgentCard\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n@click.command()\n@click.option(\"--host\", default=\"localhost\")\n@click.option(\"--port\", default=10002)\ndef main(host, port):\n  skill = AgentSkill(\n    id=\"my-project-echo-skill\",\n    name=\"Echo Tool\",\n    description=\"Echos the input given\",\n    tags=[\"echo\", \"repeater\"],\n    examples=[\"I will see this echoed back to me\"],\n    inputModes=[\"text\"],\n    outputModes=[\"text\"],\n  )\n  logging.info(skill)\n\nif __name__ == \"__main__\":\n  main()\n```\n\nNext we'll add our Agent Card:\n\n```python\n# ...\ndef main(host, port):\n  # ...\n  capabilities = AgentCapabilities()\n  agent_card = AgentCard(\n    name=\"Echo Agent\",\n    description=\"This agent echos the input given\",\n    url=f\"http://{host}:{port}/\",\n    version=\"0.1.0\",\n    defaultInputModes=[\"text\"],\n    defaultOutputModes=[\"text\"],\n    capabilities=capabilities,\n    skills=[skill]\n  )\n  logging.info(agent_card)\n\nif __name__ == \"__main__\":\n  main()\n```\n\n### Test Run\n\nLet's give this a run:\n\n```bash\nuv run my-project\n```\n\nThe output should look something like this:\n\n```bash\nINFO:root:id='my-project-echo-skill' name='Echo Tool' description='Echos the input given' tags=['echo', 'repeater'] examples=['I will see this echoed back to me'] inputModes=['text'] outputModes=['text']\nINFO:root:name='Echo Agent' description='This agent echos the input given' url='http://localhost:10002/' provider=None version='0.1.0' documentationUrl=None capabilities=AgentCapabilities(streaming=False, pushNotifications=False, stateTransitionHistory=False) authentication=None defaultInputModes=['text'] defaultOutputModes=['text'] skills=[AgentSkill(id='my-project-echo-skill', name='Echo Tool', description='Echos the input given', tags=['echo', 'repeater'], examples=['I will see this echoed back to me'], inputModes=['text'], outputModes=['text'])]\n```\n\n## A2A Server\n\nWe're almost ready to start our server! We'll be using the `A2AServer` class from `Google-A2A` which under the hood starts a [uvicorn](https://www.uvicorn.org/) server.\n\n### Task Manager\n\nBefore we create our server, we need a task manager to handle incoming requests.\n\nWe'll be implementing the InMemoryTaskManager interface which requires us to implement two methods:\n\n```python\nasync def on_send_task(\n  self,\n  request: SendTaskRequest\n) -\u003e SendTaskResponse:\n  \"\"\"\n  This method queries or creates a task for the agent.\n  The caller will receive exactly one response.\n  \"\"\"\n  pass\n\nasync def on_send_task_subscribe(\n  self,\n  request: SendTaskStreamingRequest\n) -\u003e AsyncIterable[SendTaskStreamingResponse] | JSONRPCResponse:\n  \"\"\"\n  This method subscribes the caller to future updates regarding a task.\n  The caller will receive a response and additionally receive subscription\n  updates over a session established between the client and the server\n  \"\"\"\n  pass\n```\n\nOpen up `src/my_project/task_manager.py` and add the following code. We will simply return a direct echo response and immediately mark the task complete without any sessions or subscriptions:\n\n```python\nfrom typing import AsyncIterable\n\nimport google_a2a\nfrom google_a2a.common.server.task_manager import InMemoryTaskManager\nfrom google_a2a.common.types import (\n  Artifact,\n  JSONRPCResponse,\n  Message,\n  SendTaskRequest,\n  SendTaskResponse,\n  SendTaskStreamingRequest,\n  SendTaskStreamingResponse,\n  Task,\n  TaskState,\n  TaskStatus,\n  TaskStatusUpdateEvent,\n)\n\nclass MyAgentTaskManager(InMemoryTaskManager):\n  def __init__(self):\n    super().__init__()\n\n  async def on_send_task(self, request: SendTaskRequest) -\u003e SendTaskResponse:\n    # Upsert a task stored by InMemoryTaskManager\n    await self.upsert_task(request.params)\n\n    task_id = request.params.id\n    # Our custom logic that simply marks the task as complete\n    # and returns the echo text\n    received_text = request.params.message.parts[0].text\n    task = await self._update_task(\n      task_id=task_id,\n      task_state=TaskState.COMPLETED,\n      response_text=f\"on_send_task received: {received_text}\"\n    )\n\n    # Send the response\n    return SendTaskResponse(id=request.id, result=task)\n\n  async def on_send_task_subscribe(\n    self,\n    request: SendTaskStreamingRequest\n  ) -\u003e AsyncIterable[SendTaskStreamingResponse] | JSONRPCResponse:\n    pass\n\n  async def _update_task(\n    self,\n    task_id: str,\n    task_state: TaskState,\n    response_text: str,\n  ) -\u003e Task:\n    task = self.tasks[task_id]\n    agent_response_parts = [\n      {\n        \"type\": \"text\",\n        \"text\": response_text,\n      }\n    ]\n    task.status = TaskStatus(\n      state=task_state,\n      message=Message(\n        role=\"agent\",\n        parts=agent_response_parts,\n      )\n    )\n    task.artifacts = [\n      Artifact(\n        parts=agent_response_parts,\n      )\n    ]\n    return task\n```\n\n### A2A Server\n\nWith a task manager complete, we can now create our server.\n\nOpen up `src/my_project/__init__.py` and add the following code:\n\n```python\n# ...\nfrom google_a2a.common.server import A2AServer\nfrom my_project.task_manager import MyAgentTaskManager\n# ...\ndef main(host, port):\n  # ...\n\n  task_manager = MyAgentTaskManager()\n  server = A2AServer(\n    agent_card=agent_card,\n    task_manager=task_manager,\n    host=host,\n    port=port,\n  )\n  server.start()\n```\n\n### Test Run\n\nLet's give this a run:\n\n```bash\nuv run my-project\n```\n\nThe output should look something like this:\n\n```bash\nINFO:root:id='my-project-echo-skill' name='Echo Tool' description='Echos the input given' tags=['echo', 'repeater'] examples=['I will see this echoed back to me'] inputModes=['text'] outputModes=['text']\nINFO:root:name='Echo Agent' description='This agent echos the input given' url='http://localhost:10002/' provider=None version='0.1.0' documentationUrl=None capabilities=AgentCapabilities(streaming=False, pushNotifications=False, stateTransitionHistory=False) authentication=None defaultInputModes=['text'] defaultOutputModes=['text'] skills=[AgentSkill(id='my-project-echo-skill', name='Echo Tool', description='Echos the input given', tags=['echo', 'repeater'], examples=['I will see this echoed back to me'], inputModes=['text'], outputModes=['text'])]\nINFO:     Started server process [582]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://localhost:10002 (Press CTRL+C to quit)\n```\n\nCongratulations! Your A2A server is now running!\n\n## Interacting With Your A2A Server\n\nFirst we'll use Google-A2A's command-line tool to send requests to our A2A server. After trying it out, we'll write our own basic client to see how this works under the hood.\n\n### Using Google-A2A's command-line tool\n\nWith your A2A server already running from the previous run:\n\n```bash\n# This should already be running in your terminal\n$ uv run my-project\nINFO:     Started server process [20538]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://localhost:10002 (Press CTRL+C to quit)\n```\n\nOpen up a new terminal in the same directory:\n\n```bash\nsource .venv/bin/activate\nuv run google-a2a-cli --agent http://localhost:10002\n\n# if got errors, try this (make sure that there is a dir hosts in .venv/lib/python3.13/site-packages):\nuv run python -m hosts.cli --agent http://localhost:10002\n```\n\nNote: This will only work if you've installed google-a2a from this [pull request](https://github.com/google/A2A/pull/169) as the cli was not exposed previously.\n\nOtherwise you'll have to checkout the [Google/A2A repository](https://github.com/google/A2A/) directly, navigate to the `samples/python` repository and run the cli directly.\n\nYou can then send messages to your server by typing and pressing Enter:\n\n```bash\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit): Hello!\n```\n\nIf everything is working correctly you'll see this in the response:\n\n```bash\n$ uv run python -m hosts.cli --agent http://localhost:10002\n======= Agent Card ========\n{\"name\":\"Echo Agent\",\"description\":\"This agent echos the input given\",\"url\":\"http://localhost:10002/\",\"version\":\"0.1.0\",\"capabilities\":{\"streaming\":false,\"pushNotifications\":false,\"stateTransitionHistory\":false},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[{\"id\":\"my-project-echo-skill\",\"name\":\"Echo Tool\",\"description\":\"Echos the input given\",\"tags\":[\"echo\",\"repeater\"],\"examples\":[\"I will see this echoed back to me\"],\"inputModes\":[\"text\"],\"outputModes\":[\"text\"]}]}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit): hello\nSelect a file path to attach? (press enter to skip):\n\n{\"jsonrpc\":\"2.0\",\"id\":\"5b3b74b7ea80495daff4047ee48a6c48\",\"result\":{\"id\":\"740f1e21465b4ee2af4af7b8c6cacad5\",\"sessionId\":\"7fbd065264cb4d6c91ed96909589fc35\",\"status\":{\"state\":\"completed\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"on_send_task received: hello\"}]},\"timestamp\":\"2025-05-03T22:18:41.649600\"},\"artifacts\":[{\"parts\":[{\"type\":\"text\",\"text\":\"on_send_task received: hello\"}],\"index\":0}],\"history\":[{\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"text\":\"hello\"}]}]}}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit):\n```\n\nTo exit type `:q` and press Enter.\n\n## Adding Agent Capabilities\n\nNow that we have a basic A2A server running, let's add some more functionality. We'll explore how A2A can work asynchronously and stream responses.\n\n### Streaming\n\nThis allows clients to subscribe to the server and receive multiple updates instead of a single response. This can be useful for long running agent tasks, or where multiple Artifacts may be streamed back to the client.\n\nFirst we'll declare our agent as ready for streaming. Open up `src/my_project/__init__.py` and update AgentCapabilities:\n\n```python\n# ...\ndef main(host, port):\n  # ...\n  capabilities = AgentCapabilities(\n    streaming=True\n  )\n  # ...\n```\n\nNow in `src/my_project/task_manager.py` we'll have to implement `on_send_task_subscribe`:\n\n```python\nimport asyncio\n# ...\nclass MyAgentTaskManager(InMemoryTaskManager):\n  # ...\n  async def _stream_3_messages(self, request: SendTaskStreamingRequest):\n    task_id = request.params.id\n    received_text = request.params.message.parts[0].text\n\n    text_messages = [\"one\", \"two\", \"three\"]\n    for text in text_messages:\n      parts = [\n        {\n          \"type\": \"text\",\n          \"text\": f\"{received_text}: {text}\",\n        }\n      ]\n      message = Message(role=\"agent\", parts=parts)\n      is_last = text == text_messages[-1]\n      task_state = TaskState.COMPLETED if is_last else TaskState.WORKING\n      task_status = TaskStatus(\n        state=task_state,\n        message=message\n      )\n      task_update_event = TaskStatusUpdateEvent(\n        id=request.params.id,\n        status=task_status,\n        final=is_last,\n      )\n      await self.enqueue_events_for_sse(\n        request.params.id,\n        task_update_event\n      )\n\n  async def on_send_task_subscribe(\n    self,\n    request: SendTaskStreamingRequest\n  ) -\u003e AsyncIterable[SendTaskStreamingResponse] | JSONRPCResponse:\n    # Upsert a task stored by InMemoryTaskManager\n    await self.upsert_task(request.params)\n\n    task_id = request.params.id\n    # Create a queue of work to be done for this task\n    sse_event_queue = await self.setup_sse_consumer(task_id=task_id)\n\n    # Start the asynchronous work for this task\n    asyncio.create_task(self._stream_3_messages(request))\n\n    # Tell the client to expect future streaming responses\n    return self.dequeue_events_for_sse(\n      request_id=request.id,\n      task_id=task_id,\n      sse_event_queue=sse_event_queue,\n    )\n```\n\nRestart your A2A server to pickup the new changes and then rerun the cli:\n\n```bash\n$ uv run python -m hosts.cli --agent http://localhost:10002\n======= Agent Card ========\n{\"name\":\"Echo Agent\",\"description\":\"This agent echos the input given\",\"url\":\"http://localhost:10002/\",\"version\":\"0.1.0\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":false},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[{\"id\":\"my-project-echo-skill\",\"name\":\"Echo Tool\",\"description\":\"Echos the input given\",\"tags\":[\"echo\",\"repeater\"],\"examples\":[\"I will see this echoed back to me\"],\"inputModes\":[\"text\"],\"outputModes\":[\"text\"]}]}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit): Streaming?\nSelect a file path to attach? (press enter to skip):\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"c6f21c0b7e5e497caaca4a692aaefd7a\",\"result\":{\"id\":\"d7218dd3c122477c89d62e7d897fea0b\",\"status\":{\"state\":\"working\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Streaming?: one\"}]},\"timestamp\":\"2025-05-03T22:22:31.354656\"},\"final\":false}}\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"c6f21c0b7e5e497caaca4a692aaefd7a\",\"result\":{\"id\":\"d7218dd3c122477c89d62e7d897fea0b\",\"status\":{\"state\":\"working\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Streaming?: two\"}]},\"timestamp\":\"2025-05-03T22:22:31.354684\"},\"final\":false}}\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"c6f21c0b7e5e497caaca4a692aaefd7a\",\"result\":{\"id\":\"d7218dd3c122477c89d62e7d897fea0b\",\"status\":{\"state\":\"completed\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Streaming?: three\"}]},\"timestamp\":\"2025-05-03T22:22:31.354698\"},\"final\":true}}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit):\n```\n\nSometimes the agent might need additional input. For example, maybe the agent will ask the client if they'd like to keep repeating the 3 messages. In this case, the agent will respond with `TaskState.INPUT_REQUIRED` to which the client will then resend `send_task_streaming` with the same `task_id` and `session_id` but with an updated message providing the input required by the agent. On the server-side we'll update `on_send_task_subscribe` to handle this case:\n\n```python\nimport asyncio\nfrom typing import AsyncIterable\n\nfrom common.server.task_manager import InMemoryTaskManager\nfrom common.types import (\n  Artifact,\n  JSONRPCResponse,\n  Message,\n  SendTaskRequest,\n  SendTaskResponse,\n  SendTaskStreamingRequest,\n  SendTaskStreamingResponse,\n  Task,\n  TaskState,\n  TaskStatus,\n  TaskStatusUpdateEvent,\n)\n\nclass MyAgentTaskManager(InMemoryTaskManager):\n  def __init__(self):\n    super().__init__()\n\n  async def on_send_task(self, request: SendTaskRequest) -\u003e SendTaskResponse:\n    # Upsert a task stored by InMemoryTaskManager\n    await self.upsert_task(request.params)\n\n    task_id = request.params.id\n    # Our custom logic that simply marks the task as complete\n    # and returns the echo text\n    received_text = request.params.message.parts[0].text\n    task = await self._update_task(\n      task_id=task_id,\n      task_state=TaskState.COMPLETED,\n      response_text=f\"on_send_task received: {received_text}\"\n    )\n\n    # Send the response\n    return SendTaskResponse(id=request.id, result=task)\n\n  async def _stream_3_messages(self, request: SendTaskStreamingRequest):\n    task_id = request.params.id\n    received_text = request.params.message.parts[0].text\n\n    text_messages = [\"one\", \"two\", \"three\"]\n    for text in text_messages:\n      parts = [\n        {\n          \"type\": \"text\",\n          \"text\": f\"{received_text}: {text}\",\n        }\n      ]\n      message = Message(role=\"agent\", parts=parts)\n      # is_last = text == text_messages[-1]\n      task_state = TaskState.WORKING\n      # task_state = TaskState.COMPLETED if is_last else TaskState.WORKING\n      task_status = TaskStatus(\n        state=task_state,\n        message=message\n      )\n      task_update_event = TaskStatusUpdateEvent(\n        id=request.params.id,\n        status=task_status,\n        final=False,\n      )\n      await self.enqueue_events_for_sse(\n        request.params.id,\n        task_update_event\n      )\n    ask_message = Message(\n      role=\"agent\",\n      parts=[\n        {\n          \"type\": \"text\",\n          \"text\": \"Would you like more messages? (Y/N)\"\n        }\n      ]\n    )\n    task_update_event = TaskStatusUpdateEvent(\n      id=request.params.id,\n      status=TaskStatus(\n        state=TaskState.INPUT_REQUIRED,\n        message=ask_message\n      ),\n      final=True,\n    )\n    await self.enqueue_events_for_sse(\n      request.params.id,\n      task_update_event\n    )\n\n  async def on_send_task_subscribe(\n    self,\n    request: SendTaskStreamingRequest\n  ) -\u003e AsyncIterable[SendTaskStreamingResponse] | JSONRPCResponse:\n    task_id = request.params.id\n    is_new_task = task_id in self.tasks\n    # Upsert a task stored by InMemoryTaskManager\n    await self.upsert_task(request.params)\n\n    received_text = request.params.message.parts[0].text\n    sse_event_queue = await self.setup_sse_consumer(task_id=task_id)\n    if not is_new_task and received_text == \"N\":\n      task_update_event = TaskStatusUpdateEvent(\n        id=request.params.id,\n        status=TaskStatus(\n          state=TaskState.COMPLETED,\n          message=Message(\n            role=\"agent\",\n            parts=[\n              {\n                \"type\": \"text\",\n                \"text\": \"All done!\"\n              }\n            ]\n          )\n        ),\n        final=True,\n      )\n      await self.enqueue_events_for_sse(\n        request.params.id,\n        task_update_event,\n      )\n    else:\n      asyncio.create_task(self._stream_3_messages(request))\n\n    return self.dequeue_events_for_sse(\n      request_id=request.id,\n      task_id=task_id,\n      sse_event_queue=sse_event_queue,\n    )\n\n  async def _update_task(\n    self,\n    task_id: str,\n    task_state: TaskState,\n    response_text: str,\n  ) -\u003e Task:\n    task = self.tasks[task_id]\n    agent_response_parts = [\n      {\n        \"type\": \"text\",\n        \"text\": response_text,\n      }\n    ]\n    task.status = TaskStatus(\n      state=task_state,\n      message=Message(\n        role=\"agent\",\n        parts=agent_response_parts,\n      )\n    )\n    task.artifacts = [\n      Artifact(\n        parts=agent_response_parts,\n      )\n    ]\n    return task\n\n```\n\nNow after restarting the server and running the cli, we can see the task will keep running until we tell the agent `N`:\n\n```bash\nuv run python -m hosts.cli --agent http://localhost:10002\n======= Agent Card ========\n{\"name\":\"Echo Agent\",\"description\":\"This agent echos the input given\",\"url\":\"http://localhost:10002/\",\"version\":\"0.1.0\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":false},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[{\"id\":\"my-project-echo-skill\",\"name\":\"Echo Tool\",\"description\":\"Echos the input given\",\"tags\":[\"echo\",\"repeater\"],\"examples\":[\"I will see this echoed back to me\"],\"inputModes\":[\"text\"],\"outputModes\":[\"text\"]}]}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit): Streaming?\nSelect a file path to attach? (press enter to skip):\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"18357b72fc5841ef8e8ede073b91ac48\",\"result\":{\"id\":\"b02f6989e72f44818560778d39fcef18\",\"status\":{\"state\":\"working\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Streaming?: one\"}]},\"timestamp\":\"2025-05-04T09:18:18.235994\"},\"final\":false}}\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"18357b72fc5841ef8e8ede073b91ac48\",\"result\":{\"id\":\"b02f6989e72f44818560778d39fcef18\",\"status\":{\"state\":\"working\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Streaming?: two\"}]},\"timestamp\":\"2025-05-04T09:18:18.236021\"},\"final\":false}}\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"18357b72fc5841ef8e8ede073b91ac48\",\"result\":{\"id\":\"b02f6989e72f44818560778d39fcef18\",\"status\":{\"state\":\"working\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Streaming?: three\"}]},\"timestamp\":\"2025-05-04T09:18:18.236033\"},\"final\":false}}\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"18357b72fc5841ef8e8ede073b91ac48\",\"result\":{\"id\":\"b02f6989e72f44818560778d39fcef18\",\"status\":{\"state\":\"input-required\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"Would you like more messages? (Y/N)\"}]},\"timestamp\":\"2025-05-04T09:18:18.236044\"},\"final\":true}}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit): N\nSelect a file path to attach? (press enter to skip):\nstream event =\u003e {\"jsonrpc\":\"2.0\",\"id\":\"86ce510ba68b4797a5b68061c8c4780b\",\"result\":{\"id\":\"64e51665dc354d2da7c31bcc45abc8f9\",\"status\":{\"state\":\"completed\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"All done!\"}]},\"timestamp\":\"2025-05-04T09:22:24.598749\"},\"final\":true}}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit):\n```\n\nCongratulations! You now have an agent that is able to asynchronously perform work and ask users for input when needed.\n\n## Using a Local Ollama Model\n\nNow we get to the exciting part. We're going to add AI to our A2A server.\n\nIn this tutorial, we'll be setting up a local Ollama model and integrating it with our A2A server.\n\n### Requirements\n\nWe'll be installing `ollama`, `langchain` as well as downloading an ollama model that supports MCP tools (for a future tutorial).\n\n1. Download [ollama](https://ollama.com/download)\n2. Run an ollama server:\n\n```bash\n# Note: if ollama is already running, you may get an error such as\n# Error: listen tcp 127.0.0.1:11434: bind: address already in use\n# On linux you can run systemctl stop ollama to stop ollama\nollama serve\n```\n\n3. Download a model from [this list](https://ollama.com/search). We'll be using `qwq` as it supports `tools` (as shown by its tags) and runs on a 24GB graphics card:\n\n```bash\nollama pull qwq\n\n# or ollama pull qwen3:4b\n# only 2.4G\n```\n\n4. Install `langchain`:\n\n```bash\nuv add langchain langchain-ollama langgraph\n```\n\nNow with ollama setup, we can start integrating it into our A2A server.\n\n### Integrating Ollama into our A2A server\n\nFirst open up `src/my_project/__init__.py`:\n\n```python\n# ...\n\n@click.command()\n@click.option(\"--host\", default=\"localhost\")\n@click.option(\"--port\", default=10002)\n@click.option(\"--ollama-host\", default=\"http://127.0.0.1:11434\")\n@click.option(\"--ollama-model\", default=None)\ndef main(host, port, ollama_host, ollama_model):\n  # ...\n  capabilities = AgentCapabilities(\n    streaming=False # We'll leave streaming capabilities as an exercise for the reader\n  )\n  # ...\n  task_manager = MyAgentTaskManager(\n    ollama_host=ollama_host,\n    ollama_model=ollama_mode,\n  )\n  # ..\n```\n\nNow let's add AI functionality in `src/my_project/agent.py`:\n\n```python\nfrom langchain_ollama import ChatOllama\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.graph.graph import CompiledGraph\n\ndef create_ollama_agent(ollama_base_url: str, ollama_model: str):\n  ollama_chat_llm = ChatOllama(\n    base_url=ollama_base_url,\n    model=ollama_model,\n    temperature=0.2\n  )\n  agent = create_react_agent(ollama_chat_llm, tools=[])\n  return agent\n\nasync def run_ollama(ollama_agent: CompiledGraph, prompt: str):\n  agent_response = await ollama_agent.ainvoke(\n    {\"messages\": prompt }\n  )\n  message = agent_response[\"messages\"][-1].content\n  return str(message)\n```\n\nFinally let's call our ollama agent from `src/my_project/task_manager.py`:\n\n```python\n# ...\nfrom my_project.agent import create_ollama_agent, run_ollama\n\nclass MyAgentTaskManager(InMemoryTaskManager):\n  def __init__(\n    self,\n    ollama_host: str,\n    ollama_model: typing.Union[None, str]\n  ):\n    super().__init__()\n    if ollama_model is not None:\n      self.ollama_agent = create_ollama_agent(\n        ollama_base_url=ollama_host,\n        ollama_model=ollama_model\n      )\n    else:\n      self.ollama_agent = None\n\n  async def on_send_task(self, request: SendTaskRequest) -\u003e SendTaskResponse:\n    # ...\n    received_text = request.params.message.parts[0].text\n    response_text = f\"on_send_task received: {received_text}\"\n    if self.ollama_agent is not None:\n      response_text = await run_ollama(ollama_agent=self.ollama_agent, prompt=received_text)\n\n    task = await self._update_task(\n      task_id=task_id,\n      task_state=TaskState.COMPLETED,\n      response_text=response_text\n    )\n\n    # Send the response\n    return SendTaskResponse(id=request.id, result=task)\n\n  # ...\n```\n\nLet's test it out!\n\nFirst rerun our A2A server replacing `qwq` with the ollama model you downloaded:\n\n```bash\nuv run my-project --ollama-host http://127.0.0.1:11434 --ollama-model qwen3:4b\n```\n\nAnd then rerun the cli:\n\n```bash\nuv run python -m hosts.cli --agent http://localhost:10002\n```\n\nNote, if you're using a large model, it may take a while to load. The cli may timeout. In which case rerun the cli once the ollama server has finished loading the model.\n\nYou should see something like the following:\n\n```bash\n\n======= Agent Card ========\n{\"name\":\"Echo Agent\",\"description\":\"This agent echos the input given\",\"url\":\"http://localhost:10002/\",\"version\":\"0.1.0\",\"capabilities\":{\"streaming\":false,\"pushNotifications\":false,\"stateTransitionHistory\":false},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[{\"id\":\"my-project-echo-skill\",\"name\":\"Echo Tool\",\"description\":\"Echos the input given\",\"tags\":[\"echo\",\"repeater\"],\"examples\":[\"I will see this echoed back to me\"],\"inputModes\":[\"text\"],\"outputModes\":[\"text\"]}]}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit): hey\nSelect a file path to attach? (press enter to skip):\n\n{\"jsonrpc\":\"2.0\",\"id\":\"eca7ecf4d6da4a65a4ff99ab0954b957\",\"result\":{\"id\":\"62636e021ac0483bb31d40c1473796fa\",\"sessionId\":\"438927e3540f459389f3d3cb216dd945\",\"status\":{\"state\":\"completed\",\"message\":{\"role\":\"agent\",\"parts\":[{\"type\":\"text\",\"text\":\"\u003cthink\u003e\\nOkay, the user just said \\\"hey\\\". That's a pretty open-ended greeting. I need to respond in a friendly and welcoming way. Maybe start with a greeting like \\\"Hi there!\\\" to keep it casual. Then, ask how I can assist them. Since they didn't specify a topic, I should keep the response general but inviting. Let me make sure the tone is positive and approachable. Also, check if there's any specific context I should consider, but since there's no prior conversation, it's safe to assume they just want to start a new interaction. Alright, time to put that together.\\n\u003c/think\u003e\\n\\nHi there! How can I assist you today? 😊\"}]},\"timestamp\":\"2025-05-04T10:01:55.068049\"},\"artifacts\":[{\"parts\":[{\"type\":\"text\",\"text\":\"\u003cthink\u003e\\nOkay, the user just said \\\"hey\\\". That's a pretty open-ended greeting. I need to respond in a friendly and welcoming way. Maybe start with a greeting like \\\"Hi there!\\\" to keep it casual. Then, ask how I can assist them. Since they didn't specify a topic, I should keep the response general but inviting. Let me make sure the tone is positive and approachable. Also, check if there's any specific context I should consider, but since there's no prior conversation, it's safe to assume they just want to start a new interaction. Alright, time to put that together.\\n\u003c/think\u003e\\n\\nHi there! How can I assist you today? 😊\"}],\"index\":0}],\"history\":[{\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"text\":\"hey\"}]}]}}\n=========  starting a new task ========\n\nWhat do you want to send to the agent? (:q or quit to exit):\n```\n\nCongratulations! You now have an A2A server generating responses using an AI model!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsing1ee%2Fpython-a2a-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsing1ee%2Fpython-a2a-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsing1ee%2Fpython-a2a-tutorial/lists"}