swiftpy-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from swiftpy.ai.exceptions import (
|
|
11
|
+
AIConnectionError,
|
|
12
|
+
AIModuleDisabledError,
|
|
13
|
+
)
|
|
14
|
+
from swiftpy.ai.pool import AIHttpPool
|
|
15
|
+
from swiftpy.ai.vectorstore.base import (
|
|
16
|
+
ALLOWED_METRICS,
|
|
17
|
+
RESERVED_META_KEYS,
|
|
18
|
+
SearchResult,
|
|
19
|
+
VectorRecord,
|
|
20
|
+
VectorStore,
|
|
21
|
+
VectorStoreCapabilities,
|
|
22
|
+
dedupe_preserve_order,
|
|
23
|
+
encode_path_segment,
|
|
24
|
+
handle_adapter_http_error,
|
|
25
|
+
normalize_record,
|
|
26
|
+
validate_collection_name,
|
|
27
|
+
validate_vector,
|
|
28
|
+
)
|
|
29
|
+
from swiftpy.core.config import Config
|
|
30
|
+
|
|
31
|
+
_HTTP_ERRORS: tuple[type[Exception], ...] = (httpx.TimeoutException, httpx.RequestError, OSError)
|
|
32
|
+
|
|
33
|
+
_METRIC_MAP = {
|
|
34
|
+
"cosine": "Cosine",
|
|
35
|
+
"l2": "Euclid",
|
|
36
|
+
"inner_product": "Dot",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _to_qdrant_id(orig_id: str) -> str:
|
|
41
|
+
"""Ensure ID conforms to Qdrant UUID or integer requirement, deterministic for strings."""
|
|
42
|
+
try:
|
|
43
|
+
uuid.UUID(orig_id)
|
|
44
|
+
return orig_id
|
|
45
|
+
except (ValueError, AttributeError):
|
|
46
|
+
return str(uuid.uuid5(uuid.NAMESPACE_DNS, orig_id))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _flatten_qdrant_must(flt: dict[str, Any], prefix: str = "") -> list[dict[str, Any]]:
|
|
50
|
+
"""Flatten nested metadata filter dicts into Qdrant dot-path must conditions."""
|
|
51
|
+
if not isinstance(flt, dict):
|
|
52
|
+
raise TypeError(f"Filter must be a dict, got {type(flt).__name__}.")
|
|
53
|
+
clauses: list[dict[str, Any]] = []
|
|
54
|
+
for k, v in flt.items():
|
|
55
|
+
if not isinstance(k, str) or not k or "\x00" in k:
|
|
56
|
+
raise ValueError(f"Invalid filter key {k!r}.")
|
|
57
|
+
if k.startswith("$"):
|
|
58
|
+
raise ValueError(f"Qdrant metadata filters do not support Mongo-style operator {k!r}.")
|
|
59
|
+
full_key = f"{prefix}.{k}" if prefix else k
|
|
60
|
+
if isinstance(v, dict):
|
|
61
|
+
if any(isinstance(sub_k, str) and sub_k.startswith("$") for sub_k in v):
|
|
62
|
+
raise ValueError(f"Qdrant metadata filters do not support operator dicts on {full_key!r}.")
|
|
63
|
+
if not v:
|
|
64
|
+
raise ValueError(f"Empty nested filter dict on key {full_key!r} is not supported.")
|
|
65
|
+
clauses.extend(_flatten_qdrant_must(v, prefix=full_key))
|
|
66
|
+
elif v is None:
|
|
67
|
+
clauses.append({"is_null": {"key": full_key}})
|
|
68
|
+
else:
|
|
69
|
+
clauses.append({"key": full_key, "match": {"value": v}})
|
|
70
|
+
return clauses
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class QdrantAdapter(VectorStore):
|
|
74
|
+
"""
|
|
75
|
+
Qdrant vector store adapter via REST API through the shared async HTTP pool.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
collection_name: str = "default",
|
|
81
|
+
dimensions: int | None = None,
|
|
82
|
+
metric: str = "cosine",
|
|
83
|
+
base_url: str | None = None,
|
|
84
|
+
api_key: str | None = None,
|
|
85
|
+
timeout: float = 30.0,
|
|
86
|
+
config: Config | None = None,
|
|
87
|
+
http_pool: AIHttpPool | None = None,
|
|
88
|
+
**kwargs: Any,
|
|
89
|
+
) -> None:
|
|
90
|
+
validate_collection_name(collection_name)
|
|
91
|
+
self._config = config or Config()
|
|
92
|
+
if not self._config.get("ai.enabled", True):
|
|
93
|
+
raise AIModuleDisabledError("AI module is disabled in configuration (ai.enabled is false).")
|
|
94
|
+
|
|
95
|
+
norm_metric = metric.lower().strip()
|
|
96
|
+
if norm_metric not in ALLOWED_METRICS:
|
|
97
|
+
raise ValueError(f"Unsupported metric '{metric}'. Expected one of {sorted(ALLOWED_METRICS)}")
|
|
98
|
+
|
|
99
|
+
self._collection = collection_name
|
|
100
|
+
self._dimensions = dimensions
|
|
101
|
+
self._metric = norm_metric
|
|
102
|
+
|
|
103
|
+
cfg_base = self._config.get("ai.vectorstores.qdrant.base_url", "http://localhost:6333")
|
|
104
|
+
self._base_url = (base_url or str(cfg_base)).rstrip("/")
|
|
105
|
+
cfg_key = self._config.get("ai.vectorstores.qdrant.api_key", "")
|
|
106
|
+
self._api_key = api_key if api_key is not None else str(cfg_key)
|
|
107
|
+
self._timeout = timeout
|
|
108
|
+
|
|
109
|
+
self._http_pool = http_pool or AIHttpPool.get_instance()
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def capabilities(self) -> VectorStoreCapabilities:
|
|
113
|
+
"""Return the declared capabilities supported by Qdrant."""
|
|
114
|
+
return VectorStoreCapabilities(
|
|
115
|
+
metadata_filtering=True,
|
|
116
|
+
nested_metadata_filtering=True,
|
|
117
|
+
namespaces=False,
|
|
118
|
+
pagination=False,
|
|
119
|
+
batch_upsert=True,
|
|
120
|
+
native_distance_metrics=("cosine", "l2", "inner_product"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _get_headers(self) -> dict[str, str]:
|
|
124
|
+
headers = {"Content-Type": "application/json"}
|
|
125
|
+
if self._api_key:
|
|
126
|
+
headers["api-key"] = self._api_key
|
|
127
|
+
return headers
|
|
128
|
+
|
|
129
|
+
def _collection_url(self, name: str | None = None, suffix: str = "") -> str:
|
|
130
|
+
coll = self._collection if name is None else validate_collection_name(name)
|
|
131
|
+
return f"{self._base_url}/collections/{encode_path_segment(coll)}{suffix}"
|
|
132
|
+
|
|
133
|
+
async def create_collection(
|
|
134
|
+
self,
|
|
135
|
+
name: str,
|
|
136
|
+
dimensions: int,
|
|
137
|
+
metric: str = "cosine",
|
|
138
|
+
) -> None:
|
|
139
|
+
validate_collection_name(name)
|
|
140
|
+
norm_metric = metric.lower().strip()
|
|
141
|
+
if norm_metric not in ALLOWED_METRICS:
|
|
142
|
+
raise ValueError(f"Unsupported metric '{metric}'. Expected one of {sorted(ALLOWED_METRICS)}")
|
|
143
|
+
if isinstance(dimensions, bool) or not isinstance(dimensions, int) or dimensions <= 0:
|
|
144
|
+
raise ValueError(f"Dimensions must be positive, got {dimensions}")
|
|
145
|
+
|
|
146
|
+
qdrant_metric = _METRIC_MAP[norm_metric]
|
|
147
|
+
url = self._collection_url(name)
|
|
148
|
+
payload = {
|
|
149
|
+
"vectors": {
|
|
150
|
+
"size": dimensions,
|
|
151
|
+
"distance": qdrant_metric,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
client = self._http_pool.get_client()
|
|
156
|
+
try:
|
|
157
|
+
resp = await client.put(
|
|
158
|
+
url,
|
|
159
|
+
json=payload,
|
|
160
|
+
headers=self._get_headers(),
|
|
161
|
+
timeout=self._timeout,
|
|
162
|
+
)
|
|
163
|
+
if resp.status_code not in (200, 409):
|
|
164
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
165
|
+
except _HTTP_ERRORS as err:
|
|
166
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
167
|
+
|
|
168
|
+
self._collection = name
|
|
169
|
+
self._dimensions = dimensions
|
|
170
|
+
self._metric = norm_metric
|
|
171
|
+
|
|
172
|
+
async def upsert(
|
|
173
|
+
self,
|
|
174
|
+
id: str,
|
|
175
|
+
vector: list[float],
|
|
176
|
+
metadata: dict[str, Any] | None = None,
|
|
177
|
+
content: str = "",
|
|
178
|
+
) -> None:
|
|
179
|
+
await self.upsert_batch([
|
|
180
|
+
VectorRecord(id=id, vector=vector, metadata=metadata or {}, content=content)
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
async def upsert_batch(
|
|
184
|
+
self,
|
|
185
|
+
records: Sequence[
|
|
186
|
+
VectorRecord
|
|
187
|
+
| dict[str, Any]
|
|
188
|
+
| tuple[str, list[float], dict[str, Any] | None]
|
|
189
|
+
],
|
|
190
|
+
) -> None:
|
|
191
|
+
if not records:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
norm_records = [
|
|
195
|
+
normalize_record(r, self._dimensions, self._metric) for r in records
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
points: list[dict[str, Any]] = []
|
|
199
|
+
for r in norm_records:
|
|
200
|
+
qid = _to_qdrant_id(r.id)
|
|
201
|
+
user_meta = {
|
|
202
|
+
k: copy.deepcopy(v)
|
|
203
|
+
for k, v in r.metadata.items()
|
|
204
|
+
if k not in RESERVED_META_KEYS
|
|
205
|
+
}
|
|
206
|
+
payload = {
|
|
207
|
+
**user_meta,
|
|
208
|
+
"_orig_id": r.id,
|
|
209
|
+
"_content": r.content,
|
|
210
|
+
}
|
|
211
|
+
points.append({
|
|
212
|
+
"id": qid,
|
|
213
|
+
"vector": r.vector,
|
|
214
|
+
"payload": payload,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
url = self._collection_url(suffix="/points?wait=true")
|
|
218
|
+
client = self._http_pool.get_client()
|
|
219
|
+
try:
|
|
220
|
+
resp = await client.put(
|
|
221
|
+
url,
|
|
222
|
+
json={"points": points},
|
|
223
|
+
headers=self._get_headers(),
|
|
224
|
+
timeout=self._timeout,
|
|
225
|
+
)
|
|
226
|
+
if resp.status_code != 200:
|
|
227
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
228
|
+
except _HTTP_ERRORS as err:
|
|
229
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
230
|
+
|
|
231
|
+
async def search(
|
|
232
|
+
self,
|
|
233
|
+
vector: list[float],
|
|
234
|
+
limit: int = 10,
|
|
235
|
+
filter: dict[str, Any] | None = None,
|
|
236
|
+
) -> list[SearchResult]:
|
|
237
|
+
if limit <= 0:
|
|
238
|
+
raise ValueError(f"Limit must be a positive integer, got {limit}")
|
|
239
|
+
|
|
240
|
+
parsed_vec = validate_vector(vector, self._dimensions, self._metric)
|
|
241
|
+
url = self._collection_url(suffix="/points/search")
|
|
242
|
+
|
|
243
|
+
body: dict[str, Any] = {
|
|
244
|
+
"vector": parsed_vec,
|
|
245
|
+
"limit": limit,
|
|
246
|
+
"with_payload": True,
|
|
247
|
+
"with_vector": True,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if filter is not None:
|
|
251
|
+
must_clauses = _flatten_qdrant_must(filter)
|
|
252
|
+
if must_clauses:
|
|
253
|
+
body["filter"] = {"must": must_clauses}
|
|
254
|
+
|
|
255
|
+
client = self._http_pool.get_client()
|
|
256
|
+
try:
|
|
257
|
+
resp = await client.post(
|
|
258
|
+
url,
|
|
259
|
+
json=body,
|
|
260
|
+
headers=self._get_headers(),
|
|
261
|
+
timeout=self._timeout,
|
|
262
|
+
)
|
|
263
|
+
if resp.status_code != 200:
|
|
264
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
265
|
+
data = resp.json().get("result", [])
|
|
266
|
+
except _HTTP_ERRORS as err:
|
|
267
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
268
|
+
|
|
269
|
+
results: list[SearchResult] = []
|
|
270
|
+
for item in data:
|
|
271
|
+
payload = item.get("payload") or {}
|
|
272
|
+
orig_id = str(payload.get("_orig_id", item.get("id", "")))
|
|
273
|
+
content = str(payload.get("_content", ""))
|
|
274
|
+
meta = {k: v for k, v in payload.items() if k not in RESERVED_META_KEYS}
|
|
275
|
+
raw_score = float(item.get("score", 0.0))
|
|
276
|
+
score = -abs(raw_score) if self._metric == "l2" else raw_score
|
|
277
|
+
vec = item.get("vector")
|
|
278
|
+
results.append(
|
|
279
|
+
SearchResult(
|
|
280
|
+
id=orig_id,
|
|
281
|
+
score=score,
|
|
282
|
+
metadata=meta,
|
|
283
|
+
content=content,
|
|
284
|
+
vector=vec,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
return results
|
|
288
|
+
|
|
289
|
+
async def get(self, id: str) -> SearchResult | None:
|
|
290
|
+
if not id or not isinstance(id, str):
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
qid = _to_qdrant_id(id)
|
|
294
|
+
url = self._collection_url(suffix="/points")
|
|
295
|
+
client = self._http_pool.get_client()
|
|
296
|
+
try:
|
|
297
|
+
resp = await client.post(
|
|
298
|
+
url,
|
|
299
|
+
json={"ids": [qid], "with_payload": True, "with_vector": True},
|
|
300
|
+
headers=self._get_headers(),
|
|
301
|
+
timeout=self._timeout,
|
|
302
|
+
)
|
|
303
|
+
if resp.status_code == 404:
|
|
304
|
+
return None
|
|
305
|
+
if resp.status_code != 200:
|
|
306
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
307
|
+
items = resp.json().get("result", [])
|
|
308
|
+
if not items:
|
|
309
|
+
return None
|
|
310
|
+
item = items[0]
|
|
311
|
+
payload = item.get("payload") or {}
|
|
312
|
+
orig_id = str(payload.get("_orig_id", item.get("id", "")))
|
|
313
|
+
content = str(payload.get("_content", ""))
|
|
314
|
+
meta = {k: v for k, v in payload.items() if k not in RESERVED_META_KEYS}
|
|
315
|
+
vec = item.get("vector")
|
|
316
|
+
return SearchResult(
|
|
317
|
+
id=orig_id,
|
|
318
|
+
score=1.0,
|
|
319
|
+
metadata=meta,
|
|
320
|
+
content=content,
|
|
321
|
+
vector=vec,
|
|
322
|
+
)
|
|
323
|
+
except _HTTP_ERRORS as err:
|
|
324
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
325
|
+
|
|
326
|
+
async def delete(self, id: str) -> bool:
|
|
327
|
+
existing = await self.get(id)
|
|
328
|
+
if existing is None:
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
qid = _to_qdrant_id(id)
|
|
332
|
+
url = self._collection_url(suffix="/points/delete?wait=true")
|
|
333
|
+
client = self._http_pool.get_client()
|
|
334
|
+
try:
|
|
335
|
+
resp = await client.post(
|
|
336
|
+
url,
|
|
337
|
+
json={"points": [qid]},
|
|
338
|
+
headers=self._get_headers(),
|
|
339
|
+
timeout=self._timeout,
|
|
340
|
+
)
|
|
341
|
+
if resp.status_code != 200:
|
|
342
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
343
|
+
return True
|
|
344
|
+
except _HTTP_ERRORS as err:
|
|
345
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
346
|
+
|
|
347
|
+
async def delete_batch(self, ids: Sequence[str]) -> int:
|
|
348
|
+
unique_ids = dedupe_preserve_order(ids)
|
|
349
|
+
if not unique_ids:
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
candidate_qids = [_to_qdrant_id(item_id) for item_id in unique_ids]
|
|
353
|
+
lookup_url = self._collection_url(suffix="/points")
|
|
354
|
+
client = self._http_pool.get_client()
|
|
355
|
+
try:
|
|
356
|
+
lookup_resp = await client.post(
|
|
357
|
+
lookup_url,
|
|
358
|
+
json={"ids": candidate_qids, "with_payload": False, "with_vector": False},
|
|
359
|
+
headers=self._get_headers(),
|
|
360
|
+
timeout=self._timeout,
|
|
361
|
+
)
|
|
362
|
+
if lookup_resp.status_code == 404:
|
|
363
|
+
return 0
|
|
364
|
+
if lookup_resp.status_code != 200:
|
|
365
|
+
handle_adapter_http_error(lookup_resp.status_code, lookup_resp.text, "qdrant")
|
|
366
|
+
found_items = lookup_resp.json().get("result", [])
|
|
367
|
+
except _HTTP_ERRORS as err:
|
|
368
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
369
|
+
|
|
370
|
+
found_qids = dedupe_preserve_order(
|
|
371
|
+
[str(item["id"]) for item in found_items if isinstance(item, dict) and item.get("id") is not None]
|
|
372
|
+
)
|
|
373
|
+
if not found_qids:
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
url = self._collection_url(suffix="/points/delete?wait=true")
|
|
377
|
+
try:
|
|
378
|
+
resp = await client.post(
|
|
379
|
+
url,
|
|
380
|
+
json={"points": found_qids},
|
|
381
|
+
headers=self._get_headers(),
|
|
382
|
+
timeout=self._timeout,
|
|
383
|
+
)
|
|
384
|
+
if resp.status_code != 200:
|
|
385
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
386
|
+
return len(found_qids)
|
|
387
|
+
except _HTTP_ERRORS as err:
|
|
388
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
389
|
+
|
|
390
|
+
async def count(self) -> int:
|
|
391
|
+
url = self._collection_url(suffix="/points/count")
|
|
392
|
+
client = self._http_pool.get_client()
|
|
393
|
+
try:
|
|
394
|
+
resp = await client.post(
|
|
395
|
+
url,
|
|
396
|
+
json={"exact": True},
|
|
397
|
+
headers=self._get_headers(),
|
|
398
|
+
timeout=self._timeout,
|
|
399
|
+
)
|
|
400
|
+
if resp.status_code != 200:
|
|
401
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
402
|
+
return int(resp.json().get("result", {}).get("count", 0))
|
|
403
|
+
except _HTTP_ERRORS as err:
|
|
404
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
|
405
|
+
|
|
406
|
+
async def drop_collection(self, name: str) -> None:
|
|
407
|
+
url = self._collection_url(name)
|
|
408
|
+
client = self._http_pool.get_client()
|
|
409
|
+
try:
|
|
410
|
+
resp = await client.delete(
|
|
411
|
+
url,
|
|
412
|
+
headers=self._get_headers(),
|
|
413
|
+
timeout=self._timeout,
|
|
414
|
+
)
|
|
415
|
+
if resp.status_code not in (200, 404):
|
|
416
|
+
handle_adapter_http_error(resp.status_code, resp.text, "qdrant")
|
|
417
|
+
except _HTTP_ERRORS as err:
|
|
418
|
+
raise AIConnectionError(f"[qdrant] Connection error: {err}") from err
|
swiftpy/auth/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from swiftpy.auth.api_key_guard import (
|
|
4
|
+
ApiKeyGuard,
|
|
5
|
+
generate_api_key,
|
|
6
|
+
get_server_pepper,
|
|
7
|
+
hash_api_key,
|
|
8
|
+
require_api_key,
|
|
9
|
+
verify_api_key_hash,
|
|
10
|
+
)
|
|
11
|
+
from swiftpy.auth.auth_manager import AuthManager, CurrentUser, current_user
|
|
12
|
+
from swiftpy.auth.exceptions import (
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
InvalidApiKeyError,
|
|
15
|
+
TokenRevokedError,
|
|
16
|
+
)
|
|
17
|
+
from swiftpy.auth.jwt_guard import JWTGuard, require_jwt
|
|
18
|
+
from swiftpy.auth.jwt_issuer import (
|
|
19
|
+
InvalidAlgorithmError,
|
|
20
|
+
InvalidSignatureError,
|
|
21
|
+
InvalidTokenError,
|
|
22
|
+
JWTError,
|
|
23
|
+
JWTIssuer,
|
|
24
|
+
TokenExpiredError,
|
|
25
|
+
)
|
|
26
|
+
from swiftpy.auth.token_blacklist import TokenBlacklist
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ApiKeyGuard",
|
|
30
|
+
"AuthManager",
|
|
31
|
+
"AuthenticationError",
|
|
32
|
+
"CurrentUser",
|
|
33
|
+
"InvalidAlgorithmError",
|
|
34
|
+
"InvalidApiKeyError",
|
|
35
|
+
"InvalidSignatureError",
|
|
36
|
+
"InvalidTokenError",
|
|
37
|
+
"JWTError",
|
|
38
|
+
"JWTGuard",
|
|
39
|
+
"JWTIssuer",
|
|
40
|
+
"TokenBlacklist",
|
|
41
|
+
"TokenExpiredError",
|
|
42
|
+
"TokenRevokedError",
|
|
43
|
+
"current_user",
|
|
44
|
+
"generate_api_key",
|
|
45
|
+
"get_server_pepper",
|
|
46
|
+
"hash_api_key",
|
|
47
|
+
"require_api_key",
|
|
48
|
+
"require_jwt",
|
|
49
|
+
"verify_api_key_hash",
|
|
50
|
+
]
|