smarta2a 0.2.5__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,8 @@ import httpx
4
4
  import json
5
5
  from httpx_sse import connect_sse
6
6
  from inspect import signature, Parameter, iscoroutinefunction
7
- from pydantic import create_model, Field, BaseModel
7
+ from pydantic import create_model, Field, BaseModel, ValidationError
8
+ from typing import Optional, Union
8
9
 
9
10
  # Local imports
10
11
  from smarta2a.utils.types import (
@@ -182,86 +183,119 @@ class A2AClient:
182
183
 
183
184
 
184
185
  async def list_tools(self) -> list[dict[str, Any]]:
185
- """Return metadata for all available tools."""
186
+ """Return metadata for all available tools with minimal inputSchema."""
186
187
  tools = []
187
- tool_names = [
188
- 'send'
189
- ]
188
+ tool_names = ['send'] # add other tool names here
190
189
  for name in tool_names:
191
190
  method = getattr(self, name)
192
191
  doc = method.__doc__ or ""
193
192
  description = doc.strip().split('\n')[0] if doc else ""
194
193
 
195
- # Generate input schema
196
194
  sig = signature(method)
197
- parameters = sig.parameters
198
-
199
- fields = {}
200
- required = []
201
- for param_name, param in parameters.items():
195
+ properties: dict[str, Any] = {}
196
+ required: list[str] = []
197
+ for param_name, param in sig.parameters.items():
202
198
  if param_name == 'self':
203
199
  continue
204
- annotation = param.annotation
205
- if annotation is Parameter.empty:
206
- annotation = Any
207
- # Handle Literal types
208
- if get_origin(annotation) is Literal:
209
- enum_values = get_args(annotation)
210
- annotation = Literal.__getitem__(enum_values)
211
- # Handle default
200
+
201
+ ann = param.annotation
212
202
  default = param.default
203
+
204
+ # Handle Literal types
205
+ if get_origin(ann) is Literal:
206
+ enum_vals = list(get_args(ann))
207
+ schema_field: dict[str, Any] = {
208
+ "title": param_name.replace('_', ' ').title(),
209
+ "enum": enum_vals
210
+ }
211
+ # For Literals we'd typically not mark required if there's a default
212
+ else:
213
+ # map basic Python types to JSON Schema types
214
+ type_map = {
215
+ str: "string",
216
+ int: "integer",
217
+ float: "number",
218
+ bool: "boolean",
219
+ dict: "object",
220
+ list: "array",
221
+ Any: None
222
+ }
223
+ json_type = type_map.get(ann, None)
224
+ schema_field = {"title": param_name.replace('_', ' ').title()}
225
+ if json_type:
226
+ schema_field["type"] = json_type
227
+
228
+ # default vs required
213
229
  if default is Parameter.empty:
214
230
  required.append(param_name)
215
- field = Field(...)
231
+ # no default key
216
232
  else:
217
- field = Field(default=default)
218
- fields[param_name] = (annotation, field)
219
-
220
- # Create dynamic Pydantic model
221
- model = create_model(f"{name}_Input", **fields)
222
- schema = model.schema()
233
+ schema_field["default"] = default
234
+
235
+ properties[param_name] = schema_field
223
236
 
237
+ inputSchema = {
238
+ "title": f"{name}_Arguments",
239
+ "type": "object",
240
+ "properties": properties,
241
+ "required": required,
242
+ }
243
+
224
244
  tools.append({
225
- 'name': name,
226
- 'description': description,
227
- 'inputSchema': schema
245
+ "name": name,
246
+ "description": description,
247
+ "inputSchema": inputSchema
228
248
  })
249
+
229
250
  return tools
230
251
 
231
252
  async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
232
253
  """Call a tool by name with validated arguments."""
254
+ # 1) lookup
233
255
  if not hasattr(self, tool_name):
234
256
  raise ValueError(f"Tool {tool_name} not found")
235
257
  method = getattr(self, tool_name)
236
-
237
- # Validate arguments using the same schema as list_tools
258
+
259
+ # 2) build a minimal pydantic model for validation
238
260
  sig = signature(method)
239
- parameters = sig.parameters
240
-
241
- fields = {}
242
- for param_name, param in parameters.items():
261
+ model_fields: dict[str, tuple] = {}
262
+
263
+ for param_name, param in sig.parameters.items():
243
264
  if param_name == 'self':
244
265
  continue
245
- annotation = param.annotation
246
- if annotation is Parameter.empty:
247
- annotation = Any
248
- # Handle Literal
249
- if get_origin(annotation) is Literal:
250
- enum_values = get_args(annotation)
251
- annotation = Literal.__getitem__(enum_values)
266
+
267
+ # annotation
268
+ ann = param.annotation
269
+ if ann is Parameter.empty:
270
+ ann = Any
271
+
272
+ # default
252
273
  default = param.default
253
274
  if default is Parameter.empty:
254
- fields[param_name] = (annotation, Field(...))
275
+ # required field
276
+ model_fields[param_name] = (ann, Field(...))
255
277
  else:
256
- fields[param_name] = (annotation, Field(default=default))
257
-
258
- # Create validation model
259
- model = create_model(f"{tool_name}_ValidationModel", **fields)
260
- validated_args = model(**arguments).dict()
261
-
262
- # Call the method
278
+ # optional field: if default is None, widen annotation
279
+ if default is None and get_origin(ann) is not Union:
280
+ ann = Optional[ann]
281
+ model_fields[param_name] = (ann, Field(default=default))
282
+
283
+ ValidationModel = create_model(
284
+ f"{tool_name}_ValidationModel",
285
+ **model_fields
286
+ )
287
+
288
+ # 3) validate (will raise ValidationError on bad args)
289
+ try:
290
+ validated = ValidationModel(**arguments)
291
+ except ValidationError as e:
292
+ # re-raise or wrap as you like
293
+ raise ValueError(f"Invalid arguments for tool {tool_name}: {e}") from e
294
+
295
+ validated_args = validated.dict()
296
+
297
+ # 4) call
263
298
  if iscoroutinefunction(method):
264
299
  return await method(**validated_args)
265
300
  else:
266
- # Note: Synchronous methods (like subscribe) will block the event loop
267
301
  return method(**validated_args)
@@ -1,5 +1,6 @@
1
1
  # Library imports
2
2
  import re
3
+ import shlex
3
4
  from typing import Dict, Any
4
5
  from contextlib import AsyncExitStack
5
6
  from mcp import ClientSession, StdioServerParameters
@@ -40,6 +41,7 @@ class MCPClient:
40
41
  if server_script_path.startswith("@") or "/" not in server_script_path:
41
42
  # Assume it's an npm package
42
43
  is_javascript = True
44
+ args = shlex.split(server_script_path)
43
45
  command = "npx"
44
46
  else:
45
47
  # It's a file path
@@ -55,6 +57,7 @@ class MCPClient:
55
57
  args=args,
56
58
  env=None
57
59
  )
60
+ print(server_params)
58
61
 
59
62
  # Start the server
60
63
  stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
@@ -5,7 +5,7 @@ from typing import List, Dict, Any, Union, Literal
5
5
  # Local imports
6
6
  from smarta2a.client.mcp_client import MCPClient
7
7
  from smarta2a.client.a2a_client import A2AClient
8
- from smarta2a.utils.types import AgentCard
8
+ from smarta2a.utils.types import AgentCard, Tool
9
9
 
10
10
  class ToolsManager:
11
11
  """
@@ -21,16 +21,28 @@ class ToolsManager:
21
21
  mcp_client = await MCPClient.create(url)
22
22
  tools = await mcp_client.list_tools()
23
23
  for tool in tools:
24
- self.tools_list.append(tool)
25
- self.clients[tool.name] = mcp_client
24
+ # Generate key and ensure Tool type with key
25
+ key = f"mcp---{tool.name}"
26
+ validated_tool = Tool(
27
+ key=key,
28
+ **tool.model_dump() # Pydantic 2.x syntax (use .dict() for Pydantic 1.x)
29
+ )
30
+ self.tools_list.append(validated_tool)
31
+ self.clients[key] = mcp_client
26
32
 
27
33
  async def load_a2a_tools(self, agent_cards: List[AgentCard]) -> None:
28
34
  for agent_card in agent_cards:
29
35
  a2a_client = A2AClient(agent_card)
30
- tools = await a2a_client.list_tools()
31
- for tool in tools:
32
- self.tools_list.append(tool)
33
- self.clients[tool.name] = a2a_client
36
+ tools_list = await a2a_client.list_tools()
37
+ for tool_dict in tools_list:
38
+ # Generate key from agent URL and tool name
39
+ key = f"{agent_card.name}---{tool_dict['name']}"
40
+ validated_tool = Tool(
41
+ key=key,
42
+ **tool_dict
43
+ )
44
+ self.tools_list.append(validated_tool)
45
+ self.clients[key] = a2a_client
34
46
 
35
47
  def get_tools(self) -> List[Any]:
36
48
  return self.tools_list
@@ -42,20 +54,24 @@ class ToolsManager:
42
54
  schema = json.dumps(tool.inputSchema, indent=2) # Fix: use inputSchema
43
55
  if client_type == "mcp":
44
56
  lines.append(
45
- f"- **{tool.name}**: {tool.description}\n Parameters schema:\n ```json\n{schema}\n```"
57
+ f"- **{tool.key}**: {tool.description}\n Parameters schema:\n ```json\n{schema}\n```"
46
58
  )
47
59
  elif client_type == "a2a":
48
60
  lines.append(
49
- f"- **{tool.name}**: {tool.description}\n Parameters schema:\n ```json\n{schema}\n```"
61
+ f"- **{tool.key}**: {tool.description}\n Parameters schema:\n ```json\n{schema}\n```"
50
62
  )
51
63
 
52
64
  return "\n".join(lines)
53
65
 
54
- def get_client(self, tool_name: str) -> Any:
55
- return self.clients.get(tool_name)
66
+ def get_client(self, tool_key: str) -> Any:
67
+ return self.clients.get(tool_key)
56
68
 
57
- async def call_tool(self, tool_name: str, args: Dict[str, Any]) -> Any:
58
- client = self.get_client(tool_name)
69
+ async def call_tool(self, tool_key: str, args: Dict[str, Any]) -> Any:
70
+ client = self.get_client(tool_key)
71
+ tool_name = self._get_tool_name(tool_key)
59
72
  if not client:
60
73
  raise ValueError(f"Tool not found: {tool_name}")
61
- return await client.call_tool(tool_name, args)
74
+ return await client.call_tool(tool_name, args)
75
+
76
+ def _get_tool_name(self, tool_key: str) -> str:
77
+ return tool_key.split("---")[1]
@@ -0,0 +1,33 @@
1
+ # Imports
2
+ from dotenv import load_dotenv
3
+ import os
4
+ import uvicorn
5
+ from smarta2a.agent.a2a_agent import A2AAgent
6
+ from smarta2a.model_providers.openai_provider import OpenAIProvider
7
+
8
+
9
+
10
+ # Load environment variables from the .env file
11
+ load_dotenv()
12
+
13
+ # Fetch the value using os.getenv
14
+ api_key = os.getenv("OPENAI_API_KEY")
15
+
16
+
17
+ openai_provider = OpenAIProvider(
18
+ api_key=api_key,
19
+ model="gpt-4o-mini",
20
+ base_system_prompt="You are a cheerful assistant that specialises in helping with airbnb related queries",
21
+ mcp_server_urls_or_paths=["@openbnb/mcp-server-airbnb --ignore-robots-txt"]
22
+ )
23
+ # /Users/apple/.npm/_npx/1629930a2e066932/node_modules/@openbnb/mcp-server-airbnb/dist/index.js
24
+
25
+ # Create the agent
26
+ agent = A2AAgent(
27
+ name="openai_agent",
28
+ model_provider=openai_provider,
29
+ )
30
+
31
+ # Entry point
32
+ if __name__ == "__main__":
33
+ uvicorn.run(agent.get_app(), host="0.0.0.0", port=8002)
File without changes
@@ -0,0 +1,51 @@
1
+ # Imports
2
+ from dotenv import load_dotenv
3
+ import os
4
+ import uvicorn
5
+ from smarta2a.agent.a2a_agent import A2AAgent
6
+ from smarta2a.model_providers.openai_provider import OpenAIProvider
7
+ from smarta2a.utils.types import AgentCard, AgentCapabilities, AgentSkill
8
+
9
+
10
+ # Load environment variables from the .env file
11
+ load_dotenv()
12
+
13
+ # Fetch the value using os.getenv
14
+ api_key = os.getenv("OPENAI_API_KEY")
15
+
16
+ weather_agent_card = AgentCard(
17
+ name="weather_agent",
18
+ description="A weather agent that can help with weather related queries",
19
+ version="0.1.0",
20
+ url="http://localhost:8000",
21
+ capabilities=AgentCapabilities(),
22
+ skills=[AgentSkill(id="weather_forecasting", name="Weather Forecasting", description="Can get weather forecast for a given latitude and longitude"),
23
+ AgentSkill(id="weather_alerts", name="Weather Alerts", description="Can get weather alerts for a US state")]
24
+ )
25
+
26
+ airbnb_agent_card = AgentCard(
27
+ name="airbnb_agent",
28
+ description="An airbnb agent that can help with airbnb related queries",
29
+ version="0.1.0",
30
+ url="http://localhost:8002",
31
+ capabilities=AgentCapabilities(),
32
+ skills=[AgentSkill(id="search_listings", name="Search listings", description="Search for Airbnb listings by location, dates, guests, and more"),
33
+ AgentSkill(id="get_listing_details", name="Get listing details", description="Get detailed information about a specific Airbnb listing by listing id, dates, guests, and more")]
34
+ )
35
+
36
+
37
+ openai_provider = OpenAIProvider(
38
+ api_key=api_key,
39
+ model="gpt-4o-mini",
40
+ agent_cards=[weather_agent_card, airbnb_agent_card]
41
+ )
42
+
43
+ # Create the agent
44
+ agent = A2AAgent(
45
+ name="openai_agent",
46
+ model_provider=openai_provider,
47
+ )
48
+
49
+ # Entry point
50
+ if __name__ == "__main__":
51
+ uvicorn.run(agent.get_app(), host="0.0.0.0", port=8001)
File without changes
@@ -2,7 +2,6 @@
2
2
  from dotenv import load_dotenv
3
3
  import os
4
4
  import uvicorn
5
- import asyncio
6
5
  from smarta2a.agent.a2a_agent import A2AAgent
7
6
  from smarta2a.model_providers.openai_provider import OpenAIProvider
8
7
 
@@ -18,6 +17,7 @@ api_key = os.getenv("OPENAI_API_KEY")
18
17
  openai_provider = OpenAIProvider(
19
18
  api_key=api_key,
20
19
  model="gpt-4o-mini",
20
+ base_system_prompt="You are a cheerful assistant that specialises in helping with weather related queries",
21
21
  mcp_server_urls_or_paths=["/Users/apple/Desktop/Code/weather/weather.py"],
22
22
  )
23
23
 
@@ -125,7 +125,7 @@ class OpenAIProvider(BaseLLMProvider):
125
125
  openai_tools.append({
126
126
  "type": "function",
127
127
  "function": {
128
- "name": tool.name,
128
+ "name": tool.key,
129
129
  "description": tool.description,
130
130
  "parameters": tool.inputSchema
131
131
  }
@@ -140,7 +140,7 @@ class OpenAIProvider(BaseLLMProvider):
140
140
  # Ensure messages are Message objects
141
141
  messages = [msg if isinstance(msg, Message) else Message(**msg) for msg in messages]
142
142
  converted_messages = self._convert_messages(messages)
143
- max_iterations = 10
143
+ max_iterations = 30
144
144
 
145
145
  for iteration in range(max_iterations):
146
146
  response = await self.client.chat.completions.create(
@@ -161,7 +161,7 @@ class OpenAIProvider(BaseLLMProvider):
161
161
  args_raw = tc.function.arguments
162
162
  else:
163
163
  return message.content
164
-
164
+ print(message)
165
165
  # Append the assistant's intent
166
166
  converted_messages.append({
167
167
  "role": "assistant",
smarta2a/utils/types.py CHANGED
@@ -482,3 +482,9 @@ class StateData(BaseModel):
482
482
  sessionId: str
483
483
  history: List[Message]
484
484
  metadata: Dict[str, Any]
485
+
486
+ class Tool(BaseModel):
487
+ key: str
488
+ name: str
489
+ description: str
490
+ inputSchema: Dict[str, Any]
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.4
2
+ Name: smarta2a
3
+ Version: 0.3.1
4
+ Summary: a Python framework that helps you build servers and AI agents that communicate using the A2A protocol
5
+ Project-URL: Homepage, https://github.com/siddharthsma/smarta2a
6
+ Project-URL: Bug Tracker, https://github.com/siddharthsma/smarta2a/issues
7
+ Author-email: Siddharth Ambegaonkar <siddharthsma@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: anyio>=4.9.0
14
+ Requires-Dist: fastapi>=0.115.12
15
+ Requires-Dist: httpx>=0.28.1
16
+ Requires-Dist: mcp>=0.1.0
17
+ Requires-Dist: openai>=1.0.0
18
+ Requires-Dist: pydantic>=2.11.3
19
+ Requires-Dist: sse-starlette>=2.2.1
20
+ Requires-Dist: starlette>=0.46.2
21
+ Requires-Dist: typing-extensions>=4.13.2
22
+ Requires-Dist: uvicorn>=0.34.1
23
+ Description-Content-Type: text/markdown
24
+
25
+ <picture>
26
+ <img alt="SmartA2A Logo" src="documentation/smarta2a_docs/smarta2a_small_banner.png" width="100%">
27
+ </picture>
28
+
29
+ <div>
30
+ <br>
31
+ </div>
32
+
33
+ ![PyPI](https://img.shields.io/pypi/v/Smarta2a.svg)
34
+ ![Downloads](https://static.pepy.tech/badge/Smarta2a)
35
+ ![GitHub Repo stars](https://img.shields.io/github/stars/siddharthsma/smarta2a?style=social)
36
+
37
+ **SmartA2A** is a Python framework that helps you build servers and AI agents that communicate using the [A2A (Agent2Agent) protocol](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/). A2A defines a common language that enables agents to exchange information and collaborate effectively across systems.
38
+
39
+ SmartA2A abstracts the complexities of the protocol, so you can focus on what matters—your agent's logic and behavior. It supports two primary use cases:
40
+
41
+ ### ✅ 1. Build A2A-Compliant Servers
42
+
43
+ Use SmartA2A to expose key A2A methods through a simple decorator-based API. This allows you to:
44
+
45
+ - Build compliant server endpoints quickly
46
+ - Integrate your own agent logic using any framework (e.g., LangGraph, Google ADK, or custom code)
47
+ - Stay fully aligned with the A2A spec without boilerplate
48
+
49
+ ### 🤖 2. Create Full-Fledged Agents with Minimal Code
50
+
51
+ SmartA2A can also be used to create standalone agents that:
52
+
53
+ - Speak the A2A protocol out of the box
54
+ - Collaborate with other agents seamlessly
55
+ - Connect to any MCP server you choose
56
+ - Require only a few lines of setup
57
+
58
+ SmartA2A makes it easy to build interoperable, communication-ready AI systems—whether you're extending existing frameworks or starting from scratch.
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install -U smarta2a
66
+ ```
67
+
68
+ ## Examples
69
+
70
+ There are 3 cool examples for you to try out - eacher cooler than the next!
71
+
72
+ 1. The first is a simple Echo server implementation that demonstrates the FastAPI'esk style in which you can create a A2A compliant server. If you're using another Agent framework like LangGraph you can easily use it it tandem with this server.
73
+
74
+ 2. This is where things start to get real interesting - you can easily create an LLM and MCP powered Agent in just a few lines of code. In this case a US weather agent.
75
+
76
+ 3. If you get here, this is where things things start to get really awesome. You will create a multi-agent system where a delegator agent delelgates tasks to the weather agent you created before and to an Airbnb agent. You can now plan your stay based on the weather!
77
+
78
+ Note that you can find all the examples in the examples folder.
79
+
80
+ ### Simple Echo Server Implementation
81
+
82
+ Here's a simple example of an echo server that demonstrates the core features of SmartA2A:
83
+
84
+ ```python
85
+ from smarta2a.server import SmartA2A
86
+ from smarta2a.utils.types import A2AResponse, TaskStatus, TaskState, TextPart, FileContent, FilePart
87
+ from smarta2a.state_stores.inmemory_state_store import InMemoryStateStore
88
+
89
+ # Initialize the server with an in-memory state store
90
+ state_store = InMemoryStateStore()
91
+ app = SmartA2A("EchoServer", state_store=state_store)
92
+
93
+ @app.on_send_task()
94
+ async def handle_task(request, state):
95
+ """Echo the input text back as a completed task. If you were using something like LangGraph you could call your graph.invoke method here """
96
+ input_text = request.content[0].text
97
+ return f"Response to task: {input_text}"
98
+
99
+ @app.on_send_subscribe_task()
100
+ async def handle_subscribe_task(request, state):
101
+ """Subscribe to the task and stream multiple responses"""
102
+ input_text = request.content[0].text
103
+ yield f"First response to the task: {input_text}"
104
+ yield f"Second response to the task: {input_text}"
105
+ yield f"Third response to the task: {input_text}"
106
+
107
+ @app.task_get()
108
+ def handle_get_task(request):
109
+ """Get the task status"""
110
+ return f"Task: {request.id}"
111
+
112
+ @app.task_cancel()
113
+ def handle_cancel_task(request):
114
+ """Cancel the task"""
115
+ return f"Task cancelled: {request.id}"
116
+ ```
117
+
118
+ This example shows:
119
+ - Setting up a basic A2A server with state management
120
+ - Handling synchronous task requests with text and file responses
121
+ - Implementing streaming responses for subscription tasks
122
+ - Basic task management (get and cancel operations)
123
+
124
+ To run the echo server:
125
+ ```bash
126
+ uvicorn path.to.echo_server.main.py:app
127
+ ```
128
+
129
+ You can test the echo server using curl commands:
130
+
131
+ ```bash
132
+ # Test sending a task
133
+ curl -X POST http://localhost:8000/ \
134
+ -H "Content-Type: application/json" \
135
+ -d '{
136
+ "jsonrpc": "2.0",
137
+ "id": "1",
138
+ "method": "tasks/send",
139
+ "params": {
140
+ "id": "task1",
141
+ "message": {
142
+ "role": "user",
143
+ "parts": [{"type": "text", "text": "Hello, echo server!"}]
144
+ }
145
+ }
146
+ }'
147
+
148
+ # Test subscribing to a task
149
+ curl -X POST http://localhost:8000/ \
150
+ -H "Content-Type: application/json" \
151
+ -d '{
152
+ "jsonrpc": "2.0",
153
+ "id": "2",
154
+ "method": "tasks/sendSubscribe",
155
+ "params": {
156
+ "id": "task2",
157
+ "message": {
158
+ "role": "user",
159
+ "parts": [{"type": "text", "text": "Hello, streaming echo!"}]
160
+ }
161
+ }
162
+ }'
163
+ ```
164
+
165
+ ### Weather Agent Example
166
+
167
+ Here's an example of a weather agent that uses OpenAI's GPT model to handle weather-related queries. Note that you will need to setup the weather MCP server as a pre-requisite as specified by the [MCP Quick-start server tutorial](https://modelcontextprotocol.io/quickstart/server). You will also need to add your OpenAI API key to the .env file.
168
+
169
+ ```python
170
+ from dotenv import load_dotenv
171
+ import os
172
+ import uvicorn
173
+ from smarta2a.agent.a2a_agent import A2AAgent
174
+ from smarta2a.model_providers.openai_provider import OpenAIProvider
175
+
176
+ # Load environment variables from the .env file
177
+ load_dotenv()
178
+
179
+ # Initialize OpenAI provider with weather-specific configuration
180
+ openai_provider = OpenAIProvider(
181
+ api_key=os.getenv("OPENAI_API_KEY"),
182
+ model="gpt-4o-mini",
183
+ base_system_prompt="You are a cheerful assistant that specialises in helping with weather related queries",
184
+ mcp_server_urls_or_paths=["/path/to/weather.py"], # Path to your weather service
185
+ )
186
+
187
+ # Create and run the agent
188
+ agent = A2AAgent(
189
+ name="openai_agent",
190
+ model_provider=openai_provider,
191
+ )
192
+
193
+ if __name__ == "__main__":
194
+ uvicorn.run(agent.get_app(), host="0.0.0.0", port=8000)
195
+ ```
196
+
197
+ This example demonstrates:
198
+ - Setting up an AI agent with OpenAI integration
199
+ - Configuring a specialized system prompt for weather queries
200
+ - Connecting to external weather services via MCP
201
+ - Running the agent as a standalone server
202
+
203
+ To run the weather agent:
204
+ ```bash
205
+ python path/to/weather_agent/main.py
206
+ ```
207
+
208
+ To test the weather agent, ensure:
209
+ 1. Your weather MCP server is running
210
+ 2. Your OpenAI API key is set in the .env file
211
+ 3. The agent is running as shown above
212
+
213
+ Then test it with curl:
214
+
215
+ ```bash
216
+ # Test weather query
217
+ curl -X POST http://localhost:8000/ \
218
+ -H "Content-Type: application/json" \
219
+ -d '{
220
+ "jsonrpc": "2.0",
221
+ "id": "1",
222
+ "method": "tasks/send",
223
+ "params": {
224
+ "id": "weather1",
225
+ "message": {
226
+ "role": "user",
227
+ "parts": [{"type": "text", "text": "What is the weather in New York?"}]
228
+ }
229
+ }
230
+ }'
231
+ ```
232
+
233
+ ### Multi-agent Weather-Aware Stay Planner
234
+
235
+ This example demonstrates how to create a multi-agent system where a delegator agent coordinates between a weather agent and an Airbnb agent to help plan stays based on weather conditions. You'll need to run three agents:
236
+
237
+ 1. The weather agent (from the previous example)
238
+ 2. An Airbnb agent that connects to the Airbnb MCP server
239
+ 3. A delegator agent that coordinates between them
240
+
241
+ First, let's create the Airbnb agent:
242
+
243
+ ```python
244
+ from dotenv import load_dotenv
245
+ import os
246
+ import uvicorn
247
+ from smarta2a.agent.a2a_agent import A2AAgent
248
+ from smarta2a.model_providers.openai_provider import OpenAIProvider
249
+
250
+ # Load environment variables
251
+ load_dotenv()
252
+
253
+ # Initialize OpenAI provider with Airbnb-specific configuration
254
+ openai_provider = OpenAIProvider(
255
+ api_key=os.getenv("OPENAI_API_KEY"),
256
+ model="gpt-4o-mini",
257
+ base_system_prompt="You are a cheerful assistant that specialises in helping with Airbnb related queries",
258
+ mcp_server_urls_or_paths=["@openbnb/mcp-server-airbnb --ignore-robots-txt"]
259
+ )
260
+
261
+ # Create and run the Airbnb agent
262
+ agent = A2AAgent(
263
+ name="airbnb_agent",
264
+ model_provider=openai_provider,
265
+ )
266
+
267
+ if __name__ == "__main__":
268
+ uvicorn.run(agent.get_app(), host="0.0.0.0", port=8002)
269
+ ```
270
+
271
+ Now, let's create the delegator agent that coordinates between the weather and Airbnb agents:
272
+
273
+ ```python
274
+ from dotenv import load_dotenv
275
+ import os
276
+ import uvicorn
277
+ from smarta2a.agent.a2a_agent import A2AAgent
278
+ from smarta2a.model_providers.openai_provider import OpenAIProvider
279
+ from smarta2a.utils.types import AgentCard, AgentCapabilities, AgentSkill
280
+
281
+ # Load environment variables
282
+ load_dotenv()
283
+
284
+ # Define the weather agent's capabilities
285
+ weather_agent_card = AgentCard(
286
+ name="weather_agent",
287
+ description="A weather agent that can help with weather related queries",
288
+ version="0.1.0",
289
+ url="http://localhost:8000",
290
+ capabilities=AgentCapabilities(),
291
+ skills=[
292
+ AgentSkill(
293
+ id="weather_forecasting",
294
+ name="Weather Forecasting",
295
+ description="Can get weather forecast for a given location"
296
+ ),
297
+ AgentSkill(
298
+ id="weather_alerts",
299
+ name="Weather Alerts",
300
+ description="Can get weather alerts for a US state"
301
+ )
302
+ ]
303
+ )
304
+
305
+ # Define the Airbnb agent's capabilities
306
+ airbnb_agent_card = AgentCard(
307
+ name="airbnb_agent",
308
+ description="An Airbnb agent that can help with accommodation queries",
309
+ version="0.1.0",
310
+ url="http://localhost:8002",
311
+ capabilities=AgentCapabilities(),
312
+ skills=[
313
+ AgentSkill(
314
+ id="search_listings",
315
+ name="Search Listings",
316
+ description="Search for Airbnb listings by location, dates, and guests"
317
+ ),
318
+ AgentSkill(
319
+ id="get_listing_details",
320
+ name="Get Listing Details",
321
+ description="Get detailed information about specific Airbnb listings"
322
+ )
323
+ ]
324
+ )
325
+
326
+ # Initialize OpenAI provider with both agent cards
327
+ openai_provider = OpenAIProvider(
328
+ api_key=os.getenv("OPENAI_API_KEY"),
329
+ model="gpt-4o-mini",
330
+ agent_cards=[weather_agent_card, airbnb_agent_card]
331
+ )
332
+
333
+ # Create and run the delegator agent
334
+ agent = A2AAgent(
335
+ name="delegator_agent",
336
+ model_provider=openai_provider,
337
+ )
338
+
339
+ if __name__ == "__main__":
340
+ uvicorn.run(agent.get_app(), host="0.0.0.0", port=8001)
341
+ ```
342
+
343
+ To run the multi-agent system:
344
+
345
+ 1. Start the weather agent:
346
+ ```bash
347
+ python path/to/weather_agent/main.py
348
+ ```
349
+
350
+ 2. Start the Airbnb agent: No need to do anything - just ensure that node and npx are installed on your machine
351
+
352
+
353
+ 3. Start the delegator agent:
354
+ ```bash
355
+ python path/to/delegator_agent/main.py
356
+ ```
357
+
358
+ You can then test the system with a curl command:
359
+
360
+ ```bash
361
+ # Test multi-agent planning
362
+ curl -X POST http://localhost:8001/ \
363
+ -H "Content-Type: application/json" \
364
+ -d '{
365
+ "jsonrpc": "2.0",
366
+ "id": "1",
367
+ "method": "tasks/send",
368
+ "params": {
369
+ "id": "plan1",
370
+ "message": {
371
+ "role": "user",
372
+ "parts": [{"type": "text", "text": "I want to go someplace sunny in California. Just me and my wife. Can you check the weather forecast in the major cities and give me some airbnb recommendations that are appropriate. I want to stay from the 7th of May and checking out on the 10th of May. I'm not fussed about the budget."}]
373
+ }
374
+ }
375
+ }'
376
+ ```
377
+
378
+ This example demonstrates:
379
+ - Creating specialized agents for different domains (weather and Airbnb)
380
+ - Defining agent capabilities and skills using AgentCard
381
+ - Setting up a delegator agent that can coordinate between multiple agents
382
+ - Running multiple agents on different ports
383
+ - Making complex queries that require coordination between agents
384
+
385
+ ---
386
+
387
+ ## Roadmap
388
+
389
+ There's still a lot more to come ...
390
+
391
+ - Documentation site with tutorials, explanation of concepts, blog and a contribution guide
392
+ - Support for more LLM providers
393
+ - Support for more memory types
394
+ - Authorization and observability framework built-in
395
+ - Many more !
396
+
397
+ ## License
398
+
399
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -3,21 +3,25 @@ smarta2a/agent/a2a_agent.py,sha256=zk6ZZtq4HrnKMDuZKnQPeKploV1IGXSZ1WmVPdIZAeY,1
3
3
  smarta2a/agent/a2a_mcp_server.py,sha256=X_mxkgYgCA_dSNtCvs0rSlOoWYc-8d3Qyxv0e-a7NKY,1015
4
4
  smarta2a/archive/smart_mcp_client.py,sha256=NjyMR8xvrhy0-iFamj2XQnKp5qzkkFya7EQY4Kzl5dw,2574
5
5
  smarta2a/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- smarta2a/client/a2a_client.py,sha256=Ulrfl_vaGsig5UmPrkPfKJs-I7f0m8Bs5U7g0kkvhMM,9895
7
- smarta2a/client/mcp_client.py,sha256=ANjEzQFXEhKGQtNC9AjLI_IFtjv5twCCQhcB0TrwUpc,3534
8
- smarta2a/client/tools_manager.py,sha256=IvkjNoa2vSUP3l2Bz87EGH9My30zErpD2HE-KPyThVI,2273
6
+ smarta2a/client/a2a_client.py,sha256=5WIincf_6Nqh61_x3f0NYUSLXlGztlnzUpo0m6ch_j0,11038
7
+ smarta2a/client/mcp_client.py,sha256=PM4D1CgOycxK5kJEJTGpKq0eXFjZ69-2720TuRUkyGc,3627
8
+ smarta2a/client/tools_manager.py,sha256=JDD2lqcDe70JVHlPoRU5oHzUYcZUkH5QzjtEkWYfrrA,2970
9
9
  smarta2a/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  smarta2a/examples/echo_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  smarta2a/examples/echo_server/curl.txt,sha256=MYBbAgrU3PeCsksrrBc0xgaTD5Dro7xiTsCUz8Ocvt8,247
12
12
  smarta2a/examples/echo_server/main.py,sha256=i_XLQbRAv6XxkJX0vLV3CxHNANG41yAxgP-V-b4Iysc,1261
13
- smarta2a/examples/openai_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- smarta2a/examples/openai_agent/main.py,sha256=TCKqm2YSAdXtuFBhHNbqC-JKllXEJazVhmibqPZZ3FY,699
13
+ smarta2a/examples/openai_airbnb_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ smarta2a/examples/openai_airbnb_agent/main.py,sha256=CaIYi-LQD32Rm_BsXI7sLfFqx_kMpl0k8zCayFB4068,892
15
+ smarta2a/examples/openai_delegator_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ smarta2a/examples/openai_delegator_agent/main.py,sha256=zP6YOMxTBLfMtrZOPwPLQhSVodULbDkCCAy4pB4ltto,1805
17
+ smarta2a/examples/openai_weather_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ smarta2a/examples/openai_weather_agent/main.py,sha256=o8QqLu9JC3zRXRPGWhYdd7mVl3ooys5UwkPCPv2JHmQ,796
15
19
  smarta2a/history_update_strategies/__init__.py,sha256=x5WtiE9rG5ze8d8hA6E6wJOciBhWHa_ZgGgoIAZcXEQ,213
16
20
  smarta2a/history_update_strategies/append_strategy.py,sha256=j7Qbhs69Wwr-HBLB8GJ3-mEPaBSHiBV2xz9ZZi86k2w,312
17
21
  smarta2a/history_update_strategies/history_update_strategy.py,sha256=n2sfIGu8ztKI7gJTwRX26m4tZr28B8Xdhrk6RlBFlI8,373
18
22
  smarta2a/model_providers/__init__.py,sha256=2FhblAUiwG9Xv27yEpuuz0VrnIZ-rlpgIuPwm-UIX5U,147
19
23
  smarta2a/model_providers/base_llm_provider.py,sha256=6QjTUjYEnvHZji4_VWZz6CvLYKLyutxRUfIeH3seQg4,424
20
- smarta2a/model_providers/openai_provider.py,sha256=Xk2d5L9PbOs4NRjWMxDVdTNqy88YTeeMpW6Wox_Rffw,10845
24
+ smarta2a/model_providers/openai_provider.py,sha256=yTiBVJfEFOTSQsVj8mNx5qVZWvOvslYvw7TQ0tLLf7U,10870
21
25
  smarta2a/server/__init__.py,sha256=f2X454Ll4vJc02V4JLJHTN-h8u0TBm4d_FkiO4t686U,53
22
26
  smarta2a/server/handler_registry.py,sha256=OVRG5dTvxB7qUNXgsqWxVNxIyRljUShSYxb1gtbi5XM,820
23
27
  smarta2a/server/server.py,sha256=WJGQVOprF2Hh_036eW4GJrRr3fLC0S_B3dAfONCgZrs,32180
@@ -31,8 +35,8 @@ smarta2a/utils/__init__.py,sha256=5db5VgDGgbMUGEF-xuyaC3qrgRQkUE9WAITkFSiNqSA,70
31
35
  smarta2a/utils/prompt_helpers.py,sha256=jLETieoeBJLQXcGzwFeoKT5b2pS4tJ5770lPLImtKLo,1439
32
36
  smarta2a/utils/task_builder.py,sha256=wqSyfVHNTaXuGESu09dhlaDi7D007gcN3-8tH-nPQ40,5159
33
37
  smarta2a/utils/task_request_builder.py,sha256=6cOGOqj2Rg43xWM03GRJQzlIZHBptsMCJRp7oD-TDAQ,3362
34
- smarta2a/utils/types.py,sha256=ASHXmgJ634s2Qy65YSATEe7lqweyWMHarcXMDOIx4sY,13086
35
- smarta2a-0.2.5.dist-info/METADATA,sha256=ECE4bZ4faHmvA_VKeavBvNp8MumUltkclMN1NATyP8k,2726
36
- smarta2a-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- smarta2a-0.2.5.dist-info/licenses/LICENSE,sha256=ECMEVHuFkvpEmH-_A9HSxs_UnnsUqpCkiAYNHPCf2z0,1078
38
- smarta2a-0.2.5.dist-info/RECORD,,
38
+ smarta2a/utils/types.py,sha256=h4bQw_RmeJCDq022aEd9XJZhNpJOydhV40tqERvLeQE,13190
39
+ smarta2a-0.3.1.dist-info/METADATA,sha256=Z2Z6w1tr9o6bkmxplJ4OeG5CZ7RAk9RIjkf1CYnuwfY,13005
40
+ smarta2a-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
+ smarta2a-0.3.1.dist-info/licenses/LICENSE,sha256=ECMEVHuFkvpEmH-_A9HSxs_UnnsUqpCkiAYNHPCf2z0,1078
42
+ smarta2a-0.3.1.dist-info/RECORD,,
@@ -1,103 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: smarta2a
3
- Version: 0.2.5
4
- Summary: A simple Python framework (built on top of FastAPI) for creating Agents following Google's Agent2Agent protocol
5
- Project-URL: Homepage, https://github.com/siddharthsma/smarta2a
6
- Project-URL: Bug Tracker, https://github.com/siddharthsma/smarta2a/issues
7
- Author-email: Siddharth Ambegaonkar <siddharthsma@gmail.com>
8
- License-File: LICENSE
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: >=3.8
13
- Requires-Dist: anyio>=4.9.0
14
- Requires-Dist: fastapi>=0.115.12
15
- Requires-Dist: httpx>=0.28.1
16
- Requires-Dist: mcp>=0.1.0
17
- Requires-Dist: openai>=1.0.0
18
- Requires-Dist: pydantic>=2.11.3
19
- Requires-Dist: sse-starlette>=2.2.1
20
- Requires-Dist: starlette>=0.46.2
21
- Requires-Dist: typing-extensions>=4.13.2
22
- Requires-Dist: uvicorn>=0.34.1
23
- Description-Content-Type: text/markdown
24
-
25
- # SmartA2A
26
-
27
- A Python package for creating a server following Google's Agent2Agent protocol
28
-
29
- ## Features
30
-
31
- ✅ **Full A2A Protocol Compliance** - Implements all required endpoints and response formats
32
-
33
- ⚡ **Decorator-Driven Development** - Rapid endpoint configuration with type safety
34
-
35
- 🧩 **Automatic Protocol Conversion** - Simple returns become valid A2A responses
36
-
37
- 🔀 **Flexible Response Handling** - Support for Tasks, Artifacts, Streaming, and raw protocol types if needed!
38
-
39
- 🛡️ **Built-in Validation** - Automatic Pydantic validation of A2A schemas
40
-
41
- ⚡ **Single File Setup** - Get compliant in <10 lines of code
42
-
43
- 🌍 **Production Ready** - CORS, async support, and error handling included
44
-
45
- ## Installation
46
-
47
- ```bash
48
- pip install smarta2a
49
- ```
50
-
51
- ## Simple Echo Server Implementation
52
-
53
- ```python
54
- from smarta2a.server import SmartA2A
55
-
56
- app = SmartA2A("EchoServer")
57
-
58
- @app.on_send_task()
59
- def handle_task(request):
60
- """Echo the input text back as a completed task"""
61
- input_text = request.content[0].text
62
- return f"Echo: {input_text}"
63
-
64
- if __name__ == "__main__":
65
- app.run()
66
- ```
67
-
68
- Automatically contructs the response:
69
-
70
- ```json
71
- {
72
- "jsonrpc": "2.0",
73
- "id": "test",
74
- "result": {
75
- "id": "echo-task",
76
- "status": {"state": "completed"},
77
- "artifacts": [{
78
- "parts": [{"type": "text", "text": "Echo: Hello!"}]
79
- }]
80
- }
81
- }
82
- ```
83
-
84
- ## Development
85
-
86
- To set up the development environment:
87
-
88
- ```bash
89
- # Clone the repository
90
- git clone https://github.com/siddharthsma/smarta2a.git
91
- cd smarta2a
92
-
93
- # Create and activate virtual environment
94
- python -m venv venv
95
- source venv/bin/activate # On Windows: venv\Scripts\activate
96
-
97
- # Install development dependencies
98
- pip install -e ".[dev]"
99
- ```
100
-
101
- ## License
102
-
103
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.