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,30 +1,29 @@
1
1
  # Library imports
2
-
2
+ from fastapi import Query
3
+ from fastapi.responses import JSONResponse
4
+ from typing import Optional
3
5
 
4
6
  # Local imports
5
7
  from smarta2a.server import SmartA2A
6
8
  from smarta2a.model_providers.base_llm_provider import BaseLLMProvider
7
- from smarta2a.history_update_strategies.history_update_strategy import HistoryUpdateStrategy
8
- from smarta2a.history_update_strategies.append_strategy import AppendStrategy
9
- from smarta2a.state_stores.base_state_store import BaseStateStore
10
- from smarta2a.state_stores.inmemory_state_store import InMemoryStateStore
11
- from smarta2a.utils.types import StateData, SendTaskRequest
9
+ from smarta2a.server.state_manager import StateManager
10
+ from smarta2a.utils.types import StateData, SendTaskRequest, AgentCard, WebhookRequest, WebhookResponse, TextPart, DataPart, FilePart
11
+ from smarta2a.client.a2a_client import A2AClient
12
12
 
13
13
  class A2AAgent:
14
14
  def __init__(
15
15
  self,
16
16
  name: str,
17
17
  model_provider: BaseLLMProvider,
18
- history_update_strategy: HistoryUpdateStrategy = None,
19
- state_store: BaseStateStore = None,
18
+ agent_card: AgentCard = None,
19
+ state_manager: StateManager = None,
20
20
  ):
21
21
  self.model_provider = model_provider
22
- self.history_update_strategy = history_update_strategy or AppendStrategy()
23
- self.state_store = state_store or InMemoryStateStore()
22
+ self.state_manager = state_manager
24
23
  self.app = SmartA2A(
25
24
  name=name,
26
- history_update_strategy=self.history_update_strategy,
27
- state_store=self.state_store
25
+ agent_card=agent_card,
26
+ state_manager=self.state_manager
28
27
  )
29
28
  self.__register_handlers()
30
29
 
@@ -32,12 +31,23 @@ class A2AAgent:
32
31
  @self.app.on_event("startup")
33
32
  async def on_startup():
34
33
  await self.model_provider.load()
35
-
36
- @self.app.on_send_task()
34
+
35
+ @self.app.app.get("/tasks")
36
+ async def get_tasks(fields: Optional[str] = Query(None)):
37
+ state_store = self.state_manager.get_store()
38
+ tasks_data = state_store.get_all_tasks(fields)
39
+ return JSONResponse(content=tasks_data)
40
+
41
+ @self.app.on_send_task(forward_to_webhook=False)
37
42
  async def on_send_task(request: SendTaskRequest, state: StateData):
38
- response = await self.model_provider.generate(state.history)
43
+ response = await self.model_provider.generate(state)
39
44
  return response
40
45
 
46
+ @self.app.webhook()
47
+ async def on_webhook(request: WebhookRequest, state: StateData):
48
+ response = await self.model_provider.generate(state)
49
+ return response
50
+
41
51
  def get_app(self):
42
52
  return self.app
43
53
 
@@ -0,0 +1,56 @@
1
+ # TODO: Implement a human agent that can be used to interact with the A2A server
2
+
3
+ # Library imports
4
+ from fastapi import Query
5
+ from fastapi.responses import JSONResponse
6
+ from typing import Optional
7
+
8
+ # Local imports
9
+ from smarta2a.server import SmartA2A
10
+ from smarta2a.model_providers.base_llm_provider import BaseLLMProvider
11
+ from smarta2a.server.state_manager import StateManager
12
+ from smarta2a.utils.types import StateData, SendTaskRequest, AgentCard, WebhookRequest, WebhookResponse, TextPart, DataPart, FilePart
13
+ from smarta2a.client.a2a_client import A2AClient
14
+
15
+ class A2AHuman:
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ model_provider: BaseLLMProvider,
20
+ agent_card: AgentCard = None,
21
+ state_manager: StateManager = None,
22
+ ):
23
+ self.model_provider = model_provider
24
+ self.state_manager = state_manager
25
+ self.app = SmartA2A(
26
+ name=name,
27
+ agent_card=agent_card,
28
+ state_manager=self.state_manager
29
+ )
30
+ self.__register_handlers()
31
+
32
+ def __register_handlers(self):
33
+ @self.app.on_event("startup")
34
+ async def on_startup():
35
+ await self.model_provider.load()
36
+
37
+ @self.app.app.get("/tasks")
38
+ async def get_tasks(fields: Optional[str] = Query(None)):
39
+ state_store = self.state_mgr.get_store()
40
+ tasks_data = state_store.get_all_tasks(fields)
41
+ return JSONResponse(content=tasks_data)
42
+
43
+ @self.app.on_send_task(forward_to_webhook=True)
44
+ async def on_send_task(request: SendTaskRequest, state: StateData):
45
+ return "Give me some time to respond"
46
+
47
+ @self.app.webhook()
48
+ async def on_webhook(request: WebhookRequest, state: StateData):
49
+ pass
50
+
51
+ def get_app(self):
52
+ return self.app
53
+
54
+
55
+
56
+
@@ -61,3 +61,50 @@ class SmartMCPClient:
61
61
  if session_id:
62
62
  headers["x-session-id"] = session_id
63
63
  return headers
64
+
65
+
66
+
67
+ '''
68
+ @self.app.app.post("/callback")
69
+ async def callback(request: CallbackResponse):
70
+
71
+ # This callback updates the task history and the state data in the state store for that task
72
+
73
+ # Get task id and task
74
+ task_id = request.result.id
75
+ task = request.result
76
+
77
+ # Get state data based on task id
78
+ state_data = self.state_store.get_state(task_id)
79
+
80
+ # Extract the messages from the task artifacts
81
+ messages = []
82
+ if task.artifacts:
83
+ for artifact in task.artifacts:
84
+ messages.append(Message(
85
+ role="agent",
86
+ parts=artifact.parts,
87
+ metadata=artifact.metadata
88
+ ))
89
+
90
+ # Update the history
91
+ history = state_data.task.history.copy()
92
+ history.extend(messages)
93
+ state_data.task.history = history
94
+
95
+ # Update context history with a strategy - this is the history that will be passed to an LLM call
96
+ context_history = self.history_update_strategy.update_history(
97
+ existing_history=state_data.context_history,
98
+ new_messages=messages
99
+ )
100
+
101
+ # Update the task
102
+ task.history = history
103
+
104
+ # Update state store
105
+ self.state_store.update_state(task_id, StateData(task_id=task_id, task=task, context_history=context_history))
106
+
107
+ # Call on_send_task
108
+ await self.on_send_task(request)
109
+
110
+ '''
@@ -0,0 +1,85 @@
1
+ # Library imports
2
+ from typing import Optional, List, Dict, Any, AsyncGenerator, Union
3
+ from datetime import datetime
4
+ from collections import defaultdict
5
+ from uuid import uuid4
6
+ from fastapi.responses import StreamingResponse
7
+ from sse_starlette.sse import EventSourceResponse
8
+
9
+ # Local imports
10
+ from smarta2a.server.handler_registry import HandlerRegistry
11
+ from smarta2a.server.state_manager import StateManager
12
+ from smarta2a.utils.types import (
13
+ Message, StateData, SendTaskStreamingRequest, SendTaskStreamingResponse,
14
+ TaskSendParams, A2AStatus, A2AStreamResponse, TaskStatusUpdateEvent,
15
+ TaskStatus, TaskState, TaskArtifactUpdateEvent, Artifact, TextPart,
16
+ FilePart, DataPart, FileContent, MethodNotFoundError, TaskNotFoundError,
17
+ InternalError
18
+ )
19
+
20
+ class SubscriptionService:
21
+ def __init__(self, registry: HandlerRegistry, state_mgr: StateManager):
22
+ self.registry = registry
23
+ self.state_mgr = state_mgr
24
+
25
+ async def subscribe(self, request: SendTaskStreamingRequest, state: Optional[StateData]) -> StreamingResponse:
26
+ handler = self.registry.get_subscription("tasks/sendSubscribe")
27
+ if not handler:
28
+ err = SendTaskStreamingResponse(jsonrpc="2.0", id=request.id, error=MethodNotFoundError()).model_dump_json()
29
+ return EventSourceResponse(err)
30
+
31
+ task_id = state.task_id if state else request.params.id or str(uuid4())
32
+ context_history = state.context_history.copy() if state else [request.params.message]
33
+ task_history = state.task_history.copy() if state else [request.params.message]
34
+ metadata = state.metadata.copy() if state else (request.params.metadata or {})
35
+
36
+ async def event_stream():
37
+ try:
38
+ events = handler(request, state) if state else handler(request)
39
+ async for ev in self._normalize(request.params, events, context_history.copy(), task_history.copy(), metadata.copy(), task_id):
40
+ yield f"data: {ev}\n\n"
41
+ except Exception as e:
42
+ err = TaskNotFoundError() if 'not found' in str(e).lower() else InternalError(data=str(e))
43
+ msg = SendTaskStreamingResponse(jsonrpc="2.0", id=request.id, error=err).model_dump_json()
44
+ yield f"data: {msg}\n\n"
45
+
46
+ return StreamingResponse(event_stream(), media_type="text/event-stream; charset=utf-8")
47
+
48
+ async def _normalize(self, params: TaskSendParams, events: AsyncGenerator, context_history: List[Message], task_history: List[Message], metadata: Dict[str, Any], task_id: str) -> AsyncGenerator[Union[TaskStatusUpdateEvent, TaskArtifactUpdateEvent], None]:
49
+ async for evt in events:
50
+ if isinstance(evt, TaskArtifactUpdateEvent):
51
+ # Create agent message from artifact parts
52
+ agent_message = Message(
53
+ role="agent",
54
+ parts=[p for p in evt.artifact.parts],
55
+ metadata=evt.artifact.metadata
56
+ )
57
+
58
+ # Update task history (always append)
59
+ task_history.append(agent_message)
60
+ # Update context history using strategy
61
+ new_context_history = self.state_mgr.strategy.update_history(context_history, [agent_message])
62
+
63
+ # Merge metadata
64
+ new_metadata = {
65
+ **metadata,
66
+ **(evt.artifact.metadata or {})
67
+ }
68
+
69
+ # Update state store if configured
70
+ if self.state_mgr.store:
71
+ self.state_mgr.store.update_state(
72
+ task_id=task_id,
73
+ state_data=StateData(
74
+ task_id=task_id,
75
+ context_history=new_context_history,
76
+ task_history=task_history,
77
+ metadata=new_metadata
78
+ )
79
+ )
80
+
81
+ # Update streaming state
82
+ context_history = new_context_history
83
+ metadata = new_metadata
84
+
85
+ yield evt
@@ -29,8 +29,9 @@ class TaskService:
29
29
  if not handler:
30
30
  return SendTaskResponse(id=request.id, error=MethodNotFoundError())
31
31
 
32
- session_id = state.sessionId if state else request.params.sessionId or str(uuid4())
33
- history = state.history.copy() if state else [request.params.message]
32
+ task_id = state.task_id if state else request.params.id or str(uuid4())
33
+ context_history = state.context_history.copy() if state else [request.params.message]
34
+ task_history = state.task_history.copy() if state else [request.params.message]
34
35
  metadata = state.metadata.copy() if state else (request.params.metadata or {})
35
36
 
36
37
  try:
@@ -40,18 +41,26 @@ class TaskService:
40
41
 
41
42
  task = self.builder.build(
42
43
  content=raw,
43
- task_id=request.params.id,
44
- session_id=session_id,
44
+ task_id=task_id,
45
+ session_id=task_id, # Use task_id as session_id for backward compatibility
45
46
  metadata=metadata,
46
- history=history
47
+ history=context_history # Use context_history for task history
47
48
  )
48
49
 
49
50
  if task.artifacts:
50
51
  parts = [p for a in task.artifacts for p in a.parts]
51
52
  agent_msg = Message(role="agent", parts=parts, metadata=task.metadata)
52
- new_hist = self.state_mgr.strategy.update_history(history, [agent_msg])
53
- task.history = new_hist
54
- self.state_mgr.update(StateData(sessionId=session_id, history=new_hist, metadata=metadata))
53
+ # Update task history (always append)
54
+ task_history.append(agent_msg)
55
+ # Update context history using strategy
56
+ new_context_history = self.state_mgr.strategy.update_history(context_history, [agent_msg])
57
+ task.history = new_context_history # Use context_history for task history
58
+ self.state_mgr.update(StateData(
59
+ task_id=task_id,
60
+ context_history=new_context_history,
61
+ task_history=task_history,
62
+ metadata=metadata
63
+ ))
55
64
 
56
65
  return SendTaskResponse(id=request.id, result=task)
57
66
  except JSONRPCError as e:
@@ -23,6 +23,9 @@ from smarta2a.utils.types import (
23
23
  CancelTaskResponse,
24
24
  SetTaskPushNotificationResponse,
25
25
  GetTaskPushNotificationResponse,
26
+ WebhookRequest,
27
+ WebhookResponse,
28
+ Task,
26
29
  )
27
30
  from smarta2a.utils.task_request_builder import TaskRequestBuilder
28
31
 
@@ -34,30 +37,30 @@ class A2AClient:
34
37
  elif url:
35
38
  self.url = url
36
39
  else:
37
- raise ValueError("Must provide either agent_card or url")
40
+ pass
38
41
 
39
42
  async def send(
40
43
  self,
41
44
  *,
42
45
  id: str,
43
46
  role: Literal["user", "agent"] = "user",
44
- text: str | None = None,
47
+ text: str,
45
48
  data: dict[str, Any] | None = None,
46
49
  file_uri: str | None = None,
47
- session_id: str | None = None,
50
+ sessionId: str | None = None,
48
51
  accepted_output_modes: list[str] | None = None,
49
52
  push_notification: PushNotificationConfig | None = None,
50
53
  history_length: int | None = None,
51
54
  metadata: dict[str, Any] | None = None,
52
55
  ):
53
- """Send a task to another Agent"""
56
+ """Send a task to another Agent."""
54
57
  params = TaskRequestBuilder.build_send_task_request(
55
58
  id=id,
56
59
  role=role,
57
60
  text=text,
58
61
  data=data,
59
62
  file_uri=file_uri,
60
- session_id=session_id,
63
+ session_id=sessionId, # Need to amend the TaskRequestBuilder at a later time to take sessionId not session_id
61
64
  accepted_output_modes=accepted_output_modes,
62
65
  push_notification=push_notification,
63
66
  history_length=history_length,
@@ -80,7 +83,7 @@ class A2AClient:
80
83
  history_length: int | None = None,
81
84
  metadata: dict[str, Any] | None = None,
82
85
  ):
83
- """Send to another Agent and receive a stream of responses"""
86
+ """Send to another Agent and receive a stream of responses."""
84
87
  params = TaskRequestBuilder.build_send_task_request(
85
88
  id=id,
86
89
  role=role,
@@ -152,6 +155,16 @@ class A2AClient:
152
155
  req = TaskRequestBuilder.get_push_notification(id, metadata)
153
156
  raw = await self._send_request(req)
154
157
  return GetTaskPushNotificationResponse(**raw)
158
+
159
+ async def send_to_webhook(
160
+ self,
161
+ webhook_url: str,
162
+ id: str,
163
+ task: Task
164
+ ):
165
+ """Send a task to another Agent"""
166
+ request = WebhookRequest(id=id, result=task)
167
+ return WebhookResponse(**await self._send_webhook_request(webhook_url, request))
155
168
 
156
169
 
157
170
  async def _send_request(self, request: JSONRPCRequest) -> dict[str, Any]:
@@ -167,6 +180,20 @@ class A2AClient:
167
180
  raise A2AClientHTTPError(e.response.status_code, str(e)) from e
168
181
  except json.JSONDecodeError as e:
169
182
  raise A2AClientJSONError(str(e)) from e
183
+
184
+ async def _send_webhook_request(self, webhook_url: str, request: WebhookRequest) -> dict[str, Any]:
185
+ async with httpx.AsyncClient() as client:
186
+ try:
187
+ # Image generation could take time, adding timeout
188
+ response = await client.post(
189
+ webhook_url, json=request.model_dump(), timeout=30
190
+ )
191
+ response.raise_for_status()
192
+ return response.json()
193
+ except httpx.HTTPStatusError as e:
194
+ raise A2AClientHTTPError(e.response.status_code, str(e)) from e
195
+ except json.JSONDecodeError as e:
196
+ raise A2AClientJSONError(str(e)) from e
170
197
 
171
198
  async def _send_streaming_request(self, request: JSONRPCRequest) -> AsyncIterable[SendTaskStreamingResponse]:
172
199
  with httpx.Client(timeout=None) as client:
@@ -234,7 +261,7 @@ class A2AClient:
234
261
 
235
262
  properties[param_name] = schema_field
236
263
 
237
- input_schema = {
264
+ inputSchema = {
238
265
  "title": f"{name}_Arguments",
239
266
  "type": "object",
240
267
  "properties": properties,
@@ -244,7 +271,7 @@ class A2AClient:
244
271
  tools.append({
245
272
  "name": name,
246
273
  "description": description,
247
- "inputSchema": input_schema
274
+ "inputSchema": inputSchema
248
275
  })
249
276
 
250
277
  return tools
@@ -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))
@@ -0,0 +1,16 @@
1
+ # Library imports
2
+ from typing import List
3
+
4
+ # Local imports
5
+ from smarta2a.utils.types import Message
6
+
7
+ class RollingWindowStrategy:
8
+ def __init__(self, window_size: int):
9
+ if window_size < 1:
10
+ raise ValueError("window_size must be at least 1")
11
+ self.window_size = window_size
12
+
13
+ """Default append behavior"""
14
+ def update_history(self, existing_history: List[Message], new_messages: List[Message]) -> List[Message]:
15
+ combined = existing_history + new_messages
16
+ return combined[-self.window_size:]
@@ -1,5 +1,5 @@
1
1
  """
2
2
  Model provider implementations for different AI models.
3
3
  """
4
-
4
+
5
5
  # This package is currently empty but will contain model provider implementations
@@ -3,13 +3,13 @@ from abc import ABC, abstractmethod
3
3
  from typing import AsyncGenerator, List
4
4
 
5
5
  # Local imports
6
- from smarta2a.utils.types import Message
6
+ from smarta2a.utils.types import StateData
7
7
 
8
8
  class BaseLLMProvider(ABC):
9
9
  @abstractmethod
10
- async def generate(self, messages: List[Message], **kwargs) -> str:
10
+ async def generate(self, state: StateData, **kwargs) -> str:
11
11
  pass
12
12
 
13
13
  @abstractmethod
14
- async def generate_stream(self, messages: List[Message], **kwargs) -> AsyncGenerator[str, None]:
14
+ async def generate_stream(self, state: StateData, **kwargs) -> AsyncGenerator[str, None]:
15
15
  pass