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,268 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Callable, Iterator
|
|
4
|
+
from contextvars import ContextVar, Token
|
|
5
|
+
from typing import Any, TypeVar, cast
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from coco_core.callbacks import BaseCallbackHandler
|
|
9
|
+
|
|
10
|
+
from coco_graph._internal._constants import NS_SEP
|
|
11
|
+
from coco_graph.constants import TAG_NOSTREAM
|
|
12
|
+
from coco_graph.pregel.protocol import StreamChunk
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from coco_core.tracers._streaming import _StreamingCallbackHandler
|
|
16
|
+
except ImportError:
|
|
17
|
+
_StreamingCallbackHandler = object # type: ignore[assignment,misc]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
ToolCallWriter = Callable[[Any], None]
|
|
23
|
+
"""A closure bound to a single tool call that emits `tool-output-delta` events."""
|
|
24
|
+
|
|
25
|
+
_tool_call_writer: ContextVar[ToolCallWriter | None] = ContextVar(
|
|
26
|
+
"coco_graph_tool_call_writer", default=None
|
|
27
|
+
)
|
|
28
|
+
"""ContextVar holding the writer for the currently-executing tool call.
|
|
29
|
+
|
|
30
|
+
Set by `StreamToolCallHandler.on_tool_start` and reset on end/error.
|
|
31
|
+
Read by `ToolRuntime.emit_output_delta` (in `coco_graph.prebuilt`).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StreamToolCallHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|
36
|
+
"""Callback handler that emits tool-call lifecycle events on the stream.
|
|
37
|
+
|
|
38
|
+
Fires on Coco's `on_tool_*` callbacks and pushes to the `tools`
|
|
39
|
+
stream mode. Emits `tool-started` / `tool-output-delta` /
|
|
40
|
+
`tool-finished` / `tool-error` payloads keyed by `tool_call_id`.
|
|
41
|
+
|
|
42
|
+
While a tool is executing, this handler sets `_tool_call_writer` to a
|
|
43
|
+
closure bound to that call's namespace and `tool_call_id`.
|
|
44
|
+
`ToolRuntime.emit_output_delta` reads that ContextVar so tool bodies
|
|
45
|
+
can stream partial output without threading the writer through their
|
|
46
|
+
own signature.
|
|
47
|
+
|
|
48
|
+
Attached by `Pregel.stream` / `astream` when `"tools"` is in
|
|
49
|
+
`stream_modes`. `run_inline = True` keeps event ordering
|
|
50
|
+
deterministic.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
run_inline = True
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
stream: Callable[[StreamChunk], None],
|
|
58
|
+
subgraphs: bool,
|
|
59
|
+
*,
|
|
60
|
+
parent_ns: tuple[str, ...] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Configure the handler to stream tool-call events.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
stream: Callable that accepts a `StreamChunk` tuple
|
|
66
|
+
`(namespace, mode, payload)` and enqueues it.
|
|
67
|
+
subgraphs: Whether to emit events from tools called inside
|
|
68
|
+
nested subgraphs. When False, only tools at the
|
|
69
|
+
handler's own scope (`parent_ns`) emit.
|
|
70
|
+
parent_ns: Namespace where the handler was attached.
|
|
71
|
+
Mirrors the `StreamMessagesHandler` escape hatch:
|
|
72
|
+
tools whose containing namespace equals `parent_ns`
|
|
73
|
+
still emit even with `subgraphs=False`, so a node that
|
|
74
|
+
explicitly streams a subgraph with `stream_mode="tools"`
|
|
75
|
+
sees its own tools.
|
|
76
|
+
"""
|
|
77
|
+
self.stream = stream
|
|
78
|
+
self.subgraphs = subgraphs
|
|
79
|
+
self.parent_ns = parent_ns
|
|
80
|
+
# run_id → (namespace, tool_call_id, ContextVar token)
|
|
81
|
+
# `on_tool_end` does not receive `tool_call_id` in kwargs, so
|
|
82
|
+
# we correlate by `run_id` which is present on every callback.
|
|
83
|
+
self._run_to_call: dict[
|
|
84
|
+
UUID, tuple[tuple[str, ...], str, Token[ToolCallWriter | None]]
|
|
85
|
+
] = {}
|
|
86
|
+
|
|
87
|
+
def _ns_for_emit(
|
|
88
|
+
self,
|
|
89
|
+
metadata: dict[str, Any] | None,
|
|
90
|
+
tags: list[str] | None,
|
|
91
|
+
) -> tuple[str, ...] | None:
|
|
92
|
+
"""Resolve the namespace this tool call should emit at, or `None` to skip.
|
|
93
|
+
|
|
94
|
+
Mirrors `StreamMessagesHandler.on_chat_model_start`'s namespace
|
|
95
|
+
derivation: parses `coco_graph_checkpoint_ns` (which ends with
|
|
96
|
+
the `node_name:task_id` of the calling node), drops that
|
|
97
|
+
trailing segment, and returns the containing subgraph's own
|
|
98
|
+
namespace. Returns `None` when the call should be silently
|
|
99
|
+
suppressed:
|
|
100
|
+
|
|
101
|
+
- `metadata` is missing — handler is attached to a context
|
|
102
|
+
without Pregel routing info.
|
|
103
|
+
- `TAG_NOSTREAM` is in `tags` — caller explicitly opted out.
|
|
104
|
+
- Tool runs in a subgraph (`len(ns) > 0`) and the handler was
|
|
105
|
+
attached with `subgraphs=False` and a different `parent_ns`
|
|
106
|
+
than the call's containing subgraph.
|
|
107
|
+
"""
|
|
108
|
+
if not metadata:
|
|
109
|
+
return None
|
|
110
|
+
if tags and TAG_NOSTREAM in tags:
|
|
111
|
+
return None
|
|
112
|
+
nskey = metadata.get("coco_graph_checkpoint_ns")
|
|
113
|
+
if not nskey:
|
|
114
|
+
ns: tuple[str, ...] = ()
|
|
115
|
+
else:
|
|
116
|
+
ns = tuple(cast(str, nskey).split(NS_SEP))[:-1]
|
|
117
|
+
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
|
|
118
|
+
return None
|
|
119
|
+
return ns
|
|
120
|
+
|
|
121
|
+
def _start(
|
|
122
|
+
self,
|
|
123
|
+
serialized: dict[str, Any] | None,
|
|
124
|
+
input_str: str,
|
|
125
|
+
*,
|
|
126
|
+
run_id: UUID,
|
|
127
|
+
metadata: dict[str, Any] | None,
|
|
128
|
+
tags: list[str] | None,
|
|
129
|
+
inputs: dict[str, Any] | None,
|
|
130
|
+
kwargs: dict[str, Any],
|
|
131
|
+
) -> None:
|
|
132
|
+
ns = self._ns_for_emit(metadata, tags)
|
|
133
|
+
if ns is None:
|
|
134
|
+
return
|
|
135
|
+
tool_call_id = cast("str | None", kwargs.get("tool_call_id")) or str(run_id)
|
|
136
|
+
tool_name = (
|
|
137
|
+
(serialized or {}).get("name")
|
|
138
|
+
or cast("str | None", kwargs.get("name"))
|
|
139
|
+
or ""
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def writer(delta: Any) -> None:
|
|
143
|
+
self.stream(
|
|
144
|
+
(
|
|
145
|
+
ns,
|
|
146
|
+
"tools",
|
|
147
|
+
{
|
|
148
|
+
"event": "tool-output-delta",
|
|
149
|
+
"tool_call_id": tool_call_id,
|
|
150
|
+
"delta": delta,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
token = _tool_call_writer.set(writer)
|
|
156
|
+
self._run_to_call[run_id] = (ns, tool_call_id, token)
|
|
157
|
+
|
|
158
|
+
payload: dict[str, Any] = {
|
|
159
|
+
"event": "tool-started",
|
|
160
|
+
"tool_call_id": tool_call_id,
|
|
161
|
+
"tool_name": tool_name,
|
|
162
|
+
}
|
|
163
|
+
if inputs is not None:
|
|
164
|
+
payload["input"] = inputs
|
|
165
|
+
self.stream((ns, "tools", payload))
|
|
166
|
+
|
|
167
|
+
def _end(self, output: Any, *, run_id: UUID) -> None:
|
|
168
|
+
info = self._run_to_call.pop(run_id, None)
|
|
169
|
+
if info is None:
|
|
170
|
+
return
|
|
171
|
+
ns, tool_call_id, token = info
|
|
172
|
+
self._reset_writer(token)
|
|
173
|
+
self.stream(
|
|
174
|
+
(
|
|
175
|
+
ns,
|
|
176
|
+
"tools",
|
|
177
|
+
{
|
|
178
|
+
"event": "tool-finished",
|
|
179
|
+
"tool_call_id": tool_call_id,
|
|
180
|
+
"output": output,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _error(self, error: BaseException, *, run_id: UUID) -> None:
|
|
186
|
+
info = self._run_to_call.pop(run_id, None)
|
|
187
|
+
if info is None:
|
|
188
|
+
return
|
|
189
|
+
ns, tool_call_id, token = info
|
|
190
|
+
self._reset_writer(token)
|
|
191
|
+
self.stream(
|
|
192
|
+
(
|
|
193
|
+
ns,
|
|
194
|
+
"tools",
|
|
195
|
+
{
|
|
196
|
+
"event": "tool-error",
|
|
197
|
+
"tool_call_id": tool_call_id,
|
|
198
|
+
"message": str(error),
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def tap_output_aiter(
|
|
204
|
+
self, run_id: UUID, output: AsyncIterator[T]
|
|
205
|
+
) -> AsyncIterator[T]:
|
|
206
|
+
"""Pass-through — required by the `_StreamingCallbackHandler` protocol."""
|
|
207
|
+
return output
|
|
208
|
+
|
|
209
|
+
def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
|
|
210
|
+
"""Pass-through — sync counterpart to `tap_output_aiter`."""
|
|
211
|
+
return output
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _reset_writer(token: Token[ToolCallWriter | None]) -> None:
|
|
215
|
+
# Token is invalid if `on_tool_end` runs in a different context
|
|
216
|
+
# than `on_tool_start` (e.g. coco may hand off to a thread
|
|
217
|
+
# worker without copying the context). Swallow that case; the
|
|
218
|
+
# ContextVar lifetime is bounded by the enclosing task anyway.
|
|
219
|
+
try:
|
|
220
|
+
_tool_call_writer.reset(token)
|
|
221
|
+
except ValueError:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
# Sync callbacks
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def on_tool_start(
|
|
229
|
+
self,
|
|
230
|
+
serialized: dict[str, Any],
|
|
231
|
+
input_str: str,
|
|
232
|
+
*,
|
|
233
|
+
run_id: UUID,
|
|
234
|
+
parent_run_id: UUID | None = None,
|
|
235
|
+
tags: list[str] | None = None,
|
|
236
|
+
metadata: dict[str, Any] | None = None,
|
|
237
|
+
inputs: dict[str, Any] | None = None,
|
|
238
|
+
**kwargs: Any,
|
|
239
|
+
) -> Any:
|
|
240
|
+
self._start(
|
|
241
|
+
serialized,
|
|
242
|
+
input_str,
|
|
243
|
+
run_id=run_id,
|
|
244
|
+
metadata=metadata,
|
|
245
|
+
tags=tags,
|
|
246
|
+
inputs=inputs,
|
|
247
|
+
kwargs=kwargs,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def on_tool_end(
|
|
251
|
+
self,
|
|
252
|
+
output: Any,
|
|
253
|
+
*,
|
|
254
|
+
run_id: UUID,
|
|
255
|
+
parent_run_id: UUID | None = None,
|
|
256
|
+
**kwargs: Any,
|
|
257
|
+
) -> Any:
|
|
258
|
+
self._end(output, run_id=run_id)
|
|
259
|
+
|
|
260
|
+
def on_tool_error(
|
|
261
|
+
self,
|
|
262
|
+
error: BaseException,
|
|
263
|
+
*,
|
|
264
|
+
run_id: UUID,
|
|
265
|
+
parent_run_id: UUID | None = None,
|
|
266
|
+
**kwargs: Any,
|
|
267
|
+
) -> Any:
|
|
268
|
+
self._error(error, run_id=run_id)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dis
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from functools import partial
|
|
7
|
+
from types import CodeType, FunctionType
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from coco_core.runnables import (
|
|
11
|
+
Runnable,
|
|
12
|
+
RunnableLambda,
|
|
13
|
+
RunnableParallel,
|
|
14
|
+
RunnableSequence,
|
|
15
|
+
)
|
|
16
|
+
from coco_core.runnables.base import RunnableBindingBase
|
|
17
|
+
from coco_core.runnables.config import run_in_executor
|
|
18
|
+
from coco_graph.checkpoint.base import ChannelVersions
|
|
19
|
+
|
|
20
|
+
from coco_graph._internal._runnable import RunnableCallable, RunnableSeq
|
|
21
|
+
from coco_graph._internal._timeout import sync_timeout_unsupported
|
|
22
|
+
from coco_graph.pregel.protocol import PregelProtocol
|
|
23
|
+
|
|
24
|
+
_SEQUENCE_TYPES = (RunnableSeq, RunnableSequence)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_new_channel_versions(
|
|
28
|
+
previous_versions: ChannelVersions, current_versions: ChannelVersions
|
|
29
|
+
) -> ChannelVersions:
|
|
30
|
+
"""Get subset of current_versions that are newer than previous_versions."""
|
|
31
|
+
if previous_versions:
|
|
32
|
+
version_type = type(next(iter(current_versions.values()), None))
|
|
33
|
+
null_version = version_type() # type: ignore[misc]
|
|
34
|
+
new_versions = {
|
|
35
|
+
k: v
|
|
36
|
+
for k, v in current_versions.items()
|
|
37
|
+
if v > previous_versions.get(k, null_version) # type: ignore[operator]
|
|
38
|
+
}
|
|
39
|
+
else:
|
|
40
|
+
new_versions = current_versions
|
|
41
|
+
|
|
42
|
+
return new_versions
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
|
|
46
|
+
from coco_graph.pregel import Pregel
|
|
47
|
+
|
|
48
|
+
candidates: list[Runnable] = [candidate]
|
|
49
|
+
|
|
50
|
+
for c in candidates:
|
|
51
|
+
if (
|
|
52
|
+
isinstance(c, PregelProtocol)
|
|
53
|
+
# subgraphs that disabled checkpointing are not considered
|
|
54
|
+
and (not isinstance(c, Pregel) or c.checkpointer is not False)
|
|
55
|
+
):
|
|
56
|
+
return c
|
|
57
|
+
elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
|
|
58
|
+
candidates.extend(c.steps)
|
|
59
|
+
elif isinstance(c, RunnableLambda):
|
|
60
|
+
candidates.extend(c.deps)
|
|
61
|
+
elif isinstance(c, RunnableCallable):
|
|
62
|
+
if c.func is not None:
|
|
63
|
+
candidates.extend(
|
|
64
|
+
nl.__self__ if hasattr(nl, "__self__") else nl
|
|
65
|
+
for nl in get_function_nonlocals(c.func)
|
|
66
|
+
)
|
|
67
|
+
elif c.afunc is not None:
|
|
68
|
+
candidates.extend(
|
|
69
|
+
nl.__self__ if hasattr(nl, "__self__") else nl
|
|
70
|
+
for nl in get_function_nonlocals(c.afunc)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _sequence_steps(runnable: Runnable) -> Sequence[Runnable] | None:
|
|
77
|
+
if isinstance(runnable, _SEQUENCE_TYPES):
|
|
78
|
+
return runnable.steps
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _parallel_steps(runnable: Runnable) -> Sequence[Runnable] | None:
|
|
83
|
+
if isinstance(runnable, RunnableParallel):
|
|
84
|
+
return tuple(runnable.steps__.values())
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _has_method_override(runnable: Runnable, method_name: str) -> bool:
|
|
89
|
+
method = getattr(type(runnable), method_name, None)
|
|
90
|
+
return method is not None and method is not getattr(Runnable, method_name)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_executor_backed_afunc(afunc: Callable[..., Any] | None) -> bool:
|
|
94
|
+
return isinstance(afunc, partial) and afunc.func is run_in_executor
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _has_native_async(runnable: Runnable) -> bool:
|
|
98
|
+
if isinstance(runnable, RunnableCallable):
|
|
99
|
+
return runnable.afunc is not None and not _is_executor_backed_afunc(
|
|
100
|
+
runnable.afunc
|
|
101
|
+
)
|
|
102
|
+
if isinstance(runnable, RunnableLambda):
|
|
103
|
+
return bool(getattr(runnable, "afunc", False))
|
|
104
|
+
return _has_method_override(runnable, "ainvoke")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _runnable_has_native_async(runnable: Runnable) -> bool:
|
|
108
|
+
"""Return whether a runnable can be idle-timed without known sync code.
|
|
109
|
+
|
|
110
|
+
For custom runnable subclasses, an `ainvoke` override is treated as the
|
|
111
|
+
async contract. We do not introspect whether that implementation delegates
|
|
112
|
+
to blocking work internally — e.g. a subclass whose `ainvoke` calls
|
|
113
|
+
`asyncio.to_thread(self.invoke, ...)` will pass this check but the wrapped
|
|
114
|
+
sync work is still uncancellable. Idle-timeout enforcement on such a
|
|
115
|
+
runnable will fire `NodeTimeoutError` correctly, but the background thread
|
|
116
|
+
will keep running until its sync work returns.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
while isinstance(runnable, RunnableBindingBase):
|
|
120
|
+
runnable = runnable.bound
|
|
121
|
+
steps = _sequence_steps(runnable)
|
|
122
|
+
if steps is None:
|
|
123
|
+
steps = _parallel_steps(runnable)
|
|
124
|
+
if steps is not None:
|
|
125
|
+
return all(_runnable_has_native_async(step) for step in steps)
|
|
126
|
+
# Raw callables and the common composition wrappers created by graph
|
|
127
|
+
# builders fall through here. We do not exhaustively unwrap every Runnable
|
|
128
|
+
# wrapper — wrappers that provide `ainvoke` are treated as owning the async
|
|
129
|
+
# contract.
|
|
130
|
+
return _has_native_async(runnable)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_timeout_supported(runnable: Runnable, *, name: str) -> None:
|
|
134
|
+
if not _runnable_has_native_async(runnable):
|
|
135
|
+
raise sync_timeout_unsupported(name)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Values treated as dead ends when deciding whether to walk a function's
|
|
139
|
+
# bytecode. A container can hold a graph, but `find_subgraph_pregel` does not
|
|
140
|
+
# look inside one, so skipping it costs nothing while that holds. Matched by
|
|
141
|
+
# exact type, since a subclass of a builtin can carry attributes.
|
|
142
|
+
_LEAF_TYPES = frozenset(
|
|
143
|
+
{
|
|
144
|
+
int,
|
|
145
|
+
float,
|
|
146
|
+
complex,
|
|
147
|
+
bool,
|
|
148
|
+
str,
|
|
149
|
+
bytes,
|
|
150
|
+
bytearray,
|
|
151
|
+
list,
|
|
152
|
+
tuple,
|
|
153
|
+
dict,
|
|
154
|
+
set,
|
|
155
|
+
frozenset,
|
|
156
|
+
type(None),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_function_nonlocals(func: Callable) -> list[Any]:
|
|
162
|
+
"""Get the values a function reaches from outside its own scope.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
func: The function to check.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Every captured cell value, the globals the function names, and each
|
|
169
|
+
value along an attribute path it loads. Over-approximates: a value can
|
|
170
|
+
come back without the function reaching it at runtime.
|
|
171
|
+
"""
|
|
172
|
+
func = getattr(func, "__func__", func) # bound method -> function
|
|
173
|
+
wrapped = getattr(func, "__wrapped__", None)
|
|
174
|
+
if callable(wrapped):
|
|
175
|
+
func = getattr(wrapped, "__func__", wrapped)
|
|
176
|
+
if not isinstance(func, FunctionType):
|
|
177
|
+
return []
|
|
178
|
+
code = func.__code__
|
|
179
|
+
|
|
180
|
+
cells: dict[str, Any] = {}
|
|
181
|
+
for name, cell in zip(code.co_freevars, func.__closure__ or ()):
|
|
182
|
+
try:
|
|
183
|
+
cells[name] = cell.cell_contents
|
|
184
|
+
except ValueError:
|
|
185
|
+
continue # empty cell: a recursive def not yet bound
|
|
186
|
+
|
|
187
|
+
# Every captured value counts, referenced or not: over-declaring costs an
|
|
188
|
+
# introspection entry, under-declaring drops the subgraph's checkpoints and
|
|
189
|
+
# stream events. Checking each cell against the bytecode would cost more and
|
|
190
|
+
# only trade the cheap error for the expensive one.
|
|
191
|
+
values: list[Any] = list(cells.values())
|
|
192
|
+
global_ns = func.__globals__
|
|
193
|
+
globals_ = {name: global_ns[name] for name in code.co_names if name in global_ns}
|
|
194
|
+
if all(type(v) in _LEAF_TYPES for v in (*cells.values(), *globals_.values())):
|
|
195
|
+
return values
|
|
196
|
+
|
|
197
|
+
# Nested code objects hold the references made by inner defs, lambdas and
|
|
198
|
+
# comprehensions, which resolve against the namespaces gathered above.
|
|
199
|
+
codes = [code]
|
|
200
|
+
for c in codes:
|
|
201
|
+
codes.extend(k for k in c.co_consts if isinstance(k, CodeType))
|
|
202
|
+
value: Any = None
|
|
203
|
+
for instruction in dis.get_instructions(c):
|
|
204
|
+
opname = instruction.opname
|
|
205
|
+
if opname == "LOAD_GLOBAL":
|
|
206
|
+
value = globals_.get(instruction.argval)
|
|
207
|
+
elif opname == "LOAD_DEREF":
|
|
208
|
+
value = cells.get(instruction.argval)
|
|
209
|
+
elif opname in ("LOAD_ATTR", "LOAD_METHOD"):
|
|
210
|
+
value = getattr(value, instruction.argval, None)
|
|
211
|
+
else:
|
|
212
|
+
value = None # anything else ends the chain: `a, b.c` is not `a.c`
|
|
213
|
+
continue
|
|
214
|
+
if value is not None:
|
|
215
|
+
values.append(value)
|
|
216
|
+
return values
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def is_xxh3_128_hexdigest(value: str) -> bool:
|
|
220
|
+
"""Check if the given string matches the format of xxh3_128_hexdigest."""
|
|
221
|
+
return bool(re.fullmatch(r"[0-9a-f]{32}", value))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from coco_graph._internal._constants import RESERVED
|
|
7
|
+
from coco_graph.channels.base import BaseChannel
|
|
8
|
+
from coco_graph.managed.base import ManagedValueMapping
|
|
9
|
+
from coco_graph.pregel._read import PregelNode
|
|
10
|
+
from coco_graph.types import All
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_graph(
|
|
14
|
+
nodes: Mapping[str, PregelNode],
|
|
15
|
+
channels: dict[str, BaseChannel],
|
|
16
|
+
managed: ManagedValueMapping,
|
|
17
|
+
input_channels: str | Sequence[str],
|
|
18
|
+
output_channels: str | Sequence[str],
|
|
19
|
+
stream_channels: str | Sequence[str] | None,
|
|
20
|
+
interrupt_after_nodes: All | Sequence[str],
|
|
21
|
+
interrupt_before_nodes: All | Sequence[str],
|
|
22
|
+
) -> None:
|
|
23
|
+
for chan in channels:
|
|
24
|
+
if chan in RESERVED:
|
|
25
|
+
raise ValueError(f"Channel name '{chan}' is reserved")
|
|
26
|
+
for name in managed:
|
|
27
|
+
if name in RESERVED:
|
|
28
|
+
raise ValueError(f"Managed name '{name}' is reserved")
|
|
29
|
+
|
|
30
|
+
subscribed_channels = set[str]()
|
|
31
|
+
for name, node in nodes.items():
|
|
32
|
+
if name in RESERVED:
|
|
33
|
+
raise ValueError(f"Node name '{name}' is reserved")
|
|
34
|
+
if isinstance(node, PregelNode):
|
|
35
|
+
subscribed_channels.update(node.triggers)
|
|
36
|
+
if isinstance(node.channels, str):
|
|
37
|
+
if node.channels not in channels:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Node {name} reads channel '{node.channels}' "
|
|
40
|
+
f"not in known channels: '{repr(sorted(channels))[:100]}'"
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
for chan in node.channels:
|
|
44
|
+
if chan not in channels and chan not in managed:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Node {name} reads channel '{chan}' "
|
|
47
|
+
f"not in known channels: '{repr(sorted(channels))[:100]}'"
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
raise TypeError(
|
|
51
|
+
f"Invalid node type {type(node)}, expected PregelNode or NodeBuilder"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
for chan in subscribed_channels:
|
|
55
|
+
if chan not in channels:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Subscribed channel '{chan}' not "
|
|
58
|
+
f"in known channels: '{repr(sorted(channels))[:100]}'"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if isinstance(input_channels, str):
|
|
62
|
+
if input_channels not in channels:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Input channel '{input_channels}' not "
|
|
65
|
+
f"in known channels: '{repr(sorted(channels))[:100]}'"
|
|
66
|
+
)
|
|
67
|
+
if input_channels not in subscribed_channels:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Input channel {input_channels} is not subscribed to by any node"
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
for chan in input_channels:
|
|
73
|
+
if chan not in channels:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Input channel '{chan}' not in '{repr(sorted(channels))[:100]}'"
|
|
76
|
+
)
|
|
77
|
+
if all(chan not in subscribed_channels for chan in input_channels):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"None of the input channels {input_channels} "
|
|
80
|
+
f"are subscribed to by any node"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
all_output_channels = set[str]()
|
|
84
|
+
if isinstance(output_channels, str):
|
|
85
|
+
all_output_channels.add(output_channels)
|
|
86
|
+
else:
|
|
87
|
+
all_output_channels.update(output_channels)
|
|
88
|
+
if isinstance(stream_channels, str):
|
|
89
|
+
all_output_channels.add(stream_channels)
|
|
90
|
+
elif stream_channels is not None:
|
|
91
|
+
all_output_channels.update(stream_channels)
|
|
92
|
+
|
|
93
|
+
for chan in all_output_channels:
|
|
94
|
+
if chan not in channels:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Output channel '{chan}' not "
|
|
97
|
+
f"in known channels: '{repr(sorted(channels))[:100]}'"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if interrupt_after_nodes != "*":
|
|
101
|
+
for n in interrupt_after_nodes:
|
|
102
|
+
if n not in nodes:
|
|
103
|
+
raise ValueError(f"Node {n} not in nodes")
|
|
104
|
+
if interrupt_before_nodes != "*":
|
|
105
|
+
for n in interrupt_before_nodes:
|
|
106
|
+
if n not in nodes:
|
|
107
|
+
raise ValueError(f"Node {n} not in nodes")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_keys(
|
|
111
|
+
keys: str | Sequence[str] | None,
|
|
112
|
+
channels: Mapping[str, Any],
|
|
113
|
+
) -> None:
|
|
114
|
+
if isinstance(keys, str):
|
|
115
|
+
if keys not in channels:
|
|
116
|
+
raise ValueError(f"Key {keys} not in channels")
|
|
117
|
+
elif keys is not None:
|
|
118
|
+
for chan in keys:
|
|
119
|
+
if chan not in channels:
|
|
120
|
+
raise ValueError(f"Key {chan} not in channels")
|