distributed-a2a 0.1.6rc6__tar.gz → 0.1.6rc8__tar.gz
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.
- {distributed_a2a-0.1.6rc6/distributed_a2a.egg-info → distributed_a2a-0.1.6rc8}/PKG-INFO +1 -1
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/agent.py +5 -5
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/client.py +13 -4
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/executors.py +16 -4
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/model.py +3 -3
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/server.py +5 -5
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8/distributed_a2a.egg-info}/PKG-INFO +1 -1
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/pyproject.toml +13 -2
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/LICENSE +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/MANIFEST.in +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/README.md +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/__init__.py +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/registry.py +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a/router.py +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a.egg-info/SOURCES.txt +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a.egg-info/dependency_links.txt +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a.egg-info/requires.txt +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a.egg-info/top_level.txt +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/requirements.txt +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/setup.cfg +0 -0
- {distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/setup.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import Literal, Optional
|
|
2
2
|
|
|
3
3
|
from a2a.types import TaskState
|
|
4
4
|
from langchain.agents import create_agent
|
|
@@ -7,7 +7,7 @@ from langchain_core.tools import BaseTool
|
|
|
7
7
|
from langgraph_dynamodb_checkpoint import DynamoDBSaver
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
-
from .model import get_model,
|
|
10
|
+
from .model import get_model, LLMConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class AgentResponse(BaseModel):
|
|
@@ -58,10 +58,10 @@ class StatusAgent[ResponseT: AgentResponse]:
|
|
|
58
58
|
name=name
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
async def __call__(self, message: str, context_id: str = None) -> ResponseT:
|
|
61
|
+
async def __call__(self, message: str, context_id: Optional[str] = None) -> ResponseT:
|
|
62
62
|
config: RunnableConfig = RunnableConfig(configurable={'thread_id': context_id})
|
|
63
|
-
response = await self.agent.ainvoke(
|
|
64
|
-
return response['structured_response']
|
|
63
|
+
response = await self.agent.ainvoke({"messages": [("user", message)]}, config) # type: ignore[arg-type]
|
|
64
|
+
return response['structured_response'] # type: ignore[no-any-return]
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class LangGraphMessage(BaseModel):
|
|
@@ -16,6 +16,11 @@ class RemoteAgentConnection:
|
|
|
16
16
|
"""A class to hold the connections to the remote agents."""
|
|
17
17
|
|
|
18
18
|
def __init__(self, agent_card: AgentCard, client: httpx.AsyncClient):
|
|
19
|
+
if agent_card.preferred_transport is None:
|
|
20
|
+
raise ValueError("Agent card preferred transport must be provided.")
|
|
21
|
+
if agent_card.capabilities.streaming is None:
|
|
22
|
+
raise ValueError("Agent card streaming capability must be provided.")
|
|
23
|
+
|
|
19
24
|
client_config = ClientConfig(
|
|
20
25
|
httpx_client=client,
|
|
21
26
|
supported_transports=[agent_card.preferred_transport],
|
|
@@ -29,7 +34,8 @@ class RemoteAgentConnection:
|
|
|
29
34
|
|
|
30
35
|
responses: list[ClientEvent] = []
|
|
31
36
|
async for response in self.agent_client.send_message(message_request):
|
|
32
|
-
|
|
37
|
+
if isinstance(response, tuple):
|
|
38
|
+
responses.append(response)
|
|
33
39
|
|
|
34
40
|
task_response: Task | None = None
|
|
35
41
|
match responses:
|
|
@@ -44,8 +50,8 @@ class RemoteAgentConnection:
|
|
|
44
50
|
response: Task = await self.agent_client.get_task(query_params)
|
|
45
51
|
return response
|
|
46
52
|
|
|
47
|
-
async def send_message(self, message_to_send: str, context_id, task_id: None | str = None,
|
|
48
|
-
count=0) -> str | AgentCard:
|
|
53
|
+
async def send_message(self, message_to_send: str, context_id: str, task_id: None | str = None,
|
|
54
|
+
count: int = 0) -> str | AgentCard:
|
|
49
55
|
message: Message = create_text_message_object(content=message_to_send)
|
|
50
56
|
message.message_id = str(uuid4())
|
|
51
57
|
message.context_id = context_id
|
|
@@ -87,7 +93,7 @@ class RoutingA2AClient:
|
|
|
87
93
|
self.client = httpx.AsyncClient()
|
|
88
94
|
self.current_card: AgentCard | None = None
|
|
89
95
|
|
|
90
|
-
async def fetch_current_card(self):
|
|
96
|
+
async def fetch_current_card(self) -> None:
|
|
91
97
|
card_resolver = A2ACardResolver(
|
|
92
98
|
self.client, self.url
|
|
93
99
|
)
|
|
@@ -100,6 +106,9 @@ class RoutingA2AClient:
|
|
|
100
106
|
raise Exception("Maximum recursion depth exceeded. This is likely due to an infinite loop in your agent.")
|
|
101
107
|
if self.current_card is None:
|
|
102
108
|
await self.fetch_current_card()
|
|
109
|
+
|
|
110
|
+
if self.current_card is None:
|
|
111
|
+
raise ValueError("Failed to fetch current agent card.")
|
|
103
112
|
|
|
104
113
|
agent_connection = RemoteAgentConnection(self.current_card, self.client)
|
|
105
114
|
|
|
@@ -25,6 +25,9 @@ class RoutingAgentExecutor(AgentExecutor):
|
|
|
25
25
|
def __init__(self, agent_config: AgentConfig, routing_tool: BaseTool, tools: list[BaseTool] | None = None):
|
|
26
26
|
super().__init__()
|
|
27
27
|
api_key = os.environ.get(agent_config.agent.llm.api_key_env)
|
|
28
|
+
if api_key is None:
|
|
29
|
+
raise ValueError("No API key found for LLM.")
|
|
30
|
+
|
|
28
31
|
self.agent = StatusAgent[StringResponse](
|
|
29
32
|
llm_config=agent_config.agent.llm,
|
|
30
33
|
system_prompt=agent_config.agent.system_prompt,
|
|
@@ -46,6 +49,9 @@ class RoutingAgentExecutor(AgentExecutor):
|
|
|
46
49
|
raise NotImplementedError
|
|
47
50
|
|
|
48
51
|
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
52
|
+
if context.context_id is None or context.task_id is None:
|
|
53
|
+
raise ValueError("Context ID and Task ID must be provided.")
|
|
54
|
+
|
|
49
55
|
# set status to processing
|
|
50
56
|
await event_queue.enqueue_event(TaskStatusUpdateEvent(status=TaskStatus(state=TaskState.working),
|
|
51
57
|
final=False,
|
|
@@ -56,13 +62,13 @@ class RoutingAgentExecutor(AgentExecutor):
|
|
|
56
62
|
|
|
57
63
|
artifact: Artifact
|
|
58
64
|
if agent_response.status == TaskState.rejected:
|
|
59
|
-
|
|
65
|
+
routing_agent_response: RoutingResponse = await self.routing_agent(message=context.get_user_input(),
|
|
60
66
|
context_id=context.context_id)
|
|
61
|
-
agent_name: str = json.loads(
|
|
67
|
+
agent_name: str = json.loads(routing_agent_response.agent_card)["name"]
|
|
62
68
|
logger.info(f"Request with id {context.context_id} got rejected and will be rerouted to a '{agent_name}'.",
|
|
63
|
-
extra={"card":
|
|
69
|
+
extra={"card": routing_agent_response.agent_card})
|
|
64
70
|
artifact = new_text_artifact(name='target_agent', description='New target agent for request.',
|
|
65
|
-
text=
|
|
71
|
+
text=routing_agent_response.agent_card)
|
|
66
72
|
else:
|
|
67
73
|
logger.info(f"Request with id {context.context_id} was successfully processed by agent.")
|
|
68
74
|
artifact = new_text_artifact(name='current_result', description='Result of request to agent.',
|
|
@@ -86,6 +92,9 @@ class RoutingExecutor(AgentExecutor):
|
|
|
86
92
|
def __init__(self, router_config: RouterConfig, routing_tool: BaseTool) -> None:
|
|
87
93
|
super().__init__()
|
|
88
94
|
api_key = os.environ.get(router_config.router.llm.api_key_env)
|
|
95
|
+
if api_key is None:
|
|
96
|
+
raise ValueError("No API key found for LLM.")
|
|
97
|
+
|
|
89
98
|
self.routing_agent = StatusAgent[RoutingResponse](
|
|
90
99
|
llm_config=router_config.router.llm,
|
|
91
100
|
system_prompt=ROUTING_SYSTEM_PROMPT,
|
|
@@ -99,6 +108,9 @@ class RoutingExecutor(AgentExecutor):
|
|
|
99
108
|
raise NotImplementedError
|
|
100
109
|
|
|
101
110
|
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
111
|
+
if context.context_id is None or context.task_id is None:
|
|
112
|
+
raise ValueError("Context ID and Task ID must be provided.")
|
|
113
|
+
|
|
102
114
|
await event_queue.enqueue_event(TaskStatusUpdateEvent(status=TaskStatus(state=TaskState.working),
|
|
103
115
|
final=False,
|
|
104
116
|
context_id=context.context_id,
|
|
@@ -59,8 +59,8 @@ class RouterConfig(BaseModel):
|
|
|
59
59
|
|
|
60
60
|
def get_model(api_key: str, model: str, base_url: str, reasoning_effort: str) -> BaseChatModel:
|
|
61
61
|
return ChatOpenAI(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
model_name=model,
|
|
63
|
+
openai_api_base=base_url,
|
|
64
|
+
openai_api_key=lambda: api_key,
|
|
65
65
|
reasoning_effort=reasoning_effort
|
|
66
66
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
3
|
from contextlib import asynccontextmanager
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, AsyncGenerator
|
|
5
5
|
|
|
6
6
|
import boto3
|
|
7
7
|
from a2a.server.apps import A2ARESTFastAPIApplication
|
|
@@ -25,7 +25,7 @@ AGENT_CARD_TABLE = "agent-cards"
|
|
|
25
25
|
def get_expire_at() -> int:
|
|
26
26
|
return int(time.time() + MAX_HEART_BEAT_MISSES * HEART_BEAT_INTERVAL_SEC)
|
|
27
27
|
|
|
28
|
-
async def heart_beat(name: str, agent_card_table: str, agent_card: AgentCard):
|
|
28
|
+
async def heart_beat(name: str, agent_card_table: str, agent_card: AgentCard) -> None:
|
|
29
29
|
table = boto3.resource("dynamodb", region_name="eu-central-1").Table(agent_card_table)
|
|
30
30
|
table.put_item(Item={"id": name, "card": agent_card.model_dump_json(), "expireAt": get_expire_at()})
|
|
31
31
|
while True:
|
|
@@ -39,9 +39,9 @@ async def heart_beat(name: str, agent_card_table: str, agent_card: AgentCard):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def load_app(agent_config:
|
|
42
|
+
def load_app(agent_config: Any) -> FastAPI:
|
|
43
43
|
|
|
44
|
-
agent_config= AgentConfig.model_validate(agent_config)
|
|
44
|
+
agent_config= AgentConfig.model_validate(obj=agent_config)
|
|
45
45
|
|
|
46
46
|
skills = [AgentSkill(
|
|
47
47
|
id=skill.id,
|
|
@@ -73,7 +73,7 @@ def load_app(agent_config: dict[str, Any]) -> FastAPI:
|
|
|
73
73
|
routing_tool=DynamoDbRegistryLookup(agent_card_tabel=AGENT_CARD_TABLE).as_tool())
|
|
74
74
|
|
|
75
75
|
@asynccontextmanager
|
|
76
|
-
async def lifespan(_: FastAPI):
|
|
76
|
+
async def lifespan(_: FastAPI) -> AsyncGenerator[None, Any]:
|
|
77
77
|
asyncio.create_task(heart_beat(name=agent_card.name, agent_card_table=AGENT_CARD_TABLE, agent_card=agent_card))
|
|
78
78
|
yield
|
|
79
79
|
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "distributed_a2a"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6rc8"
|
|
8
8
|
description = "A library for building A2A agents with routing capabilities"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Fabian Bell", email = "fabian.bell@barrabytes.com"}
|
|
@@ -30,3 +30,14 @@ dependencies = {file = ["requirements.txt"]}
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Homepage = "https://github.com/Barra-Technologies/distributed-a2a"
|
|
32
32
|
Repository = "https://github.com/Barra-Technologies/distributed-a2a"
|
|
33
|
+
|
|
34
|
+
[tool.mypy]
|
|
35
|
+
python_version = "3.14"
|
|
36
|
+
strict = true
|
|
37
|
+
ignore_missing_imports = true
|
|
38
|
+
plugins = ["pydantic.mypy"]
|
|
39
|
+
|
|
40
|
+
[tool.pydantic-mypy]
|
|
41
|
+
init_forbid_extra = true
|
|
42
|
+
init_typed = true
|
|
43
|
+
warn_required_dynamic_aliases = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{distributed_a2a-0.1.6rc6 → distributed_a2a-0.1.6rc8}/distributed_a2a.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|