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/utils/usage.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Usage utilities."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _dict_int_op(
|
|
8
|
+
left: dict[str, Any],
|
|
9
|
+
right: dict[str, Any],
|
|
10
|
+
op: Callable[[int, int], int],
|
|
11
|
+
*,
|
|
12
|
+
default: int = 0,
|
|
13
|
+
depth: int = 0,
|
|
14
|
+
max_depth: int = 100,
|
|
15
|
+
) -> dict[str, Any]:
|
|
16
|
+
"""Apply an integer operation to corresponding values in two dictionaries.
|
|
17
|
+
|
|
18
|
+
Recursively combines two dictionaries by applying the given operation to integer
|
|
19
|
+
values at matching keys.
|
|
20
|
+
|
|
21
|
+
Supports nested dictionaries.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
left: First dictionary to combine.
|
|
25
|
+
right: Second dictionary to combine.
|
|
26
|
+
op: Binary operation function to apply to integer values.
|
|
27
|
+
default: Default value to use when a key is missing from a dictionary.
|
|
28
|
+
depth: Current recursion depth (used internally).
|
|
29
|
+
max_depth: Maximum recursion depth (to prevent infinite loops).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A new dictionary with combined values.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If `max_depth` is exceeded or if value types are not supported.
|
|
36
|
+
"""
|
|
37
|
+
if depth >= max_depth:
|
|
38
|
+
msg = f"{max_depth=} exceeded, unable to combine dicts."
|
|
39
|
+
raise ValueError(msg)
|
|
40
|
+
combined: dict[str, Any] = {}
|
|
41
|
+
for k in set(left).union(right):
|
|
42
|
+
if isinstance(left.get(k, default), int) and isinstance(
|
|
43
|
+
right.get(k, default), int
|
|
44
|
+
):
|
|
45
|
+
combined[k] = op(left.get(k, default), right.get(k, default))
|
|
46
|
+
elif isinstance(left.get(k, {}), dict) and isinstance(right.get(k, {}), dict):
|
|
47
|
+
combined[k] = _dict_int_op(
|
|
48
|
+
left.get(k, {}),
|
|
49
|
+
right.get(k, {}),
|
|
50
|
+
op,
|
|
51
|
+
default=default,
|
|
52
|
+
depth=depth + 1,
|
|
53
|
+
max_depth=max_depth,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
types = [type(d[k]) for d in (left, right) if k in d]
|
|
57
|
+
msg = (
|
|
58
|
+
f"Unknown value types: {types}. Only dict and int values are supported."
|
|
59
|
+
)
|
|
60
|
+
raise ValueError(msg) # noqa: TRY004
|
|
61
|
+
return combined
|
coco_core/utils/utils.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""Generic utility functions."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import datetime
|
|
5
|
+
import functools
|
|
6
|
+
import importlib
|
|
7
|
+
import os
|
|
8
|
+
import warnings
|
|
9
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
from typing import Any, overload
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from packaging.version import parse
|
|
15
|
+
from pydantic import SecretStr
|
|
16
|
+
from requests import HTTPError, Response
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
|
|
19
|
+
from coco_core.utils.pydantic import (
|
|
20
|
+
is_pydantic_v1_subclass,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def xor_args(*arg_groups: tuple[str, ...]) -> Callable[..., Any]:
|
|
25
|
+
"""Validate specified keyword args are mutually exclusive.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
*arg_groups: Groups of mutually exclusive keyword args.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Decorator that validates the specified keyword args are mutually exclusive.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
35
|
+
@functools.wraps(func)
|
|
36
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
37
|
+
"""Validate exactly one arg in each group is not None."""
|
|
38
|
+
counts = [
|
|
39
|
+
sum(1 for arg in arg_group if kwargs.get(arg) is not None)
|
|
40
|
+
for arg_group in arg_groups
|
|
41
|
+
]
|
|
42
|
+
invalid_groups = [i for i, count in enumerate(counts) if count != 1]
|
|
43
|
+
if invalid_groups:
|
|
44
|
+
invalid_group_names = [", ".join(arg_groups[i]) for i in invalid_groups]
|
|
45
|
+
msg = (
|
|
46
|
+
"Exactly one argument in each of the following"
|
|
47
|
+
" groups must be defined:"
|
|
48
|
+
f" {', '.join(invalid_group_names)}"
|
|
49
|
+
)
|
|
50
|
+
raise ValueError(msg)
|
|
51
|
+
return func(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def raise_for_status_with_text(response: Response) -> None:
|
|
59
|
+
"""Raise an error with the response text.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
response: The response to check for errors.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the response has an error status code.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
except HTTPError as e:
|
|
70
|
+
raise ValueError(response.text) from e
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@contextlib.contextmanager
|
|
74
|
+
def mock_now(dt_value: datetime.datetime) -> Iterator[type]:
|
|
75
|
+
"""Context manager for mocking out datetime.now() in unit tests.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
dt_value: The datetime value to use for datetime.now().
|
|
79
|
+
|
|
80
|
+
Yields:
|
|
81
|
+
The mocked datetime class.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
```python
|
|
85
|
+
with mock_now(datetime.datetime(2011, 2, 3, 10, 11)):
|
|
86
|
+
assert datetime.datetime.now() == datetime.datetime(2011, 2, 3, 10, 11)
|
|
87
|
+
```
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
class MockDateTime(datetime.datetime):
|
|
91
|
+
"""Mock datetime.datetime.now() with a fixed datetime."""
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
@override
|
|
95
|
+
def now(cls, tz: datetime.tzinfo | None = None) -> "MockDateTime":
|
|
96
|
+
# Create a copy of dt_value.
|
|
97
|
+
return MockDateTime(
|
|
98
|
+
dt_value.year,
|
|
99
|
+
dt_value.month,
|
|
100
|
+
dt_value.day,
|
|
101
|
+
dt_value.hour,
|
|
102
|
+
dt_value.minute,
|
|
103
|
+
dt_value.second,
|
|
104
|
+
dt_value.microsecond,
|
|
105
|
+
dt_value.tzinfo,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
real_datetime = datetime.datetime
|
|
109
|
+
datetime.datetime = MockDateTime # type: ignore[misc]
|
|
110
|
+
try:
|
|
111
|
+
yield datetime.datetime
|
|
112
|
+
finally:
|
|
113
|
+
datetime.datetime = real_datetime # type: ignore[misc]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def guard_import(
|
|
117
|
+
module_name: str, *, pip_name: str | None = None, package: str | None = None
|
|
118
|
+
) -> Any:
|
|
119
|
+
"""Dynamically import a module.
|
|
120
|
+
|
|
121
|
+
Raise an exception if the module is not installed.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
module_name: The name of the module to import.
|
|
125
|
+
pip_name: The name of the module to install with pip.
|
|
126
|
+
package: The package to import the module from.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The imported module.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ImportError: If the module is not installed.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
module = importlib.import_module(module_name, package)
|
|
136
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
137
|
+
top_level = module_name.split(".", maxsplit=1)[0]
|
|
138
|
+
# Coco modules ship in trustcoco distributions: coco_core in
|
|
139
|
+
# trustcoco-core, coco in trustcoco. Any other module keeps the
|
|
140
|
+
# underscore to hyphen rule.
|
|
141
|
+
default = top_level.replace("_", "-")
|
|
142
|
+
if top_level == "coco" or top_level.startswith("coco_"):
|
|
143
|
+
default = "trust" + default
|
|
144
|
+
pip_name = pip_name or default
|
|
145
|
+
msg = (
|
|
146
|
+
f"Could not import {module_name} python package. "
|
|
147
|
+
f"Please install it with `pip install {pip_name}`."
|
|
148
|
+
)
|
|
149
|
+
raise ImportError(msg) from e
|
|
150
|
+
return module
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_package_version(
|
|
154
|
+
package: str,
|
|
155
|
+
lt_version: str | None = None,
|
|
156
|
+
lte_version: str | None = None,
|
|
157
|
+
gt_version: str | None = None,
|
|
158
|
+
gte_version: str | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Check the version of a package.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
package: The name of the package.
|
|
164
|
+
lt_version: The version must be less than this.
|
|
165
|
+
lte_version: The version must be less than or equal to this.
|
|
166
|
+
gt_version: The version must be greater than this.
|
|
167
|
+
gte_version: The version must be greater than or equal to this.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If the package version does not meet the requirements.
|
|
172
|
+
"""
|
|
173
|
+
imported_version = parse(version(package))
|
|
174
|
+
if lt_version is not None and imported_version >= parse(lt_version):
|
|
175
|
+
msg = (
|
|
176
|
+
f"Expected {package} version to be < {lt_version}. Received "
|
|
177
|
+
f"{imported_version}."
|
|
178
|
+
)
|
|
179
|
+
raise ValueError(msg)
|
|
180
|
+
if lte_version is not None and imported_version > parse(lte_version):
|
|
181
|
+
msg = (
|
|
182
|
+
f"Expected {package} version to be <= {lte_version}. Received "
|
|
183
|
+
f"{imported_version}."
|
|
184
|
+
)
|
|
185
|
+
raise ValueError(msg)
|
|
186
|
+
if gt_version is not None and imported_version <= parse(gt_version):
|
|
187
|
+
msg = (
|
|
188
|
+
f"Expected {package} version to be > {gt_version}. Received "
|
|
189
|
+
f"{imported_version}."
|
|
190
|
+
)
|
|
191
|
+
raise ValueError(msg)
|
|
192
|
+
if gte_version is not None and imported_version < parse(gte_version):
|
|
193
|
+
msg = (
|
|
194
|
+
f"Expected {package} version to be >= {gte_version}. Received "
|
|
195
|
+
f"{imported_version}."
|
|
196
|
+
)
|
|
197
|
+
raise ValueError(msg)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_pydantic_field_names(pydantic_cls: Any) -> set[str]:
|
|
201
|
+
"""Get field names, including aliases, for a pydantic class.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
pydantic_cls: Pydantic class.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Field names.
|
|
208
|
+
"""
|
|
209
|
+
all_required_field_names = set()
|
|
210
|
+
if is_pydantic_v1_subclass(pydantic_cls):
|
|
211
|
+
for field in pydantic_cls.__fields__.values():
|
|
212
|
+
all_required_field_names.add(field.name)
|
|
213
|
+
if field.has_alias:
|
|
214
|
+
all_required_field_names.add(field.alias)
|
|
215
|
+
else: # Assuming pydantic 2 for now
|
|
216
|
+
for name, field in pydantic_cls.model_fields.items():
|
|
217
|
+
all_required_field_names.add(name)
|
|
218
|
+
if field.alias:
|
|
219
|
+
all_required_field_names.add(field.alias)
|
|
220
|
+
return all_required_field_names
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _build_model_kwargs(
|
|
224
|
+
values: dict[str, Any],
|
|
225
|
+
all_required_field_names: set[str],
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Build `model_kwargs` param from Pydantic constructor values.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
values: All init args passed in by user.
|
|
231
|
+
all_required_field_names: All required field names for the pydantic class.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Extra kwargs.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
ValueError: If a field is specified in both `values` and `extra_kwargs`.
|
|
238
|
+
ValueError: If a field is specified in `model_kwargs`.
|
|
239
|
+
"""
|
|
240
|
+
extra_kwargs = values.get("model_kwargs", {})
|
|
241
|
+
for field_name in list(values):
|
|
242
|
+
if field_name in extra_kwargs:
|
|
243
|
+
msg = f"Found {field_name} supplied twice."
|
|
244
|
+
raise ValueError(msg)
|
|
245
|
+
if field_name not in all_required_field_names:
|
|
246
|
+
warnings.warn(
|
|
247
|
+
f"""WARNING! {field_name} is not default parameter.
|
|
248
|
+
{field_name} was transferred to model_kwargs.
|
|
249
|
+
Please confirm that {field_name} is what you intended.""",
|
|
250
|
+
stacklevel=7,
|
|
251
|
+
)
|
|
252
|
+
extra_kwargs[field_name] = values.pop(field_name)
|
|
253
|
+
|
|
254
|
+
invalid_model_kwargs = all_required_field_names.intersection(extra_kwargs.keys())
|
|
255
|
+
if invalid_model_kwargs:
|
|
256
|
+
warnings.warn(
|
|
257
|
+
f"Parameters {invalid_model_kwargs} should be specified explicitly. "
|
|
258
|
+
f"Instead they were passed in as part of `model_kwargs` parameter.",
|
|
259
|
+
stacklevel=7,
|
|
260
|
+
)
|
|
261
|
+
for k in invalid_model_kwargs:
|
|
262
|
+
values[k] = extra_kwargs.pop(k)
|
|
263
|
+
|
|
264
|
+
values["model_kwargs"] = extra_kwargs
|
|
265
|
+
return values
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# DON'T USE! Kept for backwards-compatibility but should never have been public.
|
|
269
|
+
def build_extra_kwargs(
|
|
270
|
+
extra_kwargs: dict[str, Any],
|
|
271
|
+
values: dict[str, Any],
|
|
272
|
+
all_required_field_names: set[str],
|
|
273
|
+
) -> dict[str, Any]:
|
|
274
|
+
"""Build extra kwargs from values and extra_kwargs.
|
|
275
|
+
|
|
276
|
+
!!! danger "DON'T USE"
|
|
277
|
+
|
|
278
|
+
Kept for backwards-compatibility but should never have been public. Use the
|
|
279
|
+
internal `_build_model_kwargs` function instead.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
extra_kwargs: Extra kwargs passed in by user.
|
|
283
|
+
values: Values passed in by user.
|
|
284
|
+
all_required_field_names: All required field names for the pydantic class.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Extra kwargs.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
ValueError: If a field is specified in both `values` and `extra_kwargs`.
|
|
291
|
+
ValueError: If a field is specified in `model_kwargs`.
|
|
292
|
+
"""
|
|
293
|
+
# DON'T USE! Kept for backwards-compatibility but should never have been public.
|
|
294
|
+
for field_name in list(values):
|
|
295
|
+
if field_name in extra_kwargs:
|
|
296
|
+
msg = f"Found {field_name} supplied twice."
|
|
297
|
+
raise ValueError(msg)
|
|
298
|
+
if field_name not in all_required_field_names:
|
|
299
|
+
warnings.warn(
|
|
300
|
+
f"""WARNING! {field_name} is not default parameter.
|
|
301
|
+
{field_name} was transferred to model_kwargs.
|
|
302
|
+
Please confirm that {field_name} is what you intended.""",
|
|
303
|
+
stacklevel=7,
|
|
304
|
+
)
|
|
305
|
+
extra_kwargs[field_name] = values.pop(field_name)
|
|
306
|
+
|
|
307
|
+
# DON'T USE! Kept for backwards-compatibility but should never have been public.
|
|
308
|
+
invalid_model_kwargs = all_required_field_names.intersection(extra_kwargs.keys())
|
|
309
|
+
if invalid_model_kwargs:
|
|
310
|
+
msg = (
|
|
311
|
+
f"Parameters {invalid_model_kwargs} should be specified explicitly. "
|
|
312
|
+
f"Instead they were passed in as part of `model_kwargs` parameter."
|
|
313
|
+
)
|
|
314
|
+
raise ValueError(msg)
|
|
315
|
+
|
|
316
|
+
# DON'T USE! Kept for backwards-compatibility but should never have been public.
|
|
317
|
+
return extra_kwargs
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def convert_to_secret_str(value: SecretStr | str) -> SecretStr:
|
|
321
|
+
"""Convert a string to a `SecretStr` if needed.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
value: The value to convert.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The `SecretStr` value.
|
|
328
|
+
"""
|
|
329
|
+
if isinstance(value, SecretStr):
|
|
330
|
+
return value
|
|
331
|
+
return SecretStr(value)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class _NoDefaultType:
|
|
335
|
+
"""Type to indicate no default value is provided."""
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
_NoDefault = _NoDefaultType()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@overload
|
|
342
|
+
def from_env(key: str, /) -> Callable[[], str]: ...
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@overload
|
|
346
|
+
def from_env(key: str, /, *, default: str) -> Callable[[], str]: ...
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@overload
|
|
350
|
+
def from_env(key: Sequence[str], /, *, default: str) -> Callable[[], str]: ...
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@overload
|
|
354
|
+
def from_env(key: str, /, *, error_message: str) -> Callable[[], str]: ...
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@overload
|
|
358
|
+
def from_env(
|
|
359
|
+
key: str | Sequence[str], /, *, default: str, error_message: str | None
|
|
360
|
+
) -> Callable[[], str]: ...
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@overload
|
|
364
|
+
def from_env(
|
|
365
|
+
key: str, /, *, default: None, error_message: str | None
|
|
366
|
+
) -> Callable[[], str | None]: ...
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@overload
|
|
370
|
+
def from_env(
|
|
371
|
+
key: str | Sequence[str], /, *, default: None
|
|
372
|
+
) -> Callable[[], str | None]: ...
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def from_env(
|
|
376
|
+
key: str | Sequence[str],
|
|
377
|
+
/,
|
|
378
|
+
*,
|
|
379
|
+
default: str | _NoDefaultType | None = _NoDefault,
|
|
380
|
+
error_message: str | None = None,
|
|
381
|
+
) -> Callable[[], str] | Callable[[], str | None]:
|
|
382
|
+
"""Create a factory method that gets a value from an environment variable.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
key: The environment variable to look up.
|
|
386
|
+
|
|
387
|
+
If a list of keys is provided, the first key found in the environment will
|
|
388
|
+
be used. If no key is found, the default value will be used if set,
|
|
389
|
+
otherwise an error will be raised.
|
|
390
|
+
default: The default value to return if the environment variable is not set.
|
|
391
|
+
error_message: The error message which will be raised if the key is not found
|
|
392
|
+
and no default value is provided.
|
|
393
|
+
|
|
394
|
+
This will be raised as a ValueError.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Factory method that will look up the value from the environment.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def get_from_env_fn() -> str | None:
|
|
401
|
+
"""Get a value from an environment variable.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
ValueError: If the environment variable is not set and no default is
|
|
405
|
+
provided.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
The value from the environment.
|
|
409
|
+
"""
|
|
410
|
+
if isinstance(key, (list, tuple)):
|
|
411
|
+
for k in key:
|
|
412
|
+
if k in os.environ:
|
|
413
|
+
return os.environ[k]
|
|
414
|
+
if isinstance(key, str) and key in os.environ:
|
|
415
|
+
return os.environ[key]
|
|
416
|
+
|
|
417
|
+
if isinstance(default, (str, type(None))):
|
|
418
|
+
return default
|
|
419
|
+
if error_message:
|
|
420
|
+
raise ValueError(error_message)
|
|
421
|
+
msg = (
|
|
422
|
+
f"Did not find {key}, please add an environment variable"
|
|
423
|
+
f" `{key}` which contains it, or pass"
|
|
424
|
+
f" `{key}` as a named parameter."
|
|
425
|
+
)
|
|
426
|
+
raise ValueError(msg)
|
|
427
|
+
|
|
428
|
+
return get_from_env_fn
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@overload
|
|
432
|
+
def secret_from_env(key: str | Sequence[str], /) -> Callable[[], SecretStr]: ...
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@overload
|
|
436
|
+
def secret_from_env(key: str, /, *, default: str) -> Callable[[], SecretStr]: ...
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@overload
|
|
440
|
+
def secret_from_env(
|
|
441
|
+
key: str | Sequence[str], /, *, default: None
|
|
442
|
+
) -> Callable[[], SecretStr | None]: ...
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@overload
|
|
446
|
+
def secret_from_env(key: str, /, *, error_message: str) -> Callable[[], SecretStr]: ...
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def secret_from_env(
|
|
450
|
+
key: str | Sequence[str],
|
|
451
|
+
/,
|
|
452
|
+
*,
|
|
453
|
+
default: str | _NoDefaultType | None = _NoDefault,
|
|
454
|
+
error_message: str | None = None,
|
|
455
|
+
) -> Callable[[], SecretStr | None] | Callable[[], SecretStr]:
|
|
456
|
+
"""Secret from env.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
key: The environment variable to look up.
|
|
460
|
+
default: The default value to return if the environment variable is not set.
|
|
461
|
+
error_message: The error message which will be raised if the key is not found
|
|
462
|
+
and no default value is provided.
|
|
463
|
+
|
|
464
|
+
This will be raised as a `ValueError`.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Factory method that will look up the secret from the environment.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
def get_secret_from_env() -> SecretStr | None:
|
|
471
|
+
"""Get a value from an environment variable.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
ValueError: If the environment variable is not set and no default is
|
|
475
|
+
provided.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
The secret from the environment.
|
|
479
|
+
"""
|
|
480
|
+
if isinstance(key, (list, tuple)):
|
|
481
|
+
for k in key:
|
|
482
|
+
if k in os.environ:
|
|
483
|
+
return SecretStr(os.environ[k])
|
|
484
|
+
if isinstance(key, str) and key in os.environ:
|
|
485
|
+
return SecretStr(os.environ[key])
|
|
486
|
+
if isinstance(default, str):
|
|
487
|
+
return SecretStr(default)
|
|
488
|
+
if default is None:
|
|
489
|
+
return None
|
|
490
|
+
if error_message:
|
|
491
|
+
raise ValueError(error_message)
|
|
492
|
+
msg = (
|
|
493
|
+
f"Did not find {key}, please add an environment variable"
|
|
494
|
+
f" `{key}` which contains it, or pass"
|
|
495
|
+
f" `{key}` as a named parameter."
|
|
496
|
+
)
|
|
497
|
+
raise ValueError(msg)
|
|
498
|
+
|
|
499
|
+
return get_secret_from_env
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
LC_AUTO_PREFIX = "lc_"
|
|
503
|
+
"""Coco auto-generated ID prefix for messages and content blocks."""
|
|
504
|
+
|
|
505
|
+
LC_ID_PREFIX = "lc_run-"
|
|
506
|
+
"""Internal tracing/callback system identifier.
|
|
507
|
+
|
|
508
|
+
Used for:
|
|
509
|
+
|
|
510
|
+
- Tracing. Every Coco operation (LLM call, chain execution, tool use, etc.)
|
|
511
|
+
gets a unique run_id (UUID)
|
|
512
|
+
- Enables tracking parent-child relationships between operations
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def ensure_id(id_val: str | None) -> str:
|
|
517
|
+
"""Ensure the ID is a valid string, generating a new UUID if not provided.
|
|
518
|
+
|
|
519
|
+
Auto-generated UUIDs are prefixed by `'lc_'` to indicate they are
|
|
520
|
+
Coco-generated IDs.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
id_val: Optional string ID value to validate.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
A string ID, either the validated provided value or a newly generated UUID4.
|
|
527
|
+
"""
|
|
528
|
+
return id_val or f"{LC_AUTO_PREFIX}{uuid4()}"
|
coco_core/utils/uuid.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""UUID utility functions.
|
|
2
|
+
|
|
3
|
+
This module exports a uuid7 function to generate monotonic, time-ordered UUIDs
|
|
4
|
+
for tracing and similar operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typing
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from uuid_utils.compat import uuid7 as _uuid_utils_uuid7
|
|
13
|
+
|
|
14
|
+
if typing.TYPE_CHECKING:
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
_NANOS_PER_SECOND: typing.Final = 1_000_000_000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _to_timestamp_and_nanos(nanoseconds: int) -> tuple[int, int]:
|
|
21
|
+
"""Split a nanosecond timestamp into seconds and remaining nanoseconds."""
|
|
22
|
+
seconds, nanos = divmod(nanoseconds, _NANOS_PER_SECOND)
|
|
23
|
+
return seconds, nanos
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def uuid7(nanoseconds: int | None = None) -> UUID:
|
|
27
|
+
"""Generate a UUID from a Unix timestamp in nanoseconds and random bits.
|
|
28
|
+
|
|
29
|
+
UUIDv7 objects feature monotonicity within a millisecond.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
nanoseconds: Optional ns timestamp. If not provided, uses current time.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A UUIDv7 object.
|
|
36
|
+
"""
|
|
37
|
+
# --- 48 --- -- 4 -- --- 12 --- -- 2 -- --- 30 --- - 32 -
|
|
38
|
+
# unix_ts_ms | version | counter_hi | variant | counter_lo | random
|
|
39
|
+
#
|
|
40
|
+
# 'counter = counter_hi | counter_lo' is a 42-bit counter constructed
|
|
41
|
+
# with Method 1 of RFC 9562, §6.2, and its MSB is set to 0.
|
|
42
|
+
#
|
|
43
|
+
# 'random' is a 32-bit random value regenerated for every new UUID.
|
|
44
|
+
#
|
|
45
|
+
# If multiple UUIDs are generated within the same millisecond, the LSB
|
|
46
|
+
# of 'counter' is incremented by 1. When overflowing, the timestamp is
|
|
47
|
+
# advanced and the counter is reset to a random 42-bit integer with MSB
|
|
48
|
+
# set to 0.
|
|
49
|
+
|
|
50
|
+
# For now, just delegate to the uuid_utils implementation
|
|
51
|
+
if nanoseconds is None:
|
|
52
|
+
return _uuid_utils_uuid7()
|
|
53
|
+
seconds, nanos = _to_timestamp_and_nanos(nanoseconds)
|
|
54
|
+
return _uuid_utils_uuid7(timestamp=seconds, nanos=nanos)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["uuid7"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Vector stores."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from coco_core._import_utils import import_attr
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from coco_core.vectorstores.base import VST, VectorStore, VectorStoreRetriever
|
|
9
|
+
from coco_core.vectorstores.in_memory import InMemoryVectorStore
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"VST",
|
|
13
|
+
"InMemoryVectorStore",
|
|
14
|
+
"VectorStore",
|
|
15
|
+
"VectorStoreRetriever",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_dynamic_imports = {
|
|
19
|
+
"VectorStore": "base",
|
|
20
|
+
"VST": "base",
|
|
21
|
+
"VectorStoreRetriever": "base",
|
|
22
|
+
"InMemoryVectorStore": "in_memory",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(attr_name: str) -> object:
|
|
27
|
+
"""Dynamically import and return an attribute from a submodule.
|
|
28
|
+
|
|
29
|
+
This function enables lazy loading of vectorstore classes from submodules, reducing
|
|
30
|
+
initial import time and circular dependency issues.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
attr_name: Name of the attribute to import.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The imported attribute object.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
AttributeError: If the attribute is not found in `_dynamic_imports`.
|
|
40
|
+
"""
|
|
41
|
+
module_name = _dynamic_imports.get(attr_name)
|
|
42
|
+
result = import_attr(attr_name, module_name, __spec__.parent)
|
|
43
|
+
globals()[attr_name] = result
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def __dir__() -> list[str]:
|
|
48
|
+
"""Return a list of available attributes for this module.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of attribute names that can be imported from this module.
|
|
52
|
+
"""
|
|
53
|
+
return list(__all__)
|