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.
Files changed (37) hide show
  1. langgraph_agent_common-2.0.0/PKG-INFO +67 -0
  2. langgraph_agent_common-2.0.0/README.md +42 -0
  3. langgraph_agent_common-2.0.0/pyproject.toml +51 -0
  4. langgraph_agent_common-2.0.0/setup.cfg +4 -0
  5. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/__init__.py +0 -0
  6. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/__init__.py +0 -0
  7. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/agent_executor.py +421 -0
  8. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/conversion.py +167 -0
  9. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/adaptor/sanitize_messages_for_llm.py +34 -0
  10. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/fastapi_application.py +152 -0
  11. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/__init__.py +0 -0
  12. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/checkpointer.py +102 -0
  13. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/graph.py +49 -0
  14. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/store.py +64 -0
  15. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/langgraph/validator.py +42 -0
  16. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/openapi_schema.json +277 -0
  17. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/static/css/style.css +480 -0
  18. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/static/index.html +107 -0
  19. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/resources/static/js/app.js +896 -0
  20. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/server.py +149 -0
  21. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/tasks/__init__.py +0 -0
  22. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/tasks/push_notification_config_store.py +24 -0
  23. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/tasks/task_store.py +66 -0
  24. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/utils/__init__.py +0 -0
  25. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/utils/lock.py +31 -0
  26. langgraph_agent_common-2.0.0/src/langgraph_a2a_api/utils/types.py +3 -0
  27. langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/PKG-INFO +67 -0
  28. langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/SOURCES.txt +35 -0
  29. langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/dependency_links.txt +1 -0
  30. langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/requires.txt +16 -0
  31. langgraph_agent_common-2.0.0/src/langgraph_agent_common.egg-info/top_level.txt +2 -0
  32. langgraph_agent_common-2.0.0/src/langgraph_aux/__init__.py +0 -0
  33. langgraph_agent_common-2.0.0/src/langgraph_aux/callbacks/__init__.py +11 -0
  34. langgraph_agent_common-2.0.0/src/langgraph_aux/callbacks/talos_callback_handler.py +340 -0
  35. langgraph_agent_common-2.0.0/src/langgraph_aux/utils/__init__.py +0 -0
  36. langgraph_agent_common-2.0.0/src/langgraph_aux/utils/stream_output.py +13 -0
  37. 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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+