langgraph-stream-parser 0.4.0__tar.gz → 0.4.1__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_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/CHANGELOG.md +13 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/PKG-INFO +1 -1
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/pyproject.toml +1 -1
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/agui/__init__.py +54 -4
- langgraph_stream_parser-0.4.1/tests/test_agui_matrix.py +261 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/.github/workflows/ci.yml +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/.github/workflows/release.yml +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/.gitignore +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/LICENSE +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/README.md +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/assets/header.svg +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/docs/adr/0001-adopt-ag-ui-for-the-wire.md +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/examples/agent.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/examples/fastapi_websocket.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/examples/jupyter_example.ipynb +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/spec.md +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/base.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/cli.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/fastapi.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/jupyter.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/print.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/adapters/session.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/agui/__main__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/compat.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/demo/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/demo/agent.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/demo/stub.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/events.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/extractors/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/extractors/base.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/extractors/builtins.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/extractors/interrupts.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/extractors/messages.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/handlers/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/handlers/messages.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/handlers/updates.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/host/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/host/__main__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/host/config.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/host/loader.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/host/workspace.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/parser.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/src/langgraph_stream_parser/resume.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/fixtures/__init__.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/fixtures/mocks.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_agui.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_cli_adapter.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_compat.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_demo.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_demo_stub.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_dual_mode.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_events.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_extractors.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_fastapi_adapter.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_generic_extractor.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_host.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_host_config.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_jupyter.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_lc14_compat.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_parser.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_print_adapter.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_real_model.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_reasoning_display.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_resume.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_session_adapter.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_subagent.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_v2_stream.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_wire_contract.py +0 -0
- {langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/uv.lock +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.1] - 2026-06-14
|
|
4
|
+
|
|
5
|
+
AG-UI bridge robustness — an edge-case audit (`tests/test_agui_matrix.py`, run against purpose-built tool-calling / interrupting / erroring / checkpointer-less agents) surfaced three real issues, now fixed:
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Graphs compiled without a checkpointer no longer hard-crash.** The AG-UI adapter calls `graph.aget_state()`, which raises `No checkpointer set` — common for plain user graphs. `build_agent()` now auto-attaches an in-memory checkpointer when the graph lacks one (AG-UI needs threaded state for interrupts/resume regardless).
|
|
9
|
+
- **Agent exceptions mid-run now emit a terminal `RUN_ERROR`** instead of silently killing the stream / unhandled 500. The endpoint is now a resilient wrapper around the agent run.
|
|
10
|
+
|
|
11
|
+
### Verified (was previously only claimed)
|
|
12
|
+
- Tool calls map to `TOOL_CALL_START`/`ARGS`/`END` + `TOOL_CALL_RESULT`.
|
|
13
|
+
- Interrupts surface as a `CUSTOM` `on_interrupt` event; resume via `forwardedProps.command.resume` continues the run (HITL round-trip).
|
|
14
|
+
- Multi-turn thread state persists; concurrent requests are isolated (per-request agent clone). Note: AG-UI clients must use **unique message ids** per turn — the adapter dedupes by id.
|
|
15
|
+
|
|
3
16
|
## [0.4.0] - 2026-06-14
|
|
4
17
|
|
|
5
18
|
Adopt **AG-UI** for the wire (see `docs/adr/0001-adopt-ag-ui-for-the-wire.md`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-stream-parser
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Universal parser + host layer for LangGraph agents — typed stream events, agent-spec loading, layered config, and an AG-UI bridge
|
|
5
5
|
Project-URL: Homepage, https://github.com/dkedar7/langgraph-stream-parser
|
|
6
6
|
Project-URL: Documentation, https://github.com/dkedar7/langgraph-stream-parser#readme
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "langgraph-stream-parser"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "Universal parser + host layer for LangGraph agents — typed stream events, agent-spec loading, layered config, and an AG-UI bridge"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -24,8 +24,9 @@ Quick start::
|
|
|
24
24
|
from langgraph_stream_parser.agui import build_app
|
|
25
25
|
app = build_app(my_compiled_graph) # an ASGI app; run with uvicorn
|
|
26
26
|
"""
|
|
27
|
-
from __future__ import annotations
|
|
28
|
-
|
|
27
|
+
# NB: intentionally NOT `from __future__ import annotations`. The resilient
|
|
28
|
+
# endpoint below needs real (non-string) annotations so FastAPI can resolve
|
|
29
|
+
# RunAgentInput as the request body; PEP 604 unions work natively on >=3.11.
|
|
29
30
|
from typing import Any
|
|
30
31
|
|
|
31
32
|
__all__ = ["build_agent", "add_agui_endpoint", "build_app", "serve", "DEFAULT_AGENT_NAME"]
|
|
@@ -71,6 +72,17 @@ def build_agent(
|
|
|
71
72
|
from ag_ui_langgraph import LangGraphAgent
|
|
72
73
|
except ImportError as e: # pragma: no cover - exercised only without the extra
|
|
73
74
|
raise RuntimeError(_IMPORT_HINT) from e
|
|
75
|
+
# AG-UI requires threaded state — the adapter calls graph.aget_state() and
|
|
76
|
+
# supports interrupts/resume, both of which need a checkpointer. Many user
|
|
77
|
+
# graphs are compiled without one (and would otherwise hard-crash with
|
|
78
|
+
# "No checkpointer set"), so attach an in-memory default when absent.
|
|
79
|
+
if getattr(graph, "checkpointer", None) is None:
|
|
80
|
+
try:
|
|
81
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
82
|
+
|
|
83
|
+
graph.checkpointer = InMemorySaver()
|
|
84
|
+
except Exception: # pragma: no cover - best-effort; LangGraphAgent will surface real issues
|
|
85
|
+
pass
|
|
74
86
|
return LangGraphAgent(name=name, graph=graph, description=description, config=config)
|
|
75
87
|
|
|
76
88
|
|
|
@@ -87,15 +99,53 @@ def add_agui_endpoint(
|
|
|
87
99
|
|
|
88
100
|
``graph`` may be a compiled graph or an already-built ``LangGraphAgent``.
|
|
89
101
|
Returns the same ``app`` for chaining.
|
|
102
|
+
|
|
103
|
+
The endpoint is *resilient*: if the agent raises mid-run, a terminal
|
|
104
|
+
``RUN_ERROR`` event is emitted and the stream closes cleanly, rather than
|
|
105
|
+
crashing the connection with an unhandled 500 (the bare upstream adapter
|
|
106
|
+
lets node exceptions propagate). Each request runs on its own cloned agent.
|
|
90
107
|
"""
|
|
91
108
|
try:
|
|
92
|
-
from
|
|
109
|
+
from ag_ui.core import EventType, RunAgentInput, RunErrorEvent
|
|
110
|
+
from ag_ui.encoder import EventEncoder
|
|
111
|
+
from fastapi import Request
|
|
112
|
+
from fastapi.responses import StreamingResponse
|
|
93
113
|
except ImportError as e: # pragma: no cover
|
|
94
114
|
raise RuntimeError(_IMPORT_HINT) from e
|
|
115
|
+
|
|
95
116
|
agent = graph if _is_langgraph_agent(graph) else build_agent(
|
|
96
117
|
graph, name=name, description=description, config=config
|
|
97
118
|
)
|
|
98
|
-
|
|
119
|
+
|
|
120
|
+
@app.post(path)
|
|
121
|
+
async def _run(input_data: RunAgentInput, request: Request):
|
|
122
|
+
accept = request.headers.get("accept")
|
|
123
|
+
try:
|
|
124
|
+
encoder = EventEncoder(accept=accept)
|
|
125
|
+
except TypeError: # pragma: no cover - older SDKs without the accept kwarg
|
|
126
|
+
encoder = EventEncoder()
|
|
127
|
+
media_type = getattr(encoder, "get_content_type", lambda: "text/event-stream")()
|
|
128
|
+
run_agent = agent.clone()
|
|
129
|
+
|
|
130
|
+
async def gen():
|
|
131
|
+
try:
|
|
132
|
+
async for ev in run_agent.run(input_data):
|
|
133
|
+
# run() yields SSE-encoded strings; encode objects defensively.
|
|
134
|
+
yield ev if isinstance(ev, (str, bytes)) else encoder.encode(ev)
|
|
135
|
+
except Exception as exc: # noqa: BLE001 - surfaced to the client as RUN_ERROR
|
|
136
|
+
yield encoder.encode(
|
|
137
|
+
RunErrorEvent(
|
|
138
|
+
type=EventType.RUN_ERROR,
|
|
139
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return StreamingResponse(gen(), media_type=media_type)
|
|
144
|
+
|
|
145
|
+
@app.get(path)
|
|
146
|
+
async def _health():
|
|
147
|
+
return {"status": "ok", "agent": {"name": getattr(agent, "name", name)}}
|
|
148
|
+
|
|
99
149
|
return app
|
|
100
150
|
|
|
101
151
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""AG-UI edge-case matrix — exercises the bridge against purpose-built agents
|
|
2
|
+
that each stress one event class, asserting the *observed* AG-UI output.
|
|
3
|
+
|
|
4
|
+
This is the audit that converts the langgraph-stream-parser -> AG-UI mapping
|
|
5
|
+
from "claimed" to "proven", and it pins the three robustness fixes the audit
|
|
6
|
+
surfaced:
|
|
7
|
+
1. graphs compiled WITHOUT a checkpointer are auto-handled (AG-UI's
|
|
8
|
+
aget_state would otherwise hard-crash with "No checkpointer set");
|
|
9
|
+
2. an agent exception mid-run becomes a terminal RUN_ERROR event instead of
|
|
10
|
+
a silently-dying stream / unhandled 500;
|
|
11
|
+
3. interrupts surface as a CUSTOM `on_interrupt` event and resume via
|
|
12
|
+
forwardedProps.command.resume.
|
|
13
|
+
|
|
14
|
+
Every agent here is compiled WITHOUT a checkpointer on purpose, so the suite
|
|
15
|
+
also proves the auto-attach fix end to end.
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
19
|
+
from typing import Iterator, List
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
pytest.importorskip("ag_ui_langgraph", reason="needs the 'agui' extra")
|
|
24
|
+
pytest.importorskip("fastapi", reason="needs fastapi")
|
|
25
|
+
pytest.importorskip("langgraph", reason="needs langgraph")
|
|
26
|
+
|
|
27
|
+
from fastapi.testclient import TestClient # noqa: E402
|
|
28
|
+
from langchain_core.language_models import BaseChatModel # noqa: E402
|
|
29
|
+
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage # noqa: E402
|
|
30
|
+
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult # noqa: E402
|
|
31
|
+
from langchain_core.tools import tool # noqa: E402
|
|
32
|
+
from langgraph.graph import END, START, MessagesState, StateGraph # noqa: E402
|
|
33
|
+
from langgraph.prebuilt import ToolNode # noqa: E402
|
|
34
|
+
from langgraph.types import interrupt # noqa: E402
|
|
35
|
+
|
|
36
|
+
from langgraph_stream_parser.agui import build_app # noqa: E402
|
|
37
|
+
from langgraph_stream_parser.demo import create_stub_agent # noqa: E402
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── driver ───────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def _run_input(text, *, thread="t1", run="r1", resume=None):
|
|
43
|
+
# NB: each turn needs a UNIQUE message id — the AG-UI/LangGraph adapter
|
|
44
|
+
# dedupes messages by id, so reusing an id silently drops later turns.
|
|
45
|
+
body = {
|
|
46
|
+
"threadId": thread, "runId": run,
|
|
47
|
+
"messages": [{"id": f"msg-{thread}-{run}", "role": "user", "content": text}],
|
|
48
|
+
"tools": [], "context": [], "state": {}, "forwardedProps": {},
|
|
49
|
+
}
|
|
50
|
+
if resume is not None:
|
|
51
|
+
body["forwardedProps"] = {"command": {"resume": resume}}
|
|
52
|
+
return body
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _drive(app, body):
|
|
56
|
+
"""POST a run, return (status_code, [event dicts])."""
|
|
57
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
58
|
+
resp = client.post("/", json=body)
|
|
59
|
+
events = []
|
|
60
|
+
for line in resp.text.splitlines():
|
|
61
|
+
line = line.strip()
|
|
62
|
+
if line.startswith("data:"):
|
|
63
|
+
try:
|
|
64
|
+
events.append(json.loads(line[5:].strip()))
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
pass
|
|
67
|
+
return resp.status_code, events
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _types(events):
|
|
71
|
+
return [e.get("type") for e in events]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _text(events):
|
|
75
|
+
return "".join(e.get("delta", "") for e in events if e.get("type") == "TEXT_MESSAGE_CONTENT")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── purpose-built agents (all compiled WITHOUT a checkpointer) ─────────
|
|
79
|
+
|
|
80
|
+
@tool
|
|
81
|
+
def add(a: int, b: int) -> int:
|
|
82
|
+
"""Add two integers."""
|
|
83
|
+
return a + b
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _ToolThenAnswer(BaseChatModel):
|
|
87
|
+
@property
|
|
88
|
+
def _llm_type(self) -> str:
|
|
89
|
+
return "tool-matrix-stub"
|
|
90
|
+
|
|
91
|
+
def bind_tools(self, *a, **k):
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def _generate(self, messages, stop=None, run_manager=None, **kw):
|
|
95
|
+
return ChatResult(generations=[ChatGeneration(message=self._reply(messages))])
|
|
96
|
+
|
|
97
|
+
def _reply(self, messages: List[BaseMessage]) -> AIMessage:
|
|
98
|
+
if messages and getattr(messages[-1], "type", None) == "tool":
|
|
99
|
+
return AIMessage(content="the sum is 5")
|
|
100
|
+
return AIMessage(content="", tool_calls=[{"id": "call_1", "name": "add", "args": {"a": 2, "b": 3}}])
|
|
101
|
+
|
|
102
|
+
def _stream(self, messages, stop=None, run_manager=None, **kw) -> Iterator[ChatGenerationChunk]:
|
|
103
|
+
if messages and getattr(messages[-1], "type", None) == "tool":
|
|
104
|
+
for tok in ["the ", "sum ", "is ", "5"]:
|
|
105
|
+
ch = ChatGenerationChunk(message=AIMessageChunk(content=tok))
|
|
106
|
+
if run_manager:
|
|
107
|
+
run_manager.on_llm_new_token(tok, chunk=ch)
|
|
108
|
+
yield ch
|
|
109
|
+
else:
|
|
110
|
+
yield ChatGenerationChunk(message=AIMessageChunk(
|
|
111
|
+
content="",
|
|
112
|
+
tool_call_chunks=[{"name": "add", "args": '{"a": 2, "b": 3}', "id": "call_1", "index": 0}],
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _tool_agent():
|
|
117
|
+
model = _ToolThenAnswer()
|
|
118
|
+
|
|
119
|
+
def call_model(state):
|
|
120
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
121
|
+
|
|
122
|
+
def route(state):
|
|
123
|
+
return "tools" if getattr(state["messages"][-1], "tool_calls", None) else END
|
|
124
|
+
|
|
125
|
+
b = StateGraph(MessagesState)
|
|
126
|
+
b.add_node("model", call_model)
|
|
127
|
+
b.add_node("tools", ToolNode([add]))
|
|
128
|
+
b.add_edge(START, "model")
|
|
129
|
+
b.add_conditional_edges("model", route, {"tools": "tools", END: END})
|
|
130
|
+
b.add_edge("tools", "model")
|
|
131
|
+
return b.compile() # NO checkpointer — exercises the auto-attach fix
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _interrupt_agent():
|
|
135
|
+
def ask(state):
|
|
136
|
+
decision = interrupt({"question": "approve?", "tool": "deploy"})
|
|
137
|
+
return {"messages": [AIMessage(content=f"resumed: {decision}")]}
|
|
138
|
+
|
|
139
|
+
b = StateGraph(MessagesState)
|
|
140
|
+
b.add_node("ask", ask)
|
|
141
|
+
b.add_edge(START, "ask")
|
|
142
|
+
b.add_edge("ask", END)
|
|
143
|
+
return b.compile() # NO checkpointer
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _error_agent():
|
|
147
|
+
def boom(state):
|
|
148
|
+
raise ValueError("intentional boom")
|
|
149
|
+
|
|
150
|
+
b = StateGraph(MessagesState)
|
|
151
|
+
b.add_node("boom", boom)
|
|
152
|
+
b.add_edge(START, "boom")
|
|
153
|
+
b.add_edge("boom", END)
|
|
154
|
+
return b.compile() # NO checkpointer
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _plain_echo_no_checkpointer():
|
|
158
|
+
"""An echo graph with NO checkpointer (create_stub_agent ships one)."""
|
|
159
|
+
from langchain_core.messages import AIMessage as _AI
|
|
160
|
+
|
|
161
|
+
def respond(state):
|
|
162
|
+
last = next((m.content for m in reversed(state["messages"])
|
|
163
|
+
if getattr(m, "type", None) == "human"), "")
|
|
164
|
+
return {"messages": [_AI(content=f"echo: {last}")]}
|
|
165
|
+
|
|
166
|
+
b = StateGraph(MessagesState)
|
|
167
|
+
b.add_node("respond", respond)
|
|
168
|
+
b.add_edge(START, "respond")
|
|
169
|
+
b.add_edge("respond", END)
|
|
170
|
+
return b.compile()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── tests ────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
def test_tool_calls_map_to_agui():
|
|
176
|
+
status, events = _drive(build_app(_tool_agent()), _run_input("add 2 and 3"))
|
|
177
|
+
assert status == 200
|
|
178
|
+
t = _types(events)
|
|
179
|
+
assert "TOOL_CALL_START" in t
|
|
180
|
+
assert "TOOL_CALL_RESULT" in t
|
|
181
|
+
start = next(e for e in events if e["type"] == "TOOL_CALL_START")
|
|
182
|
+
result = next(e for e in events if e["type"] == "TOOL_CALL_RESULT")
|
|
183
|
+
assert start.get("toolCallName") == "add"
|
|
184
|
+
assert result.get("content") == "5"
|
|
185
|
+
assert "the sum is 5" in _text(events)
|
|
186
|
+
assert "RUN_FINISHED" in t and "RUN_ERROR" not in t
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_interrupt_is_signaled_and_resumes():
|
|
190
|
+
app = build_app(_interrupt_agent())
|
|
191
|
+
|
|
192
|
+
status, first = _drive(app, _run_input("go", thread="ti", run="r1"))
|
|
193
|
+
assert status == 200
|
|
194
|
+
customs = [e for e in first if e.get("type") == "CUSTOM" and e.get("name") == "on_interrupt"]
|
|
195
|
+
assert customs, f"interrupt not signaled as CUSTOM/on_interrupt: {_types(first)}"
|
|
196
|
+
# the interrupt payload travels to the client
|
|
197
|
+
assert "approve?" in json.dumps(customs[0])
|
|
198
|
+
assert "RUN_ERROR" not in _types(first)
|
|
199
|
+
|
|
200
|
+
# resume the SAME thread with a decision via forwardedProps.command.resume
|
|
201
|
+
status, second = _drive(app, _run_input("", thread="ti", run="r2", resume="approved"))
|
|
202
|
+
assert status == 200
|
|
203
|
+
assert "RUN_FINISHED" in _types(second)
|
|
204
|
+
assert "RUN_ERROR" not in _types(second)
|
|
205
|
+
# the resumed value reached the agent (it echoed "resumed: approved" into state)
|
|
206
|
+
assert "resumed: approved" in json.dumps(second)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_agent_error_becomes_run_error():
|
|
210
|
+
"""The resilient endpoint emits a terminal RUN_ERROR instead of a
|
|
211
|
+
silently-dying stream / unhandled 500."""
|
|
212
|
+
status, events = _drive(build_app(_error_agent()), _run_input("crash"))
|
|
213
|
+
assert status == 200
|
|
214
|
+
errs = [e for e in events if e.get("type") == "RUN_ERROR"]
|
|
215
|
+
assert errs, f"expected a RUN_ERROR event, got: {_types(events)}"
|
|
216
|
+
assert "intentional boom" in errs[0].get("message", "")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_graph_without_checkpointer_is_handled():
|
|
220
|
+
"""AG-UI's aget_state needs a checkpointer; the bridge auto-attaches one
|
|
221
|
+
so a plain compiled graph doesn't hard-crash."""
|
|
222
|
+
status, events = _drive(build_app(_plain_echo_no_checkpointer()), _run_input("ping"))
|
|
223
|
+
assert status == 200
|
|
224
|
+
# The fix's job: no hard-crash, run completes. (This node appends an
|
|
225
|
+
# AIMessage directly rather than streaming a model, so its content lands in
|
|
226
|
+
# the state/messages snapshot, not as TEXT_MESSAGE_CONTENT deltas.)
|
|
227
|
+
assert "RUN_ERROR" not in _types(events)
|
|
228
|
+
assert "RUN_FINISHED" in _types(events)
|
|
229
|
+
assert "echo: ping" in json.dumps(events)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_multi_turn_thread_persists():
|
|
233
|
+
app = build_app(create_stub_agent(reply_prefix="echo: "))
|
|
234
|
+
s1, e1 = _drive(app, _run_input("first", thread="conv", run="r1"))
|
|
235
|
+
s2, e2 = _drive(app, _run_input("second", thread="conv", run="r2"))
|
|
236
|
+
assert s1 == 200 and s2 == 200
|
|
237
|
+
assert "first" in _text(e1)
|
|
238
|
+
assert "second" in _text(e2)
|
|
239
|
+
# state persisted across turns: the second run carries BOTH turns
|
|
240
|
+
blob = json.dumps(e2)
|
|
241
|
+
assert "first" in blob and "second" in blob
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_concurrent_requests_are_isolated():
|
|
245
|
+
"""The adapter clones per request; distinct threads must not cross-talk."""
|
|
246
|
+
app = build_app(create_stub_agent(reply_prefix="echo: "))
|
|
247
|
+
inputs = [f"msg-{i}" for i in range(8)]
|
|
248
|
+
|
|
249
|
+
def one(i):
|
|
250
|
+
_, events = _drive(app, _run_input(inputs[i], thread=f"th-{i}", run=f"r-{i}"))
|
|
251
|
+
return inputs[i], _text(events)
|
|
252
|
+
|
|
253
|
+
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
254
|
+
results = list(ex.map(one, range(8)))
|
|
255
|
+
|
|
256
|
+
for sent, got in results:
|
|
257
|
+
assert sent in got, f"{sent!r} not echoed in {got!r}"
|
|
258
|
+
# no other request's payload leaked into this response
|
|
259
|
+
for other in inputs:
|
|
260
|
+
if other != sent:
|
|
261
|
+
assert other not in got, f"cross-talk: {other!r} leaked into {got!r}"
|
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/examples/fastapi_websocket.py
RENAMED
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/examples/jupyter_example.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_fastapi_adapter.py
RENAMED
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_generic_extractor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_reasoning_display.py
RENAMED
|
File without changes
|
|
File without changes
|
{langgraph_stream_parser-0.4.0 → langgraph_stream_parser-0.4.1}/tests/test_session_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|