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,327 @@
|
|
|
1
|
+
"""Model retry middleware for agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from coco_core.messages import AIMessage
|
|
10
|
+
from coco_graph.errors import GraphBubbleUp
|
|
11
|
+
|
|
12
|
+
from coco.agents.middleware._retry import (
|
|
13
|
+
OnFailure,
|
|
14
|
+
RetryOn,
|
|
15
|
+
calculate_delay,
|
|
16
|
+
default_retry_on,
|
|
17
|
+
should_retry_exception,
|
|
18
|
+
validate_retry_params,
|
|
19
|
+
)
|
|
20
|
+
from coco.agents.middleware.types import (
|
|
21
|
+
AgentMiddleware,
|
|
22
|
+
AgentState,
|
|
23
|
+
ContextT,
|
|
24
|
+
ModelRequest,
|
|
25
|
+
ModelResponse,
|
|
26
|
+
ResponseT,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Awaitable, Callable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ModelRetryMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
|
|
34
|
+
"""Middleware that automatically retries failed model calls with configurable backoff.
|
|
35
|
+
|
|
36
|
+
Supports retrying on specific exceptions and exponential backoff.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
!!! example "Basic usage with default settings (2 retries, exponential backoff)"
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from coco.agents import create_agent
|
|
43
|
+
from coco.agents.middleware import ModelRetryMiddleware
|
|
44
|
+
|
|
45
|
+
agent = create_agent(model, tools=[search_tool], middleware=[ModelRetryMiddleware()])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
!!! example "Retry specific exceptions only"
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from anthropic import RateLimitError
|
|
52
|
+
from openai import APITimeoutError
|
|
53
|
+
|
|
54
|
+
retry = ModelRetryMiddleware(
|
|
55
|
+
max_retries=4,
|
|
56
|
+
retry_on=(APITimeoutError, RateLimitError),
|
|
57
|
+
backoff_factor=1.5,
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
!!! example "Custom exception filtering"
|
|
62
|
+
|
|
63
|
+
Exceptions for which the callable returns `False` propagate to the caller.
|
|
64
|
+
They are not passed to `on_failure`.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from anthropic import APIStatusError
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def should_retry(exc: Exception) -> bool:
|
|
71
|
+
# Retry 5xx; anything else propagates
|
|
72
|
+
if isinstance(exc, APIStatusError):
|
|
73
|
+
return 500 <= exc.status_code < 600
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
retry = ModelRetryMiddleware(
|
|
78
|
+
max_retries=3,
|
|
79
|
+
retry_on=should_retry,
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
!!! example "Custom error handling"
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def format_error(exc: Exception) -> str:
|
|
87
|
+
return "Model temporarily unavailable. Please try again later."
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
retry = ModelRetryMiddleware(
|
|
91
|
+
max_retries=4,
|
|
92
|
+
on_failure=format_error,
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
!!! example "Constant backoff (no exponential growth)"
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
retry = ModelRetryMiddleware(
|
|
100
|
+
max_retries=5,
|
|
101
|
+
backoff_factor=0.0, # No exponential growth
|
|
102
|
+
initial_delay=2.0, # Always wait 2 seconds
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
!!! example "Raise exception on failure"
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
retry = ModelRetryMiddleware(
|
|
110
|
+
max_retries=2,
|
|
111
|
+
on_failure="error", # Re-raise exception instead of returning message
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
max_retries: int = 2,
|
|
120
|
+
retry_on: RetryOn = default_retry_on,
|
|
121
|
+
on_failure: OnFailure = "continue",
|
|
122
|
+
backoff_factor: float = 2.0,
|
|
123
|
+
initial_delay: float = 1.0,
|
|
124
|
+
max_delay: float = 60.0,
|
|
125
|
+
jitter: bool = True,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Initialize `ModelRetryMiddleware`.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
max_retries: Maximum number of retry attempts after the initial call.
|
|
131
|
+
|
|
132
|
+
Must be `>= 0`.
|
|
133
|
+
retry_on: Either a tuple of exception types to retry on, or a callable
|
|
134
|
+
that takes an exception and returns `True` if it should be retried.
|
|
135
|
+
|
|
136
|
+
By default, retryable model errors and all unclassified exceptions are
|
|
137
|
+
retried. Exceptions that do not match propagate immediately and are not
|
|
138
|
+
handled by `on_failure`.
|
|
139
|
+
on_failure: Behavior when all retries are exhausted.
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
|
|
143
|
+
- `'continue'`: Return an `AIMessage` with error details,
|
|
144
|
+
allowing the agent to continue with an error response.
|
|
145
|
+
- `'error'`: Re-raise the exception, stopping agent execution.
|
|
146
|
+
- **Custom callable:** Function that takes the exception and returns a
|
|
147
|
+
string for the `AIMessage` content, allowing custom error
|
|
148
|
+
formatting.
|
|
149
|
+
backoff_factor: Multiplier for exponential backoff.
|
|
150
|
+
|
|
151
|
+
Each retry waits `initial_delay * (backoff_factor ** retry_number)`
|
|
152
|
+
seconds.
|
|
153
|
+
|
|
154
|
+
Set to `0.0` for constant delay.
|
|
155
|
+
initial_delay: Initial delay in seconds before first retry.
|
|
156
|
+
max_delay: Maximum delay in seconds between retries.
|
|
157
|
+
|
|
158
|
+
Caps exponential backoff growth.
|
|
159
|
+
jitter: Whether to add random jitter (`±25%`) to delay to avoid thundering herd.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If `max_retries < 0` or delays are negative.
|
|
163
|
+
"""
|
|
164
|
+
super().__init__()
|
|
165
|
+
|
|
166
|
+
# Validate parameters
|
|
167
|
+
validate_retry_params(max_retries, initial_delay, max_delay, backoff_factor)
|
|
168
|
+
|
|
169
|
+
self.max_retries = max_retries
|
|
170
|
+
self.tools = [] # No additional tools registered by this middleware
|
|
171
|
+
self.retry_on = retry_on
|
|
172
|
+
self.on_failure = on_failure
|
|
173
|
+
self.backoff_factor = backoff_factor
|
|
174
|
+
self.initial_delay = initial_delay
|
|
175
|
+
self.max_delay = max_delay
|
|
176
|
+
self.jitter = jitter
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _format_failure_message(exc: Exception, attempts_made: int) -> AIMessage:
|
|
180
|
+
"""Format the failure message when retries are exhausted.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
exc: The exception that caused the failure.
|
|
184
|
+
attempts_made: Number of attempts actually made.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
`AIMessage` with formatted error message.
|
|
188
|
+
"""
|
|
189
|
+
exc_type = type(exc).__name__
|
|
190
|
+
exc_msg = str(exc)
|
|
191
|
+
attempt_word = "attempt" if attempts_made == 1 else "attempts"
|
|
192
|
+
content = (
|
|
193
|
+
f"Model call failed after {attempts_made} {attempt_word} with {exc_type}: {exc_msg}"
|
|
194
|
+
)
|
|
195
|
+
return AIMessage(content=content)
|
|
196
|
+
|
|
197
|
+
def _handle_failure(self, exc: Exception, attempts_made: int) -> ModelResponse[ResponseT]:
|
|
198
|
+
"""Handle failure when all retries are exhausted.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
exc: The exception that caused the failure.
|
|
202
|
+
attempts_made: Number of attempts actually made.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
`ModelResponse` with error details.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
Exception: If `on_failure` is `'error'`, re-raises the exception.
|
|
209
|
+
"""
|
|
210
|
+
if self.on_failure == "error":
|
|
211
|
+
raise exc
|
|
212
|
+
|
|
213
|
+
if callable(self.on_failure):
|
|
214
|
+
content = self.on_failure(exc)
|
|
215
|
+
ai_msg = AIMessage(content=content)
|
|
216
|
+
else:
|
|
217
|
+
ai_msg = self._format_failure_message(exc, attempts_made)
|
|
218
|
+
|
|
219
|
+
return ModelResponse(result=[ai_msg])
|
|
220
|
+
|
|
221
|
+
def wrap_model_call(
|
|
222
|
+
self,
|
|
223
|
+
request: ModelRequest[ContextT],
|
|
224
|
+
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
|
|
225
|
+
) -> ModelResponse[ResponseT] | AIMessage:
|
|
226
|
+
"""Intercept model execution and retry on failure.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
request: Model request with model, messages, state, and runtime.
|
|
230
|
+
handler: Callable to execute the model (can be called multiple times).
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
`ModelResponse` or `AIMessage` (the final result).
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
Exception: Any exception not matched by `retry_on` is re-raised immediately,
|
|
237
|
+
without being passed to `on_failure`.
|
|
238
|
+
RuntimeError: If the retry loop completes without returning. (This should not happen.)
|
|
239
|
+
"""
|
|
240
|
+
# Initial attempt + retries
|
|
241
|
+
for attempt in range(self.max_retries + 1):
|
|
242
|
+
try:
|
|
243
|
+
return handler(request)
|
|
244
|
+
except GraphBubbleUp:
|
|
245
|
+
raise
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
attempts_made = attempt + 1 # attempt is 0-indexed
|
|
248
|
+
|
|
249
|
+
# Check if we should retry this exception
|
|
250
|
+
if not should_retry_exception(exc, self.retry_on):
|
|
251
|
+
# Exception is not retryable, re-raise immediately
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
# Check if we have more retries left
|
|
255
|
+
if attempt < self.max_retries:
|
|
256
|
+
# Calculate and apply backoff delay
|
|
257
|
+
delay = calculate_delay(
|
|
258
|
+
attempt,
|
|
259
|
+
backoff_factor=self.backoff_factor,
|
|
260
|
+
initial_delay=self.initial_delay,
|
|
261
|
+
max_delay=self.max_delay,
|
|
262
|
+
jitter=self.jitter,
|
|
263
|
+
)
|
|
264
|
+
if delay > 0:
|
|
265
|
+
time.sleep(delay)
|
|
266
|
+
# Continue to next retry
|
|
267
|
+
else:
|
|
268
|
+
# No more retries, handle failure
|
|
269
|
+
return self._handle_failure(exc, attempts_made)
|
|
270
|
+
|
|
271
|
+
# Unreachable: loop always returns via handler success or _handle_failure
|
|
272
|
+
msg = "Unexpected: retry loop completed without returning"
|
|
273
|
+
raise RuntimeError(msg)
|
|
274
|
+
|
|
275
|
+
async def awrap_model_call(
|
|
276
|
+
self,
|
|
277
|
+
request: ModelRequest[ContextT],
|
|
278
|
+
handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],
|
|
279
|
+
) -> ModelResponse[ResponseT] | AIMessage:
|
|
280
|
+
"""Intercept and control async model execution with retry logic.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
request: Model request with model, messages, state, and runtime.
|
|
284
|
+
handler: Async callable to execute the model and returns `ModelResponse`.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
`ModelResponse` or `AIMessage` (the final result).
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
Exception: Any exception not matched by `retry_on` is re-raised immediately,
|
|
291
|
+
without being passed to `on_failure`.
|
|
292
|
+
RuntimeError: If the retry loop completes without returning. (This should not happen.)
|
|
293
|
+
"""
|
|
294
|
+
# Initial attempt + retries
|
|
295
|
+
for attempt in range(self.max_retries + 1):
|
|
296
|
+
try:
|
|
297
|
+
return await handler(request)
|
|
298
|
+
except GraphBubbleUp:
|
|
299
|
+
raise
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
attempts_made = attempt + 1 # attempt is 0-indexed
|
|
302
|
+
|
|
303
|
+
# Check if we should retry this exception
|
|
304
|
+
if not should_retry_exception(exc, self.retry_on):
|
|
305
|
+
# Exception is not retryable, re-raise immediately
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
# Check if we have more retries left
|
|
309
|
+
if attempt < self.max_retries:
|
|
310
|
+
# Calculate and apply backoff delay
|
|
311
|
+
delay = calculate_delay(
|
|
312
|
+
attempt,
|
|
313
|
+
backoff_factor=self.backoff_factor,
|
|
314
|
+
initial_delay=self.initial_delay,
|
|
315
|
+
max_delay=self.max_delay,
|
|
316
|
+
jitter=self.jitter,
|
|
317
|
+
)
|
|
318
|
+
if delay > 0:
|
|
319
|
+
await asyncio.sleep(delay)
|
|
320
|
+
# Continue to next retry
|
|
321
|
+
else:
|
|
322
|
+
# No more retries, handle failure
|
|
323
|
+
return self._handle_failure(exc, attempts_made)
|
|
324
|
+
|
|
325
|
+
# Unreachable: loop always returns via handler success or _handle_failure
|
|
326
|
+
msg = "Unexpected: retry loop completed without returning"
|
|
327
|
+
raise RuntimeError(msg)
|