smarta2a 0.3.0__py3-none-any.whl → 0.4.0__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.
Files changed (42) hide show
  1. smarta2a/agent/a2a_agent.py +25 -15
  2. smarta2a/agent/a2a_human.py +56 -0
  3. smarta2a/archive/smart_mcp_client.py +47 -0
  4. smarta2a/archive/subscription_service.py +85 -0
  5. smarta2a/{server → archive}/task_service.py +17 -8
  6. smarta2a/client/a2a_client.py +35 -8
  7. smarta2a/client/mcp_client.py +3 -0
  8. smarta2a/history_update_strategies/rolling_window_strategy.py +16 -0
  9. smarta2a/model_providers/__init__.py +1 -1
  10. smarta2a/model_providers/base_llm_provider.py +3 -3
  11. smarta2a/model_providers/openai_provider.py +126 -89
  12. smarta2a/nats-server.conf +12 -0
  13. smarta2a/server/json_rpc_request_processor.py +130 -0
  14. smarta2a/server/nats_client.py +49 -0
  15. smarta2a/server/request_handler.py +667 -0
  16. smarta2a/server/send_task_handler.py +174 -0
  17. smarta2a/server/server.py +124 -726
  18. smarta2a/server/state_manager.py +173 -19
  19. smarta2a/server/webhook_request_processor.py +112 -0
  20. smarta2a/state_stores/base_state_store.py +3 -3
  21. smarta2a/state_stores/inmemory_state_store.py +21 -7
  22. smarta2a/utils/agent_discovery_manager.py +121 -0
  23. smarta2a/utils/prompt_helpers.py +1 -1
  24. smarta2a/utils/tools_manager.py +108 -0
  25. smarta2a/utils/types.py +18 -3
  26. smarta2a-0.4.0.dist-info/METADATA +402 -0
  27. smarta2a-0.4.0.dist-info/RECORD +41 -0
  28. smarta2a-0.4.0.dist-info/licenses/LICENSE +35 -0
  29. smarta2a/client/tools_manager.py +0 -62
  30. smarta2a/examples/__init__.py +0 -0
  31. smarta2a/examples/echo_server/__init__.py +0 -0
  32. smarta2a/examples/echo_server/curl.txt +0 -1
  33. smarta2a/examples/echo_server/main.py +0 -39
  34. smarta2a/examples/openai_delegator_agent/__init__.py +0 -0
  35. smarta2a/examples/openai_delegator_agent/main.py +0 -41
  36. smarta2a/examples/openai_weather_agent/__init__.py +0 -0
  37. smarta2a/examples/openai_weather_agent/main.py +0 -32
  38. smarta2a/server/subscription_service.py +0 -109
  39. smarta2a-0.3.0.dist-info/METADATA +0 -103
  40. smarta2a-0.3.0.dist-info/RECORD +0 -40
  41. smarta2a-0.3.0.dist-info/licenses/LICENSE +0 -21
  42. {smarta2a-0.3.0.dist-info → smarta2a-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,34 +1,188 @@
1
1
  # Library imports
2
- from typing import Optional, Dict, Any
2
+ from typing import Optional, Dict, Any, List
3
3
  from uuid import uuid4
4
+ from nats.aio.client import Client as NATS
5
+ import asyncio
4
6
 
5
7
  # Local imports
6
8
  from smarta2a.state_stores.base_state_store import BaseStateStore
7
9
  from smarta2a.history_update_strategies.history_update_strategy import HistoryUpdateStrategy
8
- from smarta2a.utils.types import Message, StateData
10
+ from smarta2a.utils.types import Message, StateData, Task, TaskStatus, TaskState, PushNotificationConfig, Part
11
+ from smarta2a.server.nats_client import NATSClient
9
12
 
10
13
  class StateManager:
11
- def __init__(self, store: Optional[BaseStateStore], history_strategy: HistoryUpdateStrategy):
12
- self.store = store
14
+ def __init__(self, state_store: BaseStateStore, history_strategy: HistoryUpdateStrategy):
15
+ self.state_store = state_store
13
16
  self.strategy = history_strategy
17
+ self.nats_client = NATSClient(server_url="nats://localhost:4222")
18
+
19
+ async def load(self):
20
+ await self.nats_client.connect()
21
+
22
+
23
+ async def unload(self):
24
+ await self.nats_client.close()
25
+
26
+ def _initialize_empty_state(
27
+ self,
28
+ task_id: str,
29
+ session_id: str,
30
+ push_notification_config: Optional[PushNotificationConfig] = None
31
+ ) -> StateData:
32
+ """
33
+ Build a fresh StateData and persist it.
34
+ """
35
+ initial_task = Task(
36
+ id=task_id,
37
+ sessionId=session_id,
38
+ status=TaskStatus(state=TaskState.WORKING),
39
+ artifacts=[],
40
+ history=[],
41
+ metadata={}
42
+ )
43
+ state = StateData(
44
+ task_id=task_id,
45
+ task=initial_task,
46
+ context_history=[],
47
+ push_notification_config=push_notification_config
48
+ )
49
+ self.state_store.initialize_state(state)
50
+ return state
51
+
52
+ async def get_or_create_and_update_state(
53
+ self,
54
+ task_id: str,
55
+ session_id: str,
56
+ message: Message,
57
+ metadata: Optional[Dict[str, Any]] = None,
58
+ push_notification_config: Optional[PushNotificationConfig] = None
59
+ ) -> StateData:
60
+ """
61
+ Fetch existing StateData, or initialize & persist a new one.
62
+ """
63
+ existing_state = self.state_store.get_state(task_id)
64
+ if not existing_state:
65
+ latest_state = self._initialize_empty_state(
66
+ task_id, session_id, push_notification_config
67
+ )
68
+ else:
69
+ latest_state = existing_state.copy()
70
+
71
+ latest_state.task.history.append(message)
72
+ latest_state.context_history = self.strategy.update_history(
73
+ existing_history=latest_state.context_history,
74
+ new_messages=[message]
75
+ )
76
+ latest_state.task.metadata = metadata
14
77
 
15
- def init_or_get(self, session_id: Optional[str], message: Message, metadata: Dict[str, Any]) -> StateData:
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
78
+ await self.update_state(latest_state)
24
79
 
25
- def update(self, state: StateData):
26
- if self.store:
27
- self.store.update_state(state.sessionId, state)
80
+ return latest_state
81
+
82
+ def get_and_update_state_from_webhook(self, task_id: str, result: Task) -> StateData:
83
+ """
84
+ Update existing state with webhook result data, including:
85
+ - Merges task history from result
86
+ - Extracts messages from artifacts' parts
87
+ - Updates context history using strategy
88
+ - Merges artifacts and metadata
89
+
90
+ Raises ValueError if no existing state is found
91
+ """
92
+ existing_state = self.state_store.get_state(task_id)
93
+ if not existing_state:
94
+ raise ValueError(f"No existing state found for task_id: {task_id}")
95
+
96
+ updated_state = existing_state.copy()
97
+ new_messages = []
98
+
99
+ # Add messages from result's history
100
+ if result.history:
101
+ new_messages.extend(result.history)
102
+
103
+ # Extract messages from result's artifacts
104
+ for artifact in result.artifacts or []:
105
+ if artifact.parts:
106
+ artifact_message = Message(
107
+ role="tool",
108
+ parts=artifact.parts,
109
+ metadata=artifact.metadata
110
+ )
111
+ new_messages.append(artifact_message)
112
+
113
+ # Update task history (merge with existing)
114
+ if new_messages:
115
+ if updated_state.task.history is None:
116
+ updated_state.task.history = []
117
+ updated_state.task.history.extend(new_messages)
118
+
119
+ # Update context history using strategy
120
+ updated_state.context_history = self.strategy.update_history(
121
+ existing_history=updated_state.context_history,
122
+ new_messages=new_messages
123
+ )
124
+
125
+ # Merge artifacts
126
+ if result.artifacts:
127
+ if updated_state.task.artifacts is None:
128
+ updated_state.task.artifacts = []
129
+ updated_state.task.artifacts.extend(result.artifacts)
130
+
131
+ # Merge metadata
132
+ if result.metadata:
133
+ updated_state.task.metadata = {
134
+ **(updated_state.task.metadata or {}),
135
+ **(result.metadata or {})
136
+ }
137
+
138
+ # Update task status if provided
139
+ if result.status:
140
+ updated_state.task.status = result.status
141
+
142
+ return updated_state
143
+
144
+ def get_state(self, task_id: str) -> Optional[StateData]:
145
+ return self.state_store.get_state(task_id)
146
+
147
+ async def update_state(self, state_data: StateData):
148
+ self.state_store.update_state(state_data.task_id, state_data)
149
+
150
+ # Publish update through NATS client
151
+ payload = self._prepare_update_payload(state_data)
152
+ await self.nats_client.publish("state.updates", payload)
28
153
 
29
154
  def get_store(self) -> Optional[BaseStateStore]:
30
- return self.store
155
+ return self.state_store
31
156
 
32
- def get_strategy(self) -> HistoryUpdateStrategy:
157
+ def get_history_strategy(self) -> HistoryUpdateStrategy:
33
158
  return self.strategy
34
-
159
+
160
+ # Private methods
161
+
162
+ def _serialize_part(self, part: Part) -> dict:
163
+ """Serialize a Part to frontend-compatible format"""
164
+ part_data = part.model_dump()
165
+ if part.type == "file" and part.file:
166
+ if not part.file.bytes and not part.file.uri:
167
+ raise ValueError("FilePart must have either bytes or uri")
168
+ return part_data
169
+
170
+ def _prepare_update_payload(self, state: StateData) -> Dict[str, Any]:
171
+ """Prepare NATS message payload from state data"""
172
+ return {
173
+ "taskId": state.task_id,
174
+ "parts": self._extract_artifact_parts(state.task),
175
+ "complete": state.task.status.state == TaskState.COMPLETED
176
+ }
177
+
178
+ def _extract_artifact_parts(self, task: Task) -> List[dict]:
179
+ """Extract and serialize parts from all artifacts"""
180
+ parts = []
181
+ if task.artifacts:
182
+ for artifact in task.artifacts:
183
+ for part in artifact.parts:
184
+ try:
185
+ parts.append(self._serialize_part(part))
186
+ except ValueError as e:
187
+ print(f"Invalid part in artifact: {e}")
188
+ 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, session_id: str) -> Optional[StateData]:
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, session_id: str, state_data: StateData) -> None:
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, session_id: str) -> None:
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 get_state(self, session_id: str) -> Optional[StateData]:
14
- return self.states.get(session_id)
13
+ def initialize_state(self, state_data: StateData) -> None:
14
+ self.states[state_data.task_id] = state_data
15
15
 
16
- def update_state(self, session_id: str, state_data: StateData):
17
- self.states[session_id] = state_data
16
+ def get_state(self, task_id: str) -> Optional[StateData]:
17
+ return self.states.get(task_id)
18
18
 
19
- def delete_state(self, session_id: str):
20
- if session_id in self.states:
21
- del self.states[session_id]
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
@@ -2,7 +2,7 @@
2
2
  from typing import Optional, List
3
3
 
4
4
  # Local imports
5
- from smarta2a.client.tools_manager import ToolsManager
5
+ from smarta2a.utils.tools_manager import ToolsManager
6
6
  from smarta2a.utils.types import AgentCard
7
7
 
8
8
  def build_system_prompt(
@@ -0,0 +1,108 @@
1
+ # Library imports
2
+ import json
3
+ from typing import List, Dict, Any, Union, Literal, Optional
4
+
5
+ # Local imports
6
+ from smarta2a.client.mcp_client import MCPClient
7
+ from smarta2a.client.a2a_client import A2AClient
8
+ from smarta2a.utils.types import AgentCard, Tool
9
+
10
+ class ToolsManager:
11
+ """
12
+ Manages loading, describing, and invoking tools from various providers.
13
+ Acts as a wrapper around the MCP and A2A clients.
14
+ """
15
+ def __init__(self):
16
+ self.tools_list: List[Any] = []
17
+ self.clients: Dict[str, Union[MCPClient, A2AClient]] = {}
18
+
19
+ async def load_mcp_tools(self, urls_or_paths: List[str]) -> None:
20
+ for url in urls_or_paths:
21
+ mcp_client = await MCPClient.create(url)
22
+ tools = await mcp_client.list_tools()
23
+ for tool in tools:
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
32
+
33
+ async def load_a2a_tools(self, agent_cards: List[AgentCard]) -> None:
34
+ for agent_card in agent_cards:
35
+ a2a_client = A2AClient(agent_card)
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
+
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
+
60
+ validated_tool = Tool(
61
+ key=key,
62
+ **tool_dict
63
+ )
64
+ self.tools_list.append(validated_tool)
65
+ self.clients[key] = a2a_client
66
+
67
+ def get_tools(self) -> List[Any]:
68
+ return self.tools_list
69
+
70
+
71
+ def describe_tools(self, client_type: Literal["mcp", "a2a"]) -> str:
72
+ lines = []
73
+ for tool in self.tools_list:
74
+ schema = json.dumps(tool.inputSchema, indent=2) # Fix: use inputSchema
75
+ if client_type == "mcp":
76
+ lines.append(
77
+ f"- **{tool.key}**: {tool.description}\n Parameters schema:\n ```json\n{schema}\n```"
78
+ )
79
+ elif client_type == "a2a":
80
+ lines.append(
81
+ f"- **{tool.key}**: {tool.description}\n Parameters schema:\n ```json\n{schema}\n```"
82
+ )
83
+
84
+ return "\n".join(lines)
85
+
86
+ def get_client(self, tool_key: str) -> Any:
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
96
+
97
+ except Exception as e:
98
+ # This will catch ANY error in the body above
99
+ raise
100
+
101
+ def _get_tool_name(self, tool_key: str) -> str:
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,11 +479,26 @@ class A2AStreamResponse(BaseModel):
479
479
  metadata: dict[str, Any] | None = None
480
480
 
481
481
  class StateData(BaseModel):
482
- sessionId: str
483
- history: List[Message]
484
- metadata: Dict[str, Any]
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):
488
+ key: str
487
489
  name: str
488
490
  description: str
489
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