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
coco_core/tools/base.py
ADDED
|
@@ -0,0 +1,1948 @@
|
|
|
1
|
+
"""Base classes and utilities for Coco tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import typing
|
|
10
|
+
import warnings
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
13
|
+
from inspect import signature
|
|
14
|
+
from typing import (
|
|
15
|
+
TYPE_CHECKING,
|
|
16
|
+
Annotated,
|
|
17
|
+
Any,
|
|
18
|
+
Literal,
|
|
19
|
+
TypeVar,
|
|
20
|
+
cast,
|
|
21
|
+
get_args,
|
|
22
|
+
get_origin,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
import typing_extensions
|
|
26
|
+
from pydantic import (
|
|
27
|
+
BaseModel,
|
|
28
|
+
ConfigDict,
|
|
29
|
+
Field,
|
|
30
|
+
PrivateAttr,
|
|
31
|
+
PydanticDeprecationWarning,
|
|
32
|
+
SkipValidation,
|
|
33
|
+
ValidationError,
|
|
34
|
+
validate_arguments,
|
|
35
|
+
)
|
|
36
|
+
from pydantic.fields import FieldInfo
|
|
37
|
+
from pydantic.v1 import BaseModel as BaseModelV1
|
|
38
|
+
from pydantic.v1 import ValidationError as ValidationErrorV1
|
|
39
|
+
from pydantic.v1 import validate_arguments as validate_arguments_v1
|
|
40
|
+
from typing_extensions import Self, get_type_hints, override
|
|
41
|
+
|
|
42
|
+
from coco_core.callbacks import (
|
|
43
|
+
AsyncCallbackManager,
|
|
44
|
+
CallbackManager,
|
|
45
|
+
Callbacks,
|
|
46
|
+
)
|
|
47
|
+
from coco_core.messages.tool import ToolCall, ToolMessage, ToolOutputMixin
|
|
48
|
+
from coco_core.runnables import (
|
|
49
|
+
RunnableConfig,
|
|
50
|
+
RunnableSerializable,
|
|
51
|
+
ensure_config,
|
|
52
|
+
patch_config,
|
|
53
|
+
run_in_executor,
|
|
54
|
+
)
|
|
55
|
+
from coco_core.runnables.config import set_config_context
|
|
56
|
+
from coco_core.runnables.utils import coro_with_context
|
|
57
|
+
from coco_core.utils.function_calling import (
|
|
58
|
+
_parse_google_docstring,
|
|
59
|
+
_py_38_safe_origin,
|
|
60
|
+
)
|
|
61
|
+
from coco_core.utils.pydantic import (
|
|
62
|
+
TypeBaseModel,
|
|
63
|
+
_create_subset_model,
|
|
64
|
+
get_fields,
|
|
65
|
+
is_basemodel_subclass,
|
|
66
|
+
is_pydantic_v1_subclass,
|
|
67
|
+
is_pydantic_v2_subclass,
|
|
68
|
+
model_json_schema,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if TYPE_CHECKING:
|
|
72
|
+
import uuid
|
|
73
|
+
|
|
74
|
+
FILTERED_ARGS = ("run_manager", "callbacks")
|
|
75
|
+
TOOL_MESSAGE_BLOCK_TYPES = (
|
|
76
|
+
"text",
|
|
77
|
+
"image_url",
|
|
78
|
+
"image",
|
|
79
|
+
"json",
|
|
80
|
+
"search_result",
|
|
81
|
+
"custom_tool_call_output",
|
|
82
|
+
"document",
|
|
83
|
+
"file",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_logger = logging.getLogger(__name__)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SchemaAnnotationError(TypeError):
|
|
90
|
+
"""Raised when `args_schema` is missing or has an incorrect type annotation."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_annotated_type(typ: type[Any]) -> bool:
|
|
94
|
+
"""Check if a type is an `Annotated` type.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
typ: The type to check.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
`True` if the type is an `Annotated` type, `False` otherwise.
|
|
101
|
+
"""
|
|
102
|
+
return get_origin(typ) in {typing.Annotated, typing_extensions.Annotated}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_annotation_description(arg_type: type) -> str | None:
|
|
106
|
+
"""Extract description from an `Annotated` type.
|
|
107
|
+
|
|
108
|
+
Checks for string annotations and `FieldInfo` objects with descriptions.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
arg_type: The type to extract description from.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The description string if found, `None` otherwise.
|
|
115
|
+
"""
|
|
116
|
+
if _is_annotated_type(arg_type):
|
|
117
|
+
annotated_args = get_args(arg_type)
|
|
118
|
+
for annotation in annotated_args[1:]:
|
|
119
|
+
if isinstance(annotation, str):
|
|
120
|
+
return annotation
|
|
121
|
+
if isinstance(annotation, FieldInfo) and annotation.description:
|
|
122
|
+
return annotation.description
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_python_function_docstring(
|
|
127
|
+
function: Callable[..., Any],
|
|
128
|
+
annotations: dict[str, Any],
|
|
129
|
+
*,
|
|
130
|
+
error_on_invalid_docstring: bool = False,
|
|
131
|
+
) -> tuple[str, dict[str, str]]:
|
|
132
|
+
"""Parse function and argument descriptions from a docstring.
|
|
133
|
+
|
|
134
|
+
Assumes the function docstring follows Google Python style guide.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
function: The function to parse the docstring from.
|
|
138
|
+
annotations: Type annotations for the function parameters.
|
|
139
|
+
error_on_invalid_docstring: Whether to raise an error on invalid docstring.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A tuple containing the function description and argument descriptions.
|
|
143
|
+
"""
|
|
144
|
+
docstring = inspect.getdoc(function)
|
|
145
|
+
return _parse_google_docstring(
|
|
146
|
+
docstring,
|
|
147
|
+
list(annotations),
|
|
148
|
+
error_on_invalid_docstring=error_on_invalid_docstring,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _validate_docstring_args_against_annotations(
|
|
153
|
+
arg_descriptions: dict[str, str], annotations: dict[str, Any]
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Validate that docstring arguments match function annotations.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
arg_descriptions: Arguments described in the docstring.
|
|
159
|
+
annotations: Type annotations from the function signature.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If a docstring argument is not found in function signature.
|
|
163
|
+
"""
|
|
164
|
+
for docstring_arg in arg_descriptions:
|
|
165
|
+
if docstring_arg not in annotations:
|
|
166
|
+
msg = f"Arg {docstring_arg} in docstring not found in function signature."
|
|
167
|
+
raise ValueError(msg)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _infer_arg_descriptions(
|
|
171
|
+
fn: Callable[..., Any],
|
|
172
|
+
*,
|
|
173
|
+
parse_docstring: bool = False,
|
|
174
|
+
error_on_invalid_docstring: bool = False,
|
|
175
|
+
) -> tuple[str, dict[str, str]]:
|
|
176
|
+
"""Infer argument descriptions from function docstring and annotations.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
fn: The function to infer descriptions from.
|
|
180
|
+
parse_docstring: Whether to parse the docstring for descriptions.
|
|
181
|
+
error_on_invalid_docstring: Whether to raise error on invalid docstring.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A tuple containing the function description and argument descriptions.
|
|
185
|
+
"""
|
|
186
|
+
annotations = get_type_hints(fn, include_extras=True)
|
|
187
|
+
if parse_docstring:
|
|
188
|
+
description, arg_descriptions = _parse_python_function_docstring(
|
|
189
|
+
fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
description = inspect.getdoc(fn) or ""
|
|
193
|
+
arg_descriptions = {}
|
|
194
|
+
if parse_docstring:
|
|
195
|
+
_validate_docstring_args_against_annotations(arg_descriptions, annotations)
|
|
196
|
+
for arg, arg_type in annotations.items():
|
|
197
|
+
if arg in arg_descriptions:
|
|
198
|
+
continue
|
|
199
|
+
if desc := _get_annotation_description(arg_type):
|
|
200
|
+
arg_descriptions[arg] = desc
|
|
201
|
+
return description, arg_descriptions
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _is_pydantic_annotation(annotation: Any, pydantic_version: str = "v2") -> bool:
|
|
205
|
+
"""Check if a type annotation is a Pydantic model.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
annotation: The type annotation to check.
|
|
209
|
+
pydantic_version: The Pydantic version to check against (`'v1'` or `'v2'`).
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
`True` if the annotation is a Pydantic model, `False` otherwise.
|
|
213
|
+
"""
|
|
214
|
+
base_model_class = BaseModelV1 if pydantic_version == "v1" else BaseModel
|
|
215
|
+
try:
|
|
216
|
+
return issubclass(annotation, base_model_class)
|
|
217
|
+
except TypeError:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _function_annotations_are_pydantic_v1(
|
|
222
|
+
signature: inspect.Signature, func: Callable[..., Any]
|
|
223
|
+
) -> bool:
|
|
224
|
+
"""Check if all Pydantic annotations in a function are from v1.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
signature: The function signature to check.
|
|
228
|
+
func: The function being checked.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if all Pydantic annotations are from v1, `False` otherwise.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
NotImplementedError: If the function contains mixed v1 and v2 annotations.
|
|
235
|
+
"""
|
|
236
|
+
any_v1_annotations = any(
|
|
237
|
+
_is_pydantic_annotation(parameter.annotation, pydantic_version="v1")
|
|
238
|
+
for parameter in signature.parameters.values()
|
|
239
|
+
)
|
|
240
|
+
any_v2_annotations = any(
|
|
241
|
+
_is_pydantic_annotation(parameter.annotation, pydantic_version="v2")
|
|
242
|
+
for parameter in signature.parameters.values()
|
|
243
|
+
)
|
|
244
|
+
if any_v1_annotations and any_v2_annotations:
|
|
245
|
+
msg = (
|
|
246
|
+
f"Function {func} contains a mix of Pydantic v1 and v2 annotations. "
|
|
247
|
+
"Only one version of Pydantic annotations per function is supported."
|
|
248
|
+
)
|
|
249
|
+
raise NotImplementedError(msg)
|
|
250
|
+
return any_v1_annotations and not any_v2_annotations
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class _SchemaConfig:
|
|
254
|
+
"""Configuration for Pydantic models generated from function signatures."""
|
|
255
|
+
|
|
256
|
+
extra: str = "forbid"
|
|
257
|
+
"""Whether to allow extra fields in the model."""
|
|
258
|
+
|
|
259
|
+
arbitrary_types_allowed: bool = True
|
|
260
|
+
"""Whether to allow arbitrary types in the model."""
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def create_schema_from_function(
|
|
264
|
+
model_name: str,
|
|
265
|
+
func: Callable[..., Any],
|
|
266
|
+
*,
|
|
267
|
+
filter_args: Sequence[str] | None = None,
|
|
268
|
+
parse_docstring: bool = False,
|
|
269
|
+
error_on_invalid_docstring: bool = False,
|
|
270
|
+
include_injected: bool = True,
|
|
271
|
+
) -> TypeBaseModel:
|
|
272
|
+
"""Create a Pydantic schema from a function's signature.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
model_name: Name to assign to the generated Pydantic schema.
|
|
276
|
+
func: Function to generate the schema from.
|
|
277
|
+
filter_args: Optional list of arguments to exclude from the schema.
|
|
278
|
+
|
|
279
|
+
Defaults to `FILTERED_ARGS`.
|
|
280
|
+
parse_docstring: Whether to parse the function's docstring for descriptions
|
|
281
|
+
for each argument.
|
|
282
|
+
error_on_invalid_docstring: If `parse_docstring` is provided, configure
|
|
283
|
+
whether to raise `ValueError` on invalid Google Style docstrings.
|
|
284
|
+
include_injected: Whether to include injected arguments in the schema.
|
|
285
|
+
|
|
286
|
+
Defaults to `True`, since we want to include them in the schema when
|
|
287
|
+
*validating* tool inputs.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
A Pydantic model with the same arguments as the function.
|
|
291
|
+
"""
|
|
292
|
+
sig = inspect.signature(func)
|
|
293
|
+
|
|
294
|
+
if _function_annotations_are_pydantic_v1(sig, func):
|
|
295
|
+
validated = validate_arguments_v1(func, config=_SchemaConfig) # type: ignore[call-overload]
|
|
296
|
+
else:
|
|
297
|
+
# https://docs.pydantic.dev/latest/usage/validation_decorator/
|
|
298
|
+
with warnings.catch_warnings():
|
|
299
|
+
# We are using deprecated functionality here.
|
|
300
|
+
# This code should be re-written to simply construct a Pydantic model
|
|
301
|
+
# using inspect.signature and create_model.
|
|
302
|
+
warnings.simplefilter("ignore", category=PydanticDeprecationWarning)
|
|
303
|
+
validated = validate_arguments(func, config=_SchemaConfig) # type: ignore[operator]
|
|
304
|
+
|
|
305
|
+
# Let's ignore `self` and `cls` arguments for class and instance methods
|
|
306
|
+
# If qualified name has a ".", then it likely belongs in a class namespace
|
|
307
|
+
in_class = bool(func.__qualname__ and "." in func.__qualname__)
|
|
308
|
+
|
|
309
|
+
has_args = False
|
|
310
|
+
has_kwargs = False
|
|
311
|
+
|
|
312
|
+
for param in sig.parameters.values():
|
|
313
|
+
if param.kind == param.VAR_POSITIONAL:
|
|
314
|
+
has_args = True
|
|
315
|
+
elif param.kind == param.VAR_KEYWORD:
|
|
316
|
+
has_kwargs = True
|
|
317
|
+
|
|
318
|
+
inferred_model = validated.model
|
|
319
|
+
|
|
320
|
+
if filter_args:
|
|
321
|
+
# Copy to avoid mutating the caller's sequence below.
|
|
322
|
+
filter_args_ = list(filter_args)
|
|
323
|
+
else:
|
|
324
|
+
# Handle classmethods and instance methods
|
|
325
|
+
existing_params: list[str] = list(sig.parameters.keys())
|
|
326
|
+
if existing_params and existing_params[0] in {"self", "cls"} and in_class:
|
|
327
|
+
filter_args_ = [existing_params[0], *list(FILTERED_ARGS)]
|
|
328
|
+
else:
|
|
329
|
+
filter_args_ = list(FILTERED_ARGS)
|
|
330
|
+
|
|
331
|
+
# Exclude injected args from the schema regardless of whether `filter_args`
|
|
332
|
+
# was provided. Injected args (e.g. `InjectedToolArg`, `ToolRuntime`) are
|
|
333
|
+
# supplied at runtime rather than by the model, so they should not appear in
|
|
334
|
+
# the generated schema.
|
|
335
|
+
if not include_injected:
|
|
336
|
+
for existing_param in sig.parameters:
|
|
337
|
+
if (
|
|
338
|
+
_is_injected_arg_type(sig.parameters[existing_param].annotation)
|
|
339
|
+
and existing_param not in filter_args_
|
|
340
|
+
):
|
|
341
|
+
filter_args_.append(existing_param)
|
|
342
|
+
|
|
343
|
+
description, arg_descriptions = _infer_arg_descriptions(
|
|
344
|
+
func,
|
|
345
|
+
parse_docstring=parse_docstring,
|
|
346
|
+
error_on_invalid_docstring=error_on_invalid_docstring,
|
|
347
|
+
)
|
|
348
|
+
# Pydantic adds placeholder virtual fields we need to strip
|
|
349
|
+
valid_properties = []
|
|
350
|
+
for field in get_fields(inferred_model):
|
|
351
|
+
if not has_args and field == "args":
|
|
352
|
+
continue
|
|
353
|
+
if not has_kwargs and field == "kwargs":
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
if field == "v__duplicate_kwargs": # Internal pydantic field
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
if field not in filter_args_:
|
|
360
|
+
valid_properties.append(field)
|
|
361
|
+
|
|
362
|
+
return _create_subset_model(
|
|
363
|
+
model_name,
|
|
364
|
+
inferred_model,
|
|
365
|
+
list(valid_properties),
|
|
366
|
+
descriptions=arg_descriptions,
|
|
367
|
+
fn_description=description,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class ToolException(Exception): # noqa: N818
|
|
372
|
+
"""Exception thrown when a tool execution error occurs.
|
|
373
|
+
|
|
374
|
+
This exception allows tools to signal errors without stopping the agent.
|
|
375
|
+
|
|
376
|
+
The error is handled according to the tool's `handle_tool_error` setting, and the
|
|
377
|
+
result is returned as an observation to the agent.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
ArgsSchema = TypeBaseModel | dict[str, Any]
|
|
382
|
+
MessageContentBlock = str | dict[str, Any]
|
|
383
|
+
"""A single message content block: plain text or a structured block.
|
|
384
|
+
|
|
385
|
+
A dict block is only considered valid at runtime when its `type` key is one of
|
|
386
|
+
`TOOL_MESSAGE_BLOCK_TYPES` (see `_is_message_content_block`); the static type
|
|
387
|
+
intentionally stays broad because block payloads vary by provider format.
|
|
388
|
+
"""
|
|
389
|
+
ToolExceptionHandlerOutput = str | Sequence[MessageContentBlock]
|
|
390
|
+
"""Content returned by a `handle_tool_error` callable.
|
|
391
|
+
|
|
392
|
+
Error handlers may return plain text or a sequence of structured message
|
|
393
|
+
content blocks. When the original tool call includes a `tool_call_id`, this
|
|
394
|
+
content is normalized to the content of a `ToolMessage` with `status="error"`.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
_EMPTY_SET: frozenset[str] = frozenset()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
_TOOL_CALL_SCHEMA_FIELDS = frozenset({"name", "description", "args_schema"})
|
|
401
|
+
"""Fields the memoized `tool_call_schema` is built from; reassignment clears it."""
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _patch_json_schema_cache(model_cls: type) -> None:
|
|
405
|
+
"""Patch `model_json_schema` (or `schema` for pydantic v1) to cache.
|
|
406
|
+
|
|
407
|
+
Pydantic regenerates the full JSON-schema dict on every
|
|
408
|
+
`model_json_schema()` call — there is no per-class cache. When the
|
|
409
|
+
model class is stable (memoized on a `BaseTool` instance), this patch
|
|
410
|
+
caches the dict on the class so repeated calls return instantly.
|
|
411
|
+
|
|
412
|
+
Only calls with all-default arguments are cached; any explicit arguments
|
|
413
|
+
bypass the cache and delegate to the original method.
|
|
414
|
+
"""
|
|
415
|
+
method_name = (
|
|
416
|
+
"model_json_schema" if hasattr(model_cls, "model_json_schema") else "schema"
|
|
417
|
+
)
|
|
418
|
+
orig = getattr(model_cls, method_name)
|
|
419
|
+
|
|
420
|
+
def _cached_json_schema(cls: type, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
421
|
+
if not args and not kwargs:
|
|
422
|
+
cached = cls.__dict__.get("_json_schema_cache")
|
|
423
|
+
if cached is not None:
|
|
424
|
+
return cast("dict[str, Any]", cached)
|
|
425
|
+
result = orig(*args, **kwargs)
|
|
426
|
+
if not args and not kwargs:
|
|
427
|
+
cls._json_schema_cache = result # type: ignore[attr-defined]
|
|
428
|
+
return cast("dict[str, Any]", result)
|
|
429
|
+
|
|
430
|
+
setattr(model_cls, method_name, classmethod(_cached_json_schema))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class BaseTool(RunnableSerializable[str | dict[str, Any] | ToolCall, Any]):
|
|
434
|
+
"""Base class for all Coco tools.
|
|
435
|
+
|
|
436
|
+
This abstract class defines the interface that all Coco tools must implement.
|
|
437
|
+
|
|
438
|
+
Tools are components that can be called by agents to perform specific actions.
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
442
|
+
"""Validate the tool class definition during subclass creation.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
**kwargs: Additional keyword arguments passed to the parent class.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
SchemaAnnotationError: If `args_schema` has incorrect type annotation.
|
|
449
|
+
"""
|
|
450
|
+
super().__init_subclass__(**kwargs)
|
|
451
|
+
|
|
452
|
+
args_schema_type = cls.__annotations__.get("args_schema", None)
|
|
453
|
+
|
|
454
|
+
if args_schema_type is not None and args_schema_type == BaseModel:
|
|
455
|
+
# Throw errors for common mis-annotations.
|
|
456
|
+
# TODO: Use get_args / get_origin and fully
|
|
457
|
+
# specify valid annotations.
|
|
458
|
+
typehint_mandate = """
|
|
459
|
+
class ChildTool(BaseTool):
|
|
460
|
+
...
|
|
461
|
+
args_schema: Type[BaseModel] = SchemaClass
|
|
462
|
+
..."""
|
|
463
|
+
name = cls.__name__
|
|
464
|
+
msg = (
|
|
465
|
+
f"Tool definition for {name} must include valid type annotations"
|
|
466
|
+
f" for argument 'args_schema' to behave as expected.\n"
|
|
467
|
+
f"Expected annotation of 'Type[BaseModel]'"
|
|
468
|
+
f" but got '{args_schema_type}'.\n"
|
|
469
|
+
f"Expected class looks like:\n"
|
|
470
|
+
f"{typehint_mandate}"
|
|
471
|
+
)
|
|
472
|
+
raise SchemaAnnotationError(msg)
|
|
473
|
+
|
|
474
|
+
name: str
|
|
475
|
+
"""The unique name of the tool that clearly communicates its purpose."""
|
|
476
|
+
|
|
477
|
+
description: str
|
|
478
|
+
"""Used to tell the model how/when/why to use the tool.
|
|
479
|
+
|
|
480
|
+
You can provide few-shot examples as a part of the description.
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
args_schema: Annotated[ArgsSchema | None, SkipValidation()] = Field(
|
|
484
|
+
default=None, description="The tool schema."
|
|
485
|
+
)
|
|
486
|
+
"""Pydantic model class to validate and parse the tool's input arguments.
|
|
487
|
+
|
|
488
|
+
Args schema should be either:
|
|
489
|
+
|
|
490
|
+
- A subclass of `pydantic.BaseModel`.
|
|
491
|
+
- A subclass of `pydantic.v1.BaseModel` if accessing v1 namespace in pydantic 2
|
|
492
|
+
- A JSON schema dict
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
return_direct: bool = False
|
|
496
|
+
"""Whether to return the tool's output directly.
|
|
497
|
+
|
|
498
|
+
Setting this to `True` means that after the tool is called, the `AgentExecutor` will
|
|
499
|
+
stop looping.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
verbose: bool = False
|
|
503
|
+
"""Whether to log the tool's progress."""
|
|
504
|
+
|
|
505
|
+
callbacks: Callbacks = Field(default=None, exclude=True)
|
|
506
|
+
"""Callbacks to be called during tool execution."""
|
|
507
|
+
|
|
508
|
+
tags: list[str] | None = None
|
|
509
|
+
"""Optional list of tags associated with the tool.
|
|
510
|
+
|
|
511
|
+
These tags will be associated with each call to this tool,
|
|
512
|
+
and passed as arguments to the handlers defined in `callbacks`.
|
|
513
|
+
|
|
514
|
+
You can use these to, e.g., identify a specific instance of a tool with its use
|
|
515
|
+
case.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
metadata: dict[str, Any] | None = None
|
|
519
|
+
"""Optional metadata associated with the tool.
|
|
520
|
+
|
|
521
|
+
This metadata will be associated with each call to this tool,
|
|
522
|
+
and passed as arguments to the handlers defined in `callbacks`.
|
|
523
|
+
|
|
524
|
+
You can use these to, e.g., identify a specific instance of a tool with its usecase.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
handle_tool_error: (
|
|
528
|
+
bool | str | Callable[[ToolException], ToolExceptionHandlerOutput] | None
|
|
529
|
+
) = False
|
|
530
|
+
"""Handle `ToolException` raised by tool execution.
|
|
531
|
+
|
|
532
|
+
If `False`, the exception is re-raised. If `True`, the exception message is
|
|
533
|
+
returned as tool output. If a string is passed, that string is returned
|
|
534
|
+
as tool output. If a callable is passed, it receives the exception and
|
|
535
|
+
its return value is used as the tool output.
|
|
536
|
+
|
|
537
|
+
Callable handlers may return either a string or a list of message
|
|
538
|
+
content blocks. If the tool was invoked with a `tool_call_id`, the handled
|
|
539
|
+
content is wrapped in a `ToolMessage` with `status="error"`.
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
handle_validation_error: (
|
|
543
|
+
bool | str | Callable[[ValidationError | ValidationErrorV1], str] | None
|
|
544
|
+
) = False
|
|
545
|
+
"""Handle the content of the `ValidationError` thrown."""
|
|
546
|
+
|
|
547
|
+
response_format: Literal["content", "content_and_artifact"] = "content"
|
|
548
|
+
"""The tool response format.
|
|
549
|
+
|
|
550
|
+
If `'content'` then the output of the tool is interpreted as the contents of a
|
|
551
|
+
`ToolMessage`. If `'content_and_artifact'` then the output is expected to be a
|
|
552
|
+
two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`.
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
extras: dict[str, Any] | None = None
|
|
556
|
+
"""Optional provider-specific extra fields for the tool.
|
|
557
|
+
|
|
558
|
+
This is used to pass provider-specific configuration that doesn't fit into
|
|
559
|
+
standard tool fields.
|
|
560
|
+
|
|
561
|
+
Example:
|
|
562
|
+
Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching),
|
|
563
|
+
[`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search),
|
|
564
|
+
or `input_examples`.
|
|
565
|
+
|
|
566
|
+
```python
|
|
567
|
+
@tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}})
|
|
568
|
+
def my_tool(x: str) -> str:
|
|
569
|
+
return x
|
|
570
|
+
```
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
574
|
+
"""Initialize the tool.
|
|
575
|
+
|
|
576
|
+
Raises:
|
|
577
|
+
TypeError: If `args_schema` is not a subclass of pydantic `BaseModel` or
|
|
578
|
+
`dict`.
|
|
579
|
+
"""
|
|
580
|
+
if (
|
|
581
|
+
"args_schema" in kwargs
|
|
582
|
+
and kwargs["args_schema"] is not None
|
|
583
|
+
and not is_basemodel_subclass(kwargs["args_schema"])
|
|
584
|
+
and not isinstance(kwargs["args_schema"], dict)
|
|
585
|
+
):
|
|
586
|
+
msg = (
|
|
587
|
+
"args_schema must be a subclass of pydantic BaseModel or "
|
|
588
|
+
f"a JSON schema dict. Got: {kwargs['args_schema']}."
|
|
589
|
+
)
|
|
590
|
+
raise TypeError(msg)
|
|
591
|
+
super().__init__(**kwargs)
|
|
592
|
+
|
|
593
|
+
model_config = ConfigDict(
|
|
594
|
+
arbitrary_types_allowed=True,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def is_single_input(self) -> bool:
|
|
599
|
+
"""Check if the tool accepts only a single input argument.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
`True` if the tool has only one input argument, `False` otherwise.
|
|
603
|
+
"""
|
|
604
|
+
keys = {k for k in self.args if k != "kwargs"}
|
|
605
|
+
return len(keys) == 1
|
|
606
|
+
|
|
607
|
+
@property
|
|
608
|
+
def args(self) -> dict[str, Any]:
|
|
609
|
+
"""Get the tool's input arguments schema.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
`dict` containing the tool's argument properties.
|
|
613
|
+
"""
|
|
614
|
+
if isinstance(self.args_schema, dict):
|
|
615
|
+
json_schema = self.args_schema
|
|
616
|
+
else:
|
|
617
|
+
input_schema = self.tool_call_schema
|
|
618
|
+
if isinstance(input_schema, dict):
|
|
619
|
+
json_schema = input_schema
|
|
620
|
+
else:
|
|
621
|
+
json_schema = model_json_schema(input_schema)
|
|
622
|
+
return cast("dict[str, Any]", json_schema["properties"])
|
|
623
|
+
|
|
624
|
+
_tool_call_schema_memo: ArgsSchema | None = PrivateAttr(default=None)
|
|
625
|
+
"""Memoized `tool_call_schema` result.
|
|
626
|
+
|
|
627
|
+
Building the subset model is expensive, and pydantic does not cache
|
|
628
|
+
`model_json_schema()` per class, so agent loops would otherwise pay full
|
|
629
|
+
schema generation for every tool on every model call. The subset model
|
|
630
|
+
class is memoized here and its `model_json_schema`/`schema` method is
|
|
631
|
+
patched to cache the generated dict, so both costs are paid only once per
|
|
632
|
+
tool instance.
|
|
633
|
+
Cleared whenever `name`, `description`, or `args_schema` is reassigned (see
|
|
634
|
+
`__setattr__` and `model_copy`).
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
@override
|
|
638
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
639
|
+
"""Clear the tool-call schema memo when an input to it is reassigned."""
|
|
640
|
+
super().__setattr__(name, value)
|
|
641
|
+
if name in _TOOL_CALL_SCHEMA_FIELDS and self.__pydantic_private__ is not None:
|
|
642
|
+
self._tool_call_schema_memo = None
|
|
643
|
+
|
|
644
|
+
@override
|
|
645
|
+
def model_copy(
|
|
646
|
+
self, *, update: Mapping[str, Any] | None = None, deep: bool = False
|
|
647
|
+
) -> Self:
|
|
648
|
+
"""Copy the tool, clearing the schema memo if `update` affects it.
|
|
649
|
+
|
|
650
|
+
`model_copy` writes `update` directly to the copy's `__dict__` without
|
|
651
|
+
going through `__setattr__`, and private attributes (including the
|
|
652
|
+
memo) carry over to the copy, so the memo is cleared here when the
|
|
653
|
+
update touches one of the fields the schema is built from.
|
|
654
|
+
"""
|
|
655
|
+
copied = super().model_copy(update=update, deep=deep)
|
|
656
|
+
if update and not _TOOL_CALL_SCHEMA_FIELDS.isdisjoint(update):
|
|
657
|
+
copied._tool_call_schema_memo = None # noqa: SLF001
|
|
658
|
+
return copied
|
|
659
|
+
|
|
660
|
+
def __getstate__(self) -> dict[Any, Any]:
|
|
661
|
+
"""Drop the tool-call schema memo when pickling.
|
|
662
|
+
|
|
663
|
+
The memoized subset model is a dynamically created class that cannot be
|
|
664
|
+
pickled by reference; it is rebuilt lazily on next access.
|
|
665
|
+
"""
|
|
666
|
+
state = super().__getstate__()
|
|
667
|
+
private = state.get("__pydantic_private__")
|
|
668
|
+
if private and private.get("_tool_call_schema_memo") is not None:
|
|
669
|
+
state = dict(state)
|
|
670
|
+
state["__pydantic_private__"] = {
|
|
671
|
+
**private,
|
|
672
|
+
"_tool_call_schema_memo": None,
|
|
673
|
+
}
|
|
674
|
+
return state
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def tool_call_schema(self) -> ArgsSchema:
|
|
678
|
+
"""Get the schema for tool calls, excluding injected arguments.
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
The schema that should be used for tool calls from language models.
|
|
682
|
+
|
|
683
|
+
The returned model class is memoized per tool instance (invalidated
|
|
684
|
+
when `name`, `description`, or `args_schema` is reassigned) so
|
|
685
|
+
repeated access does not regenerate the class. The class's
|
|
686
|
+
`model_json_schema` method is also patched to cache the generated
|
|
687
|
+
schema dict, since pydantic does not cache it per class.
|
|
688
|
+
"""
|
|
689
|
+
if isinstance(self.args_schema, dict):
|
|
690
|
+
if self.description:
|
|
691
|
+
return {
|
|
692
|
+
**self.args_schema,
|
|
693
|
+
"description": self.description,
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return self.args_schema
|
|
697
|
+
|
|
698
|
+
if (memo := self._tool_call_schema_memo) is not None:
|
|
699
|
+
return memo
|
|
700
|
+
|
|
701
|
+
full_schema = self.get_input_schema()
|
|
702
|
+
fields = []
|
|
703
|
+
|
|
704
|
+
# Accommodates a condition where forward references were not resolved
|
|
705
|
+
# during model construction. At introspection time, we fail fast if
|
|
706
|
+
# the model schema is not complete so the underlying serialized schema
|
|
707
|
+
# doesn't narrow the propreties in the tool json schema to an empty dict
|
|
708
|
+
if (
|
|
709
|
+
is_pydantic_v2_subclass(full_schema)
|
|
710
|
+
and not full_schema.__pydantic_complete__
|
|
711
|
+
):
|
|
712
|
+
full_schema.model_rebuild()
|
|
713
|
+
|
|
714
|
+
for name, type_ in get_all_basemodel_annotations(full_schema).items():
|
|
715
|
+
if not _is_injected_arg_type(type_):
|
|
716
|
+
fields.append(name)
|
|
717
|
+
subset_model = _create_subset_model(
|
|
718
|
+
self.name, full_schema, fields, fn_description=self.description
|
|
719
|
+
)
|
|
720
|
+
_patch_json_schema_cache(subset_model)
|
|
721
|
+
self._tool_call_schema_memo = subset_model
|
|
722
|
+
return subset_model
|
|
723
|
+
|
|
724
|
+
@functools.cached_property
|
|
725
|
+
def _injected_args_keys(self) -> frozenset[str]:
|
|
726
|
+
# Inspect the tool's `_run` (falling back to `_arun` for async-only
|
|
727
|
+
# subclasses) for directly injected args like `ToolRuntime` or args
|
|
728
|
+
# annotated with `InjectedToolArg`. These are supplied at call time
|
|
729
|
+
# rather than by the model, so they must be excluded from the schema and
|
|
730
|
+
# re-injected during execution. `StructuredTool` overrides this to
|
|
731
|
+
# inspect its wrapped `func`/`coroutine` instead.
|
|
732
|
+
for method in (self._run, self._arun):
|
|
733
|
+
keys = _get_injected_args_keys_from_signature(method)
|
|
734
|
+
if keys:
|
|
735
|
+
return keys
|
|
736
|
+
return _EMPTY_SET
|
|
737
|
+
|
|
738
|
+
# --- Runnable ---
|
|
739
|
+
|
|
740
|
+
@override
|
|
741
|
+
def get_input_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
|
|
742
|
+
"""The tool's input schema.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
config: The configuration for the tool.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
The input schema for the tool.
|
|
749
|
+
"""
|
|
750
|
+
if self.args_schema is not None:
|
|
751
|
+
if isinstance(self.args_schema, dict):
|
|
752
|
+
return super().get_input_schema(config)
|
|
753
|
+
return self.args_schema
|
|
754
|
+
return create_schema_from_function(self.name, self._run)
|
|
755
|
+
|
|
756
|
+
@override
|
|
757
|
+
def invoke(
|
|
758
|
+
self,
|
|
759
|
+
input: str | dict[str, Any] | ToolCall,
|
|
760
|
+
config: RunnableConfig | None = None,
|
|
761
|
+
**kwargs: Any,
|
|
762
|
+
) -> Any:
|
|
763
|
+
tool_input, kwargs = _prep_run_args(input, config, **kwargs)
|
|
764
|
+
return self.run(tool_input, **kwargs)
|
|
765
|
+
|
|
766
|
+
@override
|
|
767
|
+
async def ainvoke(
|
|
768
|
+
self,
|
|
769
|
+
input: str | dict[str, Any] | ToolCall,
|
|
770
|
+
config: RunnableConfig | None = None,
|
|
771
|
+
**kwargs: Any,
|
|
772
|
+
) -> Any:
|
|
773
|
+
tool_input, kwargs = _prep_run_args(input, config, **kwargs)
|
|
774
|
+
return await self.arun(tool_input, **kwargs)
|
|
775
|
+
|
|
776
|
+
# --- Tool ---
|
|
777
|
+
|
|
778
|
+
def _parse_input(
|
|
779
|
+
self, tool_input: str | dict[str, Any], tool_call_id: str | None
|
|
780
|
+
) -> str | dict[str, Any]:
|
|
781
|
+
"""Parse and validate tool input using the args schema.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
tool_input: The raw input to the tool.
|
|
785
|
+
tool_call_id: The ID of the tool call, if available.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
The parsed and validated input.
|
|
789
|
+
|
|
790
|
+
Raises:
|
|
791
|
+
ValueError: If `string` input is provided with JSON schema `args_schema`.
|
|
792
|
+
ValueError: If `InjectedToolCallId` is required but `tool_call_id` is not
|
|
793
|
+
provided.
|
|
794
|
+
TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.
|
|
795
|
+
"""
|
|
796
|
+
input_args = self.args_schema
|
|
797
|
+
|
|
798
|
+
if isinstance(tool_input, str):
|
|
799
|
+
if input_args is not None:
|
|
800
|
+
if isinstance(input_args, dict):
|
|
801
|
+
msg = (
|
|
802
|
+
"String tool inputs are not allowed when "
|
|
803
|
+
"using tools with JSON schema args_schema."
|
|
804
|
+
)
|
|
805
|
+
raise ValueError(msg)
|
|
806
|
+
key_ = next(iter(get_fields(input_args).keys()))
|
|
807
|
+
if issubclass(input_args, BaseModel):
|
|
808
|
+
input_args.model_validate({key_: tool_input})
|
|
809
|
+
elif issubclass(input_args, BaseModelV1):
|
|
810
|
+
input_args.parse_obj({key_: tool_input})
|
|
811
|
+
else:
|
|
812
|
+
msg = f"args_schema must be a Pydantic BaseModel, got {input_args}" # type: ignore[unreachable]
|
|
813
|
+
raise TypeError(msg)
|
|
814
|
+
return tool_input
|
|
815
|
+
|
|
816
|
+
if input_args is not None:
|
|
817
|
+
if isinstance(input_args, dict):
|
|
818
|
+
return tool_input
|
|
819
|
+
result: BaseModel | BaseModelV1
|
|
820
|
+
if issubclass(input_args, BaseModel):
|
|
821
|
+
# Check args_schema for InjectedToolCallId
|
|
822
|
+
for k, v in get_all_basemodel_annotations(input_args).items():
|
|
823
|
+
if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
|
|
824
|
+
if tool_call_id is None:
|
|
825
|
+
msg = (
|
|
826
|
+
"When tool includes an InjectedToolCallId "
|
|
827
|
+
"argument, tool must always be invoked with a full "
|
|
828
|
+
"model ToolCall of the form: {'args': {...}, "
|
|
829
|
+
"'name': '...', 'type': 'tool_call', "
|
|
830
|
+
"'tool_call_id': '...'}"
|
|
831
|
+
)
|
|
832
|
+
raise ValueError(msg)
|
|
833
|
+
tool_input[k] = tool_call_id
|
|
834
|
+
result_v2 = input_args.model_validate(tool_input)
|
|
835
|
+
result_dict = result_v2.model_dump()
|
|
836
|
+
provided_fields = result_v2.model_fields_set
|
|
837
|
+
result = result_v2
|
|
838
|
+
elif issubclass(input_args, BaseModelV1):
|
|
839
|
+
# Check args_schema for InjectedToolCallId
|
|
840
|
+
for k, v in get_all_basemodel_annotations(input_args).items():
|
|
841
|
+
if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
|
|
842
|
+
if tool_call_id is None:
|
|
843
|
+
msg = (
|
|
844
|
+
"When tool includes an InjectedToolCallId "
|
|
845
|
+
"argument, tool must always be invoked with a full "
|
|
846
|
+
"model ToolCall of the form: {'args': {...}, "
|
|
847
|
+
"'name': '...', 'type': 'tool_call', "
|
|
848
|
+
"'tool_call_id': '...'}"
|
|
849
|
+
)
|
|
850
|
+
raise ValueError(msg)
|
|
851
|
+
tool_input[k] = tool_call_id
|
|
852
|
+
result_v1 = input_args.parse_obj(tool_input)
|
|
853
|
+
result_dict = result_v1.dict()
|
|
854
|
+
provided_fields = result_v1.__fields_set__
|
|
855
|
+
result = result_v1
|
|
856
|
+
else:
|
|
857
|
+
msg = ( # type: ignore[unreachable]
|
|
858
|
+
f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
|
|
859
|
+
)
|
|
860
|
+
raise NotImplementedError(msg)
|
|
861
|
+
|
|
862
|
+
# Include fields from tool_input, fields provided through Pydantic aliases,
|
|
863
|
+
# plus fields with explicit defaults. This applies Pydantic defaults (like
|
|
864
|
+
# Field(default=1)) while excluding synthetic "args"/"kwargs" fields that
|
|
865
|
+
# Pydantic creates for *args/**kwargs.
|
|
866
|
+
field_info = get_fields(input_args)
|
|
867
|
+
validated_input = {}
|
|
868
|
+
for k in result_dict:
|
|
869
|
+
if k in tool_input:
|
|
870
|
+
# Field was provided in input - include it (validated)
|
|
871
|
+
validated_input[k] = getattr(result, k)
|
|
872
|
+
elif k in provided_fields:
|
|
873
|
+
# Field was provided through a Pydantic alias - include it.
|
|
874
|
+
validated_input[k] = getattr(result, k)
|
|
875
|
+
elif k in field_info and k not in {"args", "kwargs"}:
|
|
876
|
+
# Check if field has an explicit default defined in the schema.
|
|
877
|
+
# Exclude "args"/"kwargs" as these are synthetic fields for variadic
|
|
878
|
+
# parameters that should not be passed as keyword arguments.
|
|
879
|
+
fi = field_info[k]
|
|
880
|
+
# Pydantic v2 uses is_required() method, v1 uses required attribute
|
|
881
|
+
has_default = (
|
|
882
|
+
not fi.is_required()
|
|
883
|
+
if hasattr(fi, "is_required")
|
|
884
|
+
else not getattr(fi, "required", True)
|
|
885
|
+
)
|
|
886
|
+
if has_default:
|
|
887
|
+
validated_input[k] = getattr(result, k)
|
|
888
|
+
|
|
889
|
+
for k in self._injected_args_keys:
|
|
890
|
+
if k in tool_input:
|
|
891
|
+
validated_input[k] = tool_input[k]
|
|
892
|
+
elif k == "tool_call_id":
|
|
893
|
+
if tool_call_id is None:
|
|
894
|
+
msg = (
|
|
895
|
+
"When tool includes an InjectedToolCallId "
|
|
896
|
+
"argument, tool must always be invoked with a full "
|
|
897
|
+
"model ToolCall of the form: {'args': {...}, "
|
|
898
|
+
"'name': '...', 'type': 'tool_call', "
|
|
899
|
+
"'tool_call_id': '...'}"
|
|
900
|
+
)
|
|
901
|
+
raise ValueError(msg)
|
|
902
|
+
validated_input[k] = tool_call_id
|
|
903
|
+
|
|
904
|
+
return validated_input
|
|
905
|
+
|
|
906
|
+
return tool_input
|
|
907
|
+
|
|
908
|
+
@abstractmethod
|
|
909
|
+
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
|
910
|
+
"""Use the tool.
|
|
911
|
+
|
|
912
|
+
Add `run_manager: CallbackManagerForToolRun | None = None` to child
|
|
913
|
+
implementations to enable tracing.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
The result of the tool execution.
|
|
917
|
+
"""
|
|
918
|
+
|
|
919
|
+
async def _arun(self, *args: Any, **kwargs: Any) -> Any:
|
|
920
|
+
"""Use the tool asynchronously.
|
|
921
|
+
|
|
922
|
+
Add `run_manager: AsyncCallbackManagerForToolRun | None = None` to child
|
|
923
|
+
implementations to enable tracing.
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
The result of the tool execution.
|
|
927
|
+
"""
|
|
928
|
+
if kwargs.get("run_manager") and signature(self._run).parameters.get(
|
|
929
|
+
"run_manager"
|
|
930
|
+
):
|
|
931
|
+
kwargs["run_manager"] = kwargs["run_manager"].get_sync()
|
|
932
|
+
return await run_in_executor(None, self._run, *args, **kwargs)
|
|
933
|
+
|
|
934
|
+
def _filter_injected_args(self, tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
935
|
+
"""Filter out injected tool arguments from the input dictionary.
|
|
936
|
+
|
|
937
|
+
Injected arguments are those annotated with `InjectedToolArg` or its
|
|
938
|
+
subclasses, or arguments in `FILTERED_ARGS` like `run_manager` and callbacks.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
tool_input: The tool input dictionary to filter.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
A filtered dictionary with injected arguments removed.
|
|
945
|
+
"""
|
|
946
|
+
# Start with filtered args from the constant
|
|
947
|
+
filtered_keys = set[str](FILTERED_ARGS)
|
|
948
|
+
|
|
949
|
+
# Add injected args from function signature (e.g., ToolRuntime parameters)
|
|
950
|
+
filtered_keys.update(self._injected_args_keys)
|
|
951
|
+
|
|
952
|
+
# If we have an args_schema, use it to identify injected args
|
|
953
|
+
# Skip if args_schema is a dict (JSON Schema) as it's not a Pydantic model
|
|
954
|
+
if self.args_schema is not None and not isinstance(self.args_schema, dict):
|
|
955
|
+
try:
|
|
956
|
+
annotations = get_all_basemodel_annotations(self.args_schema)
|
|
957
|
+
for field_name, field_type in annotations.items():
|
|
958
|
+
if _is_injected_arg_type(field_type):
|
|
959
|
+
filtered_keys.add(field_name)
|
|
960
|
+
except Exception:
|
|
961
|
+
# If we can't get annotations, just use FILTERED_ARGS
|
|
962
|
+
_logger.debug(
|
|
963
|
+
"Failed to get args_schema annotations for filtering.",
|
|
964
|
+
exc_info=True,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Filter out the injected keys from tool_input
|
|
968
|
+
return {k: v for k, v in tool_input.items() if k not in filtered_keys}
|
|
969
|
+
|
|
970
|
+
def _to_args_and_kwargs(
|
|
971
|
+
self, tool_input: str | dict[str, Any], tool_call_id: str | None
|
|
972
|
+
) -> tuple[tuple[str, ...], dict[str, Any]]:
|
|
973
|
+
"""Convert tool input to positional and keyword arguments.
|
|
974
|
+
|
|
975
|
+
Args:
|
|
976
|
+
tool_input: The input to the tool.
|
|
977
|
+
tool_call_id: The ID of the tool call, if available.
|
|
978
|
+
|
|
979
|
+
Returns:
|
|
980
|
+
A tuple of `(positional_args, keyword_args)` for the tool.
|
|
981
|
+
|
|
982
|
+
Raises:
|
|
983
|
+
TypeError: If the tool input type is invalid.
|
|
984
|
+
"""
|
|
985
|
+
if (
|
|
986
|
+
self.args_schema is not None
|
|
987
|
+
and isinstance(self.args_schema, type)
|
|
988
|
+
and is_basemodel_subclass(self.args_schema)
|
|
989
|
+
and not get_fields(self.args_schema)
|
|
990
|
+
):
|
|
991
|
+
# StructuredTool with no args
|
|
992
|
+
return (), {}
|
|
993
|
+
tool_input = self._parse_input(tool_input, tool_call_id)
|
|
994
|
+
# For backwards compatibility, if run_input is a string,
|
|
995
|
+
# pass as a positional argument.
|
|
996
|
+
if isinstance(tool_input, str):
|
|
997
|
+
return (tool_input,), {}
|
|
998
|
+
if isinstance(tool_input, dict):
|
|
999
|
+
# Make a shallow copy of the input to allow downstream code
|
|
1000
|
+
# to modify the root level of the input without affecting the
|
|
1001
|
+
# original input.
|
|
1002
|
+
# This is used by the tool to inject run time information like
|
|
1003
|
+
# the callback manager.
|
|
1004
|
+
return (), tool_input.copy()
|
|
1005
|
+
# This code path is not expected to be reachable.
|
|
1006
|
+
msg = f"Invalid tool input type: {type(tool_input)}" # type: ignore[unreachable]
|
|
1007
|
+
raise TypeError(msg)
|
|
1008
|
+
|
|
1009
|
+
def run(
|
|
1010
|
+
self,
|
|
1011
|
+
tool_input: str | dict[str, Any],
|
|
1012
|
+
verbose: bool | None = None, # noqa: FBT001
|
|
1013
|
+
start_color: str | None = "green",
|
|
1014
|
+
color: str | None = "green",
|
|
1015
|
+
callbacks: Callbacks = None,
|
|
1016
|
+
*,
|
|
1017
|
+
tags: list[str] | None = None,
|
|
1018
|
+
metadata: dict[str, Any] | None = None,
|
|
1019
|
+
run_name: str | None = None,
|
|
1020
|
+
run_id: uuid.UUID | None = None,
|
|
1021
|
+
config: RunnableConfig | None = None,
|
|
1022
|
+
tool_call_id: str | None = None,
|
|
1023
|
+
**kwargs: Any,
|
|
1024
|
+
) -> Any:
|
|
1025
|
+
"""Run the tool.
|
|
1026
|
+
|
|
1027
|
+
Args:
|
|
1028
|
+
tool_input: The input to the tool.
|
|
1029
|
+
verbose: Whether to log the tool's progress.
|
|
1030
|
+
start_color: The color to use when starting the tool.
|
|
1031
|
+
color: The color to use when ending the tool.
|
|
1032
|
+
callbacks: Callbacks to be called during tool execution.
|
|
1033
|
+
tags: Optional list of tags associated with the tool.
|
|
1034
|
+
metadata: Optional metadata associated with the tool.
|
|
1035
|
+
run_name: The name of the run.
|
|
1036
|
+
run_id: The id of the run.
|
|
1037
|
+
config: The configuration for the tool.
|
|
1038
|
+
tool_call_id: The id of the tool call.
|
|
1039
|
+
**kwargs: Keyword arguments to be passed to tool callbacks (event handler)
|
|
1040
|
+
|
|
1041
|
+
Returns:
|
|
1042
|
+
The output of the tool.
|
|
1043
|
+
|
|
1044
|
+
Raises:
|
|
1045
|
+
ToolException: If an error occurs during tool execution.
|
|
1046
|
+
"""
|
|
1047
|
+
callback_manager = CallbackManager.configure(
|
|
1048
|
+
callbacks,
|
|
1049
|
+
self.callbacks,
|
|
1050
|
+
self.verbose or bool(verbose),
|
|
1051
|
+
tags,
|
|
1052
|
+
self.tags,
|
|
1053
|
+
metadata,
|
|
1054
|
+
self.metadata,
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
# Filter out injected arguments from callback inputs
|
|
1058
|
+
filtered_tool_input = (
|
|
1059
|
+
self._filter_injected_args(tool_input)
|
|
1060
|
+
if isinstance(tool_input, dict)
|
|
1061
|
+
else None
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
# Use filtered inputs for the input_str parameter as well
|
|
1065
|
+
tool_input_str = (
|
|
1066
|
+
tool_input
|
|
1067
|
+
if isinstance(tool_input, str)
|
|
1068
|
+
else str(
|
|
1069
|
+
filtered_tool_input if filtered_tool_input is not None else tool_input
|
|
1070
|
+
)
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
run_manager = callback_manager.on_tool_start(
|
|
1074
|
+
{"name": self.name, "description": self.description},
|
|
1075
|
+
tool_input_str,
|
|
1076
|
+
color=start_color,
|
|
1077
|
+
name=run_name,
|
|
1078
|
+
run_id=run_id,
|
|
1079
|
+
inputs=filtered_tool_input,
|
|
1080
|
+
tool_call_id=tool_call_id,
|
|
1081
|
+
**kwargs,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
content = None
|
|
1085
|
+
artifact = None
|
|
1086
|
+
status = "success"
|
|
1087
|
+
error_to_raise: Exception | KeyboardInterrupt | None = None
|
|
1088
|
+
try:
|
|
1089
|
+
child_config = patch_config(config, callbacks=run_manager.get_child())
|
|
1090
|
+
with set_config_context(child_config) as context:
|
|
1091
|
+
tool_args, tool_kwargs = self._to_args_and_kwargs(
|
|
1092
|
+
tool_input, tool_call_id
|
|
1093
|
+
)
|
|
1094
|
+
if signature(self._run).parameters.get("run_manager"):
|
|
1095
|
+
tool_kwargs |= {"run_manager": run_manager}
|
|
1096
|
+
if config_param := _get_runnable_config_param(self._run):
|
|
1097
|
+
tool_kwargs |= {config_param: config}
|
|
1098
|
+
response = context.run(self._run, *tool_args, **tool_kwargs)
|
|
1099
|
+
if self.response_format == "content_and_artifact":
|
|
1100
|
+
msg = (
|
|
1101
|
+
"Since response_format='content_and_artifact' "
|
|
1102
|
+
"a two-tuple of the message content and raw tool output is "
|
|
1103
|
+
f"expected. Instead, generated response is of type: "
|
|
1104
|
+
f"{type(response)}."
|
|
1105
|
+
)
|
|
1106
|
+
if not isinstance(response, tuple):
|
|
1107
|
+
error_to_raise = ValueError(msg)
|
|
1108
|
+
else:
|
|
1109
|
+
try:
|
|
1110
|
+
content, artifact = response
|
|
1111
|
+
except ValueError:
|
|
1112
|
+
error_to_raise = ValueError(msg)
|
|
1113
|
+
else:
|
|
1114
|
+
content = response
|
|
1115
|
+
except (ValidationError, ValidationErrorV1) as e:
|
|
1116
|
+
if not self.handle_validation_error:
|
|
1117
|
+
error_to_raise = e
|
|
1118
|
+
else:
|
|
1119
|
+
content = _handle_validation_error(e, flag=self.handle_validation_error)
|
|
1120
|
+
status = "error"
|
|
1121
|
+
except ToolException as e:
|
|
1122
|
+
if not self.handle_tool_error:
|
|
1123
|
+
error_to_raise = e
|
|
1124
|
+
else:
|
|
1125
|
+
content = _handle_tool_error(e, flag=self.handle_tool_error)
|
|
1126
|
+
status = "error"
|
|
1127
|
+
except (Exception, KeyboardInterrupt) as e:
|
|
1128
|
+
error_to_raise = e
|
|
1129
|
+
|
|
1130
|
+
if error_to_raise:
|
|
1131
|
+
run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
|
|
1132
|
+
raise error_to_raise
|
|
1133
|
+
output = _format_output(content, artifact, tool_call_id, self.name, status)
|
|
1134
|
+
run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
|
|
1135
|
+
return output
|
|
1136
|
+
|
|
1137
|
+
async def arun(
|
|
1138
|
+
self,
|
|
1139
|
+
tool_input: str | dict[str, Any],
|
|
1140
|
+
verbose: bool | None = None, # noqa: FBT001
|
|
1141
|
+
start_color: str | None = "green",
|
|
1142
|
+
color: str | None = "green",
|
|
1143
|
+
callbacks: Callbacks = None,
|
|
1144
|
+
*,
|
|
1145
|
+
tags: list[str] | None = None,
|
|
1146
|
+
metadata: dict[str, Any] | None = None,
|
|
1147
|
+
run_name: str | None = None,
|
|
1148
|
+
run_id: uuid.UUID | None = None,
|
|
1149
|
+
config: RunnableConfig | None = None,
|
|
1150
|
+
tool_call_id: str | None = None,
|
|
1151
|
+
**kwargs: Any,
|
|
1152
|
+
) -> Any:
|
|
1153
|
+
"""Run the tool asynchronously.
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
tool_input: The input to the tool.
|
|
1157
|
+
verbose: Whether to log the tool's progress.
|
|
1158
|
+
start_color: The color to use when starting the tool.
|
|
1159
|
+
color: The color to use when ending the tool.
|
|
1160
|
+
callbacks: Callbacks to be called during tool execution.
|
|
1161
|
+
tags: Optional list of tags associated with the tool.
|
|
1162
|
+
metadata: Optional metadata associated with the tool.
|
|
1163
|
+
run_name: The name of the run.
|
|
1164
|
+
run_id: The id of the run.
|
|
1165
|
+
config: The configuration for the tool.
|
|
1166
|
+
tool_call_id: The id of the tool call.
|
|
1167
|
+
**kwargs: Keyword arguments to be passed to tool callbacks
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
The output of the tool.
|
|
1171
|
+
|
|
1172
|
+
Raises:
|
|
1173
|
+
ToolException: If an error occurs during tool execution.
|
|
1174
|
+
"""
|
|
1175
|
+
callback_manager = AsyncCallbackManager.configure(
|
|
1176
|
+
callbacks,
|
|
1177
|
+
self.callbacks,
|
|
1178
|
+
self.verbose or bool(verbose),
|
|
1179
|
+
tags,
|
|
1180
|
+
self.tags,
|
|
1181
|
+
metadata,
|
|
1182
|
+
self.metadata,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
# Filter out injected arguments from callback inputs
|
|
1186
|
+
filtered_tool_input = (
|
|
1187
|
+
self._filter_injected_args(tool_input)
|
|
1188
|
+
if isinstance(tool_input, dict)
|
|
1189
|
+
else None
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
# Use filtered inputs for the input_str parameter as well
|
|
1193
|
+
tool_input_str = (
|
|
1194
|
+
tool_input
|
|
1195
|
+
if isinstance(tool_input, str)
|
|
1196
|
+
else str(
|
|
1197
|
+
filtered_tool_input if filtered_tool_input is not None else tool_input
|
|
1198
|
+
)
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
run_manager = await callback_manager.on_tool_start(
|
|
1202
|
+
{"name": self.name, "description": self.description},
|
|
1203
|
+
tool_input_str,
|
|
1204
|
+
color=start_color,
|
|
1205
|
+
name=run_name,
|
|
1206
|
+
run_id=run_id,
|
|
1207
|
+
inputs=filtered_tool_input,
|
|
1208
|
+
tool_call_id=tool_call_id,
|
|
1209
|
+
**kwargs,
|
|
1210
|
+
)
|
|
1211
|
+
content = None
|
|
1212
|
+
artifact = None
|
|
1213
|
+
status = "success"
|
|
1214
|
+
error_to_raise: Exception | KeyboardInterrupt | None = None
|
|
1215
|
+
try:
|
|
1216
|
+
tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input, tool_call_id)
|
|
1217
|
+
child_config = patch_config(config, callbacks=run_manager.get_child())
|
|
1218
|
+
with set_config_context(child_config) as context:
|
|
1219
|
+
func_to_check = (
|
|
1220
|
+
self._run if self.__class__._arun is BaseTool._arun else self._arun # noqa: SLF001
|
|
1221
|
+
)
|
|
1222
|
+
if signature(func_to_check).parameters.get("run_manager"):
|
|
1223
|
+
tool_kwargs["run_manager"] = run_manager
|
|
1224
|
+
if config_param := _get_runnable_config_param(func_to_check):
|
|
1225
|
+
tool_kwargs[config_param] = config
|
|
1226
|
+
|
|
1227
|
+
coro = self._arun(*tool_args, **tool_kwargs)
|
|
1228
|
+
response = await coro_with_context(coro, context)
|
|
1229
|
+
if self.response_format == "content_and_artifact":
|
|
1230
|
+
msg = (
|
|
1231
|
+
"Since response_format='content_and_artifact' "
|
|
1232
|
+
"a two-tuple of the message content and raw tool output is "
|
|
1233
|
+
f"expected. Instead, generated response is of type: "
|
|
1234
|
+
f"{type(response)}."
|
|
1235
|
+
)
|
|
1236
|
+
if not isinstance(response, tuple):
|
|
1237
|
+
error_to_raise = ValueError(msg)
|
|
1238
|
+
else:
|
|
1239
|
+
try:
|
|
1240
|
+
content, artifact = response
|
|
1241
|
+
except ValueError:
|
|
1242
|
+
error_to_raise = ValueError(msg)
|
|
1243
|
+
else:
|
|
1244
|
+
content = response
|
|
1245
|
+
except (ValidationError, ValidationErrorV1) as e:
|
|
1246
|
+
if not self.handle_validation_error:
|
|
1247
|
+
error_to_raise = e
|
|
1248
|
+
else:
|
|
1249
|
+
content = _handle_validation_error(e, flag=self.handle_validation_error)
|
|
1250
|
+
status = "error"
|
|
1251
|
+
except ToolException as e:
|
|
1252
|
+
if not self.handle_tool_error:
|
|
1253
|
+
error_to_raise = e
|
|
1254
|
+
else:
|
|
1255
|
+
content = _handle_tool_error(e, flag=self.handle_tool_error)
|
|
1256
|
+
status = "error"
|
|
1257
|
+
except (Exception, KeyboardInterrupt) as e:
|
|
1258
|
+
error_to_raise = e
|
|
1259
|
+
|
|
1260
|
+
if error_to_raise:
|
|
1261
|
+
await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
|
|
1262
|
+
raise error_to_raise
|
|
1263
|
+
|
|
1264
|
+
output = _format_output(content, artifact, tool_call_id, self.name, status)
|
|
1265
|
+
await run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
|
|
1266
|
+
return output
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _is_tool_call(x: Any) -> bool:
|
|
1270
|
+
"""Check if the input is a tool call dictionary.
|
|
1271
|
+
|
|
1272
|
+
Args:
|
|
1273
|
+
x: The input to check.
|
|
1274
|
+
|
|
1275
|
+
Returns:
|
|
1276
|
+
`True` if the input is a tool call, `False` otherwise.
|
|
1277
|
+
"""
|
|
1278
|
+
return isinstance(x, dict) and x.get("type") == "tool_call"
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _handle_validation_error(
|
|
1282
|
+
e: ValidationError | ValidationErrorV1,
|
|
1283
|
+
*,
|
|
1284
|
+
flag: Literal[True] | str | Callable[[ValidationError | ValidationErrorV1], str],
|
|
1285
|
+
) -> str:
|
|
1286
|
+
"""Handle validation errors based on the configured flag.
|
|
1287
|
+
|
|
1288
|
+
Args:
|
|
1289
|
+
e: The validation error that occurred.
|
|
1290
|
+
flag: How to handle the error (`bool`, `str`, or `Callable`).
|
|
1291
|
+
|
|
1292
|
+
Returns:
|
|
1293
|
+
The error message to return.
|
|
1294
|
+
|
|
1295
|
+
Raises:
|
|
1296
|
+
ValueError: If the flag type is unexpected.
|
|
1297
|
+
"""
|
|
1298
|
+
if isinstance(flag, bool):
|
|
1299
|
+
content = "Tool input validation error"
|
|
1300
|
+
elif isinstance(flag, str):
|
|
1301
|
+
content = flag
|
|
1302
|
+
elif callable(flag):
|
|
1303
|
+
content = flag(e)
|
|
1304
|
+
else:
|
|
1305
|
+
msg = ( # type: ignore[unreachable]
|
|
1306
|
+
f"Got unexpected type of `handle_validation_error`. Expected bool, "
|
|
1307
|
+
f"str or callable. Received: {flag}"
|
|
1308
|
+
)
|
|
1309
|
+
raise ValueError(msg) # noqa: TRY004
|
|
1310
|
+
return content
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def _handle_tool_error(
|
|
1314
|
+
e: ToolException,
|
|
1315
|
+
*,
|
|
1316
|
+
flag: Literal[True]
|
|
1317
|
+
| str
|
|
1318
|
+
| Callable[[ToolException], ToolExceptionHandlerOutput]
|
|
1319
|
+
| None,
|
|
1320
|
+
) -> ToolExceptionHandlerOutput:
|
|
1321
|
+
"""Convert a `ToolException` into handled tool output content.
|
|
1322
|
+
|
|
1323
|
+
Args:
|
|
1324
|
+
e: The tool exception that occurred.
|
|
1325
|
+
flag: How to handle the error. `True` uses the exception message, a string
|
|
1326
|
+
replaces the message, and a callable computes replacement content from
|
|
1327
|
+
the exception.
|
|
1328
|
+
|
|
1329
|
+
Returns:
|
|
1330
|
+
The handled error content. This may be plain text or structured message
|
|
1331
|
+
content blocks; callers pass it through normal tool
|
|
1332
|
+
output formatting.
|
|
1333
|
+
|
|
1334
|
+
Raises:
|
|
1335
|
+
ValueError: If the flag type is unexpected.
|
|
1336
|
+
"""
|
|
1337
|
+
if isinstance(flag, bool):
|
|
1338
|
+
content = e.args[0] if e.args else "Tool execution error"
|
|
1339
|
+
elif isinstance(flag, str):
|
|
1340
|
+
content = flag
|
|
1341
|
+
elif callable(flag):
|
|
1342
|
+
content = flag(e)
|
|
1343
|
+
else:
|
|
1344
|
+
msg = (
|
|
1345
|
+
f"Got unexpected type of `handle_tool_error`. Expected bool, str "
|
|
1346
|
+
f"or callable. Received: {flag}"
|
|
1347
|
+
)
|
|
1348
|
+
raise ValueError(msg) # noqa: TRY004
|
|
1349
|
+
return content
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _prep_run_args(
|
|
1353
|
+
value: str | dict[str, Any] | ToolCall,
|
|
1354
|
+
config: RunnableConfig | None,
|
|
1355
|
+
**kwargs: Any,
|
|
1356
|
+
) -> tuple[str | dict[str, Any], dict[str, Any]]:
|
|
1357
|
+
"""Prepare arguments for tool execution.
|
|
1358
|
+
|
|
1359
|
+
Args:
|
|
1360
|
+
value: The input value (`str`, `dict`, or `ToolCall`).
|
|
1361
|
+
config: The runnable configuration.
|
|
1362
|
+
**kwargs: Additional keyword arguments.
|
|
1363
|
+
|
|
1364
|
+
Returns:
|
|
1365
|
+
A tuple of `(tool_input, run_kwargs)`.
|
|
1366
|
+
"""
|
|
1367
|
+
config = ensure_config(config)
|
|
1368
|
+
tool_input: str | dict[str, Any]
|
|
1369
|
+
if _is_tool_call(value):
|
|
1370
|
+
tool_call_id: str | None = cast("ToolCall", value)["id"]
|
|
1371
|
+
tool_input = cast("ToolCall", value)["args"].copy()
|
|
1372
|
+
else:
|
|
1373
|
+
tool_call_id = None
|
|
1374
|
+
tool_input = cast("str | dict[str, Any]", value)
|
|
1375
|
+
return (
|
|
1376
|
+
tool_input,
|
|
1377
|
+
dict(
|
|
1378
|
+
callbacks=config.get("callbacks"),
|
|
1379
|
+
tags=config.get("tags"),
|
|
1380
|
+
metadata=config.get("metadata"),
|
|
1381
|
+
run_name=config.get("run_name"),
|
|
1382
|
+
run_id=config.pop("run_id", None),
|
|
1383
|
+
config=config,
|
|
1384
|
+
tool_call_id=tool_call_id,
|
|
1385
|
+
**kwargs,
|
|
1386
|
+
),
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def _format_output(
|
|
1391
|
+
content: Any,
|
|
1392
|
+
artifact: Any,
|
|
1393
|
+
tool_call_id: str | None,
|
|
1394
|
+
name: str,
|
|
1395
|
+
status: str,
|
|
1396
|
+
) -> ToolOutputMixin | Any:
|
|
1397
|
+
"""Format tool output as a `ToolMessage` if appropriate.
|
|
1398
|
+
|
|
1399
|
+
Args:
|
|
1400
|
+
content: The main content of the tool output.
|
|
1401
|
+
artifact: Any artifact data from the tool.
|
|
1402
|
+
tool_call_id: The ID of the tool call.
|
|
1403
|
+
name: The name of the tool.
|
|
1404
|
+
status: The execution status.
|
|
1405
|
+
|
|
1406
|
+
Returns:
|
|
1407
|
+
The formatted output, either as a `ToolMessage`, the original content,
|
|
1408
|
+
or an unchanged list of `ToolOutputMixin` instances.
|
|
1409
|
+
"""
|
|
1410
|
+
if (
|
|
1411
|
+
isinstance(content, list)
|
|
1412
|
+
and content
|
|
1413
|
+
and all(isinstance(item, ToolOutputMixin) for item in content)
|
|
1414
|
+
):
|
|
1415
|
+
return content
|
|
1416
|
+
if isinstance(content, ToolOutputMixin) or tool_call_id is None:
|
|
1417
|
+
return content
|
|
1418
|
+
normalized_content = _normalize_message_content(content)
|
|
1419
|
+
content = _stringify(content) if normalized_content is None else normalized_content
|
|
1420
|
+
return ToolMessage(
|
|
1421
|
+
content,
|
|
1422
|
+
artifact=artifact,
|
|
1423
|
+
tool_call_id=tool_call_id,
|
|
1424
|
+
name=name,
|
|
1425
|
+
status=status,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _normalize_message_content(obj: Any) -> str | list[MessageContentBlock] | None:
|
|
1430
|
+
"""Coerce valid message content to the shape expected by `ToolMessage`.
|
|
1431
|
+
|
|
1432
|
+
A string passes through unchanged; any `Sequence` of valid content blocks
|
|
1433
|
+
(e.g. a list or tuple) is materialized into a `list`. Returning `None`
|
|
1434
|
+
signals the caller (`_format_output`) that `obj` is not message content and
|
|
1435
|
+
should be stringified instead.
|
|
1436
|
+
|
|
1437
|
+
Args:
|
|
1438
|
+
obj: The object to normalize.
|
|
1439
|
+
|
|
1440
|
+
Returns:
|
|
1441
|
+
The normalized content, or `None` if `obj` is not valid message content.
|
|
1442
|
+
"""
|
|
1443
|
+
if isinstance(obj, str):
|
|
1444
|
+
return obj
|
|
1445
|
+
# Validate lazily before materializing: `all` short-circuits on the first
|
|
1446
|
+
# invalid element, so a large non-content sequence (e.g. `range(10**12)`)
|
|
1447
|
+
# falls back to stringification without allocating it.
|
|
1448
|
+
if isinstance(obj, Sequence) and all(_is_message_content_block(e) for e in obj):
|
|
1449
|
+
return list(obj)
|
|
1450
|
+
return None
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def _is_message_content_block(obj: Any) -> bool:
|
|
1454
|
+
"""Check if object is a valid message content block.
|
|
1455
|
+
|
|
1456
|
+
Validates content blocks for OpenAI or Anthropic format.
|
|
1457
|
+
|
|
1458
|
+
Args:
|
|
1459
|
+
obj: The object to check.
|
|
1460
|
+
|
|
1461
|
+
Returns:
|
|
1462
|
+
`True` if the object is a valid content block, `False` otherwise.
|
|
1463
|
+
"""
|
|
1464
|
+
if isinstance(obj, str):
|
|
1465
|
+
return True
|
|
1466
|
+
if isinstance(obj, dict):
|
|
1467
|
+
return obj.get("type", None) in TOOL_MESSAGE_BLOCK_TYPES
|
|
1468
|
+
return False
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
def _stringify(content: Any) -> str:
|
|
1472
|
+
"""Convert content to string, preferring JSON format.
|
|
1473
|
+
|
|
1474
|
+
Args:
|
|
1475
|
+
content: The content to stringify.
|
|
1476
|
+
|
|
1477
|
+
Returns:
|
|
1478
|
+
String representation of the content.
|
|
1479
|
+
"""
|
|
1480
|
+
try:
|
|
1481
|
+
return json.dumps(content, ensure_ascii=False)
|
|
1482
|
+
except Exception:
|
|
1483
|
+
return str(content)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _describe_callable(func: Any) -> str:
|
|
1487
|
+
"""Return a readable identifier for a callable, for use in log messages."""
|
|
1488
|
+
return getattr(func, "__qualname__", None) or repr(func)
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _get_type_hints(
|
|
1492
|
+
func: Callable[..., Any], *, include_extras: bool = False
|
|
1493
|
+
) -> dict[str, type] | None:
|
|
1494
|
+
"""Get type hints from a function, handling partial functions.
|
|
1495
|
+
|
|
1496
|
+
Args:
|
|
1497
|
+
func: The function to get type hints from.
|
|
1498
|
+
include_extras: Whether to preserve `Annotated` metadata in the resolved
|
|
1499
|
+
hints (e.g. to detect `Annotated[..., InjectedToolArg]`).
|
|
1500
|
+
|
|
1501
|
+
Returns:
|
|
1502
|
+
`dict` of type hints, or `None` if extraction fails.
|
|
1503
|
+
"""
|
|
1504
|
+
if isinstance(func, functools.partial):
|
|
1505
|
+
func = func.func
|
|
1506
|
+
try:
|
|
1507
|
+
return get_type_hints(func, include_extras=include_extras)
|
|
1508
|
+
except Exception:
|
|
1509
|
+
_logger.debug(
|
|
1510
|
+
"Failed to resolve type hints for %s.",
|
|
1511
|
+
_describe_callable(func),
|
|
1512
|
+
exc_info=True,
|
|
1513
|
+
)
|
|
1514
|
+
return None
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def _get_class_signature_source(cls: type) -> Callable[..., Any] | None:
|
|
1518
|
+
"""Return the callable whose parameters `signature(cls)` describes.
|
|
1519
|
+
|
|
1520
|
+
A class's own annotations describe its attributes, not its constructor
|
|
1521
|
+
parameters, so a class is never its own annotation owner. Constructor
|
|
1522
|
+
precedence mirrors `inspect.signature`: a metaclass `__call__` override,
|
|
1523
|
+
then a `__new__`/`__init__` the class defines itself, then an inherited one.
|
|
1524
|
+
|
|
1525
|
+
Args:
|
|
1526
|
+
cls: The class being inspected.
|
|
1527
|
+
|
|
1528
|
+
Returns:
|
|
1529
|
+
The constructor whose annotations describe `signature(cls)`, or `None`
|
|
1530
|
+
when only slot wrappers inherited from `object` are available.
|
|
1531
|
+
"""
|
|
1532
|
+
metaclass = type(cls)
|
|
1533
|
+
if metaclass is not type:
|
|
1534
|
+
metaclass_call = inspect.getattr_static(metaclass, "__call__", None)
|
|
1535
|
+
if inspect.isfunction(metaclass_call):
|
|
1536
|
+
return metaclass_call
|
|
1537
|
+
for attr in ("__new__", "__init__"):
|
|
1538
|
+
if attr in cls.__dict__:
|
|
1539
|
+
return cast("Callable[..., Any]", getattr(cls, attr))
|
|
1540
|
+
for attr in ("__new__", "__init__"):
|
|
1541
|
+
# `object.__new__`/`object.__init__` are slot wrappers carrying no
|
|
1542
|
+
# useful annotations, so only inherited Python constructors qualify.
|
|
1543
|
+
inherited = getattr(cls, attr, None)
|
|
1544
|
+
if inspect.isfunction(inherited):
|
|
1545
|
+
return cast("Callable[..., Any]", inherited)
|
|
1546
|
+
return None
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def _get_type_hints_source(
|
|
1550
|
+
func: Callable[..., Any],
|
|
1551
|
+
) -> Callable[..., Any] | None:
|
|
1552
|
+
"""Return the callable that owns the annotations for an effective signature.
|
|
1553
|
+
|
|
1554
|
+
Unwrapping stops as soon as a callable declares its own `__signature__` (or
|
|
1555
|
+
is a bound method), because that declaration -- not the annotations of
|
|
1556
|
+
whatever it wraps -- defines the effective signature.
|
|
1557
|
+
|
|
1558
|
+
Args:
|
|
1559
|
+
func: The callable being inspected.
|
|
1560
|
+
|
|
1561
|
+
Returns:
|
|
1562
|
+
The function or method whose annotations describe `signature(func)`, or
|
|
1563
|
+
`None` when an explicit `__signature__` supplies the annotations or no
|
|
1564
|
+
annotation owner can be identified.
|
|
1565
|
+
"""
|
|
1566
|
+
func = inspect.unwrap(
|
|
1567
|
+
func,
|
|
1568
|
+
stop=lambda wrapped: (
|
|
1569
|
+
hasattr(wrapped, "__signature__") or inspect.ismethod(wrapped)
|
|
1570
|
+
),
|
|
1571
|
+
)
|
|
1572
|
+
if inspect.ismethod(func):
|
|
1573
|
+
return _get_type_hints_source(func.__func__)
|
|
1574
|
+
if getattr(func, "__signature__", None) is not None:
|
|
1575
|
+
return None
|
|
1576
|
+
if isinstance(func, functools.partial):
|
|
1577
|
+
return _get_type_hints_source(func.func)
|
|
1578
|
+
if inspect.isclass(func):
|
|
1579
|
+
constructor = _get_class_signature_source(func)
|
|
1580
|
+
if constructor is None:
|
|
1581
|
+
return None
|
|
1582
|
+
return _get_type_hints_source(constructor)
|
|
1583
|
+
if not inspect.isroutine(func):
|
|
1584
|
+
callable_obj = cast("Any", func)
|
|
1585
|
+
return _get_type_hints_source(cast("Callable[..., Any]", callable_obj.__call__))
|
|
1586
|
+
return func
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def _get_callable_globals(func: Callable[..., Any]) -> dict[str, Any]:
|
|
1590
|
+
"""Return the globals namespace associated with a callable.
|
|
1591
|
+
|
|
1592
|
+
Explicit `__signature__` objects store annotations separately from a
|
|
1593
|
+
callable's `__annotations__`. String annotations on such signatures
|
|
1594
|
+
still need the callable's defining globals to be resolved.
|
|
1595
|
+
|
|
1596
|
+
Args:
|
|
1597
|
+
func: The callable whose defining globals to locate.
|
|
1598
|
+
|
|
1599
|
+
Returns:
|
|
1600
|
+
The callable's globals namespace, or an empty `dict` when none can be
|
|
1601
|
+
located (e.g. classes, builtins, and other C-implemented callables), in
|
|
1602
|
+
which case no string annotation will resolve.
|
|
1603
|
+
"""
|
|
1604
|
+
if isinstance(func, functools.partial):
|
|
1605
|
+
return _get_callable_globals(func.func)
|
|
1606
|
+
if inspect.ismethod(func):
|
|
1607
|
+
return _get_callable_globals(func.__func__)
|
|
1608
|
+
globalns = getattr(func, "__globals__", None)
|
|
1609
|
+
if isinstance(globalns, dict):
|
|
1610
|
+
return globalns
|
|
1611
|
+
if not inspect.isroutine(func) and not inspect.isclass(func):
|
|
1612
|
+
callable_obj = cast("Any", func)
|
|
1613
|
+
return _get_callable_globals(cast("Callable[..., Any]", callable_obj.__call__))
|
|
1614
|
+
return {}
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def _get_injected_args_keys_from_signature(func: Callable[..., Any]) -> frozenset[str]:
|
|
1618
|
+
"""Identify injected-argument parameters of a callable.
|
|
1619
|
+
|
|
1620
|
+
Resolve annotations with `get_type_hints` (rather than reading raw
|
|
1621
|
+
`signature` annotations) so postponed annotations -- e.g. from
|
|
1622
|
+
`from __future__ import annotations` or quoted forward references -- are
|
|
1623
|
+
recognized. `get_type_hints` resolves every annotation at once and raises
|
|
1624
|
+
on the first failure, so any parameter it leaves uncovered is retried on its
|
|
1625
|
+
own: an unrelated unresolvable forward reference must not disable injection
|
|
1626
|
+
for parameters whose annotations do resolve. Each distinct string annotation
|
|
1627
|
+
is resolved at most once per call, failures included.
|
|
1628
|
+
|
|
1629
|
+
A *string* annotation naming a type that exists only in the callable's
|
|
1630
|
+
defining local scope cannot be resolved here, since only globals are
|
|
1631
|
+
available. Such a parameter keeps its raw string annotation, which
|
|
1632
|
+
`_is_injected_arg_type` never classifies as injected, so it stays visible to
|
|
1633
|
+
the model rather than being injected at call time. Non-string annotations
|
|
1634
|
+
are unaffected, since `signature` yields the live object.
|
|
1635
|
+
|
|
1636
|
+
Args:
|
|
1637
|
+
func: The function (or bound method) whose signature to inspect.
|
|
1638
|
+
|
|
1639
|
+
Returns:
|
|
1640
|
+
`frozenset` of parameter names annotated as injected arguments.
|
|
1641
|
+
"""
|
|
1642
|
+
params = signature(func).parameters
|
|
1643
|
+
hint_source = _get_type_hints_source(func)
|
|
1644
|
+
hints = (
|
|
1645
|
+
_get_type_hints(hint_source, include_extras=True)
|
|
1646
|
+
if hint_source is not None
|
|
1647
|
+
else None
|
|
1648
|
+
) or {}
|
|
1649
|
+
globalns: dict[str, Any] | None = None
|
|
1650
|
+
resolved_annotations: dict[str, Any] = {}
|
|
1651
|
+
keys = set()
|
|
1652
|
+
for name, param in params.items():
|
|
1653
|
+
annotation = hints.get(name, param.annotation)
|
|
1654
|
+
if isinstance(annotation, str):
|
|
1655
|
+
if globalns is None:
|
|
1656
|
+
globalns = _get_callable_globals(
|
|
1657
|
+
func if hint_source is None else hint_source
|
|
1658
|
+
)
|
|
1659
|
+
if annotation not in resolved_annotations:
|
|
1660
|
+
resolved_annotations[annotation] = _resolve_forward_ref(
|
|
1661
|
+
annotation, globalns
|
|
1662
|
+
)
|
|
1663
|
+
resolved = resolved_annotations[annotation]
|
|
1664
|
+
if resolved is None:
|
|
1665
|
+
_logger.debug(
|
|
1666
|
+
"Could not resolve annotation %r for parameter %r of %s; it "
|
|
1667
|
+
"will not be treated as an injected argument.",
|
|
1668
|
+
annotation,
|
|
1669
|
+
name,
|
|
1670
|
+
_describe_callable(func),
|
|
1671
|
+
)
|
|
1672
|
+
else:
|
|
1673
|
+
annotation = resolved
|
|
1674
|
+
if _is_injected_arg_type(annotation):
|
|
1675
|
+
keys.add(name)
|
|
1676
|
+
return frozenset(keys)
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
def _resolve_forward_ref(annotation: str, globalns: dict[str, Any]) -> Any:
|
|
1680
|
+
"""Resolve a single string annotation, returning `None` on failure.
|
|
1681
|
+
|
|
1682
|
+
Uses a temporary annotated function so each annotation can be passed through
|
|
1683
|
+
the public `get_type_hints` API independently.
|
|
1684
|
+
|
|
1685
|
+
Args:
|
|
1686
|
+
annotation: The raw string annotation to resolve.
|
|
1687
|
+
globalns: The globals namespace to resolve names against.
|
|
1688
|
+
|
|
1689
|
+
Returns:
|
|
1690
|
+
The resolved type, or `None` if the annotation cannot be resolved.
|
|
1691
|
+
"""
|
|
1692
|
+
|
|
1693
|
+
def _annotation_holder() -> None:
|
|
1694
|
+
pass
|
|
1695
|
+
|
|
1696
|
+
_annotation_holder.__annotations__ = {"value": annotation}
|
|
1697
|
+
try:
|
|
1698
|
+
return get_type_hints(
|
|
1699
|
+
_annotation_holder,
|
|
1700
|
+
globalns=globalns,
|
|
1701
|
+
include_extras=True,
|
|
1702
|
+
)["value"]
|
|
1703
|
+
except Exception:
|
|
1704
|
+
_logger.debug("Failed to resolve annotation %r.", annotation, exc_info=True)
|
|
1705
|
+
return None
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
def _get_runnable_config_param(func: Callable[..., Any]) -> str | None:
|
|
1709
|
+
"""Find the parameter name for `RunnableConfig` in a function.
|
|
1710
|
+
|
|
1711
|
+
Args:
|
|
1712
|
+
func: The function to check.
|
|
1713
|
+
|
|
1714
|
+
Returns:
|
|
1715
|
+
The parameter name for `RunnableConfig`, or `None` if not found.
|
|
1716
|
+
"""
|
|
1717
|
+
type_hints = _get_type_hints(func)
|
|
1718
|
+
if not type_hints:
|
|
1719
|
+
return None
|
|
1720
|
+
for name, type_ in type_hints.items():
|
|
1721
|
+
if type_ is RunnableConfig:
|
|
1722
|
+
return name
|
|
1723
|
+
return None
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
class InjectedToolArg:
|
|
1727
|
+
"""Annotation for tool arguments that are injected at runtime.
|
|
1728
|
+
|
|
1729
|
+
Tool arguments annotated with this class are not included in the tool
|
|
1730
|
+
schema sent to language models and are instead injected during execution.
|
|
1731
|
+
"""
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
class _DirectlyInjectedToolArg:
|
|
1735
|
+
"""Annotation for tool arguments that are injected at runtime.
|
|
1736
|
+
|
|
1737
|
+
Injected via direct type annotation, rather than annotated metadata.
|
|
1738
|
+
|
|
1739
|
+
For example, `ToolRuntime` is a directly injected argument.
|
|
1740
|
+
|
|
1741
|
+
Note the direct annotation rather than the verbose alternative:
|
|
1742
|
+
`Annotated[ToolRuntime, InjectedRuntime]`
|
|
1743
|
+
|
|
1744
|
+
```python
|
|
1745
|
+
from coco_core.tools import tool, ToolRuntime
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
@tool
|
|
1749
|
+
def foo(x: int, runtime: ToolRuntime) -> str:
|
|
1750
|
+
# use runtime.state, runtime.context, runtime.store, etc.
|
|
1751
|
+
...
|
|
1752
|
+
```
|
|
1753
|
+
"""
|
|
1754
|
+
|
|
1755
|
+
|
|
1756
|
+
class InjectedToolCallId(InjectedToolArg):
|
|
1757
|
+
"""Annotation for injecting the tool call ID.
|
|
1758
|
+
|
|
1759
|
+
This annotation is used to mark a tool parameter that should receive the tool call
|
|
1760
|
+
ID at runtime.
|
|
1761
|
+
|
|
1762
|
+
```python
|
|
1763
|
+
from typing import Annotated
|
|
1764
|
+
from coco_core.messages import ToolMessage
|
|
1765
|
+
from coco_core.tools import tool, InjectedToolCallId
|
|
1766
|
+
|
|
1767
|
+
@tool
|
|
1768
|
+
def foo(
|
|
1769
|
+
x: int, tool_call_id: Annotated[str, InjectedToolCallId]
|
|
1770
|
+
) -> ToolMessage:
|
|
1771
|
+
\"\"\"Return x.\"\"\"
|
|
1772
|
+
return ToolMessage(
|
|
1773
|
+
str(x),
|
|
1774
|
+
artifact=x,
|
|
1775
|
+
name="foo",
|
|
1776
|
+
tool_call_id=tool_call_id
|
|
1777
|
+
)
|
|
1778
|
+
```
|
|
1779
|
+
"""
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
def _is_directly_injected_arg_type(type_: Any) -> bool:
|
|
1783
|
+
"""Check if a type annotation indicates a directly injected argument.
|
|
1784
|
+
|
|
1785
|
+
This is currently only used for `ToolRuntime`.
|
|
1786
|
+
|
|
1787
|
+
Checks if either the annotation itself is a subclass of `_DirectlyInjectedToolArg`
|
|
1788
|
+
or the origin of the annotation is a subclass of `_DirectlyInjectedToolArg`.
|
|
1789
|
+
|
|
1790
|
+
For example, `ToolRuntime` or `ToolRuntime[ContextT, StateT]` would both return
|
|
1791
|
+
`True`.
|
|
1792
|
+
"""
|
|
1793
|
+
return (
|
|
1794
|
+
isinstance(type_, type) and issubclass(type_, _DirectlyInjectedToolArg)
|
|
1795
|
+
) or (
|
|
1796
|
+
(origin := get_origin(type_)) is not None
|
|
1797
|
+
and isinstance(origin, type)
|
|
1798
|
+
and issubclass(origin, _DirectlyInjectedToolArg)
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def _is_injected_arg_type(
|
|
1803
|
+
type_: type | TypeVar, injected_type: type[InjectedToolArg] | None = None
|
|
1804
|
+
) -> bool:
|
|
1805
|
+
"""Check if a type annotation indicates an injected argument.
|
|
1806
|
+
|
|
1807
|
+
Args:
|
|
1808
|
+
type_: The type annotation to check.
|
|
1809
|
+
injected_type: The specific injected type to check for.
|
|
1810
|
+
|
|
1811
|
+
Returns:
|
|
1812
|
+
`True` if the type is an injected argument, `False` otherwise.
|
|
1813
|
+
"""
|
|
1814
|
+
if injected_type is None:
|
|
1815
|
+
# if no injected type is specified,
|
|
1816
|
+
# check if the type is a directly injected argument
|
|
1817
|
+
if _is_directly_injected_arg_type(type_):
|
|
1818
|
+
return True
|
|
1819
|
+
injected_type = InjectedToolArg
|
|
1820
|
+
|
|
1821
|
+
# if the type is an Annotated type, check if annotated metadata
|
|
1822
|
+
# is an intance or subclass of the injected type
|
|
1823
|
+
return any(
|
|
1824
|
+
isinstance(arg, injected_type)
|
|
1825
|
+
or (isinstance(arg, type) and issubclass(arg, injected_type))
|
|
1826
|
+
for arg in get_args(type_)[1:]
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def get_all_basemodel_annotations(
|
|
1831
|
+
cls: TypeBaseModel | Any, *, default_to_bound: bool = True
|
|
1832
|
+
) -> dict[str, type | TypeVar]:
|
|
1833
|
+
"""Get all annotations from a Pydantic `BaseModel` and its parents.
|
|
1834
|
+
|
|
1835
|
+
Args:
|
|
1836
|
+
cls: The Pydantic `BaseModel` class.
|
|
1837
|
+
default_to_bound: Whether to default to the bound of a `TypeVar` if it exists.
|
|
1838
|
+
|
|
1839
|
+
Returns:
|
|
1840
|
+
`dict` of field names to their type annotations.
|
|
1841
|
+
"""
|
|
1842
|
+
orig_bases: tuple[type, ...]
|
|
1843
|
+
# cls has no subscript: cls = FooBar
|
|
1844
|
+
if isinstance(cls, type):
|
|
1845
|
+
fields = get_fields(cls)
|
|
1846
|
+
alias_map = {field.alias: name for name, field in fields.items() if field.alias}
|
|
1847
|
+
|
|
1848
|
+
annotations: dict[str, type | TypeVar] = {}
|
|
1849
|
+
for name, param in inspect.signature(cls).parameters.items():
|
|
1850
|
+
# Exclude hidden init args added by pydantic Config. For example if
|
|
1851
|
+
# BaseModel(extra="allow") then "extra_data" will part of init sig.
|
|
1852
|
+
if name not in fields and name not in alias_map:
|
|
1853
|
+
continue
|
|
1854
|
+
field_name = alias_map.get(name, name)
|
|
1855
|
+
annotations[field_name] = param.annotation
|
|
1856
|
+
orig_bases = getattr(cls, "__orig_bases__", ())
|
|
1857
|
+
# cls has subscript: cls = FooBar[int]
|
|
1858
|
+
else:
|
|
1859
|
+
annotations = get_all_basemodel_annotations(
|
|
1860
|
+
get_origin(cls), default_to_bound=False
|
|
1861
|
+
)
|
|
1862
|
+
orig_bases = (cls,)
|
|
1863
|
+
|
|
1864
|
+
# Pydantic v2 automatically resolves inherited generics, Pydantic v1 does not.
|
|
1865
|
+
if not (isinstance(cls, type) and is_pydantic_v2_subclass(cls)):
|
|
1866
|
+
# if cls = FooBar inherits from Baz[str], orig_bases will contain Baz[str]
|
|
1867
|
+
# if cls = FooBar inherits from Baz, orig_bases will contain Baz
|
|
1868
|
+
# if cls = FooBar[int], orig_bases will contain FooBar[int]
|
|
1869
|
+
for parent in orig_bases:
|
|
1870
|
+
# if class = FooBar inherits from Baz, parent = Baz
|
|
1871
|
+
if isinstance(parent, type) and is_pydantic_v1_subclass(parent):
|
|
1872
|
+
annotations.update(
|
|
1873
|
+
get_all_basemodel_annotations(parent, default_to_bound=False)
|
|
1874
|
+
)
|
|
1875
|
+
continue
|
|
1876
|
+
|
|
1877
|
+
parent_origin = get_origin(parent)
|
|
1878
|
+
|
|
1879
|
+
# if class = FooBar inherits from non-pydantic class
|
|
1880
|
+
if not parent_origin:
|
|
1881
|
+
continue
|
|
1882
|
+
|
|
1883
|
+
# if class = FooBar inherits from Baz[str]:
|
|
1884
|
+
# parent = class Baz[str],
|
|
1885
|
+
# parent_origin = class Baz,
|
|
1886
|
+
# generic_type_vars = (type vars in Baz)
|
|
1887
|
+
# generic_map = {type var in Baz: str}
|
|
1888
|
+
generic_type_vars: tuple[TypeVar, ...] = getattr(
|
|
1889
|
+
parent_origin, "__parameters__", ()
|
|
1890
|
+
)
|
|
1891
|
+
generic_map = dict(zip(generic_type_vars, get_args(parent), strict=False))
|
|
1892
|
+
for field in getattr(parent_origin, "__annotations__", {}):
|
|
1893
|
+
annotations[field] = _replace_type_vars(
|
|
1894
|
+
annotations[field], generic_map, default_to_bound=default_to_bound
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
return {
|
|
1898
|
+
k: _replace_type_vars(v, default_to_bound=default_to_bound)
|
|
1899
|
+
for k, v in annotations.items()
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
def _replace_type_vars(
|
|
1904
|
+
type_: type | TypeVar,
|
|
1905
|
+
generic_map: dict[TypeVar, type] | None = None,
|
|
1906
|
+
*,
|
|
1907
|
+
default_to_bound: bool = True,
|
|
1908
|
+
) -> type | TypeVar:
|
|
1909
|
+
"""Replace `TypeVar`s in a type annotation with concrete types.
|
|
1910
|
+
|
|
1911
|
+
Args:
|
|
1912
|
+
type_: The type annotation to process.
|
|
1913
|
+
generic_map: Mapping of `TypeVar`s to concrete types.
|
|
1914
|
+
default_to_bound: Whether to use `TypeVar` bounds as defaults.
|
|
1915
|
+
|
|
1916
|
+
Returns:
|
|
1917
|
+
The type with `TypeVar`s replaced.
|
|
1918
|
+
"""
|
|
1919
|
+
generic_map = generic_map or {}
|
|
1920
|
+
if isinstance(type_, TypeVar):
|
|
1921
|
+
if type_ in generic_map:
|
|
1922
|
+
return generic_map[type_]
|
|
1923
|
+
if default_to_bound:
|
|
1924
|
+
return type_.__bound__ if type_.__bound__ is not None else Any
|
|
1925
|
+
return type_
|
|
1926
|
+
if (origin := get_origin(type_)) and (args := get_args(type_)):
|
|
1927
|
+
new_args = tuple(
|
|
1928
|
+
_replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)
|
|
1929
|
+
for arg in args
|
|
1930
|
+
)
|
|
1931
|
+
return cast("type", _py_38_safe_origin(origin)[new_args]) # type: ignore[index]
|
|
1932
|
+
return type_
|
|
1933
|
+
|
|
1934
|
+
|
|
1935
|
+
class BaseToolkit(BaseModel, ABC):
|
|
1936
|
+
"""Base class for toolkits containing related tools.
|
|
1937
|
+
|
|
1938
|
+
A toolkit is a collection of related tools that can be used together to accomplish a
|
|
1939
|
+
specific task or work with a particular system.
|
|
1940
|
+
"""
|
|
1941
|
+
|
|
1942
|
+
@abstractmethod
|
|
1943
|
+
def get_tools(self) -> list[BaseTool]:
|
|
1944
|
+
"""Get all tools in the toolkit.
|
|
1945
|
+
|
|
1946
|
+
Returns:
|
|
1947
|
+
List of tools contained in this toolkit.
|
|
1948
|
+
"""
|