agent-api-server 2.1.9__tar.gz → 2.1.11__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.
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/PKG-INFO +2 -2
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/agent_card_builder.py +2 -2
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/call_a2a_client.py +68 -5
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/langgraph_executor.py +177 -58
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/acp_bridge/go/agent.go +19 -16
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/acp_bridge/go/main.go +40 -143
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/v1/thread.py +21 -97
- agent_api_server-2.1.11/agent_api_server/orchestrator/__init__.py +85 -0
- agent_api_server-2.1.11/agent_api_server/orchestrator/agent_registry.py +128 -0
- agent_api_server-2.1.11/agent_api_server/orchestrator/modes/__init__.py +16 -0
- agent_api_server-2.1.11/agent_api_server/orchestrator/modes/chain.py +503 -0
- agent_api_server-2.1.11/agent_api_server/orchestrator/modes/discussion.py +1319 -0
- agent_api_server-2.1.11/agent_api_server/orchestrator/orchestrator.py +234 -0
- agent_api_server-2.1.11/agent_api_server/orchestrator/types.py +171 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/pyproject.toml +1 -1
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/README.md +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/redis_task_store.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/server.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/acp_bridge/go/Makefile +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/acp_bridge/go/go.mod +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/acp_bridge/go/go.sum +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/agent_registration.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/v1/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/v1/api.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/v1/config.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/v1/graph.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/api/v1/schema.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/app.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/cache/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/cache/redis_cache.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/callback_handler.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/client/css/styles.css +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/client/favicon.ico +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/client/index.html +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/client/js/app.js +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/client/js/index.umd.js +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/config_center/config_center.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/configs/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/configs/config.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/dynamic_llm/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/dynamic_llm/dynamic_llm.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/listener/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/listener/base.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/listener/nats_listener.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/listener/redis_listener.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/log/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/log/formatters.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/log/logging.json +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/mcp_convert/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/mcp_convert/mcp_convert.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/mcp_server.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/memory/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/memory/postgres.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/register/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/register/register.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/service.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/service_hub/service_hub.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/service_hub/service_hub_test.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/services/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/services/config_builder.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/services/stream_service.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/ase.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/base_model.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/common.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/decode_token.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/detect_message.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/exceptions.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/get_model_info.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/graph_loader.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/message.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/model_config.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/shared/util_func.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/sso_service/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/sso_service/sdk/__init__.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/sso_service/sdk/client.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/sso_service/sdk/credential.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/sso_service/sdk/encoding.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/sso_service/sso_service.py +0 -0
- {agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-api-server
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.11
|
|
4
4
|
Summary: A Langgraph agent API server that implements Langgraph agent's web capabilities and can interact with chatbot
|
|
5
5
|
Author: zijie.zhang@advantech.com.cn
|
|
6
6
|
Requires-Python: >=3.10,<3.13
|
{agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/agent_card_builder.py
RENAMED
|
@@ -222,7 +222,7 @@ class AgentCardBuilder:
|
|
|
222
222
|
return await self._build_card_for_agent("AgentHub", config)
|
|
223
223
|
|
|
224
224
|
capabilities = AgentCapabilities(
|
|
225
|
-
streaming=
|
|
225
|
+
streaming=True,
|
|
226
226
|
push_notifications=False,
|
|
227
227
|
)
|
|
228
228
|
|
|
@@ -304,7 +304,7 @@ class AgentCardBuilder:
|
|
|
304
304
|
self, agent_name: str, config: Dict[str, Any]
|
|
305
305
|
) -> AgentCard:
|
|
306
306
|
capabilities = AgentCapabilities(
|
|
307
|
-
streaming=
|
|
307
|
+
streaming=True,
|
|
308
308
|
push_notifications=False,
|
|
309
309
|
)
|
|
310
310
|
|
{agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/call_a2a_client.py
RENAMED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
5
8
|
from agent_api_server.shared.decode_token import decode_jwt
|
|
9
|
+
from a2a.client.client import ClientConfig
|
|
6
10
|
from a2a.client.client_factory import create_client
|
|
7
11
|
from a2a.types import (
|
|
12
|
+
AgentCard,
|
|
8
13
|
Message,
|
|
9
14
|
Part,
|
|
10
15
|
Role,
|
|
@@ -94,15 +99,56 @@ class AgentHubClient:
|
|
|
94
99
|
print(result.text)
|
|
95
100
|
"""
|
|
96
101
|
|
|
97
|
-
def __init__(
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
agent_url: str = "",
|
|
105
|
+
timeout: float = 60.0,
|
|
106
|
+
agent_card: AgentCard | None = None,
|
|
107
|
+
):
|
|
108
|
+
"""Initialize the A2A client.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
agent_url: Base URL or full AgentCard URL of the agent.
|
|
112
|
+
Used when agent_card is not provided.
|
|
113
|
+
timeout: HTTP timeout for requests.
|
|
114
|
+
agent_card: Pre-resolved AgentCard. When provided, the client
|
|
115
|
+
uses it directly without fetching from URL. This is the
|
|
116
|
+
preferred approach when the caller already has the card.
|
|
117
|
+
"""
|
|
98
118
|
self._agent_url = agent_url
|
|
119
|
+
self._timeout = timeout
|
|
120
|
+
self._agent_card = agent_card
|
|
99
121
|
self._client = None
|
|
100
122
|
|
|
101
123
|
async def _get_client(self):
|
|
102
124
|
if self._client is None:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
httpx_client = httpx.AsyncClient(
|
|
126
|
+
timeout=httpx.Timeout(self._timeout, connect=10.0),
|
|
127
|
+
)
|
|
128
|
+
client_config = ClientConfig(httpx_client=httpx_client)
|
|
129
|
+
|
|
130
|
+
if self._agent_card is not None:
|
|
131
|
+
# Use the pre-resolved AgentCard directly
|
|
132
|
+
logger.info(
|
|
133
|
+
"Creating client from AgentCard for '%s'",
|
|
134
|
+
self._agent_card.name,
|
|
135
|
+
)
|
|
136
|
+
self._client = await create_client(
|
|
137
|
+
self._agent_card,
|
|
138
|
+
client_config=client_config,
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
logger.info("Resolving AgentCard from %s", self._agent_url)
|
|
142
|
+
self._client = await create_client(
|
|
143
|
+
self._agent_url,
|
|
144
|
+
client_config=client_config,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
logger.info(
|
|
148
|
+
"Client created successfully for %s (timeout=%ss)",
|
|
149
|
+
self._agent_url or (self._agent_card.name if self._agent_card else "unknown"),
|
|
150
|
+
self._timeout,
|
|
151
|
+
)
|
|
106
152
|
return self._client
|
|
107
153
|
|
|
108
154
|
async def call(
|
|
@@ -114,6 +160,7 @@ class AgentHubClient:
|
|
|
114
160
|
files: list | None = None,
|
|
115
161
|
task_id: str | None = None,
|
|
116
162
|
context_id: str | None = None,
|
|
163
|
+
on_stream_chunk: Callable[[StreamResponse], None] | None = None,
|
|
117
164
|
**extra_metadata,
|
|
118
165
|
) -> A2AResult:
|
|
119
166
|
"""Send a message to the remote agent and consume the full event stream.
|
|
@@ -125,6 +172,8 @@ class AgentHubClient:
|
|
|
125
172
|
files: Optional file references.
|
|
126
173
|
task_id: Existing task ID for multi-turn continuation.
|
|
127
174
|
context_id: Existing context ID for multi-turn continuation.
|
|
175
|
+
on_stream_chunk: Optional callback invoked for each streaming event.
|
|
176
|
+
Enables real-time processing (e.g., logging) of stream chunks.
|
|
128
177
|
**extra_metadata: Additional metadata (e.g., graph_name, app_id).
|
|
129
178
|
|
|
130
179
|
Returns:
|
|
@@ -132,7 +181,11 @@ class AgentHubClient:
|
|
|
132
181
|
"""
|
|
133
182
|
client = await self._get_client()
|
|
134
183
|
|
|
135
|
-
|
|
184
|
+
# Use agent card name if available, otherwise fall back to graph_name or URL
|
|
185
|
+
if self._agent_card:
|
|
186
|
+
graph_name = self._agent_card.name
|
|
187
|
+
else:
|
|
188
|
+
graph_name = extra_metadata.get("graph_name", self._agent_url or "unknown")
|
|
136
189
|
|
|
137
190
|
# Extract user_id from JWT if available
|
|
138
191
|
user_id = None
|
|
@@ -178,6 +231,13 @@ class AgentHubClient:
|
|
|
178
231
|
async for response in client.send_message(request):
|
|
179
232
|
self._process_stream_response(response, result)
|
|
180
233
|
|
|
234
|
+
# Invoke streaming callback if provided
|
|
235
|
+
if on_stream_chunk is not None:
|
|
236
|
+
try:
|
|
237
|
+
on_stream_chunk(response)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning("on_stream_chunk callback error: %s", e)
|
|
240
|
+
|
|
181
241
|
# Stop consuming once we hit a terminal or interrupt state
|
|
182
242
|
if result.task_state in _TERMINAL_STATES or result.task_state in _INTERRUPT_STATES:
|
|
183
243
|
logger.info(
|
|
@@ -198,6 +258,7 @@ class AgentHubClient:
|
|
|
198
258
|
follow_up_input: str,
|
|
199
259
|
task_id: str,
|
|
200
260
|
context_id: str,
|
|
261
|
+
on_stream_chunk: Callable[[StreamResponse], None] | None = None,
|
|
201
262
|
**extra_metadata,
|
|
202
263
|
) -> A2AResult:
|
|
203
264
|
"""Send a follow-up message to an existing task in INPUT_REQUIRED state.
|
|
@@ -209,6 +270,7 @@ class AgentHubClient:
|
|
|
209
270
|
follow_up_input: The user's response to the agent's prompt.
|
|
210
271
|
task_id: The task ID from the previous result.
|
|
211
272
|
context_id: The context ID from the previous result.
|
|
273
|
+
on_stream_chunk: Optional callback invoked for each streaming event.
|
|
212
274
|
**extra_metadata: Additional metadata (e.g., graph_name).
|
|
213
275
|
|
|
214
276
|
Returns:
|
|
@@ -218,6 +280,7 @@ class AgentHubClient:
|
|
|
218
280
|
raw_question=follow_up_input,
|
|
219
281
|
task_id=task_id,
|
|
220
282
|
context_id=context_id,
|
|
283
|
+
on_stream_chunk=on_stream_chunk,
|
|
221
284
|
**extra_metadata,
|
|
222
285
|
)
|
|
223
286
|
|
{agent_api_server-2.1.9 → agent_api_server-2.1.11}/agent_api_server/a2a_bridge/langgraph_executor.py
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
|
|
4
|
-
from
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Dict, AsyncGenerator
|
|
5
|
+
from langchain_core.messages import AIMessage, AIMessageChunk
|
|
5
6
|
from langgraph.graph.state import CompiledStateGraph
|
|
6
7
|
from langgraph.types import Command
|
|
7
8
|
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
@@ -18,6 +19,71 @@ except ImportError:
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
async def _extract_text_from_stream_event(
|
|
23
|
+
stream_event: Any,
|
|
24
|
+
) -> AsyncGenerator[str, None]:
|
|
25
|
+
"""Extract readable text content from a LangGraph stream event.
|
|
26
|
+
|
|
27
|
+
Unlike handle_stream_event (which wraps output in SSE format for REST),
|
|
28
|
+
this extracts plain text suitable for A2A artifact transport.
|
|
29
|
+
|
|
30
|
+
Handles both 2-tuple (mode, event) and 3-tuple (subgraph_id, mode, event)
|
|
31
|
+
formats produced by subgraphs=True.
|
|
32
|
+
"""
|
|
33
|
+
# Unwrap subgraph tuple format: (subgraph_id, stream_mode, event)
|
|
34
|
+
if isinstance(stream_event, tuple) and len(stream_event) == 3:
|
|
35
|
+
_, stream_mode, event = stream_event
|
|
36
|
+
async for chunk in _extract_text_from_stream_event((stream_mode, event)):
|
|
37
|
+
yield chunk
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if not (isinstance(stream_event, tuple) and len(stream_event) == 2):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
stream_mode, event = stream_event
|
|
44
|
+
|
|
45
|
+
if stream_mode == "messages":
|
|
46
|
+
# Token-level streaming: (message_chunk, metadata)
|
|
47
|
+
if isinstance(event, tuple) and len(event) == 2:
|
|
48
|
+
message_chunk, metadata = event
|
|
49
|
+
if isinstance(message_chunk, AIMessageChunk):
|
|
50
|
+
content = message_chunk.content
|
|
51
|
+
# Handle list content (multimodal)
|
|
52
|
+
if isinstance(content, list):
|
|
53
|
+
text_parts = []
|
|
54
|
+
for item in content:
|
|
55
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
56
|
+
text_parts.append(item.get("text", ""))
|
|
57
|
+
elif isinstance(item, str):
|
|
58
|
+
text_parts.append(item)
|
|
59
|
+
content = "".join(text_parts)
|
|
60
|
+
if content and isinstance(content, str) and content.strip():
|
|
61
|
+
yield content
|
|
62
|
+
|
|
63
|
+
elif stream_mode == "updates":
|
|
64
|
+
# Node-level updates: {node_name: state_updates}
|
|
65
|
+
if isinstance(event, dict):
|
|
66
|
+
for node_name, updates in event.items():
|
|
67
|
+
messages = updates.get("messages", [])
|
|
68
|
+
for msg in messages:
|
|
69
|
+
content = getattr(msg, "content", None)
|
|
70
|
+
if isinstance(content, str) and content.strip():
|
|
71
|
+
yield f"[{node_name}] {content}\n"
|
|
72
|
+
elif isinstance(content, list):
|
|
73
|
+
text_parts = []
|
|
74
|
+
for item in content:
|
|
75
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
76
|
+
text_parts.append(item.get("text", ""))
|
|
77
|
+
text = "".join(text_parts)
|
|
78
|
+
if text.strip():
|
|
79
|
+
yield f"[{node_name}] {text}\n"
|
|
80
|
+
|
|
81
|
+
elif stream_mode == "custom":
|
|
82
|
+
# Custom events — yield as-is if string
|
|
83
|
+
if isinstance(event, str) and event.strip():
|
|
84
|
+
yield event
|
|
85
|
+
|
|
86
|
+
|
|
21
87
|
class LangGraphAgentExecutor(AgentExecutor):
|
|
22
88
|
"""A2A AgentExecutor that bridges to LangGraph agents.
|
|
23
89
|
|
|
@@ -41,6 +107,11 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
41
107
|
CANCELED state. The cancel() method actively cancels the running task
|
|
42
108
|
and serves as a fallback for edge cases.
|
|
43
109
|
|
|
110
|
+
Streaming:
|
|
111
|
+
Uses graph.astream() with stream_mode=["updates", "messages", "custom"]
|
|
112
|
+
to emit incremental artifact chunks. Text content is extracted directly
|
|
113
|
+
from stream events (no SSE wrapping), suitable for A2A transport.
|
|
114
|
+
|
|
44
115
|
Graph usage:
|
|
45
116
|
from langgraph.types import interrupt
|
|
46
117
|
|
|
@@ -131,24 +202,13 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
131
202
|
try:
|
|
132
203
|
await graph_task
|
|
133
204
|
except asyncio.CancelledError:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await updater.cancel()
|
|
137
|
-
except RuntimeError:
|
|
138
|
-
pass
|
|
139
|
-
except Exception as e:
|
|
140
|
-
logger.error("Error executing graph '%s': %s", graph_name, e, exc_info=True)
|
|
141
|
-
await updater.failed(
|
|
142
|
-
message=Message(
|
|
143
|
-
role=Role.ROLE_AGENT,
|
|
144
|
-
parts=[Part(text=f"Graph execution failed: {e}")],
|
|
145
|
-
)
|
|
146
|
-
)
|
|
205
|
+
await updater.cancel()
|
|
206
|
+
logger.info("Task %s -> CANCELED", task_id)
|
|
147
207
|
finally:
|
|
148
208
|
self._running_tasks.pop(task_id, None)
|
|
149
209
|
|
|
150
|
-
@staticmethod
|
|
151
210
|
async def _run_graph(
|
|
211
|
+
self,
|
|
152
212
|
graph: CompiledStateGraph,
|
|
153
213
|
user_input: str,
|
|
154
214
|
metadata: Dict[str, Any],
|
|
@@ -156,12 +216,16 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
156
216
|
updater: TaskUpdater,
|
|
157
217
|
is_continuation: bool,
|
|
158
218
|
) -> None:
|
|
159
|
-
"""Execute the LangGraph and map
|
|
219
|
+
"""Execute the LangGraph with streaming and map output to A2A states.
|
|
220
|
+
|
|
221
|
+
Uses graph.astream() for real-time streaming of graph execution events
|
|
222
|
+
to the A2A client via incremental artifact updates.
|
|
160
223
|
|
|
161
224
|
Uses LangGraph's native interrupt/resume mechanism for multi-turn:
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
225
|
+
- Checks graph state before streaming for existing interrupts (resume)
|
|
226
|
+
- Streams events and emits each as an artifact chunk (append=True)
|
|
227
|
+
- After streaming, checks final state for interrupts -> INPUT_REQUIRED
|
|
228
|
+
- Otherwise -> COMPLETED
|
|
165
229
|
"""
|
|
166
230
|
graph_name = config.get("configurable", {}).get("graph_name", "a2a_agent")
|
|
167
231
|
callbacks_config = build_langfuse_callbacks(run_name=f"A2A_{graph_name}")
|
|
@@ -171,7 +235,14 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
171
235
|
session_id = configurable.get("thread_id", "")
|
|
172
236
|
user_id = configurable.get("user_id", "")
|
|
173
237
|
|
|
174
|
-
|
|
238
|
+
# Check for existing interrupts (resume detection) before streaming
|
|
239
|
+
state_get = await graph.aget_state(config=a2a_config)
|
|
240
|
+
has_interrupts = any(
|
|
241
|
+
getattr(task, "interrupts", None)
|
|
242
|
+
for task in state_get.tasks
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if has_interrupts or is_continuation:
|
|
175
246
|
graph_input = Command(resume=user_input)
|
|
176
247
|
else:
|
|
177
248
|
graph_input = {}
|
|
@@ -182,34 +253,85 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
182
253
|
graph_input["agent_name"] = graph_name
|
|
183
254
|
graph_input["user_input"] = user_input
|
|
184
255
|
|
|
185
|
-
|
|
186
|
-
|
|
256
|
+
# Generate a stable artifact ID for all streaming chunks
|
|
257
|
+
artifact_id = str(uuid.uuid4())
|
|
258
|
+
_trace_name = f"A2A_{graph_name}"
|
|
259
|
+
|
|
260
|
+
trace_ctx = (
|
|
261
|
+
propagate_attributes(
|
|
187
262
|
session_id=session_id,
|
|
188
263
|
user_id=user_id,
|
|
189
|
-
trace_name=
|
|
190
|
-
)
|
|
191
|
-
|
|
264
|
+
trace_name=_trace_name,
|
|
265
|
+
)
|
|
266
|
+
if callbacks_config and propagate_attributes
|
|
267
|
+
else None
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if trace_ctx:
|
|
271
|
+
with trace_ctx:
|
|
272
|
+
await self._stream_graph_and_emit_artifacts(
|
|
273
|
+
graph, graph_input, a2a_config, updater, artifact_id,
|
|
274
|
+
)
|
|
192
275
|
else:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
276
|
+
await self._stream_graph_and_emit_artifacts(
|
|
277
|
+
graph, graph_input, a2a_config, updater, artifact_id,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# After streaming, check final state for interrupts
|
|
281
|
+
final_state = await graph.aget_state(config=a2a_config)
|
|
282
|
+
if final_state.next:
|
|
283
|
+
# Graph is interrupted — extract interrupt value for the prompt
|
|
284
|
+
interrupt_values = []
|
|
285
|
+
for task in final_state.tasks:
|
|
286
|
+
for interrupt in getattr(task, "interrupts", []):
|
|
287
|
+
if hasattr(interrupt, "value") and interrupt.value is not None:
|
|
288
|
+
interrupt_values.append(str(interrupt.value))
|
|
289
|
+
prompt = interrupt_values[0] if interrupt_values else "Additional input required"
|
|
290
|
+
await updater.requires_input(
|
|
291
|
+
message=Message(
|
|
292
|
+
role=Role.ROLE_AGENT,
|
|
293
|
+
parts=[Part(text=prompt)],
|
|
205
294
|
)
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
)
|
|
296
|
+
logger.info("Task %s -> INPUT_REQUIRED: %s", updater.task_id, prompt)
|
|
297
|
+
else:
|
|
298
|
+
await updater.complete()
|
|
299
|
+
logger.info("Task %s -> COMPLETED", updater.task_id)
|
|
208
300
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
301
|
+
async def _stream_graph_and_emit_artifacts(
|
|
302
|
+
self,
|
|
303
|
+
graph: CompiledStateGraph,
|
|
304
|
+
graph_input: Any,
|
|
305
|
+
config: Dict[str, Any],
|
|
306
|
+
updater: TaskUpdater,
|
|
307
|
+
artifact_id: str,
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Stream graph execution and emit text content as artifact chunks.
|
|
310
|
+
|
|
311
|
+
Extracts text directly from stream events (no SSE wrapping).
|
|
312
|
+
Each chunk is sent as an A2A artifact update with append=True
|
|
313
|
+
for incremental delivery.
|
|
314
|
+
"""
|
|
315
|
+
first_chunk = True
|
|
316
|
+
async for stream_event in graph.astream(
|
|
317
|
+
graph_input,
|
|
318
|
+
config=config,
|
|
319
|
+
stream_mode=["updates", "messages", "custom"],
|
|
320
|
+
subgraphs=True,
|
|
321
|
+
):
|
|
322
|
+
async for text_chunk in _extract_text_from_stream_event(stream_event):
|
|
323
|
+
if first_chunk:
|
|
324
|
+
await updater.add_artifact(
|
|
325
|
+
parts=[Part(text=text_chunk)],
|
|
326
|
+
artifact_id=artifact_id,
|
|
327
|
+
)
|
|
328
|
+
first_chunk = False
|
|
329
|
+
else:
|
|
330
|
+
await updater.add_artifact(
|
|
331
|
+
parts=[Part(text=text_chunk)],
|
|
332
|
+
artifact_id=artifact_id,
|
|
333
|
+
append=True,
|
|
334
|
+
)
|
|
213
335
|
|
|
214
336
|
def _build_config(
|
|
215
337
|
self,
|
|
@@ -218,11 +340,20 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
218
340
|
metadata: Dict[str, Any] = None,
|
|
219
341
|
) -> Dict[str, Any]:
|
|
220
342
|
metadata = metadata or {}
|
|
221
|
-
ts_tenant = metadata.get("
|
|
343
|
+
ts_tenant = metadata.get("tenant_id", "")
|
|
344
|
+
|
|
345
|
+
# Namespace thread_id by graph_name to isolate checkpoints
|
|
346
|
+
# across multiple agents sharing the same PostgreSQL checkpointer.
|
|
347
|
+
# Without this, agents on the same server with the same context_id
|
|
348
|
+
# would overwrite each other's conversation state.
|
|
349
|
+
namespaced_thread_id = (
|
|
350
|
+
f"{graph_name}:{context.context_id}"
|
|
351
|
+
if graph_name else context.context_id
|
|
352
|
+
)
|
|
222
353
|
|
|
223
354
|
configurable = build_configurable(
|
|
224
355
|
graph_name=graph_name,
|
|
225
|
-
thread_id=
|
|
356
|
+
thread_id=namespaced_thread_id,
|
|
226
357
|
ts_tenant=ts_tenant,
|
|
227
358
|
ei_token=metadata.get("ei_token", ""),
|
|
228
359
|
files=metadata.get("files"),
|
|
@@ -257,15 +388,3 @@ class LangGraphAgentExecutor(AgentExecutor):
|
|
|
257
388
|
await updater.cancel()
|
|
258
389
|
except RuntimeError:
|
|
259
390
|
pass
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def _extract_response_text(result: Any) -> str:
|
|
263
|
-
"""Extract the response text from a LangGraph output."""
|
|
264
|
-
if isinstance(result, dict) and "messages" in result and result["messages"]:
|
|
265
|
-
last_message = result["messages"][-1]
|
|
266
|
-
if isinstance(last_message, AIMessage):
|
|
267
|
-
return last_message.content
|
|
268
|
-
return str(last_message)
|
|
269
|
-
if isinstance(result, str):
|
|
270
|
-
return result
|
|
271
|
-
return str(result) if result else "No response generated"
|
|
@@ -39,6 +39,12 @@ type SessionState struct {
|
|
|
39
39
|
UpdatedAt string `json:"updated_at,omitempty"`
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// pendingToolCall holds buffered tool call info until the tool result is available.
|
|
43
|
+
type pendingToolCall struct {
|
|
44
|
+
name string
|
|
45
|
+
args interface{}
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
// AgentHubACPAgent implements the ACP protocol over stdio, proxying to an HTTP backend.
|
|
43
49
|
type AgentHubACPAgent struct {
|
|
44
50
|
serviceURL string
|
|
@@ -54,7 +60,7 @@ type AgentHubACPAgent struct {
|
|
|
54
60
|
mu sync.Mutex
|
|
55
61
|
httpClient *http.Client
|
|
56
62
|
sessions map[string]*SessionState
|
|
57
|
-
pendingToolCalls map[string]map[string]
|
|
63
|
+
pendingToolCalls map[string]map[string]*pendingToolCall // sessionID -> tcID -> info
|
|
58
64
|
streamedNodes map[string]map[string]bool // sessionID -> set of nodes
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -77,7 +83,7 @@ func NewAgentHubACPAgent(
|
|
|
77
83
|
extraInputs: extraInputs,
|
|
78
84
|
streamMode: streamMode,
|
|
79
85
|
sessions: make(map[string]*SessionState),
|
|
80
|
-
pendingToolCalls: make(map[string]map[string]
|
|
86
|
+
pendingToolCalls: make(map[string]map[string]*pendingToolCall),
|
|
81
87
|
streamedNodes: make(map[string]map[string]bool),
|
|
82
88
|
httpClient: &http.Client{
|
|
83
89
|
Timeout: 300 * time.Second,
|
|
@@ -348,24 +354,22 @@ func (a *AgentHubACPAgent) processSSEEvent(sessionID, raw string) {
|
|
|
348
354
|
a.mu.Lock()
|
|
349
355
|
pending := a.pendingToolCalls[sessionID]
|
|
350
356
|
if pending == nil {
|
|
351
|
-
pending = make(map[string]
|
|
357
|
+
pending = make(map[string]*pendingToolCall)
|
|
352
358
|
a.pendingToolCalls[sessionID] = pending
|
|
353
359
|
}
|
|
354
360
|
a.mu.Unlock()
|
|
355
361
|
for _, tc := range toolCalls {
|
|
356
|
-
tcID
|
|
357
|
-
|
|
358
|
-
tcID = uuid.New().String()
|
|
359
|
-
}
|
|
362
|
+
// Always use UUID for tcID to avoid invalid IDs from LangChain
|
|
363
|
+
tcID := uuid.New().String()
|
|
360
364
|
tcName, _ := tc["name"].(string)
|
|
361
365
|
if tcName == "" {
|
|
362
366
|
tcName = "unknown"
|
|
363
367
|
}
|
|
364
368
|
tcArgs := tc["args"]
|
|
369
|
+
// Buffer tool call info; emit complete tool_call when tools_message arrives
|
|
365
370
|
a.mu.Lock()
|
|
366
|
-
pending[tcID] = tcName
|
|
371
|
+
pending[tcID] = &pendingToolCall{name: tcName, args: tcArgs}
|
|
367
372
|
a.mu.Unlock()
|
|
368
|
-
a.sendStartToolCall(sessionID, tcID, tcName, tcArgs, nil, "")
|
|
369
373
|
}
|
|
370
374
|
} else if text != "" {
|
|
371
375
|
a.mu.Lock()
|
|
@@ -390,12 +394,13 @@ func (a *AgentHubACPAgent) processSSEEvent(sessionID, raw string) {
|
|
|
390
394
|
a.mu.Unlock()
|
|
391
395
|
|
|
392
396
|
if len(pending) > 0 {
|
|
393
|
-
for tcID,
|
|
397
|
+
for tcID, info := range pending {
|
|
394
398
|
output := text
|
|
395
399
|
if output == "" {
|
|
396
|
-
output =
|
|
400
|
+
output = info.name + " completed"
|
|
397
401
|
}
|
|
398
|
-
|
|
402
|
+
// Emit single complete tool_call with rawInput + rawOutput
|
|
403
|
+
a.sendStartToolCall(sessionID, tcID, info.name, info.args, output, "completed")
|
|
399
404
|
}
|
|
400
405
|
a.mu.Lock()
|
|
401
406
|
delete(a.pendingToolCalls, sessionID)
|
|
@@ -412,10 +417,8 @@ func (a *AgentHubACPAgent) processSSEEvent(sessionID, raw string) {
|
|
|
412
417
|
}
|
|
413
418
|
}
|
|
414
419
|
for _, tc := range toolCalls {
|
|
415
|
-
tcID
|
|
416
|
-
|
|
417
|
-
tcID = uuid.New().String()
|
|
418
|
-
}
|
|
420
|
+
// Always use UUID for tcID to avoid invalid IDs from LangChain
|
|
421
|
+
tcID := uuid.New().String()
|
|
419
422
|
tcName, _ := tc["name"].(string)
|
|
420
423
|
if tcName == "" {
|
|
421
424
|
tcName = "unknown"
|