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/agent/a2a_agent.py
CHANGED
@@ -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.
|
8
|
-
from smarta2a.
|
9
|
-
from smarta2a.
|
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
|
-
|
19
|
-
|
18
|
+
agent_card: AgentCard = None,
|
19
|
+
state_manager: StateManager = None,
|
20
20
|
):
|
21
21
|
self.model_provider = model_provider
|
22
|
-
self.
|
23
|
-
self.state_store = state_store or InMemoryStateStore()
|
22
|
+
self.state_manager = state_manager
|
24
23
|
self.app = SmartA2A(
|
25
24
|
name=name,
|
26
|
-
|
27
|
-
|
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.
|
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
|
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
|
-
|
33
|
-
|
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=
|
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
|
-
|
53
|
-
|
54
|
-
|
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:
|
smarta2a/client/a2a_client.py
CHANGED
@@ -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
|
-
|
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
|
47
|
+
text: str,
|
45
48
|
data: dict[str, Any] | None = None,
|
46
49
|
file_uri: str | None = None,
|
47
|
-
|
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:
|
@@ -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:]
|
@@ -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
|
6
|
+
from smarta2a.utils.types import StateData
|
7
7
|
|
8
8
|
class BaseLLMProvider(ABC):
|
9
9
|
@abstractmethod
|
10
|
-
async def generate(self,
|
10
|
+
async def generate(self, state: StateData, **kwargs) -> str:
|
11
11
|
pass
|
12
12
|
|
13
13
|
@abstractmethod
|
14
|
-
async def generate_stream(self,
|
14
|
+
async def generate_stream(self, state: StateData, **kwargs) -> AsyncGenerator[str, None]:
|
15
15
|
pass
|