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.
Files changed (40) 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 +33 -6
  7. smarta2a/history_update_strategies/rolling_window_strategy.py +16 -0
  8. smarta2a/model_providers/__init__.py +1 -1
  9. smarta2a/model_providers/base_llm_provider.py +3 -3
  10. smarta2a/model_providers/openai_provider.py +126 -89
  11. smarta2a/server/json_rpc_request_processor.py +130 -0
  12. smarta2a/server/nats_client.py +49 -0
  13. smarta2a/server/request_handler.py +667 -0
  14. smarta2a/server/send_task_handler.py +174 -0
  15. smarta2a/server/server.py +124 -726
  16. smarta2a/server/state_manager.py +171 -20
  17. smarta2a/server/webhook_request_processor.py +112 -0
  18. smarta2a/state_stores/base_state_store.py +3 -3
  19. smarta2a/state_stores/inmemory_state_store.py +21 -7
  20. smarta2a/utils/agent_discovery_manager.py +121 -0
  21. smarta2a/utils/prompt_helpers.py +1 -1
  22. smarta2a/{client → utils}/tools_manager.py +39 -8
  23. smarta2a/utils/types.py +17 -3
  24. {smarta2a-0.3.1.dist-info → smarta2a-0.4.1.dist-info}/METADATA +7 -4
  25. smarta2a-0.4.1.dist-info/RECORD +40 -0
  26. smarta2a-0.4.1.dist-info/licenses/LICENSE +35 -0
  27. smarta2a/examples/__init__.py +0 -0
  28. smarta2a/examples/echo_server/__init__.py +0 -0
  29. smarta2a/examples/echo_server/curl.txt +0 -1
  30. smarta2a/examples/echo_server/main.py +0 -39
  31. smarta2a/examples/openai_airbnb_agent/__init__.py +0 -0
  32. smarta2a/examples/openai_airbnb_agent/main.py +0 -33
  33. smarta2a/examples/openai_delegator_agent/__init__.py +0 -0
  34. smarta2a/examples/openai_delegator_agent/main.py +0 -51
  35. smarta2a/examples/openai_weather_agent/__init__.py +0 -0
  36. smarta2a/examples/openai_weather_agent/main.py +0 -32
  37. smarta2a/server/subscription_service.py +0 -109
  38. smarta2a-0.3.1.dist-info/RECORD +0 -42
  39. smarta2a-0.3.1.dist-info/licenses/LICENSE +0 -21
  40. {smarta2a-0.3.1.dist-info → smarta2a-0.4.1.dist-info}/WHEEL +0 -0
@@ -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, store: Optional[BaseStateStore], history_strategy: HistoryUpdateStrategy):
12
- self.store = store
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
- 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
75
+ await self.update_state(latest_state)
24
76
 
25
- def update(self, state: StateData):
26
- if self.store:
27
- self.store.update_state(state.sessionId, state)
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.store
152
+ return self.state_store
31
153
 
32
- def get_strategy(self) -> HistoryUpdateStrategy:
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, 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(
@@ -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
- 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)
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
- 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):
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.1
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 a A2A compliant server. If you're using another Agent framework like LangGraph you can easily use it it tandem with this server.
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 and file responses
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 Airbnb agent: No need to do anything - just ensure that node and npx are installed on your machine
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,,