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,663 @@
|
|
|
1
|
+
"""`Runnable` that can fallback to other `Runnable` objects if it fails."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import typing
|
|
6
|
+
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from coco_core.callbacks.manager import AsyncCallbackManager, CallbackManager
|
|
14
|
+
from coco_core.runnables.base import Runnable, RunnableSerializable
|
|
15
|
+
from coco_core.runnables.config import (
|
|
16
|
+
RunnableConfig,
|
|
17
|
+
ensure_config,
|
|
18
|
+
get_async_callback_manager_for_config,
|
|
19
|
+
get_callback_manager_for_config,
|
|
20
|
+
get_config_list,
|
|
21
|
+
patch_config,
|
|
22
|
+
set_config_context,
|
|
23
|
+
)
|
|
24
|
+
from coco_core.runnables.utils import (
|
|
25
|
+
ConfigurableFieldSpec,
|
|
26
|
+
Input,
|
|
27
|
+
Output,
|
|
28
|
+
coro_with_context,
|
|
29
|
+
get_unique_config_specs,
|
|
30
|
+
)
|
|
31
|
+
from coco_core.utils.pydantic import TypeBaseModel
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from coco_core.callbacks.manager import AsyncCallbackManagerForChainRun
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
|
38
|
+
"""`Runnable` that can fallback to other `Runnable` objects if it fails.
|
|
39
|
+
|
|
40
|
+
External APIs (e.g., APIs for a language model) may at times experience
|
|
41
|
+
degraded performance or even downtime.
|
|
42
|
+
|
|
43
|
+
In these cases, it can be useful to have a fallback `Runnable` that can be
|
|
44
|
+
used in place of the original `Runnable` (e.g., fallback to another LLM provider).
|
|
45
|
+
|
|
46
|
+
Fallbacks can be defined at the level of a single `Runnable`, or at the level
|
|
47
|
+
of a chain of `Runnable`s. Fallbacks are tried in order until one succeeds or
|
|
48
|
+
all fail.
|
|
49
|
+
|
|
50
|
+
While you can instantiate a `RunnableWithFallbacks` directly, it is usually
|
|
51
|
+
more convenient to use the `with_fallbacks` method on a `Runnable`.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
```python
|
|
55
|
+
from coco_anthropic import ChatAnthropic
|
|
56
|
+
from coco_openai import ChatOpenAI
|
|
57
|
+
|
|
58
|
+
model = ChatAnthropic(model="claude-sonnet-5").with_fallbacks(
|
|
59
|
+
[ChatOpenAI(model="gpt-5.4-mini")]
|
|
60
|
+
)
|
|
61
|
+
# Will usually use ChatAnthropic, but fallback to ChatOpenAI
|
|
62
|
+
# if ChatAnthropic fails.
|
|
63
|
+
model.invoke("hello")
|
|
64
|
+
|
|
65
|
+
# And you can also use fallbacks at the level of a chain.
|
|
66
|
+
# Here if both LLM providers fail, we'll fallback to a good hardcoded
|
|
67
|
+
# response.
|
|
68
|
+
|
|
69
|
+
from coco_core.prompts import PromptTemplate
|
|
70
|
+
from coco_core.output_parser import StrOutputParser
|
|
71
|
+
from coco_core.runnables import RunnableLambda
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def when_all_is_lost(inputs):
|
|
75
|
+
return (
|
|
76
|
+
"Looks like our LLM providers are down. "
|
|
77
|
+
"Here's a nice 🦜️ emoji for you instead."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
chain_with_fallback = (
|
|
82
|
+
PromptTemplate.from_template("Tell me a joke about {topic}")
|
|
83
|
+
| model
|
|
84
|
+
| StrOutputParser()
|
|
85
|
+
).with_fallbacks([RunnableLambda(when_all_is_lost)])
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
runnable: Runnable[Input, Output]
|
|
90
|
+
"""The `Runnable` to run first."""
|
|
91
|
+
fallbacks: Sequence[Runnable[Input, Output]]
|
|
92
|
+
"""A sequence of fallbacks to try."""
|
|
93
|
+
exceptions_to_handle: tuple[type[BaseException], ...] = (Exception,)
|
|
94
|
+
"""The exceptions on which fallbacks should be tried.
|
|
95
|
+
|
|
96
|
+
Any exception that is not a subclass of these exceptions will be raised immediately.
|
|
97
|
+
"""
|
|
98
|
+
exception_key: str | None = None
|
|
99
|
+
"""If `string` is specified then handled exceptions will be passed to fallbacks as
|
|
100
|
+
part of the input under the specified key.
|
|
101
|
+
|
|
102
|
+
If `None`, exceptions will not be passed to fallbacks.
|
|
103
|
+
|
|
104
|
+
If used, the base `Runnable` and its fallbacks must accept a dictionary as input.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
model_config = ConfigDict(
|
|
108
|
+
arbitrary_types_allowed=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
@override
|
|
113
|
+
def InputType(self) -> type[Input]:
|
|
114
|
+
return self.runnable.InputType
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
@override
|
|
118
|
+
def OutputType(self) -> type[Output]:
|
|
119
|
+
return self.runnable.OutputType
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
def get_input_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
|
|
123
|
+
return self.runnable.get_input_schema(config)
|
|
124
|
+
|
|
125
|
+
@override
|
|
126
|
+
def get_output_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
|
|
127
|
+
return self.runnable.get_output_schema(config)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
@override
|
|
131
|
+
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
|
132
|
+
return get_unique_config_specs(
|
|
133
|
+
spec
|
|
134
|
+
for step in [self.runnable, *self.fallbacks]
|
|
135
|
+
for spec in step.config_specs
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
@override
|
|
140
|
+
def is_lc_serializable(cls) -> bool:
|
|
141
|
+
"""Return `True` as this class is serializable."""
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
@override
|
|
146
|
+
def get_lc_namespace(cls) -> list[str]:
|
|
147
|
+
"""Get the namespace of the Coco object.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
`["coco", "schema", "runnable"]`
|
|
151
|
+
"""
|
|
152
|
+
return ["coco", "schema", "runnable"]
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def runnables(self) -> Iterator[Runnable[Input, Output]]:
|
|
156
|
+
"""Iterator over the `Runnable` and its fallbacks.
|
|
157
|
+
|
|
158
|
+
Yields:
|
|
159
|
+
The `Runnable` then its fallbacks.
|
|
160
|
+
"""
|
|
161
|
+
yield self.runnable
|
|
162
|
+
yield from self.fallbacks
|
|
163
|
+
|
|
164
|
+
@override
|
|
165
|
+
def invoke(
|
|
166
|
+
self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
|
|
167
|
+
) -> Output:
|
|
168
|
+
if self.exception_key is not None and not isinstance(input, dict):
|
|
169
|
+
msg = (
|
|
170
|
+
"If 'exception_key' is specified then input must be a dictionary."
|
|
171
|
+
f"However found a type of {type(input)} for input"
|
|
172
|
+
)
|
|
173
|
+
raise ValueError(msg)
|
|
174
|
+
# setup callbacks
|
|
175
|
+
config = ensure_config(config)
|
|
176
|
+
callback_manager = get_callback_manager_for_config(config)
|
|
177
|
+
# start the root run
|
|
178
|
+
run_manager = callback_manager.on_chain_start(
|
|
179
|
+
None,
|
|
180
|
+
input,
|
|
181
|
+
name=config.get("run_name") or self.get_name(),
|
|
182
|
+
run_id=config.pop("run_id", None),
|
|
183
|
+
)
|
|
184
|
+
first_error = None
|
|
185
|
+
last_error = None
|
|
186
|
+
for runnable in self.runnables:
|
|
187
|
+
try:
|
|
188
|
+
if self.exception_key and last_error is not None:
|
|
189
|
+
input[self.exception_key] = last_error # type: ignore[index]
|
|
190
|
+
child_config = patch_config(config, callbacks=run_manager.get_child())
|
|
191
|
+
with set_config_context(child_config) as context:
|
|
192
|
+
output = context.run(
|
|
193
|
+
runnable.invoke,
|
|
194
|
+
input,
|
|
195
|
+
config,
|
|
196
|
+
**kwargs,
|
|
197
|
+
)
|
|
198
|
+
except self.exceptions_to_handle as e:
|
|
199
|
+
if first_error is None:
|
|
200
|
+
first_error = e
|
|
201
|
+
last_error = e
|
|
202
|
+
except BaseException as e:
|
|
203
|
+
run_manager.on_chain_error(e)
|
|
204
|
+
raise
|
|
205
|
+
else:
|
|
206
|
+
run_manager.on_chain_end(output)
|
|
207
|
+
return output
|
|
208
|
+
if first_error is None:
|
|
209
|
+
msg = "No error stored at end of fallbacks."
|
|
210
|
+
raise ValueError(msg)
|
|
211
|
+
run_manager.on_chain_error(first_error)
|
|
212
|
+
raise first_error
|
|
213
|
+
|
|
214
|
+
@override
|
|
215
|
+
async def ainvoke(
|
|
216
|
+
self,
|
|
217
|
+
input: Input,
|
|
218
|
+
config: RunnableConfig | None = None,
|
|
219
|
+
**kwargs: Any | None,
|
|
220
|
+
) -> Output:
|
|
221
|
+
if self.exception_key is not None and not isinstance(input, dict):
|
|
222
|
+
msg = (
|
|
223
|
+
"If 'exception_key' is specified then input must be a dictionary."
|
|
224
|
+
f"However found a type of {type(input)} for input"
|
|
225
|
+
)
|
|
226
|
+
raise ValueError(msg)
|
|
227
|
+
# setup callbacks
|
|
228
|
+
config = ensure_config(config)
|
|
229
|
+
callback_manager = get_async_callback_manager_for_config(config)
|
|
230
|
+
# start the root run
|
|
231
|
+
run_manager = await callback_manager.on_chain_start(
|
|
232
|
+
None,
|
|
233
|
+
input,
|
|
234
|
+
name=config.get("run_name") or self.get_name(),
|
|
235
|
+
run_id=config.pop("run_id", None),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
first_error = None
|
|
239
|
+
last_error = None
|
|
240
|
+
for runnable in self.runnables:
|
|
241
|
+
try:
|
|
242
|
+
if self.exception_key and last_error is not None:
|
|
243
|
+
input[self.exception_key] = last_error # type: ignore[index]
|
|
244
|
+
child_config = patch_config(config, callbacks=run_manager.get_child())
|
|
245
|
+
with set_config_context(child_config) as context:
|
|
246
|
+
coro = context.run(runnable.ainvoke, input, config, **kwargs)
|
|
247
|
+
output = await coro_with_context(coro, context)
|
|
248
|
+
except self.exceptions_to_handle as e:
|
|
249
|
+
if first_error is None:
|
|
250
|
+
first_error = e
|
|
251
|
+
last_error = e
|
|
252
|
+
except BaseException as e:
|
|
253
|
+
await run_manager.on_chain_error(e)
|
|
254
|
+
raise
|
|
255
|
+
else:
|
|
256
|
+
await run_manager.on_chain_end(output)
|
|
257
|
+
return output
|
|
258
|
+
if first_error is None:
|
|
259
|
+
msg = "No error stored at end of fallbacks."
|
|
260
|
+
raise ValueError(msg)
|
|
261
|
+
await run_manager.on_chain_error(first_error)
|
|
262
|
+
raise first_error
|
|
263
|
+
|
|
264
|
+
@override
|
|
265
|
+
def batch(
|
|
266
|
+
self,
|
|
267
|
+
inputs: list[Input],
|
|
268
|
+
config: RunnableConfig | list[RunnableConfig] | None = None,
|
|
269
|
+
*,
|
|
270
|
+
return_exceptions: bool = False,
|
|
271
|
+
**kwargs: Any | None,
|
|
272
|
+
) -> list[Output]:
|
|
273
|
+
if self.exception_key is not None and not all(
|
|
274
|
+
isinstance(input_, dict) for input_ in inputs
|
|
275
|
+
):
|
|
276
|
+
msg = (
|
|
277
|
+
"If 'exception_key' is specified then inputs must be dictionaries."
|
|
278
|
+
f"However found a type of {type(inputs[0])} for input"
|
|
279
|
+
)
|
|
280
|
+
raise ValueError(msg)
|
|
281
|
+
|
|
282
|
+
if not inputs:
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
# setup callbacks
|
|
286
|
+
configs = get_config_list(config, len(inputs))
|
|
287
|
+
callback_managers = [
|
|
288
|
+
CallbackManager.configure(
|
|
289
|
+
inheritable_callbacks=config.get("callbacks"),
|
|
290
|
+
local_callbacks=None,
|
|
291
|
+
verbose=False,
|
|
292
|
+
inheritable_tags=config.get("tags"),
|
|
293
|
+
local_tags=None,
|
|
294
|
+
inheritable_metadata=config.get("metadata"),
|
|
295
|
+
local_metadata=None,
|
|
296
|
+
)
|
|
297
|
+
for config in configs
|
|
298
|
+
]
|
|
299
|
+
# start the root runs, one per input
|
|
300
|
+
run_managers = [
|
|
301
|
+
cm.on_chain_start(
|
|
302
|
+
None,
|
|
303
|
+
input_ if isinstance(input_, dict) else {"input": input_},
|
|
304
|
+
name=config.get("run_name") or self.get_name(),
|
|
305
|
+
run_id=config.pop("run_id", None),
|
|
306
|
+
)
|
|
307
|
+
for cm, input_, config in zip(
|
|
308
|
+
callback_managers, inputs, configs, strict=False
|
|
309
|
+
)
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
to_return: dict[int, Any] = {}
|
|
313
|
+
run_again = dict(enumerate(inputs))
|
|
314
|
+
handled_exceptions: dict[int, BaseException] = {}
|
|
315
|
+
first_to_raise = None
|
|
316
|
+
for runnable in self.runnables:
|
|
317
|
+
outputs = runnable.batch(
|
|
318
|
+
[input_ for _, input_ in sorted(run_again.items())],
|
|
319
|
+
[
|
|
320
|
+
# each step a child run of the corresponding root run
|
|
321
|
+
patch_config(configs[i], callbacks=run_managers[i].get_child())
|
|
322
|
+
for i in sorted(run_again)
|
|
323
|
+
],
|
|
324
|
+
return_exceptions=True,
|
|
325
|
+
**kwargs,
|
|
326
|
+
)
|
|
327
|
+
for (i, input_), output in zip(
|
|
328
|
+
sorted(run_again.copy().items()), outputs, strict=False
|
|
329
|
+
):
|
|
330
|
+
if isinstance(output, BaseException) and not isinstance(
|
|
331
|
+
output, self.exceptions_to_handle
|
|
332
|
+
):
|
|
333
|
+
if not return_exceptions:
|
|
334
|
+
first_to_raise = first_to_raise or output
|
|
335
|
+
else:
|
|
336
|
+
handled_exceptions[i] = output
|
|
337
|
+
run_again.pop(i)
|
|
338
|
+
elif isinstance(output, self.exceptions_to_handle):
|
|
339
|
+
if self.exception_key:
|
|
340
|
+
input_[self.exception_key] = output # type: ignore[index]
|
|
341
|
+
handled_exceptions[i] = output
|
|
342
|
+
else:
|
|
343
|
+
run_managers[i].on_chain_end(output)
|
|
344
|
+
to_return[i] = output
|
|
345
|
+
run_again.pop(i)
|
|
346
|
+
handled_exceptions.pop(i, None)
|
|
347
|
+
if first_to_raise:
|
|
348
|
+
raise first_to_raise
|
|
349
|
+
if not run_again:
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
sorted_handled_exceptions = sorted(handled_exceptions.items())
|
|
353
|
+
for i, error in sorted_handled_exceptions:
|
|
354
|
+
run_managers[i].on_chain_error(error)
|
|
355
|
+
if not return_exceptions and sorted_handled_exceptions:
|
|
356
|
+
raise sorted_handled_exceptions[0][1]
|
|
357
|
+
to_return.update(handled_exceptions)
|
|
358
|
+
return [output for _, output in sorted(to_return.items())]
|
|
359
|
+
|
|
360
|
+
@override
|
|
361
|
+
async def abatch(
|
|
362
|
+
self,
|
|
363
|
+
inputs: list[Input],
|
|
364
|
+
config: RunnableConfig | list[RunnableConfig] | None = None,
|
|
365
|
+
*,
|
|
366
|
+
return_exceptions: bool = False,
|
|
367
|
+
**kwargs: Any | None,
|
|
368
|
+
) -> list[Output]:
|
|
369
|
+
if self.exception_key is not None and not all(
|
|
370
|
+
isinstance(input_, dict) for input_ in inputs
|
|
371
|
+
):
|
|
372
|
+
msg = (
|
|
373
|
+
"If 'exception_key' is specified then inputs must be dictionaries."
|
|
374
|
+
f"However found a type of {type(inputs[0])} for input"
|
|
375
|
+
)
|
|
376
|
+
raise ValueError(msg)
|
|
377
|
+
|
|
378
|
+
if not inputs:
|
|
379
|
+
return []
|
|
380
|
+
|
|
381
|
+
# setup callbacks
|
|
382
|
+
configs = get_config_list(config, len(inputs))
|
|
383
|
+
callback_managers = [
|
|
384
|
+
AsyncCallbackManager.configure(
|
|
385
|
+
inheritable_callbacks=config.get("callbacks"),
|
|
386
|
+
local_callbacks=None,
|
|
387
|
+
verbose=False,
|
|
388
|
+
inheritable_tags=config.get("tags"),
|
|
389
|
+
local_tags=None,
|
|
390
|
+
inheritable_metadata=config.get("metadata"),
|
|
391
|
+
local_metadata=None,
|
|
392
|
+
)
|
|
393
|
+
for config in configs
|
|
394
|
+
]
|
|
395
|
+
# start the root runs, one per input
|
|
396
|
+
run_managers: list[AsyncCallbackManagerForChainRun] = await asyncio.gather(
|
|
397
|
+
*(
|
|
398
|
+
cm.on_chain_start(
|
|
399
|
+
None,
|
|
400
|
+
input_,
|
|
401
|
+
name=config.get("run_name") or self.get_name(),
|
|
402
|
+
run_id=config.pop("run_id", None),
|
|
403
|
+
)
|
|
404
|
+
for cm, input_, config in zip(
|
|
405
|
+
callback_managers, inputs, configs, strict=False
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
to_return: dict[int, Output | BaseException] = {}
|
|
411
|
+
run_again = dict(enumerate(inputs))
|
|
412
|
+
handled_exceptions: dict[int, BaseException] = {}
|
|
413
|
+
first_to_raise = None
|
|
414
|
+
for runnable in self.runnables:
|
|
415
|
+
outputs = await runnable.abatch(
|
|
416
|
+
[input_ for _, input_ in sorted(run_again.items())],
|
|
417
|
+
[
|
|
418
|
+
# each step a child run of the corresponding root run
|
|
419
|
+
patch_config(configs[i], callbacks=run_managers[i].get_child())
|
|
420
|
+
for i in sorted(run_again)
|
|
421
|
+
],
|
|
422
|
+
return_exceptions=True,
|
|
423
|
+
**kwargs,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
for (i, input_), output in zip(
|
|
427
|
+
sorted(run_again.copy().items()), outputs, strict=False
|
|
428
|
+
):
|
|
429
|
+
if isinstance(output, BaseException) and not isinstance(
|
|
430
|
+
output, self.exceptions_to_handle
|
|
431
|
+
):
|
|
432
|
+
if not return_exceptions:
|
|
433
|
+
first_to_raise = first_to_raise or output
|
|
434
|
+
else:
|
|
435
|
+
handled_exceptions[i] = output
|
|
436
|
+
run_again.pop(i)
|
|
437
|
+
elif isinstance(output, self.exceptions_to_handle):
|
|
438
|
+
if self.exception_key:
|
|
439
|
+
input_[self.exception_key] = output # type: ignore[index]
|
|
440
|
+
handled_exceptions[i] = output
|
|
441
|
+
else:
|
|
442
|
+
to_return[i] = output
|
|
443
|
+
await run_managers[i].on_chain_end(output)
|
|
444
|
+
run_again.pop(i)
|
|
445
|
+
handled_exceptions.pop(i, None)
|
|
446
|
+
|
|
447
|
+
if first_to_raise:
|
|
448
|
+
raise first_to_raise
|
|
449
|
+
if not run_again:
|
|
450
|
+
break
|
|
451
|
+
|
|
452
|
+
sorted_handled_exceptions = sorted(handled_exceptions.items())
|
|
453
|
+
await asyncio.gather(
|
|
454
|
+
*(
|
|
455
|
+
run_managers[i].on_chain_error(error)
|
|
456
|
+
for i, error in sorted_handled_exceptions
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
if not return_exceptions and sorted_handled_exceptions:
|
|
460
|
+
raise sorted_handled_exceptions[0][1]
|
|
461
|
+
to_return.update(handled_exceptions)
|
|
462
|
+
return [cast("Output", output) for _, output in sorted(to_return.items())]
|
|
463
|
+
|
|
464
|
+
@override
|
|
465
|
+
def stream(
|
|
466
|
+
self,
|
|
467
|
+
input: Input,
|
|
468
|
+
config: RunnableConfig | None = None,
|
|
469
|
+
**kwargs: Any | None,
|
|
470
|
+
) -> Iterator[Output]:
|
|
471
|
+
if self.exception_key is not None and not isinstance(input, dict):
|
|
472
|
+
msg = (
|
|
473
|
+
"If 'exception_key' is specified then input must be a dictionary."
|
|
474
|
+
f"However found a type of {type(input)} for input"
|
|
475
|
+
)
|
|
476
|
+
raise ValueError(msg)
|
|
477
|
+
# setup callbacks
|
|
478
|
+
config = ensure_config(config)
|
|
479
|
+
callback_manager = get_callback_manager_for_config(config)
|
|
480
|
+
# start the root run
|
|
481
|
+
run_manager = callback_manager.on_chain_start(
|
|
482
|
+
None,
|
|
483
|
+
input,
|
|
484
|
+
name=config.get("run_name") or self.get_name(),
|
|
485
|
+
run_id=config.pop("run_id", None),
|
|
486
|
+
)
|
|
487
|
+
first_error = None
|
|
488
|
+
last_error = None
|
|
489
|
+
for runnable in self.runnables:
|
|
490
|
+
try:
|
|
491
|
+
if self.exception_key and last_error is not None:
|
|
492
|
+
input[self.exception_key] = last_error # type: ignore[index]
|
|
493
|
+
child_config = patch_config(config, callbacks=run_manager.get_child())
|
|
494
|
+
with set_config_context(child_config) as context:
|
|
495
|
+
stream = context.run(
|
|
496
|
+
runnable.stream,
|
|
497
|
+
input,
|
|
498
|
+
**kwargs,
|
|
499
|
+
)
|
|
500
|
+
chunk: Output = context.run(next, stream)
|
|
501
|
+
except self.exceptions_to_handle as e:
|
|
502
|
+
first_error = e if first_error is None else first_error
|
|
503
|
+
last_error = e
|
|
504
|
+
except BaseException as e:
|
|
505
|
+
run_manager.on_chain_error(e)
|
|
506
|
+
raise
|
|
507
|
+
else:
|
|
508
|
+
first_error = None
|
|
509
|
+
break
|
|
510
|
+
if first_error:
|
|
511
|
+
run_manager.on_chain_error(first_error)
|
|
512
|
+
raise first_error
|
|
513
|
+
|
|
514
|
+
yield chunk
|
|
515
|
+
output: Output | None = chunk
|
|
516
|
+
try:
|
|
517
|
+
for chunk in stream:
|
|
518
|
+
yield chunk
|
|
519
|
+
try:
|
|
520
|
+
output = output + chunk # type: ignore[operator]
|
|
521
|
+
except TypeError:
|
|
522
|
+
output = None
|
|
523
|
+
except BaseException as e:
|
|
524
|
+
run_manager.on_chain_error(e)
|
|
525
|
+
raise
|
|
526
|
+
run_manager.on_chain_end(output)
|
|
527
|
+
|
|
528
|
+
@override
|
|
529
|
+
async def astream(
|
|
530
|
+
self,
|
|
531
|
+
input: Input,
|
|
532
|
+
config: RunnableConfig | None = None,
|
|
533
|
+
**kwargs: Any | None,
|
|
534
|
+
) -> AsyncIterator[Output]:
|
|
535
|
+
if self.exception_key is not None and not isinstance(input, dict):
|
|
536
|
+
msg = (
|
|
537
|
+
"If 'exception_key' is specified then input must be a dictionary."
|
|
538
|
+
f"However found a type of {type(input)} for input"
|
|
539
|
+
)
|
|
540
|
+
raise ValueError(msg)
|
|
541
|
+
# setup callbacks
|
|
542
|
+
config = ensure_config(config)
|
|
543
|
+
callback_manager = get_async_callback_manager_for_config(config)
|
|
544
|
+
# start the root run
|
|
545
|
+
run_manager = await callback_manager.on_chain_start(
|
|
546
|
+
None,
|
|
547
|
+
input,
|
|
548
|
+
name=config.get("run_name") or self.get_name(),
|
|
549
|
+
run_id=config.pop("run_id", None),
|
|
550
|
+
)
|
|
551
|
+
first_error = None
|
|
552
|
+
last_error = None
|
|
553
|
+
for runnable in self.runnables:
|
|
554
|
+
try:
|
|
555
|
+
if self.exception_key and last_error is not None:
|
|
556
|
+
input[self.exception_key] = last_error # type: ignore[index]
|
|
557
|
+
child_config = patch_config(config, callbacks=run_manager.get_child())
|
|
558
|
+
with set_config_context(child_config) as context:
|
|
559
|
+
stream = runnable.astream(
|
|
560
|
+
input,
|
|
561
|
+
child_config,
|
|
562
|
+
**kwargs,
|
|
563
|
+
)
|
|
564
|
+
chunk = await coro_with_context(anext(stream), context)
|
|
565
|
+
except self.exceptions_to_handle as e:
|
|
566
|
+
first_error = e if first_error is None else first_error
|
|
567
|
+
last_error = e
|
|
568
|
+
except BaseException as e:
|
|
569
|
+
await run_manager.on_chain_error(e)
|
|
570
|
+
raise
|
|
571
|
+
else:
|
|
572
|
+
first_error = None
|
|
573
|
+
break
|
|
574
|
+
if first_error:
|
|
575
|
+
await run_manager.on_chain_error(first_error)
|
|
576
|
+
raise first_error
|
|
577
|
+
|
|
578
|
+
yield chunk
|
|
579
|
+
output: Output | None = chunk
|
|
580
|
+
try:
|
|
581
|
+
async for chunk in stream:
|
|
582
|
+
yield chunk
|
|
583
|
+
try:
|
|
584
|
+
output = output + chunk # type: ignore[operator]
|
|
585
|
+
except TypeError:
|
|
586
|
+
output = None
|
|
587
|
+
except BaseException as e:
|
|
588
|
+
await run_manager.on_chain_error(e)
|
|
589
|
+
raise
|
|
590
|
+
await run_manager.on_chain_end(output)
|
|
591
|
+
|
|
592
|
+
def __getattr__(self, name: str) -> Any:
|
|
593
|
+
"""Get an attribute from the wrapped `Runnable` and its fallbacks.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
If the attribute is anything other than a method that outputs a `Runnable`,
|
|
597
|
+
returns `getattr(self.runnable, name)`. If the attribute is a method that
|
|
598
|
+
does return a new `Runnable` (e.g. `model.bind_tools([...])` outputs a new
|
|
599
|
+
`RunnableBinding`) then `self.runnable` and each of the runnables in
|
|
600
|
+
`self.fallbacks` is replaced with `getattr(x, name)`.
|
|
601
|
+
|
|
602
|
+
Example:
|
|
603
|
+
```python
|
|
604
|
+
from coco_openai import ChatOpenAI
|
|
605
|
+
from coco_anthropic import ChatAnthropic
|
|
606
|
+
|
|
607
|
+
gpt_55 = ChatOpenAI(model="gpt-5.5")
|
|
608
|
+
claude_3_sonnet = ChatAnthropic(model="claude-sonnet-5")
|
|
609
|
+
model = gpt_55.with_fallbacks([claude_3_sonnet])
|
|
610
|
+
|
|
611
|
+
model.model_name
|
|
612
|
+
# -> "gpt-5.5"
|
|
613
|
+
|
|
614
|
+
# .bind_tools() is called on both ChatOpenAI and ChatAnthropic
|
|
615
|
+
# Equivalent to:
|
|
616
|
+
# gpt_55.bind_tools([...]).with_fallbacks([claude_3_sonnet.bind_tools([...])])
|
|
617
|
+
model.bind_tools([...])
|
|
618
|
+
# -> RunnableWithFallbacks(
|
|
619
|
+
runnable=RunnableBinding(bound=ChatOpenAI(...), kwargs={"tools": [...]}),
|
|
620
|
+
fallbacks=[RunnableBinding(bound=ChatAnthropic(...), kwargs={"tools": [...]})],
|
|
621
|
+
)
|
|
622
|
+
```
|
|
623
|
+
""" # noqa: E501
|
|
624
|
+
attr = getattr(self.runnable, name)
|
|
625
|
+
if _returns_runnable(attr):
|
|
626
|
+
|
|
627
|
+
@wraps(attr)
|
|
628
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
629
|
+
new_runnable = attr(*args, **kwargs)
|
|
630
|
+
new_fallbacks = []
|
|
631
|
+
for fallback in self.fallbacks:
|
|
632
|
+
fallback_attr = getattr(fallback, name)
|
|
633
|
+
new_fallbacks.append(fallback_attr(*args, **kwargs))
|
|
634
|
+
|
|
635
|
+
return self.__class__(
|
|
636
|
+
**{
|
|
637
|
+
**self.model_dump(),
|
|
638
|
+
"runnable": new_runnable,
|
|
639
|
+
"fallbacks": new_fallbacks,
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return wrapped
|
|
644
|
+
|
|
645
|
+
return attr
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _returns_runnable(attr: Any) -> bool:
|
|
649
|
+
if not callable(attr):
|
|
650
|
+
return False
|
|
651
|
+
return_type = typing.get_type_hints(attr).get("return")
|
|
652
|
+
return bool(return_type and _is_runnable_type(return_type))
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _is_runnable_type(type_: Any) -> bool:
|
|
656
|
+
if inspect.isclass(type_):
|
|
657
|
+
return issubclass(type_, Runnable)
|
|
658
|
+
origin = getattr(type_, "__origin__", None)
|
|
659
|
+
if inspect.isclass(origin):
|
|
660
|
+
return issubclass(origin, Runnable)
|
|
661
|
+
if origin is typing.Union:
|
|
662
|
+
return all(_is_runnable_type(t) for t in type_.__args__)
|
|
663
|
+
return False
|