deeptutor-knowledge 1.7.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.
- deeptutor/api/routers/knowledge.py +5225 -0
- deeptutor/api/routers/marginnote4.py +278 -0
- deeptutor/capabilities/ima/__init__.py +6 -0
- deeptutor/capabilities/ima/binding.py +118 -0
- deeptutor/capabilities/ima/capability.py +92 -0
- deeptutor/capabilities/ima/prompts/en/system.md +33 -0
- deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
- deeptutor/capabilities/ima/tools.py +476 -0
- deeptutor/capabilities/marginnote4/__init__.py +13 -0
- deeptutor/capabilities/marginnote4/binding.py +66 -0
- deeptutor/capabilities/marginnote4/capability.py +80 -0
- deeptutor/capabilities/marginnote4/models.py +173 -0
- deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
- deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
- deeptutor/capabilities/marginnote4/store.py +562 -0
- deeptutor/capabilities/marginnote4/tools.py +329 -0
- deeptutor/capabilities/obsidian/__init__.py +6 -0
- deeptutor/capabilities/obsidian/binding.py +75 -0
- deeptutor/capabilities/obsidian/capability.py +82 -0
- deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
- deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
- deeptutor/capabilities/obsidian/tools.py +399 -0
- deeptutor/capabilities/obsidian/vault.py +341 -0
- deeptutor/knowledge/__init__.py +28 -0
- deeptutor/knowledge/add_documents.py +734 -0
- deeptutor/knowledge/indexing_run.py +432 -0
- deeptutor/knowledge/initializer.py +448 -0
- deeptutor/knowledge/manager.py +2570 -0
- deeptutor/knowledge/module_service.py +29 -0
- deeptutor/knowledge/naming.py +40 -0
- deeptutor/knowledge/progress_events.py +45 -0
- deeptutor/knowledge/progress_tracker.py +355 -0
- deeptutor/knowledge/staging.py +69 -0
- deeptutor/services/config/knowledge_base_config.py +239 -0
- deeptutor/services/embedding/__init__.py +46 -0
- deeptutor/services/embedding/adapters/__init__.py +40 -0
- deeptutor/services/embedding/adapters/base.py +196 -0
- deeptutor/services/embedding/adapters/cohere.py +176 -0
- deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
- deeptutor/services/embedding/adapters/gemini.py +462 -0
- deeptutor/services/embedding/adapters/jina.py +161 -0
- deeptutor/services/embedding/adapters/ollama.py +145 -0
- deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
- deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
- deeptutor/services/embedding/client.py +323 -0
- deeptutor/services/embedding/config.py +114 -0
- deeptutor/services/embedding/request_options.py +46 -0
- deeptutor/services/embedding/validation.py +128 -0
- deeptutor/services/github_source/__init__.py +5 -0
- deeptutor/services/github_source/client.py +164 -0
- deeptutor/services/github_source/sync.py +384 -0
- deeptutor/services/github_source/sync_service.py +72 -0
- deeptutor/services/rag/__init__.py +21 -0
- deeptutor/services/rag/embedding_binding.py +331 -0
- deeptutor/services/rag/embedding_signature.py +63 -0
- deeptutor/services/rag/eval/__init__.py +92 -0
- deeptutor/services/rag/eval/dataset.py +244 -0
- deeptutor/services/rag/eval/diff.py +312 -0
- deeptutor/services/rag/eval/matching.py +218 -0
- deeptutor/services/rag/eval/metrics.py +213 -0
- deeptutor/services/rag/eval/report.py +111 -0
- deeptutor/services/rag/eval/runner.py +309 -0
- deeptutor/services/rag/factory.py +369 -0
- deeptutor/services/rag/file_routing.py +5 -0
- deeptutor/services/rag/index_probe.py +327 -0
- deeptutor/services/rag/index_versioning.py +387 -0
- deeptutor/services/rag/kb_paths.py +50 -0
- deeptutor/services/rag/linked_kb.py +237 -0
- deeptutor/services/rag/pipelines/__init__.py +7 -0
- deeptutor/services/rag/pipelines/base.py +41 -0
- deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
- deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
- deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
- deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
- deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
- deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
- deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
- deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
- deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
- deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
- deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
- deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
- deeptutor/services/rag/pipelines/ima/client.py +364 -0
- deeptutor/services/rag/pipelines/ima/config.py +140 -0
- deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
- deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
- deeptutor/services/rag/pipelines/ima/media.py +190 -0
- deeptutor/services/rag/pipelines/ima/models.py +348 -0
- deeptutor/services/rag/pipelines/ima/notes.py +240 -0
- deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
- deeptutor/services/rag/pipelines/ima/probe.py +105 -0
- deeptutor/services/rag/pipelines/ima/sources.py +105 -0
- deeptutor/services/rag/pipelines/ima/transport.py +130 -0
- deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
- deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
- deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
- deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
- deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
- deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
- deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
- deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
- deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
- deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
- deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
- deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
- deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
- deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
- deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
- deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
- deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
- deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
- deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
- deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
- deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
- deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
- deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
- deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
- deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
- deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
- deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
- deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
- deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
- deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
- deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
- deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
- deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
- deeptutor/services/rag/pipelines/modes.py +48 -0
- deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
- deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
- deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
- deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
- deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
- deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
- deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
- deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
- deeptutor/services/rag/pipelines/weknora/client.py +106 -0
- deeptutor/services/rag/pipelines/weknora/config.py +56 -0
- deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
- deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
- deeptutor/services/rag/preflight.py +274 -0
- deeptutor/services/rag/provider_binding.py +70 -0
- deeptutor/services/rag/query_rewrite.py +186 -0
- deeptutor/services/rag/service.py +379 -0
- deeptutor/services/rag/smart_retriever.py +81 -0
- deeptutor/services/rag/source_pages.py +292 -0
- deeptutor/services/rag/source_visuals.py +434 -0
- deeptutor/services/rag/visual_assets.py +430 -0
- deeptutor/services/rag/visual_coverage.py +165 -0
- deeptutor/services/web_source/__init__.py +1 -0
- deeptutor/services/web_source/bilingual.py +255 -0
- deeptutor/services/web_source/crawler.py +681 -0
- deeptutor/services/web_source/html_extractor.py +643 -0
- deeptutor/services/web_source/markdown.py +28 -0
- deeptutor/services/web_source/navigation.py +72 -0
- deeptutor/services/web_source/repository.py +640 -0
- deeptutor/services/web_source/robots.py +182 -0
- deeptutor/services/web_source/scheduler.py +336 -0
- deeptutor/services/web_source/snapshot_assets.py +171 -0
- deeptutor/services/web_source/sync.py +253 -0
- deeptutor/services/workspace/kb_move.py +446 -0
- deeptutor/tools/rag_tool.py +110 -0
- deeptutor_cli/kb.py +756 -0
- deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
- deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
- deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Cohere Embedding Adapter for v1 and v2 API."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from deeptutor.services.llm.openai_http_client import disable_ssl_verify_enabled
|
|
9
|
+
|
|
10
|
+
from .base import BaseEmbeddingAdapter, EmbeddingRequest, EmbeddingResponse
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CohereEmbeddingAdapter(BaseEmbeddingAdapter):
|
|
16
|
+
"""Adapter for Cohere Embed API (v1 and v2)."""
|
|
17
|
+
|
|
18
|
+
# Cohere requires input_type and already defaults to "search_document",
|
|
19
|
+
# so document vectors are unchanged and only queries gain the right role.
|
|
20
|
+
SUPPORTS_INPUT_TYPE = True
|
|
21
|
+
|
|
22
|
+
MODELS_INFO = {
|
|
23
|
+
"embed-v4.0": {
|
|
24
|
+
"dimensions": [256, 512, 1024, 1536],
|
|
25
|
+
"default": 1024,
|
|
26
|
+
"api_version": "v2",
|
|
27
|
+
"multimodal": True,
|
|
28
|
+
},
|
|
29
|
+
"embed-english-v3.0": {
|
|
30
|
+
"dimensions": [1024],
|
|
31
|
+
"default": 1024,
|
|
32
|
+
"api_version": "v1",
|
|
33
|
+
"multimodal": False,
|
|
34
|
+
},
|
|
35
|
+
"embed-multilingual-v3.0": {
|
|
36
|
+
"dimensions": [1024],
|
|
37
|
+
"default": 1024,
|
|
38
|
+
"api_version": "v1",
|
|
39
|
+
"multimodal": False,
|
|
40
|
+
},
|
|
41
|
+
"embed-multilingual-light-v3.0": {
|
|
42
|
+
"dimensions": [384],
|
|
43
|
+
"default": 384,
|
|
44
|
+
"api_version": "v1",
|
|
45
|
+
"multimodal": False,
|
|
46
|
+
},
|
|
47
|
+
"embed-english-light-v3.0": {
|
|
48
|
+
"dimensions": [384],
|
|
49
|
+
"default": 384,
|
|
50
|
+
"api_version": "v1",
|
|
51
|
+
"multimodal": False,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
}
|
|
60
|
+
headers.update({str(k): str(v) for k, v in self.extra_headers.items()})
|
|
61
|
+
|
|
62
|
+
model_name = request.model or self.model
|
|
63
|
+
model_info = self.MODELS_INFO.get(model_name, {})
|
|
64
|
+
# `api_version` is now purely a request-shape selector (v1 vs v2 payload).
|
|
65
|
+
# The URL itself is whatever the user configured. Resolution order:
|
|
66
|
+
# explicit self.api_version (catalog/env override) → MODELS_INFO entry → "v2"
|
|
67
|
+
api_version = self.api_version or model_info.get("api_version") or "v2"
|
|
68
|
+
dimension = request.dimensions or self.dimensions
|
|
69
|
+
|
|
70
|
+
input_type = request.input_type or "search_document"
|
|
71
|
+
|
|
72
|
+
if api_version == "v1":
|
|
73
|
+
if request.contents:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Cohere v1 API does not support multimodal `contents`. "
|
|
76
|
+
"Use embed-v4.0 (v2 API) for multimodal."
|
|
77
|
+
)
|
|
78
|
+
payload = {
|
|
79
|
+
"texts": request.texts,
|
|
80
|
+
"model": model_name,
|
|
81
|
+
"input_type": input_type,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if not request.truncate:
|
|
85
|
+
payload["truncate"] = "NONE"
|
|
86
|
+
else:
|
|
87
|
+
if request.contents and not bool(model_info.get("multimodal", False)):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Cohere model '{model_name}' does not support multimodal `contents`."
|
|
90
|
+
)
|
|
91
|
+
payload = {
|
|
92
|
+
"model": model_name,
|
|
93
|
+
"embedding_types": ["float"],
|
|
94
|
+
"input_type": input_type,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if request.contents:
|
|
98
|
+
# Cohere v2 multimodal: `inputs: [{content: [{type, text|image_url}]}]`
|
|
99
|
+
# We translate the simple [{text|image|video}] contract into v2's
|
|
100
|
+
# nested form. v2 cannot mix text+image in one input, so each
|
|
101
|
+
# content dict becomes its own input item.
|
|
102
|
+
inputs = []
|
|
103
|
+
for item in request.contents:
|
|
104
|
+
if not isinstance(item, dict):
|
|
105
|
+
continue
|
|
106
|
+
kind, value = next(iter(item.items()))
|
|
107
|
+
if kind == "text":
|
|
108
|
+
inputs.append({"content": [{"type": "text", "text": value}]})
|
|
109
|
+
elif kind == "image":
|
|
110
|
+
inputs.append(
|
|
111
|
+
{"content": [{"type": "image_url", "image_url": {"url": value}}]}
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Cohere v2 does not support content type '{kind}'")
|
|
115
|
+
payload["inputs"] = inputs
|
|
116
|
+
else:
|
|
117
|
+
payload["texts"] = request.texts
|
|
118
|
+
|
|
119
|
+
supported_dims = model_info.get("dimensions", [])
|
|
120
|
+
if isinstance(supported_dims, list) and len(supported_dims) > 1:
|
|
121
|
+
payload["output_dimension"] = dimension or model_info.get("default")
|
|
122
|
+
|
|
123
|
+
if not request.truncate:
|
|
124
|
+
payload["truncate"] = "NONE"
|
|
125
|
+
|
|
126
|
+
url = self.base_url
|
|
127
|
+
|
|
128
|
+
logger.debug(f"Sending embedding request to {url} with {len(request.texts)} texts")
|
|
129
|
+
|
|
130
|
+
async with httpx.AsyncClient(
|
|
131
|
+
timeout=self.request_timeout, verify=not disable_ssl_verify_enabled()
|
|
132
|
+
) as client:
|
|
133
|
+
response = await client.post(url, json=payload, headers=headers)
|
|
134
|
+
|
|
135
|
+
if response.status_code >= 400:
|
|
136
|
+
logger.error(f"HTTP {response.status_code} response body: {response.text}")
|
|
137
|
+
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
data = response.json()
|
|
140
|
+
|
|
141
|
+
if api_version == "v1":
|
|
142
|
+
embeddings = data["embeddings"]
|
|
143
|
+
else:
|
|
144
|
+
embeddings = data["embeddings"]["float"]
|
|
145
|
+
|
|
146
|
+
actual_dims = len(embeddings[0]) if embeddings else 0
|
|
147
|
+
expected_dims = request.dimensions or self.dimensions
|
|
148
|
+
|
|
149
|
+
if expected_dims and actual_dims != expected_dims:
|
|
150
|
+
logger.warning(f"Dimension mismatch: expected {expected_dims}, got {actual_dims}")
|
|
151
|
+
|
|
152
|
+
logger.info(
|
|
153
|
+
f"Successfully generated {len(embeddings)} embeddings "
|
|
154
|
+
f"(model: {data.get('model', self.model)}, dimensions: {actual_dims})"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return EmbeddingResponse(
|
|
158
|
+
embeddings=embeddings,
|
|
159
|
+
model=data.get("model", self.model),
|
|
160
|
+
dimensions=actual_dims,
|
|
161
|
+
usage=data.get("meta", {}).get("billed_units", {}),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def get_model_info(self) -> Dict[str, Any]:
|
|
165
|
+
model_info = self.MODELS_INFO.get(self.model, {})
|
|
166
|
+
dimensions_list = model_info.get("dimensions", [])
|
|
167
|
+
api_version = self.api_version or model_info.get("api_version") or "v2"
|
|
168
|
+
return {
|
|
169
|
+
"model": self.model,
|
|
170
|
+
"dimensions": model_info.get("default", self.dimensions),
|
|
171
|
+
"supports_variable_dimensions": len(dimensions_list) > 1
|
|
172
|
+
if isinstance(dimensions_list, list)
|
|
173
|
+
else False,
|
|
174
|
+
"multimodal": bool(model_info.get("multimodal", False)) and api_version != "v1",
|
|
175
|
+
"provider": "cohere",
|
|
176
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Aliyun DashScope embedding adapter (text + multimodal).
|
|
2
|
+
|
|
3
|
+
Uses the ``dashscope`` Python SDK rather than the OpenAI contract because
|
|
4
|
+
DashScope's native API shape does not match it. DashScope splits embeddings
|
|
5
|
+
across two surfaces served from different endpoints, and the SDK derives the
|
|
6
|
+
endpoint from the model id:
|
|
7
|
+
|
|
8
|
+
* multimodal models (``qwen3-vl-embedding``, ``multimodal-embedding-v1``) use
|
|
9
|
+
``dashscope.MultiModalEmbedding.call`` (``input=[{text|image|video}]`` +
|
|
10
|
+
``parameters={dimension, enable_fusion}``);
|
|
11
|
+
* text models (``text-embedding-v1..v4``) use ``dashscope.TextEmbedding.call``
|
|
12
|
+
(``input=[str, ...]``).
|
|
13
|
+
|
|
14
|
+
Routing a text model through the multimodal call sends it to the multimodal
|
|
15
|
+
endpoint and fails with HTTP 400 "url error" (issue #660), so ``embed`` picks
|
|
16
|
+
the surface from the model id. Both calls are synchronous, so we run them in a
|
|
17
|
+
thread pool to keep the embedding stack non-blocking.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
from http import HTTPStatus
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any, Dict, List
|
|
26
|
+
|
|
27
|
+
from deeptutor.services.config.embedding_endpoint import (
|
|
28
|
+
is_dashscope_multimodal_embedding_model,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .base import BaseEmbeddingAdapter, EmbeddingRequest, EmbeddingResponse
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DashScopeMultiModalEmbeddingAdapter(BaseEmbeddingAdapter):
|
|
37
|
+
"""Adapter for Aliyun DashScope (Bailian) text + multimodal embedding."""
|
|
38
|
+
|
|
39
|
+
MODELS_INFO = {
|
|
40
|
+
"qwen3-vl-embedding": {
|
|
41
|
+
"default": 2560,
|
|
42
|
+
"dimensions": [256, 512, 768, 1024, 1536, 2048, 2560],
|
|
43
|
+
"multimodal": True,
|
|
44
|
+
},
|
|
45
|
+
"multimodal-embedding-v1": {
|
|
46
|
+
"default": 1536,
|
|
47
|
+
"dimensions": [],
|
|
48
|
+
"multimodal": True,
|
|
49
|
+
},
|
|
50
|
+
"text-embedding-v3": {
|
|
51
|
+
"default": 1024,
|
|
52
|
+
"dimensions": [],
|
|
53
|
+
"multimodal": False,
|
|
54
|
+
},
|
|
55
|
+
"text-embedding-v4": {
|
|
56
|
+
"default": 1024,
|
|
57
|
+
"dimensions": [],
|
|
58
|
+
"multimodal": False,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def _build_contents(self, request: EmbeddingRequest) -> List[Dict[str, Any]]:
|
|
63
|
+
if request.contents:
|
|
64
|
+
return [item for item in request.contents if isinstance(item, dict)]
|
|
65
|
+
return [{"text": text} for text in request.texts]
|
|
66
|
+
|
|
67
|
+
def _build_parameters(self, request: EmbeddingRequest) -> Dict[str, Any]:
|
|
68
|
+
params: Dict[str, Any] = {}
|
|
69
|
+
dim_value = request.dimensions or self.dimensions
|
|
70
|
+
if dim_value:
|
|
71
|
+
params["dimension"] = dim_value
|
|
72
|
+
if request.enable_fusion is not None:
|
|
73
|
+
params["enable_fusion"] = bool(request.enable_fusion)
|
|
74
|
+
return params
|
|
75
|
+
|
|
76
|
+
def _build_text_inputs(self, request: EmbeddingRequest) -> List[str]:
|
|
77
|
+
"""Flatten the request to the plain string list TextEmbedding expects."""
|
|
78
|
+
if request.texts:
|
|
79
|
+
return list(request.texts)
|
|
80
|
+
return [
|
|
81
|
+
item["text"]
|
|
82
|
+
for item in (request.contents or [])
|
|
83
|
+
if isinstance(item, dict) and item.get("text")
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
def _build_text_parameters(self, request: EmbeddingRequest) -> Dict[str, Any]:
|
|
87
|
+
# TextEmbedding takes `dimension` (v3/v4 support it) but has no
|
|
88
|
+
# `enable_fusion` — that is a multimodal-only knob.
|
|
89
|
+
params: Dict[str, Any] = {}
|
|
90
|
+
dim_value = request.dimensions or self.dimensions
|
|
91
|
+
if dim_value:
|
|
92
|
+
params["dimension"] = dim_value
|
|
93
|
+
return params
|
|
94
|
+
|
|
95
|
+
async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
|
|
96
|
+
model_name = request.model or self.model
|
|
97
|
+
if is_dashscope_multimodal_embedding_model(model_name):
|
|
98
|
+
return await self._embed_multimodal(request, model_name)
|
|
99
|
+
return await self._embed_text(request, model_name)
|
|
100
|
+
|
|
101
|
+
async def _embed_multimodal(
|
|
102
|
+
self, request: EmbeddingRequest, model_name: str
|
|
103
|
+
) -> EmbeddingResponse:
|
|
104
|
+
try:
|
|
105
|
+
from dashscope import MultiModalEmbedding
|
|
106
|
+
except ImportError as exc:
|
|
107
|
+
raise ImportError(
|
|
108
|
+
"dashscope SDK not installed. Run `pip install dashscope` "
|
|
109
|
+
"(or add to your project deps) to enable Aliyun DashScope."
|
|
110
|
+
) from exc
|
|
111
|
+
|
|
112
|
+
contents = self._build_contents(request)
|
|
113
|
+
parameters = self._build_parameters(request)
|
|
114
|
+
|
|
115
|
+
logger.debug(
|
|
116
|
+
"Calling dashscope.MultiModalEmbedding.call "
|
|
117
|
+
f"(model={model_name}, items={len(contents)}, params={parameters})"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# SDK call is sync — run in worker thread to avoid blocking the loop.
|
|
121
|
+
# IMPORTANT: the dashscope SDK takes a flat list for `input`
|
|
122
|
+
# (e.g. ``input=[{"text": "..."}]``) and internally wraps it as
|
|
123
|
+
# ``{"contents": [...]}`` before POSTing to the REST endpoint. Do NOT
|
|
124
|
+
# pass ``{"contents": contents}`` here — that produces a double-wrap
|
|
125
|
+
# and the API responds with HTTP 400 ("Input should be a valid list").
|
|
126
|
+
resp = await asyncio.to_thread(
|
|
127
|
+
MultiModalEmbedding.call,
|
|
128
|
+
api_key=self.api_key,
|
|
129
|
+
model=model_name,
|
|
130
|
+
input=contents,
|
|
131
|
+
**parameters,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self._raise_on_error(resp, model_name)
|
|
135
|
+
return self._parse_response(resp, model_name, request)
|
|
136
|
+
|
|
137
|
+
async def _embed_text(self, request: EmbeddingRequest, model_name: str) -> EmbeddingResponse:
|
|
138
|
+
try:
|
|
139
|
+
from dashscope import TextEmbedding
|
|
140
|
+
except ImportError as exc:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
"dashscope SDK not installed. Run `pip install dashscope` "
|
|
143
|
+
"(or add to your project deps) to enable Aliyun DashScope."
|
|
144
|
+
) from exc
|
|
145
|
+
|
|
146
|
+
inputs = self._build_text_inputs(request)
|
|
147
|
+
parameters = self._build_text_parameters(request)
|
|
148
|
+
|
|
149
|
+
logger.debug(
|
|
150
|
+
"Calling dashscope.TextEmbedding.call "
|
|
151
|
+
f"(model={model_name}, items={len(inputs)}, params={parameters})"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# TextEmbedding.call POSTs to the DashScope text-embedding endpoint and
|
|
155
|
+
# accepts a flat list of strings for `input`. Response/usage/error shape
|
|
156
|
+
# matches MultiModalEmbedding, so we reuse the shared parsers below.
|
|
157
|
+
resp = await asyncio.to_thread(
|
|
158
|
+
TextEmbedding.call,
|
|
159
|
+
api_key=self.api_key,
|
|
160
|
+
model=model_name,
|
|
161
|
+
input=inputs,
|
|
162
|
+
**parameters,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self._raise_on_error(resp, model_name)
|
|
166
|
+
return self._parse_response(resp, model_name, request)
|
|
167
|
+
|
|
168
|
+
def _raise_on_error(self, resp: Any, model_name: str) -> None:
|
|
169
|
+
status_code = getattr(resp, "status_code", None)
|
|
170
|
+
if status_code is None or status_code == HTTPStatus.OK:
|
|
171
|
+
return
|
|
172
|
+
code = getattr(resp, "code", "") or ""
|
|
173
|
+
message = getattr(resp, "message", "") or ""
|
|
174
|
+
request_id = getattr(resp, "request_id", "") or ""
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
f"DashScope MultiModalEmbedding call failed: "
|
|
177
|
+
f"status={status_code}, code={code}, message={message}, "
|
|
178
|
+
f"model={model_name}, request_id={request_id}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _parse_response(
|
|
182
|
+
self, resp: Any, model_name: str, request: EmbeddingRequest
|
|
183
|
+
) -> EmbeddingResponse:
|
|
184
|
+
output = getattr(resp, "output", None)
|
|
185
|
+
if output is None:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"DashScope response missing `output` (request_id={getattr(resp, 'request_id', '')})"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# `output` is dict-like in the SDK.
|
|
191
|
+
if isinstance(output, dict):
|
|
192
|
+
raw = output.get("embeddings") or []
|
|
193
|
+
else:
|
|
194
|
+
raw = getattr(output, "embeddings", None) or []
|
|
195
|
+
|
|
196
|
+
embeddings: List[List[float]] = []
|
|
197
|
+
for item in raw:
|
|
198
|
+
if isinstance(item, dict):
|
|
199
|
+
vec = item.get("embedding")
|
|
200
|
+
else:
|
|
201
|
+
vec = getattr(item, "embedding", None)
|
|
202
|
+
if vec is None:
|
|
203
|
+
continue
|
|
204
|
+
embeddings.append(list(vec))
|
|
205
|
+
|
|
206
|
+
if not embeddings:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"DashScope response parsed successfully but no embedding vectors were returned."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
usage = getattr(resp, "usage", {}) or {}
|
|
212
|
+
if not isinstance(usage, dict):
|
|
213
|
+
usage = {
|
|
214
|
+
k: getattr(usage, k, None)
|
|
215
|
+
for k in ("input_tokens", "output_tokens", "total_tokens")
|
|
216
|
+
if hasattr(usage, k)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
actual_dims = len(embeddings[0]) if embeddings else 0
|
|
220
|
+
logger.info(
|
|
221
|
+
f"Successfully generated {len(embeddings)} DashScope embeddings "
|
|
222
|
+
f"(model: {model_name}, dimensions: {actual_dims}, "
|
|
223
|
+
f"fusion={request.enable_fusion})"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return EmbeddingResponse(
|
|
227
|
+
embeddings=embeddings,
|
|
228
|
+
model=model_name,
|
|
229
|
+
dimensions=actual_dims,
|
|
230
|
+
usage=usage,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def get_model_info(self) -> Dict[str, Any]:
|
|
234
|
+
info = self.MODELS_INFO.get(self.model or "", {})
|
|
235
|
+
return {
|
|
236
|
+
"model": self.model,
|
|
237
|
+
"dimensions": info.get("default", self.dimensions),
|
|
238
|
+
"supported_dimensions": info.get("dimensions", []),
|
|
239
|
+
"supports_variable_dimensions": bool(info.get("dimensions")),
|
|
240
|
+
"multimodal": bool(info.get("multimodal", False)),
|
|
241
|
+
"provider": "aliyun",
|
|
242
|
+
}
|