coco-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
"""Methods for creating function specs in the style of OpenAI Functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import collections
|
|
6
|
+
import copy
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import types
|
|
10
|
+
import typing
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import (
|
|
13
|
+
TYPE_CHECKING,
|
|
14
|
+
Annotated,
|
|
15
|
+
Any,
|
|
16
|
+
Literal,
|
|
17
|
+
Union,
|
|
18
|
+
cast,
|
|
19
|
+
get_args,
|
|
20
|
+
get_origin,
|
|
21
|
+
get_type_hints,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
import typing_extensions
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
from pydantic.errors import PydanticInvalidForJsonSchema
|
|
27
|
+
from pydantic.v1 import BaseModel as BaseModelV1
|
|
28
|
+
from pydantic.v1 import Field as Field_v1
|
|
29
|
+
from pydantic.v1 import create_model as create_model_v1
|
|
30
|
+
from typing_extensions import TypedDict, is_typeddict
|
|
31
|
+
|
|
32
|
+
from coco_core._api import beta
|
|
33
|
+
from coco_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
|
|
34
|
+
from coco_core.utils.json_schema import dereference_refs
|
|
35
|
+
from coco_core.utils.pydantic import is_basemodel_subclass
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable, Mapping
|
|
39
|
+
|
|
40
|
+
from coco_core.tools import BaseTool
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
PYTHON_TO_JSON_TYPES = {
|
|
45
|
+
"str": "string",
|
|
46
|
+
"int": "integer",
|
|
47
|
+
"float": "number",
|
|
48
|
+
"bool": "boolean",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_ORIGIN_MAP: dict[type, Any] = {
|
|
52
|
+
dict: dict,
|
|
53
|
+
list: list,
|
|
54
|
+
tuple: tuple,
|
|
55
|
+
set: set,
|
|
56
|
+
collections.abc.Iterable: typing.Iterable,
|
|
57
|
+
collections.abc.Mapping: typing.Mapping,
|
|
58
|
+
collections.abc.Sequence: typing.Sequence,
|
|
59
|
+
collections.abc.MutableMapping: typing.MutableMapping,
|
|
60
|
+
types.UnionType: Union,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FunctionDescription(TypedDict):
|
|
65
|
+
"""Representation of a callable function to send to an LLM."""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
"""The name of the function."""
|
|
69
|
+
|
|
70
|
+
description: str
|
|
71
|
+
"""A description of the function."""
|
|
72
|
+
|
|
73
|
+
parameters: dict[str, Any]
|
|
74
|
+
"""The parameters of the function."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ToolDescription(TypedDict):
|
|
78
|
+
"""Representation of a callable function to the OpenAI API."""
|
|
79
|
+
|
|
80
|
+
type: Literal["function"]
|
|
81
|
+
"""The type of the tool."""
|
|
82
|
+
|
|
83
|
+
function: FunctionDescription
|
|
84
|
+
"""The function description."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _rm_titles(kv: dict[str, Any], prev_key: str = "") -> dict[str, Any]:
|
|
88
|
+
"""Recursively removes `'title'` fields from a JSON schema dictionary.
|
|
89
|
+
|
|
90
|
+
Remove `'title'` fields from the input JSON schema dictionary,
|
|
91
|
+
except when a `'title'` appears within a property definition under `'properties'`.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
kv: The input JSON schema as a dictionary.
|
|
95
|
+
prev_key: The key from the parent dictionary, used to identify context.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
A new dictionary with appropriate `'title'` fields removed.
|
|
99
|
+
"""
|
|
100
|
+
new_kv = {}
|
|
101
|
+
|
|
102
|
+
for k, v in kv.items():
|
|
103
|
+
if k == "title":
|
|
104
|
+
# If the value is a nested dict and part of a property under "properties",
|
|
105
|
+
# preserve the title but continue recursion
|
|
106
|
+
if isinstance(v, dict) and prev_key == "properties":
|
|
107
|
+
new_kv[k] = _rm_titles(v, k)
|
|
108
|
+
else:
|
|
109
|
+
# Otherwise, remove this "title" key
|
|
110
|
+
continue
|
|
111
|
+
elif isinstance(v, dict):
|
|
112
|
+
# Recurse into nested dictionaries
|
|
113
|
+
new_kv[k] = _rm_titles(v, k)
|
|
114
|
+
else:
|
|
115
|
+
# Leave non-dict values untouched
|
|
116
|
+
new_kv[k] = v
|
|
117
|
+
|
|
118
|
+
return new_kv
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _convert_json_schema_to_openai_function(
|
|
122
|
+
schema: dict[str, Any],
|
|
123
|
+
*,
|
|
124
|
+
name: str | None = None,
|
|
125
|
+
description: str | None = None,
|
|
126
|
+
rm_titles: bool = True,
|
|
127
|
+
) -> FunctionDescription:
|
|
128
|
+
"""Converts a Pydantic model to a function description for the OpenAI API.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
schema: The JSON schema to convert.
|
|
132
|
+
name: The name of the function.
|
|
133
|
+
|
|
134
|
+
If not provided, the title of the schema will be used.
|
|
135
|
+
description: The description of the function.
|
|
136
|
+
|
|
137
|
+
If not provided, the description of the schema will be used.
|
|
138
|
+
rm_titles: Whether to remove titles from the schema.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The function description.
|
|
142
|
+
"""
|
|
143
|
+
schema = dereference_refs(schema)
|
|
144
|
+
if "definitions" in schema: # pydantic 1
|
|
145
|
+
schema.pop("definitions", None)
|
|
146
|
+
if "$defs" in schema: # pydantic 2
|
|
147
|
+
schema.pop("$defs", None)
|
|
148
|
+
title = schema.pop("title", "")
|
|
149
|
+
default_description = schema.pop("description", "")
|
|
150
|
+
return {
|
|
151
|
+
"name": name or title,
|
|
152
|
+
"description": description or default_description,
|
|
153
|
+
"parameters": _rm_titles(schema) if rm_titles else schema,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _convert_pydantic_to_openai_function(
|
|
158
|
+
model: type,
|
|
159
|
+
*,
|
|
160
|
+
name: str | None = None,
|
|
161
|
+
description: str | None = None,
|
|
162
|
+
rm_titles: bool = True,
|
|
163
|
+
) -> FunctionDescription:
|
|
164
|
+
"""Converts a Pydantic model to a function description for the OpenAI API.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
model: The Pydantic model to convert.
|
|
168
|
+
name: The name of the function.
|
|
169
|
+
|
|
170
|
+
If not provided, the title of the schema will be used.
|
|
171
|
+
description: The description of the function.
|
|
172
|
+
|
|
173
|
+
If not provided, the description of the schema will be used.
|
|
174
|
+
rm_titles: Whether to remove titles from the schema.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
TypeError: If the model is not a Pydantic model.
|
|
178
|
+
TypeError: If the model contains types that cannot be converted to JSON schema.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The function description.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
if hasattr(model, "model_json_schema"):
|
|
185
|
+
schema = model.model_json_schema() # Pydantic 2
|
|
186
|
+
elif hasattr(model, "schema"):
|
|
187
|
+
schema = model.schema() # Pydantic 1
|
|
188
|
+
else:
|
|
189
|
+
msg = "Model must be a Pydantic model."
|
|
190
|
+
raise TypeError(msg)
|
|
191
|
+
except PydanticInvalidForJsonSchema as e:
|
|
192
|
+
model_name = getattr(model, "__name__", str(model))
|
|
193
|
+
msg = (
|
|
194
|
+
f"Failed to generate JSON schema for '{model_name}': {e}\n\n"
|
|
195
|
+
"Tool argument schemas must be JSON-serializable. If your schema includes "
|
|
196
|
+
"custom Python classes, consider:\n"
|
|
197
|
+
" 1. Converting them to Pydantic models with JSON-compatible fields\n"
|
|
198
|
+
" 2. Using primitive types (str, int, float, bool, list, dict) instead\n"
|
|
199
|
+
" 3. Passing the data as serialized JSON strings\n\n"
|
|
200
|
+
)
|
|
201
|
+
raise PydanticInvalidForJsonSchema(msg) from e
|
|
202
|
+
return _convert_json_schema_to_openai_function(
|
|
203
|
+
schema, name=name, description=description, rm_titles=rm_titles
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _get_python_function_name(function: Callable[..., Any]) -> str:
|
|
208
|
+
"""Get the name of a Python function."""
|
|
209
|
+
return function.__name__
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _convert_python_function_to_openai_function(
|
|
213
|
+
function: Callable[..., Any],
|
|
214
|
+
) -> FunctionDescription:
|
|
215
|
+
"""Convert a Python function to an OpenAI function-calling API compatible dict.
|
|
216
|
+
|
|
217
|
+
Assumes the Python function has type hints and a docstring with a description. If
|
|
218
|
+
the docstring has Google Python style argument descriptions, these will be included
|
|
219
|
+
as well.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
function: The Python function to convert.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The OpenAI function description.
|
|
226
|
+
"""
|
|
227
|
+
# Import locally to prevent circular import
|
|
228
|
+
from coco_core.tools.base import create_schema_from_function # noqa: PLC0415
|
|
229
|
+
|
|
230
|
+
func_name = _get_python_function_name(function)
|
|
231
|
+
model = create_schema_from_function(
|
|
232
|
+
func_name,
|
|
233
|
+
function,
|
|
234
|
+
filter_args=(),
|
|
235
|
+
parse_docstring=True,
|
|
236
|
+
error_on_invalid_docstring=False,
|
|
237
|
+
include_injected=False,
|
|
238
|
+
)
|
|
239
|
+
return _convert_pydantic_to_openai_function(
|
|
240
|
+
model,
|
|
241
|
+
name=func_name,
|
|
242
|
+
description=model.__doc__,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _convert_typed_dict_to_openai_function(typed_dict: type) -> FunctionDescription:
|
|
247
|
+
visited: dict[type, type] = {}
|
|
248
|
+
|
|
249
|
+
model = cast(
|
|
250
|
+
"type[BaseModel]",
|
|
251
|
+
_convert_any_typed_dicts_to_pydantic(typed_dict, visited=visited),
|
|
252
|
+
)
|
|
253
|
+
return _convert_pydantic_to_openai_function(model)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
_MAX_TYPED_DICT_RECURSION = 25
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _convert_any_typed_dicts_to_pydantic(
|
|
260
|
+
type_: type,
|
|
261
|
+
*,
|
|
262
|
+
visited: dict[type, type],
|
|
263
|
+
depth: int = 0,
|
|
264
|
+
) -> type:
|
|
265
|
+
if type_ in visited:
|
|
266
|
+
return visited[type_]
|
|
267
|
+
if depth >= _MAX_TYPED_DICT_RECURSION:
|
|
268
|
+
return type_
|
|
269
|
+
if is_typeddict(type_):
|
|
270
|
+
typed_dict = type_
|
|
271
|
+
docstring = inspect.getdoc(typed_dict)
|
|
272
|
+
# Use get_type_hints to properly resolve forward references and
|
|
273
|
+
# string annotations in Python 3.14+ (PEP 649 deferred annotations).
|
|
274
|
+
# include_extras=True preserves Annotated metadata.
|
|
275
|
+
try:
|
|
276
|
+
annotations_ = get_type_hints(typed_dict, include_extras=True)
|
|
277
|
+
except Exception:
|
|
278
|
+
# Fallback for edge cases where get_type_hints might fail
|
|
279
|
+
annotations_ = typed_dict.__annotations__
|
|
280
|
+
description, arg_descriptions = _parse_google_docstring(
|
|
281
|
+
docstring, list(annotations_)
|
|
282
|
+
)
|
|
283
|
+
fields: dict[str, Any] = {}
|
|
284
|
+
for arg, arg_type in annotations_.items():
|
|
285
|
+
if get_origin(arg_type) in {Annotated, typing_extensions.Annotated}:
|
|
286
|
+
annotated_args = get_args(arg_type)
|
|
287
|
+
new_arg_type = _convert_any_typed_dicts_to_pydantic(
|
|
288
|
+
annotated_args[0], depth=depth + 1, visited=visited
|
|
289
|
+
)
|
|
290
|
+
field_kwargs = dict(
|
|
291
|
+
zip(("default", "description"), annotated_args[1:], strict=False)
|
|
292
|
+
)
|
|
293
|
+
if (field_desc := field_kwargs.get("description")) and not isinstance(
|
|
294
|
+
field_desc, str
|
|
295
|
+
):
|
|
296
|
+
msg = (
|
|
297
|
+
f"Invalid annotation for field {arg}. Third argument to "
|
|
298
|
+
f"Annotated must be a string description, received value of "
|
|
299
|
+
f"type {type(field_desc)}."
|
|
300
|
+
)
|
|
301
|
+
raise ValueError(msg)
|
|
302
|
+
if arg_desc := arg_descriptions.get(arg):
|
|
303
|
+
field_kwargs["description"] = arg_desc
|
|
304
|
+
fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
|
|
305
|
+
else:
|
|
306
|
+
new_arg_type = _convert_any_typed_dicts_to_pydantic(
|
|
307
|
+
arg_type, depth=depth + 1, visited=visited
|
|
308
|
+
)
|
|
309
|
+
field_kwargs = {"default": ...}
|
|
310
|
+
if arg_desc := arg_descriptions.get(arg):
|
|
311
|
+
field_kwargs["description"] = arg_desc
|
|
312
|
+
fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
|
|
313
|
+
model = cast(
|
|
314
|
+
"type[BaseModelV1]", create_model_v1(typed_dict.__name__, **fields)
|
|
315
|
+
)
|
|
316
|
+
model.__doc__ = description
|
|
317
|
+
visited[typed_dict] = model
|
|
318
|
+
return model
|
|
319
|
+
if (origin := get_origin(type_)) and (type_args := get_args(type_)):
|
|
320
|
+
subscriptable_origin = _py_38_safe_origin(origin)
|
|
321
|
+
type_args = tuple(
|
|
322
|
+
_convert_any_typed_dicts_to_pydantic(arg, depth=depth + 1, visited=visited)
|
|
323
|
+
for arg in type_args
|
|
324
|
+
)
|
|
325
|
+
return cast("type", subscriptable_origin[type_args]) # type: ignore[index]
|
|
326
|
+
return type_
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
|
|
330
|
+
"""Format tool into the OpenAI function API.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
tool: The tool to format.
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
ValueError: If the tool call schema is not supported.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The function description.
|
|
340
|
+
"""
|
|
341
|
+
# Import locally to prevent circular import
|
|
342
|
+
from coco_core.tools import Tool # noqa: PLC0415
|
|
343
|
+
|
|
344
|
+
is_simple_oai_tool = isinstance(tool, Tool) and not tool.args_schema
|
|
345
|
+
if tool.tool_call_schema and not is_simple_oai_tool:
|
|
346
|
+
if isinstance(tool.tool_call_schema, dict):
|
|
347
|
+
return _convert_json_schema_to_openai_function(
|
|
348
|
+
tool.tool_call_schema, name=tool.name, description=tool.description
|
|
349
|
+
)
|
|
350
|
+
if issubclass(tool.tool_call_schema, (BaseModel, BaseModelV1)):
|
|
351
|
+
return _convert_pydantic_to_openai_function(
|
|
352
|
+
tool.tool_call_schema, name=tool.name, description=tool.description
|
|
353
|
+
)
|
|
354
|
+
error_msg = ( # type: ignore[unreachable]
|
|
355
|
+
f"Unsupported tool call schema: {tool.tool_call_schema}. "
|
|
356
|
+
"Tool call schema must be a JSON schema dict or a Pydantic model."
|
|
357
|
+
)
|
|
358
|
+
raise ValueError(error_msg)
|
|
359
|
+
return {
|
|
360
|
+
"name": tool.name,
|
|
361
|
+
"description": tool.description,
|
|
362
|
+
"parameters": {
|
|
363
|
+
# This is a hack to get around the fact that some tools
|
|
364
|
+
# do not expose an args_schema, and expect an argument
|
|
365
|
+
# which is a string.
|
|
366
|
+
# And Open AI does not support an array type for the
|
|
367
|
+
# parameters.
|
|
368
|
+
"properties": {
|
|
369
|
+
"__arg1": {"title": "__arg1", "type": "string"},
|
|
370
|
+
},
|
|
371
|
+
"required": ["__arg1"],
|
|
372
|
+
"type": "object",
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def convert_to_openai_function(
|
|
378
|
+
function: Mapping[str, Any] | type | Callable[..., Any] | BaseTool,
|
|
379
|
+
*,
|
|
380
|
+
strict: bool | None = None,
|
|
381
|
+
) -> dict[str, Any]:
|
|
382
|
+
"""Convert a raw function/class to an OpenAI function.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
function: A dictionary, Pydantic `BaseModel` class, `TypedDict` class, a
|
|
386
|
+
Coco `Tool` object, or a Python function.
|
|
387
|
+
|
|
388
|
+
If a dictionary is passed in, it is assumed to already be a valid OpenAI
|
|
389
|
+
function, a JSON schema with top-level `title` key specified, an Anthropic
|
|
390
|
+
format tool, or an Amazon Bedrock Converse format tool.
|
|
391
|
+
strict: If `True`, model output is guaranteed to exactly match the JSON Schema
|
|
392
|
+
provided in the function definition.
|
|
393
|
+
|
|
394
|
+
If `None`, `strict` argument will not be included in function definition.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
A dict version of the passed in function which is compatible with the OpenAI
|
|
398
|
+
function-calling API.
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
ValueError: If function is not in a supported format.
|
|
402
|
+
|
|
403
|
+
!!! warning "Behavior changed in `trustcoco-core` 0.3.16"
|
|
404
|
+
|
|
405
|
+
`description` and `parameters` keys are now optional. Only `name` is
|
|
406
|
+
required and guaranteed to be part of the output.
|
|
407
|
+
"""
|
|
408
|
+
# Import locally to prevent circular import
|
|
409
|
+
from coco_core.tools import BaseTool # noqa: PLC0415
|
|
410
|
+
|
|
411
|
+
if strict and isinstance(function, dict):
|
|
412
|
+
function = copy.deepcopy(function)
|
|
413
|
+
|
|
414
|
+
# an Anthropic format tool
|
|
415
|
+
if isinstance(function, dict) and all(
|
|
416
|
+
k in function for k in ("name", "input_schema")
|
|
417
|
+
):
|
|
418
|
+
oai_function = {
|
|
419
|
+
"name": function["name"],
|
|
420
|
+
"parameters": function["input_schema"],
|
|
421
|
+
}
|
|
422
|
+
if "description" in function:
|
|
423
|
+
oai_function["description"] = function["description"]
|
|
424
|
+
# an Amazon Bedrock Converse format tool
|
|
425
|
+
elif isinstance(function, dict) and "toolSpec" in function:
|
|
426
|
+
oai_function = {
|
|
427
|
+
"name": function["toolSpec"]["name"],
|
|
428
|
+
"parameters": function["toolSpec"]["inputSchema"]["json"],
|
|
429
|
+
}
|
|
430
|
+
if "description" in function["toolSpec"]:
|
|
431
|
+
oai_function["description"] = function["toolSpec"]["description"]
|
|
432
|
+
# already in OpenAI function format
|
|
433
|
+
elif isinstance(function, dict) and "name" in function:
|
|
434
|
+
oai_function = {
|
|
435
|
+
k: v
|
|
436
|
+
for k, v in function.items()
|
|
437
|
+
if k in {"name", "description", "parameters", "strict"}
|
|
438
|
+
}
|
|
439
|
+
# a JSON schema with title and description
|
|
440
|
+
elif isinstance(function, dict) and "title" in function:
|
|
441
|
+
function_copy = function.copy()
|
|
442
|
+
oai_function = {"name": function_copy.pop("title")}
|
|
443
|
+
if "description" in function_copy:
|
|
444
|
+
oai_function["description"] = function_copy.pop("description")
|
|
445
|
+
if function_copy and "properties" in function_copy:
|
|
446
|
+
oai_function["parameters"] = function_copy
|
|
447
|
+
elif isinstance(function, type) and is_basemodel_subclass(function):
|
|
448
|
+
oai_function = cast(
|
|
449
|
+
"dict[str, Any]", _convert_pydantic_to_openai_function(function)
|
|
450
|
+
)
|
|
451
|
+
elif is_typeddict(function):
|
|
452
|
+
oai_function = cast(
|
|
453
|
+
"dict[str, Any]",
|
|
454
|
+
_convert_typed_dict_to_openai_function(cast("type", function)),
|
|
455
|
+
)
|
|
456
|
+
elif isinstance(function, BaseTool):
|
|
457
|
+
oai_function = cast("dict[str, Any]", _format_tool_to_openai_function(function))
|
|
458
|
+
elif callable(function):
|
|
459
|
+
oai_function = cast(
|
|
460
|
+
"dict[str, Any]", _convert_python_function_to_openai_function(function)
|
|
461
|
+
)
|
|
462
|
+
else:
|
|
463
|
+
if isinstance(function, dict) and (
|
|
464
|
+
"type" in function or "properties" in function
|
|
465
|
+
):
|
|
466
|
+
msg = (
|
|
467
|
+
f"Unsupported function\n\n{function}\n\nTo use a JSON schema as a "
|
|
468
|
+
"function, it must have a top-level 'title' key to be used as the "
|
|
469
|
+
"function name."
|
|
470
|
+
)
|
|
471
|
+
raise ValueError(msg)
|
|
472
|
+
msg = (
|
|
473
|
+
f"Unsupported function\n\n{function}\n\nFunctions must be passed in"
|
|
474
|
+
" as Dict, pydantic.BaseModel, or Callable. If they're a dict they must"
|
|
475
|
+
" either be in OpenAI function format or valid JSON schema with top-level"
|
|
476
|
+
" 'title' key."
|
|
477
|
+
)
|
|
478
|
+
raise ValueError(msg)
|
|
479
|
+
|
|
480
|
+
if strict is not None:
|
|
481
|
+
if "strict" in oai_function and oai_function["strict"] != strict:
|
|
482
|
+
msg = (
|
|
483
|
+
f"Tool/function already has a 'strict' key with value "
|
|
484
|
+
f"{oai_function['strict']} which is different from the explicit "
|
|
485
|
+
f"`strict` arg received {strict=}."
|
|
486
|
+
)
|
|
487
|
+
raise ValueError(msg)
|
|
488
|
+
oai_function["strict"] = strict
|
|
489
|
+
if strict:
|
|
490
|
+
oai_function["parameters"] = _recursive_set_additional_properties_false(
|
|
491
|
+
oai_function["parameters"]
|
|
492
|
+
)
|
|
493
|
+
return oai_function
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# List of well known tools supported by OpenAI's chat models or responses API.
|
|
497
|
+
# These tools are not expected to be supported by other chat model providers
|
|
498
|
+
# that conform to the OpenAI function-calling API.
|
|
499
|
+
_WellKnownOpenAITools = (
|
|
500
|
+
"function",
|
|
501
|
+
"file_search",
|
|
502
|
+
"computer",
|
|
503
|
+
"computer_use_preview",
|
|
504
|
+
"code_interpreter",
|
|
505
|
+
"mcp",
|
|
506
|
+
"image_generation",
|
|
507
|
+
"web_search_preview",
|
|
508
|
+
"web_search",
|
|
509
|
+
"tool_search",
|
|
510
|
+
"apply_patch",
|
|
511
|
+
"namespace",
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def convert_to_openai_tool(
|
|
516
|
+
tool: Mapping[str, Any] | type[BaseModel] | Callable[..., Any] | BaseTool,
|
|
517
|
+
*,
|
|
518
|
+
strict: bool | None = None,
|
|
519
|
+
) -> dict[str, Any]:
|
|
520
|
+
"""Convert a tool-like object to an OpenAI tool schema.
|
|
521
|
+
|
|
522
|
+
[OpenAI tool schema reference](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools)
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
tool: Either a dictionary, a `pydantic.BaseModel` class, Python function, or
|
|
526
|
+
`BaseTool`.
|
|
527
|
+
|
|
528
|
+
If a dictionary is passed in, it is assumed to already be a valid OpenAI
|
|
529
|
+
function, a JSON schema with top-level `title` key specified, an Anthropic
|
|
530
|
+
format tool, or an Amazon Bedrock Converse format tool.
|
|
531
|
+
strict: If `True`, model output is guaranteed to exactly match the JSON Schema
|
|
532
|
+
provided in the function definition.
|
|
533
|
+
|
|
534
|
+
If `None`, `strict` argument will not be included in tool definition.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
A dict version of the passed in tool which is compatible with the OpenAI
|
|
538
|
+
tool-calling API.
|
|
539
|
+
|
|
540
|
+
!!! warning "Behavior changed in `trustcoco-core` 0.3.16"
|
|
541
|
+
|
|
542
|
+
`description` and `parameters` keys are now optional. Only `name` is
|
|
543
|
+
required and guaranteed to be part of the output.
|
|
544
|
+
|
|
545
|
+
!!! warning "Behavior changed in `trustcoco-core` 0.3.44"
|
|
546
|
+
|
|
547
|
+
Return OpenAI Responses API-style tools unchanged. This includes
|
|
548
|
+
any dict with `"type"` in `"file_search"`, `"function"`,
|
|
549
|
+
`"computer_use_preview"`, `"web_search_preview"`, `"apply_patch"`.
|
|
550
|
+
|
|
551
|
+
!!! warning "Behavior changed in `trustcoco-core` 0.3.63"
|
|
552
|
+
|
|
553
|
+
Added support for OpenAI's image generation built-in tool.
|
|
554
|
+
"""
|
|
555
|
+
# Import locally to prevent circular import
|
|
556
|
+
from coco_core.tools import Tool # noqa: PLC0415
|
|
557
|
+
|
|
558
|
+
if isinstance(tool, dict):
|
|
559
|
+
if tool.get("type") in _WellKnownOpenAITools:
|
|
560
|
+
return tool
|
|
561
|
+
# As of 03.12.25 can be "web_search_preview" or "web_search_preview_2025_03_11"
|
|
562
|
+
if (tool.get("type") or "").startswith("web_search_preview"):
|
|
563
|
+
return tool
|
|
564
|
+
if isinstance(tool, Tool) and (tool.metadata or {}).get("type") == "custom_tool":
|
|
565
|
+
oai_tool = {
|
|
566
|
+
"type": "custom",
|
|
567
|
+
"name": tool.name,
|
|
568
|
+
"description": tool.description,
|
|
569
|
+
}
|
|
570
|
+
if tool.metadata is not None and "format" in tool.metadata:
|
|
571
|
+
oai_tool["format"] = tool.metadata["format"]
|
|
572
|
+
return oai_tool
|
|
573
|
+
oai_function = convert_to_openai_function(tool, strict=strict)
|
|
574
|
+
return {"type": "function", "function": oai_function}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def convert_to_json_schema(
|
|
578
|
+
schema: dict[str, Any] | type[BaseModel] | Callable[..., Any] | BaseTool,
|
|
579
|
+
*,
|
|
580
|
+
strict: bool | None = None,
|
|
581
|
+
) -> dict[str, Any]:
|
|
582
|
+
"""Convert a schema representation to a JSON schema.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
schema: The schema to convert.
|
|
586
|
+
strict: If `True`, model output is guaranteed to exactly match the JSON Schema
|
|
587
|
+
provided in the function definition.
|
|
588
|
+
|
|
589
|
+
If `None`, `strict` argument will not be included in function definition.
|
|
590
|
+
|
|
591
|
+
Raises:
|
|
592
|
+
ValueError: If the input is not a valid OpenAI-format tool.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
A JSON schema representation of the input schema.
|
|
596
|
+
"""
|
|
597
|
+
openai_tool = convert_to_openai_tool(schema, strict=strict)
|
|
598
|
+
if (
|
|
599
|
+
not isinstance(openai_tool, dict)
|
|
600
|
+
or "function" not in openai_tool
|
|
601
|
+
or "name" not in openai_tool["function"]
|
|
602
|
+
):
|
|
603
|
+
error_message = "Input must be a valid OpenAI-format tool."
|
|
604
|
+
raise ValueError(error_message)
|
|
605
|
+
|
|
606
|
+
openai_function = openai_tool["function"]
|
|
607
|
+
json_schema = {}
|
|
608
|
+
json_schema["title"] = openai_function["name"]
|
|
609
|
+
|
|
610
|
+
if "description" in openai_function:
|
|
611
|
+
json_schema["description"] = openai_function["description"]
|
|
612
|
+
|
|
613
|
+
if "parameters" in openai_function:
|
|
614
|
+
parameters = openai_function["parameters"].copy()
|
|
615
|
+
json_schema.update(parameters)
|
|
616
|
+
|
|
617
|
+
return json_schema
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@beta()
|
|
621
|
+
def tool_example_to_messages(
|
|
622
|
+
input: str,
|
|
623
|
+
tool_calls: list[BaseModel],
|
|
624
|
+
tool_outputs: list[str] | None = None,
|
|
625
|
+
*,
|
|
626
|
+
ai_response: str | None = None,
|
|
627
|
+
) -> list[BaseMessage]:
|
|
628
|
+
"""Convert an example into a list of messages that can be fed into an LLM.
|
|
629
|
+
|
|
630
|
+
This code is an adapter that converts a single example to a list of messages
|
|
631
|
+
that can be fed into a chat model.
|
|
632
|
+
|
|
633
|
+
The list of messages per example by default corresponds to:
|
|
634
|
+
|
|
635
|
+
1. `HumanMessage`: contains the content from which content should be extracted.
|
|
636
|
+
2. `AIMessage`: contains the extracted information from the model
|
|
637
|
+
3. `ToolMessage`: contains confirmation to the model that the model requested a
|
|
638
|
+
tool correctly.
|
|
639
|
+
|
|
640
|
+
If `ai_response` is specified, there will be a final `AIMessage` with that
|
|
641
|
+
response.
|
|
642
|
+
|
|
643
|
+
The `ToolMessage` is required because some chat models are hyper-optimized for
|
|
644
|
+
agents rather than for an extraction use case.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
input: The user input
|
|
648
|
+
tool_calls: Tool calls represented as Pydantic BaseModels
|
|
649
|
+
tool_outputs: Tool call outputs.
|
|
650
|
+
|
|
651
|
+
Does not need to be provided.
|
|
652
|
+
|
|
653
|
+
If not provided, a placeholder value will be inserted.
|
|
654
|
+
ai_response: If provided, content for a final `AIMessage`.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
A list of messages
|
|
658
|
+
|
|
659
|
+
Examples:
|
|
660
|
+
```python
|
|
661
|
+
from typing import Optional
|
|
662
|
+
from pydantic import BaseModel, Field
|
|
663
|
+
from coco_openai import ChatOpenAI
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class Person(BaseModel):
|
|
667
|
+
'''Information about a person.'''
|
|
668
|
+
|
|
669
|
+
name: str | None = Field(..., description="The name of the person")
|
|
670
|
+
hair_color: str | None = Field(
|
|
671
|
+
..., description="The color of the person's hair if known"
|
|
672
|
+
)
|
|
673
|
+
height_in_meters: str | None = Field(..., description="Height in METERS")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
examples = [
|
|
677
|
+
(
|
|
678
|
+
"The ocean is vast and blue. It's more than 20,000 feet deep.",
|
|
679
|
+
Person(name=None, height_in_meters=None, hair_color=None),
|
|
680
|
+
),
|
|
681
|
+
(
|
|
682
|
+
"Fiona traveled far from France to Spain.",
|
|
683
|
+
Person(name="Fiona", height_in_meters=None, hair_color=None),
|
|
684
|
+
),
|
|
685
|
+
]
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
messages = []
|
|
689
|
+
|
|
690
|
+
for txt, tool_call in examples:
|
|
691
|
+
messages.extend(tool_example_to_messages(txt, [tool_call]))
|
|
692
|
+
```
|
|
693
|
+
"""
|
|
694
|
+
messages: list[BaseMessage] = [HumanMessage(content=input)]
|
|
695
|
+
|
|
696
|
+
openai_tool_calls = [
|
|
697
|
+
{
|
|
698
|
+
"id": str(uuid.uuid4()),
|
|
699
|
+
"type": "function",
|
|
700
|
+
"function": {
|
|
701
|
+
# The name of the function right now corresponds to the name
|
|
702
|
+
# of the Pydantic model. This is implicit in the API right now,
|
|
703
|
+
# and will be improved over time.
|
|
704
|
+
"name": tool_call.__class__.__name__,
|
|
705
|
+
"arguments": tool_call.model_dump_json(),
|
|
706
|
+
},
|
|
707
|
+
}
|
|
708
|
+
for tool_call in tool_calls
|
|
709
|
+
]
|
|
710
|
+
|
|
711
|
+
messages.append(
|
|
712
|
+
AIMessage(content="", additional_kwargs={"tool_calls": openai_tool_calls})
|
|
713
|
+
)
|
|
714
|
+
if tool_outputs is not None and len(tool_outputs) != len(openai_tool_calls):
|
|
715
|
+
msg = (
|
|
716
|
+
f"The number of tool_outputs ({len(tool_outputs)}) must match the number "
|
|
717
|
+
f"of tool_calls ({len(openai_tool_calls)}). Got {len(tool_outputs)} "
|
|
718
|
+
f"output(s) for {len(openai_tool_calls)} tool call(s)."
|
|
719
|
+
)
|
|
720
|
+
raise ValueError(msg)
|
|
721
|
+
tool_outputs = tool_outputs or ["You have correctly called this tool."] * len(
|
|
722
|
+
openai_tool_calls
|
|
723
|
+
)
|
|
724
|
+
for output, tool_call_dict in zip(tool_outputs, openai_tool_calls, strict=False):
|
|
725
|
+
messages.append(ToolMessage(content=output, tool_call_id=tool_call_dict["id"]))
|
|
726
|
+
|
|
727
|
+
if ai_response:
|
|
728
|
+
messages.append(AIMessage(content=ai_response))
|
|
729
|
+
return messages
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
_MIN_DOCSTRING_BLOCKS = 2
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _parse_google_docstring(
|
|
736
|
+
docstring: str | None,
|
|
737
|
+
args: list[str],
|
|
738
|
+
*,
|
|
739
|
+
error_on_invalid_docstring: bool = False,
|
|
740
|
+
) -> tuple[str, dict[str, str]]:
|
|
741
|
+
"""Parse the function and argument descriptions from the docstring of a function.
|
|
742
|
+
|
|
743
|
+
Assumes the function docstring follows Google Python style guide.
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
docstring: The docstring to parse.
|
|
747
|
+
args: The list of argument names to extract descriptions for.
|
|
748
|
+
error_on_invalid_docstring: Whether to raise an error if the docstring is
|
|
749
|
+
invalid.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
A tuple of the function description and a dictionary of argument descriptions.
|
|
753
|
+
"""
|
|
754
|
+
if docstring:
|
|
755
|
+
docstring_blocks = docstring.split("\n\n")
|
|
756
|
+
if error_on_invalid_docstring:
|
|
757
|
+
filtered_annotations = {
|
|
758
|
+
arg
|
|
759
|
+
for arg in args
|
|
760
|
+
if arg not in {"run_manager", "callbacks", "runtime", "return"}
|
|
761
|
+
}
|
|
762
|
+
if filtered_annotations and (
|
|
763
|
+
len(docstring_blocks) < _MIN_DOCSTRING_BLOCKS
|
|
764
|
+
or not any(block.startswith("Args:") for block in docstring_blocks[1:])
|
|
765
|
+
):
|
|
766
|
+
msg = "Found invalid Google-Style docstring."
|
|
767
|
+
raise ValueError(msg)
|
|
768
|
+
descriptors = []
|
|
769
|
+
args_block = None
|
|
770
|
+
past_descriptors = False
|
|
771
|
+
for block in docstring_blocks:
|
|
772
|
+
if block.startswith("Args:"):
|
|
773
|
+
args_block = block
|
|
774
|
+
break
|
|
775
|
+
if block.startswith(("Returns:", "Example:")):
|
|
776
|
+
# Don't break in case Args come after
|
|
777
|
+
past_descriptors = True
|
|
778
|
+
elif not past_descriptors:
|
|
779
|
+
descriptors.append(block)
|
|
780
|
+
else:
|
|
781
|
+
continue
|
|
782
|
+
description = " ".join(descriptors).strip()
|
|
783
|
+
else:
|
|
784
|
+
if error_on_invalid_docstring:
|
|
785
|
+
msg = "Found invalid Google-Style docstring."
|
|
786
|
+
raise ValueError(msg)
|
|
787
|
+
description = ""
|
|
788
|
+
args_block = None
|
|
789
|
+
arg_descriptions: dict[str, str] = {}
|
|
790
|
+
if args_block:
|
|
791
|
+
arg: str | None = None
|
|
792
|
+
# Base indentation, latched once from the first argument line, lets us
|
|
793
|
+
# distinguish new argument definitions from continuation lines. This
|
|
794
|
+
# assumes Google-style uniform indentation of argument names: a line
|
|
795
|
+
# indented deeper than the first argument is treated as a continuation
|
|
796
|
+
# (even if it contains a colon), so a more-indented later `name:` line
|
|
797
|
+
# in a malformed, non-uniformly-indented block folds into the previous
|
|
798
|
+
# argument rather than starting a new one.
|
|
799
|
+
arg_indent: int | None = None
|
|
800
|
+
for line in args_block.split("\n")[1:]:
|
|
801
|
+
if not line.strip():
|
|
802
|
+
continue
|
|
803
|
+
current_indent = len(line) - len(line.lstrip())
|
|
804
|
+
if arg_indent is None and ":" in line:
|
|
805
|
+
arg_indent = current_indent
|
|
806
|
+
is_continuation = arg_indent is not None and current_indent > arg_indent
|
|
807
|
+
if arg is not None and is_continuation:
|
|
808
|
+
arg_descriptions[arg] += " " + line.strip()
|
|
809
|
+
elif ":" in line:
|
|
810
|
+
arg, desc = line.split(":", maxsplit=1)
|
|
811
|
+
arg = arg.strip()
|
|
812
|
+
arg_name, _, annotations_ = arg.partition(" ")
|
|
813
|
+
if annotations_.startswith("(") and annotations_.endswith(")"):
|
|
814
|
+
arg = arg_name
|
|
815
|
+
arg_descriptions[arg] = desc.strip()
|
|
816
|
+
elif arg:
|
|
817
|
+
arg_descriptions[arg] += " " + line.strip()
|
|
818
|
+
return description, arg_descriptions
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _py_38_safe_origin(origin: type) -> type:
|
|
822
|
+
return cast("type", _ORIGIN_MAP.get(origin, origin))
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _recursive_set_additional_properties_false(
|
|
826
|
+
schema: dict[str, Any],
|
|
827
|
+
) -> dict[str, Any]:
|
|
828
|
+
if isinstance(schema, dict):
|
|
829
|
+
# OpenAI strict mode requires every property to appear in `required` at every
|
|
830
|
+
# level of nesting. Without this, nested object schemas are rejected.
|
|
831
|
+
properties = schema.get("properties")
|
|
832
|
+
if isinstance(properties, dict) and properties:
|
|
833
|
+
schema["required"] = list(properties.keys())
|
|
834
|
+
|
|
835
|
+
# Check if 'required' is a key at the current level or if the schema is empty,
|
|
836
|
+
# in which case additionalProperties still needs to be specified.
|
|
837
|
+
if (
|
|
838
|
+
"required" in schema
|
|
839
|
+
or ("properties" in schema and not schema["properties"])
|
|
840
|
+
# Since Pydantic 2.11, it will always add `additionalProperties: True`
|
|
841
|
+
# for arbitrary dictionary schemas
|
|
842
|
+
# See: https://pydantic.dev/articles/pydantic-v2-11-release#changes
|
|
843
|
+
# If it is already set to True, we need override it to False
|
|
844
|
+
or "additionalProperties" in schema
|
|
845
|
+
):
|
|
846
|
+
schema["additionalProperties"] = False
|
|
847
|
+
|
|
848
|
+
# Recursively check 'properties' and 'items' if they exist
|
|
849
|
+
if "anyOf" in schema:
|
|
850
|
+
for sub_schema in schema["anyOf"]:
|
|
851
|
+
_recursive_set_additional_properties_false(sub_schema)
|
|
852
|
+
# Pydantic <2.9 wraps a referenced model field in 'allOf' when it has
|
|
853
|
+
# sibling keys (e.g. 'description'), instead of merging them directly.
|
|
854
|
+
if "allOf" in schema:
|
|
855
|
+
for sub_schema in schema["allOf"]:
|
|
856
|
+
_recursive_set_additional_properties_false(sub_schema)
|
|
857
|
+
if "properties" in schema:
|
|
858
|
+
for sub_schema in schema["properties"].values():
|
|
859
|
+
_recursive_set_additional_properties_false(sub_schema)
|
|
860
|
+
if "items" in schema:
|
|
861
|
+
_recursive_set_additional_properties_false(schema["items"])
|
|
862
|
+
# Raw JSON schemas may keep nested objects in `$defs` and reference them via
|
|
863
|
+
# `$ref`; walk those definitions too so they're made strict.
|
|
864
|
+
for defs_key in ("$defs", "definitions"):
|
|
865
|
+
if isinstance(schema.get(defs_key), dict):
|
|
866
|
+
for sub_schema in schema[defs_key].values():
|
|
867
|
+
_recursive_set_additional_properties_false(sub_schema)
|
|
868
|
+
|
|
869
|
+
return schema
|