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,251 @@
|
|
|
1
|
+
"""Tool emulator middleware for testing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic
|
|
7
|
+
|
|
8
|
+
from coco_core.language_models.chat_models import BaseChatModel
|
|
9
|
+
from coco_core.messages import HumanMessage, ToolMessage
|
|
10
|
+
|
|
11
|
+
from coco.agents.middleware.internal_call_transformer import (
|
|
12
|
+
InternalCallTransformer,
|
|
13
|
+
internal_call_metadata,
|
|
14
|
+
)
|
|
15
|
+
from coco.agents.middleware.types import AgentMiddleware, AgentState, ContextT
|
|
16
|
+
from coco.chat_models.base import init_chat_model
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Awaitable, Callable
|
|
20
|
+
|
|
21
|
+
from coco_graph.types import Command
|
|
22
|
+
|
|
23
|
+
from coco.agents.middleware.types import ToolCallRequest
|
|
24
|
+
from coco.tools import BaseTool
|
|
25
|
+
|
|
26
|
+
_DEFAULT_EMULATOR_MODEL = "anthropic:claude-sonnet-4-5-20250929"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LLMToolEmulator(AgentMiddleware[AgentState[Any], ContextT], Generic[ContextT]):
|
|
30
|
+
"""Emulates specified tools using an LLM instead of executing them.
|
|
31
|
+
|
|
32
|
+
This middleware allows selective emulation of tools for testing purposes.
|
|
33
|
+
|
|
34
|
+
By default (when `tools=None`), all tools are emulated. You can specify which
|
|
35
|
+
tools to emulate by passing a list of tool names or `BaseTool` instances.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
!!! example "Emulate all tools (default behavior)"
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from coco.agents.middleware import LLMToolEmulator
|
|
42
|
+
|
|
43
|
+
middleware = LLMToolEmulator()
|
|
44
|
+
|
|
45
|
+
agent = create_agent(
|
|
46
|
+
model="openai:gpt-5.5",
|
|
47
|
+
tools=[get_weather, get_user_location, calculator],
|
|
48
|
+
middleware=[middleware],
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
!!! example "Emulate specific tools by name"
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
middleware = LLMToolEmulator(tools=["get_weather", "get_user_location"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
!!! example "Use a custom model for emulation"
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
middleware = LLMToolEmulator(
|
|
62
|
+
tools=["get_weather"], model="anthropic:claude-sonnet-4-5-20250929"
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
!!! example "Emulate specific tools by passing tool instances"
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
middleware = LLMToolEmulator(tools=[get_weather, get_user_location])
|
|
70
|
+
```
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
transformers = (InternalCallTransformer,)
|
|
74
|
+
"""Keeps the emulation model call's tokens out of `run.messages`.
|
|
75
|
+
|
|
76
|
+
Registered only when this middleware is used — see
|
|
77
|
+
`InternalCallTransformer` for why the call needs tagging and filtering.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
tools: list[str | BaseTool] | None = None,
|
|
84
|
+
model: str | BaseChatModel | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Initialize the tool emulator.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tools: List of tool names (`str`) or `BaseTool` instances to emulate.
|
|
90
|
+
|
|
91
|
+
If `None`, ALL tools will be emulated.
|
|
92
|
+
|
|
93
|
+
If empty list, no tools will be emulated.
|
|
94
|
+
model: Model to use for emulation.
|
|
95
|
+
|
|
96
|
+
Defaults to `'anthropic:claude-sonnet-4-5-20250929'`, which requires
|
|
97
|
+
`trustcoco-anthropic` to be installed.
|
|
98
|
+
|
|
99
|
+
!!! warning "Deprecated"
|
|
100
|
+
Relying on the implicit default is deprecated and will be
|
|
101
|
+
removed in a future release, since it makes this middleware
|
|
102
|
+
depend on `trustcoco-anthropic` even when unspecified. Pass
|
|
103
|
+
`model` explicitly instead.
|
|
104
|
+
|
|
105
|
+
Can be a model identifier string or `BaseChatModel` instance.
|
|
106
|
+
"""
|
|
107
|
+
super().__init__()
|
|
108
|
+
|
|
109
|
+
# Extract tool names from tools
|
|
110
|
+
# None means emulate all tools
|
|
111
|
+
self.emulate_all = tools is None
|
|
112
|
+
self.tools_to_emulate: set[str] = set()
|
|
113
|
+
|
|
114
|
+
if tools is not None:
|
|
115
|
+
for tool in tools:
|
|
116
|
+
if isinstance(tool, str):
|
|
117
|
+
self.tools_to_emulate.add(tool)
|
|
118
|
+
else:
|
|
119
|
+
# Assume BaseTool with .name attribute
|
|
120
|
+
self.tools_to_emulate.add(tool.name)
|
|
121
|
+
|
|
122
|
+
# Initialize emulator model
|
|
123
|
+
if model is None:
|
|
124
|
+
warnings.warn(
|
|
125
|
+
"LLMToolEmulator's default model "
|
|
126
|
+
f"({_DEFAULT_EMULATOR_MODEL!r}) is deprecated and will be removed "
|
|
127
|
+
"in a future release. Pass `model` explicitly instead.",
|
|
128
|
+
DeprecationWarning,
|
|
129
|
+
stacklevel=2,
|
|
130
|
+
)
|
|
131
|
+
try:
|
|
132
|
+
self.model = init_chat_model(_DEFAULT_EMULATOR_MODEL, temperature=1)
|
|
133
|
+
except ImportError as e:
|
|
134
|
+
msg = (
|
|
135
|
+
"LLMToolEmulator's default model requires `trustcoco-anthropic` "
|
|
136
|
+
"to be installed. Install it with `pip install trustcoco-anthropic`, "
|
|
137
|
+
"or pass `model=...` explicitly to use a different provider."
|
|
138
|
+
)
|
|
139
|
+
raise ImportError(msg) from e
|
|
140
|
+
elif isinstance(model, BaseChatModel):
|
|
141
|
+
self.model = model
|
|
142
|
+
else:
|
|
143
|
+
self.model = init_chat_model(model, temperature=1)
|
|
144
|
+
|
|
145
|
+
def wrap_tool_call(
|
|
146
|
+
self,
|
|
147
|
+
request: ToolCallRequest,
|
|
148
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
|
|
149
|
+
) -> ToolMessage | Command[Any]:
|
|
150
|
+
"""Emulate tool execution using LLM if tool should be emulated.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
request: Tool call request to potentially emulate.
|
|
154
|
+
handler: Callback to execute the tool (can be called multiple times).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
ToolMessage with emulated response if tool should be emulated,
|
|
158
|
+
otherwise calls handler for normal execution.
|
|
159
|
+
"""
|
|
160
|
+
tool_name = request.tool_call["name"]
|
|
161
|
+
|
|
162
|
+
# Check if this tool should be emulated
|
|
163
|
+
should_emulate = self.emulate_all or tool_name in self.tools_to_emulate
|
|
164
|
+
|
|
165
|
+
if not should_emulate:
|
|
166
|
+
# Let it execute normally by calling the handler
|
|
167
|
+
return handler(request)
|
|
168
|
+
|
|
169
|
+
# Extract tool information for emulation
|
|
170
|
+
tool_args = request.tool_call["args"]
|
|
171
|
+
tool_description = request.tool.description if request.tool else "No description available"
|
|
172
|
+
|
|
173
|
+
# Build prompt for emulator LLM
|
|
174
|
+
prompt = (
|
|
175
|
+
f"You are emulating a tool call for testing purposes.\n\n"
|
|
176
|
+
f"Tool: {tool_name}\n"
|
|
177
|
+
f"Description: {tool_description}\n"
|
|
178
|
+
f"Arguments: {tool_args}\n\n"
|
|
179
|
+
f"Generate a realistic response that this tool would return "
|
|
180
|
+
f"given these arguments.\n"
|
|
181
|
+
f"Return ONLY the tool's output, no explanation or preamble. "
|
|
182
|
+
f"Introduce variation into your responses."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Get emulated response from LLM
|
|
186
|
+
response = self.model.invoke(
|
|
187
|
+
[HumanMessage(prompt)],
|
|
188
|
+
config={"metadata": {"lc_source": "tool_emulation", **internal_call_metadata()}},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Short-circuit: return emulated result without executing real tool
|
|
192
|
+
return ToolMessage(
|
|
193
|
+
content=response.content,
|
|
194
|
+
tool_call_id=request.tool_call["id"],
|
|
195
|
+
name=tool_name,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def awrap_tool_call(
|
|
199
|
+
self,
|
|
200
|
+
request: ToolCallRequest,
|
|
201
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
|
|
202
|
+
) -> ToolMessage | Command[Any]:
|
|
203
|
+
"""Async version of `wrap_tool_call`.
|
|
204
|
+
|
|
205
|
+
Emulate tool execution using LLM if tool should be emulated.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
request: Tool call request to potentially emulate.
|
|
209
|
+
handler: Async callback to execute the tool (can be called multiple times).
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
ToolMessage with emulated response if tool should be emulated,
|
|
213
|
+
otherwise calls handler for normal execution.
|
|
214
|
+
"""
|
|
215
|
+
tool_name = request.tool_call["name"]
|
|
216
|
+
|
|
217
|
+
# Check if this tool should be emulated
|
|
218
|
+
should_emulate = self.emulate_all or tool_name in self.tools_to_emulate
|
|
219
|
+
|
|
220
|
+
if not should_emulate:
|
|
221
|
+
# Let it execute normally by calling the handler
|
|
222
|
+
return await handler(request)
|
|
223
|
+
|
|
224
|
+
# Extract tool information for emulation
|
|
225
|
+
tool_args = request.tool_call["args"]
|
|
226
|
+
tool_description = request.tool.description if request.tool else "No description available"
|
|
227
|
+
|
|
228
|
+
# Build prompt for emulator LLM
|
|
229
|
+
prompt = (
|
|
230
|
+
f"You are emulating a tool call for testing purposes.\n\n"
|
|
231
|
+
f"Tool: {tool_name}\n"
|
|
232
|
+
f"Description: {tool_description}\n"
|
|
233
|
+
f"Arguments: {tool_args}\n\n"
|
|
234
|
+
f"Generate a realistic response that this tool would return "
|
|
235
|
+
f"given these arguments.\n"
|
|
236
|
+
f"Return ONLY the tool's output, no explanation or preamble. "
|
|
237
|
+
f"Introduce variation into your responses."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Get emulated response from LLM (using async invoke)
|
|
241
|
+
response = await self.model.ainvoke(
|
|
242
|
+
[HumanMessage(prompt)],
|
|
243
|
+
config={"metadata": {"lc_source": "tool_emulation", **internal_call_metadata()}},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Short-circuit: return emulated result without executing real tool
|
|
247
|
+
return ToolMessage(
|
|
248
|
+
content=response.content,
|
|
249
|
+
tool_call_id=request.tool_call["id"],
|
|
250
|
+
name=tool_name,
|
|
251
|
+
)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Tool error middleware for agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
from coco_core.messages import ToolMessage
|
|
8
|
+
from coco_graph.errors import GraphBubbleUp
|
|
9
|
+
|
|
10
|
+
from coco.agents.middleware.types import (
|
|
11
|
+
AgentMiddleware,
|
|
12
|
+
AgentState,
|
|
13
|
+
ContextT,
|
|
14
|
+
ResponseT,
|
|
15
|
+
TracePolicy,
|
|
16
|
+
omit_payload,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
from coco_core.messages import ContentBlock
|
|
23
|
+
from coco_graph.types import Command
|
|
24
|
+
|
|
25
|
+
from coco.agents.middleware.types import ToolCallRequest
|
|
26
|
+
from coco.tools import BaseTool
|
|
27
|
+
|
|
28
|
+
OnError = Callable[[Exception, ToolCallRequest], str | list[ContentBlock] | None]
|
|
29
|
+
"""Sync handler: return content to surface the error as a `ToolMessage`; return
|
|
30
|
+
`None` (or nothing) to let the exception propagate."""
|
|
31
|
+
|
|
32
|
+
AOnError = Callable[[Exception, ToolCallRequest], Awaitable[str | list[ContentBlock] | None]]
|
|
33
|
+
"""Async handler: return content to surface the error as a `ToolMessage`; return
|
|
34
|
+
`None` (or nothing) to let the exception propagate."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ToolErrorMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
|
|
38
|
+
"""Return selected tool-execution exceptions to the model as error `ToolMessage`s.
|
|
39
|
+
|
|
40
|
+
`on_error` is called for each exception raised by tool execution. Return content
|
|
41
|
+
(a `str` or a list of content blocks) to convert the exception into a
|
|
42
|
+
`ToolMessage(status="error")`; return `None` — or simply don't return — to let the
|
|
43
|
+
exception propagate (halting the run). Handling is therefore opt-in — exceptions you
|
|
44
|
+
do not return content for propagate unchanged, so arbitrary internal exceptions are
|
|
45
|
+
never serialized to the model or end user unless you choose to surface them.
|
|
46
|
+
|
|
47
|
+
CocoGraph control-flow signals (interrupts, parent commands) always propagate and
|
|
48
|
+
never reach `on_error`.
|
|
49
|
+
|
|
50
|
+
Prefer returning content that names the exception type over the raw exception message,
|
|
51
|
+
which may carry sensitive or internal detail.
|
|
52
|
+
|
|
53
|
+
Provide at least one of `on_error` or `aon_error`. `aon_error` handles errors on the
|
|
54
|
+
async execution path (falling back to `on_error` when omitted); the sync path only
|
|
55
|
+
ever calls `on_error`. For async-only usage, pass `aon_error` alone — running such a
|
|
56
|
+
middleware on the sync path raises, since the async handler cannot be awaited there.
|
|
57
|
+
|
|
58
|
+
This middleware does not retry. For retries, compose with `ToolRetryMiddleware`
|
|
59
|
+
placed *inner* and configured with `on_failure="error"` so exceptions reach this
|
|
60
|
+
middleware.
|
|
61
|
+
|
|
62
|
+
This middleware only sees exceptions raised by tool *execution*. Argument-binding
|
|
63
|
+
and validation errors are handled upstream by `ToolNode` (converted to an error
|
|
64
|
+
`ToolMessage` before the tool runs), so they do not reach `on_error`.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
```python
|
|
68
|
+
from coco.agents import create_agent
|
|
69
|
+
from coco.agents.middleware import ToolErrorMiddleware
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def on_error(exc: Exception, request: ToolCallRequest) -> str | None:
|
|
73
|
+
if isinstance(exc, ValueError):
|
|
74
|
+
return f"`{request.tool_call['name']}` failed; fix the input and retry."
|
|
75
|
+
return None # propagate everything else
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
agent = create_agent(model, tools=[...], middleware=[ToolErrorMiddleware(on_error)])
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
trace_policy = TracePolicy(process_inputs=omit_payload)
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
on_error: OnError | None = None,
|
|
87
|
+
*,
|
|
88
|
+
aon_error: AOnError | None = None,
|
|
89
|
+
tools: list[BaseTool | str] | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Initialize `ToolErrorMiddleware`.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
on_error: Handler called for each exception raised by tool execution. Return
|
|
95
|
+
content (`str` or list of content blocks) to convert the exception into an
|
|
96
|
+
error `ToolMessage`. Return `None` — or simply don't return — to let the
|
|
97
|
+
exception propagate. Falling through without a return therefore re-raises,
|
|
98
|
+
so handle only the exceptions you mean to. Receives the exception and the
|
|
99
|
+
tool call request (tool name, args, call id). Used on the sync path and,
|
|
100
|
+
unless `aon_error` is given, on the async path.
|
|
101
|
+
aon_error: Optional async handler, used on the async execution path. Falls back
|
|
102
|
+
to `on_error` when not provided.
|
|
103
|
+
tools: Optional list of tools or tool names to apply handling to. If `None`,
|
|
104
|
+
applies to all tools.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If neither `on_error` nor `aon_error` is provided.
|
|
108
|
+
"""
|
|
109
|
+
super().__init__()
|
|
110
|
+
|
|
111
|
+
if on_error is None and aon_error is None:
|
|
112
|
+
msg = "ToolErrorMiddleware requires `on_error` and/or `aon_error`."
|
|
113
|
+
raise ValueError(msg)
|
|
114
|
+
|
|
115
|
+
self.on_error = on_error
|
|
116
|
+
self.aon_error = aon_error
|
|
117
|
+
|
|
118
|
+
# Extract tool names from BaseTool instances or strings
|
|
119
|
+
self._tool_filter: list[str] | None
|
|
120
|
+
if tools is not None:
|
|
121
|
+
self._tool_filter = [tool.name if not isinstance(tool, str) else tool for tool in tools]
|
|
122
|
+
else:
|
|
123
|
+
self._tool_filter = None
|
|
124
|
+
|
|
125
|
+
self.tools = [] # No additional tools registered by this middleware
|
|
126
|
+
|
|
127
|
+
def _should_handle_tool(self, tool_name: str) -> bool:
|
|
128
|
+
"""Check if error handling should apply to this tool."""
|
|
129
|
+
if self._tool_filter is None:
|
|
130
|
+
return True
|
|
131
|
+
return tool_name in self._tool_filter
|
|
132
|
+
|
|
133
|
+
def wrap_tool_call(
|
|
134
|
+
self,
|
|
135
|
+
request: ToolCallRequest,
|
|
136
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
|
|
137
|
+
) -> ToolMessage | Command[Any]:
|
|
138
|
+
"""Intercept tool execution and convert handled exceptions to error messages.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
request: Tool call request with call dict, `BaseTool`, state, and runtime.
|
|
142
|
+
handler: Callable to execute the tool.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
`ToolMessage` or `Command` (the final result).
|
|
146
|
+
"""
|
|
147
|
+
tool_name = request.tool.name if request.tool else request.tool_call["name"]
|
|
148
|
+
|
|
149
|
+
if not self._should_handle_tool(tool_name):
|
|
150
|
+
return handler(request)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
return handler(request)
|
|
154
|
+
except GraphBubbleUp:
|
|
155
|
+
# Control-flow signals (interrupts, parent commands) must propagate.
|
|
156
|
+
raise
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
if self.on_error is None:
|
|
159
|
+
# Async-only config (aon_error) cannot be awaited on the sync path.
|
|
160
|
+
msg = (
|
|
161
|
+
"ToolErrorMiddleware has no sync `on_error`; run async "
|
|
162
|
+
"(ainvoke/astream) or provide `on_error`."
|
|
163
|
+
)
|
|
164
|
+
raise RuntimeError(msg) from exc
|
|
165
|
+
content = self.on_error(exc, request)
|
|
166
|
+
if content is None:
|
|
167
|
+
raise
|
|
168
|
+
return ToolMessage(
|
|
169
|
+
content=cast("str | list[str | dict[Any, Any]]", content),
|
|
170
|
+
tool_call_id=request.tool_call["id"],
|
|
171
|
+
name=tool_name,
|
|
172
|
+
status="error",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def awrap_tool_call(
|
|
176
|
+
self,
|
|
177
|
+
request: ToolCallRequest,
|
|
178
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
|
|
179
|
+
) -> ToolMessage | Command[Any]:
|
|
180
|
+
"""Async version of `wrap_tool_call`.
|
|
181
|
+
|
|
182
|
+
Uses `aon_error` if provided, otherwise the sync `on_error`. The sync path never
|
|
183
|
+
awaits.
|
|
184
|
+
"""
|
|
185
|
+
tool_name = request.tool.name if request.tool else request.tool_call["name"]
|
|
186
|
+
|
|
187
|
+
if not self._should_handle_tool(tool_name):
|
|
188
|
+
return await handler(request)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
return await handler(request)
|
|
192
|
+
except GraphBubbleUp:
|
|
193
|
+
# Control-flow signals (interrupts, parent commands) must propagate.
|
|
194
|
+
raise
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
if self.aon_error is not None:
|
|
197
|
+
content = await self.aon_error(exc, request)
|
|
198
|
+
elif self.on_error is not None:
|
|
199
|
+
content = self.on_error(exc, request)
|
|
200
|
+
else: # pragma: no cover - __init__ guarantees at least one handler
|
|
201
|
+
raise
|
|
202
|
+
if content is None:
|
|
203
|
+
raise
|
|
204
|
+
return ToolMessage(
|
|
205
|
+
content=cast("str | list[str | dict[Any, Any]]", content),
|
|
206
|
+
tool_call_id=request.tool_call["id"],
|
|
207
|
+
name=tool_name,
|
|
208
|
+
status="error",
|
|
209
|
+
)
|