smarta2a 0.4.11__py3-none-any.whl → 0.4.12__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_human.py +2 -5
- smarta2a/client/a2a_client.py +18 -0
- smarta2a/file_stores/base_file_store.py +27 -0
- smarta2a/file_stores/local_file_store.py +75 -0
- smarta2a/model_providers/openai_provider.py +17 -19
- smarta2a/server/state_manager.py +21 -2
- {smarta2a-0.4.11.dist-info → smarta2a-0.4.12.dist-info}/METADATA +1 -1
- {smarta2a-0.4.11.dist-info → smarta2a-0.4.12.dist-info}/RECORD +10 -8
- {smarta2a-0.4.11.dist-info → smarta2a-0.4.12.dist-info}/WHEEL +0 -0
- {smarta2a-0.4.11.dist-info → smarta2a-0.4.12.dist-info}/licenses/LICENSE +0 -0
smarta2a/agent/a2a_human.py
CHANGED
@@ -7,7 +7,6 @@ from typing import Optional
|
|
7
7
|
|
8
8
|
# Local imports
|
9
9
|
from smarta2a.server import SmartA2A
|
10
|
-
from smarta2a.model_providers.base_llm_provider import BaseLLMProvider
|
11
10
|
from smarta2a.server.state_manager import StateManager
|
12
11
|
from smarta2a.utils.types import StateData, SendTaskRequest, AgentCard, WebhookRequest, WebhookResponse, TextPart, DataPart, FilePart
|
13
12
|
from smarta2a.client.a2a_client import A2AClient
|
@@ -16,11 +15,9 @@ class A2AHuman:
|
|
16
15
|
def __init__(
|
17
16
|
self,
|
18
17
|
name: str,
|
19
|
-
model_provider: BaseLLMProvider,
|
20
18
|
agent_card: AgentCard = None,
|
21
19
|
state_manager: StateManager = None,
|
22
20
|
):
|
23
|
-
self.model_provider = model_provider
|
24
21
|
self.state_manager = state_manager
|
25
22
|
self.app = SmartA2A(
|
26
23
|
name=name,
|
@@ -32,7 +29,7 @@ class A2AHuman:
|
|
32
29
|
def __register_handlers(self):
|
33
30
|
@self.app.on_event("startup")
|
34
31
|
async def on_startup():
|
35
|
-
|
32
|
+
pass
|
36
33
|
|
37
34
|
@self.app.app.get("/tasks")
|
38
35
|
async def get_tasks(fields: Optional[str] = Query(None)):
|
@@ -42,7 +39,7 @@ class A2AHuman:
|
|
42
39
|
|
43
40
|
@self.app.on_send_task(forward_to_webhook=True)
|
44
41
|
async def on_send_task(request: SendTaskRequest, state: StateData):
|
45
|
-
return "
|
42
|
+
return "I am human and take some time to respond, but I will definitely respond to your request"
|
46
43
|
|
47
44
|
@self.app.webhook()
|
48
45
|
async def on_webhook(request: WebhookRequest, state: StateData):
|
smarta2a/client/a2a_client.py
CHANGED
@@ -54,6 +54,15 @@ class A2AClient:
|
|
54
54
|
metadata: dict[str, Any] | None = None,
|
55
55
|
):
|
56
56
|
"""Send a task to another Agent."""
|
57
|
+
|
58
|
+
# Auto-create PushNotificationConfig if not provided and we have a URL
|
59
|
+
if push_notification is None and self.url:
|
60
|
+
push_notification = PushNotificationConfig(
|
61
|
+
url=f"{self.url}/webhook",
|
62
|
+
token=None,
|
63
|
+
authentication=None
|
64
|
+
)
|
65
|
+
|
57
66
|
params = TaskRequestBuilder.build_send_task_request(
|
58
67
|
id=id,
|
59
68
|
role=role,
|
@@ -84,6 +93,15 @@ class A2AClient:
|
|
84
93
|
metadata: dict[str, Any] | None = None,
|
85
94
|
):
|
86
95
|
"""Send to another Agent and receive a stream of responses."""
|
96
|
+
|
97
|
+
# Auto-create PushNotificationConfig if not provided and we have a URL
|
98
|
+
if push_notification is None and self.url:
|
99
|
+
push_notification = PushNotificationConfig(
|
100
|
+
url=f"{self.url}/webhook",
|
101
|
+
token=None,
|
102
|
+
authentication=None
|
103
|
+
)
|
104
|
+
|
87
105
|
params = TaskRequestBuilder.build_send_task_request(
|
88
106
|
id=id,
|
89
107
|
role=role,
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Library imports
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
class BaseFileStore(ABC):
|
6
|
+
"""Separate interface for file operations"""
|
7
|
+
|
8
|
+
@abstractmethod
|
9
|
+
async def upload(
|
10
|
+
self,
|
11
|
+
content: bytes,
|
12
|
+
task_id: str,
|
13
|
+
filename: Optional[str] = None
|
14
|
+
) -> str:
|
15
|
+
pass
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
async def download(self, uri: str) -> bytes:
|
19
|
+
pass
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
async def delete_for_task(self, task_id: str):
|
23
|
+
pass
|
24
|
+
|
25
|
+
@abstractmethod
|
26
|
+
async def list_files(self, task_id: str) -> list[str]:
|
27
|
+
pass
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Library imports
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Optional
|
4
|
+
import hashlib
|
5
|
+
import aiofiles
|
6
|
+
import aiofiles.os
|
7
|
+
import shutil
|
8
|
+
|
9
|
+
# Local imports
|
10
|
+
from smarta2a.file_stores.base_file_store import BaseFileStore
|
11
|
+
|
12
|
+
class LocalFileStore(BaseFileStore):
|
13
|
+
"""
|
14
|
+
Local filesystem implementation that mimics cloud storage patterns
|
15
|
+
- Stores files in task-specific directories
|
16
|
+
- Uses content-addressable storage for deduplication
|
17
|
+
- Generates file:// URIs for compatibility
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, base_path: str = "./filestore"):
|
21
|
+
self.base_path = Path(base_path).resolve()
|
22
|
+
self.base_path.mkdir(parents=True, exist_ok=True)
|
23
|
+
|
24
|
+
async def upload(self, content: bytes, task_id: str, filename: Optional[str] = None) -> str:
|
25
|
+
# Create task directory if not exists
|
26
|
+
task_dir = self.base_path / task_id
|
27
|
+
await aiofiles.os.makedirs(task_dir, exist_ok=True)
|
28
|
+
|
29
|
+
# Ensure content is bytes for hashing
|
30
|
+
if isinstance(content, str):
|
31
|
+
content = content.encode('utf-8')
|
32
|
+
|
33
|
+
# Generate content hash for deduplication
|
34
|
+
content_hash = hashlib.sha256(content).hexdigest()
|
35
|
+
file_ext = Path(filename).suffix if filename else ""
|
36
|
+
unique_name = f"{content_hash}{file_ext}"
|
37
|
+
|
38
|
+
# Write file
|
39
|
+
file_path = task_dir / unique_name
|
40
|
+
async with aiofiles.open(file_path, "wb") as f:
|
41
|
+
await f.write(content)
|
42
|
+
|
43
|
+
return f"file://{file_path}"
|
44
|
+
|
45
|
+
|
46
|
+
async def download(self, uri: str) -> bytes:
|
47
|
+
# Parse file:// URI
|
48
|
+
path = Path(uri.replace("file://", ""))
|
49
|
+
if not path.exists():
|
50
|
+
raise FileNotFoundError(f"File not found at {uri}")
|
51
|
+
|
52
|
+
async with aiofiles.open(path, "rb") as f:
|
53
|
+
return await f.read()
|
54
|
+
|
55
|
+
async def delete_for_task(self, task_id: str) -> None:
|
56
|
+
task_dir = self.base_path / task_id
|
57
|
+
if await aiofiles.os.path.exists(task_dir):
|
58
|
+
await aiofiles.os.rmdir(task_dir)
|
59
|
+
|
60
|
+
async def list_files(self, task_id: str) -> list[str]:
|
61
|
+
task_dir = self.base_path / task_id
|
62
|
+
if not await aiofiles.os.path.exists(task_dir):
|
63
|
+
return []
|
64
|
+
|
65
|
+
return [
|
66
|
+
f"file://{file_path}"
|
67
|
+
for file_path in task_dir.iterdir()
|
68
|
+
if file_path.is_file()
|
69
|
+
]
|
70
|
+
|
71
|
+
async def clear_all(self) -> None:
|
72
|
+
"""Clear entire storage (useful for testing)"""
|
73
|
+
if await aiofiles.os.path.exists(self.base_path):
|
74
|
+
shutil.rmtree(self.base_path)
|
75
|
+
await aiofiles.os.makedirs(self.base_path, exist_ok=True)
|
@@ -69,35 +69,33 @@ class OpenAIProvider(BaseLLMProvider):
|
|
69
69
|
self.agent_cards
|
70
70
|
)
|
71
71
|
|
72
|
+
|
72
73
|
def _convert_part(self, part: Union[TextPart, FilePart, DataPart]) -> dict:
|
73
74
|
"""Convert a single part to OpenAI-compatible format"""
|
74
75
|
if isinstance(part, TextPart):
|
75
76
|
return {"type": "text", "text": part.text}
|
76
77
|
|
77
78
|
elif isinstance(part, FilePart):
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
if
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
elif isinstance(part, DataPart):
|
79
|
+
# Treat all files as attachments with placeholder metadata
|
80
|
+
fc = part.file
|
81
|
+
# Determine URI (either direct or data URL)
|
82
|
+
if fc.uri:
|
83
|
+
uri = fc.uri
|
84
|
+
else:
|
85
|
+
uri = f"data:{fc.mimeType};base64,{fc.bytes}"
|
86
|
+
|
87
|
+
placeholder = (
|
88
|
+
f'[FileAttachment name="{fc.name or ""}" '
|
89
|
+
f'mimeType="{fc.mimeType or ""}" '
|
90
|
+
f'uri="{uri}"]'
|
91
|
+
)
|
92
|
+
return {"type": "text", "text": placeholder}
|
93
|
+
|
94
|
+
else:
|
95
95
|
return {
|
96
96
|
"type": "text",
|
97
97
|
"text": f"[Structured Data]\n{json.dumps(part.data, indent=2)}"
|
98
98
|
}
|
99
|
-
|
100
|
-
raise ValueError(f"Unsupported part type: {type(part)}")
|
101
99
|
|
102
100
|
|
103
101
|
def _convert_messages(self, messages: List[Message]) -> List[dict]:
|
smarta2a/server/state_manager.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
# Library imports
|
2
2
|
from typing import Optional, Dict, Any, List
|
3
|
+
import base64
|
3
4
|
|
4
5
|
# Local imports
|
5
6
|
from smarta2a.state_stores.base_state_store import BaseStateStore
|
7
|
+
from smarta2a.file_stores.base_file_store import BaseFileStore
|
6
8
|
from smarta2a.history_update_strategies.history_update_strategy import HistoryUpdateStrategy
|
7
|
-
from smarta2a.utils.types import Message, StateData, Task, TaskStatus, TaskState, PushNotificationConfig, Part
|
9
|
+
from smarta2a.utils.types import Message, StateData, Task, TaskStatus, TaskState, PushNotificationConfig, Part, FilePart
|
8
10
|
from smarta2a.server.nats_client import NATSClient
|
9
11
|
|
10
12
|
class StateManager:
|
11
|
-
def __init__(self, state_store: BaseStateStore, history_strategy: HistoryUpdateStrategy, nats_server_url: Optional[str] = "nats://localhost:4222"):
|
13
|
+
def __init__(self, state_store: BaseStateStore, file_store: BaseFileStore, history_strategy: HistoryUpdateStrategy, nats_server_url: Optional[str] = "nats://localhost:4222"):
|
12
14
|
self.state_store = state_store
|
15
|
+
self.file_store = file_store
|
13
16
|
self.strategy = history_strategy
|
14
17
|
self.nats_client = NATSClient(server_url=nats_server_url)
|
15
18
|
|
@@ -72,6 +75,9 @@ class StateManager:
|
|
72
75
|
)
|
73
76
|
latest_state.task.metadata = metadata
|
74
77
|
|
78
|
+
# Process files before persistence
|
79
|
+
await self._process_file_parts(latest_state)
|
80
|
+
|
75
81
|
await self.update_state(latest_state)
|
76
82
|
|
77
83
|
return latest_state
|
@@ -187,3 +193,16 @@ class StateManager:
|
|
187
193
|
except ValueError as e:
|
188
194
|
print(f"Invalid part in artifact: {e}")
|
189
195
|
return parts
|
196
|
+
|
197
|
+
async def _process_file_parts(self, state: StateData):
|
198
|
+
"""Replace file bytes with URIs and persist files"""
|
199
|
+
for msg in state.context_history:
|
200
|
+
for part in msg.parts:
|
201
|
+
if isinstance(part, FilePart) and part.file.bytes:
|
202
|
+
uri = await self.file_store.upload(
|
203
|
+
content=base64.b64decode(part.file.bytes),
|
204
|
+
task_id=state.task_id,
|
205
|
+
filename=part.file.name
|
206
|
+
)
|
207
|
+
part.file.uri = uri
|
208
|
+
part.file.bytes = None # Remove from state
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: smarta2a
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.12
|
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
|
@@ -1,20 +1,22 @@
|
|
1
1
|
smarta2a/__init__.py,sha256=T_EECYqWrxshix0FbgUv22zlKRX22HFU-HKXcYTOb3w,175
|
2
2
|
smarta2a/agent/a2a_agent.py,sha256=EurcxpV14e3OPWCMutYL0EXMHb5ZKQqAHEGZZF6pNgg,1892
|
3
|
-
smarta2a/agent/a2a_human.py,sha256=
|
3
|
+
smarta2a/agent/a2a_human.py,sha256=EZP9TD54Gum509OMOGmfFeCeoMk2Lyi1Y24RoRWh4hY,1720
|
4
4
|
smarta2a/agent/a2a_mcp_server.py,sha256=X_mxkgYgCA_dSNtCvs0rSlOoWYc-8d3Qyxv0e-a7NKY,1015
|
5
5
|
smarta2a/archive/smart_mcp_client.py,sha256=0s2OWFKWSv-_UF7rb9fOrsh1OIYsYOsGukkXXp_E1cU,4158
|
6
6
|
smarta2a/archive/subscription_service.py,sha256=vftmZD94HbdjPFa_1UBvsBm-WkW-s3ZCVq60fF7OCgA,4109
|
7
7
|
smarta2a/archive/task_service.py,sha256=ptf-oMHy98Rw4XSxyK1-lpqc1JtkCkEEHTmwAaunet4,8199
|
8
8
|
smarta2a/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
-
smarta2a/client/a2a_client.py,sha256=
|
9
|
+
smarta2a/client/a2a_client.py,sha256=Tl9FLADlK5n_y_0C3qcFB25T6Uu02iXDSxopm40LbKs,12726
|
10
10
|
smarta2a/client/mcp_client.py,sha256=JeXhBqxM9TYAArpExLRtEr3lZeQZMcnTmGFl6XKsdu8,3797
|
11
|
+
smarta2a/file_stores/base_file_store.py,sha256=fcwFIOoFjLQiIKb8lIRVujnV6udyuI9Dq8cEc2ldmIQ,591
|
12
|
+
smarta2a/file_stores/local_file_store.py,sha256=4GLDrsKxSoLWn2Oha4OD-P2r5vBpfV-8ePvZ5bhP1e8,2616
|
11
13
|
smarta2a/history_update_strategies/__init__.py,sha256=x5WtiE9rG5ze8d8hA6E6wJOciBhWHa_ZgGgoIAZcXEQ,213
|
12
14
|
smarta2a/history_update_strategies/append_strategy.py,sha256=j7Qbhs69Wwr-HBLB8GJ3-mEPaBSHiBV2xz9ZZi86k2w,312
|
13
15
|
smarta2a/history_update_strategies/history_update_strategy.py,sha256=n2sfIGu8ztKI7gJTwRX26m4tZr28B8Xdhrk6RlBFlI8,373
|
14
16
|
smarta2a/history_update_strategies/rolling_window_strategy.py,sha256=7Ch042JWt4TM_r1-sFKlSIxHj8VX1P3ZoqjCvIdeSqA,540
|
15
17
|
smarta2a/model_providers/__init__.py,sha256=hJj0w00JjqTiBgJmHmOWwL6MU_hwmro9xTiX3XYf6ts,148
|
16
18
|
smarta2a/model_providers/base_llm_provider.py,sha256=iQUqjnypl0f2M929iU0WhHoxAE4ek-NUFJPbEnNQ8-4,412
|
17
|
-
smarta2a/model_providers/openai_provider.py,sha256=
|
19
|
+
smarta2a/model_providers/openai_provider.py,sha256=dGVnQ94H6-pohUYwT_IzmUf0Nc7BDMJtI_dpjHLL2ak,12131
|
18
20
|
smarta2a/server/__init__.py,sha256=f2X454Ll4vJc02V4JLJHTN-h8u0TBm4d_FkiO4t686U,53
|
19
21
|
smarta2a/server/handler_registry.py,sha256=OVRG5dTvxB7qUNXgsqWxVNxIyRljUShSYxb1gtbi5XM,820
|
20
22
|
smarta2a/server/json_rpc_request_processor.py,sha256=qRB3sfj_n9ImkIOCdaUKMsDmKcO7CiMhaZ4VdQS7Mb4,6993
|
@@ -22,7 +24,7 @@ smarta2a/server/nats_client.py,sha256=akyNg1hLd9XYoLSH_qQVs8uoiTQerztgvqu_3TifSg
|
|
22
24
|
smarta2a/server/request_handler.py,sha256=5KMtfpHQX6bOgk1DJbhs1fUCQ5tSvMYXWzheT3IW2Bo,26374
|
23
25
|
smarta2a/server/send_task_handler.py,sha256=fiBeCCHCu9c2H4EJOUc0t3EZgpHVFJy4B_6qZOC140s,6336
|
24
26
|
smarta2a/server/server.py,sha256=RKkQM8jpSndt_nOuUB0kswOqLdm7JfvjZA1O424sYdY,6722
|
25
|
-
smarta2a/server/state_manager.py,sha256=
|
27
|
+
smarta2a/server/state_manager.py,sha256=JhF6jma8t2YtBzb3sADGvxtmUMOlDafqxllPzLb3DU0,7668
|
26
28
|
smarta2a/server/webhook_request_processor.py,sha256=_0XoUDmueSl9CvFQE-1zgKRSts-EW8QxbmolPTfFER8,5306
|
27
29
|
smarta2a/state_stores/__init__.py,sha256=vafxAqpwvag_cYFH2XKGk3DPmJIWJr4Ioey30yLFkVQ,220
|
28
30
|
smarta2a/state_stores/base_state_store.py,sha256=_3LInM-qepKwwdypJTDNs9-DozBNrKVycwPwUm7bYdU,512
|
@@ -34,7 +36,7 @@ smarta2a/utils/task_builder.py,sha256=wqSyfVHNTaXuGESu09dhlaDi7D007gcN3-8tH-nPQ4
|
|
34
36
|
smarta2a/utils/task_request_builder.py,sha256=6cOGOqj2Rg43xWM03GRJQzlIZHBptsMCJRp7oD-TDAQ,3362
|
35
37
|
smarta2a/utils/tools_manager.py,sha256=igKYeSi0SaYzd36jUqOMPvnYd5kK55EPQ0X_pdTo5e4,4857
|
36
38
|
smarta2a/utils/types.py,sha256=kzA6Vv5xXfu1sJuxhEXrglI9e9S6eZVIljMnsrQVyN0,13650
|
37
|
-
smarta2a-0.4.
|
38
|
-
smarta2a-0.4.
|
39
|
-
smarta2a-0.4.
|
40
|
-
smarta2a-0.4.
|
39
|
+
smarta2a-0.4.12.dist-info/METADATA,sha256=_dkL7gOJcF84lsAYt8vnal-hetsh3gWC8J-CDCGLQww,12988
|
40
|
+
smarta2a-0.4.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
41
|
+
smarta2a-0.4.12.dist-info/licenses/LICENSE,sha256=lDbqrxVnzDMY5KJ8JS1WhvkWE8TJaw-O-CHDy-ecsJA,2095
|
42
|
+
smarta2a-0.4.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|