langgraph-api 0.15.0.dev1__py3-none-any.whl → 0.15.0.dev2__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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/a2a.py +317 -27
- langgraph_api/api/a2ui.py +86 -0
- langgraph_api/api/a2ui_schemas/README.md +5 -0
- langgraph_api/api/a2ui_schemas/client_capabilities.json +74 -0
- langgraph_api/api/a2ui_schemas/client_data_model.json +15 -0
- langgraph_api/api/a2ui_schemas/client_to_server.json +56 -0
- langgraph_api/api/a2ui_schemas/server_to_client.json +86 -0
- langgraph_api/api/assistants.py +48 -57
- langgraph_api/api/runs.py +88 -52
- langgraph_api/api/store.py +17 -3
- langgraph_api/api/threads.py +42 -48
- langgraph_api/auth/custom.py +10 -1
- langgraph_api/cron_scheduler.py +4 -1
- langgraph_api/encryption/middleware.py +18 -0
- langgraph_api/js/client.mts +1 -1
- langgraph_api/js/remote.py +2 -2
- langgraph_api/models/run.py +6 -8
- langgraph_api/worker.py +4 -1
- {langgraph_api-0.15.0.dev1.dist-info → langgraph_api-0.15.0.dev2.dist-info}/METADATA +1 -1
- {langgraph_api-0.15.0.dev1.dist-info → langgraph_api-0.15.0.dev2.dist-info}/RECORD +27 -21
- langgraph_grpc_common/proto/store_pb2.py +51 -47
- langgraph_grpc_common/proto/store_pb2.pyi +27 -1
- langgraph_grpc_common/store.py +20 -3
- {langgraph_api-0.15.0.dev1.dist-info → langgraph_api-0.15.0.dev2.dist-info}/WHEEL +0 -0
- {langgraph_api-0.15.0.dev1.dist-info → langgraph_api-0.15.0.dev2.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.15.0.dev1.dist-info → langgraph_api-0.15.0.dev2.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.15.0.
|
|
1
|
+
__version__ = "0.15.0.dev2"
|
langgraph_api/api/a2a.py
CHANGED
|
@@ -16,6 +16,7 @@ import binascii
|
|
|
16
16
|
import functools
|
|
17
17
|
import os
|
|
18
18
|
import uuid
|
|
19
|
+
from dataclasses import dataclass, replace
|
|
19
20
|
from datetime import UTC, datetime
|
|
20
21
|
from typing import Any, Literal, NotRequired, cast
|
|
21
22
|
|
|
@@ -27,6 +28,15 @@ from starlette.responses import JSONResponse, Response
|
|
|
27
28
|
from typing_extensions import TypedDict
|
|
28
29
|
|
|
29
30
|
from langgraph_api import __version__
|
|
31
|
+
from langgraph_api.api.a2ui import (
|
|
32
|
+
A2UI_EXTENSION_URI,
|
|
33
|
+
A2UI_MIME_TYPE,
|
|
34
|
+
A2UIValidationError,
|
|
35
|
+
is_a2ui_mime_type,
|
|
36
|
+
normalize_metadata,
|
|
37
|
+
validate_client_metadata,
|
|
38
|
+
validate_payload,
|
|
39
|
+
)
|
|
30
40
|
from langgraph_api.config import A2A_ALLOWED_TOOL_CALL_RESULTS
|
|
31
41
|
from langgraph_api.metadata import USER_API_URL
|
|
32
42
|
from langgraph_api.route import ApiRequest, ApiRoute
|
|
@@ -103,6 +113,73 @@ class InvalidAgentResponseError(ValueError):
|
|
|
103
113
|
"""Raised when public agent output cannot be represented as an A2A part."""
|
|
104
114
|
|
|
105
115
|
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class A2UIContext:
|
|
118
|
+
enabled: bool
|
|
119
|
+
active: bool
|
|
120
|
+
metadata: dict[str, Any]
|
|
121
|
+
extensions: list[str]
|
|
122
|
+
drop_invalid: bool = False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_A2UI_DISABLED_CONTEXT = A2UIContext(False, False, {}, [])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _a2ui_enabled(assistant: dict[str, Any]) -> bool:
|
|
129
|
+
metadata = assistant.get("metadata")
|
|
130
|
+
a2a_metadata = metadata.get("a2a") if isinstance(metadata, dict) else None
|
|
131
|
+
return isinstance(a2a_metadata, dict) and a2a_metadata.get("a2ui") is True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _a2ui_context_for_run(
|
|
135
|
+
run: dict[str, Any], *, enabled: bool | None = None
|
|
136
|
+
) -> A2UIContext:
|
|
137
|
+
metadata = run.get("metadata")
|
|
138
|
+
a2a_metadata = metadata.get("a2a") if isinstance(metadata, dict) else None
|
|
139
|
+
if not isinstance(a2a_metadata, dict):
|
|
140
|
+
a2a_metadata = {}
|
|
141
|
+
return A2UIContext(
|
|
142
|
+
enabled=a2a_metadata.get("a2ui") is True if enabled is None else enabled,
|
|
143
|
+
active=a2a_metadata.get("a2ui_active") is True,
|
|
144
|
+
metadata={},
|
|
145
|
+
extensions=[],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _a2ui_run_metadata(
|
|
150
|
+
assistant: dict[str, Any], context: A2UIContext
|
|
151
|
+
) -> dict[str, Any] | None:
|
|
152
|
+
if not context.enabled:
|
|
153
|
+
return None
|
|
154
|
+
metadata = assistant.get("metadata")
|
|
155
|
+
a2a_metadata = metadata.get("a2a") if isinstance(metadata, dict) else None
|
|
156
|
+
return {
|
|
157
|
+
"a2a": {
|
|
158
|
+
**(a2a_metadata if isinstance(a2a_metadata, dict) else {}),
|
|
159
|
+
"a2ui_active": context.active,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _a2ui_message_context(
|
|
165
|
+
message: dict[str, Any], assistant: dict[str, Any]
|
|
166
|
+
) -> A2UIContext:
|
|
167
|
+
metadata = message.get("metadata") or {}
|
|
168
|
+
extensions = message.get("extensions") or []
|
|
169
|
+
if (
|
|
170
|
+
not isinstance(metadata, dict)
|
|
171
|
+
or not isinstance(extensions, list)
|
|
172
|
+
or not all(isinstance(extension, str) for extension in extensions)
|
|
173
|
+
):
|
|
174
|
+
raise A2UIValidationError("Invalid A2A message metadata or extensions")
|
|
175
|
+
return A2UIContext(
|
|
176
|
+
enabled=_a2ui_enabled(assistant),
|
|
177
|
+
active=A2UI_EXTENSION_URI in extensions,
|
|
178
|
+
metadata=metadata,
|
|
179
|
+
extensions=extensions,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
106
183
|
# ============================================================================
|
|
107
184
|
# Legacy (v0.x) format helpers
|
|
108
185
|
# ============================================================================
|
|
@@ -284,7 +361,15 @@ async def _validate_supports_messages(
|
|
|
284
361
|
|
|
285
362
|
# Validate messages field only if there are text or file parts
|
|
286
363
|
has_message_parts = any(
|
|
287
|
-
("text" in part)
|
|
364
|
+
("text" in part)
|
|
365
|
+
or ("file" in part)
|
|
366
|
+
or (
|
|
367
|
+
"data" in part
|
|
368
|
+
and isinstance(part.get("metadata"), dict)
|
|
369
|
+
and is_a2ui_mime_type(part["metadata"].get("mimeType"))
|
|
370
|
+
)
|
|
371
|
+
for part in parts
|
|
372
|
+
if isinstance(part, dict)
|
|
288
373
|
)
|
|
289
374
|
if has_message_parts:
|
|
290
375
|
input_schema = schemas.get("input_schema") or schemas.get("state_schema")
|
|
@@ -506,12 +591,14 @@ def _process_a2a_message_parts(
|
|
|
506
591
|
parts: list[dict[str, Any]],
|
|
507
592
|
message_role: str,
|
|
508
593
|
message_id: str,
|
|
594
|
+
*,
|
|
595
|
+
a2ui: A2UIContext,
|
|
509
596
|
) -> dict[str, Any]:
|
|
510
597
|
"""Convert A2A message parts to LangChain messages format.
|
|
511
598
|
|
|
512
|
-
Text-only messages keep today's per-part string content. When
|
|
513
|
-
is present, emit one consolidated message whose ``content`` is an
|
|
514
|
-
list of LangChain content blocks
|
|
599
|
+
Text-only messages keep today's per-part string content. When a file or
|
|
600
|
+
A2UI part is present, emit one consolidated message whose ``content`` is an
|
|
601
|
+
ordered list of LangChain content blocks.
|
|
515
602
|
|
|
516
603
|
Args:
|
|
517
604
|
parts: List of A2A message parts
|
|
@@ -526,7 +613,19 @@ def _process_a2a_message_parts(
|
|
|
526
613
|
messages = []
|
|
527
614
|
additional_data = {}
|
|
528
615
|
content_blocks: list[dict[str, Any]] = []
|
|
529
|
-
|
|
616
|
+
validate_client_metadata(a2ui.metadata)
|
|
617
|
+
has_content_blocks = any(
|
|
618
|
+
isinstance(part, dict)
|
|
619
|
+
and (
|
|
620
|
+
"file" in part
|
|
621
|
+
or (
|
|
622
|
+
"data" in part
|
|
623
|
+
and isinstance(part.get("metadata"), dict)
|
|
624
|
+
and is_a2ui_mime_type(part["metadata"].get("mimeType"))
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
for part in parts
|
|
628
|
+
)
|
|
530
629
|
langgraph_role = "human" if message_role == "ROLE_USER" else "assistant"
|
|
531
630
|
|
|
532
631
|
for part in parts:
|
|
@@ -544,7 +643,7 @@ def _process_a2a_message_parts(
|
|
|
544
643
|
)
|
|
545
644
|
|
|
546
645
|
if "text" in part:
|
|
547
|
-
if
|
|
646
|
+
if has_content_blocks:
|
|
548
647
|
content_blocks.append({"type": "text", "text": part["text"]})
|
|
549
648
|
else:
|
|
550
649
|
messages.append(
|
|
@@ -559,8 +658,26 @@ def _process_a2a_message_parts(
|
|
|
559
658
|
content_blocks.append(_a2a_file_part_to_content_block(part))
|
|
560
659
|
|
|
561
660
|
elif "data" in part:
|
|
562
|
-
# Data parts become structured input parameters
|
|
563
661
|
part_data = part.get("data", {})
|
|
662
|
+
part_metadata = (
|
|
663
|
+
part.get("metadata") if isinstance(part.get("metadata"), dict) else {}
|
|
664
|
+
)
|
|
665
|
+
if is_a2ui_mime_type(part_metadata.get("mimeType")):
|
|
666
|
+
if not a2ui.enabled:
|
|
667
|
+
raise A2UIValidationError("A2UI is not enabled for this agent")
|
|
668
|
+
if not a2ui.active:
|
|
669
|
+
raise A2UIValidationError("The A2UI extension must be activated")
|
|
670
|
+
validate_payload(part_data, direction="client")
|
|
671
|
+
content_blocks.append(
|
|
672
|
+
{
|
|
673
|
+
"type": "data",
|
|
674
|
+
"data": part_data,
|
|
675
|
+
"metadata": normalize_metadata(part_metadata),
|
|
676
|
+
}
|
|
677
|
+
)
|
|
678
|
+
continue
|
|
679
|
+
|
|
680
|
+
# Non-A2UI data parts remain structured top-level input parameters.
|
|
564
681
|
if not isinstance(part_data, dict):
|
|
565
682
|
raise ValueError(
|
|
566
683
|
"DataPart must contain a JSON object in the 'data' field"
|
|
@@ -573,7 +690,7 @@ def _process_a2a_message_parts(
|
|
|
573
690
|
"A2A agents support 'text', 'data', and 'file' parts only."
|
|
574
691
|
)
|
|
575
692
|
|
|
576
|
-
if
|
|
693
|
+
if has_content_blocks:
|
|
577
694
|
messages.append(
|
|
578
695
|
{
|
|
579
696
|
"role": langgraph_role,
|
|
@@ -582,6 +699,14 @@ def _process_a2a_message_parts(
|
|
|
582
699
|
}
|
|
583
700
|
)
|
|
584
701
|
|
|
702
|
+
if a2ui.metadata or a2ui.extensions:
|
|
703
|
+
a2a_data = {
|
|
704
|
+
**({"metadata": a2ui.metadata} if a2ui.metadata else {}),
|
|
705
|
+
**({"extensions": a2ui.extensions} if a2ui.extensions else {}),
|
|
706
|
+
}
|
|
707
|
+
for graph_message in messages:
|
|
708
|
+
graph_message["additional_kwargs"] = {"a2a": a2a_data}
|
|
709
|
+
|
|
585
710
|
if not messages and not additional_data:
|
|
586
711
|
raise ValueError(
|
|
587
712
|
"Message must contain at least one valid text, data, or file part"
|
|
@@ -707,7 +832,12 @@ def _content_block_to_a2a_file_part(block: dict[str, Any]) -> dict[str, Any]:
|
|
|
707
832
|
return {"kind": "file", "file": file_obj}
|
|
708
833
|
|
|
709
834
|
|
|
710
|
-
def _content_to_a2a_parts(
|
|
835
|
+
def _content_to_a2a_parts(
|
|
836
|
+
content: Any,
|
|
837
|
+
*,
|
|
838
|
+
a2ui: A2UIContext,
|
|
839
|
+
a2ui_direction: Literal["client", "server"] = "server",
|
|
840
|
+
) -> list[dict[str, Any]]:
|
|
711
841
|
"""Convert public LangChain message content to ordered A2A parts."""
|
|
712
842
|
|
|
713
843
|
if isinstance(content, str):
|
|
@@ -732,6 +862,39 @@ def _content_to_a2a_parts(content: Any) -> list[dict[str, Any]]:
|
|
|
732
862
|
text = block.get("text")
|
|
733
863
|
if isinstance(text, str):
|
|
734
864
|
text_parts.append(text)
|
|
865
|
+
elif isinstance(block, dict) and block.get("type") == "data":
|
|
866
|
+
metadata = (
|
|
867
|
+
block.get("metadata") if isinstance(block.get("metadata"), dict) else {}
|
|
868
|
+
)
|
|
869
|
+
if not is_a2ui_mime_type(metadata.get("mimeType")):
|
|
870
|
+
continue
|
|
871
|
+
if not a2ui.enabled:
|
|
872
|
+
if a2ui.drop_invalid:
|
|
873
|
+
continue
|
|
874
|
+
raise A2UIValidationError(
|
|
875
|
+
"Agent emitted A2UI while the extension was disabled"
|
|
876
|
+
)
|
|
877
|
+
if not a2ui.active:
|
|
878
|
+
continue
|
|
879
|
+
data = block.get("data")
|
|
880
|
+
try:
|
|
881
|
+
validate_payload(data, direction=a2ui_direction)
|
|
882
|
+
except A2UIValidationError:
|
|
883
|
+
if not a2ui.drop_invalid:
|
|
884
|
+
raise
|
|
885
|
+
logger.warning(
|
|
886
|
+
"Dropping invalid A2UI block from task history",
|
|
887
|
+
direction=a2ui_direction,
|
|
888
|
+
)
|
|
889
|
+
continue
|
|
890
|
+
flush_text()
|
|
891
|
+
parts.append(
|
|
892
|
+
{
|
|
893
|
+
"kind": "data",
|
|
894
|
+
"data": data,
|
|
895
|
+
"metadata": normalize_metadata(metadata),
|
|
896
|
+
}
|
|
897
|
+
)
|
|
735
898
|
elif (
|
|
736
899
|
isinstance(block, dict) and block.get("type") in _MEDIA_CONTENT_BLOCK_TYPES
|
|
737
900
|
):
|
|
@@ -746,7 +909,7 @@ def _a2a_parts_have_content(parts: list[dict[str, Any]]) -> bool:
|
|
|
746
909
|
"""Return whether converted parts contain public text or file content."""
|
|
747
910
|
|
|
748
911
|
return any(
|
|
749
|
-
part.get("kind")
|
|
912
|
+
part.get("kind") in {"data", "file"}
|
|
750
913
|
or (part.get("kind") == "text" and bool(part.get("text")))
|
|
751
914
|
for part in parts
|
|
752
915
|
)
|
|
@@ -772,7 +935,9 @@ def _content_to_text(content: Any) -> str:
|
|
|
772
935
|
return str(content)
|
|
773
936
|
|
|
774
937
|
|
|
775
|
-
def _extract_a2a_response(
|
|
938
|
+
def _extract_a2a_response(
|
|
939
|
+
result: dict[str, Any], *, a2ui: A2UIContext
|
|
940
|
+
) -> list[dict[str, Any]]:
|
|
776
941
|
"""Extract public A2A parts from the last assistant response.
|
|
777
942
|
|
|
778
943
|
Args:
|
|
@@ -802,7 +967,10 @@ def _extract_a2a_response(result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
802
967
|
if not isinstance(message, dict):
|
|
803
968
|
continue
|
|
804
969
|
if _a2a_role_for_message(message) == "ROLE_AGENT" and "content" in message:
|
|
805
|
-
parts = _content_to_a2a_parts(
|
|
970
|
+
parts = _content_to_a2a_parts(
|
|
971
|
+
message["content"],
|
|
972
|
+
a2ui=a2ui,
|
|
973
|
+
)
|
|
806
974
|
if _a2a_parts_have_content(parts):
|
|
807
975
|
return parts
|
|
808
976
|
|
|
@@ -1135,6 +1303,7 @@ def _convert_messages_to_a2a_format(
|
|
|
1135
1303
|
context_id: str,
|
|
1136
1304
|
*,
|
|
1137
1305
|
history_length: int | None = None,
|
|
1306
|
+
a2ui: A2UIContext,
|
|
1138
1307
|
) -> list[dict[str, Any]]:
|
|
1139
1308
|
"""Convert LangChain messages to A2A message format.
|
|
1140
1309
|
|
|
@@ -1154,6 +1323,8 @@ def _convert_messages_to_a2a_format(
|
|
|
1154
1323
|
List of A2A messages
|
|
1155
1324
|
"""
|
|
1156
1325
|
|
|
1326
|
+
history_a2ui = replace(a2ui, drop_invalid=True)
|
|
1327
|
+
|
|
1157
1328
|
# Convert each LangChain message to A2A format
|
|
1158
1329
|
a2a_messages = []
|
|
1159
1330
|
for msg in messages:
|
|
@@ -1169,7 +1340,11 @@ def _convert_messages_to_a2a_format(
|
|
|
1169
1340
|
a2a_role = "ROLE_AGENT"
|
|
1170
1341
|
parts = [_tool_results_data_part([tool_result])]
|
|
1171
1342
|
else:
|
|
1172
|
-
parts = _content_to_a2a_parts(
|
|
1343
|
+
parts = _content_to_a2a_parts(
|
|
1344
|
+
msg.get("content", ""),
|
|
1345
|
+
a2ui=history_a2ui,
|
|
1346
|
+
a2ui_direction=("client" if a2a_role == "ROLE_USER" else "server"),
|
|
1347
|
+
)
|
|
1173
1348
|
if a2a_role == "ROLE_AGENT" and not _a2a_parts_have_content(parts):
|
|
1174
1349
|
continue
|
|
1175
1350
|
if not parts:
|
|
@@ -1197,6 +1372,7 @@ async def _create_task_response(
|
|
|
1197
1372
|
context_id: str,
|
|
1198
1373
|
result: dict[str, Any],
|
|
1199
1374
|
assistant_id: str,
|
|
1375
|
+
a2ui: A2UIContext,
|
|
1200
1376
|
history_length: int | None = None,
|
|
1201
1377
|
) -> dict[str, Any]:
|
|
1202
1378
|
"""Create A2A Task response structure for both success and failure cases.
|
|
@@ -1218,6 +1394,7 @@ async def _create_task_response(
|
|
|
1218
1394
|
task_id,
|
|
1219
1395
|
context_id,
|
|
1220
1396
|
history_length=history_length,
|
|
1397
|
+
a2ui=a2ui,
|
|
1221
1398
|
)
|
|
1222
1399
|
|
|
1223
1400
|
base_task: dict[str, Any] = {
|
|
@@ -1257,7 +1434,13 @@ async def _create_task_response(
|
|
|
1257
1434
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
1258
1435
|
}
|
|
1259
1436
|
base_task["artifacts"] = [
|
|
1260
|
-
_create_response_artifact(
|
|
1437
|
+
_create_response_artifact(
|
|
1438
|
+
_extract_a2a_response(
|
|
1439
|
+
result,
|
|
1440
|
+
a2ui=a2ui,
|
|
1441
|
+
),
|
|
1442
|
+
assistant_id,
|
|
1443
|
+
)
|
|
1261
1444
|
]
|
|
1262
1445
|
|
|
1263
1446
|
return {"result": {"task": base_task}}
|
|
@@ -1510,7 +1693,7 @@ async def handle_jsonrpc_request(
|
|
|
1510
1693
|
elif method == "SendMessage":
|
|
1511
1694
|
result_or_error = await handle_message_send(request, params, assistant_id)
|
|
1512
1695
|
elif method == "GetTask":
|
|
1513
|
-
result_or_error = await handle_tasks_get(request, params)
|
|
1696
|
+
result_or_error = await handle_tasks_get(request, params, assistant_id)
|
|
1514
1697
|
elif method == "CancelTask":
|
|
1515
1698
|
result_or_error = await handle_tasks_cancel(request, params)
|
|
1516
1699
|
elif method == "ListTasks":
|
|
@@ -1656,8 +1839,21 @@ async def handle_message_send(
|
|
|
1656
1839
|
|
|
1657
1840
|
# Process A2A message parts into LangChain messages format
|
|
1658
1841
|
try:
|
|
1842
|
+
a2ui = _a2ui_message_context(message, assistant)
|
|
1659
1843
|
message_role = _normalize_input_role(message.get("role", "ROLE_USER"))
|
|
1660
|
-
input_content = _process_a2a_message_parts(
|
|
1844
|
+
input_content = _process_a2a_message_parts(
|
|
1845
|
+
parts,
|
|
1846
|
+
message_role,
|
|
1847
|
+
message_id,
|
|
1848
|
+
a2ui=a2ui,
|
|
1849
|
+
)
|
|
1850
|
+
except A2UIValidationError as e:
|
|
1851
|
+
return {
|
|
1852
|
+
"error": {
|
|
1853
|
+
"code": ERROR_CODE_INVALID_PARAMS,
|
|
1854
|
+
"message": str(e),
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1661
1857
|
except ValueError as e:
|
|
1662
1858
|
return {
|
|
1663
1859
|
"error": {
|
|
@@ -1714,6 +1910,7 @@ async def handle_message_send(
|
|
|
1714
1910
|
input=input_content,
|
|
1715
1911
|
command=command,
|
|
1716
1912
|
context=run_context,
|
|
1913
|
+
metadata=_a2ui_run_metadata(assistant, a2ui),
|
|
1717
1914
|
if_not_exists="create",
|
|
1718
1915
|
headers=request.headers,
|
|
1719
1916
|
)
|
|
@@ -1740,6 +1937,7 @@ async def handle_message_send(
|
|
|
1740
1937
|
result=result,
|
|
1741
1938
|
assistant_id=assistant_id,
|
|
1742
1939
|
history_length=history_length,
|
|
1940
|
+
a2ui=a2ui,
|
|
1743
1941
|
)
|
|
1744
1942
|
|
|
1745
1943
|
except InvalidAgentResponseError as e:
|
|
@@ -1784,7 +1982,7 @@ async def _get_historical_messages_for_task(
|
|
|
1784
1982
|
|
|
1785
1983
|
|
|
1786
1984
|
async def handle_tasks_get(
|
|
1787
|
-
request: ApiRequest, params: dict[str, Any]
|
|
1985
|
+
request: ApiRequest, params: dict[str, Any], assistant_id: str
|
|
1788
1986
|
) -> dict[str, Any]:
|
|
1789
1987
|
"""Handle tasks/get requests to retrieve task status.
|
|
1790
1988
|
|
|
@@ -1843,7 +2041,8 @@ async def handle_tasks_get(
|
|
|
1843
2041
|
|
|
1844
2042
|
try:
|
|
1845
2043
|
# TODO: fix the N+1 query issue
|
|
1846
|
-
run_info, thread_info = await asyncio.gather(
|
|
2044
|
+
assistant, run_info, thread_info = await asyncio.gather(
|
|
2045
|
+
_get_assistant(assistant_id, request.headers),
|
|
1847
2046
|
client.runs.get(
|
|
1848
2047
|
thread_id=context_id,
|
|
1849
2048
|
run_id=run_id,
|
|
@@ -1896,7 +2095,12 @@ async def handle_tasks_get(
|
|
|
1896
2095
|
task_id,
|
|
1897
2096
|
context_id,
|
|
1898
2097
|
history_length=history_length,
|
|
2098
|
+
a2ui=_a2ui_context_for_run(
|
|
2099
|
+
run_info, enabled=_a2ui_enabled(assistant)
|
|
2100
|
+
),
|
|
1899
2101
|
)
|
|
2102
|
+
except A2UIValidationError:
|
|
2103
|
+
raise
|
|
1900
2104
|
except InvalidAgentResponseError as e:
|
|
1901
2105
|
return {
|
|
1902
2106
|
"error": {
|
|
@@ -2225,6 +2429,7 @@ async def handle_list_tasks(
|
|
|
2225
2429
|
|
|
2226
2430
|
# Collect all runs from matching threads
|
|
2227
2431
|
all_tasks: list[dict[str, Any]] = []
|
|
2432
|
+
a2ui_by_task_id: dict[str, A2UIContext] = {}
|
|
2228
2433
|
for tid in thread_ids:
|
|
2229
2434
|
try:
|
|
2230
2435
|
runs = await client.runs.list(
|
|
@@ -2267,6 +2472,7 @@ async def handle_list_tasks(
|
|
|
2267
2472
|
task["artifacts"] = []
|
|
2268
2473
|
|
|
2269
2474
|
all_tasks.append(task)
|
|
2475
|
+
a2ui_by_task_id[task_id] = _a2ui_context_for_run(run)
|
|
2270
2476
|
|
|
2271
2477
|
# Sort by timestamp descending (newest first)
|
|
2272
2478
|
all_tasks.sort(
|
|
@@ -2290,6 +2496,7 @@ async def handle_list_tasks(
|
|
|
2290
2496
|
task["id"],
|
|
2291
2497
|
task["contextId"],
|
|
2292
2498
|
history_length=history_length,
|
|
2499
|
+
a2ui=a2ui_by_task_id[task["id"]],
|
|
2293
2500
|
)
|
|
2294
2501
|
|
|
2295
2502
|
await asyncio.gather(*(populate_history(task) for task in page_tasks))
|
|
@@ -2445,6 +2652,25 @@ async def generate_agent_card(request: ApiRequest, assistant_id: str) -> dict[st
|
|
|
2445
2652
|
assistant.get("description") or f"{assistant_name} assistant"
|
|
2446
2653
|
)
|
|
2447
2654
|
input_modes, output_modes = _resolve_agent_card_modes(assistant)
|
|
2655
|
+
extensions: list[dict[str, Any]] = []
|
|
2656
|
+
if _a2ui_enabled(assistant):
|
|
2657
|
+
if A2UI_MIME_TYPE not in input_modes:
|
|
2658
|
+
input_modes.append(A2UI_MIME_TYPE)
|
|
2659
|
+
if A2UI_MIME_TYPE not in output_modes:
|
|
2660
|
+
output_modes.append(A2UI_MIME_TYPE)
|
|
2661
|
+
extensions.append(
|
|
2662
|
+
{
|
|
2663
|
+
"uri": A2UI_EXTENSION_URI,
|
|
2664
|
+
"description": "Ability to render A2UI v0.9",
|
|
2665
|
+
"required": False,
|
|
2666
|
+
"params": {
|
|
2667
|
+
"v0.9": {
|
|
2668
|
+
"supportedCatalogIds": [],
|
|
2669
|
+
"acceptsInlineCatalogs": False,
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
}
|
|
2673
|
+
)
|
|
2448
2674
|
|
|
2449
2675
|
# For now, each assistant has one main skill - itself
|
|
2450
2676
|
skills = [
|
|
@@ -2488,6 +2714,14 @@ async def generate_agent_card(request: ApiRequest, assistant_id: str) -> dict[st
|
|
|
2488
2714
|
|
|
2489
2715
|
agent_url = f"{base_url}{agent_path}"
|
|
2490
2716
|
|
|
2717
|
+
capabilities: dict[str, Any] = {
|
|
2718
|
+
"streaming": True,
|
|
2719
|
+
"pushNotifications": False, # Not implemented yet
|
|
2720
|
+
"stateTransitionHistory": False,
|
|
2721
|
+
}
|
|
2722
|
+
if extensions:
|
|
2723
|
+
capabilities["extensions"] = extensions
|
|
2724
|
+
|
|
2491
2725
|
return {
|
|
2492
2726
|
"name": assistant_name,
|
|
2493
2727
|
"description": assistant_description,
|
|
@@ -2495,15 +2729,11 @@ async def generate_agent_card(request: ApiRequest, assistant_id: str) -> dict[st
|
|
|
2495
2729
|
"supportedInterfaces": [
|
|
2496
2730
|
{
|
|
2497
2731
|
"url": agent_url,
|
|
2498
|
-
"protocolBinding": "
|
|
2732
|
+
"protocolBinding": "JSONRPC",
|
|
2499
2733
|
"protocolVersion": "1.0",
|
|
2500
2734
|
},
|
|
2501
2735
|
],
|
|
2502
|
-
"capabilities":
|
|
2503
|
-
"streaming": True,
|
|
2504
|
-
"pushNotifications": False, # Not implemented yet
|
|
2505
|
-
"stateTransitionHistory": False,
|
|
2506
|
-
},
|
|
2736
|
+
"capabilities": capabilities,
|
|
2507
2737
|
"defaultInputModes": input_modes,
|
|
2508
2738
|
"defaultOutputModes": output_modes,
|
|
2509
2739
|
"skills": skills,
|
|
@@ -2680,10 +2910,27 @@ async def handle_message_stream(
|
|
|
2680
2910
|
|
|
2681
2911
|
# Process A2A message parts into LangChain messages format
|
|
2682
2912
|
try:
|
|
2913
|
+
a2ui = _a2ui_message_context(message, assistant)
|
|
2683
2914
|
message_role = _normalize_input_role(message.get("role", "ROLE_USER"))
|
|
2684
2915
|
input_content = _process_a2a_message_parts(
|
|
2685
|
-
parts,
|
|
2916
|
+
parts,
|
|
2917
|
+
message_role,
|
|
2918
|
+
message_id,
|
|
2919
|
+
a2ui=a2ui,
|
|
2920
|
+
)
|
|
2921
|
+
except A2UIValidationError as e:
|
|
2922
|
+
yield (
|
|
2923
|
+
b"message",
|
|
2924
|
+
{
|
|
2925
|
+
"jsonrpc": "2.0",
|
|
2926
|
+
"id": rpc_id,
|
|
2927
|
+
"error": {
|
|
2928
|
+
"code": ERROR_CODE_INVALID_PARAMS,
|
|
2929
|
+
"message": str(e),
|
|
2930
|
+
},
|
|
2931
|
+
},
|
|
2686
2932
|
)
|
|
2933
|
+
return
|
|
2687
2934
|
except ValueError as e:
|
|
2688
2935
|
yield (
|
|
2689
2936
|
b"message",
|
|
@@ -2775,6 +3022,7 @@ async def handle_message_stream(
|
|
|
2775
3022
|
input=input_content,
|
|
2776
3023
|
command=command,
|
|
2777
3024
|
context=run_context,
|
|
3025
|
+
metadata=_a2ui_run_metadata(assistant, a2ui),
|
|
2778
3026
|
headers=request.headers,
|
|
2779
3027
|
)
|
|
2780
3028
|
|
|
@@ -2825,7 +3073,26 @@ async def handle_message_stream(
|
|
|
2825
3073
|
final_parts: list[dict[str, Any]] | None = None
|
|
2826
3074
|
if isinstance(result, dict):
|
|
2827
3075
|
try:
|
|
2828
|
-
final_parts = _extract_a2a_response(
|
|
3076
|
+
final_parts = _extract_a2a_response(
|
|
3077
|
+
result,
|
|
3078
|
+
a2ui=a2ui,
|
|
3079
|
+
)
|
|
3080
|
+
except A2UIValidationError:
|
|
3081
|
+
await logger.aexception(
|
|
3082
|
+
"Agent emitted an invalid A2UI payload"
|
|
3083
|
+
)
|
|
3084
|
+
yield (
|
|
3085
|
+
b"message",
|
|
3086
|
+
{
|
|
3087
|
+
"jsonrpc": "2.0",
|
|
3088
|
+
"id": rpc_id,
|
|
3089
|
+
"error": {
|
|
3090
|
+
"code": ERROR_CODE_INTERNAL_ERROR,
|
|
3091
|
+
"message": "Internal server error",
|
|
3092
|
+
},
|
|
3093
|
+
},
|
|
3094
|
+
)
|
|
3095
|
+
return
|
|
2829
3096
|
except InvalidAgentResponseError as e:
|
|
2830
3097
|
yield (
|
|
2831
3098
|
b"message",
|
|
@@ -3031,7 +3298,24 @@ async def handle_message_stream(
|
|
|
3031
3298
|
# its plain terminal status-update.
|
|
3032
3299
|
if fallback_state == "TASK_STATE_COMPLETED" and isinstance(result, dict):
|
|
3033
3300
|
try:
|
|
3034
|
-
fallback_parts = _extract_a2a_response(
|
|
3301
|
+
fallback_parts = _extract_a2a_response(
|
|
3302
|
+
result,
|
|
3303
|
+
a2ui=a2ui,
|
|
3304
|
+
)
|
|
3305
|
+
except A2UIValidationError:
|
|
3306
|
+
await logger.aexception("Agent emitted an invalid A2UI payload")
|
|
3307
|
+
yield (
|
|
3308
|
+
b"message",
|
|
3309
|
+
{
|
|
3310
|
+
"jsonrpc": "2.0",
|
|
3311
|
+
"id": rpc_id,
|
|
3312
|
+
"error": {
|
|
3313
|
+
"code": ERROR_CODE_INTERNAL_ERROR,
|
|
3314
|
+
"message": "Internal server error",
|
|
3315
|
+
},
|
|
3316
|
+
},
|
|
3317
|
+
)
|
|
3318
|
+
return
|
|
3035
3319
|
except InvalidAgentResponseError as e:
|
|
3036
3320
|
yield (
|
|
3037
3321
|
b"message",
|
|
@@ -3206,6 +3490,12 @@ a2a_routes = [
|
|
|
3206
3490
|
handle_a2a_assistant_endpoint,
|
|
3207
3491
|
methods=["GET", "POST", "DELETE"],
|
|
3208
3492
|
),
|
|
3493
|
+
ApiRoute(
|
|
3494
|
+
"/a2a/{assistant_id}/",
|
|
3495
|
+
handle_a2a_assistant_endpoint,
|
|
3496
|
+
methods=["GET", "POST", "DELETE"],
|
|
3497
|
+
include_in_schema=False,
|
|
3498
|
+
),
|
|
3209
3499
|
# Per-assistant agent card (multi-tenant pattern)
|
|
3210
3500
|
ApiRoute(
|
|
3211
3501
|
"/a2a/{assistant_id}/.well-known/agent-card.json",
|