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,820 @@
|
|
|
1
|
+
"""Base classes for OpenAI embeddings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
|
|
8
|
+
from typing import Any, Literal, cast
|
|
9
|
+
|
|
10
|
+
import openai
|
|
11
|
+
import tiktoken
|
|
12
|
+
from coco_core.embeddings import Embeddings
|
|
13
|
+
from coco_core.runnables.config import run_in_executor
|
|
14
|
+
from coco_core.utils import from_env, get_pydantic_field_names, secret_from_env
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
16
|
+
from typing_extensions import Self
|
|
17
|
+
|
|
18
|
+
from coco_openai.chat_models._client_utils import _resolve_sync_and_async_api_keys
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
MAX_TOKENS_PER_REQUEST = 300000
|
|
23
|
+
"""API limit per request for embedding tokens."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _process_batched_chunked_embeddings(
|
|
27
|
+
num_texts: int,
|
|
28
|
+
tokens: list[list[int] | str],
|
|
29
|
+
batched_embeddings: list[list[float]],
|
|
30
|
+
indices: list[int],
|
|
31
|
+
skip_empty: bool,
|
|
32
|
+
) -> list[list[float] | None]:
|
|
33
|
+
# for each text, this is the list of embeddings (list of list of floats)
|
|
34
|
+
# corresponding to the chunks of the text
|
|
35
|
+
results: list[list[list[float]]] = [[] for _ in range(num_texts)]
|
|
36
|
+
|
|
37
|
+
# for each text, this is the token length of each chunk
|
|
38
|
+
# for transformers tokenization, this is the string length
|
|
39
|
+
# for tiktoken, this is the number of tokens
|
|
40
|
+
num_tokens_in_batch: list[list[int]] = [[] for _ in range(num_texts)]
|
|
41
|
+
|
|
42
|
+
for i in range(len(indices)):
|
|
43
|
+
if skip_empty and len(batched_embeddings[i]) == 1:
|
|
44
|
+
continue
|
|
45
|
+
results[indices[i]].append(batched_embeddings[i])
|
|
46
|
+
num_tokens_in_batch[indices[i]].append(len(tokens[i]))
|
|
47
|
+
|
|
48
|
+
# for each text, this is the final embedding
|
|
49
|
+
embeddings: list[list[float] | None] = []
|
|
50
|
+
for i in range(num_texts):
|
|
51
|
+
# an embedding for each chunk
|
|
52
|
+
_result: list[list[float]] = results[i]
|
|
53
|
+
|
|
54
|
+
if len(_result) == 0:
|
|
55
|
+
# this will be populated with the embedding of an empty string
|
|
56
|
+
# in the sync or async code calling this
|
|
57
|
+
embeddings.append(None)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
if len(_result) == 1:
|
|
61
|
+
# if only one embedding was produced, use it
|
|
62
|
+
embeddings.append(_result[0])
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# else we need to weighted average
|
|
66
|
+
# should be same as
|
|
67
|
+
# average = np.average(_result, axis=0, weights=num_tokens_in_batch[i])
|
|
68
|
+
total_weight = sum(num_tokens_in_batch[i])
|
|
69
|
+
average = [
|
|
70
|
+
sum(
|
|
71
|
+
val * weight
|
|
72
|
+
for val, weight in zip(embedding, num_tokens_in_batch[i], strict=False)
|
|
73
|
+
)
|
|
74
|
+
/ total_weight
|
|
75
|
+
for embedding in zip(*_result, strict=False)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# should be same as
|
|
79
|
+
# embeddings.append((average / np.linalg.norm(average)).tolist())
|
|
80
|
+
magnitude = sum(val**2 for val in average) ** 0.5
|
|
81
|
+
embeddings.append([val / magnitude for val in average])
|
|
82
|
+
|
|
83
|
+
return embeddings
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class OpenAIEmbeddings(BaseModel, Embeddings):
|
|
87
|
+
"""OpenAI embedding model integration.
|
|
88
|
+
|
|
89
|
+
Setup:
|
|
90
|
+
Install `coco_openai` and set environment variable `OPENAI_API_KEY`.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -U coco_openai
|
|
94
|
+
export OPENAI_API_KEY="your-api-key"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Key init args — embedding params:
|
|
98
|
+
model:
|
|
99
|
+
Name of OpenAI model to use.
|
|
100
|
+
dimensions:
|
|
101
|
+
The number of dimensions the resulting output embeddings should have.
|
|
102
|
+
Only supported in `'text-embedding-3'` and later models.
|
|
103
|
+
|
|
104
|
+
Key init args — client params:
|
|
105
|
+
api_key:
|
|
106
|
+
OpenAI API key.
|
|
107
|
+
organization:
|
|
108
|
+
OpenAI organization ID. If not passed in will be read
|
|
109
|
+
from env var `OPENAI_ORG_ID`.
|
|
110
|
+
max_retries:
|
|
111
|
+
Maximum number of retries to make when generating.
|
|
112
|
+
request_timeout:
|
|
113
|
+
Timeout for requests to OpenAI completion API
|
|
114
|
+
|
|
115
|
+
See full list of supported init args and their descriptions in the params section.
|
|
116
|
+
|
|
117
|
+
Instantiate:
|
|
118
|
+
```python
|
|
119
|
+
from coco_openai import OpenAIEmbeddings
|
|
120
|
+
|
|
121
|
+
embed = OpenAIEmbeddings(
|
|
122
|
+
model="text-embedding-3-large"
|
|
123
|
+
# With the `text-embedding-3` class
|
|
124
|
+
# of models, you can specify the size
|
|
125
|
+
# of the embeddings you want returned.
|
|
126
|
+
# dimensions=1024
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Embed single text:
|
|
131
|
+
```python
|
|
132
|
+
input_text = "The meaning of life is 42"
|
|
133
|
+
vector = embeddings.embed_query("hello")
|
|
134
|
+
print(vector[:3])
|
|
135
|
+
```
|
|
136
|
+
```python
|
|
137
|
+
[-0.024603435769677162, -0.007543657906353474, 0.0039630369283258915]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Embed multiple texts:
|
|
141
|
+
```python
|
|
142
|
+
vectors = embeddings.embed_documents(["hello", "goodbye"])
|
|
143
|
+
# Showing only the first 3 coordinates
|
|
144
|
+
print(len(vectors))
|
|
145
|
+
print(vectors[0][:3])
|
|
146
|
+
```
|
|
147
|
+
```python
|
|
148
|
+
2
|
|
149
|
+
[-0.024603435769677162, -0.007543657906353474, 0.0039630369283258915]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Async:
|
|
153
|
+
```python
|
|
154
|
+
await embed.aembed_query(input_text)
|
|
155
|
+
print(vector[:3])
|
|
156
|
+
|
|
157
|
+
# multiple:
|
|
158
|
+
# await embed.aembed_documents(input_texts)
|
|
159
|
+
```
|
|
160
|
+
```python
|
|
161
|
+
[-0.009100092574954033, 0.005071679595857859, -0.0029193938244134188]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
!!! note "OpenAI-compatible APIs (e.g. OpenRouter, Ollama, vLLM)"
|
|
165
|
+
|
|
166
|
+
When using a non-OpenAI provider, set
|
|
167
|
+
`check_embedding_ctx_length=False` to send raw text instead of tokens
|
|
168
|
+
(which many providers don't support), and optionally set
|
|
169
|
+
`encoding_format` to `'float'` to avoid base64 encoding issues:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from coco_openai import OpenAIEmbeddings
|
|
173
|
+
|
|
174
|
+
embeddings = OpenAIEmbeddings(
|
|
175
|
+
model="...",
|
|
176
|
+
base_url="...",
|
|
177
|
+
check_embedding_ctx_length=False,
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
client: Any = Field(default=None, exclude=True)
|
|
184
|
+
|
|
185
|
+
async_client: Any = Field(default=None, exclude=True)
|
|
186
|
+
|
|
187
|
+
model: str = "text-embedding-ada-002"
|
|
188
|
+
|
|
189
|
+
dimensions: int | None = None
|
|
190
|
+
"""The number of dimensions the resulting output embeddings should have.
|
|
191
|
+
|
|
192
|
+
Only supported in `'text-embedding-3'` and later models.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
# to support Azure OpenAI Service custom deployment names
|
|
196
|
+
deployment: str | None = model
|
|
197
|
+
|
|
198
|
+
# TODO: Move to AzureOpenAIEmbeddings.
|
|
199
|
+
openai_api_version: str | None = Field(
|
|
200
|
+
default_factory=from_env("OPENAI_API_VERSION", default=None),
|
|
201
|
+
alias="api_version",
|
|
202
|
+
)
|
|
203
|
+
"""Version of the OpenAI API to use.
|
|
204
|
+
|
|
205
|
+
Automatically inferred from env var `OPENAI_API_VERSION` if not provided.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
# to support Azure OpenAI Service custom endpoints
|
|
209
|
+
openai_api_base: str | None = Field(
|
|
210
|
+
alias="base_url", default_factory=from_env("OPENAI_API_BASE", default=None)
|
|
211
|
+
)
|
|
212
|
+
"""Base URL path for API requests, leave blank if not using a proxy or
|
|
213
|
+
service emulator.
|
|
214
|
+
|
|
215
|
+
Resolution order (first match wins):
|
|
216
|
+
|
|
217
|
+
1. Explicit `base_url` (or `openai_api_base`) kwarg.
|
|
218
|
+
2. Env var `OPENAI_API_BASE` (read by Coco at init).
|
|
219
|
+
3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
# to support Azure OpenAI Service custom endpoints
|
|
223
|
+
openai_api_type: str | None = Field(
|
|
224
|
+
default_factory=from_env("OPENAI_API_TYPE", default=None)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# to support explicit proxy for OpenAI
|
|
228
|
+
openai_proxy: str | None = Field(
|
|
229
|
+
default_factory=from_env("OPENAI_PROXY", default=None)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
embedding_ctx_length: int = 8191
|
|
233
|
+
"""The maximum number of tokens to embed at once."""
|
|
234
|
+
|
|
235
|
+
openai_api_key: (
|
|
236
|
+
SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]
|
|
237
|
+
) = Field(
|
|
238
|
+
alias="api_key", default_factory=secret_from_env("OPENAI_API_KEY", default=None)
|
|
239
|
+
)
|
|
240
|
+
"""API key to use for API calls.
|
|
241
|
+
|
|
242
|
+
Automatically inferred from env var `OPENAI_API_KEY` if not provided.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
openai_organization: str | None = Field(
|
|
246
|
+
alias="organization",
|
|
247
|
+
default_factory=from_env(
|
|
248
|
+
["OPENAI_ORG_ID", "OPENAI_ORGANIZATION"], default=None
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
"""OpenAI organization ID to use for API calls.
|
|
252
|
+
|
|
253
|
+
Automatically inferred from env var `OPENAI_ORG_ID` if not provided.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
allowed_special: Literal["all"] | set[str] | None = None
|
|
257
|
+
|
|
258
|
+
disallowed_special: Literal["all"] | set[str] | Sequence[str] | None = None
|
|
259
|
+
|
|
260
|
+
chunk_size: int = 1000
|
|
261
|
+
"""Maximum number of texts to embed in each batch"""
|
|
262
|
+
|
|
263
|
+
max_retries: int = 2
|
|
264
|
+
"""Maximum number of retries to make when generating."""
|
|
265
|
+
|
|
266
|
+
request_timeout: float | tuple[float, float] | Any | None = Field(
|
|
267
|
+
default=None, alias="timeout"
|
|
268
|
+
)
|
|
269
|
+
"""Timeout for requests to OpenAI completion API.
|
|
270
|
+
|
|
271
|
+
Can be float, `httpx.Timeout` or `None`.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
headers: Any = None
|
|
275
|
+
|
|
276
|
+
tiktoken_enabled: bool = True
|
|
277
|
+
"""Set this to False to use HuggingFace `transformers` tokenization.
|
|
278
|
+
|
|
279
|
+
For non-OpenAI providers (OpenRouter, Ollama, vLLM, etc.), consider setting
|
|
280
|
+
`check_embedding_ctx_length=False` instead, as it bypasses tokenization
|
|
281
|
+
entirely.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
tiktoken_model_name: str | None = None
|
|
285
|
+
"""The model name to pass to tiktoken when using this class.
|
|
286
|
+
|
|
287
|
+
Tiktoken is used to count the number of tokens in documents to constrain
|
|
288
|
+
them to be under a certain limit.
|
|
289
|
+
|
|
290
|
+
By default, when set to `None`, this will be the same as the embedding model
|
|
291
|
+
name. However, there are some cases where you may want to use this
|
|
292
|
+
`Embedding` class with a model name not supported by tiktoken. This can
|
|
293
|
+
include when using Azure embeddings or when using one of the many model
|
|
294
|
+
providers that expose an OpenAI-like API but with different models. In those
|
|
295
|
+
cases, in order to avoid erroring when tiktoken is called, you can specify a
|
|
296
|
+
model name to use here.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
show_progress_bar: bool = False
|
|
300
|
+
"""Whether to show a progress bar when embedding."""
|
|
301
|
+
|
|
302
|
+
model_kwargs: dict[str, Any] = Field(default_factory=dict)
|
|
303
|
+
"""Holds any model parameters valid for `create` call not explicitly specified."""
|
|
304
|
+
|
|
305
|
+
skip_empty: bool = False
|
|
306
|
+
"""Whether to skip empty strings when embedding or raise an error."""
|
|
307
|
+
|
|
308
|
+
default_headers: Mapping[str, str] | None = None
|
|
309
|
+
|
|
310
|
+
default_query: Mapping[str, object] | None = None
|
|
311
|
+
|
|
312
|
+
# Configure a custom httpx client. See the
|
|
313
|
+
# [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
|
|
314
|
+
|
|
315
|
+
retry_min_seconds: int = 4
|
|
316
|
+
"""Min number of seconds to wait between retries"""
|
|
317
|
+
|
|
318
|
+
retry_max_seconds: int = 20
|
|
319
|
+
"""Max number of seconds to wait between retries"""
|
|
320
|
+
|
|
321
|
+
http_client: Any | None = None
|
|
322
|
+
"""Optional `httpx.Client`.
|
|
323
|
+
|
|
324
|
+
Only used for sync invocations. Must specify `http_async_client` as well if
|
|
325
|
+
you'd like a custom client for async invocations.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
http_async_client: Any | None = None
|
|
329
|
+
"""Optional `httpx.AsyncClient`.
|
|
330
|
+
|
|
331
|
+
Only used for async invocations. Must specify `http_client` as well if you'd
|
|
332
|
+
like a custom client for sync invocations.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
check_embedding_ctx_length: bool = True
|
|
336
|
+
"""Whether to check the token length of inputs and automatically split inputs
|
|
337
|
+
longer than `embedding_ctx_length`.
|
|
338
|
+
|
|
339
|
+
Set to `False` to send raw text strings directly to the API instead of
|
|
340
|
+
tokenizing. Useful for many non-OpenAI providers (e.g. OpenRouter, Ollama,
|
|
341
|
+
vLLM).
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
model_config = ConfigDict(
|
|
345
|
+
extra="forbid", populate_by_name=True, protected_namespaces=()
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@model_validator(mode="before")
|
|
349
|
+
@classmethod
|
|
350
|
+
def build_extra(cls, values: dict[str, Any]) -> Any:
|
|
351
|
+
"""Build extra kwargs from additional params that were passed in."""
|
|
352
|
+
all_required_field_names = get_pydantic_field_names(cls)
|
|
353
|
+
extra = values.get("model_kwargs", {})
|
|
354
|
+
for field_name in list(values):
|
|
355
|
+
if field_name in extra:
|
|
356
|
+
msg = f"Found {field_name} supplied twice."
|
|
357
|
+
raise ValueError(msg)
|
|
358
|
+
if field_name not in all_required_field_names:
|
|
359
|
+
warnings.warn(
|
|
360
|
+
f"""WARNING! {field_name} is not default parameter.
|
|
361
|
+
{field_name} was transferred to model_kwargs.
|
|
362
|
+
Please confirm that {field_name} is what you intended."""
|
|
363
|
+
)
|
|
364
|
+
extra[field_name] = values.pop(field_name)
|
|
365
|
+
|
|
366
|
+
invalid_model_kwargs = all_required_field_names.intersection(extra.keys())
|
|
367
|
+
if invalid_model_kwargs:
|
|
368
|
+
msg = (
|
|
369
|
+
f"Parameters {invalid_model_kwargs} should be specified explicitly. "
|
|
370
|
+
f"Instead they were passed in as part of `model_kwargs` parameter."
|
|
371
|
+
)
|
|
372
|
+
raise ValueError(msg)
|
|
373
|
+
|
|
374
|
+
values["model_kwargs"] = extra
|
|
375
|
+
return values
|
|
376
|
+
|
|
377
|
+
@model_validator(mode="after")
|
|
378
|
+
def validate_environment(self) -> Self:
|
|
379
|
+
"""Validate that api key and python package exists in environment."""
|
|
380
|
+
if self.openai_api_type in ("azure", "azure_ad", "azuread"):
|
|
381
|
+
msg = (
|
|
382
|
+
"If you are using Azure, please use the `AzureOpenAIEmbeddings` class."
|
|
383
|
+
)
|
|
384
|
+
raise ValueError(msg)
|
|
385
|
+
|
|
386
|
+
# Resolve API key from SecretStr or Callable
|
|
387
|
+
sync_api_key_value: str | Callable[[], str] | None = None
|
|
388
|
+
async_api_key_value: str | Callable[[], Awaitable[str]] | None = None
|
|
389
|
+
|
|
390
|
+
if self.openai_api_key is not None:
|
|
391
|
+
# Because OpenAI and AsyncOpenAI clients support either sync or async
|
|
392
|
+
# callables for the API key, we need to resolve separate values here.
|
|
393
|
+
sync_api_key_value, async_api_key_value = _resolve_sync_and_async_api_keys(
|
|
394
|
+
self.openai_api_key
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
client_params: dict = {
|
|
398
|
+
"organization": self.openai_organization,
|
|
399
|
+
"base_url": self.openai_api_base,
|
|
400
|
+
"timeout": self.request_timeout,
|
|
401
|
+
"max_retries": self.max_retries,
|
|
402
|
+
"default_headers": self.default_headers,
|
|
403
|
+
"default_query": self.default_query,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if self.openai_proxy and (self.http_client or self.http_async_client):
|
|
407
|
+
openai_proxy = self.openai_proxy
|
|
408
|
+
http_client = self.http_client
|
|
409
|
+
http_async_client = self.http_async_client
|
|
410
|
+
msg = (
|
|
411
|
+
"Cannot specify 'openai_proxy' if one of "
|
|
412
|
+
"'http_client'/'http_async_client' is already specified. Received:\n"
|
|
413
|
+
f"{openai_proxy=}\n{http_client=}\n{http_async_client=}"
|
|
414
|
+
)
|
|
415
|
+
raise ValueError(msg)
|
|
416
|
+
if not self.client:
|
|
417
|
+
if sync_api_key_value is None:
|
|
418
|
+
# No valid sync API key, leave client as None and raise informative
|
|
419
|
+
# error on invocation.
|
|
420
|
+
self.client = None
|
|
421
|
+
else:
|
|
422
|
+
if self.openai_proxy and not self.http_client:
|
|
423
|
+
try:
|
|
424
|
+
from coco_openai._compat import httpx
|
|
425
|
+
except ImportError as e:
|
|
426
|
+
msg = (
|
|
427
|
+
"Could not import the httpx package used by the "
|
|
428
|
+
"OpenAI SDK. It is normally installed alongside "
|
|
429
|
+
"`openai`; reinstall with `pip install openai`."
|
|
430
|
+
)
|
|
431
|
+
raise ImportError(msg) from e
|
|
432
|
+
self.http_client = httpx.Client(proxy=self.openai_proxy)
|
|
433
|
+
sync_specific = {
|
|
434
|
+
"http_client": self.http_client,
|
|
435
|
+
"api_key": sync_api_key_value,
|
|
436
|
+
}
|
|
437
|
+
self.client = openai.OpenAI(**client_params, **sync_specific).embeddings # type: ignore[arg-type]
|
|
438
|
+
if not self.async_client:
|
|
439
|
+
if self.openai_proxy and not self.http_async_client:
|
|
440
|
+
try:
|
|
441
|
+
from coco_openai._compat import httpx
|
|
442
|
+
except ImportError as e:
|
|
443
|
+
msg = (
|
|
444
|
+
"Could not import the httpx package used by the "
|
|
445
|
+
"OpenAI SDK. It is normally installed alongside "
|
|
446
|
+
"`openai`; reinstall with `pip install openai`."
|
|
447
|
+
)
|
|
448
|
+
raise ImportError(msg) from e
|
|
449
|
+
self.http_async_client = httpx.AsyncClient(proxy=self.openai_proxy)
|
|
450
|
+
async_specific = {
|
|
451
|
+
"http_client": self.http_async_client,
|
|
452
|
+
"api_key": async_api_key_value,
|
|
453
|
+
}
|
|
454
|
+
self.async_client = openai.AsyncOpenAI(
|
|
455
|
+
**client_params,
|
|
456
|
+
**async_specific, # type: ignore[arg-type]
|
|
457
|
+
).embeddings
|
|
458
|
+
return self
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def _invocation_params(self) -> dict[str, Any]:
|
|
462
|
+
params: dict = {"model": self.model, **self.model_kwargs}
|
|
463
|
+
if self.dimensions is not None:
|
|
464
|
+
params["dimensions"] = self.dimensions
|
|
465
|
+
return params
|
|
466
|
+
|
|
467
|
+
def _ensure_sync_client_available(self) -> None:
|
|
468
|
+
"""Check that sync client is available, raise error if not."""
|
|
469
|
+
if self.client is None:
|
|
470
|
+
msg = (
|
|
471
|
+
"Sync client is not available. This happens when an async callable "
|
|
472
|
+
"was provided for the API key. Use async methods (ainvoke, astream) "
|
|
473
|
+
"instead, or provide a string or sync callable for the API key."
|
|
474
|
+
)
|
|
475
|
+
raise ValueError(msg)
|
|
476
|
+
|
|
477
|
+
def _tokenize(
|
|
478
|
+
self, texts: list[str], chunk_size: int
|
|
479
|
+
) -> tuple[Iterable[int], list[list[int] | str], list[int], list[int]]:
|
|
480
|
+
"""Tokenize and batch input texts.
|
|
481
|
+
|
|
482
|
+
Splits texts based on `embedding_ctx_length` and groups them into batches
|
|
483
|
+
of size `chunk_size`.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
texts: The list of texts to tokenize.
|
|
487
|
+
chunk_size: The maximum number of texts to include in a single batch.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
A tuple containing:
|
|
491
|
+
1. An iterable of starting indices in the token list for each batch.
|
|
492
|
+
2. A list of tokenized texts (token arrays for tiktoken, strings for
|
|
493
|
+
HuggingFace).
|
|
494
|
+
3. An iterable mapping each token array to the index of the original
|
|
495
|
+
text. Same length as the token list.
|
|
496
|
+
4. A list of token counts for each tokenized text.
|
|
497
|
+
"""
|
|
498
|
+
tokens: list[list[int] | str] = []
|
|
499
|
+
indices: list[int] = []
|
|
500
|
+
token_counts: list[int] = []
|
|
501
|
+
model_name = self.tiktoken_model_name or self.model
|
|
502
|
+
|
|
503
|
+
# If tiktoken flag set to False
|
|
504
|
+
if not self.tiktoken_enabled:
|
|
505
|
+
try:
|
|
506
|
+
from transformers import AutoTokenizer
|
|
507
|
+
except ImportError:
|
|
508
|
+
msg = (
|
|
509
|
+
"Could not import transformers python package. "
|
|
510
|
+
"This is needed for OpenAIEmbeddings to work without "
|
|
511
|
+
"`tiktoken`. Please install it with `pip install transformers`. "
|
|
512
|
+
)
|
|
513
|
+
raise ValueError(msg)
|
|
514
|
+
|
|
515
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
516
|
+
pretrained_model_name_or_path=model_name
|
|
517
|
+
)
|
|
518
|
+
for i, text in enumerate(texts):
|
|
519
|
+
# Tokenize the text using HuggingFace transformers
|
|
520
|
+
tokenized: list[int] = tokenizer.encode(text, add_special_tokens=False)
|
|
521
|
+
|
|
522
|
+
# Split tokens into chunks respecting the embedding_ctx_length
|
|
523
|
+
for j in range(0, len(tokenized), self.embedding_ctx_length):
|
|
524
|
+
token_chunk: list[int] = tokenized[
|
|
525
|
+
j : j + self.embedding_ctx_length
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
# Convert token IDs back to a string
|
|
529
|
+
chunk_text: str = tokenizer.decode(token_chunk)
|
|
530
|
+
tokens.append(chunk_text)
|
|
531
|
+
indices.append(i)
|
|
532
|
+
token_counts.append(len(token_chunk))
|
|
533
|
+
else:
|
|
534
|
+
try:
|
|
535
|
+
encoding = tiktoken.encoding_for_model(model_name)
|
|
536
|
+
except KeyError:
|
|
537
|
+
encoding = tiktoken.get_encoding("cl100k_base")
|
|
538
|
+
encoder_kwargs: dict[str, Any] = {
|
|
539
|
+
k: v
|
|
540
|
+
for k, v in {
|
|
541
|
+
"allowed_special": self.allowed_special,
|
|
542
|
+
"disallowed_special": self.disallowed_special,
|
|
543
|
+
}.items()
|
|
544
|
+
if v is not None
|
|
545
|
+
}
|
|
546
|
+
for i, text in enumerate(texts):
|
|
547
|
+
if self.model.endswith("001"):
|
|
548
|
+
# See: https://github.com/openai/openai-python/
|
|
549
|
+
# issues/418#issuecomment-1525939500
|
|
550
|
+
# replace newlines, which can negatively affect performance.
|
|
551
|
+
text = text.replace("\n", " ")
|
|
552
|
+
|
|
553
|
+
if encoder_kwargs:
|
|
554
|
+
token = encoding.encode(text, **encoder_kwargs)
|
|
555
|
+
else:
|
|
556
|
+
token = encoding.encode_ordinary(text)
|
|
557
|
+
|
|
558
|
+
# Split tokens into chunks respecting the embedding_ctx_length
|
|
559
|
+
for j in range(0, len(token), self.embedding_ctx_length):
|
|
560
|
+
tokens.append(token[j : j + self.embedding_ctx_length])
|
|
561
|
+
indices.append(i)
|
|
562
|
+
token_counts.append(len(token[j : j + self.embedding_ctx_length]))
|
|
563
|
+
|
|
564
|
+
if self.show_progress_bar:
|
|
565
|
+
try:
|
|
566
|
+
from tqdm.auto import tqdm
|
|
567
|
+
|
|
568
|
+
_iter: Iterable = tqdm(range(0, len(tokens), chunk_size))
|
|
569
|
+
except ImportError:
|
|
570
|
+
_iter = range(0, len(tokens), chunk_size)
|
|
571
|
+
else:
|
|
572
|
+
_iter = range(0, len(tokens), chunk_size)
|
|
573
|
+
return _iter, tokens, indices, token_counts
|
|
574
|
+
|
|
575
|
+
# please refer to
|
|
576
|
+
# https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
|
|
577
|
+
def _get_len_safe_embeddings(
|
|
578
|
+
self,
|
|
579
|
+
texts: list[str],
|
|
580
|
+
*,
|
|
581
|
+
engine: str,
|
|
582
|
+
chunk_size: int | None = None,
|
|
583
|
+
**kwargs: Any,
|
|
584
|
+
) -> list[list[float]]:
|
|
585
|
+
"""Generate length-safe embeddings for a list of texts.
|
|
586
|
+
|
|
587
|
+
This method handles tokenization and embedding generation, respecting the
|
|
588
|
+
`embedding_ctx_length` and `chunk_size`. Supports both `tiktoken` and
|
|
589
|
+
HuggingFace `transformers` based on the `tiktoken_enabled` flag.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
texts: The list of texts to embed.
|
|
593
|
+
engine: The engine or model to use for embeddings.
|
|
594
|
+
chunk_size: The size of chunks for processing embeddings.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
A list of embeddings for each input text.
|
|
598
|
+
"""
|
|
599
|
+
_chunk_size = chunk_size or self.chunk_size
|
|
600
|
+
client_kwargs = {**self._invocation_params, **kwargs}
|
|
601
|
+
_iter, tokens, indices, token_counts = self._tokenize(texts, _chunk_size)
|
|
602
|
+
batched_embeddings: list[list[float]] = []
|
|
603
|
+
|
|
604
|
+
# Process in batches respecting the token limit
|
|
605
|
+
i = 0
|
|
606
|
+
while i < len(tokens):
|
|
607
|
+
# Determine how many chunks we can include in this batch
|
|
608
|
+
batch_token_count = 0
|
|
609
|
+
batch_end = i
|
|
610
|
+
|
|
611
|
+
for j in range(i, min(i + _chunk_size, len(tokens))):
|
|
612
|
+
chunk_tokens = token_counts[j]
|
|
613
|
+
# Check if adding this chunk would exceed the limit
|
|
614
|
+
if batch_token_count + chunk_tokens > MAX_TOKENS_PER_REQUEST:
|
|
615
|
+
if batch_end == i:
|
|
616
|
+
# Single chunk exceeds limit - handle it anyway
|
|
617
|
+
batch_end = j + 1
|
|
618
|
+
break
|
|
619
|
+
batch_token_count += chunk_tokens
|
|
620
|
+
batch_end = j + 1
|
|
621
|
+
|
|
622
|
+
# Make API call with this batch
|
|
623
|
+
batch_tokens = tokens[i:batch_end]
|
|
624
|
+
response = self.client.create(input=batch_tokens, **client_kwargs)
|
|
625
|
+
if not isinstance(response, dict):
|
|
626
|
+
response = response.model_dump()
|
|
627
|
+
batched_embeddings.extend(r["embedding"] for r in response["data"])
|
|
628
|
+
|
|
629
|
+
i = batch_end
|
|
630
|
+
|
|
631
|
+
embeddings = _process_batched_chunked_embeddings(
|
|
632
|
+
len(texts), tokens, batched_embeddings, indices, self.skip_empty
|
|
633
|
+
)
|
|
634
|
+
_cached_empty_embedding: list[float] | None = None
|
|
635
|
+
|
|
636
|
+
def empty_embedding() -> list[float]:
|
|
637
|
+
nonlocal _cached_empty_embedding
|
|
638
|
+
if _cached_empty_embedding is None:
|
|
639
|
+
average_embedded = self.client.create(input="", **client_kwargs)
|
|
640
|
+
if not isinstance(average_embedded, dict):
|
|
641
|
+
average_embedded = average_embedded.model_dump()
|
|
642
|
+
_cached_empty_embedding = average_embedded["data"][0]["embedding"]
|
|
643
|
+
return _cached_empty_embedding
|
|
644
|
+
|
|
645
|
+
return [e if e is not None else empty_embedding() for e in embeddings]
|
|
646
|
+
|
|
647
|
+
# please refer to
|
|
648
|
+
# https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.ipynb
|
|
649
|
+
async def _aget_len_safe_embeddings(
|
|
650
|
+
self,
|
|
651
|
+
texts: list[str],
|
|
652
|
+
*,
|
|
653
|
+
engine: str,
|
|
654
|
+
chunk_size: int | None = None,
|
|
655
|
+
**kwargs: Any,
|
|
656
|
+
) -> list[list[float]]:
|
|
657
|
+
"""Asynchronously generate length-safe embeddings for a list of texts.
|
|
658
|
+
|
|
659
|
+
This method handles tokenization and embedding generation, respecting the
|
|
660
|
+
`embedding_ctx_length` and `chunk_size`. Supports both `tiktoken` and
|
|
661
|
+
HuggingFace `transformers` based on the `tiktoken_enabled` flag.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
texts: The list of texts to embed.
|
|
665
|
+
engine: The engine or model to use for embeddings.
|
|
666
|
+
chunk_size: The size of chunks for processing embeddings.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
A list of embeddings for each input text.
|
|
670
|
+
"""
|
|
671
|
+
_chunk_size = chunk_size or self.chunk_size
|
|
672
|
+
client_kwargs = {**self._invocation_params, **kwargs}
|
|
673
|
+
_iter, tokens, indices, token_counts = await run_in_executor(
|
|
674
|
+
None, self._tokenize, texts, _chunk_size
|
|
675
|
+
)
|
|
676
|
+
batched_embeddings: list[list[float]] = []
|
|
677
|
+
|
|
678
|
+
# Process in batches respecting the token limit
|
|
679
|
+
i = 0
|
|
680
|
+
while i < len(tokens):
|
|
681
|
+
# Determine how many chunks we can include in this batch
|
|
682
|
+
batch_token_count = 0
|
|
683
|
+
batch_end = i
|
|
684
|
+
|
|
685
|
+
for j in range(i, min(i + _chunk_size, len(tokens))):
|
|
686
|
+
chunk_tokens = token_counts[j]
|
|
687
|
+
# Check if adding this chunk would exceed the limit
|
|
688
|
+
if batch_token_count + chunk_tokens > MAX_TOKENS_PER_REQUEST:
|
|
689
|
+
if batch_end == i:
|
|
690
|
+
# Single chunk exceeds limit - handle it anyway
|
|
691
|
+
batch_end = j + 1
|
|
692
|
+
break
|
|
693
|
+
batch_token_count += chunk_tokens
|
|
694
|
+
batch_end = j + 1
|
|
695
|
+
|
|
696
|
+
# Make API call with this batch
|
|
697
|
+
batch_tokens = tokens[i:batch_end]
|
|
698
|
+
response = await self.async_client.create(
|
|
699
|
+
input=batch_tokens, **client_kwargs
|
|
700
|
+
)
|
|
701
|
+
if not isinstance(response, dict):
|
|
702
|
+
response = response.model_dump()
|
|
703
|
+
batched_embeddings.extend(r["embedding"] for r in response["data"])
|
|
704
|
+
|
|
705
|
+
i = batch_end
|
|
706
|
+
|
|
707
|
+
embeddings = _process_batched_chunked_embeddings(
|
|
708
|
+
len(texts), tokens, batched_embeddings, indices, self.skip_empty
|
|
709
|
+
)
|
|
710
|
+
_cached_empty_embedding: list[float] | None = None
|
|
711
|
+
|
|
712
|
+
async def empty_embedding() -> list[float]:
|
|
713
|
+
nonlocal _cached_empty_embedding
|
|
714
|
+
if _cached_empty_embedding is None:
|
|
715
|
+
average_embedded = await self.async_client.create(
|
|
716
|
+
input="", **client_kwargs
|
|
717
|
+
)
|
|
718
|
+
if not isinstance(average_embedded, dict):
|
|
719
|
+
average_embedded = average_embedded.model_dump()
|
|
720
|
+
_cached_empty_embedding = average_embedded["data"][0]["embedding"]
|
|
721
|
+
return _cached_empty_embedding
|
|
722
|
+
|
|
723
|
+
return [e if e is not None else await empty_embedding() for e in embeddings]
|
|
724
|
+
|
|
725
|
+
def embed_documents(
|
|
726
|
+
self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
|
|
727
|
+
) -> list[list[float]]:
|
|
728
|
+
"""Call OpenAI's embedding endpoint to embed search docs.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
texts: The list of texts to embed.
|
|
732
|
+
chunk_size: The chunk size of embeddings.
|
|
733
|
+
|
|
734
|
+
If `None`, will use the chunk size specified by the class.
|
|
735
|
+
kwargs: Additional keyword arguments to pass to the embedding API.
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
List of embeddings, one for each text.
|
|
739
|
+
"""
|
|
740
|
+
self._ensure_sync_client_available()
|
|
741
|
+
chunk_size_ = chunk_size or self.chunk_size
|
|
742
|
+
client_kwargs = {**self._invocation_params, **kwargs}
|
|
743
|
+
if not self.check_embedding_ctx_length:
|
|
744
|
+
embeddings: list[list[float]] = []
|
|
745
|
+
for i in range(0, len(texts), chunk_size_):
|
|
746
|
+
response = self.client.create(
|
|
747
|
+
input=texts[i : i + chunk_size_], **client_kwargs
|
|
748
|
+
)
|
|
749
|
+
if not isinstance(response, dict):
|
|
750
|
+
response = response.model_dump()
|
|
751
|
+
embeddings.extend(r["embedding"] for r in response["data"])
|
|
752
|
+
return embeddings
|
|
753
|
+
|
|
754
|
+
# Unconditionally call _get_len_safe_embeddings to handle length safety.
|
|
755
|
+
# This could be optimized to avoid double work when all texts are short enough.
|
|
756
|
+
engine = cast(str, self.deployment)
|
|
757
|
+
return self._get_len_safe_embeddings(
|
|
758
|
+
texts, engine=engine, chunk_size=chunk_size, **kwargs
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
async def aembed_documents(
|
|
762
|
+
self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
|
|
763
|
+
) -> list[list[float]]:
|
|
764
|
+
"""Asynchronously call OpenAI's embedding endpoint to embed search docs.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
texts: The list of texts to embed.
|
|
768
|
+
chunk_size: The chunk size of embeddings.
|
|
769
|
+
|
|
770
|
+
If `None`, will use the chunk size specified by the class.
|
|
771
|
+
kwargs: Additional keyword arguments to pass to the embedding API.
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
List of embeddings, one for each text.
|
|
775
|
+
"""
|
|
776
|
+
chunk_size_ = chunk_size or self.chunk_size
|
|
777
|
+
client_kwargs = {**self._invocation_params, **kwargs}
|
|
778
|
+
if not self.check_embedding_ctx_length:
|
|
779
|
+
embeddings: list[list[float]] = []
|
|
780
|
+
for i in range(0, len(texts), chunk_size_):
|
|
781
|
+
response = await self.async_client.create(
|
|
782
|
+
input=texts[i : i + chunk_size_], **client_kwargs
|
|
783
|
+
)
|
|
784
|
+
if not isinstance(response, dict):
|
|
785
|
+
response = response.model_dump()
|
|
786
|
+
embeddings.extend(r["embedding"] for r in response["data"])
|
|
787
|
+
return embeddings
|
|
788
|
+
|
|
789
|
+
# Unconditionally call _get_len_safe_embeddings to handle length safety.
|
|
790
|
+
# This could be optimized to avoid double work when all texts are short enough.
|
|
791
|
+
engine = cast(str, self.deployment)
|
|
792
|
+
return await self._aget_len_safe_embeddings(
|
|
793
|
+
texts, engine=engine, chunk_size=chunk_size, **kwargs
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
def embed_query(self, text: str, **kwargs: Any) -> list[float]:
|
|
797
|
+
"""Call out to OpenAI's embedding endpoint for embedding query text.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
text: The text to embed.
|
|
801
|
+
kwargs: Additional keyword arguments to pass to the embedding API.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
Embedding for the text.
|
|
805
|
+
"""
|
|
806
|
+
self._ensure_sync_client_available()
|
|
807
|
+
return self.embed_documents([text], **kwargs)[0]
|
|
808
|
+
|
|
809
|
+
async def aembed_query(self, text: str, **kwargs: Any) -> list[float]:
|
|
810
|
+
"""Call out to OpenAI's embedding endpoint async for embedding query text.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
text: The text to embed.
|
|
814
|
+
kwargs: Additional keyword arguments to pass to the embedding API.
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
Embedding for the text.
|
|
818
|
+
"""
|
|
819
|
+
embeddings = await self.aembed_documents([text], **kwargs)
|
|
820
|
+
return embeddings[0]
|