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.
Files changed (168) hide show
  1. deeptutor/api/routers/knowledge.py +5225 -0
  2. deeptutor/api/routers/marginnote4.py +278 -0
  3. deeptutor/capabilities/ima/__init__.py +6 -0
  4. deeptutor/capabilities/ima/binding.py +118 -0
  5. deeptutor/capabilities/ima/capability.py +92 -0
  6. deeptutor/capabilities/ima/prompts/en/system.md +33 -0
  7. deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
  8. deeptutor/capabilities/ima/tools.py +476 -0
  9. deeptutor/capabilities/marginnote4/__init__.py +13 -0
  10. deeptutor/capabilities/marginnote4/binding.py +66 -0
  11. deeptutor/capabilities/marginnote4/capability.py +80 -0
  12. deeptutor/capabilities/marginnote4/models.py +173 -0
  13. deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
  14. deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
  15. deeptutor/capabilities/marginnote4/store.py +562 -0
  16. deeptutor/capabilities/marginnote4/tools.py +329 -0
  17. deeptutor/capabilities/obsidian/__init__.py +6 -0
  18. deeptutor/capabilities/obsidian/binding.py +75 -0
  19. deeptutor/capabilities/obsidian/capability.py +82 -0
  20. deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
  21. deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
  22. deeptutor/capabilities/obsidian/tools.py +399 -0
  23. deeptutor/capabilities/obsidian/vault.py +341 -0
  24. deeptutor/knowledge/__init__.py +28 -0
  25. deeptutor/knowledge/add_documents.py +734 -0
  26. deeptutor/knowledge/indexing_run.py +432 -0
  27. deeptutor/knowledge/initializer.py +448 -0
  28. deeptutor/knowledge/manager.py +2570 -0
  29. deeptutor/knowledge/module_service.py +29 -0
  30. deeptutor/knowledge/naming.py +40 -0
  31. deeptutor/knowledge/progress_events.py +45 -0
  32. deeptutor/knowledge/progress_tracker.py +355 -0
  33. deeptutor/knowledge/staging.py +69 -0
  34. deeptutor/services/config/knowledge_base_config.py +239 -0
  35. deeptutor/services/embedding/__init__.py +46 -0
  36. deeptutor/services/embedding/adapters/__init__.py +40 -0
  37. deeptutor/services/embedding/adapters/base.py +196 -0
  38. deeptutor/services/embedding/adapters/cohere.py +176 -0
  39. deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
  40. deeptutor/services/embedding/adapters/gemini.py +462 -0
  41. deeptutor/services/embedding/adapters/jina.py +161 -0
  42. deeptutor/services/embedding/adapters/ollama.py +145 -0
  43. deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
  44. deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
  45. deeptutor/services/embedding/client.py +323 -0
  46. deeptutor/services/embedding/config.py +114 -0
  47. deeptutor/services/embedding/request_options.py +46 -0
  48. deeptutor/services/embedding/validation.py +128 -0
  49. deeptutor/services/github_source/__init__.py +5 -0
  50. deeptutor/services/github_source/client.py +164 -0
  51. deeptutor/services/github_source/sync.py +384 -0
  52. deeptutor/services/github_source/sync_service.py +72 -0
  53. deeptutor/services/rag/__init__.py +21 -0
  54. deeptutor/services/rag/embedding_binding.py +331 -0
  55. deeptutor/services/rag/embedding_signature.py +63 -0
  56. deeptutor/services/rag/eval/__init__.py +92 -0
  57. deeptutor/services/rag/eval/dataset.py +244 -0
  58. deeptutor/services/rag/eval/diff.py +312 -0
  59. deeptutor/services/rag/eval/matching.py +218 -0
  60. deeptutor/services/rag/eval/metrics.py +213 -0
  61. deeptutor/services/rag/eval/report.py +111 -0
  62. deeptutor/services/rag/eval/runner.py +309 -0
  63. deeptutor/services/rag/factory.py +369 -0
  64. deeptutor/services/rag/file_routing.py +5 -0
  65. deeptutor/services/rag/index_probe.py +327 -0
  66. deeptutor/services/rag/index_versioning.py +387 -0
  67. deeptutor/services/rag/kb_paths.py +50 -0
  68. deeptutor/services/rag/linked_kb.py +237 -0
  69. deeptutor/services/rag/pipelines/__init__.py +7 -0
  70. deeptutor/services/rag/pipelines/base.py +41 -0
  71. deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
  72. deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
  73. deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
  74. deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
  75. deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
  76. deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
  77. deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
  78. deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
  79. deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
  80. deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
  81. deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
  82. deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
  83. deeptutor/services/rag/pipelines/ima/client.py +364 -0
  84. deeptutor/services/rag/pipelines/ima/config.py +140 -0
  85. deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
  86. deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
  87. deeptutor/services/rag/pipelines/ima/media.py +190 -0
  88. deeptutor/services/rag/pipelines/ima/models.py +348 -0
  89. deeptutor/services/rag/pipelines/ima/notes.py +240 -0
  90. deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
  91. deeptutor/services/rag/pipelines/ima/probe.py +105 -0
  92. deeptutor/services/rag/pipelines/ima/sources.py +105 -0
  93. deeptutor/services/rag/pipelines/ima/transport.py +130 -0
  94. deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
  95. deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
  96. deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
  97. deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
  98. deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
  99. deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
  100. deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
  101. deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
  102. deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
  103. deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
  104. deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
  105. deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
  106. deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
  107. deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
  108. deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
  109. deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
  110. deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
  111. deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
  112. deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
  113. deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
  114. deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
  115. deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
  116. deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
  117. deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
  118. deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
  119. deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
  120. deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
  121. deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
  122. deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
  123. deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
  124. deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
  125. deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
  126. deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
  127. deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
  128. deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
  129. deeptutor/services/rag/pipelines/modes.py +48 -0
  130. deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
  131. deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
  132. deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
  133. deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
  134. deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
  135. deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
  136. deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
  137. deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
  138. deeptutor/services/rag/pipelines/weknora/client.py +106 -0
  139. deeptutor/services/rag/pipelines/weknora/config.py +56 -0
  140. deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
  141. deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
  142. deeptutor/services/rag/preflight.py +274 -0
  143. deeptutor/services/rag/provider_binding.py +70 -0
  144. deeptutor/services/rag/query_rewrite.py +186 -0
  145. deeptutor/services/rag/service.py +379 -0
  146. deeptutor/services/rag/smart_retriever.py +81 -0
  147. deeptutor/services/rag/source_pages.py +292 -0
  148. deeptutor/services/rag/source_visuals.py +434 -0
  149. deeptutor/services/rag/visual_assets.py +430 -0
  150. deeptutor/services/rag/visual_coverage.py +165 -0
  151. deeptutor/services/web_source/__init__.py +1 -0
  152. deeptutor/services/web_source/bilingual.py +255 -0
  153. deeptutor/services/web_source/crawler.py +681 -0
  154. deeptutor/services/web_source/html_extractor.py +643 -0
  155. deeptutor/services/web_source/markdown.py +28 -0
  156. deeptutor/services/web_source/navigation.py +72 -0
  157. deeptutor/services/web_source/repository.py +640 -0
  158. deeptutor/services/web_source/robots.py +182 -0
  159. deeptutor/services/web_source/scheduler.py +336 -0
  160. deeptutor/services/web_source/snapshot_assets.py +171 -0
  161. deeptutor/services/web_source/sync.py +253 -0
  162. deeptutor/services/workspace/kb_move.py +446 -0
  163. deeptutor/tools/rag_tool.py +110 -0
  164. deeptutor_cli/kb.py +756 -0
  165. deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
  166. deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
  167. deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
  168. 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
+ }