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,421 @@
|
|
|
1
|
+
"""Tool retry middleware for agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from coco_core.messages import ToolMessage
|
|
11
|
+
from coco_graph.errors import GraphBubbleUp
|
|
12
|
+
|
|
13
|
+
from coco.agents.middleware._retry import (
|
|
14
|
+
OnFailure,
|
|
15
|
+
RetryOn,
|
|
16
|
+
calculate_delay,
|
|
17
|
+
default_retry_on,
|
|
18
|
+
should_retry_exception,
|
|
19
|
+
validate_retry_params,
|
|
20
|
+
)
|
|
21
|
+
from coco.agents.middleware.types import AgentMiddleware, AgentState, ContextT, ResponseT
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Awaitable, Callable
|
|
25
|
+
|
|
26
|
+
from coco_graph.types import Command
|
|
27
|
+
|
|
28
|
+
from coco.agents.middleware.types import ToolCallRequest
|
|
29
|
+
from coco.tools import BaseTool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ToolRetryMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
|
|
33
|
+
"""Middleware that automatically retries failed tool calls with configurable backoff.
|
|
34
|
+
|
|
35
|
+
Supports retrying on specific exceptions and exponential backoff.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
!!! example "Basic usage with default settings (2 retries, exponential backoff)"
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from coco.agents import create_agent
|
|
42
|
+
from coco.agents.middleware import ToolRetryMiddleware
|
|
43
|
+
|
|
44
|
+
agent = create_agent(model, tools=[search_tool], middleware=[ToolRetryMiddleware()])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
!!! example "Retry specific exceptions only"
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from requests.exceptions import RequestException, Timeout
|
|
51
|
+
|
|
52
|
+
retry = ToolRetryMiddleware(
|
|
53
|
+
max_retries=4,
|
|
54
|
+
retry_on=(RequestException, Timeout),
|
|
55
|
+
backoff_factor=1.5,
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
!!! example "Custom exception filtering"
|
|
60
|
+
|
|
61
|
+
Exceptions for which the callable returns `False` propagate to the caller.
|
|
62
|
+
They are not passed to `on_failure`.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from requests.exceptions import HTTPError
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def should_retry(exc: Exception) -> bool:
|
|
69
|
+
# Retry 5xx; anything else propagates
|
|
70
|
+
if isinstance(exc, HTTPError):
|
|
71
|
+
return 500 <= exc.status_code < 600
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
retry = ToolRetryMiddleware(
|
|
76
|
+
max_retries=3,
|
|
77
|
+
retry_on=should_retry,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
!!! example "Apply to specific tools with custom error handling"
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def format_error(exc: Exception) -> str:
|
|
85
|
+
return "Database temporarily unavailable. Please try again later."
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
retry = ToolRetryMiddleware(
|
|
89
|
+
max_retries=4,
|
|
90
|
+
tools=["search_database"],
|
|
91
|
+
on_failure=format_error,
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
!!! example "Apply to specific tools using `BaseTool` instances"
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from coco_core.tools import tool
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@tool
|
|
102
|
+
def search_database(query: str) -> str:
|
|
103
|
+
'''Search the database.'''
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
retry = ToolRetryMiddleware(
|
|
108
|
+
max_retries=4,
|
|
109
|
+
tools=[search_database], # Pass BaseTool instance
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
!!! example "Constant backoff (no exponential growth)"
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
retry = ToolRetryMiddleware(
|
|
117
|
+
max_retries=5,
|
|
118
|
+
backoff_factor=0.0, # No exponential growth
|
|
119
|
+
initial_delay=2.0, # Always wait 2 seconds
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
!!! example "Raise exception on failure"
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
retry = ToolRetryMiddleware(
|
|
127
|
+
max_retries=2,
|
|
128
|
+
on_failure="error", # Re-raise exception instead of returning message
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
max_retries: int = 2,
|
|
137
|
+
tools: list[BaseTool | str] | None = None,
|
|
138
|
+
retry_on: RetryOn = default_retry_on,
|
|
139
|
+
on_failure: OnFailure = "continue",
|
|
140
|
+
backoff_factor: float = 2.0,
|
|
141
|
+
initial_delay: float = 1.0,
|
|
142
|
+
max_delay: float = 60.0,
|
|
143
|
+
jitter: bool = True,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Initialize `ToolRetryMiddleware`.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
max_retries: Maximum number of retry attempts after the initial call.
|
|
149
|
+
|
|
150
|
+
Must be `>= 0`.
|
|
151
|
+
tools: Optional list of tools or tool names to apply retry logic to.
|
|
152
|
+
|
|
153
|
+
Can be a list of `BaseTool` instances or tool name strings.
|
|
154
|
+
|
|
155
|
+
If `None`, applies to all tools.
|
|
156
|
+
retry_on: Either a tuple of exception types to retry on, or a callable
|
|
157
|
+
that takes an exception and returns `True` if it should be retried.
|
|
158
|
+
|
|
159
|
+
By default, retryable model errors and all unclassified exceptions are
|
|
160
|
+
retried. Exceptions that do not match propagate immediately and are not
|
|
161
|
+
handled by `on_failure`.
|
|
162
|
+
on_failure: Behavior when all retries are exhausted.
|
|
163
|
+
|
|
164
|
+
Options:
|
|
165
|
+
|
|
166
|
+
- `'continue'`: Return a `ToolMessage` with error details,
|
|
167
|
+
allowing the LLM to handle the failure and potentially recover.
|
|
168
|
+
- `'error'`: Re-raise the exception, stopping agent execution.
|
|
169
|
+
- **Custom callable:** Function that takes the exception and returns a
|
|
170
|
+
string for the `ToolMessage` content, allowing custom error
|
|
171
|
+
formatting.
|
|
172
|
+
|
|
173
|
+
**Deprecated values** (for backwards compatibility):
|
|
174
|
+
|
|
175
|
+
- `'return_message'`: Use `'continue'` instead.
|
|
176
|
+
- `'raise'`: Use `'error'` instead.
|
|
177
|
+
backoff_factor: Multiplier for exponential backoff.
|
|
178
|
+
|
|
179
|
+
Each retry waits `initial_delay * (backoff_factor ** retry_number)`
|
|
180
|
+
seconds.
|
|
181
|
+
|
|
182
|
+
Set to `0.0` for constant delay.
|
|
183
|
+
initial_delay: Initial delay in seconds before first retry.
|
|
184
|
+
max_delay: Maximum delay in seconds between retries.
|
|
185
|
+
|
|
186
|
+
Caps exponential backoff growth.
|
|
187
|
+
jitter: Whether to add random jitter (`±25%`) to delay to avoid thundering herd.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: If `max_retries < 0` or delays are negative.
|
|
191
|
+
"""
|
|
192
|
+
super().__init__()
|
|
193
|
+
|
|
194
|
+
# Validate parameters
|
|
195
|
+
validate_retry_params(max_retries, initial_delay, max_delay, backoff_factor)
|
|
196
|
+
|
|
197
|
+
# Handle backwards compatibility for deprecated on_failure values
|
|
198
|
+
if on_failure == "raise": # type: ignore[comparison-overlap]
|
|
199
|
+
msg = ( # type: ignore[unreachable]
|
|
200
|
+
"on_failure='raise' is deprecated and will be removed in a future version. "
|
|
201
|
+
"Use on_failure='error' instead."
|
|
202
|
+
)
|
|
203
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
204
|
+
on_failure = "error"
|
|
205
|
+
elif on_failure == "return_message": # type: ignore[comparison-overlap]
|
|
206
|
+
msg = ( # type: ignore[unreachable]
|
|
207
|
+
"on_failure='return_message' is deprecated and will be removed "
|
|
208
|
+
"in a future version. Use on_failure='continue' instead."
|
|
209
|
+
)
|
|
210
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
211
|
+
on_failure = "continue"
|
|
212
|
+
|
|
213
|
+
self.max_retries = max_retries
|
|
214
|
+
|
|
215
|
+
# Extract tool names from BaseTool instances or strings
|
|
216
|
+
self._tool_filter: list[str] | None
|
|
217
|
+
if tools is not None:
|
|
218
|
+
self._tool_filter = [tool.name if not isinstance(tool, str) else tool for tool in tools]
|
|
219
|
+
else:
|
|
220
|
+
self._tool_filter = None
|
|
221
|
+
|
|
222
|
+
self.tools = [] # No additional tools registered by this middleware
|
|
223
|
+
self.retry_on = retry_on
|
|
224
|
+
self.on_failure = on_failure
|
|
225
|
+
self.backoff_factor = backoff_factor
|
|
226
|
+
self.initial_delay = initial_delay
|
|
227
|
+
self.max_delay = max_delay
|
|
228
|
+
self.jitter = jitter
|
|
229
|
+
|
|
230
|
+
def _should_retry_tool(self, tool_name: str) -> bool:
|
|
231
|
+
"""Check if retry logic should apply to this tool.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
tool_name: Name of the tool being called.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
`True` if retry logic should apply, `False` otherwise.
|
|
238
|
+
"""
|
|
239
|
+
if self._tool_filter is None:
|
|
240
|
+
return True
|
|
241
|
+
return tool_name in self._tool_filter
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _format_failure_message(tool_name: str, exc: Exception, attempts_made: int) -> str:
|
|
245
|
+
"""Format the failure message when retries are exhausted.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
tool_name: Name of the tool that failed.
|
|
249
|
+
exc: The exception that caused the failure.
|
|
250
|
+
attempts_made: Number of attempts actually made.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Formatted error message string.
|
|
254
|
+
"""
|
|
255
|
+
exc_type = type(exc).__name__
|
|
256
|
+
exc_msg = str(exc)
|
|
257
|
+
attempt_word = "attempt" if attempts_made == 1 else "attempts"
|
|
258
|
+
return (
|
|
259
|
+
f"Tool '{tool_name}' failed after {attempts_made} {attempt_word} "
|
|
260
|
+
f"with {exc_type}: {exc_msg}. Please try again."
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _handle_failure(
|
|
264
|
+
self, tool_name: str, tool_call_id: str | None, exc: Exception, attempts_made: int
|
|
265
|
+
) -> ToolMessage:
|
|
266
|
+
"""Handle failure when all retries are exhausted.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
tool_name: Name of the tool that failed.
|
|
270
|
+
tool_call_id: ID of the tool call (may be `None`).
|
|
271
|
+
exc: The exception that caused the failure.
|
|
272
|
+
attempts_made: Number of attempts actually made.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
`ToolMessage` with error details.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
Exception: If `on_failure` is `'error'`, re-raises the exception.
|
|
279
|
+
"""
|
|
280
|
+
if self.on_failure == "error":
|
|
281
|
+
raise exc
|
|
282
|
+
|
|
283
|
+
if callable(self.on_failure):
|
|
284
|
+
content = self.on_failure(exc)
|
|
285
|
+
else:
|
|
286
|
+
content = self._format_failure_message(tool_name, exc, attempts_made)
|
|
287
|
+
|
|
288
|
+
return ToolMessage(
|
|
289
|
+
content=content,
|
|
290
|
+
tool_call_id=tool_call_id,
|
|
291
|
+
name=tool_name,
|
|
292
|
+
status="error",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def wrap_tool_call(
|
|
296
|
+
self,
|
|
297
|
+
request: ToolCallRequest,
|
|
298
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
|
|
299
|
+
) -> ToolMessage | Command[Any]:
|
|
300
|
+
"""Intercept tool execution and retry on failure.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
request: Tool call request with call dict, `BaseTool`, state, and runtime.
|
|
304
|
+
handler: Callable to execute the tool (can be called multiple times).
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
`ToolMessage` or `Command` (the final result).
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
Exception: Any exception not matched by `retry_on` is re-raised immediately,
|
|
311
|
+
without being passed to `on_failure`.
|
|
312
|
+
RuntimeError: If the retry loop completes without returning. This should not happen.
|
|
313
|
+
"""
|
|
314
|
+
tool_name = request.tool.name if request.tool else request.tool_call["name"]
|
|
315
|
+
|
|
316
|
+
# Check if retry should apply to this tool
|
|
317
|
+
if not self._should_retry_tool(tool_name):
|
|
318
|
+
return handler(request)
|
|
319
|
+
|
|
320
|
+
tool_call_id = request.tool_call["id"]
|
|
321
|
+
|
|
322
|
+
# Initial attempt + retries
|
|
323
|
+
for attempt in range(self.max_retries + 1):
|
|
324
|
+
try:
|
|
325
|
+
return handler(request)
|
|
326
|
+
except GraphBubbleUp:
|
|
327
|
+
# Control-flow signals (interrupts, parent commands) must
|
|
328
|
+
# propagate, not be retried or converted to error messages.
|
|
329
|
+
raise
|
|
330
|
+
except Exception as exc:
|
|
331
|
+
attempts_made = attempt + 1 # attempt is 0-indexed
|
|
332
|
+
|
|
333
|
+
# Check if we should retry this exception
|
|
334
|
+
if not should_retry_exception(exc, self.retry_on):
|
|
335
|
+
# Exception is not retryable, re-raise immediately
|
|
336
|
+
raise
|
|
337
|
+
|
|
338
|
+
# Check if we have more retries left
|
|
339
|
+
if attempt < self.max_retries:
|
|
340
|
+
# Calculate and apply backoff delay
|
|
341
|
+
delay = calculate_delay(
|
|
342
|
+
attempt,
|
|
343
|
+
backoff_factor=self.backoff_factor,
|
|
344
|
+
initial_delay=self.initial_delay,
|
|
345
|
+
max_delay=self.max_delay,
|
|
346
|
+
jitter=self.jitter,
|
|
347
|
+
)
|
|
348
|
+
if delay > 0:
|
|
349
|
+
time.sleep(delay)
|
|
350
|
+
# Continue to next retry
|
|
351
|
+
else:
|
|
352
|
+
# No more retries, handle failure
|
|
353
|
+
return self._handle_failure(tool_name, tool_call_id, exc, attempts_made)
|
|
354
|
+
|
|
355
|
+
# Unreachable: loop always returns via handler success or _handle_failure
|
|
356
|
+
msg = "Unexpected: retry loop completed without returning"
|
|
357
|
+
raise RuntimeError(msg)
|
|
358
|
+
|
|
359
|
+
async def awrap_tool_call(
|
|
360
|
+
self,
|
|
361
|
+
request: ToolCallRequest,
|
|
362
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
|
|
363
|
+
) -> ToolMessage | Command[Any]:
|
|
364
|
+
"""Intercept and control async tool execution with retry logic.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
request: Tool call request with call `dict`, `BaseTool`, state, and runtime.
|
|
368
|
+
handler: Async callable to execute the tool and returns `ToolMessage` or `Command`.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
`ToolMessage` or `Command` (the final result).
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
Exception: Any exception not matched by `retry_on` is re-raised immediately,
|
|
375
|
+
without being passed to `on_failure`.
|
|
376
|
+
RuntimeError: If the retry loop completes without returning. This should not happen.
|
|
377
|
+
"""
|
|
378
|
+
tool_name = request.tool.name if request.tool else request.tool_call["name"]
|
|
379
|
+
|
|
380
|
+
# Check if retry should apply to this tool
|
|
381
|
+
if not self._should_retry_tool(tool_name):
|
|
382
|
+
return await handler(request)
|
|
383
|
+
|
|
384
|
+
tool_call_id = request.tool_call["id"]
|
|
385
|
+
|
|
386
|
+
# Initial attempt + retries
|
|
387
|
+
for attempt in range(self.max_retries + 1):
|
|
388
|
+
try:
|
|
389
|
+
return await handler(request)
|
|
390
|
+
except GraphBubbleUp:
|
|
391
|
+
# Control-flow signals (interrupts, parent commands) must
|
|
392
|
+
# propagate, not be retried or converted to error messages.
|
|
393
|
+
raise
|
|
394
|
+
except Exception as exc:
|
|
395
|
+
attempts_made = attempt + 1 # attempt is 0-indexed
|
|
396
|
+
|
|
397
|
+
# Check if we should retry this exception
|
|
398
|
+
if not should_retry_exception(exc, self.retry_on):
|
|
399
|
+
# Exception is not retryable, re-raise immediately
|
|
400
|
+
raise
|
|
401
|
+
|
|
402
|
+
# Check if we have more retries left
|
|
403
|
+
if attempt < self.max_retries:
|
|
404
|
+
# Calculate and apply backoff delay
|
|
405
|
+
delay = calculate_delay(
|
|
406
|
+
attempt,
|
|
407
|
+
backoff_factor=self.backoff_factor,
|
|
408
|
+
initial_delay=self.initial_delay,
|
|
409
|
+
max_delay=self.max_delay,
|
|
410
|
+
jitter=self.jitter,
|
|
411
|
+
)
|
|
412
|
+
if delay > 0:
|
|
413
|
+
await asyncio.sleep(delay)
|
|
414
|
+
# Continue to next retry
|
|
415
|
+
else:
|
|
416
|
+
# No more retries, handle failure
|
|
417
|
+
return self._handle_failure(tool_name, tool_call_id, exc, attempts_made)
|
|
418
|
+
|
|
419
|
+
# Unreachable: loop always returns via handler success or _handle_failure
|
|
420
|
+
msg = "Unexpected: retry loop completed without returning"
|
|
421
|
+
raise RuntimeError(msg)
|