langgraph-agent-common 2.0.0__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.
- langgraph_agent_common-2.0.0/PKG-INFO +67 -0
- langgraph_agent_common-2.0.0/README.md +42 -0
- langgraph_agent_common-2.0.0/pyproject.toml +51 -0
- langgraph_agent_common-2.0.0/setup.cfg +4 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/agent_executor.py +421 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/conversion.py +167 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/sanitize_messages_for_llm.py +34 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/fastapi_application.py +152 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/checkpointer.py +102 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/graph.py +49 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/store.py +64 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/validator.py +42 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/openapi_schema.json +277 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/static/css/style.css +480 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/static/index.html +107 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/static/js/app.js +896 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/server.py +149 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/tasks/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/tasks/push_notification_config_store.py +24 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/tasks/task_store.py +66 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/utils/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/utils/lock.py +31 -0
- langgraph_agent_common-2.0.0/src/langgraph_a2a_api/utils/types.py +3 -0
- langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/PKG-INFO +67 -0
- langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/SOURCES.txt +35 -0
- langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/dependency_links.txt +1 -0
- langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/requires.txt +16 -0
- langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/top_level.txt +2 -0
- langgraph_agent_common-2.0.0/src/langgraph_aux/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_aux/callbacks/__init__.py +11 -0
- langgraph_agent_common-2.0.0/src/langgraph_aux/callbacks/talos_callback_handler.py +340 -0
- langgraph_agent_common-2.0.0/src/langgraph_aux/utils/__init__.py +0 -0
- langgraph_agent_common-2.0.0/src/langgraph_aux/utils/stream_output.py +13 -0
- langgraph_agent_common-2.0.0/test/test_deepagents_a2a_server.py +113 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langgraph-agent-common
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: langgraph-agent-common
|
|
5
|
+
Author-email: Aegon <1006785434@qq.com>
|
|
6
|
+
Project-URL: repository, https://github.com/liuxiahuiyi/langgraph-agent-common.git
|
|
7
|
+
Keywords: Langgraph,A2A
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: langchain>=1.3.11
|
|
11
|
+
Requires-Dist: langchain-openai>=1.3.2
|
|
12
|
+
Requires-Dist: langgraph>=1.2.5
|
|
13
|
+
Requires-Dist: langgraph-api>=0.10.0
|
|
14
|
+
Requires-Dist: langgraph-cli>=0.4.30
|
|
15
|
+
Requires-Dist: langgraph-checkpoint-postgres>=3.1.0
|
|
16
|
+
Requires-Dist: deepagents>=0.6.10
|
|
17
|
+
Requires-Dist: a2a-sdk>=1.1.0
|
|
18
|
+
Requires-Dist: a2a-sdk[http-server]>=1.1.0
|
|
19
|
+
Requires-Dist: a2a-sdk[postgresql]>=1.1.0
|
|
20
|
+
Requires-Dist: fastapi>=0.137.1
|
|
21
|
+
Requires-Dist: inflection>=0.5.1
|
|
22
|
+
Requires-Dist: APScheduler>=3.11.2
|
|
23
|
+
Provides-Extra: talos
|
|
24
|
+
Requires-Dist: talos-python3>=3.1.6; extra == "talos"
|
|
25
|
+
|
|
26
|
+
# langgraph-agent-common
|
|
27
|
+
|
|
28
|
+
```~/.pypirc
|
|
29
|
+
[distutils]
|
|
30
|
+
index-servers =
|
|
31
|
+
snapshot
|
|
32
|
+
stage
|
|
33
|
+
release
|
|
34
|
+
[snapshot]
|
|
35
|
+
repository:
|
|
36
|
+
username:
|
|
37
|
+
password:
|
|
38
|
+
[stage]
|
|
39
|
+
repository:
|
|
40
|
+
username:
|
|
41
|
+
password:
|
|
42
|
+
[release]
|
|
43
|
+
repository:
|
|
44
|
+
username:
|
|
45
|
+
password:
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```~/.pip/pip.conf
|
|
49
|
+
[global]
|
|
50
|
+
timeout=60000
|
|
51
|
+
index-url =
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m build --wheel
|
|
57
|
+
twine upload --repository snapshot dist/*.whl
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
brew install snappy
|
|
62
|
+
export CPPFLAGS="-I/opt/homebrew/include"
|
|
63
|
+
export LDFLAGS="-L/opt/homebrew/lib"
|
|
64
|
+
pip install -e .[talos]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`talos-python3` is optional. Default installation does not include it; install the `talos` extra only when Talos integration is needed.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# langgraph-agent-common
|
|
2
|
+
|
|
3
|
+
```~/.pypirc
|
|
4
|
+
[distutils]
|
|
5
|
+
index-servers =
|
|
6
|
+
snapshot
|
|
7
|
+
stage
|
|
8
|
+
release
|
|
9
|
+
[snapshot]
|
|
10
|
+
repository:
|
|
11
|
+
username:
|
|
12
|
+
password:
|
|
13
|
+
[stage]
|
|
14
|
+
repository:
|
|
15
|
+
username:
|
|
16
|
+
password:
|
|
17
|
+
[release]
|
|
18
|
+
repository:
|
|
19
|
+
username:
|
|
20
|
+
password:
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```~/.pip/pip.conf
|
|
24
|
+
[global]
|
|
25
|
+
timeout=60000
|
|
26
|
+
index-url =
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m build --wheel
|
|
32
|
+
twine upload --repository snapshot dist/*.whl
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
brew install snappy
|
|
37
|
+
export CPPFLAGS="-I/opt/homebrew/include"
|
|
38
|
+
export LDFLAGS="-L/opt/homebrew/lib"
|
|
39
|
+
pip install -e .[talos]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`talos-python3` is optional. Default installation does not include it; install the `talos` extra only when Talos integration is needed.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "langgraph-agent-common"
|
|
3
|
+
version = "2.0.0"
|
|
4
|
+
description = "langgraph-agent-common"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Aegon", email = "1006785434@qq.com" }]
|
|
7
|
+
requires-python = ">=3.13"
|
|
8
|
+
keywords = ["Langgraph", "A2A"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"langchain>=1.3.11",
|
|
11
|
+
"langchain-openai>=1.3.2",
|
|
12
|
+
"langgraph>=1.2.5",
|
|
13
|
+
"langgraph-api>=0.10.0",
|
|
14
|
+
"langgraph-cli>=0.4.30",
|
|
15
|
+
"langgraph-checkpoint-postgres>=3.1.0",
|
|
16
|
+
"deepagents>=0.6.10",
|
|
17
|
+
"a2a-sdk>=1.1.0",
|
|
18
|
+
"a2a-sdk[http-server]>=1.1.0",
|
|
19
|
+
"a2a-sdk[postgresql]>=1.1.0",
|
|
20
|
+
"fastapi>=0.137.1",
|
|
21
|
+
"inflection>=0.5.1",
|
|
22
|
+
"APScheduler>=3.11.2",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
talos = [
|
|
27
|
+
"talos-python3>=3.1.6",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
repository = "https://github.com/liuxiahuiyi/langgraph-agent-common.git"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
35
|
+
build-backend = "setuptools.build_meta"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
langgraph_a2a_api = ["resources/**/*"]
|
|
42
|
+
|
|
43
|
+
[tool.pyright]
|
|
44
|
+
exclude = [
|
|
45
|
+
"**/__pycache__",
|
|
46
|
+
"**/dist",
|
|
47
|
+
"**/build",
|
|
48
|
+
"**/node_modules",
|
|
49
|
+
"**/venv",
|
|
50
|
+
"**/.venv",
|
|
51
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import AsyncIterable
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
import asyncio
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
from google.protobuf.json_format import MessageToDict
|
|
10
|
+
from google.protobuf.timestamp_pb2 import Timestamp
|
|
11
|
+
|
|
12
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
13
|
+
from langchain_core.messages import AnyMessage, AIMessage, HumanMessage, RemoveMessage
|
|
14
|
+
from langchain_core.runnables import RunnableConfig
|
|
15
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
16
|
+
from langgraph.store.base import BaseStore
|
|
17
|
+
from langgraph.types import Command, StateSnapshot, Interrupt, Overwrite
|
|
18
|
+
from langgraph.typing import ContextT
|
|
19
|
+
from langgraph.errors import GraphRecursionError, GraphDrained
|
|
20
|
+
from langgraph.graph import END
|
|
21
|
+
from langgraph.runtime import RunControl
|
|
22
|
+
|
|
23
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
24
|
+
from a2a.server.events import EventQueue
|
|
25
|
+
from a2a.server.tasks import TaskUpdater, TaskStore
|
|
26
|
+
from a2a.server.context import ServerCallContext
|
|
27
|
+
from a2a.types import Task, Message, Part, TaskState, TaskStatus, Role
|
|
28
|
+
from a2a.utils.errors import InternalError, InvalidParamsError
|
|
29
|
+
from a2a.helpers import new_text_message
|
|
30
|
+
|
|
31
|
+
from .conversion import a2a_message_to_langgraph, langgraph_message_to_a2a, any_content_to_langgraph, langgraph_part_to_a2a
|
|
32
|
+
from langgraph_a2a_api.utils.types import atomic_type
|
|
33
|
+
from langgraph_a2a_api.utils.lock import AsyncResourceLock
|
|
34
|
+
from .sanitize_messages_for_llm import sanitize_messages_for_llm
|
|
35
|
+
from langgraph_a2a_api.langgraph.validator import validate_HITLResponse, HITLResponseModel
|
|
36
|
+
|
|
37
|
+
logging.basicConfig(level=logging.WARN)
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
TASK_STATE_SUBMITTED = TaskState.Value("TASK_STATE_SUBMITTED")
|
|
41
|
+
TASK_STATE_WORKING = TaskState.Value("TASK_STATE_WORKING")
|
|
42
|
+
TASK_STATE_COMPLETED = TaskState.Value("TASK_STATE_COMPLETED")
|
|
43
|
+
TASK_STATE_FAILED = TaskState.Value("TASK_STATE_FAILED")
|
|
44
|
+
TASK_STATE_CANCELED = TaskState.Value("TASK_STATE_CANCELED")
|
|
45
|
+
TASK_STATE_INPUT_REQUIRED = TaskState.Value("TASK_STATE_INPUT_REQUIRED")
|
|
46
|
+
TASK_STATE_REJECTED = TaskState.Value("TASK_STATE_REJECTED")
|
|
47
|
+
TASK_STATE_UNSEPCIFIED = TaskState.Value("TASK_STATE_UNSPECIFIED")
|
|
48
|
+
|
|
49
|
+
negative_graph_state = {TASK_STATE_CANCELED, TASK_STATE_FAILED, TASK_STATE_REJECTED}
|
|
50
|
+
|
|
51
|
+
class GraphAgentExecutor(AgentExecutor):
|
|
52
|
+
def __init__(self, graph: CompiledStateGraph, task_store: TaskStore):
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.graph = graph
|
|
55
|
+
self.graph_with_ck = None
|
|
56
|
+
self.task_store = task_store
|
|
57
|
+
self.run_controls: dict[str, RunControl] = {}
|
|
58
|
+
self.lock = AsyncResourceLock()
|
|
59
|
+
|
|
60
|
+
def bind_runtime_to_graph(self, checkpointer: BaseCheckpointSaver, store: BaseStore | None = None):
|
|
61
|
+
runtime = {"checkpointer": checkpointer}
|
|
62
|
+
if store is not None:
|
|
63
|
+
runtime["store"] = store
|
|
64
|
+
self.graph_with_ck = self.graph.copy(runtime)
|
|
65
|
+
|
|
66
|
+
async def stream_graph(self, message: Message, history: list[Message], context_id: str, task_id: str, metadata: dict[str, Any], updater: TaskUpdater):
|
|
67
|
+
context = MessageToDict(message.metadata) if message and message.metadata else {}
|
|
68
|
+
langgraph_message = a2a_message_to_langgraph(message)
|
|
69
|
+
langgraph_history = [a2a_message_to_langgraph(m) for m in history]
|
|
70
|
+
try:
|
|
71
|
+
recursion_limit = int(float(metadata.get("recursion_limit", 300)))
|
|
72
|
+
except (ValueError, TypeError):
|
|
73
|
+
recursion_limit = 300
|
|
74
|
+
stream_mode = metadata.get("stream_mode", "updates")
|
|
75
|
+
subgraphs = metadata.get("subgraphs", True)
|
|
76
|
+
|
|
77
|
+
config = {
|
|
78
|
+
"configurable": {
|
|
79
|
+
**metadata,
|
|
80
|
+
"thread_id": context_id,
|
|
81
|
+
"graph_id": self.graph.get_name(),
|
|
82
|
+
"assistant_id": self.graph.get_name(),
|
|
83
|
+
"task_id": task_id,
|
|
84
|
+
},
|
|
85
|
+
"recursion_limit": recursion_limit,
|
|
86
|
+
}
|
|
87
|
+
appended_artifacts = set()
|
|
88
|
+
|
|
89
|
+
current_state = await self.graph_with_ck.aget_state(config, subgraphs=subgraphs)
|
|
90
|
+
current_interrupts = await self._get_interrupts(current_state, subgraphs)
|
|
91
|
+
if current_interrupts:
|
|
92
|
+
current_interrupt_ids = [c.id for c in current_interrupts]
|
|
93
|
+
if isinstance(langgraph_message.content, list) and len(current_interrupts) == len(langgraph_message.content) and all([isinstance(part, dict) and part.get("type") == "non_standard" and validate_HITLResponse(part.get("value")) for part in langgraph_message.content]):
|
|
94
|
+
decisions = {part.get("value", {}).get("hitl_id"): part.get("value", {}).get("decision") for part in langgraph_message.content}
|
|
95
|
+
if set(current_interrupt_ids) == set(decisions.keys()):
|
|
96
|
+
resume_decisions = []
|
|
97
|
+
for hitl_id in current_interrupt_ids:
|
|
98
|
+
resume_decisions.append(decisions[hitl_id])
|
|
99
|
+
resume_content = {"decisions": resume_decisions}
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Interrupt resume content: {langgraph_message.content}. Must contain overall original decision ids {current_interrupt_ids}.")
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError(f"Invalid interrupt resume content: {langgraph_message.content}. Must be a list with the same length as interrupt ids: {current_interrupt_ids}. Each item in the list must be a data with HITLResponse schema: {HITLResponseModel.schema()}.")
|
|
104
|
+
inputs = Command(resume = resume_content, update = {"messages": sanitize_messages_for_llm(langgraph_history)})
|
|
105
|
+
for k, v in current_state.values.items():
|
|
106
|
+
if k in {"messages", "graph_status"} or not v or not (isinstance(v, list) and isinstance(v, set)):
|
|
107
|
+
continue
|
|
108
|
+
appended_artifacts.add(k)
|
|
109
|
+
else:
|
|
110
|
+
inputs = {"messages": sanitize_messages_for_llm(langgraph_history + [langgraph_message])}
|
|
111
|
+
|
|
112
|
+
run_control = RunControl()
|
|
113
|
+
self.run_controls[task_id] = run_control
|
|
114
|
+
try:
|
|
115
|
+
if stream_mode == "updates":
|
|
116
|
+
async for updates in self._stream_graph_updates(inputs, config, context, subgraphs, run_control):
|
|
117
|
+
await self._update_task(updates, context_id, task_id, updater, appended_artifacts)
|
|
118
|
+
elif stream_mode == "custom":
|
|
119
|
+
async for custom in self._stream_graph_custom(inputs, config, context, subgraphs, run_control):
|
|
120
|
+
await self._update_task(custom, context_id, task_id, updater, appended_artifacts)
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(f"Unsupported stream_mode {stream_mode}.")
|
|
123
|
+
finally:
|
|
124
|
+
self.run_controls.pop(task_id, None)
|
|
125
|
+
|
|
126
|
+
async def _get_interrupts(self, state: StateSnapshot, subgraphs: bool, seen_ids: set[str] = set()) -> list[Interrupt]:
|
|
127
|
+
interrupts = []
|
|
128
|
+
for interrupt in state.interrupts:
|
|
129
|
+
if interrupt.id not in seen_ids:
|
|
130
|
+
seen_ids.add(interrupt.id)
|
|
131
|
+
interrupts.append(interrupt)
|
|
132
|
+
for task in state.tasks:
|
|
133
|
+
if task.interrupts:
|
|
134
|
+
for interrupt in task.interrupts:
|
|
135
|
+
if interrupt.id not in seen_ids:
|
|
136
|
+
seen_ids.add(interrupt.id)
|
|
137
|
+
interrupts.append(interrupt)
|
|
138
|
+
nested_state = task.state
|
|
139
|
+
if isinstance(nested_state, StateSnapshot):
|
|
140
|
+
interrupts.extend(await self._get_interrupts(nested_state, subgraphs, seen_ids))
|
|
141
|
+
elif nested_state:
|
|
142
|
+
try:
|
|
143
|
+
fetched_state = await self.graph_with_ck.aget_state(nested_state, subgraphs=subgraphs)
|
|
144
|
+
except Exception:
|
|
145
|
+
continue
|
|
146
|
+
interrupts.extend(await self._get_interrupts(fetched_state, subgraphs, seen_ids))
|
|
147
|
+
return interrupts
|
|
148
|
+
|
|
149
|
+
def _normalize_messages(self, messages: Any) -> list[AnyMessage]:
|
|
150
|
+
if messages is None:
|
|
151
|
+
return []
|
|
152
|
+
if isinstance(messages, Overwrite):
|
|
153
|
+
messages = messages.value
|
|
154
|
+
if isinstance(messages, list):
|
|
155
|
+
return messages
|
|
156
|
+
raise ValueError(f"Unsupported type of messages: {type(messages).__name__}")
|
|
157
|
+
|
|
158
|
+
async def _update_task(self, state: dict[str, Any], context_id: str, task_id: str, updater: TaskUpdater, appended_artifacts: set):
|
|
159
|
+
if not state:
|
|
160
|
+
return
|
|
161
|
+
i = 0
|
|
162
|
+
for k, v in state.items():
|
|
163
|
+
append = False
|
|
164
|
+
i = i + 1
|
|
165
|
+
if k in {"messages", "graph_status"} or not v:
|
|
166
|
+
continue
|
|
167
|
+
if isinstance(v, atomic_type):
|
|
168
|
+
parts = [langgraph_part_to_a2a(str(v))]
|
|
169
|
+
elif isinstance(v, list) or isinstance(v, tuple):
|
|
170
|
+
parts = []
|
|
171
|
+
for part in v:
|
|
172
|
+
if isinstance(part, atomic_type):
|
|
173
|
+
parts.append(langgraph_part_to_a2a(str(part)))
|
|
174
|
+
elif isinstance(part, dict):
|
|
175
|
+
parts.append(langgraph_part_to_a2a({"type": "non_standard", "value": part}))
|
|
176
|
+
elif isinstance(part, BaseModel):
|
|
177
|
+
parts.append(langgraph_part_to_a2a({"type": "non_standard", "value": part.model_dump()}))
|
|
178
|
+
else:
|
|
179
|
+
raise ValueError(f"Unsupported type of artifact part {part}.")
|
|
180
|
+
if k in appended_artifacts:
|
|
181
|
+
append = True
|
|
182
|
+
else:
|
|
183
|
+
appended_artifacts.add(k)
|
|
184
|
+
elif isinstance(v, dict):
|
|
185
|
+
parts = [langgraph_part_to_a2a({"type": "non_standard", "value": v})]
|
|
186
|
+
elif isinstance(v, BaseModel):
|
|
187
|
+
parts = [langgraph_part_to_a2a({"type": "non_standard", "value": v.model_dump()})]
|
|
188
|
+
else:
|
|
189
|
+
raise ValueError(f"Unsupported type of artifact: {k}:{v}")
|
|
190
|
+
|
|
191
|
+
await updater.add_artifact(
|
|
192
|
+
parts = parts,
|
|
193
|
+
artifact_id = k,
|
|
194
|
+
name = k,
|
|
195
|
+
append = append,
|
|
196
|
+
last_chunk = False,
|
|
197
|
+
)
|
|
198
|
+
messages = [message for message in state.get("messages", []) if not isinstance(message, RemoveMessage)]
|
|
199
|
+
for message in messages[0:-1]:
|
|
200
|
+
a2a_message = langgraph_message_to_a2a(message, context_id, task_id)
|
|
201
|
+
await updater.update_status(
|
|
202
|
+
state = TASK_STATE_WORKING,
|
|
203
|
+
message = langgraph_message_to_a2a(message, context_id, task_id),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if state["graph_status"] == TASK_STATE_COMPLETED or state["graph_status"] in negative_graph_state or len(messages) > 0:
|
|
207
|
+
await updater.update_status(
|
|
208
|
+
state = state["graph_status"],
|
|
209
|
+
message = langgraph_message_to_a2a(messages[-1], context_id, task_id) if len(messages) > 0 else None,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
async def _stream_graph_updates(self, inputs: Union[dict[str, list[AnyMessage]], Command], config: RunnableConfig, context: ContextT, subgraphs: bool, control: RunControl) -> AsyncIterable[dict[str, Any]]:
|
|
213
|
+
graph_status = TASK_STATE_UNSEPCIFIED
|
|
214
|
+
break_out = False
|
|
215
|
+
pre_yield = None
|
|
216
|
+
try:
|
|
217
|
+
async for space, updates in self.graph_with_ck.astream(inputs, config = config, context = context, stream_mode = "updates", subgraphs = subgraphs, control = control):
|
|
218
|
+
if break_out:
|
|
219
|
+
continue
|
|
220
|
+
for step, update in updates.items():
|
|
221
|
+
if break_out:
|
|
222
|
+
continue
|
|
223
|
+
if not update:
|
|
224
|
+
continue
|
|
225
|
+
if pre_yield is not None:
|
|
226
|
+
yield pre_yield
|
|
227
|
+
pre_yield = None
|
|
228
|
+
if step == "__interrupt__":
|
|
229
|
+
graph_status = TASK_STATE_INPUT_REQUIRED
|
|
230
|
+
pre_yield = {
|
|
231
|
+
"messages": [AIMessage(content=any_content_to_langgraph([{"hitl_id": interrupt.id, "content": interrupt.value} for interrupt in update]))],
|
|
232
|
+
"graph_status": graph_status,
|
|
233
|
+
}
|
|
234
|
+
break_out = True
|
|
235
|
+
else:
|
|
236
|
+
messages = self._normalize_messages(update.get("messages"))
|
|
237
|
+
graph_status = TASK_STATE_WORKING
|
|
238
|
+
pre_yield = {
|
|
239
|
+
**update,
|
|
240
|
+
"messages": messages,
|
|
241
|
+
"graph_status": graph_status,
|
|
242
|
+
}
|
|
243
|
+
if not break_out:
|
|
244
|
+
graph_status = TASK_STATE_COMPLETED
|
|
245
|
+
yield {
|
|
246
|
+
**(pre_yield or {}),
|
|
247
|
+
"graph_status": graph_status,
|
|
248
|
+
}
|
|
249
|
+
else:
|
|
250
|
+
yield pre_yield
|
|
251
|
+
|
|
252
|
+
except GraphRecursionError as e:
|
|
253
|
+
if pre_yield is not None:
|
|
254
|
+
yield pre_yield
|
|
255
|
+
graph_status = TASK_STATE_FAILED
|
|
256
|
+
yield {
|
|
257
|
+
"messages": [AIMessage(content=str(e))],
|
|
258
|
+
"graph_status": graph_status,
|
|
259
|
+
}
|
|
260
|
+
except GraphDrained:
|
|
261
|
+
if pre_yield is not None:
|
|
262
|
+
yield pre_yield
|
|
263
|
+
yield {
|
|
264
|
+
"graph_status": TASK_STATE_CANCELED,
|
|
265
|
+
}
|
|
266
|
+
async def _stream_graph_custom(self, inputs: Union[dict[str, list[AnyMessage]], Command], config: RunnableConfig, context: ContextT, subgraphs: bool, control: RunControl) -> AsyncIterable[dict[str, Any]]:
|
|
267
|
+
graph_recursion_error = None
|
|
268
|
+
graph_drained = None
|
|
269
|
+
try:
|
|
270
|
+
async for space, custom in self.graph_with_ck.astream(inputs, config = config, context = context, stream_mode = "custom", subgraphs = subgraphs, control = control):
|
|
271
|
+
if not custom:
|
|
272
|
+
yield {}
|
|
273
|
+
continue
|
|
274
|
+
yield {
|
|
275
|
+
"messages": [AIMessage(content=any_content_to_langgraph(custom))],
|
|
276
|
+
"graph_status": TASK_STATE_WORKING,
|
|
277
|
+
}
|
|
278
|
+
except GraphRecursionError as e:
|
|
279
|
+
graph_recursion_error = str(e)
|
|
280
|
+
except GraphDrained as e:
|
|
281
|
+
graph_drained = str(e)
|
|
282
|
+
|
|
283
|
+
current_state_after = await self.graph_with_ck.aget_state(config, subgraphs=subgraphs)
|
|
284
|
+
current_interrupts_after = await self._get_interrupts(current_state_after, subgraphs)
|
|
285
|
+
if current_interrupts_after:
|
|
286
|
+
yield {
|
|
287
|
+
"messages": [AIMessage(content=any_content_to_langgraph([{"hitl_id": interrupt.id, "content": interrupt.value} for interrupt in current_interrupts_after]))],
|
|
288
|
+
"graph_status": TASK_STATE_INPUT_REQUIRED,
|
|
289
|
+
}
|
|
290
|
+
else:
|
|
291
|
+
messages_raw = self._normalize_messages(current_state_after.values.get("messages"))
|
|
292
|
+
if graph_recursion_error:
|
|
293
|
+
graph_status = TASK_STATE_FAILED
|
|
294
|
+
messages_raw = messages_raw + [AIMessage(content=graph_recursion_error)]
|
|
295
|
+
elif graph_drained:
|
|
296
|
+
graph_status = TASK_STATE_CANCELED
|
|
297
|
+
messages_raw = messages_raw + [AIMessage(content=graph_drained)]
|
|
298
|
+
else:
|
|
299
|
+
graph_status = TASK_STATE_COMPLETED
|
|
300
|
+
if len(messages_raw) > 0 and not isinstance(messages_raw[-1], HumanMessage) and messages_raw[-1].content:
|
|
301
|
+
messages = [messages_raw[-1]]
|
|
302
|
+
messages_history = messages_raw[0:-1]
|
|
303
|
+
else:
|
|
304
|
+
messages = []
|
|
305
|
+
messages_history = messages_raw
|
|
306
|
+
yield {
|
|
307
|
+
**current_state_after.values,
|
|
308
|
+
"graph_status": graph_status,
|
|
309
|
+
"messages": messages,
|
|
310
|
+
"messages_history": [langgraph_message_to_a2a(message, None, None) for message in messages_history],
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async def _update_task_store_status(self, task: Task, state: int, call_context: ServerCallContext) -> None:
|
|
314
|
+
ts = Timestamp()
|
|
315
|
+
ts.FromDatetime(datetime.now(timezone.utc))
|
|
316
|
+
task.status.state = state
|
|
317
|
+
task.status.timestamp.CopyFrom(ts)
|
|
318
|
+
await self.task_store.save(task, call_context)
|
|
319
|
+
|
|
320
|
+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
321
|
+
if not self.graph_with_ck:
|
|
322
|
+
raise InternalError(message=f"Can not found checkpointer of langgraph.")
|
|
323
|
+
if not context.message:
|
|
324
|
+
raise InvalidParamsError(message=f"Request message is required.")
|
|
325
|
+
|
|
326
|
+
task_id = context.current_task.id if context.current_task else context.message.task_id or str(uuid.uuid4())
|
|
327
|
+
context_id = context.current_task.context_id if context.current_task else context.message.context_id or str(uuid.uuid4())
|
|
328
|
+
updater = TaskUpdater(event_queue, task_id, context_id)
|
|
329
|
+
|
|
330
|
+
@self.lock.async_resource_lock(context_id)
|
|
331
|
+
async def start_task() -> tuple[bool, list[Task]]:
|
|
332
|
+
alive_tasks = await self.task_store.get_alive_tasks(context_id)
|
|
333
|
+
for alive_task in alive_tasks:
|
|
334
|
+
if alive_task.status.state == TASK_STATE_INPUT_REQUIRED:
|
|
335
|
+
if alive_task.id == task_id:
|
|
336
|
+
await updater.start_work()
|
|
337
|
+
history_tasks = await self.task_store.get_rejected_tasks(context_id)
|
|
338
|
+
return (True, history_tasks)
|
|
339
|
+
else:
|
|
340
|
+
return (False, [alive_task])
|
|
341
|
+
else:
|
|
342
|
+
return (False, [alive_task])
|
|
343
|
+
await updater.start_work()
|
|
344
|
+
history_tasks = await self.task_store.get_rejected_tasks(context_id)
|
|
345
|
+
return (True, history_tasks)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
exist_task = await self.task_store.get(task_id, context.call_context)
|
|
349
|
+
if not exist_task:
|
|
350
|
+
enqueue_task = Task(
|
|
351
|
+
id = task_id,
|
|
352
|
+
context_id = context_id,
|
|
353
|
+
status = TaskStatus(state = TASK_STATE_SUBMITTED, message = context.message),
|
|
354
|
+
)
|
|
355
|
+
await event_queue.enqueue_event(enqueue_task)
|
|
356
|
+
|
|
357
|
+
started = False
|
|
358
|
+
history_tasks = []
|
|
359
|
+
for _ in range(60):
|
|
360
|
+
started, history_tasks = await start_task()
|
|
361
|
+
if started:
|
|
362
|
+
break
|
|
363
|
+
await asyncio.sleep(10)
|
|
364
|
+
if not started:
|
|
365
|
+
await updater.update_status(
|
|
366
|
+
TASK_STATE_REJECTED,
|
|
367
|
+
new_text_message(f"Context {context_id} is working in task {history_tasks[0].id}, place this task to queue."),
|
|
368
|
+
)
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
history_tasks = sorted(history_tasks, key = lambda t: t.status.timestamp.ToDatetime())
|
|
372
|
+
history = []
|
|
373
|
+
history_task_ids = []
|
|
374
|
+
for history_task in history_tasks:
|
|
375
|
+
if history_task.history:
|
|
376
|
+
history.append(history_task.history[0])
|
|
377
|
+
history_task_ids.append(history_task.id)
|
|
378
|
+
|
|
379
|
+
await self.stream_graph(context.message, history, context_id, task_id, context.metadata or {}, updater)
|
|
380
|
+
for history_task_id in history_task_ids:
|
|
381
|
+
task = await self.task_store.get(history_task_id, context.call_context)
|
|
382
|
+
if task:
|
|
383
|
+
await self._update_task_store_status(task, TASK_STATE_COMPLETED, context.call_context)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
if not isinstance(e, ValueError):
|
|
386
|
+
logger.error(f"An error occurred while streaming the response: {e}", exc_info=True)
|
|
387
|
+
raise InternalError(message=str(e))
|
|
388
|
+
except asyncio.CancelledError:
|
|
389
|
+
await updater.cancel()
|
|
390
|
+
raise
|
|
391
|
+
finally:
|
|
392
|
+
await asyncio.sleep(0)
|
|
393
|
+
|
|
394
|
+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
395
|
+
task_id = context.current_task.id if context.current_task else context.message.task_id or None
|
|
396
|
+
if not task_id:
|
|
397
|
+
raise InvalidParamsError(message="Must assign task_id for canceling task.")
|
|
398
|
+
context_id = context.current_task.context_id if context.current_task else context.message.context_id or None
|
|
399
|
+
if not context_id:
|
|
400
|
+
raise InvalidParamsError(message=f"Can not match context_id for canceling task {task_id}.")
|
|
401
|
+
exist_task = await self.task_store.get(task_id, context.call_context)
|
|
402
|
+
if exist_task and exist_task.status.state == TASK_STATE_INPUT_REQUIRED:
|
|
403
|
+
config = {
|
|
404
|
+
"configurable": {
|
|
405
|
+
"thread_id": context_id,
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
try:
|
|
409
|
+
await self.graph_with_ck.aupdate_state(config, None, as_node=END)
|
|
410
|
+
await self._update_task_store_status(exist_task, TASK_STATE_CANCELED, context.call_context)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
raise InternalError(message=f"Cancel task {task_id} failed. {e}")
|
|
413
|
+
if task_id in self.run_controls:
|
|
414
|
+
self.run_controls[task_id].request_drain()
|
|
415
|
+
for _ in range(200):
|
|
416
|
+
if task_id not in self.run_controls:
|
|
417
|
+
break
|
|
418
|
+
await asyncio.sleep(2)
|
|
419
|
+
self.run_controls.pop(task_id, None)
|
|
420
|
+
|
|
421
|
+
|