smarta2a 0.2.2__py3-none-any.whl → 0.2.3__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/__init__.py +1 -1
- smarta2a/agent/a2a_agent.py +38 -0
- smarta2a/agent/a2a_mcp_server.py +37 -0
- smarta2a/archive/mcp_client.py +86 -0
- smarta2a/client/a2a_client.py +97 -3
- smarta2a/client/smart_mcp_client.py +60 -0
- smarta2a/client/tools_manager.py +58 -0
- smarta2a/history_update_strategies/__init__.py +8 -0
- smarta2a/history_update_strategies/append_strategy.py +10 -0
- smarta2a/history_update_strategies/history_update_strategy.py +15 -0
- smarta2a/model_providers/__init__.py +5 -0
- smarta2a/model_providers/base_llm_provider.py +15 -0
- smarta2a/model_providers/openai_provider.py +281 -0
- smarta2a/server/handler_registry.py +23 -0
- smarta2a/server/server.py +225 -252
- smarta2a/server/state_manager.py +34 -0
- smarta2a/server/subscription_service.py +109 -0
- smarta2a/server/task_service.py +155 -0
- smarta2a/state_stores/__init__.py +8 -0
- smarta2a/state_stores/base_state_store.py +20 -0
- smarta2a/state_stores/inmemory_state_store.py +21 -0
- smarta2a/utils/prompt_helpers.py +38 -0
- smarta2a/utils/task_builder.py +153 -0
- smarta2a/{common → utils}/task_request_builder.py +1 -1
- smarta2a/{common → utils}/types.py +62 -2
- {smarta2a-0.2.2.dist-info → smarta2a-0.2.3.dist-info}/METADATA +12 -6
- smarta2a-0.2.3.dist-info/RECORD +32 -0
- smarta2a-0.2.2.dist-info/RECORD +0 -12
- /smarta2a/{common → utils}/__init__.py +0 -0
- {smarta2a-0.2.2.dist-info → smarta2a-0.2.3.dist-info}/WHEEL +0 -0
- {smarta2a-0.2.2.dist-info → smarta2a-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,109 @@
|
|
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
|
+
session_id = state.sessionId if state else request.params.sessionId or str(uuid4())
|
32
|
+
history = state.history.copy() if state else [request.params.message]
|
33
|
+
metadata = state.metadata.copy() if state else (request.params.metadata or {})
|
34
|
+
|
35
|
+
async def event_stream():
|
36
|
+
try:
|
37
|
+
events = handler(request, state) if state else handler(request)
|
38
|
+
async for ev in self._normalize(request.params, events, history.copy(), metadata.copy(), session_id):
|
39
|
+
yield f"data: {ev}\n\n"
|
40
|
+
except Exception as e:
|
41
|
+
err = TaskNotFoundError() if 'not found' in str(e).lower() else InternalError(data=str(e))
|
42
|
+
msg = SendTaskStreamingResponse(jsonrpc="2.0", id=request.id, error=err).model_dump_json()
|
43
|
+
yield f"data: {msg}\n\n"
|
44
|
+
|
45
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream; charset=utf-8")
|
46
|
+
|
47
|
+
async def _normalize(
|
48
|
+
self,
|
49
|
+
params: TaskSendParams,
|
50
|
+
events: AsyncGenerator,
|
51
|
+
history: List[Message],
|
52
|
+
metadata: Dict[str, Any],
|
53
|
+
session_id: str
|
54
|
+
) -> AsyncGenerator[str, None]:
|
55
|
+
artifact_state = defaultdict(lambda: {"index": 0, "last_chunk": False})
|
56
|
+
async for item in events:
|
57
|
+
if isinstance(item, SendTaskStreamingResponse):
|
58
|
+
yield item.model_dump_json()
|
59
|
+
continue
|
60
|
+
|
61
|
+
if isinstance(item, A2AStatus):
|
62
|
+
te = TaskStatusUpdateEvent(
|
63
|
+
id=params.id,
|
64
|
+
status=TaskStatus(state=TaskState(item.status), timestamp=datetime.now()),
|
65
|
+
final=item.final or (item.status.lower() == TaskState.COMPLETED),
|
66
|
+
metadata=item.metadata
|
67
|
+
)
|
68
|
+
yield SendTaskStreamingResponse(jsonrpc="2.0", id=params.id, result=te).model_dump_json()
|
69
|
+
continue
|
70
|
+
|
71
|
+
content_item = item
|
72
|
+
if not isinstance(item, A2AStreamResponse):
|
73
|
+
content_item = A2AStreamResponse(content=item)
|
74
|
+
|
75
|
+
parts: List[Union[TextPart, FilePart, DataPart]] = []
|
76
|
+
cont = content_item.content
|
77
|
+
if isinstance(cont, str): parts.append(TextPart(text=cont))
|
78
|
+
elif isinstance(cont, bytes): parts.append(FilePart(file=FileContent(bytes=cont)))
|
79
|
+
elif isinstance(cont, (TextPart, FilePart, DataPart)): parts.append(cont)
|
80
|
+
elif isinstance(cont, Artifact): parts.extend(cont.parts)
|
81
|
+
elif isinstance(cont, list):
|
82
|
+
for elem in cont:
|
83
|
+
if isinstance(elem, str): parts.append(TextPart(text=elem))
|
84
|
+
elif isinstance(elem, (TextPart, FilePart, DataPart)): parts.append(elem)
|
85
|
+
elif isinstance(elem, Artifact): parts.extend(elem.parts)
|
86
|
+
|
87
|
+
idx = content_item.index
|
88
|
+
state = artifact_state[idx]
|
89
|
+
evt = TaskArtifactUpdateEvent(
|
90
|
+
id=params.id,
|
91
|
+
artifact=Artifact(
|
92
|
+
parts=parts,
|
93
|
+
index=idx,
|
94
|
+
append=content_item.append or (state["index"] == idx),
|
95
|
+
lastChunk=content_item.final or state["last_chunk"],
|
96
|
+
metadata=content_item.metadata
|
97
|
+
)
|
98
|
+
)
|
99
|
+
if content_item.final:
|
100
|
+
state["last_chunk"] = True
|
101
|
+
state["index"] += 1
|
102
|
+
|
103
|
+
agent_msg = Message(role="agent", parts=evt.artifact.parts, metadata=evt.artifact.metadata)
|
104
|
+
new_hist = self.state_mgr.strategy.update_history(history, [agent_msg])
|
105
|
+
metadata = {**metadata, **(evt.artifact.metadata or {})}
|
106
|
+
self.state_mgr.update(StateData(session_id, new_hist, metadata))
|
107
|
+
history = new_hist
|
108
|
+
|
109
|
+
yield SendTaskStreamingResponse(jsonrpc="2.0", id=params.id, result=evt).model_dump_json()
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# Library imports
|
2
|
+
from typing import Optional, Union, Any
|
3
|
+
from uuid import uuid4
|
4
|
+
from fastapi import HTTPException
|
5
|
+
from pydantic import ValidationError
|
6
|
+
|
7
|
+
# Local imports
|
8
|
+
from smarta2a.server.handler_registry import HandlerRegistry
|
9
|
+
from smarta2a.server.state_manager import StateManager
|
10
|
+
from smarta2a.utils.task_builder import TaskBuilder
|
11
|
+
from smarta2a.utils.types import (
|
12
|
+
Message, StateData, SendTaskRequest, SendTaskResponse,
|
13
|
+
GetTaskRequest, GetTaskResponse, CancelTaskRequest, CancelTaskResponse,
|
14
|
+
SetTaskPushNotificationRequest, GetTaskPushNotificationRequest,
|
15
|
+
SetTaskPushNotificationResponse, GetTaskPushNotificationResponse,
|
16
|
+
TaskPushNotificationConfig, TaskState, A2AStatus,
|
17
|
+
JSONRPCError, MethodNotFoundError, InternalError, InvalidParamsError,
|
18
|
+
TaskNotCancelableError, UnsupportedOperationError
|
19
|
+
)
|
20
|
+
|
21
|
+
class TaskService:
|
22
|
+
def __init__(self, registry: HandlerRegistry, state_mgr: StateManager):
|
23
|
+
self.registry = registry
|
24
|
+
self.state_mgr = state_mgr
|
25
|
+
self.builder = TaskBuilder(default_status=TaskState.COMPLETED)
|
26
|
+
|
27
|
+
def send(self, request: SendTaskRequest, state: Optional[StateData]) -> SendTaskResponse:
|
28
|
+
handler = self.registry.get_handler("tasks/send")
|
29
|
+
if not handler:
|
30
|
+
return SendTaskResponse(id=request.id, error=MethodNotFoundError())
|
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]
|
34
|
+
metadata = state.metadata.copy() if state else (request.params.metadata or {})
|
35
|
+
|
36
|
+
try:
|
37
|
+
raw = handler(request, state) if state else handler(request)
|
38
|
+
if isinstance(raw, SendTaskResponse):
|
39
|
+
return raw
|
40
|
+
|
41
|
+
task = self.builder.build(
|
42
|
+
content=raw,
|
43
|
+
task_id=request.params.id,
|
44
|
+
session_id=session_id,
|
45
|
+
metadata=metadata,
|
46
|
+
history=history
|
47
|
+
)
|
48
|
+
|
49
|
+
if task.artifacts:
|
50
|
+
parts = [p for a in task.artifacts for p in a.parts]
|
51
|
+
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))
|
55
|
+
|
56
|
+
return SendTaskResponse(id=request.id, result=task)
|
57
|
+
except JSONRPCError as e:
|
58
|
+
return SendTaskResponse(id=request.id, error=e)
|
59
|
+
except Exception as e:
|
60
|
+
return SendTaskResponse(id=request.id, error=InternalError(data=str(e)))
|
61
|
+
|
62
|
+
def get(self, request: GetTaskRequest) -> GetTaskResponse:
|
63
|
+
handler = self.registry.get_handler("tasks/get")
|
64
|
+
if not handler:
|
65
|
+
return GetTaskResponse(id=request.id, error=MethodNotFoundError())
|
66
|
+
try:
|
67
|
+
raw = handler(request)
|
68
|
+
if isinstance(raw, GetTaskResponse):
|
69
|
+
return self._validate(raw, request)
|
70
|
+
|
71
|
+
task = self.builder.build(
|
72
|
+
content=raw,
|
73
|
+
task_id=request.params.id,
|
74
|
+
metadata=request.params.metadata or {}
|
75
|
+
)
|
76
|
+
return self._finalize(request, task)
|
77
|
+
except JSONRPCError as e:
|
78
|
+
return GetTaskResponse(id=request.id, error=e)
|
79
|
+
except Exception as e:
|
80
|
+
return GetTaskResponse(id=request.id, error=InternalError(data=str(e)))
|
81
|
+
|
82
|
+
def cancel(self, request: CancelTaskRequest) -> CancelTaskResponse:
|
83
|
+
handler = self.registry.get_handler("tasks/cancel")
|
84
|
+
if not handler:
|
85
|
+
return CancelTaskResponse(id=request.id, error=MethodNotFoundError())
|
86
|
+
try:
|
87
|
+
raw = handler(request)
|
88
|
+
if isinstance(raw, CancelTaskResponse):
|
89
|
+
return self._validate(raw, request)
|
90
|
+
|
91
|
+
if isinstance(raw, A2AStatus):
|
92
|
+
task = self.builder.normalize_from_status(status=raw.status, task_id=request.params.id, metadata=raw.metadata or {})
|
93
|
+
else:
|
94
|
+
task = self.builder.build(content=raw, task_id=request.params.id, metadata=raw.metadata or {})
|
95
|
+
|
96
|
+
if task.id != request.params.id:
|
97
|
+
raise InvalidParamsError(data=f"Task ID mismatch: {task.id} vs {request.params.id}")
|
98
|
+
if task.status.state not in [TaskState.CANCELED, TaskState.COMPLETED]:
|
99
|
+
raise TaskNotCancelableError()
|
100
|
+
|
101
|
+
return CancelTaskResponse(id=request.id, result=task)
|
102
|
+
except JSONRPCError as e:
|
103
|
+
return CancelTaskResponse(id=request.id, error=e)
|
104
|
+
except (InvalidParamsError, TaskNotCancelableError) as e:
|
105
|
+
return CancelTaskResponse(id=request.id, error=e)
|
106
|
+
except HTTPException as e:
|
107
|
+
if e.status_code == 405:
|
108
|
+
return CancelTaskResponse(id=request.id, error=UnsupportedOperationError())
|
109
|
+
return CancelTaskResponse(id=request.id, error=InternalError(data=str(e)))
|
110
|
+
except Exception as e:
|
111
|
+
return CancelTaskResponse(id=request.id, error=InternalError(data=str(e)))
|
112
|
+
|
113
|
+
def set_notification(self, request: SetTaskPushNotificationRequest) -> SetTaskPushNotificationResponse:
|
114
|
+
handler = self.registry.get_handler("tasks/pushNotification/set")
|
115
|
+
if not handler:
|
116
|
+
return SetTaskPushNotificationResponse(id=request.id, error=MethodNotFoundError())
|
117
|
+
try:
|
118
|
+
raw = handler(request)
|
119
|
+
if raw is None:
|
120
|
+
return SetTaskPushNotificationResponse(id=request.id, result=request.params)
|
121
|
+
if isinstance(raw, SetTaskPushNotificationResponse):
|
122
|
+
return raw
|
123
|
+
except JSONRPCError as e:
|
124
|
+
return SetTaskPushNotificationResponse(id=request.id, error=e)
|
125
|
+
except Exception as e:
|
126
|
+
return SetTaskPushNotificationResponse(id=request.id, error=InternalError(data=str(e)))
|
127
|
+
|
128
|
+
def get_notification(self, request: GetTaskPushNotificationRequest) -> GetTaskPushNotificationResponse:
|
129
|
+
handler = self.registry.get_handler("tasks/pushNotification/get")
|
130
|
+
if not handler:
|
131
|
+
return GetTaskPushNotificationResponse(id=request.id, error=MethodNotFoundError())
|
132
|
+
try:
|
133
|
+
raw = handler(request)
|
134
|
+
if isinstance(raw, GetTaskPushNotificationResponse):
|
135
|
+
return raw
|
136
|
+
cfg = TaskPushNotificationConfig.model_validate(raw)
|
137
|
+
return GetTaskPushNotificationResponse(id=request.id, result=cfg)
|
138
|
+
except ValidationError as e:
|
139
|
+
return GetTaskPushNotificationResponse(id=request.id, error=InvalidParamsError(data=e.errors()))
|
140
|
+
except JSONRPCError as e:
|
141
|
+
return GetTaskPushNotificationResponse(id=request.id, error=e)
|
142
|
+
except Exception as e:
|
143
|
+
return GetTaskPushNotificationResponse(id=request.id, error=InternalError(data=str(e)))
|
144
|
+
|
145
|
+
def _validate(self, resp: Union[SendTaskResponse, GetTaskResponse, CancelTaskResponse], req) -> Any:
|
146
|
+
if resp.result and resp.result.id != req.params.id:
|
147
|
+
return type(resp)(id=req.id, error=InvalidParamsError(data=f"Task ID mismatch: {resp.result.id} vs {req.params.id}"))
|
148
|
+
return resp
|
149
|
+
|
150
|
+
def _finalize(self, request: GetTaskRequest, task) -> GetTaskResponse:
|
151
|
+
if task.id != request.params.id:
|
152
|
+
return GetTaskResponse(id=request.id, error=InvalidParamsError(data=f"Task ID mismatch: {task.id} vs {request.params.id}"))
|
153
|
+
if request.params.historyLength and task.history:
|
154
|
+
task.history = task.history[-request.params.historyLength:]
|
155
|
+
return GetTaskResponse(id=request.id, result=task)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Library imports
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import Optional, List, Dict, Any
|
4
|
+
|
5
|
+
# Local imports
|
6
|
+
from smarta2a.utils.types import StateData, Message
|
7
|
+
|
8
|
+
class BaseStateStore(ABC):
|
9
|
+
|
10
|
+
@abstractmethod
|
11
|
+
async def get_state(self, session_id: str) -> Optional[StateData]:
|
12
|
+
pass
|
13
|
+
|
14
|
+
@abstractmethod
|
15
|
+
async def update_state(self, session_id: str, state_data: StateData) -> None:
|
16
|
+
pass
|
17
|
+
|
18
|
+
@abstractmethod
|
19
|
+
async def delete_state(self, session_id: str) -> None:
|
20
|
+
pass
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Library imports
|
2
|
+
from typing import Dict, Any, Optional, List
|
3
|
+
import uuid
|
4
|
+
|
5
|
+
# Local imports
|
6
|
+
from smarta2a.state_stores.base_state_store import BaseStateStore
|
7
|
+
from smarta2a.utils.types import StateData, Message
|
8
|
+
|
9
|
+
class InMemoryStateStore(BaseStateStore):
|
10
|
+
def __init__(self):
|
11
|
+
self.states: Dict[str, StateData] = {}
|
12
|
+
|
13
|
+
def get_state(self, session_id: str) -> Optional[StateData]:
|
14
|
+
return self.states.get(session_id)
|
15
|
+
|
16
|
+
def update_state(self, session_id: str, state_data: StateData):
|
17
|
+
self.states[session_id] = state_data
|
18
|
+
|
19
|
+
def delete_state(self, session_id: str):
|
20
|
+
if session_id in self.states:
|
21
|
+
del self.states[session_id]
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Library imports
|
2
|
+
from typing import Optional, List
|
3
|
+
|
4
|
+
# Local imports
|
5
|
+
from smarta2a.client.tools_manager import ToolsManager
|
6
|
+
from smarta2a.utils.types import AgentCard
|
7
|
+
|
8
|
+
def build_system_prompt(
|
9
|
+
base_prompt: Optional[str],
|
10
|
+
tools_manager: ToolsManager,
|
11
|
+
mcp_server_urls_or_paths: Optional[List[str]] = None,
|
12
|
+
agent_cards: Optional[List[AgentCard]] = None
|
13
|
+
) -> str:
|
14
|
+
"""
|
15
|
+
Compose the final system prompt by combining the base prompt
|
16
|
+
with a clear listing of available tools.
|
17
|
+
"""
|
18
|
+
header = base_prompt or "You are a helpful assistant with access to the following tools:"
|
19
|
+
|
20
|
+
if mcp_server_urls_or_paths:
|
21
|
+
mcp_tools_desc = tools_manager.describe_tools("mcp")
|
22
|
+
header += f"\n\nAvailable tools:\n{mcp_tools_desc}"
|
23
|
+
|
24
|
+
if agent_cards:
|
25
|
+
a2a_tools_desc = tools_manager.describe_tools("a2a")
|
26
|
+
header += f"\n\nIf needed, you can delegate parts of your task to other agents. The Agents you can use are:\n{_print_agent_list(agent_cards)}\n\nUse the following tools to send tasks to an agent:\n{a2a_tools_desc}"
|
27
|
+
|
28
|
+
return header
|
29
|
+
|
30
|
+
|
31
|
+
def _print_agent_list(agents: List[AgentCard]) -> None:
|
32
|
+
"""Prints multiple agents with separators"""
|
33
|
+
separator = "---"
|
34
|
+
agent_strings = [agent.pretty_print(include_separators=False) for agent in agents]
|
35
|
+
full_output = [separator]
|
36
|
+
full_output.extend(agent_strings)
|
37
|
+
full_output.append(separator)
|
38
|
+
print("\n".join(full_output))
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# Library imports
|
2
|
+
from uuid import uuid4
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Any, List, Optional, Dict, Union
|
5
|
+
from pydantic import ValidationError
|
6
|
+
|
7
|
+
# Local imports
|
8
|
+
from smarta2a.utils.types import (
|
9
|
+
Task,
|
10
|
+
TaskStatus,
|
11
|
+
TaskState,
|
12
|
+
Artifact,
|
13
|
+
Part,
|
14
|
+
TextPart,
|
15
|
+
FilePart,
|
16
|
+
DataPart,
|
17
|
+
Message,
|
18
|
+
A2AResponse,
|
19
|
+
)
|
20
|
+
|
21
|
+
class TaskBuilder:
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
default_status: TaskState = TaskState.COMPLETED,
|
25
|
+
):
|
26
|
+
self.default_status = default_status
|
27
|
+
|
28
|
+
def build(
|
29
|
+
self,
|
30
|
+
content: Any,
|
31
|
+
task_id: str,
|
32
|
+
session_id: Optional[str] = None,
|
33
|
+
metadata: Optional[Dict[str,Any]] = None,
|
34
|
+
history: Optional[List[Message]] = None,
|
35
|
+
) -> Task:
|
36
|
+
"""Universal task construction from various return types."""
|
37
|
+
history = history or []
|
38
|
+
metadata = metadata or {}
|
39
|
+
|
40
|
+
# 1) If the handler already gave us a full Task, just fix IDs & history:
|
41
|
+
if isinstance(content, Task):
|
42
|
+
content.sessionId = content.sessionId or session_id
|
43
|
+
content.history = history + (content.history or [])
|
44
|
+
content.metadata = content.metadata or metadata
|
45
|
+
return content
|
46
|
+
|
47
|
+
# 2) If they returned an A2AResponse, extract status/content:
|
48
|
+
if isinstance(content, A2AResponse):
|
49
|
+
# prefer the sessionId inside the A2AResponse
|
50
|
+
sid = content.sessionId or session_id
|
51
|
+
# merge metadata from builder-call and from A2AResponse
|
52
|
+
md = {**(metadata or {}), **(content.metadata or {})}
|
53
|
+
status = (
|
54
|
+
content.status
|
55
|
+
if isinstance(content.status, TaskStatus)
|
56
|
+
else TaskStatus(state=content.status)
|
57
|
+
)
|
58
|
+
artifacts = self._normalize_content(content.content)
|
59
|
+
return Task(
|
60
|
+
id=task_id,
|
61
|
+
sessionId=sid,
|
62
|
+
status=status,
|
63
|
+
artifacts=artifacts,
|
64
|
+
metadata=md,
|
65
|
+
history=history,
|
66
|
+
)
|
67
|
+
|
68
|
+
# 3) If they returned a plain dict describing a Task:
|
69
|
+
if isinstance(content, dict):
|
70
|
+
try:
|
71
|
+
return Task(
|
72
|
+
**content,
|
73
|
+
sessionId=session_id or content.get("sessionId"),
|
74
|
+
metadata=metadata or content.get("metadata", {}),
|
75
|
+
history=history,
|
76
|
+
)
|
77
|
+
except ValidationError:
|
78
|
+
pass
|
79
|
+
|
80
|
+
# 4) Fallback: treat whatever they returned as “artifact content”:
|
81
|
+
artifacts = self._normalize_content(content)
|
82
|
+
return Task(
|
83
|
+
id=task_id,
|
84
|
+
sessionId=session_id,
|
85
|
+
status=TaskStatus(state=self.default_status),
|
86
|
+
artifacts=artifacts,
|
87
|
+
metadata=metadata,
|
88
|
+
history=history,
|
89
|
+
)
|
90
|
+
|
91
|
+
def normalize_from_status(
|
92
|
+
self, status: TaskState, task_id: str, metadata: Dict[str,Any]
|
93
|
+
) -> Task:
|
94
|
+
"""Build a Task when only a cancellation or status‐only event occurs."""
|
95
|
+
return Task(
|
96
|
+
id=task_id,
|
97
|
+
sessionId="",
|
98
|
+
status=TaskStatus(state=status, timestamp=datetime.now()),
|
99
|
+
artifacts=[],
|
100
|
+
metadata=metadata,
|
101
|
+
history=[],
|
102
|
+
)
|
103
|
+
|
104
|
+
def _normalize_content(self, content: Any) -> List[Artifact]:
|
105
|
+
"""Turn any handler return value into a list of Artifact."""
|
106
|
+
if isinstance(content, Artifact):
|
107
|
+
return [content]
|
108
|
+
|
109
|
+
if isinstance(content, list) and all(isinstance(a, Artifact) for a in content):
|
110
|
+
return content
|
111
|
+
|
112
|
+
if isinstance(content, list):
|
113
|
+
return [Artifact(parts=self._parts_from_mixed(content))]
|
114
|
+
|
115
|
+
if isinstance(content, str):
|
116
|
+
return [Artifact(parts=[TextPart(text=content)])]
|
117
|
+
|
118
|
+
if isinstance(content, dict):
|
119
|
+
# raw artifact dict
|
120
|
+
return [Artifact.model_validate(content)]
|
121
|
+
|
122
|
+
# explicit `Part` subclasses
|
123
|
+
if isinstance(content, (TextPart, FilePart, DataPart)):
|
124
|
+
return [Artifact(parts=[content])]
|
125
|
+
|
126
|
+
# “unknown” object: try Pydantic → dict → fallback to text
|
127
|
+
try:
|
128
|
+
return [Artifact.model_validate(content)]
|
129
|
+
except ValidationError:
|
130
|
+
return [Artifact(parts=[TextPart(text=str(content))])]
|
131
|
+
|
132
|
+
def _parts_from_mixed(self, items: List[Any]) -> List[Part]:
|
133
|
+
parts: List[Part] = []
|
134
|
+
for item in items:
|
135
|
+
if isinstance(item, Artifact):
|
136
|
+
parts.extend(item.parts)
|
137
|
+
else:
|
138
|
+
parts.append(self._create_part(item))
|
139
|
+
return parts
|
140
|
+
|
141
|
+
def _create_part(self, item: Any) -> Part:
|
142
|
+
from smarta2a.utils.types import Part as UnionPart
|
143
|
+
# guard against Union alias
|
144
|
+
if isinstance(item, (TextPart, FilePart, DataPart)):
|
145
|
+
return item
|
146
|
+
if isinstance(item, str):
|
147
|
+
return TextPart(text=item)
|
148
|
+
if isinstance(item, dict):
|
149
|
+
try:
|
150
|
+
return UnionPart.model_validate(item)
|
151
|
+
except ValidationError:
|
152
|
+
return TextPart(text=str(item))
|
153
|
+
return TextPart(text=str(item))
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Union, Any
|
1
|
+
from typing import Union, Any, Dict
|
2
2
|
from pydantic import BaseModel, Field, TypeAdapter
|
3
3
|
from typing import Literal, List, Annotated, Optional
|
4
4
|
from datetime import datetime
|
@@ -57,7 +57,7 @@ Part = Annotated[Union[TextPart, FilePart, DataPart], Field(discriminator="type"
|
|
57
57
|
|
58
58
|
|
59
59
|
class Message(BaseModel):
|
60
|
-
role: Literal["user", "agent"]
|
60
|
+
role: Literal["user", "agent", "system"] # Added system role for system messages
|
61
61
|
parts: List[Part]
|
62
62
|
metadata: dict[str, Any] | None = None
|
63
63
|
|
@@ -357,6 +357,59 @@ class AgentCard(BaseModel):
|
|
357
357
|
defaultOutputModes: List[str] = ["text"]
|
358
358
|
skills: List[AgentSkill]
|
359
359
|
|
360
|
+
def pretty_print(self, include_separators: bool = False) -> str:
|
361
|
+
"""Returns formatted string, optionally wrapped in separators"""
|
362
|
+
output = []
|
363
|
+
output.append(f"Name: {self.name}")
|
364
|
+
|
365
|
+
if self.description:
|
366
|
+
output.append(f"Description: {self.description}")
|
367
|
+
|
368
|
+
output.append(f"URL: {self.url}")
|
369
|
+
|
370
|
+
if self.provider:
|
371
|
+
output.append(f"Provider Organization: {self.provider.organization}")
|
372
|
+
|
373
|
+
# Capabilities handling
|
374
|
+
capabilities = []
|
375
|
+
if self.capabilities.streaming:
|
376
|
+
capabilities.append("Streaming")
|
377
|
+
if self.capabilities.pushNotifications:
|
378
|
+
capabilities.append("Push Notifications")
|
379
|
+
if self.capabilities.stateTransitionHistory:
|
380
|
+
capabilities.append("State Transition History")
|
381
|
+
output.append("Capabilities: " + ", ".join(capabilities))
|
382
|
+
|
383
|
+
# Skills handling
|
384
|
+
skills_output = ["Skills:"]
|
385
|
+
for skill in self.skills:
|
386
|
+
skills_output.append(f" {skill.name} [{skill.id}]")
|
387
|
+
|
388
|
+
if skill.description:
|
389
|
+
skills_output.append(f" Description: {skill.description}")
|
390
|
+
|
391
|
+
if skill.tags:
|
392
|
+
skills_output.append(f" Tags: {', '.join(skill.tags)}")
|
393
|
+
|
394
|
+
if skill.examples:
|
395
|
+
skills_output.append(" Examples:")
|
396
|
+
skills_output.extend([f" - {ex}" for ex in skill.examples])
|
397
|
+
|
398
|
+
if skill.inputModes:
|
399
|
+
skills_output.append(f" Input Modes: {', '.join(skill.inputModes)}")
|
400
|
+
|
401
|
+
if skill.outputModes:
|
402
|
+
skills_output.append(f" Output Modes: {', '.join(skill.outputModes)}")
|
403
|
+
|
404
|
+
skills_output.append("")
|
405
|
+
|
406
|
+
output.extend(skills_output)
|
407
|
+
result = "\n".join(output).strip()
|
408
|
+
|
409
|
+
if include_separators:
|
410
|
+
return f"---\n{result}\n---"
|
411
|
+
return result
|
412
|
+
|
360
413
|
|
361
414
|
class A2AClientError(Exception):
|
362
415
|
pass
|
@@ -388,6 +441,8 @@ They are used to help with the implementation of the server.
|
|
388
441
|
class A2AResponse(BaseModel):
|
389
442
|
status: Union[TaskStatus, str]
|
390
443
|
content: Union[str, List[Any], Part, Artifact, List[Part], List[Artifact]]
|
444
|
+
sessionId: Optional[str] = None
|
445
|
+
metadata: Optional[dict[str, Any]] = None
|
391
446
|
|
392
447
|
@model_validator(mode="after")
|
393
448
|
def validate_state(self) -> 'A2AResponse':
|
@@ -422,3 +477,8 @@ class A2AStreamResponse(BaseModel):
|
|
422
477
|
append: bool = False
|
423
478
|
final: bool = False
|
424
479
|
metadata: dict[str, Any] | None = None
|
480
|
+
|
481
|
+
class StateData(BaseModel):
|
482
|
+
sessionId: str
|
483
|
+
history: List[Message]
|
484
|
+
metadata: Dict[str, Any]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: smarta2a
|
3
|
-
Version: 0.2.
|
4
|
-
Summary: A Python
|
3
|
+
Version: 0.2.3
|
4
|
+
Summary: A simple Python framework (built on top of FastAPI) for creating Agents following Google's Agent2Agent protocol
|
5
5
|
Project-URL: Homepage, https://github.com/siddharthsma/smarta2a
|
6
6
|
Project-URL: Bug Tracker, https://github.com/siddharthsma/smarta2a/issues
|
7
7
|
Author-email: Siddharth Ambegaonkar <siddharthsma@gmail.com>
|
@@ -10,10 +10,16 @@ Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
12
|
Requires-Python: >=3.8
|
13
|
-
Requires-Dist:
|
14
|
-
Requires-Dist:
|
15
|
-
Requires-Dist:
|
16
|
-
Requires-Dist:
|
13
|
+
Requires-Dist: anyio>=4.9.0
|
14
|
+
Requires-Dist: fastapi>=0.115.12
|
15
|
+
Requires-Dist: httpx>=0.28.1
|
16
|
+
Requires-Dist: mcp>=0.1.0
|
17
|
+
Requires-Dist: openai>=1.0.0
|
18
|
+
Requires-Dist: pydantic>=2.11.3
|
19
|
+
Requires-Dist: sse-starlette>=2.2.1
|
20
|
+
Requires-Dist: starlette>=0.46.2
|
21
|
+
Requires-Dist: typing-extensions>=4.13.2
|
22
|
+
Requires-Dist: uvicorn>=0.34.1
|
17
23
|
Description-Content-Type: text/markdown
|
18
24
|
|
19
25
|
# SmartA2A
|
@@ -0,0 +1,32 @@
|
|
1
|
+
smarta2a/__init__.py,sha256=T_EECYqWrxshix0FbgUv22zlKRX22HFU-HKXcYTOb3w,175
|
2
|
+
smarta2a/agent/a2a_agent.py,sha256=LrqWNgZra_dSHGmLZubhFMjZCgZ1BYdroXGuFFzv8Rk,1183
|
3
|
+
smarta2a/agent/a2a_mcp_server.py,sha256=X_mxkgYgCA_dSNtCvs0rSlOoWYc-8d3Qyxv0e-a7NKY,1015
|
4
|
+
smarta2a/archive/mcp_client.py,sha256=Fj64Kw5HFXZ0SHImWTsxTHIeP-3DgvP1nRiY_Jdgm0Q,3305
|
5
|
+
smarta2a/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
smarta2a/client/a2a_client.py,sha256=Tixof6e2EXrGbShSJls_S009i9VbO9QizyCDzbzSISE,9890
|
7
|
+
smarta2a/client/smart_mcp_client.py,sha256=-P_qwY3lnvL2uF-4e4ZdL3U0hyqMgVrD_u2cfhugQpc,2413
|
8
|
+
smarta2a/client/tools_manager.py,sha256=bTbVwxR3hYsbQ87ClaX1cnswJ34ZAEeZ0Xi8N5q47Q0,2247
|
9
|
+
smarta2a/history_update_strategies/__init__.py,sha256=x5WtiE9rG5ze8d8hA6E6wJOciBhWHa_ZgGgoIAZcXEQ,213
|
10
|
+
smarta2a/history_update_strategies/append_strategy.py,sha256=j7Qbhs69Wwr-HBLB8GJ3-mEPaBSHiBV2xz9ZZi86k2w,312
|
11
|
+
smarta2a/history_update_strategies/history_update_strategy.py,sha256=n2sfIGu8ztKI7gJTwRX26m4tZr28B8Xdhrk6RlBFlI8,373
|
12
|
+
smarta2a/model_providers/__init__.py,sha256=2FhblAUiwG9Xv27yEpuuz0VrnIZ-rlpgIuPwm-UIX5U,147
|
13
|
+
smarta2a/model_providers/base_llm_provider.py,sha256=6QjTUjYEnvHZji4_VWZz6CvLYKLyutxRUfIeH3seQg4,424
|
14
|
+
smarta2a/model_providers/openai_provider.py,sha256=bLNWlsrrSFTXxbKsTX_xXfJFuzfWmndKRKqCy0h6ZB8,10830
|
15
|
+
smarta2a/server/__init__.py,sha256=f2X454Ll4vJc02V4JLJHTN-h8u0TBm4d_FkiO4t686U,53
|
16
|
+
smarta2a/server/handler_registry.py,sha256=OVRG5dTvxB7qUNXgsqWxVNxIyRljUShSYxb1gtbi5XM,820
|
17
|
+
smarta2a/server/server.py,sha256=nqMsMeGJEvkj2kaF9U7yRnYAtj07vPCkQOdQALmaCws,31782
|
18
|
+
smarta2a/server/state_manager.py,sha256=Uc4BNr2kQvi7MAEh3CmHsKV2bP-Q8bYbGADQ35iHmZo,1350
|
19
|
+
smarta2a/server/subscription_service.py,sha256=fWqNNY0xmRksc_SZl4xt5fOPtZTQacfzou1-RMyaEd4,5188
|
20
|
+
smarta2a/server/task_service.py,sha256=TXVnFeS9ofAqH2z_7BOfk5uDmoZKv9irHHQSIuurI70,7650
|
21
|
+
smarta2a/state_stores/__init__.py,sha256=vafxAqpwvag_cYFH2XKGk3DPmJIWJr4Ioey30yLFkVQ,220
|
22
|
+
smarta2a/state_stores/base_state_store.py,sha256=LFI-LThPLf7M9z_CcXWCswajxMAtMx9tMFFVhZU0fM8,521
|
23
|
+
smarta2a/state_stores/inmemory_state_store.py,sha256=MgFGc7HxccrBxEqhVqKJ3bV-RnV1koU6iJd5m3rhhjA,682
|
24
|
+
smarta2a/utils/__init__.py,sha256=5db5VgDGgbMUGEF-xuyaC3qrgRQkUE9WAITkFSiNqSA,702
|
25
|
+
smarta2a/utils/prompt_helpers.py,sha256=jLETieoeBJLQXcGzwFeoKT5b2pS4tJ5770lPLImtKLo,1439
|
26
|
+
smarta2a/utils/task_builder.py,sha256=wqSyfVHNTaXuGESu09dhlaDi7D007gcN3-8tH-nPQ40,5159
|
27
|
+
smarta2a/utils/task_request_builder.py,sha256=6cOGOqj2Rg43xWM03GRJQzlIZHBptsMCJRp7oD-TDAQ,3362
|
28
|
+
smarta2a/utils/types.py,sha256=mtkn78SrXyzjnOw6OBcTHlnhGfHgxlhKxJsRAVlUaEQ,13078
|
29
|
+
smarta2a-0.2.3.dist-info/METADATA,sha256=XJBiT_AOoaltJAppDSd0D0y-bNKTwqxOuQiymgDj_Vs,2726
|
30
|
+
smarta2a-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
31
|
+
smarta2a-0.2.3.dist-info/licenses/LICENSE,sha256=ECMEVHuFkvpEmH-_A9HSxs_UnnsUqpCkiAYNHPCf2z0,1078
|
32
|
+
smarta2a-0.2.3.dist-info/RECORD,,
|