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,461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
|
|
4
|
+
from dataclasses import fields, is_dataclass
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
TypeVar,
|
|
8
|
+
cast,
|
|
9
|
+
)
|
|
10
|
+
from uuid import UUID, uuid4
|
|
11
|
+
|
|
12
|
+
from coco_core.callbacks import BaseCallbackHandler
|
|
13
|
+
from coco_core.messages import BaseMessage, ToolMessage
|
|
14
|
+
from coco_core.messages.utils import convert_to_messages
|
|
15
|
+
from coco_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from coco_graph._internal._config import filter_to_user_tags
|
|
19
|
+
from coco_graph._internal._constants import NS_SEP
|
|
20
|
+
from coco_graph.constants import TAG_HIDDEN, TAG_NOSTREAM
|
|
21
|
+
from coco_graph.pregel.protocol import StreamChunk
|
|
22
|
+
from coco_graph.types import Command
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from coco_core.tracers._streaming import _StreamingCallbackHandler
|
|
26
|
+
except ImportError:
|
|
27
|
+
_StreamingCallbackHandler = object # type: ignore
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from coco_core.tracers._streaming import _V2StreamingCallbackHandler
|
|
31
|
+
except ImportError:
|
|
32
|
+
_V2StreamingCallbackHandler = object # type: ignore
|
|
33
|
+
|
|
34
|
+
T = TypeVar("T")
|
|
35
|
+
Meta = tuple[tuple[str, ...], dict[str, Any]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _state_values(obj: Any) -> Sequence[Any]:
|
|
39
|
+
"""Extract top-level field values from a state object (dict, BaseModel, or dataclass)."""
|
|
40
|
+
if isinstance(obj, dict):
|
|
41
|
+
return list(obj.values())
|
|
42
|
+
elif isinstance(obj, BaseModel):
|
|
43
|
+
return [getattr(obj, k) for k in type(obj).model_fields]
|
|
44
|
+
elif is_dataclass(obj) and not isinstance(obj, type):
|
|
45
|
+
return [getattr(obj, f.name) for f in fields(obj)]
|
|
46
|
+
return ()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|
50
|
+
"""A callback handler that implements stream_mode=messages.
|
|
51
|
+
|
|
52
|
+
Collects messages from:
|
|
53
|
+
(1) chat model stream events; and
|
|
54
|
+
(2) node outputs.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
run_inline = True
|
|
58
|
+
"""We want this callback to run in the main thread to avoid order/locking issues."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
stream: Callable[[StreamChunk], None],
|
|
63
|
+
subgraphs: bool,
|
|
64
|
+
*,
|
|
65
|
+
parent_ns: tuple[str, ...] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Configure the handler to stream messages from LLMs and nodes.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
stream: A callable that takes a StreamChunk and emits it.
|
|
71
|
+
subgraphs: Whether to emit messages from subgraphs.
|
|
72
|
+
parent_ns: The namespace where the handler was created.
|
|
73
|
+
We keep track of this namespace to allow calls to subgraphs that
|
|
74
|
+
were explicitly requested as a stream with `messages` mode
|
|
75
|
+
configured.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
parent_ns is used to handle scenarios where the subgraph is explicitly
|
|
79
|
+
streamed with `stream_mode="messages"`.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def parent_graph_node():
|
|
83
|
+
# This node is in the parent graph.
|
|
84
|
+
async for event in some_subgraph(..., stream_mode="messages"):
|
|
85
|
+
do something with event # <-- these events will be emitted
|
|
86
|
+
return ...
|
|
87
|
+
|
|
88
|
+
parent_graph.invoke(subgraphs=False)
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
self.stream = stream
|
|
92
|
+
self.subgraphs = subgraphs
|
|
93
|
+
self.metadata: dict[UUID, Meta] = {}
|
|
94
|
+
self.seen: set[int | str] = set()
|
|
95
|
+
self.parent_ns = parent_ns
|
|
96
|
+
|
|
97
|
+
def _emit(self, meta: Meta, message: BaseMessage, *, dedupe: bool = False) -> None:
|
|
98
|
+
if dedupe and message.id in self.seen:
|
|
99
|
+
return
|
|
100
|
+
else:
|
|
101
|
+
if message.id is None:
|
|
102
|
+
message.id = str(uuid4())
|
|
103
|
+
self.seen.add(message.id)
|
|
104
|
+
self.stream((meta[0], "messages", (message, meta[1])))
|
|
105
|
+
|
|
106
|
+
def _find_and_emit_messages(self, meta: Meta, response: Any) -> None:
|
|
107
|
+
if isinstance(response, BaseMessage):
|
|
108
|
+
self._emit(meta, response, dedupe=True)
|
|
109
|
+
elif isinstance(response, Sequence):
|
|
110
|
+
for value in response:
|
|
111
|
+
if isinstance(value, BaseMessage):
|
|
112
|
+
self._emit(meta, value, dedupe=True)
|
|
113
|
+
else:
|
|
114
|
+
for value in _state_values(response):
|
|
115
|
+
if isinstance(value, BaseMessage):
|
|
116
|
+
self._emit(meta, value, dedupe=True)
|
|
117
|
+
elif isinstance(value, Sequence):
|
|
118
|
+
for item in value:
|
|
119
|
+
if isinstance(item, BaseMessage):
|
|
120
|
+
self._emit(meta, item, dedupe=True)
|
|
121
|
+
|
|
122
|
+
def tap_output_aiter(
|
|
123
|
+
self, run_id: UUID, output: AsyncIterator[T]
|
|
124
|
+
) -> AsyncIterator[T]:
|
|
125
|
+
return output
|
|
126
|
+
|
|
127
|
+
def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
|
|
128
|
+
return output
|
|
129
|
+
|
|
130
|
+
def on_chat_model_start(
|
|
131
|
+
self,
|
|
132
|
+
serialized: dict[str, Any],
|
|
133
|
+
messages: list[list[BaseMessage]],
|
|
134
|
+
*,
|
|
135
|
+
run_id: UUID,
|
|
136
|
+
parent_run_id: UUID | None = None,
|
|
137
|
+
tags: list[str] | None = None,
|
|
138
|
+
metadata: dict[str, Any] | None = None,
|
|
139
|
+
**kwargs: Any,
|
|
140
|
+
) -> Any:
|
|
141
|
+
if metadata and (not tags or (TAG_NOSTREAM not in tags)):
|
|
142
|
+
ns = tuple(cast(str, metadata["coco_graph_checkpoint_ns"]).split(NS_SEP))[
|
|
143
|
+
:-1
|
|
144
|
+
]
|
|
145
|
+
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
|
|
146
|
+
return
|
|
147
|
+
if (filtered_tags := filter_to_user_tags(tags)) is not None:
|
|
148
|
+
metadata["tags"] = filtered_tags
|
|
149
|
+
self.metadata[run_id] = (ns, metadata)
|
|
150
|
+
|
|
151
|
+
def on_llm_new_token(
|
|
152
|
+
self,
|
|
153
|
+
token: str,
|
|
154
|
+
*,
|
|
155
|
+
chunk: ChatGenerationChunk | None = None,
|
|
156
|
+
run_id: UUID,
|
|
157
|
+
parent_run_id: UUID | None = None,
|
|
158
|
+
tags: list[str] | None = None,
|
|
159
|
+
**kwargs: Any,
|
|
160
|
+
) -> Any:
|
|
161
|
+
if not isinstance(chunk, ChatGenerationChunk):
|
|
162
|
+
return
|
|
163
|
+
if meta := self.metadata.get(run_id):
|
|
164
|
+
self._emit(meta, chunk.message)
|
|
165
|
+
|
|
166
|
+
def on_llm_end(
|
|
167
|
+
self,
|
|
168
|
+
response: LLMResult,
|
|
169
|
+
*,
|
|
170
|
+
run_id: UUID,
|
|
171
|
+
parent_run_id: UUID | None = None,
|
|
172
|
+
**kwargs: Any,
|
|
173
|
+
) -> Any:
|
|
174
|
+
if meta := self.metadata.get(run_id):
|
|
175
|
+
if response.generations and response.generations[0]:
|
|
176
|
+
gen = response.generations[0][0]
|
|
177
|
+
if isinstance(gen, ChatGeneration):
|
|
178
|
+
self._emit(meta, gen.message, dedupe=True)
|
|
179
|
+
self.metadata.pop(run_id, None)
|
|
180
|
+
|
|
181
|
+
def on_llm_error(
|
|
182
|
+
self,
|
|
183
|
+
error: BaseException,
|
|
184
|
+
*,
|
|
185
|
+
run_id: UUID,
|
|
186
|
+
parent_run_id: UUID | None = None,
|
|
187
|
+
**kwargs: Any,
|
|
188
|
+
) -> Any:
|
|
189
|
+
self.metadata.pop(run_id, None)
|
|
190
|
+
|
|
191
|
+
def on_chain_start(
|
|
192
|
+
self,
|
|
193
|
+
serialized: dict[str, Any],
|
|
194
|
+
inputs: dict[str, Any],
|
|
195
|
+
*,
|
|
196
|
+
run_id: UUID,
|
|
197
|
+
parent_run_id: UUID | None = None,
|
|
198
|
+
tags: list[str] | None = None,
|
|
199
|
+
metadata: dict[str, Any] | None = None,
|
|
200
|
+
**kwargs: Any,
|
|
201
|
+
) -> Any:
|
|
202
|
+
if (
|
|
203
|
+
metadata
|
|
204
|
+
and kwargs.get("name") == metadata.get("coco_graph_node")
|
|
205
|
+
and (not tags or TAG_HIDDEN not in tags)
|
|
206
|
+
):
|
|
207
|
+
ns = tuple(cast(str, metadata["coco_graph_checkpoint_ns"]).split(NS_SEP))[
|
|
208
|
+
:-1
|
|
209
|
+
]
|
|
210
|
+
if not self.subgraphs and len(ns) > 0:
|
|
211
|
+
return
|
|
212
|
+
self.metadata[run_id] = (ns, metadata)
|
|
213
|
+
for value in _state_values(inputs):
|
|
214
|
+
if isinstance(value, BaseMessage):
|
|
215
|
+
if value.id is not None:
|
|
216
|
+
self.seen.add(value.id)
|
|
217
|
+
elif isinstance(value, Sequence) and not isinstance(value, str):
|
|
218
|
+
for item in value:
|
|
219
|
+
if isinstance(item, BaseMessage):
|
|
220
|
+
if item.id is not None:
|
|
221
|
+
self.seen.add(item.id)
|
|
222
|
+
|
|
223
|
+
def on_chain_end(
|
|
224
|
+
self,
|
|
225
|
+
response: Any,
|
|
226
|
+
*,
|
|
227
|
+
run_id: UUID,
|
|
228
|
+
parent_run_id: UUID | None = None,
|
|
229
|
+
**kwargs: Any,
|
|
230
|
+
) -> Any:
|
|
231
|
+
if meta := self.metadata.pop(run_id, None):
|
|
232
|
+
# Handle Command node updates
|
|
233
|
+
if isinstance(response, Command):
|
|
234
|
+
self._find_and_emit_messages(meta, response.update)
|
|
235
|
+
# Handle list of Command updates
|
|
236
|
+
elif isinstance(response, Sequence) and any(
|
|
237
|
+
isinstance(value, Command) for value in response
|
|
238
|
+
):
|
|
239
|
+
for value in response:
|
|
240
|
+
if isinstance(value, Command):
|
|
241
|
+
self._find_and_emit_messages(meta, value.update)
|
|
242
|
+
else:
|
|
243
|
+
self._find_and_emit_messages(meta, value)
|
|
244
|
+
# Handle basic updates / streaming
|
|
245
|
+
else:
|
|
246
|
+
self._find_and_emit_messages(meta, response)
|
|
247
|
+
|
|
248
|
+
def on_chain_error(
|
|
249
|
+
self,
|
|
250
|
+
error: BaseException,
|
|
251
|
+
*,
|
|
252
|
+
run_id: UUID,
|
|
253
|
+
parent_run_id: UUID | None = None,
|
|
254
|
+
**kwargs: Any,
|
|
255
|
+
) -> Any:
|
|
256
|
+
self.metadata.pop(run_id, None)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class StreamMessagesHandlerV2(StreamMessagesHandler, _V2StreamingCallbackHandler):
|
|
260
|
+
"""v2 variant of `StreamMessagesHandler`.
|
|
261
|
+
|
|
262
|
+
Declaring `_V2StreamingCallbackHandler` as a base flips
|
|
263
|
+
`BaseChatModel.invoke` to route through `_stream_chat_model_events`
|
|
264
|
+
(firing `on_stream_event`) instead of `_stream` (firing
|
|
265
|
+
`on_llm_new_token`). Inherits `on_stream_event` from the parent,
|
|
266
|
+
which forwards protocol events onto the messages stream channel.
|
|
267
|
+
|
|
268
|
+
Pregel attaches this class instead of the v1 handler only when
|
|
269
|
+
`StreamingHandler` opts in via the internal
|
|
270
|
+
`CONFIG_KEY_STREAM_MESSAGES_V2` config key; direct
|
|
271
|
+
`graph.stream(stream_mode="messages")` callers keep the v1
|
|
272
|
+
AIMessageChunk shape.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def on_llm_new_token(
|
|
276
|
+
self,
|
|
277
|
+
token: str,
|
|
278
|
+
*,
|
|
279
|
+
chunk: ChatGenerationChunk | None = None,
|
|
280
|
+
run_id: UUID,
|
|
281
|
+
parent_run_id: UUID | None = None,
|
|
282
|
+
tags: list[str] | None = None,
|
|
283
|
+
**kwargs: Any,
|
|
284
|
+
) -> Any:
|
|
285
|
+
"""Intentional no-op — v1 chunks are not used on v2-flagged runs.
|
|
286
|
+
|
|
287
|
+
The v2 marker already steers `invoke` to the event generator, so
|
|
288
|
+
`on_llm_new_token` should not fire under normal routing. This
|
|
289
|
+
override stays a pass-through (no call to `super()`) to make
|
|
290
|
+
the intent explicit and to guard against any caller (e.g. a
|
|
291
|
+
node that calls `model.stream()` directly, which still fires
|
|
292
|
+
the v1 callback) leaking AIMessageChunks onto a v2-flagged
|
|
293
|
+
messages stream.
|
|
294
|
+
"""
|
|
295
|
+
# Intentionally empty: v2 handler does not forward v1 chunks.
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
stream: Callable[[StreamChunk], None],
|
|
300
|
+
subgraphs: bool,
|
|
301
|
+
*,
|
|
302
|
+
parent_ns: tuple[str, ...] | None = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
super().__init__(stream, subgraphs, parent_ns=parent_ns)
|
|
305
|
+
self._streamed_run_ids: set[UUID] = set()
|
|
306
|
+
|
|
307
|
+
def _find_and_emit_messages(self, meta: Meta, response: Any) -> None:
|
|
308
|
+
"""Like the v1 handler, but skip ToolMessage from node outputs.
|
|
309
|
+
|
|
310
|
+
Tool results belong on the tools channel / state in v3; v2-flagged streams
|
|
311
|
+
must not replay finalized ToolMessages as chat tokens (see MessagesTransformer).
|
|
312
|
+
Legacy v1-only `stream_mode="messages"` still emits ToolMessages (see subgraph
|
|
313
|
+
streaming tests).
|
|
314
|
+
"""
|
|
315
|
+
if isinstance(response, BaseMessage) and not isinstance(response, ToolMessage):
|
|
316
|
+
self._emit(meta, response, dedupe=True)
|
|
317
|
+
elif isinstance(response, Sequence):
|
|
318
|
+
for value in response:
|
|
319
|
+
if isinstance(value, BaseMessage) and not isinstance(
|
|
320
|
+
value, ToolMessage
|
|
321
|
+
):
|
|
322
|
+
self._emit(meta, value, dedupe=True)
|
|
323
|
+
else:
|
|
324
|
+
for value in _state_values(response):
|
|
325
|
+
if isinstance(value, BaseMessage) and not isinstance(
|
|
326
|
+
value, ToolMessage
|
|
327
|
+
):
|
|
328
|
+
self._emit(meta, value, dedupe=True)
|
|
329
|
+
elif isinstance(value, Sequence):
|
|
330
|
+
for item in value:
|
|
331
|
+
if isinstance(item, BaseMessage) and not isinstance(
|
|
332
|
+
item, ToolMessage
|
|
333
|
+
):
|
|
334
|
+
self._emit(meta, item, dedupe=True)
|
|
335
|
+
|
|
336
|
+
def on_llm_end(
|
|
337
|
+
self,
|
|
338
|
+
response: LLMResult,
|
|
339
|
+
*,
|
|
340
|
+
run_id: UUID,
|
|
341
|
+
parent_run_id: UUID | None = None,
|
|
342
|
+
**kwargs: Any,
|
|
343
|
+
) -> Any:
|
|
344
|
+
if meta := self.metadata.get(run_id):
|
|
345
|
+
if response.generations and response.generations[0]:
|
|
346
|
+
gen = response.generations[0][0]
|
|
347
|
+
if isinstance(gen, ChatGeneration):
|
|
348
|
+
if run_id in self._streamed_run_ids:
|
|
349
|
+
if gen.message.id is None:
|
|
350
|
+
gen.message.id = str(uuid4())
|
|
351
|
+
self.seen.add(gen.message.id)
|
|
352
|
+
else:
|
|
353
|
+
self._emit(meta, gen.message, dedupe=True)
|
|
354
|
+
self._streamed_run_ids.discard(run_id)
|
|
355
|
+
self.metadata.pop(run_id, None)
|
|
356
|
+
|
|
357
|
+
def on_llm_error(
|
|
358
|
+
self,
|
|
359
|
+
error: BaseException,
|
|
360
|
+
*,
|
|
361
|
+
run_id: UUID,
|
|
362
|
+
parent_run_id: UUID | None = None,
|
|
363
|
+
**kwargs: Any,
|
|
364
|
+
) -> Any:
|
|
365
|
+
self._streamed_run_ids.discard(run_id)
|
|
366
|
+
super().on_llm_error(
|
|
367
|
+
error,
|
|
368
|
+
run_id=run_id,
|
|
369
|
+
parent_run_id=parent_run_id,
|
|
370
|
+
**kwargs,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def on_stream_event(
|
|
374
|
+
self,
|
|
375
|
+
event: dict[str, Any],
|
|
376
|
+
*,
|
|
377
|
+
run_id: UUID,
|
|
378
|
+
parent_run_id: UUID | None = None,
|
|
379
|
+
tags: list[str] | None = None,
|
|
380
|
+
**kwargs: Any,
|
|
381
|
+
) -> Any:
|
|
382
|
+
"""Forward a protocol event from `stream_events(version="v3")` as a messages stream part.
|
|
383
|
+
|
|
384
|
+
Fires once per `MessagesData` event (`message-start`, per-block
|
|
385
|
+
`content-block-*`, `message-finish`). The transformer layer
|
|
386
|
+
correlates events back to a single `ChatModelStream` via
|
|
387
|
+
`metadata["run_id"]` — attached here so the v1
|
|
388
|
+
`stream_mode="messages"` output (which emits
|
|
389
|
+
`(AIMessageChunk, metadata)` via `on_llm_new_token`) keeps its
|
|
390
|
+
original metadata shape.
|
|
391
|
+
|
|
392
|
+
Lives on the v2 handler rather than the v1 base: content-block
|
|
393
|
+
events are a v2-only concept, and forwarding them only when the
|
|
394
|
+
v2 handler is attached keeps the message channel's shape
|
|
395
|
+
predictable for v1 callers.
|
|
396
|
+
"""
|
|
397
|
+
if meta := self.metadata.get(run_id):
|
|
398
|
+
# Record message_id on message-start so on_chain_end's
|
|
399
|
+
# dedupe skips the finalized AIMessage the node returns
|
|
400
|
+
# (otherwise the messages projection double-counts: once
|
|
401
|
+
# from streaming, once from the chain output).
|
|
402
|
+
if event.get("event") == "message-start":
|
|
403
|
+
self._streamed_run_ids.add(run_id)
|
|
404
|
+
msg_id = event.get("message_id")
|
|
405
|
+
if msg_id:
|
|
406
|
+
self.seen.add(msg_id)
|
|
407
|
+
v2_meta = {**meta[1], "run_id": str(run_id)}
|
|
408
|
+
self.stream((meta[0], "messages", (event, v2_meta)))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# Known role values (OpenAI-style) and type values (Coco serialisation)
|
|
412
|
+
# that identify a dict as a message. Checked before coercing to BaseMessage so
|
|
413
|
+
# we don't accidentally touch unrelated dicts that happen to have a "role" key.
|
|
414
|
+
_MESSAGE_ROLES: frozenset[str] = frozenset(
|
|
415
|
+
{"user", "human", "assistant", "ai", "tool", "system", "function"}
|
|
416
|
+
)
|
|
417
|
+
_MESSAGE_TYPES: frozenset[str] = frozenset(
|
|
418
|
+
{"human", "ai", "tool", "system", "function", "remove"}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _is_message_dict(item: dict) -> bool:
|
|
423
|
+
return item.get("role") in _MESSAGE_ROLES or item.get("type") in _MESSAGE_TYPES
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def ensure_message_ids(value: Any) -> None:
|
|
427
|
+
"""Coerce message-like write values to typed BaseMessages with stable IDs.
|
|
428
|
+
|
|
429
|
+
Called in put_writes() before DeltaChannel writes are submitted to the
|
|
430
|
+
checkpointer. Without this the checkpoint may store raw dicts or id=None
|
|
431
|
+
BaseMessages; every get_state() replay then produces a different UUID and
|
|
432
|
+
the same message appears with a different ID in each LangSmith trace.
|
|
433
|
+
|
|
434
|
+
Handles three input shapes:
|
|
435
|
+
- BaseMessage objects: assign a UUID if id is None.
|
|
436
|
+
- Dicts with a known "role" (OpenAI-style) or "type" (Coco format) at
|
|
437
|
+
the root level: stamp "id" into the dict in-place. The reducer's
|
|
438
|
+
convert_to_messages call will forward the id to the resulting BaseMessage.
|
|
439
|
+
- Lists of the above: apply the same logic to each element, replacing dict
|
|
440
|
+
items with coerced BaseMessages so the shared list reference seen by
|
|
441
|
+
checkpoint_pending_writes and the background thread both get typed messages.
|
|
442
|
+
|
|
443
|
+
Mutating synchronously here (before the background thread is submitted) is
|
|
444
|
+
safe: the serialised bytes always reflect the post-coercion state.
|
|
445
|
+
"""
|
|
446
|
+
if isinstance(value, BaseMessage):
|
|
447
|
+
if value.id is None:
|
|
448
|
+
value.id = str(uuid4())
|
|
449
|
+
elif isinstance(value, dict) and _is_message_dict(value):
|
|
450
|
+
if not value.get("id"):
|
|
451
|
+
value["id"] = str(uuid4())
|
|
452
|
+
elif isinstance(value, list):
|
|
453
|
+
for i, item in enumerate(value):
|
|
454
|
+
if isinstance(item, BaseMessage):
|
|
455
|
+
if item.id is None:
|
|
456
|
+
item.id = str(uuid4())
|
|
457
|
+
elif isinstance(item, dict) and _is_message_dict(item):
|
|
458
|
+
msg = convert_to_messages([item])[0]
|
|
459
|
+
if msg.id is None:
|
|
460
|
+
msg.id = str(uuid4())
|
|
461
|
+
value[i] = msg
|