coco-framework 0.1.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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from coco_core.runnables import RunnableConfig
|
|
10
|
+
from coco_graph.checkpoint.base import (
|
|
11
|
+
WRITES_IDX_MAP,
|
|
12
|
+
ChannelVersions,
|
|
13
|
+
Checkpoint,
|
|
14
|
+
CheckpointMetadata,
|
|
15
|
+
CheckpointTuple,
|
|
16
|
+
DeltaChannelHistory,
|
|
17
|
+
get_checkpoint_id,
|
|
18
|
+
get_serializable_checkpoint_metadata,
|
|
19
|
+
)
|
|
20
|
+
from coco_graph.checkpoint.serde.base import SerializerProtocol
|
|
21
|
+
from coco_graph.checkpoint.serde.types import _DeltaSnapshot
|
|
22
|
+
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
23
|
+
from psycopg.rows import DictRow, dict_row
|
|
24
|
+
from psycopg.types.json import Jsonb
|
|
25
|
+
from psycopg_pool import ConnectionPool
|
|
26
|
+
|
|
27
|
+
from coco_graph.checkpoint.postgres import _internal
|
|
28
|
+
from coco_graph.checkpoint.postgres.base import (
|
|
29
|
+
_DELTA_PAGE_SIZE,
|
|
30
|
+
BasePostgresSaver,
|
|
31
|
+
_build_delta_stage1_sql,
|
|
32
|
+
_build_delta_stage2_sql,
|
|
33
|
+
_DeltaStage2Row,
|
|
34
|
+
)
|
|
35
|
+
from coco_graph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
|
36
|
+
|
|
37
|
+
Conn = _internal.Conn # For backward compatibility
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PostgresSaver(BasePostgresSaver):
|
|
41
|
+
"""Checkpointer that stores checkpoints in a Postgres database."""
|
|
42
|
+
|
|
43
|
+
lock: threading.Lock
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
conn: _internal.Conn,
|
|
48
|
+
pipe: Pipeline | None = None,
|
|
49
|
+
serde: SerializerProtocol | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(serde=serde)
|
|
52
|
+
if isinstance(conn, ConnectionPool) and pipe is not None:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Pipeline should be used only with a single Connection, not ConnectionPool."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self.conn = conn
|
|
58
|
+
self.pipe = pipe
|
|
59
|
+
self.lock = threading.Lock()
|
|
60
|
+
self.supports_pipeline = Capabilities().has_pipeline()
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
@contextmanager
|
|
64
|
+
def from_conn_string(
|
|
65
|
+
cls, conn_string: str, *, pipeline: bool = False
|
|
66
|
+
) -> Iterator[PostgresSaver]:
|
|
67
|
+
"""Create a new PostgresSaver instance from a connection string.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
conn_string: The Postgres connection info string.
|
|
71
|
+
pipeline: whether to use Pipeline
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
PostgresSaver: A new PostgresSaver instance.
|
|
75
|
+
"""
|
|
76
|
+
with Connection.connect(
|
|
77
|
+
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
|
78
|
+
) as conn:
|
|
79
|
+
if pipeline:
|
|
80
|
+
with conn.pipeline() as pipe:
|
|
81
|
+
yield cls(conn, pipe)
|
|
82
|
+
else:
|
|
83
|
+
yield cls(conn)
|
|
84
|
+
|
|
85
|
+
def setup(self) -> None:
|
|
86
|
+
"""Set up the checkpoint database asynchronously.
|
|
87
|
+
|
|
88
|
+
This method creates the necessary tables in the Postgres database if they don't
|
|
89
|
+
already exist and runs database migrations. It MUST be called directly by the user
|
|
90
|
+
the first time checkpointer is used.
|
|
91
|
+
"""
|
|
92
|
+
with self._cursor() as cur:
|
|
93
|
+
cur.execute(self.MIGRATIONS[0])
|
|
94
|
+
results = cur.execute(
|
|
95
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
96
|
+
)
|
|
97
|
+
row = results.fetchone()
|
|
98
|
+
if row is None:
|
|
99
|
+
version = -1
|
|
100
|
+
else:
|
|
101
|
+
version = row["v"]
|
|
102
|
+
for v, migration in zip(
|
|
103
|
+
range(version + 1, len(self.MIGRATIONS)),
|
|
104
|
+
self.MIGRATIONS[version + 1 :],
|
|
105
|
+
strict=False,
|
|
106
|
+
):
|
|
107
|
+
cur.execute(migration)
|
|
108
|
+
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
|
109
|
+
if self.pipe:
|
|
110
|
+
self.pipe.sync()
|
|
111
|
+
|
|
112
|
+
def list(
|
|
113
|
+
self,
|
|
114
|
+
config: RunnableConfig | None,
|
|
115
|
+
*,
|
|
116
|
+
filter: dict[str, Any] | None = None,
|
|
117
|
+
before: RunnableConfig | None = None,
|
|
118
|
+
limit: int | None = None,
|
|
119
|
+
) -> Iterator[CheckpointTuple]:
|
|
120
|
+
"""List checkpoints from the database.
|
|
121
|
+
|
|
122
|
+
This method retrieves a list of checkpoint tuples from the Postgres database based
|
|
123
|
+
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
config: The config to use for listing the checkpoints.
|
|
127
|
+
filter: Additional filtering criteria for metadata.
|
|
128
|
+
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
|
129
|
+
limit: The maximum number of checkpoints to return.
|
|
130
|
+
|
|
131
|
+
Yields:
|
|
132
|
+
An iterator of checkpoint tuples.
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
>>> from coco_graph.checkpoint.postgres import PostgresSaver
|
|
136
|
+
>>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
|
137
|
+
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
|
138
|
+
... # Run a graph, then list the checkpoints
|
|
139
|
+
>>> config = {"configurable": {"thread_id": "1"}}
|
|
140
|
+
>>> checkpoints = list(memory.list(config, limit=2))
|
|
141
|
+
>>> print(checkpoints)
|
|
142
|
+
[CheckpointTuple(...), CheckpointTuple(...)]
|
|
143
|
+
|
|
144
|
+
>>> config = {"configurable": {"thread_id": "1"}}
|
|
145
|
+
>>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
|
|
146
|
+
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
|
147
|
+
... # Run a graph, then list the checkpoints
|
|
148
|
+
>>> checkpoints = list(memory.list(config, before=before))
|
|
149
|
+
>>> print(checkpoints)
|
|
150
|
+
[CheckpointTuple(...), ...]
|
|
151
|
+
"""
|
|
152
|
+
where, args = self._search_where(config, filter, before)
|
|
153
|
+
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
|
154
|
+
params = list(args)
|
|
155
|
+
if limit is not None:
|
|
156
|
+
query += " LIMIT %s"
|
|
157
|
+
params.append(int(limit))
|
|
158
|
+
# if we change this to use .stream() we need to make sure to close the cursor
|
|
159
|
+
with self._cursor() as cur:
|
|
160
|
+
cur.execute(query, params)
|
|
161
|
+
values = cur.fetchall()
|
|
162
|
+
if not values:
|
|
163
|
+
return
|
|
164
|
+
# migrate pending sends if necessary
|
|
165
|
+
if to_migrate := [
|
|
166
|
+
v
|
|
167
|
+
for v in values
|
|
168
|
+
if v["checkpoint"]["v"] < 4 and v["parent_checkpoint_id"]
|
|
169
|
+
]:
|
|
170
|
+
cur.execute(
|
|
171
|
+
self.SELECT_PENDING_SENDS_SQL,
|
|
172
|
+
(
|
|
173
|
+
values[0]["thread_id"],
|
|
174
|
+
[v["parent_checkpoint_id"] for v in to_migrate],
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
grouped_by_parent = defaultdict(list)
|
|
178
|
+
for value in to_migrate:
|
|
179
|
+
grouped_by_parent[value["parent_checkpoint_id"]].append(value)
|
|
180
|
+
for sends in cur:
|
|
181
|
+
for value in grouped_by_parent[sends["checkpoint_id"]]:
|
|
182
|
+
if value["channel_values"] is None:
|
|
183
|
+
value["channel_values"] = []
|
|
184
|
+
self._migrate_pending_sends(
|
|
185
|
+
sends["sends"],
|
|
186
|
+
value["checkpoint"],
|
|
187
|
+
value["channel_values"],
|
|
188
|
+
)
|
|
189
|
+
for value in values:
|
|
190
|
+
yield self._load_checkpoint_tuple(value)
|
|
191
|
+
|
|
192
|
+
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
|
193
|
+
"""Get a checkpoint tuple from the database.
|
|
194
|
+
|
|
195
|
+
This method retrieves a checkpoint tuple from the Postgres database based on the
|
|
196
|
+
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
|
197
|
+
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
|
198
|
+
for the given thread ID is retrieved.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
config: The config to use for retrieving the checkpoint.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
|
|
208
|
+
Basic:
|
|
209
|
+
>>> config = {"configurable": {"thread_id": "1"}}
|
|
210
|
+
>>> checkpoint_tuple = memory.get_tuple(config)
|
|
211
|
+
>>> print(checkpoint_tuple)
|
|
212
|
+
CheckpointTuple(...)
|
|
213
|
+
|
|
214
|
+
With timestamp:
|
|
215
|
+
|
|
216
|
+
>>> config = {
|
|
217
|
+
... "configurable": {
|
|
218
|
+
... "thread_id": "1",
|
|
219
|
+
... "checkpoint_ns": "",
|
|
220
|
+
... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
|
221
|
+
... }
|
|
222
|
+
... }
|
|
223
|
+
>>> checkpoint_tuple = memory.get_tuple(config)
|
|
224
|
+
>>> print(checkpoint_tuple)
|
|
225
|
+
CheckpointTuple(...)
|
|
226
|
+
"""
|
|
227
|
+
thread_id = config["configurable"]["thread_id"]
|
|
228
|
+
checkpoint_id = get_checkpoint_id(config)
|
|
229
|
+
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
230
|
+
if checkpoint_id:
|
|
231
|
+
args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
|
|
232
|
+
where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
|
|
233
|
+
else:
|
|
234
|
+
args = (thread_id, checkpoint_ns)
|
|
235
|
+
where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
|
|
236
|
+
|
|
237
|
+
with self._cursor() as cur:
|
|
238
|
+
cur.execute(
|
|
239
|
+
self.SELECT_SQL + where,
|
|
240
|
+
args,
|
|
241
|
+
)
|
|
242
|
+
value = cur.fetchone()
|
|
243
|
+
if value is None:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
# migrate pending sends if necessary
|
|
247
|
+
if value["checkpoint"]["v"] < 4 and value["parent_checkpoint_id"]:
|
|
248
|
+
cur.execute(
|
|
249
|
+
self.SELECT_PENDING_SENDS_SQL,
|
|
250
|
+
(thread_id, [value["parent_checkpoint_id"]]),
|
|
251
|
+
)
|
|
252
|
+
if sends := cur.fetchone():
|
|
253
|
+
if value["channel_values"] is None:
|
|
254
|
+
value["channel_values"] = []
|
|
255
|
+
self._migrate_pending_sends(
|
|
256
|
+
sends["sends"],
|
|
257
|
+
value["checkpoint"],
|
|
258
|
+
value["channel_values"],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return self._load_checkpoint_tuple(value)
|
|
262
|
+
|
|
263
|
+
def put(
|
|
264
|
+
self,
|
|
265
|
+
config: RunnableConfig,
|
|
266
|
+
checkpoint: Checkpoint,
|
|
267
|
+
metadata: CheckpointMetadata,
|
|
268
|
+
new_versions: ChannelVersions,
|
|
269
|
+
) -> RunnableConfig:
|
|
270
|
+
"""Save a checkpoint to the database.
|
|
271
|
+
|
|
272
|
+
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
|
273
|
+
with the provided config and its parent config (if any).
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
config: The config to associate with the checkpoint.
|
|
277
|
+
checkpoint: The checkpoint to save.
|
|
278
|
+
metadata: Additional metadata to save with the checkpoint.
|
|
279
|
+
new_versions: New channel versions as of this write.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
RunnableConfig: Updated configuration after storing the checkpoint.
|
|
283
|
+
|
|
284
|
+
Examples:
|
|
285
|
+
|
|
286
|
+
>>> from coco_graph.checkpoint.postgres import PostgresSaver
|
|
287
|
+
>>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
|
|
288
|
+
>>> with PostgresSaver.from_conn_string(DB_URI) as memory:
|
|
289
|
+
>>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
|
|
290
|
+
>>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
|
|
291
|
+
>>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
|
|
292
|
+
>>> print(saved_config)
|
|
293
|
+
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
|
|
294
|
+
"""
|
|
295
|
+
configurable = config["configurable"].copy()
|
|
296
|
+
thread_id = configurable.pop("thread_id")
|
|
297
|
+
checkpoint_ns = configurable.pop("checkpoint_ns")
|
|
298
|
+
checkpoint_id = configurable.pop("checkpoint_id", None)
|
|
299
|
+
copy = checkpoint.copy()
|
|
300
|
+
copy["channel_values"] = copy["channel_values"].copy()
|
|
301
|
+
next_config = {
|
|
302
|
+
"configurable": {
|
|
303
|
+
"thread_id": thread_id,
|
|
304
|
+
"checkpoint_ns": checkpoint_ns,
|
|
305
|
+
"checkpoint_id": checkpoint["id"],
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# inline primitive values in checkpoint table
|
|
310
|
+
# others are stored in blobs table
|
|
311
|
+
blob_values = {}
|
|
312
|
+
for k, v in checkpoint["channel_values"].items():
|
|
313
|
+
if isinstance(v, _DeltaSnapshot):
|
|
314
|
+
blob_values[k] = copy["channel_values"].pop(k)
|
|
315
|
+
copy["channel_values"][k] = True
|
|
316
|
+
elif v is None or isinstance(v, (str, int, float, bool)):
|
|
317
|
+
pass
|
|
318
|
+
else:
|
|
319
|
+
blob_values[k] = copy["channel_values"].pop(k)
|
|
320
|
+
|
|
321
|
+
with self._cursor(pipeline=True) as cur:
|
|
322
|
+
if blob_versions := {
|
|
323
|
+
k: v for k, v in new_versions.items() if k in blob_values
|
|
324
|
+
}:
|
|
325
|
+
cur.executemany(
|
|
326
|
+
self.UPSERT_CHECKPOINT_BLOBS_SQL,
|
|
327
|
+
self._dump_blobs(
|
|
328
|
+
thread_id,
|
|
329
|
+
checkpoint_ns,
|
|
330
|
+
blob_values,
|
|
331
|
+
blob_versions,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
cur.execute(
|
|
335
|
+
self.UPSERT_CHECKPOINTS_SQL,
|
|
336
|
+
(
|
|
337
|
+
thread_id,
|
|
338
|
+
checkpoint_ns,
|
|
339
|
+
checkpoint["id"],
|
|
340
|
+
checkpoint_id,
|
|
341
|
+
Jsonb(copy),
|
|
342
|
+
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
return next_config
|
|
346
|
+
|
|
347
|
+
def put_writes(
|
|
348
|
+
self,
|
|
349
|
+
config: RunnableConfig,
|
|
350
|
+
writes: Sequence[tuple[str, Any]],
|
|
351
|
+
task_id: str,
|
|
352
|
+
task_path: str = "",
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Store intermediate writes linked to a checkpoint.
|
|
355
|
+
|
|
356
|
+
This method saves intermediate writes associated with a checkpoint to the Postgres database.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
config: Configuration of the related checkpoint.
|
|
360
|
+
writes: List of writes to store.
|
|
361
|
+
task_id: Identifier for the task creating the writes.
|
|
362
|
+
"""
|
|
363
|
+
query = (
|
|
364
|
+
self.UPSERT_CHECKPOINT_WRITES_SQL
|
|
365
|
+
if all(w[0] in WRITES_IDX_MAP for w in writes)
|
|
366
|
+
else self.INSERT_CHECKPOINT_WRITES_SQL
|
|
367
|
+
)
|
|
368
|
+
with self._cursor(pipeline=True) as cur:
|
|
369
|
+
cur.executemany(
|
|
370
|
+
query,
|
|
371
|
+
self._dump_writes(
|
|
372
|
+
config["configurable"]["thread_id"],
|
|
373
|
+
config["configurable"]["checkpoint_ns"],
|
|
374
|
+
config["configurable"]["checkpoint_id"],
|
|
375
|
+
task_id,
|
|
376
|
+
task_path,
|
|
377
|
+
writes,
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def delete_thread(self, thread_id: str) -> None:
|
|
382
|
+
"""Delete all checkpoints and writes associated with a thread ID.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
thread_id: The thread ID to delete.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
None
|
|
389
|
+
"""
|
|
390
|
+
with self._cursor(pipeline=True) as cur:
|
|
391
|
+
cur.execute(
|
|
392
|
+
"DELETE FROM checkpoints WHERE thread_id = %s",
|
|
393
|
+
(str(thread_id),),
|
|
394
|
+
)
|
|
395
|
+
cur.execute(
|
|
396
|
+
"DELETE FROM checkpoint_blobs WHERE thread_id = %s",
|
|
397
|
+
(str(thread_id),),
|
|
398
|
+
)
|
|
399
|
+
cur.execute(
|
|
400
|
+
"DELETE FROM checkpoint_writes WHERE thread_id = %s",
|
|
401
|
+
(str(thread_id),),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
@contextmanager
|
|
405
|
+
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
|
|
406
|
+
"""Create a database cursor as a context manager.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
pipeline: whether to use pipeline for the DB operations inside the context manager.
|
|
410
|
+
Will be applied regardless of whether the PostgresSaver instance was initialized with a pipeline.
|
|
411
|
+
If pipeline mode is not supported, will fall back to using transaction context manager.
|
|
412
|
+
"""
|
|
413
|
+
with self.lock, _internal.get_connection(self.conn) as conn:
|
|
414
|
+
if self.pipe:
|
|
415
|
+
# a connection in pipeline mode can be used concurrently
|
|
416
|
+
# in multiple threads/coroutines, but only one cursor can be
|
|
417
|
+
# used at a time
|
|
418
|
+
try:
|
|
419
|
+
with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
420
|
+
yield cur
|
|
421
|
+
finally:
|
|
422
|
+
if pipeline:
|
|
423
|
+
self.pipe.sync()
|
|
424
|
+
elif pipeline:
|
|
425
|
+
# a connection not in pipeline mode can only be used by one
|
|
426
|
+
# thread/coroutine at a time, so we acquire a lock
|
|
427
|
+
if self.supports_pipeline:
|
|
428
|
+
with (
|
|
429
|
+
conn.pipeline(),
|
|
430
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
431
|
+
):
|
|
432
|
+
yield cur
|
|
433
|
+
else:
|
|
434
|
+
# Use connection's transaction context manager when pipeline mode not supported
|
|
435
|
+
with (
|
|
436
|
+
conn.transaction(),
|
|
437
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
438
|
+
):
|
|
439
|
+
yield cur
|
|
440
|
+
else:
|
|
441
|
+
with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
442
|
+
yield cur
|
|
443
|
+
|
|
444
|
+
def get_delta_channel_history(
|
|
445
|
+
self, *, config: RunnableConfig, channels: Sequence[str]
|
|
446
|
+
) -> Mapping[str, DeltaChannelHistory]:
|
|
447
|
+
"""Fast-path override of `BaseCheckpointSaver.get_delta_channel_history`.
|
|
448
|
+
|
|
449
|
+
Two-stage query, both stages cover ALL requested channels:
|
|
450
|
+
|
|
451
|
+
* Stage 1 (paged): dynamic SELECT over `checkpoints` with K parallel
|
|
452
|
+
JSONB key lookups (one column pair per channel) — no subquery, no
|
|
453
|
+
aggregation. Pages newest-first by `checkpoint_id` with a cursor;
|
|
454
|
+
page size is `_DELTA_PAGE_SIZE`. Stops paging when every channel
|
|
455
|
+
has found its seed or the chain is exhausted.
|
|
456
|
+
|
|
457
|
+
* Stage 2 (per-channel UNION ALL): one branch per channel reading
|
|
458
|
+
`checkpoint_writes` filtered to that channel's specific
|
|
459
|
+
`chain_cids`, plus one branch per channel that has a seed reading
|
|
460
|
+
`checkpoint_blobs` for that channel + version. Avoids the
|
|
461
|
+
over-fetch of a single `channel = ANY(channels)` filter when
|
|
462
|
+
channels have different chain depths.
|
|
463
|
+
"""
|
|
464
|
+
if not channels:
|
|
465
|
+
return {}
|
|
466
|
+
channels = list(channels)
|
|
467
|
+
thread_id = config["configurable"]["thread_id"]
|
|
468
|
+
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
469
|
+
checkpoint_id = get_checkpoint_id(config)
|
|
470
|
+
if checkpoint_id is None:
|
|
471
|
+
target = self.get_tuple(config)
|
|
472
|
+
if target is None:
|
|
473
|
+
return {ch: {"writes": []} for ch in channels}
|
|
474
|
+
checkpoint_id = target.config["configurable"]["checkpoint_id"]
|
|
475
|
+
|
|
476
|
+
# Stage 1: paged K-JSONB-lookup scan, walking the parent chain in
|
|
477
|
+
# Python after each page. Stops as soon as every channel has its seed.
|
|
478
|
+
stage1_sql = _build_delta_stage1_sql(channels, paged=True)
|
|
479
|
+
parent_of: dict[str, str | None] = {}
|
|
480
|
+
ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
|
|
481
|
+
hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
|
|
482
|
+
inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
|
|
483
|
+
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
|
|
484
|
+
seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
|
|
485
|
+
seed_inline_by_ch: dict[str, Any] = {}
|
|
486
|
+
walk_cursor_by_ch: dict[str, str | None] = {}
|
|
487
|
+
seeded: set[str] = set()
|
|
488
|
+
cursor: str | None = None
|
|
489
|
+
|
|
490
|
+
with self._cursor() as cur:
|
|
491
|
+
while True:
|
|
492
|
+
stage1_params: list[Any] = []
|
|
493
|
+
for ch in channels:
|
|
494
|
+
# ver_i, blob channel, blob version, inline_i
|
|
495
|
+
stage1_params.extend([ch, ch, ch, ch])
|
|
496
|
+
stage1_params.extend(
|
|
497
|
+
[thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
|
|
498
|
+
)
|
|
499
|
+
cur.execute(stage1_sql, stage1_params)
|
|
500
|
+
page = cur.fetchall()
|
|
501
|
+
if not page:
|
|
502
|
+
break
|
|
503
|
+
oldest = self._ingest_stage1_page(
|
|
504
|
+
cast("list[Mapping[str, Any]]", page),
|
|
505
|
+
channels,
|
|
506
|
+
parent_of,
|
|
507
|
+
ver_by_i_by_cid,
|
|
508
|
+
hb_by_i_by_cid,
|
|
509
|
+
inline_by_i_by_cid,
|
|
510
|
+
)
|
|
511
|
+
self._try_advance_walks(
|
|
512
|
+
checkpoint_id,
|
|
513
|
+
channels,
|
|
514
|
+
parent_of,
|
|
515
|
+
ver_by_i_by_cid,
|
|
516
|
+
hb_by_i_by_cid,
|
|
517
|
+
inline_by_i_by_cid,
|
|
518
|
+
chain_by_ch,
|
|
519
|
+
seed_ver_by_ch,
|
|
520
|
+
seed_inline_by_ch,
|
|
521
|
+
walk_cursor_by_ch,
|
|
522
|
+
seeded,
|
|
523
|
+
)
|
|
524
|
+
# Stop if every channel is seeded, or the page was short
|
|
525
|
+
# (chain exhausted — no more rows to fetch).
|
|
526
|
+
if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
|
|
527
|
+
break
|
|
528
|
+
cursor = oldest
|
|
529
|
+
|
|
530
|
+
# Stage 2: per-channel UNION ALL — one writes branch per channel
|
|
531
|
+
# with non-empty chain, plus one blob branch per seeded channel.
|
|
532
|
+
channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
|
|
533
|
+
channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
|
|
534
|
+
stage2_sql = _build_delta_stage2_sql(
|
|
535
|
+
channels_with_chain=channels_with_chain,
|
|
536
|
+
channels_with_seed=channels_with_seed,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if stage2_sql:
|
|
540
|
+
stage2_params: list[Any] = []
|
|
541
|
+
for ch in channels_with_chain:
|
|
542
|
+
stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
|
|
543
|
+
for ch in channels_with_seed:
|
|
544
|
+
stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
|
|
545
|
+
with self._cursor() as cur:
|
|
546
|
+
cur.execute(stage2_sql, stage2_params)
|
|
547
|
+
stage2_rows = cur.fetchall()
|
|
548
|
+
else:
|
|
549
|
+
stage2_rows = []
|
|
550
|
+
|
|
551
|
+
return self._build_delta_channels_writes_history(
|
|
552
|
+
channels=channels,
|
|
553
|
+
chain_by_ch=chain_by_ch,
|
|
554
|
+
seed_ver_by_ch=seed_ver_by_ch,
|
|
555
|
+
seed_inline_by_ch=seed_inline_by_ch,
|
|
556
|
+
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
|
|
560
|
+
"""
|
|
561
|
+
Convert a database row into a CheckpointTuple object.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
value: A row from the database containing checkpoint data.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
CheckpointTuple: A structured representation of the checkpoint,
|
|
568
|
+
including its configuration, metadata, parent checkpoint (if any),
|
|
569
|
+
and pending writes.
|
|
570
|
+
"""
|
|
571
|
+
return CheckpointTuple(
|
|
572
|
+
{
|
|
573
|
+
"configurable": {
|
|
574
|
+
"thread_id": value["thread_id"],
|
|
575
|
+
"checkpoint_ns": value["checkpoint_ns"],
|
|
576
|
+
"checkpoint_id": value["checkpoint_id"],
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
**value["checkpoint"],
|
|
581
|
+
"channel_values": {
|
|
582
|
+
**(value["checkpoint"].get("channel_values") or {}),
|
|
583
|
+
**self._load_blobs(value["channel_values"]),
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
value["metadata"],
|
|
587
|
+
(
|
|
588
|
+
{
|
|
589
|
+
"configurable": {
|
|
590
|
+
"thread_id": value["thread_id"],
|
|
591
|
+
"checkpoint_ns": value["checkpoint_ns"],
|
|
592
|
+
"checkpoint_id": value["parent_checkpoint_id"],
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if value["parent_checkpoint_id"]
|
|
596
|
+
else None
|
|
597
|
+
),
|
|
598
|
+
self._load_writes(value["pending_writes"]),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
__all__ = ["PostgresSaver", "BasePostgresSaver", "ShallowPostgresSaver", "Conn"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared async utility functions for the Postgres checkpoint & storage classes."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
|
|
6
|
+
from psycopg import AsyncConnection
|
|
7
|
+
from psycopg.rows import DictRow
|
|
8
|
+
from psycopg_pool import AsyncConnectionPool
|
|
9
|
+
|
|
10
|
+
Conn = AsyncConnection[DictRow] | AsyncConnectionPool[AsyncConnection[DictRow]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@asynccontextmanager
|
|
14
|
+
async def get_connection(
|
|
15
|
+
conn: Conn,
|
|
16
|
+
) -> AsyncIterator[AsyncConnection[DictRow]]:
|
|
17
|
+
if isinstance(conn, AsyncConnection):
|
|
18
|
+
yield conn
|
|
19
|
+
elif isinstance(conn, AsyncConnectionPool):
|
|
20
|
+
async with conn.connection() as conn:
|
|
21
|
+
yield conn
|
|
22
|
+
else:
|
|
23
|
+
raise TypeError(f"Invalid connection type: {type(conn)}")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared utility functions for the Postgres checkpoint & storage classes."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
from psycopg import Connection
|
|
7
|
+
from psycopg.rows import DictRow
|
|
8
|
+
from psycopg_pool import ConnectionPool
|
|
9
|
+
|
|
10
|
+
Conn = Connection[DictRow] | ConnectionPool[Connection[DictRow]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@contextmanager
|
|
14
|
+
def get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
|
|
15
|
+
if isinstance(conn, Connection):
|
|
16
|
+
yield conn
|
|
17
|
+
elif isinstance(conn, ConnectionPool):
|
|
18
|
+
with conn.connection() as conn:
|
|
19
|
+
yield conn
|
|
20
|
+
else:
|
|
21
|
+
raise TypeError(f"Invalid connection type: {type(conn)}")
|