smarta2a 0.3.1__py3-none-any.whl → 0.4.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.
- smarta2a/agent/a2a_agent.py +25 -15
- smarta2a/agent/a2a_human.py +56 -0
- smarta2a/archive/smart_mcp_client.py +47 -0
- smarta2a/archive/subscription_service.py +85 -0
- smarta2a/{server → archive}/task_service.py +17 -8
- smarta2a/client/a2a_client.py +33 -6
- smarta2a/history_update_strategies/rolling_window_strategy.py +16 -0
- smarta2a/model_providers/__init__.py +1 -1
- smarta2a/model_providers/base_llm_provider.py +3 -3
- smarta2a/model_providers/openai_provider.py +126 -89
- smarta2a/server/json_rpc_request_processor.py +130 -0
- smarta2a/server/nats_client.py +49 -0
- smarta2a/server/request_handler.py +667 -0
- smarta2a/server/send_task_handler.py +174 -0
- smarta2a/server/server.py +124 -726
- smarta2a/server/state_manager.py +171 -20
- smarta2a/server/webhook_request_processor.py +112 -0
- smarta2a/state_stores/base_state_store.py +3 -3
- smarta2a/state_stores/inmemory_state_store.py +21 -7
- smarta2a/utils/agent_discovery_manager.py +121 -0
- smarta2a/utils/prompt_helpers.py +1 -1
- smarta2a/{client → utils}/tools_manager.py +39 -8
- smarta2a/utils/types.py +17 -3
- {smarta2a-0.3.1.dist-info → smarta2a-0.4.1.dist-info}/METADATA +7 -4
- smarta2a-0.4.1.dist-info/RECORD +40 -0
- smarta2a-0.4.1.dist-info/licenses/LICENSE +35 -0
- smarta2a/examples/__init__.py +0 -0
- smarta2a/examples/echo_server/__init__.py +0 -0
- smarta2a/examples/echo_server/curl.txt +0 -1
- smarta2a/examples/echo_server/main.py +0 -39
- smarta2a/examples/openai_airbnb_agent/__init__.py +0 -0
- smarta2a/examples/openai_airbnb_agent/main.py +0 -33
- smarta2a/examples/openai_delegator_agent/__init__.py +0 -0
- smarta2a/examples/openai_delegator_agent/main.py +0 -51
- smarta2a/examples/openai_weather_agent/__init__.py +0 -0
- smarta2a/examples/openai_weather_agent/main.py +0 -32
- smarta2a/server/subscription_service.py +0 -109
- smarta2a-0.3.1.dist-info/RECORD +0 -42
- smarta2a-0.3.1.dist-info/licenses/LICENSE +0 -21
- {smarta2a-0.3.1.dist-info → smarta2a-0.4.1.dist-info}/WHEEL +0 -0
smarta2a/server/state_manager.py
CHANGED
@@ -1,34 +1,185 @@
|
|
1
1
|
# Library imports
|
2
|
-
from typing import Optional, Dict, Any
|
3
|
-
from uuid import uuid4
|
2
|
+
from typing import Optional, Dict, Any, List
|
4
3
|
|
5
4
|
# Local imports
|
6
5
|
from smarta2a.state_stores.base_state_store import BaseStateStore
|
7
6
|
from smarta2a.history_update_strategies.history_update_strategy import HistoryUpdateStrategy
|
8
|
-
from smarta2a.utils.types import Message, StateData
|
7
|
+
from smarta2a.utils.types import Message, StateData, Task, TaskStatus, TaskState, PushNotificationConfig, Part
|
8
|
+
from smarta2a.server.nats_client import NATSClient
|
9
9
|
|
10
10
|
class StateManager:
|
11
|
-
def __init__(self,
|
12
|
-
self.
|
11
|
+
def __init__(self, state_store: BaseStateStore, history_strategy: HistoryUpdateStrategy):
|
12
|
+
self.state_store = state_store
|
13
13
|
self.strategy = history_strategy
|
14
|
+
self.nats_client = NATSClient(server_url="nats://localhost:4222")
|
15
|
+
|
16
|
+
async def load(self):
|
17
|
+
await self.nats_client.connect()
|
18
|
+
|
19
|
+
|
20
|
+
async def unload(self):
|
21
|
+
await self.nats_client.close()
|
22
|
+
|
23
|
+
def _initialize_empty_state(
|
24
|
+
self,
|
25
|
+
task_id: str,
|
26
|
+
session_id: str,
|
27
|
+
push_notification_config: Optional[PushNotificationConfig] = None
|
28
|
+
) -> StateData:
|
29
|
+
"""
|
30
|
+
Build a fresh StateData and persist it.
|
31
|
+
"""
|
32
|
+
initial_task = Task(
|
33
|
+
id=task_id,
|
34
|
+
sessionId=session_id,
|
35
|
+
status=TaskStatus(state=TaskState.WORKING),
|
36
|
+
artifacts=[],
|
37
|
+
history=[],
|
38
|
+
metadata={}
|
39
|
+
)
|
40
|
+
state = StateData(
|
41
|
+
task_id=task_id,
|
42
|
+
task=initial_task,
|
43
|
+
context_history=[],
|
44
|
+
push_notification_config=push_notification_config
|
45
|
+
)
|
46
|
+
self.state_store.initialize_state(state)
|
47
|
+
return state
|
48
|
+
|
49
|
+
async def get_or_create_and_update_state(
|
50
|
+
self,
|
51
|
+
task_id: str,
|
52
|
+
session_id: str,
|
53
|
+
message: Message,
|
54
|
+
metadata: Optional[Dict[str, Any]] = None,
|
55
|
+
push_notification_config: Optional[PushNotificationConfig] = None
|
56
|
+
) -> StateData:
|
57
|
+
"""
|
58
|
+
Fetch existing StateData, or initialize & persist a new one.
|
59
|
+
"""
|
60
|
+
existing_state = self.state_store.get_state(task_id)
|
61
|
+
if not existing_state:
|
62
|
+
latest_state = self._initialize_empty_state(
|
63
|
+
task_id, session_id, push_notification_config
|
64
|
+
)
|
65
|
+
else:
|
66
|
+
latest_state = existing_state.copy()
|
67
|
+
|
68
|
+
latest_state.task.history.append(message)
|
69
|
+
latest_state.context_history = self.strategy.update_history(
|
70
|
+
existing_history=latest_state.context_history,
|
71
|
+
new_messages=[message]
|
72
|
+
)
|
73
|
+
latest_state.task.metadata = metadata
|
14
74
|
|
15
|
-
|
16
|
-
sid = session_id or str(uuid4())
|
17
|
-
if not self.store:
|
18
|
-
return StateData(sessionId=sid, history=[message], metadata=metadata or {})
|
19
|
-
existing = self.store.get_state(sid) or StateData(sessionId=sid, history=[], metadata={})
|
20
|
-
existing.history.append(message)
|
21
|
-
existing.metadata = {**(existing.metadata or {}), **(metadata or {})}
|
22
|
-
self.store.update_state(sid, existing)
|
23
|
-
return existing
|
75
|
+
await self.update_state(latest_state)
|
24
76
|
|
25
|
-
|
26
|
-
|
27
|
-
|
77
|
+
return latest_state
|
78
|
+
|
79
|
+
def get_and_update_state_from_webhook(self, task_id: str, result: Task) -> StateData:
|
80
|
+
"""
|
81
|
+
Update existing state with webhook result data, including:
|
82
|
+
- Merges task history from result
|
83
|
+
- Extracts messages from artifacts' parts
|
84
|
+
- Updates context history using strategy
|
85
|
+
- Merges artifacts and metadata
|
86
|
+
|
87
|
+
Raises ValueError if no existing state is found
|
88
|
+
"""
|
89
|
+
existing_state = self.state_store.get_state(task_id)
|
90
|
+
if not existing_state:
|
91
|
+
raise ValueError(f"No existing state found for task_id: {task_id}")
|
92
|
+
|
93
|
+
updated_state = existing_state.copy()
|
94
|
+
new_messages = []
|
95
|
+
|
96
|
+
# Add messages from result's history
|
97
|
+
if result.history:
|
98
|
+
new_messages.extend(result.history)
|
99
|
+
|
100
|
+
# Extract messages from result's artifacts
|
101
|
+
for artifact in result.artifacts or []:
|
102
|
+
if artifact.parts:
|
103
|
+
artifact_message = Message(
|
104
|
+
role="tool",
|
105
|
+
parts=artifact.parts,
|
106
|
+
metadata=artifact.metadata
|
107
|
+
)
|
108
|
+
new_messages.append(artifact_message)
|
109
|
+
|
110
|
+
# Update task history (merge with existing)
|
111
|
+
if new_messages:
|
112
|
+
if updated_state.task.history is None:
|
113
|
+
updated_state.task.history = []
|
114
|
+
updated_state.task.history.extend(new_messages)
|
115
|
+
|
116
|
+
# Update context history using strategy
|
117
|
+
updated_state.context_history = self.strategy.update_history(
|
118
|
+
existing_history=updated_state.context_history,
|
119
|
+
new_messages=new_messages
|
120
|
+
)
|
121
|
+
|
122
|
+
# Merge artifacts
|
123
|
+
if result.artifacts:
|
124
|
+
if updated_state.task.artifacts is None:
|
125
|
+
updated_state.task.artifacts = []
|
126
|
+
updated_state.task.artifacts.extend(result.artifacts)
|
127
|
+
|
128
|
+
# Merge metadata
|
129
|
+
if result.metadata:
|
130
|
+
updated_state.task.metadata = {
|
131
|
+
**(updated_state.task.metadata or {}),
|
132
|
+
**(result.metadata or {})
|
133
|
+
}
|
134
|
+
|
135
|
+
# Update task status if provided
|
136
|
+
if result.status:
|
137
|
+
updated_state.task.status = result.status
|
138
|
+
|
139
|
+
return updated_state
|
140
|
+
|
141
|
+
def get_state(self, task_id: str) -> Optional[StateData]:
|
142
|
+
return self.state_store.get_state(task_id)
|
143
|
+
|
144
|
+
async def update_state(self, state_data: StateData):
|
145
|
+
self.state_store.update_state(state_data.task_id, state_data)
|
146
|
+
|
147
|
+
# Publish update through NATS client
|
148
|
+
payload = self._prepare_update_payload(state_data)
|
149
|
+
await self.nats_client.publish("state.updates", payload)
|
28
150
|
|
29
151
|
def get_store(self) -> Optional[BaseStateStore]:
|
30
|
-
return self.
|
152
|
+
return self.state_store
|
31
153
|
|
32
|
-
def
|
154
|
+
def get_history_strategy(self) -> HistoryUpdateStrategy:
|
33
155
|
return self.strategy
|
34
|
-
|
156
|
+
|
157
|
+
# Private methods
|
158
|
+
|
159
|
+
def _serialize_part(self, part: Part) -> dict:
|
160
|
+
"""Serialize a Part to frontend-compatible format"""
|
161
|
+
part_data = part.model_dump()
|
162
|
+
if part.type == "file" and part.file:
|
163
|
+
if not part.file.bytes and not part.file.uri:
|
164
|
+
raise ValueError("FilePart must have either bytes or uri")
|
165
|
+
return part_data
|
166
|
+
|
167
|
+
def _prepare_update_payload(self, state: StateData) -> Dict[str, Any]:
|
168
|
+
"""Prepare NATS message payload from state data"""
|
169
|
+
return {
|
170
|
+
"taskId": state.task_id,
|
171
|
+
"parts": self._extract_artifact_parts(state.task),
|
172
|
+
"complete": state.task.status.state == TaskState.COMPLETED
|
173
|
+
}
|
174
|
+
|
175
|
+
def _extract_artifact_parts(self, task: Task) -> List[dict]:
|
176
|
+
"""Extract and serialize parts from all artifacts"""
|
177
|
+
parts = []
|
178
|
+
if task.artifacts:
|
179
|
+
for artifact in task.artifacts:
|
180
|
+
for part in artifact.parts:
|
181
|
+
try:
|
182
|
+
parts.append(self._serialize_part(part))
|
183
|
+
except ValueError as e:
|
184
|
+
print(f"Invalid part in artifact: {e}")
|
185
|
+
return parts
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# Library imports
|
2
|
+
from typing import Callable, Any, Optional
|
3
|
+
|
4
|
+
# Local imports
|
5
|
+
from smarta2a.server.state_manager import StateManager
|
6
|
+
from smarta2a.utils.types import WebhookRequest, WebhookResponse, StateData, Message
|
7
|
+
from smarta2a.client.a2a_client import A2AClient
|
8
|
+
|
9
|
+
class WebhookRequestProcessor:
|
10
|
+
def __init__(self, webhook_fn: Callable[[WebhookRequest], Any], state_manager: Optional[StateManager] = None):
|
11
|
+
self.webhook_fn = webhook_fn
|
12
|
+
self.state_manager = state_manager
|
13
|
+
self.a2a_aclient = A2AClient()
|
14
|
+
|
15
|
+
async def process_request(self, request: WebhookRequest) -> WebhookResponse:
|
16
|
+
if self.state_manager:
|
17
|
+
state_data = self.state_manager.get_and_update_state_from_webhook(request.id, request.result)
|
18
|
+
return await self._handle_webhook(request, state_data)
|
19
|
+
else:
|
20
|
+
return await self._handle_webhook(request)
|
21
|
+
|
22
|
+
|
23
|
+
async def _webhook_handler(self, request: WebhookRequest, state_data: Optional[StateData] = None) -> WebhookResponse:
|
24
|
+
try:
|
25
|
+
# --- Step 1: Process Incoming Task ---
|
26
|
+
if request.result:
|
27
|
+
incoming_task = request.result
|
28
|
+
|
29
|
+
# Initialize state_data if missing
|
30
|
+
if not state_data:
|
31
|
+
state_data = StateData(
|
32
|
+
task_id=incoming_task.id,
|
33
|
+
task=incoming_task.copy(update={"artifacts": incoming_task.artifacts}),
|
34
|
+
context_history=[],
|
35
|
+
push_notification_config=None
|
36
|
+
)
|
37
|
+
else:
|
38
|
+
existing_task = state_data.task
|
39
|
+
|
40
|
+
# Overwrite artifacts
|
41
|
+
existing_task.artifacts = incoming_task.artifacts.copy() if incoming_task.artifacts else []
|
42
|
+
|
43
|
+
# Merge metadata
|
44
|
+
existing_task.metadata = {**(existing_task.metadata or {}), **(incoming_task.metadata or {})}
|
45
|
+
|
46
|
+
# Build messages from artifact parts (role="agent" as in handle_send_task)
|
47
|
+
all_parts = [part for artifact in incoming_task.artifacts for part in artifact.parts] if incoming_task.artifacts else []
|
48
|
+
new_messages = [Message(role="agent", parts=all_parts, metadata=incoming_task.metadata)]
|
49
|
+
|
50
|
+
# Update context history using strategy
|
51
|
+
history_strategy = self.state_manager.get_history_strategy()
|
52
|
+
state_data.context_history = history_strategy.update_history(
|
53
|
+
existing_history=state_data.context_history,
|
54
|
+
new_messages=new_messages
|
55
|
+
)
|
56
|
+
|
57
|
+
# Persist state
|
58
|
+
await self.state_manager.update_state(state_data)
|
59
|
+
|
60
|
+
# --- Step 2: Call Webhook Function ---
|
61
|
+
webhook_response = await self.webhook_fn(request, state_data) if state_data else await self.webhook_fn(request)
|
62
|
+
|
63
|
+
# --- Step 3: Process Webhook Response ---
|
64
|
+
if webhook_response.result:
|
65
|
+
updated_task = webhook_response.result
|
66
|
+
existing_task = state_data.task
|
67
|
+
|
68
|
+
# Overwrite artifacts from response
|
69
|
+
existing_task.artifacts = updated_task.artifacts.copy() if updated_task.artifacts else []
|
70
|
+
|
71
|
+
# Merge metadata
|
72
|
+
existing_task.metadata = {**(existing_task.metadata or {}), **(updated_task.metadata or {})}
|
73
|
+
|
74
|
+
# Build messages from updated artifacts
|
75
|
+
updated_parts = [part for artifact in updated_task.artifacts for part in artifact.parts] if updated_task.artifacts else []
|
76
|
+
updated_messages = [Message(role="agent", parts=updated_parts, metadata=updated_task.metadata)]
|
77
|
+
|
78
|
+
# Update context history again
|
79
|
+
state_data.context_history = history_strategy.update_history(
|
80
|
+
existing_history=state_data.context_history,
|
81
|
+
new_messages=updated_messages
|
82
|
+
)
|
83
|
+
|
84
|
+
await self.state_manager.update_state(state_data)
|
85
|
+
|
86
|
+
# --- Step 4: Push Notification ---
|
87
|
+
push_url = (
|
88
|
+
state_data.push_notification_config.url
|
89
|
+
if state_data and state_data.push_notification_config
|
90
|
+
else None
|
91
|
+
)
|
92
|
+
if push_url:
|
93
|
+
try:
|
94
|
+
self.a2a_aclient.send_to_webhook(
|
95
|
+
webhook_url=push_url,
|
96
|
+
id=state_data.task_id,
|
97
|
+
task=state_data.task
|
98
|
+
)
|
99
|
+
except Exception as e:
|
100
|
+
return WebhookResponse(
|
101
|
+
id=request.id,
|
102
|
+
error=f"Push notification failed: {str(e)}"
|
103
|
+
)
|
104
|
+
|
105
|
+
# --- Step 5: Return Final Response ---
|
106
|
+
return WebhookResponse(
|
107
|
+
id=request.id,
|
108
|
+
result=state_data.task if state_data else None
|
109
|
+
)
|
110
|
+
|
111
|
+
except Exception as e:
|
112
|
+
return WebhookResponse(id=request.id, error=f"Internal error: {str(e)}")
|
@@ -8,13 +8,13 @@ from smarta2a.utils.types import StateData, Message
|
|
8
8
|
class BaseStateStore(ABC):
|
9
9
|
|
10
10
|
@abstractmethod
|
11
|
-
async def get_state(self,
|
11
|
+
async def get_state(self, task_id: str) -> Optional[StateData]:
|
12
12
|
pass
|
13
13
|
|
14
14
|
@abstractmethod
|
15
|
-
async def update_state(self,
|
15
|
+
async def update_state(self, task_id: str, state_data: StateData) -> None:
|
16
16
|
pass
|
17
17
|
|
18
18
|
@abstractmethod
|
19
|
-
async def delete_state(self,
|
19
|
+
async def delete_state(self, task_id: str) -> None:
|
20
20
|
pass
|
@@ -10,12 +10,26 @@ class InMemoryStateStore(BaseStateStore):
|
|
10
10
|
def __init__(self):
|
11
11
|
self.states: Dict[str, StateData] = {}
|
12
12
|
|
13
|
-
def
|
14
|
-
|
13
|
+
def initialize_state(self, state_data: StateData) -> None:
|
14
|
+
self.states[state_data.task_id] = state_data
|
15
15
|
|
16
|
-
def
|
17
|
-
self.states
|
16
|
+
def get_state(self, task_id: str) -> Optional[StateData]:
|
17
|
+
return self.states.get(task_id)
|
18
18
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
19
|
+
def update_state(self, task_id: str, state_data: StateData):
|
20
|
+
self.states[task_id] = state_data
|
21
|
+
|
22
|
+
def delete_state(self, task_id: str):
|
23
|
+
if task_id in self.states:
|
24
|
+
del self.states[task_id]
|
25
|
+
|
26
|
+
def get_all_tasks(self, fields: Optional[str] = None) -> List[Dict[str, Any]]:
|
27
|
+
all_tasks = [state_data.task.model_dump() for state_data in self.states.values()]
|
28
|
+
if fields:
|
29
|
+
requested_fields = fields.split(",")
|
30
|
+
fields_filtered_tasks = [
|
31
|
+
{field: task[field] for field in requested_fields if field in task}
|
32
|
+
for task in all_tasks
|
33
|
+
]
|
34
|
+
return fields_filtered_tasks
|
35
|
+
return all_tasks
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Library imports
|
2
|
+
import httpx
|
3
|
+
from typing import List, Optional
|
4
|
+
from pydantic import HttpUrl, ValidationError
|
5
|
+
|
6
|
+
# Local imports
|
7
|
+
from smarta2a.utils.types import AgentCard
|
8
|
+
|
9
|
+
class AgentDiscoveryManager:
|
10
|
+
"""Centralized service for discovering agents through multiple methods"""
|
11
|
+
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
agent_cards: Optional[List[AgentCard]] = None,
|
15
|
+
agent_base_urls: Optional[List[HttpUrl]] = None,
|
16
|
+
discovery_endpoint: Optional[HttpUrl] = None,
|
17
|
+
timeout: float = 5.0,
|
18
|
+
retries: int = 2
|
19
|
+
):
|
20
|
+
self.explicit_cards = agent_cards or []
|
21
|
+
self.agent_base_urls = agent_base_urls or []
|
22
|
+
self.discovery_endpoint = discovery_endpoint
|
23
|
+
self.timeout = timeout
|
24
|
+
self.retries = retries
|
25
|
+
self.discovered_cards: List[AgentCard] = []
|
26
|
+
|
27
|
+
async def discover_agents(self) -> List[AgentCard]:
|
28
|
+
"""Discover agents through all configured methods"""
|
29
|
+
self.discovered_cards = []
|
30
|
+
|
31
|
+
# 1. Add explicit cards first
|
32
|
+
self.discovered_cards.extend(self.explicit_cards)
|
33
|
+
|
34
|
+
# 2. Discover via base URLs
|
35
|
+
if self.agent_base_urls:
|
36
|
+
base_url_cards = await self._discover_via_base_urls()
|
37
|
+
self.discovered_cards.extend(base_url_cards)
|
38
|
+
|
39
|
+
# 3. Discover via central endpoint
|
40
|
+
if self.discovery_endpoint:
|
41
|
+
endpoint_cards = await self._discover_via_endpoint()
|
42
|
+
self.discovered_cards.extend(endpoint_cards)
|
43
|
+
|
44
|
+
return self.discovered_cards
|
45
|
+
|
46
|
+
async def _discover_via_base_urls(self) -> List[AgentCard]:
|
47
|
+
"""Discover agents from provided base URLs"""
|
48
|
+
cards = []
|
49
|
+
client = httpx.AsyncClient()
|
50
|
+
|
51
|
+
try:
|
52
|
+
for base_url in self.agent_base_urls:
|
53
|
+
try:
|
54
|
+
agent_url = f"{base_url}/.well-known/agent.json"
|
55
|
+
card = await self._fetch_agent_card(client, agent_url)
|
56
|
+
cards.append(card)
|
57
|
+
except Exception:
|
58
|
+
pass
|
59
|
+
finally:
|
60
|
+
await client.aclose()
|
61
|
+
|
62
|
+
return cards
|
63
|
+
|
64
|
+
async def _discover_via_endpoint(self) -> List[AgentCard]:
|
65
|
+
"""Discover agents through a centralized endpoint"""
|
66
|
+
client = httpx.AsyncClient()
|
67
|
+
cards = []
|
68
|
+
|
69
|
+
try:
|
70
|
+
# Fetch service registry
|
71
|
+
try:
|
72
|
+
response = await client.get(
|
73
|
+
str(self.discovery_endpoint),
|
74
|
+
timeout=self.timeout
|
75
|
+
)
|
76
|
+
response.raise_for_status()
|
77
|
+
services = response.json()["services"]
|
78
|
+
except Exception:
|
79
|
+
return []
|
80
|
+
|
81
|
+
# Fetch all discovered agent cards
|
82
|
+
for service in services:
|
83
|
+
try:
|
84
|
+
agent_url = f"{service['base_url']}/.well-known/agent.json"
|
85
|
+
card = await self._fetch_agent_card(client, agent_url)
|
86
|
+
cards.append(card)
|
87
|
+
except Exception:
|
88
|
+
pass
|
89
|
+
finally:
|
90
|
+
await client.aclose()
|
91
|
+
|
92
|
+
return cards
|
93
|
+
|
94
|
+
async def _fetch_agent_card(self, client: httpx.AsyncClient, url: str) -> AgentCard:
|
95
|
+
"""Fetch and validate a single agent.json"""
|
96
|
+
try:
|
97
|
+
response = await client.get(
|
98
|
+
url,
|
99
|
+
timeout=self.timeout,
|
100
|
+
follow_redirects=True,
|
101
|
+
headers={"User-Agent": "AgentDiscovery/1.0"}
|
102
|
+
)
|
103
|
+
response.raise_for_status()
|
104
|
+
|
105
|
+
# Validate content type
|
106
|
+
content_type = response.headers.get('Content-Type', '')
|
107
|
+
if 'application/json' not in content_type:
|
108
|
+
raise ValueError(f"Unexpected Content-Type: {content_type}")
|
109
|
+
|
110
|
+
data = response.json()
|
111
|
+
|
112
|
+
# Enforce required 'url' field
|
113
|
+
if "url" not in data:
|
114
|
+
raise ValueError("AgentCard requires 'url' field in agent.json")
|
115
|
+
|
116
|
+
return AgentCard(**data)
|
117
|
+
|
118
|
+
except ValidationError as e:
|
119
|
+
raise
|
120
|
+
except Exception as e:
|
121
|
+
raise
|
smarta2a/utils/prompt_helpers.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Library imports
|
2
2
|
import json
|
3
|
-
from typing import List, Dict, Any, Union, Literal
|
3
|
+
from typing import List, Dict, Any, Union, Literal, Optional
|
4
4
|
|
5
5
|
# Local imports
|
6
6
|
from smarta2a.client.mcp_client import MCPClient
|
@@ -37,6 +37,26 @@ class ToolsManager:
|
|
37
37
|
for tool_dict in tools_list:
|
38
38
|
# Generate key from agent URL and tool name
|
39
39
|
key = f"{agent_card.name}---{tool_dict['name']}"
|
40
|
+
|
41
|
+
# Build new description
|
42
|
+
components = []
|
43
|
+
original_desc = tool_dict['description']
|
44
|
+
if original_desc:
|
45
|
+
components.append(original_desc)
|
46
|
+
if agent_card.description:
|
47
|
+
components.append(f"Agent Description: {agent_card.description}")
|
48
|
+
|
49
|
+
# Collect skill descriptions
|
50
|
+
skill_descriptions = []
|
51
|
+
for skill in agent_card.skills:
|
52
|
+
if skill.description:
|
53
|
+
skill_descriptions.append(skill.description)
|
54
|
+
if skill_descriptions:
|
55
|
+
components.append(f"Agent's skills: {', '.join(skill_descriptions)}")
|
56
|
+
|
57
|
+
# Update tool_dict with new description
|
58
|
+
tool_dict['description'] = ". ".join(components)
|
59
|
+
|
40
60
|
validated_tool = Tool(
|
41
61
|
key=key,
|
42
62
|
**tool_dict
|
@@ -65,13 +85,24 @@ class ToolsManager:
|
|
65
85
|
|
66
86
|
def get_client(self, tool_key: str) -> Any:
|
67
87
|
return self.clients.get(tool_key)
|
88
|
+
|
89
|
+
async def call_tool(self, tool_key: str, args: Dict[str, Any], override_args: Optional[Dict[str, Any]] = None) -> Any:
|
90
|
+
try:
|
91
|
+
client = self.get_client(tool_key)
|
92
|
+
tool_name = self._get_tool_name(tool_key)
|
93
|
+
new_args = self._replace_with_override_args(args, override_args)
|
94
|
+
result = await client.call_tool(tool_name, new_args)
|
95
|
+
return result
|
68
96
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
if not client:
|
73
|
-
raise ValueError(f"Tool not found: {tool_name}")
|
74
|
-
return await client.call_tool(tool_name, args)
|
97
|
+
except Exception as e:
|
98
|
+
# This will catch ANY error in the body above
|
99
|
+
raise
|
75
100
|
|
76
101
|
def _get_tool_name(self, tool_key: str) -> str:
|
77
|
-
return tool_key.split("---")[1]
|
102
|
+
return tool_key.split("---")[1]
|
103
|
+
|
104
|
+
def _replace_with_override_args(self, args: Dict[str, Any], override_args: Optional[Dict[str, Any]] = None):
|
105
|
+
new_args = args.copy()
|
106
|
+
if override_args:
|
107
|
+
new_args.update(override_args)
|
108
|
+
return new_args
|
smarta2a/utils/types.py
CHANGED
@@ -479,12 +479,26 @@ class A2AStreamResponse(BaseModel):
|
|
479
479
|
metadata: dict[str, Any] | None = None
|
480
480
|
|
481
481
|
class StateData(BaseModel):
|
482
|
-
|
483
|
-
|
484
|
-
|
482
|
+
task_id: str
|
483
|
+
task: Task
|
484
|
+
context_history: List[Message]
|
485
|
+
push_notification_config: PushNotificationConfig | None = None
|
485
486
|
|
486
487
|
class Tool(BaseModel):
|
487
488
|
key: str
|
488
489
|
name: str
|
489
490
|
description: str
|
490
491
|
inputSchema: Dict[str, Any]
|
492
|
+
|
493
|
+
'''
|
494
|
+
The callback request may simply be a message without a result - basically acknowledging the task was completed.
|
495
|
+
It can also include a result, which is the task that was completed along with the full artifact.
|
496
|
+
'''
|
497
|
+
class WebhookRequest(BaseModel):
|
498
|
+
id: str
|
499
|
+
result: Task | None = None
|
500
|
+
|
501
|
+
class WebhookResponse(BaseModel):
|
502
|
+
id: str
|
503
|
+
result: Task | None = None
|
504
|
+
error: str | None = None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: smarta2a
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.1
|
4
4
|
Summary: a Python framework that helps you build servers and AI agents that communicate using the A2A protocol
|
5
5
|
Project-URL: Homepage, https://github.com/siddharthsma/smarta2a
|
6
6
|
Project-URL: Bug Tracker, https://github.com/siddharthsma/smarta2a/issues
|
@@ -69,7 +69,7 @@ pip install -U smarta2a
|
|
69
69
|
|
70
70
|
There are 3 cool examples for you to try out - eacher cooler than the next!
|
71
71
|
|
72
|
-
1. The first is a simple Echo server implementation that demonstrates the FastAPI'esk style in which you can create
|
72
|
+
1. The first is a simple Echo server implementation that demonstrates the FastAPI'esk style in which you can create an A2A compliant server. If you're using another Agent framework like LangGraph you can easily use it in tandem with this server.
|
73
73
|
|
74
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
75
|
|
@@ -117,7 +117,7 @@ def handle_cancel_task(request):
|
|
117
117
|
|
118
118
|
This example shows:
|
119
119
|
- Setting up a basic A2A server with state management
|
120
|
-
- Handling synchronous task requests with text
|
120
|
+
- Handling synchronous task requests with text responses (btw also handles files!)
|
121
121
|
- Implementing streaming responses for subscription tasks
|
122
122
|
- Basic task management (get and cancel operations)
|
123
123
|
|
@@ -347,7 +347,10 @@ To run the multi-agent system:
|
|
347
347
|
python path/to/weather_agent/main.py
|
348
348
|
```
|
349
349
|
|
350
|
-
2. Start the
|
350
|
+
2. Start the weather agent:
|
351
|
+
```bash
|
352
|
+
python path/to/airbnb_agent/main.py
|
353
|
+
```
|
351
354
|
|
352
355
|
|
353
356
|
3. Start the delegator agent:
|
@@ -0,0 +1,40 @@
|
|
1
|
+
smarta2a/__init__.py,sha256=T_EECYqWrxshix0FbgUv22zlKRX22HFU-HKXcYTOb3w,175
|
2
|
+
smarta2a/agent/a2a_agent.py,sha256=EurcxpV14e3OPWCMutYL0EXMHb5ZKQqAHEGZZF6pNgg,1892
|
3
|
+
smarta2a/agent/a2a_human.py,sha256=tXrEtl0OqZEXZfmPTLnbbAiREMTmlqA42XIf7KocqMU,1850
|
4
|
+
smarta2a/agent/a2a_mcp_server.py,sha256=X_mxkgYgCA_dSNtCvs0rSlOoWYc-8d3Qyxv0e-a7NKY,1015
|
5
|
+
smarta2a/archive/smart_mcp_client.py,sha256=0s2OWFKWSv-_UF7rb9fOrsh1OIYsYOsGukkXXp_E1cU,4158
|
6
|
+
smarta2a/archive/subscription_service.py,sha256=vftmZD94HbdjPFa_1UBvsBm-WkW-s3ZCVq60fF7OCgA,4109
|
7
|
+
smarta2a/archive/task_service.py,sha256=ptf-oMHy98Rw4XSxyK1-lpqc1JtkCkEEHTmwAaunet4,8199
|
8
|
+
smarta2a/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
smarta2a/client/a2a_client.py,sha256=apDkKFtq61T79LpkbkzVTKWA0mSjR_eTNdGPUYozyvk,12100
|
10
|
+
smarta2a/client/mcp_client.py,sha256=PM4D1CgOycxK5kJEJTGpKq0eXFjZ69-2720TuRUkyGc,3627
|
11
|
+
smarta2a/history_update_strategies/__init__.py,sha256=x5WtiE9rG5ze8d8hA6E6wJOciBhWHa_ZgGgoIAZcXEQ,213
|
12
|
+
smarta2a/history_update_strategies/append_strategy.py,sha256=j7Qbhs69Wwr-HBLB8GJ3-mEPaBSHiBV2xz9ZZi86k2w,312
|
13
|
+
smarta2a/history_update_strategies/history_update_strategy.py,sha256=n2sfIGu8ztKI7gJTwRX26m4tZr28B8Xdhrk6RlBFlI8,373
|
14
|
+
smarta2a/history_update_strategies/rolling_window_strategy.py,sha256=7Ch042JWt4TM_r1-sFKlSIxHj8VX1P3ZoqjCvIdeSqA,540
|
15
|
+
smarta2a/model_providers/__init__.py,sha256=hJj0w00JjqTiBgJmHmOWwL6MU_hwmro9xTiX3XYf6ts,148
|
16
|
+
smarta2a/model_providers/base_llm_provider.py,sha256=iQUqjnypl0f2M929iU0WhHoxAE4ek-NUFJPbEnNQ8-4,412
|
17
|
+
smarta2a/model_providers/openai_provider.py,sha256=YGHF6IIsBBE-Otiq9q9hSd49sh5unxqINRh9q3nKPQI,12088
|
18
|
+
smarta2a/server/__init__.py,sha256=f2X454Ll4vJc02V4JLJHTN-h8u0TBm4d_FkiO4t686U,53
|
19
|
+
smarta2a/server/handler_registry.py,sha256=OVRG5dTvxB7qUNXgsqWxVNxIyRljUShSYxb1gtbi5XM,820
|
20
|
+
smarta2a/server/json_rpc_request_processor.py,sha256=qRB3sfj_n9ImkIOCdaUKMsDmKcO7CiMhaZ4VdQS7Mb4,6993
|
21
|
+
smarta2a/server/nats_client.py,sha256=akyNg1hLd9XYoLSH_qQVs8uoiTQerztgvqu_3TifSgE,1617
|
22
|
+
smarta2a/server/request_handler.py,sha256=5KMtfpHQX6bOgk1DJbhs1fUCQ5tSvMYXWzheT3IW2Bo,26374
|
23
|
+
smarta2a/server/send_task_handler.py,sha256=fiBeCCHCu9c2H4EJOUc0t3EZgpHVFJy4B_6qZOC140s,6336
|
24
|
+
smarta2a/server/server.py,sha256=grE2n-MAyaBR5rUro2ZJ0kI6sBFplHvRFL5MMe1DsPs,7094
|
25
|
+
smarta2a/server/state_manager.py,sha256=bu75JFNnmeW5ar2XHAOogRev0DyrZV6JGeue13lA3jo,6587
|
26
|
+
smarta2a/server/webhook_request_processor.py,sha256=_0XoUDmueSl9CvFQE-1zgKRSts-EW8QxbmolPTfFER8,5306
|
27
|
+
smarta2a/state_stores/__init__.py,sha256=vafxAqpwvag_cYFH2XKGk3DPmJIWJr4Ioey30yLFkVQ,220
|
28
|
+
smarta2a/state_stores/base_state_store.py,sha256=_3LInM-qepKwwdypJTDNs9-DozBNrKVycwPwUm7bYdU,512
|
29
|
+
smarta2a/state_stores/inmemory_state_store.py,sha256=nEBBUiiqhEluP2MYJjFUImcjIwLJEvL8BWwMbLCb8Fw,1268
|
30
|
+
smarta2a/utils/__init__.py,sha256=5db5VgDGgbMUGEF-xuyaC3qrgRQkUE9WAITkFSiNqSA,702
|
31
|
+
smarta2a/utils/agent_discovery_manager.py,sha256=6KpRSQH_EDUOZbF4wFRsZneZGIPLXFP4VjWPN4Ydv-A,4172
|
32
|
+
smarta2a/utils/prompt_helpers.py,sha256=M3UUjFQEspEAnNm54Dip0-D7mMFFZLrP_s_89ZPe6fs,1438
|
33
|
+
smarta2a/utils/task_builder.py,sha256=wqSyfVHNTaXuGESu09dhlaDi7D007gcN3-8tH-nPQ40,5159
|
34
|
+
smarta2a/utils/task_request_builder.py,sha256=6cOGOqj2Rg43xWM03GRJQzlIZHBptsMCJRp7oD-TDAQ,3362
|
35
|
+
smarta2a/utils/tools_manager.py,sha256=yOiJ6tyxfKDJDM2C0FoVuqh3nNNee75iRw_M7LmZdzA,4319
|
36
|
+
smarta2a/utils/types.py,sha256=kzA6Vv5xXfu1sJuxhEXrglI9e9S6eZVIljMnsrQVyN0,13650
|
37
|
+
smarta2a-0.4.1.dist-info/METADATA,sha256=I-2mH0BsJaIoalw4kWobwRfUzzUojtNry_WplJRtBC0,12987
|
38
|
+
smarta2a-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
39
|
+
smarta2a-0.4.1.dist-info/licenses/LICENSE,sha256=lDbqrxVnzDMY5KJ8JS1WhvkWE8TJaw-O-CHDy-ecsJA,2095
|
40
|
+
smarta2a-0.4.1.dist-info/RECORD,,
|