Skip to content

Commit bbde6d9

Browse files
committed
adding dynamic agent request handler
1 parent 21048fb commit bbde6d9

File tree

9 files changed

+995
-71
lines changed

9 files changed

+995
-71
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@
6969
"coverage": "c8 npm run test",
7070
"generate": "curl https://raw.githubusercontent.com/google-a2a/A2A/refs/heads/main/specification/json/a2a.json > spec.json && node scripts/generateTypes.js && rm spec.json",
7171
"sample:cli": "tsx src/samples/cli.ts",
72-
"sample:movie-agent": "tsx src/samples/agents/movie-agent/index.ts"
72+
"sample:movie-agent": "tsx src/samples/agents/movie-agent/index.ts",
73+
"sample:dynamic-agents": "tsx src/samples/servers/dynamic_agents/index.ts",
74+
"sample:single_agent": "tsx src/samples/servers/single_agent/index.ts"
7375
},
7476
"dependencies": {
7577
"uuid": "^11.1.0"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Dynamic Agent Server
2+
3+
This sample demonstrates how to use `DynamicAgentRequestHandler` to create a server that can dynamically route to different agents based on the URL path.
4+
5+
The server provides two working example agents:
6+
- **Calculator Agent**: Performs basic math operations like "2 + 2" or "10 * 5"
7+
- **Weather Agent**: Provides fake weather data for major cities (London, Paris, Tokyo, New York, Sydney)
8+
9+
## Running the Sample
10+
11+
```bash
12+
npm run sample:dynamic-agents
13+
```
14+
15+
The server will start on `http://localhost:3000`.
16+
17+
## Available Endpoints
18+
19+
- `GET /agents/calculator/.well-known/agent-card.json` - Calculator agent card
20+
- `POST /agents/calculator/` - Calculator agent messages
21+
- `GET /agents/weather/.well-known/agent-card.json` - Weather agent card
22+
- `POST /agents/weather/` - Weather agent messages
23+
24+
## How It Works
25+
26+
The `DynamicAgentRequestHandler` inspects the incoming URL and dynamically determines:
27+
1. Which agent card to return
28+
2. Which task store to use
29+
3. Which agent executor to run
30+
31+
This allows a single route setup (`/agents`) to handle multiple different agent behaviors without needing separate route configurations for each agent.
32+
33+
## Testing the Agents
34+
35+
### Using the CLI Client
36+
37+
The easiest way to test the agents is using the built-in CLI client:
38+
39+
```bash
40+
# Test the calculator agent
41+
npm run sample:cli -- http://localhost:3000/agents/calculator/
42+
# Try typing: 1 + 1
43+
44+
# Test the weather agent
45+
npm run sample:cli -- http://localhost:3000/agents/weather/
46+
# Try typing: whats weather in tokyo
47+
```
48+
49+
### Using curl or HTTP clients
50+
51+
You can also test the agents using curl or any HTTP client:
52+
53+
```bash
54+
# Get calculator agent card
55+
curl http://localhost:3000/agents/calculator/.well-known/agent-card.json
56+
57+
# Send a math problem to calculator
58+
curl -X POST http://localhost:3000/agents/calculator/ \
59+
-H "Content-Type: application/json" \
60+
-d '{
61+
"jsonrpc": "2.0",
62+
"method": "sendMessage",
63+
"params": {
64+
"message": {
65+
"messageId": "test-1",
66+
"role": "user",
67+
"parts": [{"kind": "text", "text": "What is 15 * 7?"}],
68+
"kind": "message"
69+
}
70+
},
71+
"id": 1
72+
}'
73+
74+
# Ask weather agent about a city
75+
curl -X POST http://localhost:3000/agents/weather/ \
76+
-H "Content-Type: application/json" \
77+
-d '{
78+
"jsonrpc": "2.0",
79+
"method": "sendMessage",
80+
"params": {
81+
"message": {
82+
"messageId": "test-2",
83+
"role": "user",
84+
"parts": [{"kind": "text", "text": "What is the weather in Tokyo?"}],
85+
"kind": "message"
86+
}
87+
},
88+
"id": 2
89+
}'
90+
```
91+
92+
## Key Benefits
93+
94+
- **Single Route Setup**: One `setupRoutes()` call handles all agents
95+
- **Dynamic Routing**: URL inspection determines agent behavior
96+
- **Easy Extension**: Add new agents by updating resolver functions, no new routes needed
97+
- **Working Examples**: Both agents actually respond to messages with proper A2A protocol
98+
- **Clean API**: No need for complex route configuration
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import express from 'express';
2+
import {
3+
DynamicAgentRequestHandler,
4+
RouteContext
5+
} from '../../../server/index.js';
6+
import { AgentCard, Message, TextPart } from '../../../types.js';
7+
import { InMemoryTaskStore } from '../../../server/store.js';
8+
import { AgentExecutor } from '../../../server/agent_execution/agent_executor.js';
9+
import { A2AExpressApp } from '../../../server/express/a2a_express_app.js';
10+
import { RequestContext } from '../../../server/agent_execution/request_context.js';
11+
import { ExecutionEventBus } from '../../../server/events/execution_event_bus.js';
12+
import { v4 as uuidv4 } from 'uuid';
13+
14+
// Simple agent cards for calculator and weather
15+
const calculatorAgentCard: AgentCard = {
16+
name: 'Calculator Agent',
17+
description: 'Performs mathematical calculations',
18+
version: '1.0.0',
19+
protocolVersion: '2024-11-05',
20+
capabilities: {
21+
streaming: true,
22+
pushNotifications: false,
23+
},
24+
defaultInputModes: [],
25+
defaultOutputModes: [],
26+
skills: [],
27+
url: "http://localhost:3000/agents/calculator"
28+
};
29+
30+
const weatherAgentCard: AgentCard = {
31+
name: 'Weather Agent',
32+
description: 'Provides weather information and forecasts',
33+
version: '1.0.0',
34+
protocolVersion: '2024-11-05',
35+
capabilities: {
36+
streaming: true,
37+
pushNotifications: false,
38+
},
39+
defaultInputModes: [],
40+
defaultOutputModes: [],
41+
skills: [],
42+
url: "http://localhost:3000/agents/weather"
43+
};
44+
45+
// Calculator agent that performs basic math operations
46+
class CalculatorAgentExecutor implements AgentExecutor {
47+
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
48+
const userMessage = requestContext.userMessage;
49+
console.log(`Calculator processing: ${JSON.stringify(userMessage.parts)}`);
50+
51+
// Extract text from user message
52+
const textParts = userMessage.parts.filter(part => part.kind === 'text');
53+
const userText = textParts.map(part => (part as TextPart).text).join(' ');
54+
55+
// Simple calculation logic
56+
let result: string;
57+
try {
58+
// Look for basic math expressions
59+
const mathMatch = userText.match(/(\d+\.?\d*)\s*([\+\-\*\/])\s*(\d+\.?\d*)/);
60+
if (mathMatch) {
61+
const [, num1, operator, num2] = mathMatch;
62+
const a = parseFloat(num1);
63+
const b = parseFloat(num2);
64+
65+
switch (operator) {
66+
case '+': result = `${a} + ${b} = ${a + b}`; break;
67+
case '-': result = `${a} - ${b} = ${a - b}`; break;
68+
case '*': result = `${a} × ${b} = ${a * b}`; break;
69+
case '/': result = b !== 0 ? `${a} ÷ ${b} = ${a / b}` : 'Error: Division by zero'; break;
70+
default: result = 'Unknown operation';
71+
}
72+
} else {
73+
result = "I can help you with basic math! Try asking me something like '2 + 2' or '10 * 5'.";
74+
}
75+
} catch (error) {
76+
result = 'Sorry, I had trouble understanding that math expression.';
77+
}
78+
79+
// Create response message
80+
const responseMessage: Message = {
81+
kind: 'message',
82+
role: 'agent',
83+
messageId: uuidv4(),
84+
parts: [{ kind: 'text', text: result }],
85+
taskId: requestContext.taskId,
86+
contextId: userMessage.contextId,
87+
};
88+
89+
// Publish the response
90+
eventBus.publish(responseMessage);
91+
eventBus.finished();
92+
}
93+
94+
async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {
95+
console.log(`Canceling calculation task: ${taskId}`);
96+
eventBus.finished();
97+
}
98+
}
99+
100+
// Weather agent that provides fake weather information
101+
class WeatherAgentExecutor implements AgentExecutor {
102+
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
103+
const userMessage = requestContext.userMessage;
104+
console.log(`Weather agent processing: ${JSON.stringify(userMessage.parts)}`);
105+
106+
// Extract text from user message
107+
const textParts = userMessage.parts.filter(part => part.kind === 'text');
108+
const userText = textParts.map(part => (part as TextPart).text).join(' ').toLowerCase();
109+
110+
// Simple weather response logic
111+
let result: string;
112+
const cities = ['london', 'paris', 'tokyo', 'new york', 'sydney'];
113+
const foundCity = cities.find(city => userText.includes(city));
114+
115+
if (foundCity) {
116+
const temp = Math.floor(Math.random() * 30) + 5; // Random temp between 5-35°C
117+
const conditions = ['sunny', 'cloudy', 'rainy', 'partly cloudy'][Math.floor(Math.random() * 4)];
118+
result = `The weather in ${foundCity.charAt(0).toUpperCase() + foundCity.slice(1)} is currently ${temp}°C and ${conditions}. (This is fake data for demonstration purposes!)`;
119+
} else {
120+
result = "I can provide weather information! Try asking about the weather in London, Paris, Tokyo, New York, or Sydney.";
121+
}
122+
123+
// Create response message
124+
const responseMessage: Message = {
125+
kind: 'message',
126+
role: 'agent',
127+
messageId: uuidv4(),
128+
parts: [{ kind: 'text', text: result }],
129+
taskId: requestContext.taskId,
130+
contextId: userMessage.contextId,
131+
};
132+
133+
// Publish the response
134+
eventBus.publish(responseMessage);
135+
eventBus.finished();
136+
}
137+
138+
async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {
139+
console.log(`Canceling weather task: ${taskId}`);
140+
eventBus.finished();
141+
}
142+
}
143+
144+
// Create shared instances for persistence across requests
145+
const taskStore = new InMemoryTaskStore();
146+
const calculatorExecutor = new CalculatorAgentExecutor();
147+
const weatherExecutor = new WeatherAgentExecutor();
148+
149+
// Create the dynamic request handler
150+
const dynamicHandler = new DynamicAgentRequestHandler(
151+
// Agent card resolver - determines which agent card to use based on route
152+
async (route: RouteContext) => {
153+
console.log(`Resolving agent card for route: ${route.url}`);
154+
155+
// For other requests, check path segments
156+
if (route.url.includes('calculator')) {
157+
return calculatorAgentCard;
158+
} else if (route.url.includes('weather')) {
159+
return weatherAgentCard;
160+
}
161+
162+
throw new Error("request for invalid agent");
163+
},
164+
165+
// Task store resolver - same store instance for all agents to ensure persistence
166+
async (route: RouteContext) => {
167+
return taskStore;
168+
},
169+
170+
// Agent executor resolver - returns singleton instances for efficiency
171+
async (route: RouteContext) => {
172+
console.log(`Resolving agent executor for route: ${route.url}`);
173+
174+
if (route.url.includes('calculator')) {
175+
return calculatorExecutor;
176+
} else if (route.url.includes('weather')) {
177+
return weatherExecutor;
178+
}
179+
180+
throw new Error("request for invalid agent");
181+
}
182+
);
183+
184+
// Create the A2A Express app - dynamic routing is auto-detected from the handler
185+
const appBuilder = new A2AExpressApp(dynamicHandler);
186+
const app = appBuilder.setupRoutes(express(), '/agents');
187+
188+
// Start the server
189+
const PORT = process.env.PORT || 3000;
190+
191+
app.listen(PORT, () => {
192+
console.log(`🚀 Server started on http://localhost:${PORT}`);
193+
console.log('\nThe dynamic handler will route based on URL context.');
194+
console.log('Try accessing different URLs to see dynamic routing in action!');
195+
});
196+
197+
// That's it! Simple and clean - just like the original A2A pattern.
198+
// The dynamic handler inspects the request context to determine routing.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Single Agent Server
2+
3+
A simple A2A agent that demonstrates the basic agent pattern by responding "Hello World!" to any user message.
4+
5+
## Features
6+
7+
- **Simple Response**: Always responds with "Hello World!" regardless of user input
8+
- **Basic A2A Protocol**: Demonstrates core A2A agent structure
9+
- **No Dependencies**: Minimal setup with no external API keys or services required
10+
- **Single Agent**: Uses `DefaultRequestHandler` for straightforward single-agent routing
11+
12+
## Running the Agent
13+
14+
1. **Install dependencies** (from project root):
15+
```bash
16+
npm install
17+
```
18+
19+
2. **Start the agent**:
20+
```bash
21+
npx tsx src/samples/agents/hello-world-agent/index.ts
22+
```
23+
24+
3. **View the agent card**:
25+
```
26+
http://localhost:3001/.well-known/agent-card.json
27+
```
28+
29+
## Testing the Agent
30+
31+
You can test the agent using any A2A client or curl:
32+
33+
```bash
34+
# Send a message to the agent
35+
curl -X POST http://localhost:3001/ \
36+
-H "Content-Type: application/json" \
37+
-d '{
38+
"jsonrpc": "2.0",
39+
"id": "test-1",
40+
"method": "agents/chat",
41+
"params": {
42+
"message": {
43+
"kind": "message",
44+
"role": "user",
45+
"messageId": "msg-1",
46+
"parts": [{"kind": "text", "text": "What is your name?"}],
47+
"contextId": "ctx-1"
48+
}
49+
}
50+
}'
51+
```
52+
53+
No matter what you send, the agent will always respond with "Hello World!"
54+
55+
## Testing the Agents
56+
57+
### Using the CLI Client
58+
59+
The easiest way to test the agents is using the built-in CLI client:
60+
61+
```bash
62+
# Test the hello world agent
63+
npm run sample:cli -- http://localhost:3000/
64+
# Try typing anything
65+
```
66+
67+
## Code Structure
68+
69+
- **HelloWorldAgentExecutor**: Implements the core agent logic
70+
- **Agent Card**: Defines the agent's capabilities and metadata
71+
- **Express Server**: Sets up HTTP endpoints using A2AExpressApp
72+
73+
This sample is perfect for understanding the basic A2A agent structure before building more complex agents.

0 commit comments

Comments
 (0)