adaptive-memory-multi-model-router 2.15.4 → 2.15.5
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.
- package/.github/workflows/adapters-ci.yml +142 -0
- package/.github/workflows/ci.yml +2 -5
- package/.github/workflows/pypi-publish.yml +102 -0
- package/AGENT_COUNCIL_FINDINGS.md +123 -98
- package/CAMPAIGN_SUMMARY.md +87 -0
- package/CHANGELOG.md +7 -1
- package/ENTERPRISE_INTEGRATIONS.md +94 -0
- package/INTEGRATION_PROGRESS.md +96 -0
- package/OPPORTUNITIES_100.md +277 -0
- package/POPULARITY_BOOSTERS.md +1 -1
- package/README.md +137 -142
- package/README_ja.md +17 -21
- package/README_zh.md +13 -17
- package/TODO_BROWSER_AUTOMATION.md +87 -0
- package/a3m-integrations-summary.md +73 -0
- package/adapters/a3m_adapter/__init__.py +39 -3
- package/adapters/a3m_adapter/adapter/__init__.py +16 -1
- package/adapters/a3m_adapter/adapter/autogen.py +169 -0
- package/adapters/a3m_adapter/adapter/haystack.py +197 -0
- package/adapters/a3m_adapter/adapter/langgraph.py +196 -0
- package/adapters/a3m_adapter/adapter/pinecone.py +217 -0
- package/adapters/a3m_adapter/adapter/vercel.py +188 -0
- package/adapters/a3m_adapter/tests/test_adapters.py +29 -31
- package/adapters/a3m_adapter/tests/test_integration.py +80 -0
- package/adapters/requirements-dev.txt +6 -0
- package/adapters/requirements.txt +4 -0
- package/apps/cost-calculator/README.md +72 -0
- package/apps/cost-calculator/calculator.css +280 -0
- package/apps/cost-calculator/calculator.js +150 -0
- package/apps/cost-calculator/index.html +321 -0
- package/apps/cost-calculator/package.json +13 -0
- package/articles/ANNOUNCEMENT_reddit_ml.md +76 -0
- package/articles/ANNOUNCEMENT_vc/347/244/276/345/214/272.md +71 -0
- package/articles/ANNOUNCEMENT_vercel.md +85 -0
- package/articles/CHINESE_DIRECTORIES.md +2 -2
- package/articles/CHINESE_SUBMISSIONS_READY.md +5 -5
- package/articles/HN_POST_READY.md +16 -20
- package/articles/INDIEHACKERS_READY.md +19 -34
- package/demo.py +251 -0
- package/dist/analytics/costAnalytics.d.ts +0 -1
- package/dist/benchmark/reproducible.d.ts +0 -1
- package/dist/cache/semanticCache.d.ts +0 -1
- package/dist/cost/budgetEnforcer.d.ts +0 -1
- package/dist/cost/costTracker.d.ts +0 -1
- package/dist/ensemble/multiRoundDialog.d.ts +0 -1
- package/dist/ensemble/shapleyValue.d.ts +0 -1
- package/dist/ensemble.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/integrations/oauth.d.ts +0 -1
- package/dist/integrations/scienceAdapter.d.ts +0 -1
- package/dist/memory/autoFetch.d.ts +0 -1
- package/dist/memory/hybridMemory.d.ts +0 -1
- package/dist/memory/memoryTree.d.ts +0 -1
- package/dist/memory/obsidianVault.d.ts +0 -1
- package/dist/memory/reasoningBank.d.ts +0 -1
- package/dist/observability/changeWatch.d.ts +0 -1
- package/dist/observability/fatigueDetector.d.ts +0 -1
- package/dist/observability/index.d.ts +0 -1
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/middleware.d.ts +0 -1
- package/dist/observability/tracer.d.ts +0 -1
- package/dist/observability/types.d.ts +0 -1
- package/dist/providers/providerConfig.d.ts +5 -2
- package/dist/providers/providerConfig.js +1006 -1
- package/dist/providers/providerConfig.js.map +1 -1
- package/dist/routing/advancedRouter.d.ts +1 -2
- package/dist/routing/advancedRouter.js +84 -0
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/crossModelValidation.d.ts +0 -1
- package/dist/routing/providerHealth.d.ts +77 -2
- package/dist/routing/providerHealth.js +190 -4
- package/dist/routing/providerHealth.js.map +1 -1
- package/dist/routing/providerRetry.d.ts +0 -1
- package/dist/routing/shadowSampler.d.ts +187 -0
- package/dist/routing/shadowSampler.js +306 -0
- package/dist/routing/shadowSampler.js.map +1 -0
- package/dist/sdk.d.ts +0 -1
- package/dist/security/guardrails.d.ts +0 -1
- package/dist/server/dashboard.d.ts +0 -1
- package/dist/server/handlers/chatHandler.d.ts +0 -1
- package/dist/server/handlers/completionsHandler.d.ts +0 -1
- package/dist/server/handlers/embeddingsHandler.d.ts +0 -1
- package/dist/server/handlers/healthHandler.d.ts +0 -1
- package/dist/server/handlers/metricsHandler.d.ts +0 -1
- package/dist/server/handlers/modelsHandler.d.ts +0 -1
- package/dist/server/metrics.d.ts +0 -1
- package/dist/server/modelMapper.d.ts +0 -1
- package/dist/server/proxyServer.d.ts +0 -1
- package/dist/server/router.d.ts +0 -1
- package/dist/server/state.d.ts +0 -1
- package/dist/tui/dashboard.d.ts +0 -1
- package/dist/utils/costUtils.d.ts +0 -1
- package/dist/utils/sorting.d.ts +0 -1
- package/dist/utils/tokenUtils.d.ts +0 -1
- package/docker-compose.yml +84 -60
- package/docs/comparison.md +2 -2
- package/docs/llms-full.txt +89 -0
- package/docs/llms.txt +25 -67
- package/docs-site/index.html +9 -9
- package/integrations/browser-use/README.md +155 -0
- package/integrations/browser-use/examples/form-filling.ts +179 -0
- package/integrations/browser-use/index.ts +145 -0
- package/integrations/browser-use/package.json +34 -0
- package/integrations/browser-use/tsconfig.json +17 -0
- package/integrations/mcp-browser/README.md +177 -0
- package/integrations/mcp-browser/package.json +38 -0
- package/integrations/mcp-browser/src/index.ts +297 -0
- package/integrations/sota-browser/README.md +191 -0
- package/integrations/sota-browser/package.json +34 -0
- package/llms-full.txt +6 -6
- package/llms.txt +25 -67
- package/package.json +34 -5
- package/packages/a3m-vercel-ai/README.md +161 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +12 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js +289 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +1 -0
- package/packages/a3m-vercel-ai/dist/index.d.ts +82 -0
- package/packages/a3m-vercel-ai/dist/index.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/index.js +79 -0
- package/packages/a3m-vercel-ai/dist/index.js.map +1 -0
- package/packages/a3m-vercel-ai/dist/types.d.ts +97 -0
- package/packages/a3m-vercel-ai/dist/types.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/types.js +5 -0
- package/packages/a3m-vercel-ai/dist/types.js.map +1 -0
- package/packages/a3m-vercel-ai/package-lock.json +969 -0
- package/packages/a3m-vercel-ai/package.json +46 -0
- package/packages/a3m-vercel-ai/src/a3m-language-model.ts +381 -0
- package/packages/a3m-vercel-ai/src/index.ts +104 -0
- package/packages/a3m-vercel-ai/src/types.ts +116 -0
- package/packages/a3m-vercel-ai/tsconfig.json +20 -0
- package/packages/agentkit-adapter/LICENSE +21 -0
- package/packages/agentkit-adapter/README.md +126 -0
- package/packages/agentkit-adapter/examples/agentkit-example.ts +139 -0
- package/packages/agentkit-adapter/package.json +57 -0
- package/packages/agentkit-adapter/src/adapter.ts +381 -0
- package/packages/agentkit-adapter/src/index.ts +36 -0
- package/packages/agentkit-adapter/src/types.ts +105 -0
- package/packages/agentkit-adapter/src/util.ts +13 -0
- package/packages/agentkit-adapter/tsconfig.json +22 -0
- package/prometheus.yml +8 -0
- package/python/README.md +92 -65
- package/python/a3m/__init__.py +32 -3
- package/python/a3m/adapters/__init__.py +21 -0
- package/python/a3m/adapters/langchain.py +190 -0
- package/python/a3m/adapters/llamaindex.py +249 -0
- package/python/a3m/adapters/qdrant.py +240 -0
- package/python/a3m/adapters/weaviate.py +263 -0
- package/python/a3m/client.py +5 -0
- package/python/a3m_router.egg-info/PKG-INFO +172 -0
- package/python/a3m_router.egg-info/SOURCES.txt +17 -0
- package/python/a3m_router.egg-info/dependency_links.txt +1 -0
- package/python/a3m_router.egg-info/requires.txt +24 -0
- package/python/a3m_router.egg-info/top_level.txt +1 -0
- package/python/build_verify.sh +32 -0
- package/python/dist/a3m_router-2.2.1-py3-none-any.whl +0 -0
- package/python/dist/a3m_router-2.2.1.tar.gz +0 -0
- package/python/dist/a3m_router-2.2.2-py3-none-any.whl +0 -0
- package/python/dist/a3m_router-2.2.2.tar.gz +0 -0
- package/python/mcp-server/README.md +172 -0
- package/python/mcp-server/a3m_mcp/__init__.py +15 -0
- package/python/mcp-server/a3m_mcp/__main__.py +15 -0
- package/python/mcp-server/a3m_mcp/server.py +339 -0
- package/python/mcp-server/pyproject.toml +24 -0
- package/python/pyproject.toml +56 -5
- package/python/setup.py +3 -28
- package/src/providers/providerConfig.ts +1053 -1
- package/summary.txt +38 -0
- package/tsconfig.build.json +1 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -94
- package/.github/ISSUE_TEMPLATE/config.yml +0 -17
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -71
- package/.well-known/ai-plugin.json +0 -16
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router LlamaIndex adapter.
|
|
3
|
+
|
|
4
|
+
Drop-in LLM for LlamaIndex that routes through A3M Router.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from llama_index import VectorStoreIndex, SimpleWebPageReader
|
|
8
|
+
from a3m.adapters import LlamaIndexAdapter
|
|
9
|
+
|
|
10
|
+
llm = LlamaIndexAdapter(
|
|
11
|
+
base_url="http://localhost:8787",
|
|
12
|
+
model="auto",
|
|
13
|
+
temperature=0.7,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
index = VectorStoreIndex.from_documents(
|
|
17
|
+
documents,
|
|
18
|
+
llm=llm, # Use A3M Router as the LLM
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any, Awaitable, List, Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
LLAMAINDEX_AVAILABLE = False
|
|
30
|
+
try:
|
|
31
|
+
from llama_index.llms import BaseLLM
|
|
32
|
+
from llama_index.llms.custom import CustomLLM
|
|
33
|
+
from llama_index.types import ModelType
|
|
34
|
+
from llama_index.output_parsers.base import BaseOutputParser
|
|
35
|
+
LLAMAINDEX_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
logger.warning("LlamaIndex not installed. pip install llama-index")
|
|
38
|
+
|
|
39
|
+
from a3m.client import A3MRouter, A3MRouterError
|
|
40
|
+
|
|
41
|
+
if LLAMAINDEX_AVAILABLE:
|
|
42
|
+
class LlamaIndexAdapter(BaseLLM):
|
|
43
|
+
"""
|
|
44
|
+
LlamaIndex LLM that routes through A3M Router.
|
|
45
|
+
|
|
46
|
+
Drop-in replacement for OpenAI/GPT LLMs in LlamaIndex pipelines.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
base_url: A3M Router server URL.
|
|
50
|
+
model: Model name or "auto" for intelligent routing.
|
|
51
|
+
temperature: Sampling temperature.
|
|
52
|
+
max_tokens: Max tokens to generate.
|
|
53
|
+
parallel_ensemble: Number of providers for ensemble calls.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
base_url: str = "http://localhost:8787"
|
|
57
|
+
model: str = "auto"
|
|
58
|
+
temperature: float = 0.7
|
|
59
|
+
max_tokens: Optional[int] = 4096
|
|
60
|
+
parallel_ensemble: int = 1
|
|
61
|
+
api_key: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
64
|
+
super().__init__(**kwargs)
|
|
65
|
+
self._router: Optional[A3MRouter] = None
|
|
66
|
+
|
|
67
|
+
def _get_router(self) -> A3MRouter:
|
|
68
|
+
"""Get or create A3M Router client."""
|
|
69
|
+
if self._router is None:
|
|
70
|
+
self._router = A3MRouter(
|
|
71
|
+
base_url=self.base_url,
|
|
72
|
+
api_key=self.api_key,
|
|
73
|
+
default_model=self.model,
|
|
74
|
+
default_temperature=self.temperature,
|
|
75
|
+
default_max_tokens=self.max_tokens,
|
|
76
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
77
|
+
)
|
|
78
|
+
return self._router
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def model_type(self) -> ModelType:
|
|
82
|
+
return ModelType.LLM
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def class_name(self) -> str:
|
|
86
|
+
return "A3MRouter"
|
|
87
|
+
|
|
88
|
+
def complete(
|
|
89
|
+
self,
|
|
90
|
+
prompt: str,
|
|
91
|
+
formatted: bool = False,
|
|
92
|
+
**kwargs: Any,
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""
|
|
95
|
+
Synchronous completion.
|
|
96
|
+
|
|
97
|
+
LlamaIndex calls this for text completion.
|
|
98
|
+
"""
|
|
99
|
+
router = self._get_router()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
response = router.chat(
|
|
103
|
+
messages=[{"role": "user", "content": prompt}],
|
|
104
|
+
model=self.model,
|
|
105
|
+
temperature=self.temperature,
|
|
106
|
+
max_tokens=self.max_tokens,
|
|
107
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
108
|
+
**kwargs,
|
|
109
|
+
)
|
|
110
|
+
return response.content
|
|
111
|
+
except A3MRouterError as e:
|
|
112
|
+
logger.error(f"A3M Router error: {e}")
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
async def acomplete(
|
|
116
|
+
self,
|
|
117
|
+
prompt: str,
|
|
118
|
+
formatted: bool = False,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> Any:
|
|
121
|
+
"""Async completion."""
|
|
122
|
+
router = self._get_router()
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
response = await router.achat(
|
|
126
|
+
messages=[{"role": "user", "content": prompt}],
|
|
127
|
+
model=self.model,
|
|
128
|
+
temperature=self.temperature,
|
|
129
|
+
max_tokens=self.max_tokens,
|
|
130
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
131
|
+
**kwargs,
|
|
132
|
+
)
|
|
133
|
+
return response.content
|
|
134
|
+
except A3MRouterError as e:
|
|
135
|
+
logger.error(f"A3M Router error: {e}")
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
def stream_complete(
|
|
139
|
+
self,
|
|
140
|
+
prompt: str,
|
|
141
|
+
**kwargs: Any,
|
|
142
|
+
) -> Any:
|
|
143
|
+
"""
|
|
144
|
+
Streaming completion.
|
|
145
|
+
|
|
146
|
+
Returns a generator that yields response chunks.
|
|
147
|
+
"""
|
|
148
|
+
router = self._get_router()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
chunks = router.stream_chat(
|
|
152
|
+
messages=[{"role": "user", "content": prompt}],
|
|
153
|
+
model=self.model,
|
|
154
|
+
temperature=self.temperature,
|
|
155
|
+
max_tokens=self.max_tokens,
|
|
156
|
+
**kwargs,
|
|
157
|
+
)
|
|
158
|
+
for chunk in chunks:
|
|
159
|
+
yield chunk.content
|
|
160
|
+
except A3MRouterError as e:
|
|
161
|
+
logger.error(f"A3M Router error: {e}")
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
async def astream_complete(
|
|
165
|
+
self,
|
|
166
|
+
prompt: str,
|
|
167
|
+
**kwargs: Any,
|
|
168
|
+
) -> Awaitable[Any]:
|
|
169
|
+
"""Async streaming completion."""
|
|
170
|
+
router = self._get_router()
|
|
171
|
+
|
|
172
|
+
async def gen():
|
|
173
|
+
async for chunk in router.astream_chat(
|
|
174
|
+
messages=[{"role": "user", "content": prompt}],
|
|
175
|
+
model=self.model,
|
|
176
|
+
temperature=self.temperature,
|
|
177
|
+
max_tokens=self.max_tokens,
|
|
178
|
+
**kwargs,
|
|
179
|
+
):
|
|
180
|
+
yield chunk.content
|
|
181
|
+
|
|
182
|
+
return gen()
|
|
183
|
+
|
|
184
|
+
def chat(self, messages: List[Any], **kwargs: Any) -> Any:
|
|
185
|
+
"""
|
|
186
|
+
Synchronous chat.
|
|
187
|
+
|
|
188
|
+
Converts messages to a single prompt.
|
|
189
|
+
"""
|
|
190
|
+
router = self._get_router()
|
|
191
|
+
|
|
192
|
+
# Convert message objects to content strings
|
|
193
|
+
if hasattr(messages[0], "content"):
|
|
194
|
+
content = "\n".join([getattr(m, "content", str(m)) for m in messages])
|
|
195
|
+
else:
|
|
196
|
+
content = str(messages[0])
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
response = router.chat(
|
|
200
|
+
messages=[{"role": "user", "content": content}],
|
|
201
|
+
model=self.model,
|
|
202
|
+
temperature=self.temperature,
|
|
203
|
+
max_tokens=self.max_tokens,
|
|
204
|
+
**kwargs,
|
|
205
|
+
)
|
|
206
|
+
# Return a simple object that LlamaIndex expects
|
|
207
|
+
return ChatMessage(content=response.content)
|
|
208
|
+
except A3MRouterError as e:
|
|
209
|
+
logger.error(f"A3M Router error: {e}")
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
async def achat(self, messages: List[Any], **kwargs: Any) -> Any:
|
|
213
|
+
"""Async chat."""
|
|
214
|
+
router = self._get_router()
|
|
215
|
+
|
|
216
|
+
if hasattr(messages[0], "content"):
|
|
217
|
+
content = "\n".join([getattr(m, "content", str(m)) for m in messages])
|
|
218
|
+
else:
|
|
219
|
+
content = str(messages[0])
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
response = await router.achat(
|
|
223
|
+
messages=[{"role": "user", "content": content}],
|
|
224
|
+
model=self.model,
|
|
225
|
+
temperature=self.temperature,
|
|
226
|
+
max_tokens=self.max_tokens,
|
|
227
|
+
**kwargs,
|
|
228
|
+
)
|
|
229
|
+
return ChatMessage(content=response.content)
|
|
230
|
+
except A3MRouterError as e:
|
|
231
|
+
logger.error(f"A3M Router error: {e}")
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
else:
|
|
235
|
+
# Stub class when LlamaIndex is not installed
|
|
236
|
+
class LlamaIndexAdapter:
|
|
237
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
238
|
+
raise ImportError(
|
|
239
|
+
"LlamaIndex is not installed. "
|
|
240
|
+
"Install with: pip install llama-index"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# Simple message class for LlamaIndex compatibility
|
|
245
|
+
class ChatMessage:
|
|
246
|
+
"""Simple chat message for LlamaIndex compatibility."""
|
|
247
|
+
def __init__(self, content: str) -> None:
|
|
248
|
+
self.content = content
|
|
249
|
+
self.raw = {}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router Qdrant adapter.
|
|
3
|
+
|
|
4
|
+
Enables A3M Router as the embedding + reranking layer for Qdrant vector search.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from qdrant_client import QdrantClient
|
|
8
|
+
from a3m.adapters import QdrantAdapter
|
|
9
|
+
|
|
10
|
+
# Create A3M-powered Qdrant client
|
|
11
|
+
client = QdrantAdapter(
|
|
12
|
+
base_url="http://localhost:8787",
|
|
13
|
+
collection_name="my_docs",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Generate embeddings via A3M
|
|
17
|
+
query_embedding = client.embed_query("What is machine learning?")
|
|
18
|
+
|
|
19
|
+
# Search Qdrant
|
|
20
|
+
results = client.search(query_vector=query_embedding, limit=5)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import Any, Dict, List, Optional, Union
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
QDRANT_AVAILABLE = False
|
|
31
|
+
try:
|
|
32
|
+
from qdrant_client import QdrantClient
|
|
33
|
+
from qdrant_client.http.exceptions import UnexpectedResponse
|
|
34
|
+
QDRANT_AVAILABLE = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
logger.warning("Qdrant client not installed. pip install qdrant-client")
|
|
37
|
+
|
|
38
|
+
from a3m.client import A3MRouter, A3MRouterError
|
|
39
|
+
|
|
40
|
+
if QDRANT_AVAILABLE:
|
|
41
|
+
class QdrantAdapter(QdrantClient):
|
|
42
|
+
"""
|
|
43
|
+
Qdrant client wrapper that uses A3M Router for embeddings.
|
|
44
|
+
|
|
45
|
+
Provides intelligent embedding generation and can be used as a
|
|
46
|
+
drop-in replacement for QdrantClient in most use cases.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
base_url: A3M Router server URL (for embeddings).
|
|
50
|
+
collection_name: Default collection to search.
|
|
51
|
+
embed_model: Embedding model ("auto" for A3M selection).
|
|
52
|
+
host: Qdrant server host.
|
|
53
|
+
port: Qdrant server port.
|
|
54
|
+
**kwargs: Additional QdrantClient options.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
base_url: str = "http://localhost:8787",
|
|
60
|
+
collection_name: Optional[str] = None,
|
|
61
|
+
embed_model: str = "auto",
|
|
62
|
+
host: Optional[str] = None,
|
|
63
|
+
port: Optional[int] = None,
|
|
64
|
+
**kwargs: Any,
|
|
65
|
+
) -> None:
|
|
66
|
+
# Initialize QdrantClient
|
|
67
|
+
if host and port:
|
|
68
|
+
super().__init__(host=host, port=port, **kwargs)
|
|
69
|
+
else:
|
|
70
|
+
super().__init__(**kwargs)
|
|
71
|
+
|
|
72
|
+
self._a3m_router = A3MRouter(base_url=base_url, default_model=embed_model)
|
|
73
|
+
self._collection_name = collection_name
|
|
74
|
+
self._embed_model = embed_model
|
|
75
|
+
|
|
76
|
+
def embed_query(
|
|
77
|
+
self,
|
|
78
|
+
query: str,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
) -> List[float]:
|
|
81
|
+
"""
|
|
82
|
+
Generate embedding for a query string.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
query: Text to embed.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Embedding vector (list of floats).
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
response = self._a3m_router.embed(texts=[query], model=self._embed_model)
|
|
92
|
+
return response.embedding
|
|
93
|
+
except A3MRouterError as e:
|
|
94
|
+
logger.error(f"A3M Router embedding error: {e}")
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
def embedTexts(
|
|
98
|
+
self,
|
|
99
|
+
texts: List[str],
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> List[List[float]]:
|
|
102
|
+
"""
|
|
103
|
+
Generate embeddings for multiple texts.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
texts: List of texts to embed.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of embedding vectors.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
response = self._a3m_router.embed(texts=texts, model=self._embed_model)
|
|
113
|
+
if isinstance(response, list):
|
|
114
|
+
return [r.embedding for r in response]
|
|
115
|
+
return [response.embedding]
|
|
116
|
+
except A3MRouterError as e:
|
|
117
|
+
logger.error(f"A3M Router embedding error: {e}")
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
def search(
|
|
121
|
+
self,
|
|
122
|
+
collection_name: Optional[str] = None,
|
|
123
|
+
query_vector: Optional[List[float]] = None,
|
|
124
|
+
query_filter: Optional[Any] = None,
|
|
125
|
+
limit: int = 10,
|
|
126
|
+
offset: Optional[int] = None,
|
|
127
|
+
with_vectors: bool = False,
|
|
128
|
+
with_payload: bool = True,
|
|
129
|
+
**kwargs: Any,
|
|
130
|
+
) -> List[Any]:
|
|
131
|
+
"""
|
|
132
|
+
Search Qdrant with A3M-generated query embedding.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
collection_name: Collection to search. Uses default if None.
|
|
136
|
+
query_vector: Query embedding. If None, uses A3M to embed a query.
|
|
137
|
+
query_filter: Qdrant filter condition.
|
|
138
|
+
limit: Number of results to return.
|
|
139
|
+
offset: Pagination offset.
|
|
140
|
+
with_vectors: Include vectors in results.
|
|
141
|
+
with_payload: Include payload in results.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of search results.
|
|
145
|
+
"""
|
|
146
|
+
collection = collection_name or self._collection_name
|
|
147
|
+
if not collection:
|
|
148
|
+
raise ValueError("collection_name is required")
|
|
149
|
+
|
|
150
|
+
if query_vector is None:
|
|
151
|
+
raise ValueError("query_vector is required")
|
|
152
|
+
|
|
153
|
+
return super().search(
|
|
154
|
+
collection_name=collection,
|
|
155
|
+
query_vector=query_vector,
|
|
156
|
+
query_filter=query_filter,
|
|
157
|
+
limit=limit,
|
|
158
|
+
offset=offset,
|
|
159
|
+
with_vectors=with_vectors,
|
|
160
|
+
with_payload=with_payload,
|
|
161
|
+
**kwargs,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def rag_search(
|
|
165
|
+
self,
|
|
166
|
+
query: str,
|
|
167
|
+
collection_name: Optional[str] = None,
|
|
168
|
+
limit: int = 5,
|
|
169
|
+
rerank: bool = True,
|
|
170
|
+
**kwargs: Any,
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Perform RAG search: embed query + Qdrant search + context build.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
query: User query string.
|
|
177
|
+
collection_name: Collection to search.
|
|
178
|
+
limit: Number of chunks to retrieve.
|
|
179
|
+
rerank: Whether to rerank results using A3M.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dict with 'chunks', 'context', 'answer', 'provider'.
|
|
183
|
+
"""
|
|
184
|
+
collection = collection_name or self._collection_name
|
|
185
|
+
if not collection:
|
|
186
|
+
raise ValueError("collection_name is required")
|
|
187
|
+
|
|
188
|
+
# 1. Embed query via A3M
|
|
189
|
+
query_embedding = self.embed_query(query)
|
|
190
|
+
|
|
191
|
+
# 2. Search Qdrant
|
|
192
|
+
results = self.search(
|
|
193
|
+
collection_name=collection,
|
|
194
|
+
query_vector=query_embedding,
|
|
195
|
+
limit=limit,
|
|
196
|
+
with_payload=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# 3. Build context from results
|
|
200
|
+
chunks = []
|
|
201
|
+
for r in results:
|
|
202
|
+
payload = r.payload or {}
|
|
203
|
+
text = payload.get("text", payload.get("content", str(r.id)))
|
|
204
|
+
chunks.append({"id": r.id, "text": text, "score": r.score})
|
|
205
|
+
|
|
206
|
+
context = "\n\n".join([c["text"] for c in chunks])
|
|
207
|
+
|
|
208
|
+
# 4. Generate answer via A3M if reranking
|
|
209
|
+
answer = None
|
|
210
|
+
provider = None
|
|
211
|
+
if rerank and chunks:
|
|
212
|
+
try:
|
|
213
|
+
response = self._a3m_router.chat(
|
|
214
|
+
messages=[{
|
|
215
|
+
"role": "user",
|
|
216
|
+
"content": f"Context:\n{context}\n\nQuestion: {query}\n\nAnswer concisely:"
|
|
217
|
+
}],
|
|
218
|
+
model=self._embed_model,
|
|
219
|
+
)
|
|
220
|
+
answer = response.content
|
|
221
|
+
provider = response.provider
|
|
222
|
+
except A3MRouterError:
|
|
223
|
+
answer = chunks[0]["text"] if chunks else ""
|
|
224
|
+
provider = "qdrant"
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"chunks": chunks,
|
|
228
|
+
"context": context,
|
|
229
|
+
"answer": answer,
|
|
230
|
+
"provider": provider,
|
|
231
|
+
"query_embedding": query_embedding[:10], # First 10 dims for debug
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
else:
|
|
235
|
+
class QdrantAdapter:
|
|
236
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
237
|
+
raise ImportError(
|
|
238
|
+
"Qdrant client not installed. "
|
|
239
|
+
"Install with: pip install qdrant-client"
|
|
240
|
+
)
|