databricks-agentbricks 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""Conversation session store for the agent.
|
|
2
|
+
|
|
3
|
+
LangGraph persists conversation state through a **checkpointer** keyed by a ``thread_id`` (passed in
|
|
4
|
+
the run config), not through a session object. ``checkpointer()`` returns the checkpointer the agent
|
|
5
|
+
is built with, and ``thread_config(session_id)`` maps a session id onto that thread.
|
|
6
|
+
|
|
7
|
+
Default (no config): an in-memory checkpointer (``InMemorySaver``) — multi-turn history is preserved
|
|
8
|
+
within a single running process, no database. It does NOT survive restarts or span replicas.
|
|
9
|
+
|
|
10
|
+
Durable (``AGENT_SESSION_STORE`` set): a ``DatabricksSessionStoreSaver``. Instead of a database the
|
|
11
|
+
app connects to directly, it serializes each LangGraph checkpoint into ordered **session items** and
|
|
12
|
+
stores them through the managed Session Store REST API. Full graph state — including human-in-the-loop
|
|
13
|
+
pauses (pending writes + interrupts) — is durable across restarts and replicas, over RPCs only (no
|
|
14
|
+
Lakebase/Postgres connection). Setting the env var is the only change; the agent code is identical.
|
|
15
|
+
|
|
16
|
+
The saver is adapted from the first-party ``databricks_agent_client.langgraph`` prototype, over a
|
|
17
|
+
vendored REST client (``session_store_client.py``) so the template needs no unpublished dependency;
|
|
18
|
+
swap both for the published package when it lands.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import builtins
|
|
25
|
+
import hashlib
|
|
26
|
+
import random
|
|
27
|
+
from collections import defaultdict
|
|
28
|
+
from typing import Any, Iterator, Mapping, Optional, Sequence
|
|
29
|
+
|
|
30
|
+
from langchain_core.runnables import RunnableConfig
|
|
31
|
+
from langgraph.checkpoint.base import (
|
|
32
|
+
BaseCheckpointSaver,
|
|
33
|
+
ChannelVersions,
|
|
34
|
+
Checkpoint,
|
|
35
|
+
CheckpointMetadata,
|
|
36
|
+
CheckpointTuple,
|
|
37
|
+
SerializerProtocol,
|
|
38
|
+
get_checkpoint_id,
|
|
39
|
+
get_checkpoint_metadata,
|
|
40
|
+
)
|
|
41
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
42
|
+
|
|
43
|
+
from databricks_agentkit.runtime.session_store_client import Session, SessionStoreClient
|
|
44
|
+
|
|
45
|
+
# Discriminators stored inside each session item's `data` JSON.
|
|
46
|
+
_EVENT_CHECKPOINT = "checkpoint"
|
|
47
|
+
_EVENT_CHANNEL_DATA = "channel_data"
|
|
48
|
+
_EVENT_WRITES = "writes"
|
|
49
|
+
|
|
50
|
+
# Sentinel for a channel that has a version bump but no materialized value yet.
|
|
51
|
+
_EMPTY_CHANNEL_VALUE = "__databricks_empty_channel__"
|
|
52
|
+
|
|
53
|
+
# Fetch items oldest-first so replaying them reconstructs state in write order.
|
|
54
|
+
_ORDER_BY = "create_time asc"
|
|
55
|
+
|
|
56
|
+
# Checkpoint metadata kept by generated agents.
|
|
57
|
+
INVOCATION_METADATA_KEY = "databricks_agentkit.invocation_id"
|
|
58
|
+
|
|
59
|
+
# One saver per process, opened lazily on first use and shared thereafter — that's what makes
|
|
60
|
+
# multi-turn work in-process (and, for the durable saver, reuses one client/session cache).
|
|
61
|
+
_saver: BaseCheckpointSaver | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def invocation_metadata(invocation_id: str) -> dict[str, str]:
|
|
65
|
+
"""Return checkpoint metadata for the current AgentKit runtime."""
|
|
66
|
+
return {INVOCATION_METADATA_KEY: invocation_id}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def invocation_id_from_metadata(metadata: Mapping[str, Any] | None) -> str | None:
|
|
70
|
+
"""Read a checkpoint invocation id from AgentKit metadata."""
|
|
71
|
+
if not metadata:
|
|
72
|
+
return None
|
|
73
|
+
value = metadata.get(INVOCATION_METADATA_KEY)
|
|
74
|
+
return value if isinstance(value, str) else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def checkpointer(store: str | None = None) -> BaseCheckpointSaver:
|
|
78
|
+
"""The checkpointer the agent persists conversation state to (built once, then shared).
|
|
79
|
+
|
|
80
|
+
In-memory by default; a durable ``DatabricksSessionStoreSaver`` when a managed store is
|
|
81
|
+
configured. The store resolves ``store`` arg → ``AGENT_SESSION_STORE`` env → the
|
|
82
|
+
``[session_store]`` binding in agent.toml (`ab sessions bind`) → none (in-memory).
|
|
83
|
+
"""
|
|
84
|
+
from databricks_agentkit.runtime.tool_manifest import resolve_session_store
|
|
85
|
+
|
|
86
|
+
global _saver
|
|
87
|
+
if _saver is None:
|
|
88
|
+
store = resolve_session_store(store)
|
|
89
|
+
_saver = DatabricksSessionStoreSaver(store) if store else InMemorySaver()
|
|
90
|
+
return _saver
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def thread_config(session_id: str, actor: str | None = None) -> dict:
|
|
94
|
+
"""Run config that anchors this request to ``session_id``'s conversation thread.
|
|
95
|
+
|
|
96
|
+
Includes ``actor_id`` because the durable saver maps it onto the Session's actor; it's ignored by
|
|
97
|
+
the in-memory default. ``actor`` partitions the durable store — the caller supplies it (typically
|
|
98
|
+
the signed-in user), so each user's threads stay separate; it defaults to ``session_id`` (one
|
|
99
|
+
actor per conversation).
|
|
100
|
+
"""
|
|
101
|
+
return {"configurable": {"thread_id": session_id, "actor_id": actor or session_id}}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DatabricksSessionStoreSaver(BaseCheckpointSaver[str]):
|
|
105
|
+
"""LangGraph ``BaseCheckpointSaver`` over the Databricks Session Store REST API.
|
|
106
|
+
|
|
107
|
+
A single ``put`` fans out into several session items so large, immutable channel values are stored
|
|
108
|
+
once per version rather than copied into every checkpoint: one ``checkpoint`` item (the checkpoint
|
|
109
|
+
dict minus ``channel_values`` + metadata), one ``channel_data`` item per changed channel, and
|
|
110
|
+
``writes`` items (via ``put_writes``). Each item's ``data`` is plain JSON with an ``event_type``
|
|
111
|
+
discriminator, because the Session Store treats item ``data`` as opaque JSON. Both ``thread_id``
|
|
112
|
+
and ``actor_id`` are required in the run config's ``configurable`` section.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
session_store_name: str,
|
|
118
|
+
*,
|
|
119
|
+
client: Optional[SessionStoreClient] = None,
|
|
120
|
+
workspace_client: Optional[Any] = None,
|
|
121
|
+
serde: Optional[SerializerProtocol] = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
super().__init__(serde=serde)
|
|
124
|
+
if not session_store_name:
|
|
125
|
+
raise ValueError("session_store_name is required")
|
|
126
|
+
self._client = (client or SessionStoreClient(workspace_client)).set_session_store(
|
|
127
|
+
session_store_name
|
|
128
|
+
)
|
|
129
|
+
# Cache resolved Session objects per derived session_id to avoid re-resolving every put/get.
|
|
130
|
+
self._sessions: dict[str, Session] = {}
|
|
131
|
+
|
|
132
|
+
# ----- write path ---------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def put(
|
|
135
|
+
self,
|
|
136
|
+
config: RunnableConfig,
|
|
137
|
+
checkpoint: Checkpoint,
|
|
138
|
+
metadata: CheckpointMetadata,
|
|
139
|
+
new_versions: ChannelVersions,
|
|
140
|
+
) -> RunnableConfig:
|
|
141
|
+
"""Persist a checkpoint and its changed channel values."""
|
|
142
|
+
thread_id, actor_id, checkpoint_ns = _parse_config(config)
|
|
143
|
+
session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
|
|
144
|
+
|
|
145
|
+
checkpoint_copy = dict(checkpoint)
|
|
146
|
+
channel_values = checkpoint_copy.pop("channel_values", {}) or {}
|
|
147
|
+
|
|
148
|
+
items: list[dict[str, Any]] = [
|
|
149
|
+
self._channel_data_item(
|
|
150
|
+
channel, str(version), channel_values.get(channel, _EMPTY_CHANNEL_VALUE)
|
|
151
|
+
)
|
|
152
|
+
for channel, version in new_versions.items()
|
|
153
|
+
]
|
|
154
|
+
items.append(
|
|
155
|
+
self._checkpoint_item(
|
|
156
|
+
checkpoint_id=checkpoint["id"],
|
|
157
|
+
checkpoint_data=checkpoint_copy,
|
|
158
|
+
metadata=dict(get_checkpoint_metadata(config, metadata)), # type: ignore[arg-type]
|
|
159
|
+
parent_checkpoint_id=get_checkpoint_id(config),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
self._client.append_items(session, items=items)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"configurable": {
|
|
166
|
+
"thread_id": thread_id,
|
|
167
|
+
"actor_id": actor_id,
|
|
168
|
+
"checkpoint_ns": checkpoint_ns,
|
|
169
|
+
"checkpoint_id": checkpoint["id"],
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
def put_writes(
|
|
174
|
+
self,
|
|
175
|
+
config: RunnableConfig,
|
|
176
|
+
writes: Sequence[tuple[str, Any]],
|
|
177
|
+
task_id: str,
|
|
178
|
+
task_path: str = "",
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Persist pending intermediate writes for a checkpoint."""
|
|
181
|
+
thread_id, actor_id, checkpoint_ns = _parse_config(config)
|
|
182
|
+
checkpoint_id = get_checkpoint_id(config)
|
|
183
|
+
if not checkpoint_id:
|
|
184
|
+
raise ValueError("checkpoint_id is required in config for put_writes")
|
|
185
|
+
|
|
186
|
+
session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
|
|
187
|
+
item = {
|
|
188
|
+
"event_type": _EVENT_WRITES,
|
|
189
|
+
"checkpoint_id": checkpoint_id,
|
|
190
|
+
"writes": [
|
|
191
|
+
{
|
|
192
|
+
"task_id": task_id,
|
|
193
|
+
"task_path": task_path,
|
|
194
|
+
"channel": channel,
|
|
195
|
+
"value": self._serialize(value),
|
|
196
|
+
}
|
|
197
|
+
for channel, value in writes
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
self._client.append_items(session, items=[item])
|
|
201
|
+
|
|
202
|
+
def delete_thread(self, thread_id: str, actor_id: str = "") -> None:
|
|
203
|
+
"""Remove all checkpoints and writes for a thread (via ClearSessionItems)."""
|
|
204
|
+
session = self._resolve_session(thread_id, actor_id, "")
|
|
205
|
+
self._client.clear_items(session)
|
|
206
|
+
|
|
207
|
+
# ----- read path ----------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
210
|
+
"""Return the checkpoint named in ``config``, or the latest one."""
|
|
211
|
+
thread_id, actor_id, checkpoint_ns = _parse_config(config)
|
|
212
|
+
session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
|
|
213
|
+
checkpoints, writes_by_ckpt, channel_data = self._read(session)
|
|
214
|
+
if not checkpoints:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
wanted = get_checkpoint_id(config)
|
|
218
|
+
if wanted:
|
|
219
|
+
event = checkpoints.get(wanted)
|
|
220
|
+
if event is None:
|
|
221
|
+
return None
|
|
222
|
+
else:
|
|
223
|
+
event = checkpoints[max(checkpoints)]
|
|
224
|
+
|
|
225
|
+
return self._build_tuple(
|
|
226
|
+
event,
|
|
227
|
+
writes_by_ckpt.get(event["checkpoint_id"], []),
|
|
228
|
+
channel_data,
|
|
229
|
+
thread_id,
|
|
230
|
+
actor_id,
|
|
231
|
+
checkpoint_ns,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def list(
|
|
235
|
+
self,
|
|
236
|
+
config: RunnableConfig | None,
|
|
237
|
+
*,
|
|
238
|
+
filter: Optional[dict[str, Any]] = None,
|
|
239
|
+
before: RunnableConfig | None = None,
|
|
240
|
+
limit: Optional[int] = None,
|
|
241
|
+
) -> Iterator[CheckpointTuple]:
|
|
242
|
+
"""Yield stored checkpoints newest-first, honoring ``before``/``limit``."""
|
|
243
|
+
if config is None:
|
|
244
|
+
raise ValueError("config with a thread_id is required to list checkpoints")
|
|
245
|
+
thread_id, actor_id, checkpoint_ns = _parse_config(config)
|
|
246
|
+
session = self._resolve_session(thread_id, actor_id, checkpoint_ns)
|
|
247
|
+
checkpoints, writes_by_ckpt, channel_data = self._read(session)
|
|
248
|
+
|
|
249
|
+
before_id = get_checkpoint_id(before) if before else None
|
|
250
|
+
count = 0
|
|
251
|
+
for checkpoint_id in sorted(checkpoints, reverse=True):
|
|
252
|
+
if before_id and checkpoint_id >= before_id:
|
|
253
|
+
continue
|
|
254
|
+
if limit is not None and count >= limit:
|
|
255
|
+
break
|
|
256
|
+
yield self._build_tuple(
|
|
257
|
+
checkpoints[checkpoint_id],
|
|
258
|
+
writes_by_ckpt.get(checkpoint_id, []),
|
|
259
|
+
channel_data,
|
|
260
|
+
thread_id,
|
|
261
|
+
actor_id,
|
|
262
|
+
checkpoint_ns,
|
|
263
|
+
)
|
|
264
|
+
count += 1
|
|
265
|
+
|
|
266
|
+
def get_next_version(self, current: Optional[Any], channel: Optional[str] = None) -> str:
|
|
267
|
+
"""Monotonic version string, matching LangGraph's default scheme."""
|
|
268
|
+
if current is None:
|
|
269
|
+
current_v = 0
|
|
270
|
+
elif isinstance(current, int):
|
|
271
|
+
current_v = current
|
|
272
|
+
else:
|
|
273
|
+
current_v = int(str(current).split(".")[0])
|
|
274
|
+
return f"{current_v + 1:032}.{random.random():016}"
|
|
275
|
+
|
|
276
|
+
# ----- async wrappers (the Session Store client is synchronous) -----------
|
|
277
|
+
|
|
278
|
+
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
|
279
|
+
return await _run_sync(self.get_tuple, config)
|
|
280
|
+
|
|
281
|
+
async def alist(
|
|
282
|
+
self,
|
|
283
|
+
config: RunnableConfig | None,
|
|
284
|
+
*,
|
|
285
|
+
filter: Optional[dict[str, Any]] = None,
|
|
286
|
+
before: RunnableConfig | None = None,
|
|
287
|
+
limit: Optional[int] = None,
|
|
288
|
+
):
|
|
289
|
+
for item in await _run_sync(
|
|
290
|
+
lambda: list(self.list(config, filter=filter, before=before, limit=limit))
|
|
291
|
+
):
|
|
292
|
+
yield item
|
|
293
|
+
|
|
294
|
+
async def aput(
|
|
295
|
+
self,
|
|
296
|
+
config: RunnableConfig,
|
|
297
|
+
checkpoint: Checkpoint,
|
|
298
|
+
metadata: CheckpointMetadata,
|
|
299
|
+
new_versions: ChannelVersions,
|
|
300
|
+
) -> RunnableConfig:
|
|
301
|
+
return await _run_sync(self.put, config, checkpoint, metadata, new_versions)
|
|
302
|
+
|
|
303
|
+
async def aput_writes(
|
|
304
|
+
self,
|
|
305
|
+
config: RunnableConfig,
|
|
306
|
+
writes: Sequence[tuple[str, Any]],
|
|
307
|
+
task_id: str,
|
|
308
|
+
task_path: str = "",
|
|
309
|
+
) -> None:
|
|
310
|
+
return await _run_sync(self.put_writes, config, writes, task_id, task_path)
|
|
311
|
+
|
|
312
|
+
async def adelete_thread(self, thread_id: str, actor_id: str = "") -> None:
|
|
313
|
+
await _run_sync(self.delete_thread, thread_id, actor_id)
|
|
314
|
+
|
|
315
|
+
# ----- internals ----------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
def _resolve_session(self, thread_id: str, actor_id: str, checkpoint_ns: str) -> Session:
|
|
318
|
+
session_id = _session_id(thread_id, checkpoint_ns)
|
|
319
|
+
cached = self._sessions.get(session_id)
|
|
320
|
+
if cached is not None:
|
|
321
|
+
return cached
|
|
322
|
+
try:
|
|
323
|
+
session = self._client.get_session(session_id=session_id)
|
|
324
|
+
except tuple(_not_found_errors()) as _: # type: ignore[misc]
|
|
325
|
+
session = self._client.create_session(
|
|
326
|
+
actor_id=actor_id,
|
|
327
|
+
session_id=session_id,
|
|
328
|
+
metadata={"langgraph_thread_id": thread_id, "checkpoint_ns": checkpoint_ns},
|
|
329
|
+
)
|
|
330
|
+
self._sessions[session_id] = session
|
|
331
|
+
return session
|
|
332
|
+
|
|
333
|
+
def _read(
|
|
334
|
+
self, session: Session
|
|
335
|
+
) -> tuple[
|
|
336
|
+
dict[str, dict[str, Any]],
|
|
337
|
+
dict[str, builtins.list[dict[str, Any]]],
|
|
338
|
+
dict[tuple[str, str], Any],
|
|
339
|
+
]:
|
|
340
|
+
"""List items and group them into checkpoints, writes, and channel data."""
|
|
341
|
+
checkpoints: dict[str, dict[str, Any]] = {}
|
|
342
|
+
writes_by_ckpt: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
343
|
+
channel_data: dict[tuple[str, str], Any] = {}
|
|
344
|
+
|
|
345
|
+
for item in self._client.list_items(session, order_by=_ORDER_BY):
|
|
346
|
+
data = item.data
|
|
347
|
+
event_type = data.get("event_type")
|
|
348
|
+
if event_type == _EVENT_CHECKPOINT:
|
|
349
|
+
checkpoints[data["checkpoint_id"]] = data
|
|
350
|
+
elif event_type == _EVENT_WRITES:
|
|
351
|
+
writes_by_ckpt[data["checkpoint_id"]].extend(data["writes"])
|
|
352
|
+
elif event_type == _EVENT_CHANNEL_DATA and data["value"] != _EMPTY_CHANNEL_VALUE:
|
|
353
|
+
channel_data[(data["channel"], data["version"])] = self._deserialize(data["value"])
|
|
354
|
+
return checkpoints, writes_by_ckpt, channel_data
|
|
355
|
+
|
|
356
|
+
def _build_tuple(
|
|
357
|
+
self,
|
|
358
|
+
checkpoint_event: dict[str, Any],
|
|
359
|
+
writes: builtins.list[dict[str, Any]],
|
|
360
|
+
channel_data: dict[tuple[str, str], Any],
|
|
361
|
+
thread_id: str,
|
|
362
|
+
actor_id: str,
|
|
363
|
+
checkpoint_ns: str,
|
|
364
|
+
) -> CheckpointTuple:
|
|
365
|
+
checkpoint = dict(self._deserialize(checkpoint_event["checkpoint_data"]))
|
|
366
|
+
channel_values: dict[str, Any] = {}
|
|
367
|
+
for channel, version in checkpoint.get("channel_versions", {}).items():
|
|
368
|
+
key = (channel, str(version))
|
|
369
|
+
if key in channel_data:
|
|
370
|
+
channel_values[channel] = channel_data[key]
|
|
371
|
+
checkpoint["channel_values"] = channel_values
|
|
372
|
+
|
|
373
|
+
pending_writes = [
|
|
374
|
+
(w["task_id"], w["channel"], self._deserialize(w["value"])) for w in writes
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
parent_config = None
|
|
378
|
+
if checkpoint_event.get("parent_checkpoint_id"):
|
|
379
|
+
parent_config = _config(
|
|
380
|
+
thread_id, actor_id, checkpoint_ns, checkpoint_event["parent_checkpoint_id"]
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return CheckpointTuple(
|
|
384
|
+
config=_config(thread_id, actor_id, checkpoint_ns, checkpoint_event["checkpoint_id"]),
|
|
385
|
+
checkpoint=checkpoint, # type: ignore[arg-type]
|
|
386
|
+
metadata=self._deserialize(checkpoint_event["metadata"]),
|
|
387
|
+
parent_config=parent_config,
|
|
388
|
+
pending_writes=pending_writes,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _checkpoint_item(
|
|
392
|
+
self,
|
|
393
|
+
*,
|
|
394
|
+
checkpoint_id: str,
|
|
395
|
+
checkpoint_data: dict[str, Any],
|
|
396
|
+
metadata: dict[str, Any],
|
|
397
|
+
parent_checkpoint_id: Optional[str],
|
|
398
|
+
) -> dict[str, Any]:
|
|
399
|
+
return {
|
|
400
|
+
"event_type": _EVENT_CHECKPOINT,
|
|
401
|
+
"checkpoint_id": checkpoint_id,
|
|
402
|
+
"checkpoint_data": self._serialize(checkpoint_data),
|
|
403
|
+
"metadata": self._serialize(metadata),
|
|
404
|
+
"parent_checkpoint_id": parent_checkpoint_id,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
def _channel_data_item(self, channel: str, version: str, value: Any) -> dict[str, Any]:
|
|
408
|
+
stored = value if value == _EMPTY_CHANNEL_VALUE else self._serialize(value)
|
|
409
|
+
return {
|
|
410
|
+
"event_type": _EVENT_CHANNEL_DATA,
|
|
411
|
+
"channel": channel,
|
|
412
|
+
"version": version,
|
|
413
|
+
"value": stored,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def _serialize(self, value: Any) -> dict[str, str]:
|
|
417
|
+
"""Serialize via the LangGraph serde into JSON-safe ``{type, data}``."""
|
|
418
|
+
type_tag, blob = self.serde.dumps_typed(value)
|
|
419
|
+
return {"type": type_tag, "data": base64.b64encode(blob).decode("utf-8")}
|
|
420
|
+
|
|
421
|
+
def _deserialize(self, serialized: dict[str, str]) -> Any:
|
|
422
|
+
return self.serde.loads_typed((serialized["type"], base64.b64decode(serialized["data"])))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ----- module-level helpers --------------------------------------------------
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _parse_config(config: RunnableConfig | dict[str, Any] | None) -> tuple[str, str, str]:
|
|
429
|
+
configurable = (config or {}).get("configurable", {}) if config else {}
|
|
430
|
+
thread_id = configurable.get("thread_id")
|
|
431
|
+
actor_id = configurable.get("actor_id")
|
|
432
|
+
if not thread_id:
|
|
433
|
+
raise ValueError("config must contain configurable.thread_id")
|
|
434
|
+
if not actor_id:
|
|
435
|
+
raise ValueError("config must contain configurable.actor_id")
|
|
436
|
+
return thread_id, actor_id, configurable.get("checkpoint_ns", "") or ""
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _session_id(thread_id: str, checkpoint_ns: str) -> str:
|
|
440
|
+
"""Deterministic session id for a (thread_id, checkpoint_ns) pair.
|
|
441
|
+
|
|
442
|
+
With a namespace we hash the pair so the id stays within the store's id constraints and never
|
|
443
|
+
collides with the bare ``thread_id`` used for the default namespace.
|
|
444
|
+
"""
|
|
445
|
+
if checkpoint_ns:
|
|
446
|
+
return hashlib.sha256(f"{thread_id}\x1f{checkpoint_ns}".encode()).hexdigest()
|
|
447
|
+
return thread_id
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _config(
|
|
451
|
+
thread_id: str, actor_id: str, checkpoint_ns: str, checkpoint_id: str
|
|
452
|
+
) -> RunnableConfig:
|
|
453
|
+
return {
|
|
454
|
+
"configurable": {
|
|
455
|
+
"thread_id": thread_id,
|
|
456
|
+
"actor_id": actor_id,
|
|
457
|
+
"checkpoint_ns": checkpoint_ns,
|
|
458
|
+
"checkpoint_id": checkpoint_id,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _not_found_errors() -> tuple[type, ...]:
|
|
464
|
+
"""Exception types that mean 'session does not exist yet'."""
|
|
465
|
+
try:
|
|
466
|
+
from databricks.sdk.errors import NotFound
|
|
467
|
+
|
|
468
|
+
return (NotFound,)
|
|
469
|
+
except ImportError: # pragma: no cover - SDK always present in practice
|
|
470
|
+
return (_SessionNotFound,)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class _SessionNotFound(Exception):
|
|
474
|
+
"""Fallback 'not found' used only when databricks.sdk is unavailable."""
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def _run_sync(fn, *args):
|
|
478
|
+
import asyncio
|
|
479
|
+
|
|
480
|
+
loop = asyncio.get_running_loop()
|
|
481
|
+
return (
|
|
482
|
+
await loop.run_in_executor(None, fn, *args)
|
|
483
|
+
if args
|
|
484
|
+
else await loop.run_in_executor(None, fn)
|
|
485
|
+
)
|