vecmindb 1.0.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.
- vecmindb/__init__.py +163 -0
- vecmindb/async_client.py +1074 -0
- vecmindb/auth.py +188 -0
- vecmindb/client.py +748 -0
- vecmindb/exceptions.py +135 -0
- vecmindb/integrations/__init__.py +1 -0
- vecmindb/integrations/langchain.py +172 -0
- vecmindb/integrations/llamaindex.py +156 -0
- vecmindb/mcp.py +515 -0
- vecmindb/mcp_server.py +138 -0
- vecmindb/memory.py +329 -0
- vecmindb/memory_plugin.py +254 -0
- vecmindb/models.py +469 -0
- vecmindb/py.typed +1 -0
- vecmindb/retry.py +179 -0
- vecmindb-1.0.0.dist-info/METADATA +143 -0
- vecmindb-1.0.0.dist-info/RECORD +19 -0
- vecmindb-1.0.0.dist-info/WHEEL +5 -0
- vecmindb-1.0.0.dist-info/top_level.txt +1 -0
vecmindb/async_client.py
ADDED
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
"""VecminDB Async Python SDK Client.
|
|
2
|
+
|
|
3
|
+
Provides :class:`AsyncVecminClient` – a fully-featured async HTTP client for
|
|
4
|
+
the VecminDB REST API. Supports connection pooling, automatic JWT refresh,
|
|
5
|
+
configurable retry with exponential back-off, and structured error handling.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
async with AsyncVecminClient("http://localhost:5520", api_key="xxx") as client:
|
|
10
|
+
await client.create_collection("docs", dimension=1536)
|
|
11
|
+
results = await client.search("docs", query=[0.1, 0.2, ...], top_k=10)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Union
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from .auth import AuthManager
|
|
23
|
+
from .exceptions import (
|
|
24
|
+
AuthenticationError,
|
|
25
|
+
ConnectionError,
|
|
26
|
+
TimeoutError,
|
|
27
|
+
VecminError,
|
|
28
|
+
exception_from_status,
|
|
29
|
+
)
|
|
30
|
+
from .models import (
|
|
31
|
+
BatchCreateVectorsRequest,
|
|
32
|
+
BatchDeleteVectorsRequest,
|
|
33
|
+
BatchInsertRequest,
|
|
34
|
+
ClusterJoinRequest,
|
|
35
|
+
ClusterLoginRequest,
|
|
36
|
+
ClusterLoginResponse,
|
|
37
|
+
ClusterNodeInfo,
|
|
38
|
+
ClusterPromoteRequest,
|
|
39
|
+
ClusterStatus,
|
|
40
|
+
CollectionInfo,
|
|
41
|
+
CollectionStats,
|
|
42
|
+
CreateCollectionRequest,
|
|
43
|
+
CreateVectorRequest,
|
|
44
|
+
GlobalStats,
|
|
45
|
+
HealthStatus,
|
|
46
|
+
IndexInfo,
|
|
47
|
+
InsertRequest,
|
|
48
|
+
SearchHit,
|
|
49
|
+
SearchRequest,
|
|
50
|
+
SearchResponse,
|
|
51
|
+
SnapshotInfo,
|
|
52
|
+
SubsystemStatus,
|
|
53
|
+
VecminResponse,
|
|
54
|
+
)
|
|
55
|
+
from .retry import RetryConfig, retry_async
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger("vecmindb.async_client")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AsyncVecminClient:
|
|
61
|
+
"""Asynchronous client for the VecminDB REST API.
|
|
62
|
+
|
|
63
|
+
This client wraps every endpoint exposed by the VecminDB server and
|
|
64
|
+
handles authentication, retries, and response parsing transparently.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
base_url: Root URL of the VecminDB server (e.g. ``http://localhost:5520``).
|
|
68
|
+
api_key: Long-lived API key for authentication.
|
|
69
|
+
jwt_token: Pre-obtained JWT bearer token.
|
|
70
|
+
admin_password: Admin password – required for JWT auto-refresh.
|
|
71
|
+
connect_timeout: TCP connection timeout in seconds.
|
|
72
|
+
read_timeout: Socket read timeout in seconds.
|
|
73
|
+
write_timeout: Socket write timeout in seconds.
|
|
74
|
+
max_connections: Maximum number of connections in the pool.
|
|
75
|
+
max_keepalive_connections: Maximum number of idle keep-alive connections.
|
|
76
|
+
retry_config: Retry policy configuration. Defaults to 3 retries with
|
|
77
|
+
0.5 s back-off factor.
|
|
78
|
+
|
|
79
|
+
Example::
|
|
80
|
+
|
|
81
|
+
async with AsyncVecminClient("http://localhost:5520", api_key="key") as c:
|
|
82
|
+
collections = await c.list_collections()
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
base_url: str = "http://localhost:5520",
|
|
88
|
+
*,
|
|
89
|
+
api_key: Optional[str] = None,
|
|
90
|
+
jwt_token: Optional[str] = None,
|
|
91
|
+
admin_password: Optional[str] = None,
|
|
92
|
+
agent_id: Optional[str] = None,
|
|
93
|
+
sovereignty_token: Optional[str] = None,
|
|
94
|
+
connect_timeout: float = 5.0,
|
|
95
|
+
read_timeout: float = 30.0,
|
|
96
|
+
write_timeout: float = 30.0,
|
|
97
|
+
max_connections: int = 100,
|
|
98
|
+
max_keepalive_connections: int = 20,
|
|
99
|
+
retry_config: Optional[RetryConfig] = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self._base_url = base_url.rstrip("/")
|
|
102
|
+
self._api_url = f"{self._base_url}/api/v1"
|
|
103
|
+
self._retry_config = retry_config or RetryConfig()
|
|
104
|
+
self.agent_id = agent_id or "default_agent"
|
|
105
|
+
self.sovereignty_token = sovereignty_token or "system"
|
|
106
|
+
self._auth = AuthManager(
|
|
107
|
+
api_key=api_key,
|
|
108
|
+
jwt_token=jwt_token,
|
|
109
|
+
admin_password=admin_password,
|
|
110
|
+
)
|
|
111
|
+
# Register the login function for JWT auto-refresh.
|
|
112
|
+
self._auth.set_login_fn(self._login_internal)
|
|
113
|
+
|
|
114
|
+
timeout = httpx.Timeout(
|
|
115
|
+
connect=connect_timeout,
|
|
116
|
+
read=read_timeout,
|
|
117
|
+
write=write_timeout,
|
|
118
|
+
pool=connect_timeout,
|
|
119
|
+
)
|
|
120
|
+
limits = httpx.Limits(
|
|
121
|
+
max_connections=max_connections,
|
|
122
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
123
|
+
)
|
|
124
|
+
self._client = httpx.AsyncClient(
|
|
125
|
+
base_url=self._base_url,
|
|
126
|
+
timeout=timeout,
|
|
127
|
+
limits=limits,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Context manager
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async def __aenter__(self) -> "AsyncVecminClient": # noqa: D105
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: D105
|
|
138
|
+
await self.close()
|
|
139
|
+
|
|
140
|
+
async def close(self) -> None:
|
|
141
|
+
"""Gracefully close the underlying HTTP connection pool."""
|
|
142
|
+
await self._client.aclose()
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# Internal helpers
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
def _build_headers(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
agent_id: Optional[str] = None,
|
|
151
|
+
model_id: Optional[str] = None,
|
|
152
|
+
request_id: Optional[str] = None,
|
|
153
|
+
) -> Dict[str, str]:
|
|
154
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
155
|
+
effective_agent_id = agent_id or self.agent_id
|
|
156
|
+
effective_model_id = model_id or self.sovereignty_token
|
|
157
|
+
headers.update(self._auth.auth_headers())
|
|
158
|
+
headers.update(self._auth.extra_headers(agent_id=effective_agent_id, model_id=effective_model_id, request_id=request_id))
|
|
159
|
+
return headers
|
|
160
|
+
|
|
161
|
+
async def _build_headers_async(self, **kwargs) -> Dict[str, str]:
|
|
162
|
+
"""Build headers, refreshing JWT asynchronously if needed."""
|
|
163
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
164
|
+
# Refresh JWT asynchronously
|
|
165
|
+
jwt = await self._auth.async_maybe_refresh_jwt()
|
|
166
|
+
if self._auth.api_key:
|
|
167
|
+
headers["x-api-key"] = self._auth.api_key
|
|
168
|
+
if jwt:
|
|
169
|
+
headers["Authorization"] = f"Bearer {jwt}"
|
|
170
|
+
effective_agent_id = kwargs.get("agent_id") or self.agent_id
|
|
171
|
+
effective_model_id = kwargs.get("model_id") or self.sovereignty_token
|
|
172
|
+
headers.update(self._auth.extra_headers(
|
|
173
|
+
agent_id=effective_agent_id,
|
|
174
|
+
model_id=effective_model_id,
|
|
175
|
+
request_id=kwargs.get("request_id"),
|
|
176
|
+
))
|
|
177
|
+
return headers
|
|
178
|
+
|
|
179
|
+
async def _request(
|
|
180
|
+
self,
|
|
181
|
+
method: str,
|
|
182
|
+
path: str,
|
|
183
|
+
*,
|
|
184
|
+
json_body: Optional[Any] = None,
|
|
185
|
+
params: Optional[Dict[str, Any]] = None,
|
|
186
|
+
**header_kwargs,
|
|
187
|
+
) -> Any:
|
|
188
|
+
"""Execute an HTTP request with retry and error handling.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
method: HTTP method (GET, POST, DELETE, etc.).
|
|
192
|
+
path: URL path relative to the server root.
|
|
193
|
+
json_body: Optional JSON-serialisable request body.
|
|
194
|
+
params: Optional query-string parameters.
|
|
195
|
+
**header_kwargs: Passed through to :meth:`_build_headers_async`.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Parsed JSON response body on success.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
VecminError: On any API-level error.
|
|
202
|
+
ConnectionError: On network failure.
|
|
203
|
+
TimeoutError: On request timeout.
|
|
204
|
+
"""
|
|
205
|
+
headers = await self._build_headers_async(**header_kwargs)
|
|
206
|
+
|
|
207
|
+
async def _do_request() -> Any:
|
|
208
|
+
try:
|
|
209
|
+
response = await self._client.request(
|
|
210
|
+
method,
|
|
211
|
+
path,
|
|
212
|
+
json=json_body,
|
|
213
|
+
params=params,
|
|
214
|
+
headers=headers,
|
|
215
|
+
)
|
|
216
|
+
except httpx.ConnectError as exc:
|
|
217
|
+
raise ConnectionError(f"Cannot connect to {self._base_url}: {exc}") from exc
|
|
218
|
+
except httpx.TimeoutException as exc:
|
|
219
|
+
raise TimeoutError(f"Request to {path} timed out: {exc}") from exc
|
|
220
|
+
except httpx.HTTPError as exc:
|
|
221
|
+
raise ConnectionError(f"HTTP error on {path}: {exc}") from exc
|
|
222
|
+
|
|
223
|
+
# Successful response
|
|
224
|
+
if 200 <= response.status_code < 300:
|
|
225
|
+
try:
|
|
226
|
+
return response.json()
|
|
227
|
+
except json.JSONDecodeError:
|
|
228
|
+
# Some endpoints (e.g. /metrics) return plain text.
|
|
229
|
+
return {"code": response.status_code, "message": "Success", "data": response.text}
|
|
230
|
+
|
|
231
|
+
# Error response – attempt to parse the standard envelope.
|
|
232
|
+
try:
|
|
233
|
+
body = response.json()
|
|
234
|
+
msg = body.get("message", response.text)
|
|
235
|
+
code = body.get("code", response.status_code)
|
|
236
|
+
except json.JSONDecodeError:
|
|
237
|
+
msg = response.text
|
|
238
|
+
code = response.status_code
|
|
239
|
+
|
|
240
|
+
raise exception_from_status(response.status_code, message=msg, code=code)
|
|
241
|
+
|
|
242
|
+
return await retry_async(_do_request, self._retry_config)
|
|
243
|
+
|
|
244
|
+
async def _api_get(self, path: str, *, params: Optional[Dict[str, Any]] = None, **kw) -> Any:
|
|
245
|
+
return await self._request("GET", f"{self._api_url}{path}", params=params, **kw)
|
|
246
|
+
|
|
247
|
+
async def _api_post(self, path: str, body: Optional[Any] = None, **kw) -> Any:
|
|
248
|
+
return await self._request("POST", f"{self._api_url}{path}", json_body=body, **kw)
|
|
249
|
+
|
|
250
|
+
async def _api_delete(self, path: str, **kw) -> Any:
|
|
251
|
+
return await self._request("DELETE", f"{self._api_url}{path}", **kw)
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# JWT login (internal)
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
async def _login_internal(self, password: str) -> tuple[str, int]:
|
|
258
|
+
"""Authenticate against the cluster login endpoint.
|
|
259
|
+
|
|
260
|
+
This method is **not** meant for direct use – it is wired into the
|
|
261
|
+
:class:`AuthManager` for automatic JWT refresh. Users should call
|
|
262
|
+
:meth:`cluster_login` instead.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
password: Admin password.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
``(token, expires_in)`` tuple.
|
|
269
|
+
"""
|
|
270
|
+
# Use a direct request without auth headers to avoid circular refresh.
|
|
271
|
+
payload = ClusterLoginRequest(password=password)
|
|
272
|
+
resp = await self._client.post(
|
|
273
|
+
f"{self._api_url}/cluster/login",
|
|
274
|
+
json=payload.model_dump(),
|
|
275
|
+
headers={"Content-Type": "application/json"},
|
|
276
|
+
)
|
|
277
|
+
if resp.status_code != 200:
|
|
278
|
+
raise AuthenticationError(f"Login failed: {resp.text}")
|
|
279
|
+
data = resp.json()
|
|
280
|
+
login = ClusterLoginResponse(**data.get("data", data))
|
|
281
|
+
return login.token, login.expires_in
|
|
282
|
+
|
|
283
|
+
# ==================================================================
|
|
284
|
+
# Health & System
|
|
285
|
+
# ==================================================================
|
|
286
|
+
|
|
287
|
+
async def health_check(self, **kw) -> HealthStatus:
|
|
288
|
+
"""Check server health (``GET /api/v1/health``).
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Server health status.
|
|
292
|
+
"""
|
|
293
|
+
data = await self._api_get("/health", **kw)
|
|
294
|
+
return HealthStatus(**data.get("data", data))
|
|
295
|
+
|
|
296
|
+
async def status_check(self, **kw) -> SubsystemStatus:
|
|
297
|
+
"""Retrieve detailed subsystem status (``GET /api/v1/status``).
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Detailed subsystem status.
|
|
301
|
+
"""
|
|
302
|
+
data = await self._api_get("/status", **kw)
|
|
303
|
+
return SubsystemStatus(**data.get("data", data))
|
|
304
|
+
|
|
305
|
+
async def liveness(self) -> bool:
|
|
306
|
+
"""Kubernetes liveness probe (``GET /healthz/live``).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
``True`` if the server process is alive.
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
resp = await self._client.get(f"{self._base_url}/healthz/live")
|
|
313
|
+
return resp.status_code == 200
|
|
314
|
+
except Exception:
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
async def readiness(self) -> bool:
|
|
318
|
+
"""Kubernetes readiness probe (``GET /healthz/ready``).
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
``True`` if the server is ready to accept traffic.
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
resp = await self._client.get(f"{self._base_url}/healthz/ready")
|
|
325
|
+
return resp.status_code == 200
|
|
326
|
+
except Exception:
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
async def metrics(self) -> str:
|
|
330
|
+
"""Fetch Prometheus metrics (``GET /metrics``).
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Plain-text Prometheus exposition.
|
|
334
|
+
"""
|
|
335
|
+
data = await self._request("GET", "/metrics")
|
|
336
|
+
return data.get("data", str(data))
|
|
337
|
+
|
|
338
|
+
# ==================================================================
|
|
339
|
+
# Collection Management
|
|
340
|
+
# ==================================================================
|
|
341
|
+
|
|
342
|
+
async def create_collection(
|
|
343
|
+
self,
|
|
344
|
+
name: str,
|
|
345
|
+
*,
|
|
346
|
+
dimension: int = 1536,
|
|
347
|
+
metric_type: str = "Cosine",
|
|
348
|
+
index_type: str = "HNSW",
|
|
349
|
+
index_params: Optional[Dict[str, Any]] = None,
|
|
350
|
+
**kw,
|
|
351
|
+
) -> CollectionInfo:
|
|
352
|
+
"""Create a new collection (``POST /api/v1/collections``).
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
name: Unique collection identifier.
|
|
356
|
+
dimension: Vector dimensionality.
|
|
357
|
+
metric_type: Distance metric.
|
|
358
|
+
index_type: Index algorithm.
|
|
359
|
+
index_params: Algorithm-specific parameters.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Created collection metadata.
|
|
363
|
+
"""
|
|
364
|
+
payload = CreateCollectionRequest(
|
|
365
|
+
name=name,
|
|
366
|
+
dimension=dimension,
|
|
367
|
+
metric_type=metric_type,
|
|
368
|
+
index_type=index_type,
|
|
369
|
+
index_params=index_params,
|
|
370
|
+
)
|
|
371
|
+
data = await self._api_post("/collections", payload.model_dump(exclude_none=True), **kw)
|
|
372
|
+
return CollectionInfo(**data.get("data", data))
|
|
373
|
+
|
|
374
|
+
async def list_collections(self, **kw) -> List[CollectionInfo]:
|
|
375
|
+
"""List all collections (``GET /api/v1/collections``).
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of collection metadata objects.
|
|
379
|
+
"""
|
|
380
|
+
data = await self._api_get("/collections", **kw)
|
|
381
|
+
items = data.get("data", data)
|
|
382
|
+
if isinstance(items, list):
|
|
383
|
+
return [CollectionInfo(**c) for c in items]
|
|
384
|
+
return [CollectionInfo(**items)]
|
|
385
|
+
|
|
386
|
+
async def get_collection(self, name: str, **kw) -> CollectionInfo:
|
|
387
|
+
"""Get collection details (``GET /api/v1/collections/{name}``).
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
name: Collection identifier.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Collection metadata.
|
|
394
|
+
"""
|
|
395
|
+
data = await self._api_get(f"/collections/{name}", **kw)
|
|
396
|
+
return CollectionInfo(**data.get("data", data))
|
|
397
|
+
|
|
398
|
+
async def delete_collection(self, name: str, **kw) -> VecminResponse:
|
|
399
|
+
"""Delete a collection (``DELETE /api/v1/collections/{name}``).
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
name: Collection identifier.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Deletion confirmation.
|
|
406
|
+
"""
|
|
407
|
+
data = await self._api_delete(f"/collections/{name}", **kw)
|
|
408
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
409
|
+
|
|
410
|
+
async def get_collection_stats(self, name: str, **kw) -> CollectionStats:
|
|
411
|
+
"""Get collection statistics (``GET /api/v1/collections/{name}/stats``).
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
name: Collection identifier.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Statistical summary.
|
|
418
|
+
"""
|
|
419
|
+
data = await self._api_get(f"/collections/{name}/stats", **kw)
|
|
420
|
+
return CollectionStats(**data.get("data", data))
|
|
421
|
+
|
|
422
|
+
async def rebuild_collection_index(self, name: str, **kw) -> VecminResponse:
|
|
423
|
+
"""Rebuild the index for a collection (``POST /api/v1/collections/{name}/rebuild``).
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
name: Collection identifier.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Operation acknowledgement.
|
|
430
|
+
"""
|
|
431
|
+
data = await self._api_post(f"/collections/{name}/rebuild", **kw)
|
|
432
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
433
|
+
|
|
434
|
+
# ==================================================================
|
|
435
|
+
# Collection-level Vector Operations
|
|
436
|
+
# ==================================================================
|
|
437
|
+
|
|
438
|
+
async def insert(
|
|
439
|
+
self,
|
|
440
|
+
collection: str,
|
|
441
|
+
*,
|
|
442
|
+
vector: List[float],
|
|
443
|
+
id: Optional[str] = None,
|
|
444
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
445
|
+
**kw,
|
|
446
|
+
) -> str:
|
|
447
|
+
"""Insert a single vector into a collection.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
collection: Target collection name.
|
|
451
|
+
vector: Embedding values.
|
|
452
|
+
id: Optional client-assigned vector identifier.
|
|
453
|
+
metadata: Optional key-value metadata.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
The vector identifier.
|
|
457
|
+
"""
|
|
458
|
+
payload = InsertRequest(id=id, values=vector, metadata=metadata)
|
|
459
|
+
data = await self._api_post(
|
|
460
|
+
f"/collections/{collection}/vectors",
|
|
461
|
+
payload.model_dump(exclude_none=True),
|
|
462
|
+
**kw,
|
|
463
|
+
)
|
|
464
|
+
result = data.get("data", data)
|
|
465
|
+
if isinstance(result, dict):
|
|
466
|
+
return result.get("id", result.get("data", str(result)))
|
|
467
|
+
return str(result)
|
|
468
|
+
|
|
469
|
+
async def batch_insert(
|
|
470
|
+
self,
|
|
471
|
+
collection: str,
|
|
472
|
+
*,
|
|
473
|
+
vectors: List[Dict[str, Any]],
|
|
474
|
+
**kw,
|
|
475
|
+
) -> List[str]:
|
|
476
|
+
"""Batch insert vectors into a collection (``POST /api/v1/collections/{name}/batch``).
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
collection: Target collection name.
|
|
480
|
+
vectors: List of dicts with keys ``values``, ``id``, ``metadata``.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
List of inserted vector identifiers.
|
|
484
|
+
"""
|
|
485
|
+
items = [InsertRequest(**v) for v in vectors]
|
|
486
|
+
payload = BatchInsertRequest(vectors=items)
|
|
487
|
+
data = await self._api_post(
|
|
488
|
+
f"/collections/{collection}/batch",
|
|
489
|
+
payload.model_dump(exclude_none=True),
|
|
490
|
+
**kw,
|
|
491
|
+
)
|
|
492
|
+
result = data.get("data", data)
|
|
493
|
+
if isinstance(result, list):
|
|
494
|
+
return [r.get("id", str(r)) if isinstance(r, dict) else str(r) for r in result]
|
|
495
|
+
return [str(result)]
|
|
496
|
+
|
|
497
|
+
async def search(
|
|
498
|
+
self,
|
|
499
|
+
collection: str,
|
|
500
|
+
*,
|
|
501
|
+
query: List[float],
|
|
502
|
+
top_k: int = 10,
|
|
503
|
+
ef_search: int = 50,
|
|
504
|
+
metric: Optional[str] = None,
|
|
505
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
506
|
+
**kw,
|
|
507
|
+
) -> SearchResponse:
|
|
508
|
+
"""Search a collection for nearest neighbours (``POST /api/v1/collections/{name}/search``).
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
collection: Collection to search.
|
|
512
|
+
query: Query embedding.
|
|
513
|
+
top_k: Number of results.
|
|
514
|
+
ef_search: HNSW search-width parameter.
|
|
515
|
+
metric: Override distance metric.
|
|
516
|
+
filter: Metadata filter expression.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Search results.
|
|
520
|
+
"""
|
|
521
|
+
payload = SearchRequest(query=query, k=top_k, ef_search=ef_search, metric=metric, filter=filter)
|
|
522
|
+
data = await self._api_post(
|
|
523
|
+
f"/collections/{collection}/search",
|
|
524
|
+
payload.model_dump(exclude_none=True),
|
|
525
|
+
**kw,
|
|
526
|
+
)
|
|
527
|
+
raw = data.get("data", data)
|
|
528
|
+
if isinstance(raw, list):
|
|
529
|
+
return SearchResponse(results=[SearchHit(**h) if isinstance(h, dict) else SearchHit(id=str(h)) for h in raw])
|
|
530
|
+
if isinstance(raw, dict):
|
|
531
|
+
hits = raw.get("results", raw.get("hits", []))
|
|
532
|
+
return SearchResponse(
|
|
533
|
+
results=[SearchHit(**h) for h in hits],
|
|
534
|
+
total=raw.get("total", len(hits)),
|
|
535
|
+
)
|
|
536
|
+
return SearchResponse()
|
|
537
|
+
|
|
538
|
+
async def get_vector(self, collection: str, id: str, **kw) -> Dict[str, Any]:
|
|
539
|
+
"""Retrieve a vector by ID (``GET /api/v1/collections/{name}/vectors/{id}``).
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
collection: Collection name.
|
|
543
|
+
id: Vector identifier.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Vector data including values and metadata.
|
|
547
|
+
"""
|
|
548
|
+
data = await self._api_get(f"/collections/{collection}/vectors/{id}", **kw)
|
|
549
|
+
return data.get("data", data)
|
|
550
|
+
|
|
551
|
+
async def delete_vector(self, collection: str, id: str, **kw) -> VecminResponse:
|
|
552
|
+
"""Delete a vector by ID (``DELETE /api/v1/collections/{name}/vectors/{id}``).
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
collection: Collection name.
|
|
556
|
+
id: Vector identifier.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Deletion confirmation.
|
|
560
|
+
"""
|
|
561
|
+
data = await self._api_delete(f"/collections/{collection}/vectors/{id}", **kw)
|
|
562
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
563
|
+
|
|
564
|
+
# ==================================================================
|
|
565
|
+
# Global Vector Operations
|
|
566
|
+
# ==================================================================
|
|
567
|
+
|
|
568
|
+
async def create_vector(
|
|
569
|
+
self,
|
|
570
|
+
*,
|
|
571
|
+
values: List[float],
|
|
572
|
+
id: Optional[str] = None,
|
|
573
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
574
|
+
collection: Optional[str] = None,
|
|
575
|
+
**kw,
|
|
576
|
+
) -> str:
|
|
577
|
+
"""Create a vector in the global namespace (``POST /api/v1/vectors``).
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
values: Embedding values.
|
|
581
|
+
id: Optional vector identifier.
|
|
582
|
+
metadata: Optional key-value metadata.
|
|
583
|
+
collection: Optional target collection.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Vector identifier.
|
|
587
|
+
"""
|
|
588
|
+
payload = CreateVectorRequest(id=id, values=values, metadata=metadata, collection=collection)
|
|
589
|
+
data = await self._api_post("/vectors", payload.model_dump(exclude_none=True), **kw)
|
|
590
|
+
result = data.get("data", data)
|
|
591
|
+
if isinstance(result, dict):
|
|
592
|
+
return result.get("id", str(result))
|
|
593
|
+
return str(result)
|
|
594
|
+
|
|
595
|
+
async def batch_create_vectors(
|
|
596
|
+
self,
|
|
597
|
+
*,
|
|
598
|
+
vectors: List[Dict[str, Any]],
|
|
599
|
+
**kw,
|
|
600
|
+
) -> List[str]:
|
|
601
|
+
"""Batch create vectors (``POST /api/v1/vectors/batch``).
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
vectors: List of vector dicts.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
List of created vector identifiers.
|
|
608
|
+
"""
|
|
609
|
+
items = [CreateVectorRequest(**v) for v in vectors]
|
|
610
|
+
payload = BatchCreateVectorsRequest(vectors=items)
|
|
611
|
+
data = await self._api_post("/vectors/batch", payload.model_dump(exclude_none=True), **kw)
|
|
612
|
+
result = data.get("data", data)
|
|
613
|
+
if isinstance(result, list):
|
|
614
|
+
return [r.get("id", str(r)) if isinstance(r, dict) else str(r) for r in result]
|
|
615
|
+
return [str(result)]
|
|
616
|
+
|
|
617
|
+
async def get_global_vector(self, id: str, **kw) -> Dict[str, Any]:
|
|
618
|
+
"""Get a global vector by ID (``GET /api/v1/vectors/{id}``).
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
id: Vector identifier.
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
Vector data.
|
|
625
|
+
"""
|
|
626
|
+
data = await self._api_get(f"/vectors/{id}", **kw)
|
|
627
|
+
return data.get("data", data)
|
|
628
|
+
|
|
629
|
+
async def delete_global_vector(self, id: str, **kw) -> VecminResponse:
|
|
630
|
+
"""Delete a global vector (``DELETE /api/v1/vectors/{id}``).
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
id: Vector identifier.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Deletion confirmation.
|
|
637
|
+
"""
|
|
638
|
+
data = await self._api_delete(f"/vectors/{id}", **kw)
|
|
639
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
640
|
+
|
|
641
|
+
async def batch_delete_vectors(self, *, ids: List[str], **kw) -> VecminResponse:
|
|
642
|
+
"""Batch delete vectors (``POST /api/v1/vectors/batch/delete``).
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
ids: List of vector identifiers to delete.
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Deletion confirmation.
|
|
649
|
+
"""
|
|
650
|
+
payload = BatchDeleteVectorsRequest(ids=ids)
|
|
651
|
+
data = await self._api_post("/vectors/batch/delete", payload.model_dump(), **kw)
|
|
652
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
653
|
+
|
|
654
|
+
async def search_vectors(
|
|
655
|
+
self,
|
|
656
|
+
*,
|
|
657
|
+
query: List[float],
|
|
658
|
+
top_k: int = 10,
|
|
659
|
+
ef_search: int = 50,
|
|
660
|
+
metric: Optional[str] = None,
|
|
661
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
662
|
+
**kw,
|
|
663
|
+
) -> SearchResponse:
|
|
664
|
+
"""Global vector search (``POST /api/v1/vectors/search``).
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
query: Query embedding.
|
|
668
|
+
top_k: Number of results.
|
|
669
|
+
ef_search: HNSW search width.
|
|
670
|
+
metric: Override distance metric.
|
|
671
|
+
filter: Metadata filter.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Search results.
|
|
675
|
+
"""
|
|
676
|
+
payload = SearchRequest(query=query, k=top_k, ef_search=ef_search, metric=metric, filter=filter)
|
|
677
|
+
data = await self._api_post("/vectors/search", payload.model_dump(exclude_none=True), **kw)
|
|
678
|
+
raw = data.get("data", data)
|
|
679
|
+
if isinstance(raw, list):
|
|
680
|
+
return SearchResponse(results=[SearchHit(**h) for h in raw if isinstance(h, dict)])
|
|
681
|
+
if isinstance(raw, dict):
|
|
682
|
+
hits = raw.get("results", [])
|
|
683
|
+
return SearchResponse(results=[SearchHit(**h) for h in hits], total=raw.get("total", len(hits)))
|
|
684
|
+
return SearchResponse()
|
|
685
|
+
|
|
686
|
+
async def count_vectors(self, **kw) -> int:
|
|
687
|
+
"""Count all vectors (``GET /api/v1/vectors/count``).
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Total number of vectors.
|
|
691
|
+
"""
|
|
692
|
+
data = await self._api_get("/vectors/count", **kw)
|
|
693
|
+
result = data.get("data", data)
|
|
694
|
+
if isinstance(result, dict):
|
|
695
|
+
return result.get("count", 0)
|
|
696
|
+
return int(result) if result else 0
|
|
697
|
+
|
|
698
|
+
async def rebuild_global_index(self, **kw) -> VecminResponse:
|
|
699
|
+
"""Rebuild global index (``POST /api/v1/vectors/rebuild-index``).
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
Operation acknowledgement.
|
|
703
|
+
"""
|
|
704
|
+
data = await self._api_post("/vectors/rebuild-index", **kw)
|
|
705
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
706
|
+
|
|
707
|
+
# ==================================================================
|
|
708
|
+
# Index Management
|
|
709
|
+
# ==================================================================
|
|
710
|
+
|
|
711
|
+
async def list_indexes(self, **kw) -> List[IndexInfo]:
|
|
712
|
+
"""List all indexes (``GET /index``).
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
List of index information objects.
|
|
716
|
+
"""
|
|
717
|
+
data = await self._request("GET", "/index", **kw)
|
|
718
|
+
items = data.get("data", data)
|
|
719
|
+
if isinstance(items, list):
|
|
720
|
+
return [IndexInfo(**idx) for idx in items]
|
|
721
|
+
return [IndexInfo(**items)] if isinstance(items, dict) else []
|
|
722
|
+
|
|
723
|
+
async def get_index_info(self, collection_name: str, **kw) -> IndexInfo:
|
|
724
|
+
"""Get index information (``GET /index/{collection_name}``).
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
collection_name: Collection name.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
Index information.
|
|
731
|
+
"""
|
|
732
|
+
data = await self._request("GET", f"/index/{collection_name}", **kw)
|
|
733
|
+
return IndexInfo(**data.get("data", data))
|
|
734
|
+
|
|
735
|
+
async def rebuild_index(self, collection_name: str, **kw) -> VecminResponse:
|
|
736
|
+
"""Rebuild an index (``POST /index/{collection_name}/rebuild``).
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
collection_name: Collection name.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
Operation acknowledgement.
|
|
743
|
+
"""
|
|
744
|
+
data = await self._request("POST", f"/index/{collection_name}/rebuild", **kw)
|
|
745
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
746
|
+
|
|
747
|
+
async def optimize_index(self, collection_name: str, **kw) -> VecminResponse:
|
|
748
|
+
"""Optimize an index (``POST /index/{collection_name}/optimize``).
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
collection_name: Collection name.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Operation acknowledgement.
|
|
755
|
+
"""
|
|
756
|
+
data = await self._request("POST", f"/index/{collection_name}/optimize", **kw)
|
|
757
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
758
|
+
|
|
759
|
+
async def get_shadow_trajectory(self, collection_name: str, **kw) -> List[Dict[str, Any]]:
|
|
760
|
+
"""Get shadow-index Nash trajectory (``GET /index/{collection_name}/shadow/trajectory``).
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
collection_name: Collection name.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
List of trajectory data points.
|
|
767
|
+
"""
|
|
768
|
+
data = await self._request("GET", f"/index/{collection_name}/shadow/trajectory", **kw)
|
|
769
|
+
items = data.get("data", data)
|
|
770
|
+
if isinstance(items, list):
|
|
771
|
+
return items
|
|
772
|
+
return [items] if isinstance(items, dict) else []
|
|
773
|
+
|
|
774
|
+
async def promote_shadow(self, collection_name: str, **kw) -> VecminResponse:
|
|
775
|
+
"""Promote shadow index to primary (``POST /index/{collection_name}/shadow/promote``).
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
collection_name: Collection name.
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
Promotion acknowledgement.
|
|
782
|
+
"""
|
|
783
|
+
data = await self._request("POST", f"/index/{collection_name}/shadow/promote", **kw)
|
|
784
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
785
|
+
|
|
786
|
+
# ==================================================================
|
|
787
|
+
# Stats
|
|
788
|
+
# ==================================================================
|
|
789
|
+
|
|
790
|
+
async def global_stats(self, **kw) -> GlobalStats:
|
|
791
|
+
"""Fetch global database statistics (``GET /stats``).
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
Aggregate statistics.
|
|
795
|
+
"""
|
|
796
|
+
data = await self._request("GET", "/stats", **kw)
|
|
797
|
+
return GlobalStats(**data.get("data", data))
|
|
798
|
+
|
|
799
|
+
async def index_stats(self, **kw) -> Dict[str, Any]:
|
|
800
|
+
"""Fetch index statistics (``GET /stats/index``).
|
|
801
|
+
|
|
802
|
+
Returns:
|
|
803
|
+
Index statistics.
|
|
804
|
+
"""
|
|
805
|
+
data = await self._request("GET", "/stats/index", **kw)
|
|
806
|
+
return data.get("data", data)
|
|
807
|
+
|
|
808
|
+
async def performance_stats(self, **kw) -> Dict[str, Any]:
|
|
809
|
+
"""Fetch performance statistics (``GET /stats/performance``).
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
Performance statistics.
|
|
813
|
+
"""
|
|
814
|
+
data = await self._request("GET", "/stats/performance", **kw)
|
|
815
|
+
return data.get("data", data)
|
|
816
|
+
|
|
817
|
+
# ==================================================================
|
|
818
|
+
# Cluster Management
|
|
819
|
+
# ==================================================================
|
|
820
|
+
|
|
821
|
+
async def cluster_login(self, password: str, **kw) -> ClusterLoginResponse:
|
|
822
|
+
"""Authenticate to the cluster and obtain a JWT (``POST /api/v1/cluster/login``).
|
|
823
|
+
|
|
824
|
+
The obtained token is automatically stored in the auth manager for
|
|
825
|
+
subsequent requests and auto-refresh.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
password: Admin password.
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
Login response containing the JWT token.
|
|
832
|
+
"""
|
|
833
|
+
payload = ClusterLoginRequest(password=password)
|
|
834
|
+
data = await self._api_post("/cluster/login", payload.model_dump(), **kw)
|
|
835
|
+
result = data.get("data", data)
|
|
836
|
+
login_resp = ClusterLoginResponse(**result)
|
|
837
|
+
self._auth.update_jwt(login_resp.token, login_resp.expires_in)
|
|
838
|
+
return login_resp
|
|
839
|
+
|
|
840
|
+
async def cluster_join(self, node_id: str, addr: str, **kw) -> VecminResponse:
|
|
841
|
+
"""Join a cluster (``POST /api/v1/cluster/join``).
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
node_id: Unique node identifier.
|
|
845
|
+
addr: Network address of the joining node.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Join acknowledgement.
|
|
849
|
+
"""
|
|
850
|
+
payload = ClusterJoinRequest(node_id=node_id, addr=addr)
|
|
851
|
+
data = await self._api_post("/cluster/join", payload.model_dump(), **kw)
|
|
852
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
853
|
+
|
|
854
|
+
async def cluster_promote(self, node_id: str, **kw) -> VecminResponse:
|
|
855
|
+
"""Promote a node (``POST /api/v1/cluster/promote``).
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
node_id: Node to promote.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
Promotion acknowledgement.
|
|
862
|
+
"""
|
|
863
|
+
payload = ClusterPromoteRequest(node_id=node_id)
|
|
864
|
+
data = await self._api_post("/cluster/promote", payload.model_dump(), **kw)
|
|
865
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
866
|
+
|
|
867
|
+
async def list_nodes(self, **kw) -> List[ClusterNodeInfo]:
|
|
868
|
+
"""List cluster nodes.
|
|
869
|
+
|
|
870
|
+
The server exposes node management via POST (add) and DELETE (remove).
|
|
871
|
+
This method calls ``GET /api/v1/cluster/status`` and extracts the
|
|
872
|
+
node list.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
List of cluster node descriptors.
|
|
876
|
+
"""
|
|
877
|
+
status = await self.cluster_status(**kw)
|
|
878
|
+
return status.nodes
|
|
879
|
+
|
|
880
|
+
async def cluster_status(self, **kw) -> ClusterStatus:
|
|
881
|
+
"""Get cluster status (``GET /api/v1/cluster/status``).
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Cluster status including node list.
|
|
885
|
+
"""
|
|
886
|
+
data = await self._api_get("/cluster/status", **kw)
|
|
887
|
+
return ClusterStatus(**data.get("data", data))
|
|
888
|
+
|
|
889
|
+
async def add_node(self, node_id: str, addr: str, **kw) -> VecminResponse:
|
|
890
|
+
"""Add a node to the cluster (``POST /api/v1/cluster/nodes``).
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
node_id: New node identifier.
|
|
894
|
+
addr: Network address.
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
Addition acknowledgement.
|
|
898
|
+
"""
|
|
899
|
+
data = await self._api_post("/cluster/nodes", {"node_id": node_id, "addr": addr}, **kw)
|
|
900
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
901
|
+
|
|
902
|
+
async def remove_node(self, node_id: str, **kw) -> VecminResponse:
|
|
903
|
+
"""Remove a node from the cluster (``DELETE /api/v1/cluster/nodes``).
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
node_id: Node identifier to remove.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
Removal acknowledgement.
|
|
910
|
+
"""
|
|
911
|
+
data = await self._api_delete(f"/cluster/nodes?node_id={node_id}", **kw)
|
|
912
|
+
return VecminResponse(**data) if isinstance(data, dict) else VecminResponse(data=data)
|
|
913
|
+
|
|
914
|
+
async def get_join_info(self, **kw) -> Dict[str, Any]:
|
|
915
|
+
"""Get cluster join information (``GET /api/v1/cluster/join_info``).
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
Join information.
|
|
919
|
+
"""
|
|
920
|
+
data = await self._api_get("/cluster/join_info", **kw)
|
|
921
|
+
return data.get("data", data)
|
|
922
|
+
|
|
923
|
+
# ==================================================================
|
|
924
|
+
# MCP Integration
|
|
925
|
+
# ==================================================================
|
|
926
|
+
|
|
927
|
+
async def mcp_store_memory(
|
|
928
|
+
self,
|
|
929
|
+
content: str,
|
|
930
|
+
*,
|
|
931
|
+
agent_id: Optional[str] = None,
|
|
932
|
+
sovereignty_token: Optional[str] = None,
|
|
933
|
+
model_id: Optional[str] = None,
|
|
934
|
+
source: str = "sdk",
|
|
935
|
+
is_factual: bool = False,
|
|
936
|
+
**kw,
|
|
937
|
+
) -> str:
|
|
938
|
+
"""Store a memory via the MCP ``store_memory`` tool."""
|
|
939
|
+
effective_agent_id = agent_id or self.agent_id
|
|
940
|
+
effective_token = sovereignty_token or self.sovereignty_token
|
|
941
|
+
effective_model_id = model_id or effective_token
|
|
942
|
+
payload = {
|
|
943
|
+
"jsonrpc": "2.0",
|
|
944
|
+
"method": "tools/call",
|
|
945
|
+
"params": {
|
|
946
|
+
"name": "store_memory",
|
|
947
|
+
"arguments": {
|
|
948
|
+
"agent_id": effective_agent_id,
|
|
949
|
+
"text": content,
|
|
950
|
+
"source": source,
|
|
951
|
+
"sovereignty_token": effective_token,
|
|
952
|
+
"model_id": effective_model_id,
|
|
953
|
+
"is_factual": is_factual,
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
"id": 1,
|
|
957
|
+
}
|
|
958
|
+
data = await self._api_post("/mcp/message", payload, agent_id=effective_agent_id, model_id=effective_model_id, **kw)
|
|
959
|
+
result = data.get("data", data)
|
|
960
|
+
if isinstance(result, dict):
|
|
961
|
+
return result.get("content", result.get("result", str(result)))
|
|
962
|
+
return str(result)
|
|
963
|
+
|
|
964
|
+
async def mcp_search_memory(
|
|
965
|
+
self,
|
|
966
|
+
query: str,
|
|
967
|
+
*,
|
|
968
|
+
agent_id: Optional[str] = None,
|
|
969
|
+
sovereignty_token: Optional[str] = None,
|
|
970
|
+
model_id: Optional[str] = None,
|
|
971
|
+
top_k: int = 5,
|
|
972
|
+
**kw,
|
|
973
|
+
) -> str:
|
|
974
|
+
"""Search memories via the MCP ``search_memory`` tool."""
|
|
975
|
+
effective_agent_id = agent_id or self.agent_id
|
|
976
|
+
effective_token = sovereignty_token or self.sovereignty_token
|
|
977
|
+
effective_model_id = model_id or effective_token
|
|
978
|
+
payload = {
|
|
979
|
+
"jsonrpc": "2.0",
|
|
980
|
+
"method": "tools/call",
|
|
981
|
+
"params": {
|
|
982
|
+
"name": "search_memory",
|
|
983
|
+
"arguments": {
|
|
984
|
+
"agent_id": effective_agent_id,
|
|
985
|
+
"query": query,
|
|
986
|
+
"top_k": top_k,
|
|
987
|
+
"sovereignty_token": effective_token,
|
|
988
|
+
"model_id": effective_model_id,
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
"id": 2,
|
|
992
|
+
}
|
|
993
|
+
data = await self._api_post("/mcp/message", payload, agent_id=effective_agent_id, model_id=effective_model_id, **kw)
|
|
994
|
+
result = data.get("data", data)
|
|
995
|
+
if isinstance(result, dict):
|
|
996
|
+
return result.get("content", result.get("result", str(result)))
|
|
997
|
+
return str(result)
|
|
998
|
+
|
|
999
|
+
async def mount_memory(
|
|
1000
|
+
self,
|
|
1001
|
+
domain: Optional[str] = None,
|
|
1002
|
+
*,
|
|
1003
|
+
name: Optional[str] = None,
|
|
1004
|
+
sovereignty_token: Optional[str] = None,
|
|
1005
|
+
mode: str = "evolutionary",
|
|
1006
|
+
model_id: Optional[str] = None,
|
|
1007
|
+
) -> "AsyncVecminMemorySpace":
|
|
1008
|
+
"""Mount an asynchronous cognitive memory space for agent operation.
|
|
1009
|
+
|
|
1010
|
+
Args:
|
|
1011
|
+
domain: Alias for name/collection identifier.
|
|
1012
|
+
name: Unified cognitive space identifier.
|
|
1013
|
+
sovereignty_token: Sovereign domain access token.
|
|
1014
|
+
mode: Operating mode (e.g., 'evolutionary').
|
|
1015
|
+
model_id: Embedding feature extraction model targeting.
|
|
1016
|
+
|
|
1017
|
+
Returns:
|
|
1018
|
+
AsyncVecminMemorySpace instance.
|
|
1019
|
+
"""
|
|
1020
|
+
from .memory import AsyncAgentMemoryManager
|
|
1021
|
+
resolved_name = domain or name or "default"
|
|
1022
|
+
resolved_token = sovereignty_token or self.sovereignty_token
|
|
1023
|
+
manager = AsyncAgentMemoryManager(
|
|
1024
|
+
client=self,
|
|
1025
|
+
agent_id=self.agent_id,
|
|
1026
|
+
sovereignty_token=resolved_token,
|
|
1027
|
+
model_id=model_id,
|
|
1028
|
+
)
|
|
1029
|
+
return await manager.mount_memory(domain=resolved_name, mode=mode)
|
|
1030
|
+
|
|
1031
|
+
# ==================================================================
|
|
1032
|
+
# Convenience: ensure_collection (backward compat)
|
|
1033
|
+
# ==================================================================
|
|
1034
|
+
|
|
1035
|
+
async def ensure_collection(
|
|
1036
|
+
self,
|
|
1037
|
+
name: str,
|
|
1038
|
+
*,
|
|
1039
|
+
dimension: int = 1536,
|
|
1040
|
+
metric_type: str = "Cosine",
|
|
1041
|
+
index_type: str = "HNSW",
|
|
1042
|
+
index_params: Optional[Dict[str, Any]] = None,
|
|
1043
|
+
) -> bool:
|
|
1044
|
+
"""Ensure a collection exists, creating it if necessary.
|
|
1045
|
+
|
|
1046
|
+
This is a convenience method that first tries :meth:`get_collection`
|
|
1047
|
+
and falls back to :meth:`create_collection`.
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
name: Collection name.
|
|
1051
|
+
dimension: Vector dimensionality.
|
|
1052
|
+
metric_type: Distance metric.
|
|
1053
|
+
index_type: Index algorithm.
|
|
1054
|
+
index_params: Algorithm parameters.
|
|
1055
|
+
|
|
1056
|
+
Returns:
|
|
1057
|
+
``True`` if the collection exists or was created.
|
|
1058
|
+
"""
|
|
1059
|
+
try:
|
|
1060
|
+
await self.get_collection(name)
|
|
1061
|
+
return True
|
|
1062
|
+
except Exception:
|
|
1063
|
+
pass
|
|
1064
|
+
try:
|
|
1065
|
+
await self.create_collection(
|
|
1066
|
+
name,
|
|
1067
|
+
dimension=dimension,
|
|
1068
|
+
metric_type=metric_type,
|
|
1069
|
+
index_type=index_type,
|
|
1070
|
+
index_params=index_params,
|
|
1071
|
+
)
|
|
1072
|
+
return True
|
|
1073
|
+
except Exception:
|
|
1074
|
+
return False
|