diff --git a/demo/README.md b/demo/README.md index 434f8a13..2f8e7e8f 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,34 +1,168 @@ -## Demo Web App +# Demo Web App -This demo application showcases agents talking to other agents over A2A. +This demo application showcases agents talking to other agents over the Agent-to-Agent (A2A) protocol. It highlights how specialized, independent agents can be integrated and orchestrated to solve complex user requests. ![image](./a2a_demo_arch.png) -- The frontend is a [mesop](https://github.com/mesop-dev/mesop) web application that renders conversations as content between the end user and the "Host Agent". This app can render text content, thought bubbles, web forms (requests for input from agents), and images. More content types coming soon - -- The [Host Agent](/samples/python/hosts/multiagent/host_agent.py) is a Google ADK agent which orchestrates user requests to Remote Agents. - -- Each [Remote Agent](/samples/python/hosts/multiagent/remote_agent_connection.py) is an A2AClient running inside a Google ADK agent. Each remote agent will retrieve the A2AServer's [AgentCard](https://google.github.io/A2A/#documentation?id=agent-card) and then proxy all requests using A2A. +## Agent Architecture + +The demonstration is built around a powerful, modular architecture where a central agent delegates work to remote, specialized agents using the A2A standard. + +### Components + +You begin by building agents using the [Agent Development Kit](https://google.github.io/adk-docs/) (ADK). ADK is an open-source toolkit developed by Google that simplifies agent construction. While any agent development framework such as [LangGraph](https://www.langchain.com/langgraph) or [CrewAI](https://www.crewai.com/) can be used, this tutorial focuses on ADK to build the core agents. + +- **Frontend**: The frontend is a [mesop](https://github.com/mesop-dev/mesop) web application that renders conversations as content between the end user and the "Host Agent". This app can render text content, thought bubbles, web forms (requests for input from agents), and images. More content types will be added soon. + +- **Host Agent (Client)**: The [Host Agent](/samples/python/hosts/multiagent/host_agent.py) is a Google ADK agent which orchestrates user requests to Remote Agents. + +- **Remote Agent**: Each [Remote Agent](/samples/python/hosts/multiagent/remote_agent_connection.py) is an A2AClient running inside a Google ADK agent. Each remote agent will retrieve the A2AServer's [AgentCard](https://google.github.io/A2A/#documentation?id=agent-card) and then proxy all requests using A2A. + +### How to build it piece by piece: Converting an Agent to A2A + +Both the remote agent (e.g., the Reimbursement Agent) and the Host Agent leverage A2A to collaborate effectively. To convert an agent's core logic into a compliant A2A agent, you define three main components: the **Agent Skill**, the **Agent Card**, and the **Agent Executor**. + +#### 1. Agent Skill and Agent Card (Metadata) + +The **Agent Skill** declares what the agent can do, and the **Agent Card** is the agent's digital business card, advertising its name, version, location (`url`), and available skills. + +```python +skill = AgentSkill( + id='process_reimbursement', + name='Process Reimbursement Tool', + description='Helps with the reimbursement process for users given the amount and purpose of the reimbursement.', + tags=['reimbursement'], + examples=[ + 'Can you reimburse me $20 for my lunch with the clients?' + ], + ) + agent_card = AgentCard( + name='Reimbursement Agent', + description='This agent handles the reimbursement process for the employees given the amount and purpose of the reimbursement.', + url=f'http://{host}:{port}/', + version='1.0.0', + default_input_modes=ReimbursementAgent.SUPPORTED_CONTENT_TYPES, + default_output_modes=ReimbursementAgent.SUPPORTED_CONTENT_TYPES, + capabilities=capabilities, + skills=[skill], + ) +``` +#### 2. Agent Executor (Execution Logic) +The Agent Executor is the bridge between the A2A protocol and the agent's core business logic (ReimbursementAgent). It implements the A2A execution interface, handles the request/task lifecycle, and manages the streaming of status updates and final artifacts back to the client. + + +```python +import json + +from a2a.server.agent_execution import AgentExecutor, RequestContext +from a2a.server.events import EventQueue +from a2a.server.tasks import TaskUpdater +from a2a.types import ( + DataPart, + Part, + Task, + TaskState, + TextPart, + UnsupportedOperationError, +) +from a2a.utils import ( + new_agent_parts_message, + new_agent_text_message, + new_task, +) +from a2a.utils.errors import ServerError +from agent import ReimbursementAgent + +class ReimbursementAgentExecutor(AgentExecutor): + """Reimbursement AgentExecutor Example.""" + + def __init__(self): + self.agent = ReimbursementAgent() + + async def execute( + self, + context: RequestContext, + event_queue: EventQueue, + ) -> None: + query = context.get_user_input() + task = context.current_task + + # This agent always produces Task objects. If this request does + # not have current task, create a new one and use it. + if not task: + task = new_task(context.message) + await event_queue.enqueue_event(task) + updater = TaskUpdater(event_queue, task.id, task.context_id) + # invoke the underlying agent, using streaming results. The streams + # now are update events. + async for item in self.agent.stream(query, task.context_id): + is_task_complete = item['is_task_complete'] + artifacts = None + if not is_task_complete: + await updater.update_status( + TaskState.working, + new_agent_text_message( + item['updates'], task.context_id, task.id + ), + ) + continue + # If the response is a dictionary, assume its a form + if isinstance(item['content'], dict): + # Verify it is a valid form + if ( + 'response' in item['content'] + and 'result' in item['content']['response'] + ): + data = json.loads(item['content']['response']['result']) + await updater.update_status( + TaskState.input_required, + new_agent_parts_message( + [Part(root=DataPart(data=data))], + task.context_id, + task.id, + ), + final=True, + ) + continue + await updater.update_status( + TaskState.failed, + new_agent_text_message( + 'Reaching an unexpected state', + task.context_id, + task.id, + ), + final=True, + ) + break + # Emit the appropriate events + await updater.add_artifact( + [Part(root=TextPart(text=item['content']))], name='form' + ) + await updater.complete() + break + + async def cancel( + self, request: RequestContext, event_queue: EventQueue + ) -> Task | None: + raise ServerError(error=UnsupportedOperationError()) +``` +#### Agent Interaction Flow +- Start the host and client: + - Clone this [GitHub repo](https://github.com/a2aproject/a2a-samples). ## Features - - -### Dynamically add agents - -Clicking on the robot icon in the web app lets you add new agents. Enter the address of the remote agent's AgentCard and the app will fetch the card and add the remote agent to the local set of known agents. - -### Speak with one or more agents - -Click on the chat button to start or continue an existing conversation. This conversation will go to the Host Agent which will then delegate the request to one or more remote agents. - -If the agent returns complex content - like an image or a web-form - the frontend will render this in the conversation view. The Remote Agent will take care of converting this content between A2A and the web apps native application representation. +| Feature | Description | +| :--- | :--- | +| **Dynamically add agents** | Clicking the robot icon lets you add new agents. Enter the remote agent's **AgentCard address** (URL) to fetch its capabilities and add it to the Host Agent's list of known collaborators. | +| **Speak with one or more agents** | Start a conversation that goes to the Host Agent, which then delegates the request to the relevant remote agent(s). The frontend renders complex content—like images or web-forms—as returned by the agent. | +| **Explore A2A Tasks** | The history view shows the raw messages sent between the web app and all agents (Host and Remote). The task list displays the critical A2A task updates and state changes from the remote agents. | ### Explore A2A Tasks -Click on the history to see the messages sent between the web app and all of the agents (Host agent and Remote agents). +- Click **history** to see the messages sent between the web app and all of the agents (Host agent and Remote agents). -Click on the task list to see all the A2A task updates from the remote agents +- Click the task list to see all the A2A task updates from the remote agents. ## Prerequisites @@ -37,7 +171,9 @@ Click on the task list to see all the A2A task updates from the remote agents - Agent servers speaking A2A ([use these samples](/samples/python/agents/README.md)) - Authentication credentials (API Key or Vertex AI) -## Running the Examples +## Run the Examples + +We have already built it for you - just download and run it. For convenience, the sample agents (the Remote Agent and the Host Agent) have been pre-built. 1. Navigate to the demo ui directory: ```bash @@ -88,7 +224,7 @@ Click on the task list to see all the A2A task updates from the remote agents Back in the demo UI you can go to the _Remote Agents_ tab and add this agent's address: - ``` + ```bash localhost:10002 ```