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,294 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import Any, NamedTuple, cast
|
|
6
|
+
|
|
7
|
+
from coco_core.runnables.config import RunnableConfig
|
|
8
|
+
from coco_core.runnables.graph import Graph, Node
|
|
9
|
+
from coco_graph.checkpoint.base import BaseCheckpointSaver
|
|
10
|
+
|
|
11
|
+
from coco_graph._internal._constants import CONF, CONFIG_KEY_SEND, INPUT
|
|
12
|
+
from coco_graph.channels.base import BaseChannel
|
|
13
|
+
from coco_graph.channels.last_value import LastValueAfterFinish
|
|
14
|
+
from coco_graph.constants import END, START
|
|
15
|
+
from coco_graph.managed.base import ManagedValueSpec
|
|
16
|
+
from coco_graph.pregel._algo import (
|
|
17
|
+
PregelTaskWrites,
|
|
18
|
+
apply_writes,
|
|
19
|
+
increment,
|
|
20
|
+
prepare_next_tasks,
|
|
21
|
+
)
|
|
22
|
+
from coco_graph.pregel._checkpoint import channels_from_checkpoint, empty_checkpoint
|
|
23
|
+
from coco_graph.pregel._io import map_input
|
|
24
|
+
from coco_graph.pregel._read import PregelNode
|
|
25
|
+
from coco_graph.pregel._write import ChannelWrite
|
|
26
|
+
from coco_graph.types import All, Checkpointer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Edge(NamedTuple):
|
|
30
|
+
source: str
|
|
31
|
+
target: str
|
|
32
|
+
conditional: bool
|
|
33
|
+
data: str | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TriggerEdge(NamedTuple):
|
|
37
|
+
source: str
|
|
38
|
+
conditional: bool
|
|
39
|
+
data: str | None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def draw_graph(
|
|
43
|
+
config: RunnableConfig,
|
|
44
|
+
*,
|
|
45
|
+
nodes: dict[str, PregelNode],
|
|
46
|
+
specs: dict[str, BaseChannel | ManagedValueSpec],
|
|
47
|
+
input_channels: str | Sequence[str],
|
|
48
|
+
interrupt_after_nodes: All | Sequence[str],
|
|
49
|
+
interrupt_before_nodes: All | Sequence[str],
|
|
50
|
+
trigger_to_nodes: Mapping[str, Sequence[str]],
|
|
51
|
+
checkpointer: Checkpointer,
|
|
52
|
+
subgraphs: dict[str, Graph],
|
|
53
|
+
limit: int = 250,
|
|
54
|
+
) -> Graph:
|
|
55
|
+
"""Get the graph for this Pregel instance.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
config: The configuration to use for the graph.
|
|
59
|
+
subgraphs: The subgraphs to include in the graph.
|
|
60
|
+
checkpointer: The checkpointer to use for the graph.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The graph for this Pregel instance.
|
|
64
|
+
"""
|
|
65
|
+
# (src, dest, is_conditional, label)
|
|
66
|
+
edges: set[Edge] = set()
|
|
67
|
+
|
|
68
|
+
step = -1
|
|
69
|
+
checkpoint = empty_checkpoint()
|
|
70
|
+
get_next_version = (
|
|
71
|
+
checkpointer.get_next_version
|
|
72
|
+
if isinstance(checkpointer, BaseCheckpointSaver)
|
|
73
|
+
else increment
|
|
74
|
+
)
|
|
75
|
+
channels, managed = channels_from_checkpoint(
|
|
76
|
+
specs,
|
|
77
|
+
checkpoint,
|
|
78
|
+
)
|
|
79
|
+
static_seen: set[Any] = set()
|
|
80
|
+
sources: dict[str, set[TriggerEdge]] = {}
|
|
81
|
+
step_sources: dict[str, set[TriggerEdge]] = {}
|
|
82
|
+
static_declared_writes: dict[str, set[TriggerEdge]] = defaultdict(set)
|
|
83
|
+
# remove node mappers
|
|
84
|
+
nodes = {
|
|
85
|
+
k: v.copy(update={"mapper": None}) if v.mapper is not None else v
|
|
86
|
+
for k, v in nodes.items()
|
|
87
|
+
}
|
|
88
|
+
# apply input writes
|
|
89
|
+
input_writes = list(map_input(input_channels, {}))
|
|
90
|
+
updated_channels = apply_writes(
|
|
91
|
+
checkpoint,
|
|
92
|
+
channels,
|
|
93
|
+
[
|
|
94
|
+
PregelTaskWrites((), INPUT, input_writes, []),
|
|
95
|
+
],
|
|
96
|
+
get_next_version,
|
|
97
|
+
trigger_to_nodes,
|
|
98
|
+
)
|
|
99
|
+
# prepare first tasks
|
|
100
|
+
tasks = prepare_next_tasks(
|
|
101
|
+
checkpoint,
|
|
102
|
+
[],
|
|
103
|
+
nodes,
|
|
104
|
+
channels,
|
|
105
|
+
managed,
|
|
106
|
+
config,
|
|
107
|
+
step,
|
|
108
|
+
-1,
|
|
109
|
+
for_execution=True,
|
|
110
|
+
store=None,
|
|
111
|
+
checkpointer=None,
|
|
112
|
+
manager=None,
|
|
113
|
+
trigger_to_nodes=trigger_to_nodes,
|
|
114
|
+
updated_channels=updated_channels,
|
|
115
|
+
)
|
|
116
|
+
start_tasks = tasks
|
|
117
|
+
# run the pregel loop
|
|
118
|
+
for step in range(step, limit):
|
|
119
|
+
if not tasks:
|
|
120
|
+
break
|
|
121
|
+
conditionals: dict[tuple[str, str, Any], str | None] = {}
|
|
122
|
+
# run task writers
|
|
123
|
+
for task in tasks.values():
|
|
124
|
+
for w in task.writers:
|
|
125
|
+
# apply regular writes
|
|
126
|
+
if isinstance(w, ChannelWrite):
|
|
127
|
+
empty_input = (
|
|
128
|
+
cast(BaseChannel, specs["__root__"]).ValueType()
|
|
129
|
+
if "__root__" in specs
|
|
130
|
+
else None
|
|
131
|
+
)
|
|
132
|
+
w.invoke(empty_input, task.config)
|
|
133
|
+
# apply conditional writes declared for static analysis, only once
|
|
134
|
+
if w not in static_seen:
|
|
135
|
+
static_seen.add(w)
|
|
136
|
+
# apply static writes
|
|
137
|
+
if writes := ChannelWrite.get_static_writes(w):
|
|
138
|
+
# END writes are not written, but become edges directly
|
|
139
|
+
for t in writes:
|
|
140
|
+
if t[0] == END:
|
|
141
|
+
edges.add(Edge(task.name, t[0], True, t[2]))
|
|
142
|
+
writes = [t for t in writes if t[0] != END]
|
|
143
|
+
conditionals.update(
|
|
144
|
+
{(task.name, t[0], t[1] or None): t[2] for t in writes}
|
|
145
|
+
)
|
|
146
|
+
# record static writes for edge creation
|
|
147
|
+
for t in writes:
|
|
148
|
+
static_declared_writes[task.name].add(
|
|
149
|
+
TriggerEdge(t[0], True, t[2])
|
|
150
|
+
)
|
|
151
|
+
task.config[CONF][CONFIG_KEY_SEND]([t[:2] for t in writes])
|
|
152
|
+
# collect sources
|
|
153
|
+
step_sources = {}
|
|
154
|
+
for task in tasks.values():
|
|
155
|
+
task_edges = {
|
|
156
|
+
TriggerEdge(
|
|
157
|
+
w[0],
|
|
158
|
+
(task.name, w[0], w[1] or None) in conditionals,
|
|
159
|
+
conditionals.get((task.name, w[0], w[1] or None)),
|
|
160
|
+
)
|
|
161
|
+
for w in task.writes
|
|
162
|
+
}
|
|
163
|
+
task_edges |= static_declared_writes.get(task.name, set())
|
|
164
|
+
step_sources[task.name] = task_edges
|
|
165
|
+
sources.update(step_sources)
|
|
166
|
+
# invert triggers
|
|
167
|
+
trigger_to_sources: dict[str, set[TriggerEdge]] = defaultdict(set)
|
|
168
|
+
for src, triggers in sources.items():
|
|
169
|
+
for trigger, cond, label in triggers:
|
|
170
|
+
trigger_to_sources[trigger].add(TriggerEdge(src, cond, label))
|
|
171
|
+
# apply writes
|
|
172
|
+
updated_channels = apply_writes(
|
|
173
|
+
checkpoint, channels, tasks.values(), get_next_version, trigger_to_nodes
|
|
174
|
+
)
|
|
175
|
+
# prepare next tasks
|
|
176
|
+
tasks = prepare_next_tasks(
|
|
177
|
+
checkpoint,
|
|
178
|
+
[],
|
|
179
|
+
nodes,
|
|
180
|
+
channels,
|
|
181
|
+
managed,
|
|
182
|
+
config,
|
|
183
|
+
step,
|
|
184
|
+
limit,
|
|
185
|
+
for_execution=True,
|
|
186
|
+
store=None,
|
|
187
|
+
checkpointer=None,
|
|
188
|
+
manager=None,
|
|
189
|
+
trigger_to_nodes=trigger_to_nodes,
|
|
190
|
+
updated_channels=updated_channels,
|
|
191
|
+
)
|
|
192
|
+
# collect deferred nodes
|
|
193
|
+
deferred_nodes: set[str] = set()
|
|
194
|
+
edges_to_deferred_nodes: set[Edge] = set()
|
|
195
|
+
for channel, item in channels.items():
|
|
196
|
+
if isinstance(item, LastValueAfterFinish):
|
|
197
|
+
deferred_node = channel.split(":", 2)[-1]
|
|
198
|
+
deferred_nodes.add(deferred_node)
|
|
199
|
+
# collect edges
|
|
200
|
+
for task in tasks.values():
|
|
201
|
+
added = False
|
|
202
|
+
for trigger in task.triggers:
|
|
203
|
+
for src, cond, label in sorted(trigger_to_sources[trigger]):
|
|
204
|
+
# record edge to be reviewed later
|
|
205
|
+
if task.name in deferred_nodes:
|
|
206
|
+
edges_to_deferred_nodes.add(Edge(src, task.name, cond, label))
|
|
207
|
+
edges.add(Edge(src, task.name, cond, label))
|
|
208
|
+
# if the edge is from this step, skip adding the implicit edges
|
|
209
|
+
if (trigger, cond, label) in step_sources.get(src, set()):
|
|
210
|
+
added = True
|
|
211
|
+
else:
|
|
212
|
+
sources[src].discard(TriggerEdge(trigger, cond, label))
|
|
213
|
+
# if no edges from this step, add implicit edges from all previous tasks
|
|
214
|
+
if not added:
|
|
215
|
+
for src in step_sources:
|
|
216
|
+
edges.add(Edge(src, task.name, True, None))
|
|
217
|
+
|
|
218
|
+
# assemble the graph
|
|
219
|
+
graph = Graph()
|
|
220
|
+
# add nodes
|
|
221
|
+
for name, node in nodes.items():
|
|
222
|
+
metadata = dict(node.metadata or {})
|
|
223
|
+
if name in deferred_nodes:
|
|
224
|
+
metadata["defer"] = True
|
|
225
|
+
if name in interrupt_before_nodes and name in interrupt_after_nodes:
|
|
226
|
+
metadata["__interrupt"] = "before,after"
|
|
227
|
+
elif name in interrupt_before_nodes:
|
|
228
|
+
metadata["__interrupt"] = "before"
|
|
229
|
+
elif name in interrupt_after_nodes:
|
|
230
|
+
metadata["__interrupt"] = "after"
|
|
231
|
+
graph.add_node(node.bound, name, metadata=metadata or None)
|
|
232
|
+
# add start node
|
|
233
|
+
if START not in nodes:
|
|
234
|
+
graph.add_node(None, START)
|
|
235
|
+
for task in start_tasks.values():
|
|
236
|
+
add_edge(graph, START, task.name)
|
|
237
|
+
# add discovered edges
|
|
238
|
+
for src, dest, is_conditional, label in sorted(edges):
|
|
239
|
+
add_edge(
|
|
240
|
+
graph,
|
|
241
|
+
src,
|
|
242
|
+
dest,
|
|
243
|
+
data=label if label != dest else None,
|
|
244
|
+
conditional=is_conditional,
|
|
245
|
+
)
|
|
246
|
+
# add end edges
|
|
247
|
+
termini = {d for _, d, _, _ in edges if d != END}.difference(
|
|
248
|
+
s for s, _, _, _ in edges
|
|
249
|
+
)
|
|
250
|
+
end_edge_exists = any(d == END for _, d, _, _ in edges)
|
|
251
|
+
if termini:
|
|
252
|
+
for src in sorted(termini):
|
|
253
|
+
add_edge(graph, src, END)
|
|
254
|
+
elif len(step_sources) == 1 and not end_edge_exists:
|
|
255
|
+
for src in sorted(step_sources):
|
|
256
|
+
add_edge(graph, src, END, conditional=True)
|
|
257
|
+
# replace subgraphs
|
|
258
|
+
for name, subgraph in subgraphs.items():
|
|
259
|
+
if (
|
|
260
|
+
len(subgraph.nodes) > 1
|
|
261
|
+
and name in graph.nodes
|
|
262
|
+
and subgraph.first_node()
|
|
263
|
+
and subgraph.last_node()
|
|
264
|
+
):
|
|
265
|
+
subgraph.trim_first_node()
|
|
266
|
+
subgraph.trim_last_node()
|
|
267
|
+
# replace the node with the subgraph
|
|
268
|
+
graph.nodes.pop(name)
|
|
269
|
+
first, last = graph.extend(subgraph, prefix=name)
|
|
270
|
+
for idx, edge in enumerate(graph.edges):
|
|
271
|
+
if edge.source == name:
|
|
272
|
+
edge = edge.copy(source=cast(Node, last).id)
|
|
273
|
+
if edge.target == name:
|
|
274
|
+
edge = edge.copy(target=cast(Node, first).id)
|
|
275
|
+
graph.edges[idx] = edge
|
|
276
|
+
|
|
277
|
+
return graph
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def add_edge(
|
|
281
|
+
graph: Graph,
|
|
282
|
+
source: str,
|
|
283
|
+
target: str,
|
|
284
|
+
*,
|
|
285
|
+
data: Any | None = None,
|
|
286
|
+
conditional: bool = False,
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Add an edge to the graph."""
|
|
289
|
+
for edge in graph.edges:
|
|
290
|
+
if edge.source == source and edge.target == target:
|
|
291
|
+
return
|
|
292
|
+
if target not in graph.nodes and target == END:
|
|
293
|
+
graph.add_node(None, END)
|
|
294
|
+
graph.add_edge(graph.nodes[source], graph.nodes[target], data, conditional)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
7
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
|
|
8
|
+
from contextvars import copy_context
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import (
|
|
11
|
+
Protocol,
|
|
12
|
+
TypeVar,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from coco_core.runnables import RunnableConfig
|
|
17
|
+
from coco_core.runnables.config import get_executor_for_config
|
|
18
|
+
from typing_extensions import ParamSpec
|
|
19
|
+
|
|
20
|
+
from coco_graph._internal._future import CONTEXT_NOT_SUPPORTED, run_coroutine_threadsafe
|
|
21
|
+
from coco_graph.errors import GraphBubbleUp
|
|
22
|
+
|
|
23
|
+
P = ParamSpec("P")
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Submit(Protocol[P, T]):
|
|
28
|
+
def __call__( # type: ignore[valid-type]
|
|
29
|
+
self,
|
|
30
|
+
fn: Callable[P, T],
|
|
31
|
+
*args: P.args,
|
|
32
|
+
__name__: str | None = None,
|
|
33
|
+
__cancel_on_exit__: bool = False,
|
|
34
|
+
__reraise_on_exit__: bool = True,
|
|
35
|
+
__next_tick__: bool = False,
|
|
36
|
+
**kwargs: P.kwargs,
|
|
37
|
+
) -> concurrent.futures.Future[T]: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BackgroundExecutor(AbstractContextManager):
|
|
41
|
+
"""A context manager that runs sync tasks in the background.
|
|
42
|
+
Uses a thread pool executor to delegate tasks to separate threads.
|
|
43
|
+
On exit,
|
|
44
|
+
- cancels any (not yet started) tasks with `__cancel_on_exit__=True`
|
|
45
|
+
- waits for all tasks to finish
|
|
46
|
+
- re-raises the first exception from tasks with `__reraise_on_exit__=True`"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, config: RunnableConfig) -> None:
|
|
49
|
+
self.stack = ExitStack()
|
|
50
|
+
self.executor = self.stack.enter_context(get_executor_for_config(config))
|
|
51
|
+
# mapping of Future to (__cancel_on_exit__, __reraise_on_exit__) flags
|
|
52
|
+
self.tasks: dict[concurrent.futures.Future, tuple[bool, bool]] = {}
|
|
53
|
+
|
|
54
|
+
def submit( # type: ignore[valid-type]
|
|
55
|
+
self,
|
|
56
|
+
fn: Callable[P, T],
|
|
57
|
+
*args: P.args,
|
|
58
|
+
__name__: str | None = None, # currently not used in sync version
|
|
59
|
+
__cancel_on_exit__: bool = False, # for sync, can cancel only if not started
|
|
60
|
+
__reraise_on_exit__: bool = True,
|
|
61
|
+
__next_tick__: bool = False,
|
|
62
|
+
**kwargs: P.kwargs,
|
|
63
|
+
) -> concurrent.futures.Future[T]:
|
|
64
|
+
ctx = copy_context()
|
|
65
|
+
if __next_tick__:
|
|
66
|
+
task = cast(
|
|
67
|
+
concurrent.futures.Future[T],
|
|
68
|
+
self.executor.submit(next_tick, ctx.run, fn, *args, **kwargs), # type: ignore[arg-type]
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
task = self.executor.submit(ctx.run, fn, *args, **kwargs)
|
|
72
|
+
self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
|
|
73
|
+
# add a callback to remove the task from the tasks dict when it's done
|
|
74
|
+
task.add_done_callback(self.done)
|
|
75
|
+
return task
|
|
76
|
+
|
|
77
|
+
def done(self, task: concurrent.futures.Future) -> None:
|
|
78
|
+
"""Remove the task from the tasks dict when it's done."""
|
|
79
|
+
try:
|
|
80
|
+
task.result()
|
|
81
|
+
except GraphBubbleUp:
|
|
82
|
+
# This exception is an interruption signal, not an error
|
|
83
|
+
# so we don't want to re-raise it on exit
|
|
84
|
+
self.tasks.pop(task)
|
|
85
|
+
except BaseException:
|
|
86
|
+
pass
|
|
87
|
+
else:
|
|
88
|
+
self.tasks.pop(task)
|
|
89
|
+
|
|
90
|
+
def __enter__(self) -> Submit:
|
|
91
|
+
return self.submit
|
|
92
|
+
|
|
93
|
+
def __exit__(
|
|
94
|
+
self,
|
|
95
|
+
exc_type: type[BaseException] | None,
|
|
96
|
+
exc_value: BaseException | None,
|
|
97
|
+
traceback: TracebackType | None,
|
|
98
|
+
) -> bool | None:
|
|
99
|
+
# copy the tasks as done() callback may modify the dict
|
|
100
|
+
tasks = self.tasks.copy()
|
|
101
|
+
# cancel all tasks that should be cancelled
|
|
102
|
+
for task, (cancel, _) in tasks.items():
|
|
103
|
+
if cancel:
|
|
104
|
+
task.cancel()
|
|
105
|
+
# wait for all tasks to finish
|
|
106
|
+
if pending := {t for t in tasks if not t.done()}:
|
|
107
|
+
concurrent.futures.wait(pending)
|
|
108
|
+
# shutdown the executor
|
|
109
|
+
self.stack.__exit__(exc_type, exc_value, traceback)
|
|
110
|
+
# if there's already an exception being raised, don't raise another one
|
|
111
|
+
if exc_type is None:
|
|
112
|
+
# re-raise the first exception that occurred in a task
|
|
113
|
+
for task, (_, reraise) in tasks.items():
|
|
114
|
+
if not reraise:
|
|
115
|
+
continue
|
|
116
|
+
try:
|
|
117
|
+
task.result()
|
|
118
|
+
except concurrent.futures.CancelledError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AsyncBackgroundExecutor(AbstractAsyncContextManager):
|
|
123
|
+
"""A context manager that runs async tasks in the background.
|
|
124
|
+
Uses the current event loop to delegate tasks to asyncio tasks.
|
|
125
|
+
On exit,
|
|
126
|
+
- cancels any tasks with `__cancel_on_exit__=True`
|
|
127
|
+
- waits for all tasks to finish
|
|
128
|
+
- re-raises the first exception from tasks with `__reraise_on_exit__=True`
|
|
129
|
+
ignoring CancelledError"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, config: RunnableConfig) -> None:
|
|
132
|
+
self.tasks: dict[asyncio.Future, tuple[bool, bool]] = {}
|
|
133
|
+
self.sentinel = object()
|
|
134
|
+
self.loop = asyncio.get_running_loop()
|
|
135
|
+
if max_concurrency := config.get("max_concurrency"):
|
|
136
|
+
self.semaphore: asyncio.Semaphore | None = asyncio.Semaphore(
|
|
137
|
+
max_concurrency
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
self.semaphore = None
|
|
141
|
+
|
|
142
|
+
def submit( # type: ignore[valid-type]
|
|
143
|
+
self,
|
|
144
|
+
fn: Callable[P, Awaitable[T]],
|
|
145
|
+
*args: P.args,
|
|
146
|
+
__name__: str | None = None,
|
|
147
|
+
__cancel_on_exit__: bool = False,
|
|
148
|
+
__reraise_on_exit__: bool = True,
|
|
149
|
+
__next_tick__: bool = False, # noop in async (always True)
|
|
150
|
+
**kwargs: P.kwargs,
|
|
151
|
+
) -> asyncio.Future[T]:
|
|
152
|
+
coro = cast(Coroutine[None, None, T], fn(*args, **kwargs))
|
|
153
|
+
if self.semaphore:
|
|
154
|
+
coro = gated(self.semaphore, coro)
|
|
155
|
+
if CONTEXT_NOT_SUPPORTED:
|
|
156
|
+
task = run_coroutine_threadsafe(
|
|
157
|
+
coro, self.loop, name=__name__, lazy=__next_tick__
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
task = run_coroutine_threadsafe(
|
|
161
|
+
coro,
|
|
162
|
+
self.loop,
|
|
163
|
+
name=__name__,
|
|
164
|
+
context=copy_context(),
|
|
165
|
+
lazy=__next_tick__,
|
|
166
|
+
)
|
|
167
|
+
self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
|
|
168
|
+
task.add_done_callback(self.done)
|
|
169
|
+
return task
|
|
170
|
+
|
|
171
|
+
def done(self, task: asyncio.Future) -> None:
|
|
172
|
+
try:
|
|
173
|
+
if exc := task.exception():
|
|
174
|
+
# This exception is an interruption signal, not an error
|
|
175
|
+
# so we don't want to re-raise it on exit
|
|
176
|
+
if isinstance(exc, GraphBubbleUp):
|
|
177
|
+
self.tasks.pop(task)
|
|
178
|
+
else:
|
|
179
|
+
self.tasks.pop(task)
|
|
180
|
+
except asyncio.CancelledError:
|
|
181
|
+
self.tasks.pop(task)
|
|
182
|
+
|
|
183
|
+
async def __aenter__(self) -> Submit:
|
|
184
|
+
return self.submit
|
|
185
|
+
|
|
186
|
+
async def __aexit__(
|
|
187
|
+
self,
|
|
188
|
+
exc_type: type[BaseException] | None,
|
|
189
|
+
exc_value: BaseException | None,
|
|
190
|
+
traceback: TracebackType | None,
|
|
191
|
+
) -> None:
|
|
192
|
+
# copy the tasks as done() callback may modify the dict
|
|
193
|
+
tasks = self.tasks.copy()
|
|
194
|
+
# cancel all tasks that should be cancelled
|
|
195
|
+
for task, (cancel, _) in tasks.items():
|
|
196
|
+
if cancel:
|
|
197
|
+
task.cancel(self.sentinel)
|
|
198
|
+
# wait for all tasks to finish
|
|
199
|
+
if tasks:
|
|
200
|
+
await asyncio.wait(tasks)
|
|
201
|
+
# if there's already an exception being raised, don't raise another one
|
|
202
|
+
if exc_type is None:
|
|
203
|
+
# re-raise the first exception that occurred in a task
|
|
204
|
+
for task, (_, reraise) in tasks.items():
|
|
205
|
+
if not reraise:
|
|
206
|
+
continue
|
|
207
|
+
try:
|
|
208
|
+
if exc := task.exception():
|
|
209
|
+
raise exc
|
|
210
|
+
except asyncio.CancelledError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def gated(semaphore: asyncio.Semaphore, coro: Coroutine[None, None, T]) -> T:
|
|
215
|
+
"""A coroutine that waits for a semaphore before running another coroutine."""
|
|
216
|
+
async with semaphore:
|
|
217
|
+
return await coro
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def next_tick(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
221
|
+
"""A function that yields control to other threads before running another function."""
|
|
222
|
+
time.sleep(0)
|
|
223
|
+
return fn(*args, **kwargs)
|
coco_graph/pregel/_io.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from coco_graph._internal._constants import (
|
|
8
|
+
ERROR,
|
|
9
|
+
INTERRUPT,
|
|
10
|
+
NULL_TASK_ID,
|
|
11
|
+
RESUME,
|
|
12
|
+
RETURN,
|
|
13
|
+
TASKS,
|
|
14
|
+
)
|
|
15
|
+
from coco_graph._internal._typing import EMPTY_SEQ, MISSING
|
|
16
|
+
from coco_graph.channels.base import BaseChannel, EmptyChannelError
|
|
17
|
+
from coco_graph.constants import START, TAG_HIDDEN
|
|
18
|
+
from coco_graph.errors import InvalidUpdateError
|
|
19
|
+
from coco_graph.pregel._log import logger
|
|
20
|
+
from coco_graph.types import Command, PregelExecutableTask, Send
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_channel(
|
|
24
|
+
channels: Mapping[str, BaseChannel],
|
|
25
|
+
chan: str,
|
|
26
|
+
*,
|
|
27
|
+
catch: bool = True,
|
|
28
|
+
) -> Any:
|
|
29
|
+
try:
|
|
30
|
+
return channels[chan].get()
|
|
31
|
+
except EmptyChannelError:
|
|
32
|
+
if catch:
|
|
33
|
+
return None
|
|
34
|
+
else:
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_channels(
|
|
39
|
+
channels: Mapping[str, BaseChannel],
|
|
40
|
+
select: Sequence[str] | str,
|
|
41
|
+
*,
|
|
42
|
+
skip_empty: bool = True,
|
|
43
|
+
) -> dict[str, Any] | Any:
|
|
44
|
+
if isinstance(select, str):
|
|
45
|
+
return read_channel(channels, select)
|
|
46
|
+
else:
|
|
47
|
+
values: dict[str, Any] = {}
|
|
48
|
+
for k in select:
|
|
49
|
+
try:
|
|
50
|
+
values[k] = read_channel(channels, k, catch=not skip_empty)
|
|
51
|
+
except EmptyChannelError:
|
|
52
|
+
pass
|
|
53
|
+
return values
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
|
|
57
|
+
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
|
|
58
|
+
if cmd.graph == Command.PARENT:
|
|
59
|
+
raise InvalidUpdateError("There is no parent graph")
|
|
60
|
+
if cmd.goto:
|
|
61
|
+
if isinstance(cmd.goto, (tuple, list)):
|
|
62
|
+
sends = cmd.goto
|
|
63
|
+
else:
|
|
64
|
+
sends = [cmd.goto]
|
|
65
|
+
for send in sends:
|
|
66
|
+
if isinstance(send, Send):
|
|
67
|
+
yield (NULL_TASK_ID, TASKS, send)
|
|
68
|
+
elif isinstance(send, str):
|
|
69
|
+
yield (NULL_TASK_ID, f"branch:to:{send}", START)
|
|
70
|
+
else:
|
|
71
|
+
raise TypeError(
|
|
72
|
+
f"In Command.goto, expected Send/str, got {type(send).__name__}"
|
|
73
|
+
)
|
|
74
|
+
if cmd.resume is not None:
|
|
75
|
+
yield (NULL_TASK_ID, RESUME, cmd.resume)
|
|
76
|
+
if cmd.update:
|
|
77
|
+
for k, v in cmd._update_as_tuples():
|
|
78
|
+
yield (NULL_TASK_ID, k, v)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def map_input(
|
|
82
|
+
input_channels: str | Sequence[str],
|
|
83
|
+
chunk: dict[str, Any] | Any | None,
|
|
84
|
+
) -> Iterator[tuple[str, Any]]:
|
|
85
|
+
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
|
|
86
|
+
if chunk is None:
|
|
87
|
+
return
|
|
88
|
+
elif isinstance(input_channels, str):
|
|
89
|
+
yield (input_channels, chunk)
|
|
90
|
+
else:
|
|
91
|
+
if not isinstance(chunk, dict):
|
|
92
|
+
raise TypeError(f"Expected chunk to be a dict, got {type(chunk).__name__}")
|
|
93
|
+
for k in chunk:
|
|
94
|
+
if k in input_channels:
|
|
95
|
+
yield (k, chunk[k])
|
|
96
|
+
else:
|
|
97
|
+
logger.warning(f"Input channel {k} not found in {input_channels}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def map_output_values(
|
|
101
|
+
output_channels: str | Sequence[str],
|
|
102
|
+
pending_writes: Literal[True] | Sequence[tuple[str, Any]],
|
|
103
|
+
channels: Mapping[str, BaseChannel],
|
|
104
|
+
) -> Iterator[dict[str, Any] | Any]:
|
|
105
|
+
"""Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
|
|
106
|
+
if isinstance(output_channels, str):
|
|
107
|
+
if pending_writes is True or any(
|
|
108
|
+
chan == output_channels for chan, _ in pending_writes
|
|
109
|
+
):
|
|
110
|
+
yield read_channel(channels, output_channels)
|
|
111
|
+
else:
|
|
112
|
+
if pending_writes is True or {
|
|
113
|
+
c for c, _ in pending_writes if c in output_channels
|
|
114
|
+
}:
|
|
115
|
+
yield read_channels(channels, output_channels)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def map_output_updates(
|
|
119
|
+
output_channels: str | Sequence[str],
|
|
120
|
+
tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]],
|
|
121
|
+
cached: bool = False,
|
|
122
|
+
) -> Iterator[dict[str, Any | dict[str, Any]]]:
|
|
123
|
+
"""Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
|
|
124
|
+
output_tasks = [
|
|
125
|
+
(t, ww)
|
|
126
|
+
for t, ww in tasks
|
|
127
|
+
if (not t.config or TAG_HIDDEN not in t.config.get("tags", EMPTY_SEQ))
|
|
128
|
+
and ww[0][0] != ERROR
|
|
129
|
+
and ww[0][0] != INTERRUPT
|
|
130
|
+
]
|
|
131
|
+
if not output_tasks:
|
|
132
|
+
return
|
|
133
|
+
updated: list[tuple[str, Any]] = []
|
|
134
|
+
for task, writes in output_tasks:
|
|
135
|
+
rtn = next((value for chan, value in writes if chan == RETURN), MISSING)
|
|
136
|
+
if rtn is not MISSING:
|
|
137
|
+
updated.append((task.name, rtn))
|
|
138
|
+
elif isinstance(output_channels, str):
|
|
139
|
+
updated.extend(
|
|
140
|
+
(task.name, value) for chan, value in writes if chan == output_channels
|
|
141
|
+
)
|
|
142
|
+
elif any(chan in output_channels for chan, _ in writes):
|
|
143
|
+
counts = Counter(chan for chan, _ in writes)
|
|
144
|
+
if any(counts[chan] > 1 for chan in output_channels):
|
|
145
|
+
updated.extend(
|
|
146
|
+
(
|
|
147
|
+
task.name,
|
|
148
|
+
{chan: value},
|
|
149
|
+
)
|
|
150
|
+
for chan, value in writes
|
|
151
|
+
if chan in output_channels
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
updated.append(
|
|
155
|
+
(
|
|
156
|
+
task.name,
|
|
157
|
+
{
|
|
158
|
+
chan: value
|
|
159
|
+
for chan, value in writes
|
|
160
|
+
if chan in output_channels
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
grouped: dict[str, Any] = {t.name: [] for t, _ in output_tasks}
|
|
165
|
+
for node, value in updated:
|
|
166
|
+
grouped[node].append(value)
|
|
167
|
+
for node, value in grouped.items():
|
|
168
|
+
if len(value) == 0:
|
|
169
|
+
grouped[node] = None
|
|
170
|
+
if len(value) == 1:
|
|
171
|
+
grouped[node] = value[0]
|
|
172
|
+
if cached:
|
|
173
|
+
grouped["__metadata__"] = {"cached": cached}
|
|
174
|
+
yield grouped
|