zizkadb-langgraph 0.1.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.
- zizkadb_langgraph-0.1.0/.gitignore +67 -0
- zizkadb_langgraph-0.1.0/PKG-INFO +34 -0
- zizkadb_langgraph-0.1.0/README.md +23 -0
- zizkadb_langgraph-0.1.0/pyproject.toml +23 -0
- zizkadb_langgraph-0.1.0/tests/test_middleware.py +41 -0
- zizkadb_langgraph-0.1.0/zizkadb_langgraph/__init__.py +5 -0
- zizkadb_langgraph-0.1.0/zizkadb_langgraph/middleware.py +78 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Environment
|
|
2
|
+
.env
|
|
3
|
+
*.env
|
|
4
|
+
.env.local
|
|
5
|
+
*.env.local
|
|
6
|
+
.env.staging
|
|
7
|
+
infra/.env.staging
|
|
8
|
+
|
|
9
|
+
# Python
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[cod]
|
|
12
|
+
*.pyo
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
dist/
|
|
18
|
+
build/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
|
|
21
|
+
# Node
|
|
22
|
+
node_modules/
|
|
23
|
+
.next/
|
|
24
|
+
.turbo/
|
|
25
|
+
*.tsbuildinfo
|
|
26
|
+
*.log
|
|
27
|
+
|
|
28
|
+
# macOS
|
|
29
|
+
.DS_Store
|
|
30
|
+
# iCloud duplicate filenames (e.g. "file 2.py")
|
|
31
|
+
* 2.*
|
|
32
|
+
* 2/
|
|
33
|
+
|
|
34
|
+
# Docker volumes
|
|
35
|
+
postgres_data/
|
|
36
|
+
qdrant_data/
|
|
37
|
+
redis_data/
|
|
38
|
+
|
|
39
|
+
# IDE
|
|
40
|
+
.idea/
|
|
41
|
+
.vscode/
|
|
42
|
+
.cursor/mcp.json
|
|
43
|
+
.mcp.json
|
|
44
|
+
*.swp
|
|
45
|
+
|
|
46
|
+
# Postgres backups (created by infra/backup-postgres.sh)
|
|
47
|
+
infra/backups/
|
|
48
|
+
|
|
49
|
+
# Local native stack (Qdrant binary, storage)
|
|
50
|
+
.local/
|
|
51
|
+
|
|
52
|
+
# Qdrant data when started from repo root by mistake
|
|
53
|
+
storage/
|
|
54
|
+
.qdrant-initialized
|
|
55
|
+
|
|
56
|
+
# Extra local venvs / caches
|
|
57
|
+
.venv-phase0/
|
|
58
|
+
.venv-ci/
|
|
59
|
+
.ruff_cache/
|
|
60
|
+
core/.venv/
|
|
61
|
+
|
|
62
|
+
# Profiling artifacts
|
|
63
|
+
*.profraw
|
|
64
|
+
default.profraw
|
|
65
|
+
|
|
66
|
+
# Agents
|
|
67
|
+
.agents/
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zizkadb-langgraph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangGraph middleware for ZizkaDB — automatic causal logging per graph node
|
|
5
|
+
License: AGPL-3.0-or-later
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: zizkadb-sdk>=0.3.0
|
|
8
|
+
Provides-Extra: langgraph
|
|
9
|
+
Requires-Dist: langgraph>=0.2.0; extra == 'langgraph'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# zizkadb-langgraph
|
|
13
|
+
|
|
14
|
+
LangGraph middleware for [ZizkaDB](https://db.zizka.ai) — logs `graph_node` / `graph_node_done` with automatic `parent_id` via graph state.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install zizkadb-langgraph
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from zizkadb import ZizkaDB
|
|
22
|
+
from zizkadb_langgraph import ZizkaDBLangGraphMiddleware, wrap_node
|
|
23
|
+
|
|
24
|
+
async with ZizkaDB(host="http://localhost:8000") as db:
|
|
25
|
+
async with db.track(agent="research-bot") as ctx:
|
|
26
|
+
mw = ZizkaDBLangGraphMiddleware(db, agent="research-bot", session_id=ctx.session_id)
|
|
27
|
+
|
|
28
|
+
async def retrieve(state):
|
|
29
|
+
return {"docs": ["a", "b"]}
|
|
30
|
+
|
|
31
|
+
graph.add_node("retrieve", wrap_node(mw, "retrieve", retrieve))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See `examples/golden-path/langgraph-rag-bot/` for the full RAG support-bot reference.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# zizkadb-langgraph
|
|
2
|
+
|
|
3
|
+
LangGraph middleware for [ZizkaDB](https://db.zizka.ai) — logs `graph_node` / `graph_node_done` with automatic `parent_id` via graph state.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install zizkadb-langgraph
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from zizkadb import ZizkaDB
|
|
11
|
+
from zizkadb_langgraph import ZizkaDBLangGraphMiddleware, wrap_node
|
|
12
|
+
|
|
13
|
+
async with ZizkaDB(host="http://localhost:8000") as db:
|
|
14
|
+
async with db.track(agent="research-bot") as ctx:
|
|
15
|
+
mw = ZizkaDBLangGraphMiddleware(db, agent="research-bot", session_id=ctx.session_id)
|
|
16
|
+
|
|
17
|
+
async def retrieve(state):
|
|
18
|
+
return {"docs": ["a", "b"]}
|
|
19
|
+
|
|
20
|
+
graph.add_node("retrieve", wrap_node(mw, "retrieve", retrieve))
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
See `examples/golden-path/langgraph-rag-bot/` for the full RAG support-bot reference.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zizkadb-langgraph"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "LangGraph middleware for ZizkaDB — automatic causal logging per graph node"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "AGPL-3.0-or-later" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"zizkadb-sdk>=0.3.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
langgraph = ["langgraph>=0.2.0"]
|
|
14
|
+
|
|
15
|
+
[project.entry-points."zizkadb.integrations"]
|
|
16
|
+
langgraph = "zizkadb_langgraph.middleware"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["zizkadb_langgraph"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""LangGraph middleware keeps graph state lineage in sync."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from zizkadb_langgraph.middleware import STATE_LAST_EVENT_KEY, ZizkaDBLangGraphMiddleware, wrap_node
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_db() -> MagicMock:
|
|
13
|
+
counter = iter(range(1, 100))
|
|
14
|
+
db = MagicMock()
|
|
15
|
+
|
|
16
|
+
async def _log(**kwargs):
|
|
17
|
+
result = MagicMock()
|
|
18
|
+
result.event_id = f"evt-{next(counter)}"
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
db.log = AsyncMock(side_effect=_log)
|
|
22
|
+
return db
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_wrap_node_updates_caller_state_after_done():
|
|
27
|
+
db = _make_db()
|
|
28
|
+
mw = ZizkaDBLangGraphMiddleware(db, agent="bot", session_id="sess-1")
|
|
29
|
+
state = {STATE_LAST_EVENT_KEY: "evt-root"}
|
|
30
|
+
|
|
31
|
+
async def node(state: dict) -> dict:
|
|
32
|
+
return {"docs": [1]}
|
|
33
|
+
|
|
34
|
+
wrapped = wrap_node(mw, "retrieve", node)
|
|
35
|
+
await wrapped(state)
|
|
36
|
+
|
|
37
|
+
assert state[STATE_LAST_EVENT_KEY] == "evt-2"
|
|
38
|
+
calls = db.log.call_args_list
|
|
39
|
+
assert len(calls) == 2
|
|
40
|
+
assert calls[0].kwargs["parent_id"] == "evt-root"
|
|
41
|
+
assert calls[1].kwargs["parent_id"] == "evt-1"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""LangGraph node wrapper — logs graph_node events with automatic parent_id."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Awaitable
|
|
6
|
+
|
|
7
|
+
from zizkadb import ZizkaDB
|
|
8
|
+
|
|
9
|
+
STATE_LAST_EVENT_KEY = "zizkadb_last_event_id"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ZizkaDBLangGraphMiddleware:
|
|
13
|
+
"""Attach to a LangGraph state dict key for cross-node lineage."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, db: ZizkaDB, agent: str, session_id: str | None = None) -> None:
|
|
16
|
+
self.db = db
|
|
17
|
+
self.agent = agent
|
|
18
|
+
self.session_id = session_id
|
|
19
|
+
|
|
20
|
+
async def log_node(
|
|
21
|
+
self,
|
|
22
|
+
state: dict[str, Any],
|
|
23
|
+
node_name: str,
|
|
24
|
+
*,
|
|
25
|
+
input_summary: dict[str, Any] | None = None,
|
|
26
|
+
output_summary: dict[str, Any] | None = None,
|
|
27
|
+
phase: str = "enter",
|
|
28
|
+
) -> None:
|
|
29
|
+
parent_id = state.get(STATE_LAST_EVENT_KEY)
|
|
30
|
+
event = "graph_node" if phase == "enter" else "graph_node_done"
|
|
31
|
+
data: dict[str, Any] = {"node": node_name, "phase": phase}
|
|
32
|
+
if input_summary:
|
|
33
|
+
data["input"] = input_summary
|
|
34
|
+
if output_summary:
|
|
35
|
+
data["output"] = output_summary
|
|
36
|
+
result = await self.db.log(
|
|
37
|
+
agent=self.agent,
|
|
38
|
+
event=event,
|
|
39
|
+
data=data,
|
|
40
|
+
parent_id=parent_id,
|
|
41
|
+
session_id=self.session_id or state.get("session_id"),
|
|
42
|
+
)
|
|
43
|
+
state[STATE_LAST_EVENT_KEY] = result.event_id
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def wrap_node(
|
|
47
|
+
middleware: ZizkaDBLangGraphMiddleware,
|
|
48
|
+
node_name: str,
|
|
49
|
+
fn: Callable[..., Awaitable[dict[str, Any]]],
|
|
50
|
+
) -> Callable[..., Awaitable[dict[str, Any]]]:
|
|
51
|
+
"""Wrap an async LangGraph node to log enter/exit with causal links."""
|
|
52
|
+
|
|
53
|
+
async def wrapped(state: dict[str, Any], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
54
|
+
await middleware.log_node(
|
|
55
|
+
state,
|
|
56
|
+
node_name,
|
|
57
|
+
input_summary={"keys": list(state.keys())[:20]},
|
|
58
|
+
phase="enter",
|
|
59
|
+
)
|
|
60
|
+
out = await fn(state, *args, **kwargs)
|
|
61
|
+
if isinstance(out, dict):
|
|
62
|
+
merged = {**state, **out}
|
|
63
|
+
else:
|
|
64
|
+
merged = dict(state)
|
|
65
|
+
await middleware.log_node(
|
|
66
|
+
merged,
|
|
67
|
+
node_name,
|
|
68
|
+
output_summary={"keys": list(merged.keys())[:20]},
|
|
69
|
+
phase="done",
|
|
70
|
+
)
|
|
71
|
+
# Propagate the done-phase event id back to the caller's state dict.
|
|
72
|
+
# log_node updates `merged` (a copy), so without this the next node reads
|
|
73
|
+
# a stale zizkadb_last_event_id and forks the causal chain.
|
|
74
|
+
state[STATE_LAST_EVENT_KEY] = merged[STATE_LAST_EVENT_KEY]
|
|
75
|
+
return out if isinstance(out, dict) else state
|
|
76
|
+
|
|
77
|
+
wrapped.__name__ = getattr(fn, "__name__", node_name)
|
|
78
|
+
return wrapped
|