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,445 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import math
|
|
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_filter_operators,
|
|
28
|
+
validate_pinecone_environment,
|
|
29
|
+
validate_pinecone_url,
|
|
30
|
+
validate_vector,
|
|
31
|
+
)
|
|
32
|
+
from swiftpy.core.config import Config
|
|
33
|
+
|
|
34
|
+
_HTTP_ERRORS: tuple[type[Exception], ...] = (httpx.TimeoutException, httpx.RequestError, OSError)
|
|
35
|
+
|
|
36
|
+
_PINECONE_METRIC_MAP = {
|
|
37
|
+
"cosine": "cosine",
|
|
38
|
+
"l2": "euclidean",
|
|
39
|
+
"inner_product": "dotproduct",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PineconeAdapter(VectorStore):
|
|
44
|
+
"""
|
|
45
|
+
Pinecone vector store adapter via REST API through the shared async HTTP pool.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
collection_name: str = "default",
|
|
51
|
+
dimensions: int | None = None,
|
|
52
|
+
metric: str = "cosine",
|
|
53
|
+
base_url: str | None = None,
|
|
54
|
+
api_key: str | None = None,
|
|
55
|
+
environment: str | None = None,
|
|
56
|
+
namespace: str = "",
|
|
57
|
+
timeout: float = 30.0,
|
|
58
|
+
config: Config | None = None,
|
|
59
|
+
http_pool: AIHttpPool | None = None,
|
|
60
|
+
**kwargs: Any,
|
|
61
|
+
) -> None:
|
|
62
|
+
validate_collection_name(collection_name)
|
|
63
|
+
self._config = config or Config()
|
|
64
|
+
if not self._config.get("ai.enabled", True):
|
|
65
|
+
raise AIModuleDisabledError("AI module is disabled in configuration (ai.enabled is false).")
|
|
66
|
+
|
|
67
|
+
norm_metric = metric.lower().strip()
|
|
68
|
+
if norm_metric not in ALLOWED_METRICS:
|
|
69
|
+
raise ValueError(f"Unsupported metric '{metric}'. Expected one of {sorted(ALLOWED_METRICS)}")
|
|
70
|
+
|
|
71
|
+
self._collection = collection_name
|
|
72
|
+
self._dimensions = dimensions
|
|
73
|
+
self._metric = norm_metric
|
|
74
|
+
self._namespace = namespace
|
|
75
|
+
self._timeout = timeout
|
|
76
|
+
|
|
77
|
+
cfg_key = self._config.get("ai.vectorstores.pinecone.api_key", "")
|
|
78
|
+
self._api_key = api_key if api_key is not None else str(cfg_key)
|
|
79
|
+
raw_env = environment or str(self._config.get("ai.vectorstores.pinecone.environment", "us-east-1"))
|
|
80
|
+
self._environment = validate_pinecone_environment(raw_env)
|
|
81
|
+
|
|
82
|
+
cfg_base = self._config.get("ai.vectorstores.pinecone.base_url", "")
|
|
83
|
+
raw_base = base_url or str(cfg_base)
|
|
84
|
+
self._explicit_base_url = bool(raw_base)
|
|
85
|
+
if raw_base:
|
|
86
|
+
self._base_url = validate_pinecone_url(raw_base, require_pinecone_host=False)
|
|
87
|
+
else:
|
|
88
|
+
self._base_url = validate_pinecone_url(
|
|
89
|
+
f"https://{self._collection}.svc.{self._environment}.pinecone.io",
|
|
90
|
+
require_pinecone_host=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self._http_pool = http_pool or AIHttpPool.get_instance()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def capabilities(self) -> VectorStoreCapabilities:
|
|
97
|
+
"""Return the declared capabilities supported by Pinecone."""
|
|
98
|
+
return VectorStoreCapabilities(
|
|
99
|
+
metadata_filtering=True,
|
|
100
|
+
nested_metadata_filtering=False,
|
|
101
|
+
namespaces=True,
|
|
102
|
+
pagination=False,
|
|
103
|
+
batch_upsert=True,
|
|
104
|
+
native_distance_metrics=("cosine", "l2", "inner_product"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _get_headers(self) -> dict[str, str]:
|
|
108
|
+
headers = {"Content-Type": "application/json"}
|
|
109
|
+
if self._api_key:
|
|
110
|
+
headers["Api-Key"] = self._api_key
|
|
111
|
+
return headers
|
|
112
|
+
|
|
113
|
+
async def create_collection(
|
|
114
|
+
self,
|
|
115
|
+
name: str,
|
|
116
|
+
dimensions: int,
|
|
117
|
+
metric: str = "cosine",
|
|
118
|
+
) -> None:
|
|
119
|
+
validate_collection_name(name)
|
|
120
|
+
norm_metric = metric.lower().strip()
|
|
121
|
+
if norm_metric not in ALLOWED_METRICS:
|
|
122
|
+
raise ValueError(f"Unsupported metric '{metric}'. Expected one of {sorted(ALLOWED_METRICS)}")
|
|
123
|
+
if isinstance(dimensions, bool) or not isinstance(dimensions, int) or dimensions <= 0:
|
|
124
|
+
raise ValueError(f"Dimensions must be positive, got {dimensions}")
|
|
125
|
+
|
|
126
|
+
pinecone_metric = _PINECONE_METRIC_MAP[norm_metric]
|
|
127
|
+
url = "https://api.pinecone.io/indexes"
|
|
128
|
+
payload = {
|
|
129
|
+
"name": name,
|
|
130
|
+
"dimension": dimensions,
|
|
131
|
+
"metric": pinecone_metric,
|
|
132
|
+
"spec": {
|
|
133
|
+
"serverless": {
|
|
134
|
+
"cloud": "aws",
|
|
135
|
+
"region": self._environment,
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
client = self._http_pool.get_client()
|
|
141
|
+
try:
|
|
142
|
+
resp = await client.post(
|
|
143
|
+
url,
|
|
144
|
+
json=payload,
|
|
145
|
+
headers=self._get_headers(),
|
|
146
|
+
timeout=self._timeout,
|
|
147
|
+
)
|
|
148
|
+
if resp.status_code not in (200, 201, 409):
|
|
149
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
150
|
+
resp_data: Any = {}
|
|
151
|
+
try:
|
|
152
|
+
resp_data = resp.json()
|
|
153
|
+
except ValueError:
|
|
154
|
+
resp_data = {}
|
|
155
|
+
except _HTTP_ERRORS as err:
|
|
156
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
157
|
+
|
|
158
|
+
self._collection = name
|
|
159
|
+
self._dimensions = dimensions
|
|
160
|
+
self._metric = norm_metric
|
|
161
|
+
|
|
162
|
+
if isinstance(resp_data, dict) and resp_data.get("host"):
|
|
163
|
+
raw_host = str(resp_data["host"]).strip()
|
|
164
|
+
candidate = raw_host if raw_host.startswith("https://") else f"https://{raw_host}"
|
|
165
|
+
self._base_url = validate_pinecone_url(candidate, require_pinecone_host=True)
|
|
166
|
+
elif not self._explicit_base_url:
|
|
167
|
+
self._base_url = validate_pinecone_url(
|
|
168
|
+
f"https://{name}.svc.{self._environment}.pinecone.io",
|
|
169
|
+
require_pinecone_host=True,
|
|
170
|
+
)
|
|
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
|
+
vectors_payload: list[dict[str, Any]] = []
|
|
199
|
+
for r in norm_records:
|
|
200
|
+
user_meta = {
|
|
201
|
+
k: copy.deepcopy(v)
|
|
202
|
+
for k, v in r.metadata.items()
|
|
203
|
+
if k not in RESERVED_META_KEYS
|
|
204
|
+
}
|
|
205
|
+
meta = {
|
|
206
|
+
**user_meta,
|
|
207
|
+
"_content": r.content,
|
|
208
|
+
"_orig_id": r.id,
|
|
209
|
+
}
|
|
210
|
+
vectors_payload.append({
|
|
211
|
+
"id": r.id,
|
|
212
|
+
"values": r.vector,
|
|
213
|
+
"metadata": meta,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
url = f"{self._base_url}/vectors/upsert"
|
|
217
|
+
body = {
|
|
218
|
+
"vectors": vectors_payload,
|
|
219
|
+
"namespace": self._namespace,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
client = self._http_pool.get_client()
|
|
223
|
+
try:
|
|
224
|
+
resp = await client.post(
|
|
225
|
+
url,
|
|
226
|
+
json=body,
|
|
227
|
+
headers=self._get_headers(),
|
|
228
|
+
timeout=self._timeout,
|
|
229
|
+
)
|
|
230
|
+
if resp.status_code != 200:
|
|
231
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
232
|
+
except _HTTP_ERRORS as err:
|
|
233
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
234
|
+
|
|
235
|
+
async def search(
|
|
236
|
+
self,
|
|
237
|
+
vector: list[float],
|
|
238
|
+
limit: int = 10,
|
|
239
|
+
filter: dict[str, Any] | None = None,
|
|
240
|
+
) -> list[SearchResult]:
|
|
241
|
+
if limit <= 0:
|
|
242
|
+
raise ValueError(f"Limit must be a positive integer, got {limit}")
|
|
243
|
+
|
|
244
|
+
parsed_vec = validate_vector(vector, self._dimensions, self._metric)
|
|
245
|
+
url = f"{self._base_url}/query"
|
|
246
|
+
|
|
247
|
+
body: dict[str, Any] = {
|
|
248
|
+
"vector": parsed_vec,
|
|
249
|
+
"topK": limit,
|
|
250
|
+
"includeMetadata": True,
|
|
251
|
+
"includeValues": True,
|
|
252
|
+
"namespace": self._namespace,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if filter is not None:
|
|
256
|
+
validate_filter_operators(filter, nested_allowed=False)
|
|
257
|
+
if filter:
|
|
258
|
+
pine_filter: dict[str, Any] = {}
|
|
259
|
+
for k, v in filter.items():
|
|
260
|
+
if k.startswith("$"):
|
|
261
|
+
pine_filter[k] = v
|
|
262
|
+
else:
|
|
263
|
+
pine_filter[k] = {"$eq": v} if not isinstance(v, dict) else v
|
|
264
|
+
body["filter"] = pine_filter
|
|
265
|
+
|
|
266
|
+
client = self._http_pool.get_client()
|
|
267
|
+
try:
|
|
268
|
+
resp = await client.post(
|
|
269
|
+
url,
|
|
270
|
+
json=body,
|
|
271
|
+
headers=self._get_headers(),
|
|
272
|
+
timeout=self._timeout,
|
|
273
|
+
)
|
|
274
|
+
if resp.status_code != 200:
|
|
275
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
276
|
+
matches = resp.json().get("matches", [])
|
|
277
|
+
except _HTTP_ERRORS as err:
|
|
278
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
279
|
+
|
|
280
|
+
results: list[SearchResult] = []
|
|
281
|
+
for item in matches:
|
|
282
|
+
meta_raw = item.get("metadata") or {}
|
|
283
|
+
content = str(meta_raw.get("_content", ""))
|
|
284
|
+
orig_id = str(meta_raw.get("_orig_id") or item.get("id", ""))
|
|
285
|
+
clean_meta = {k: v for k, v in meta_raw.items() if k not in RESERVED_META_KEYS}
|
|
286
|
+
raw_score = float(item.get("score", 0.0))
|
|
287
|
+
if self._metric == "l2":
|
|
288
|
+
score = -math.sqrt(max(0.0, raw_score))
|
|
289
|
+
else:
|
|
290
|
+
score = raw_score
|
|
291
|
+
values = item.get("values")
|
|
292
|
+
results.append(
|
|
293
|
+
SearchResult(
|
|
294
|
+
id=orig_id,
|
|
295
|
+
score=score,
|
|
296
|
+
metadata=clean_meta,
|
|
297
|
+
content=content,
|
|
298
|
+
vector=values,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
results.sort(key=lambda r: r.score, reverse=True)
|
|
303
|
+
return results
|
|
304
|
+
|
|
305
|
+
async def get(self, id: str) -> SearchResult | None:
|
|
306
|
+
if not id or not isinstance(id, str):
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
url = f"{self._base_url}/vectors/fetch"
|
|
310
|
+
params = {"ids": id, "namespace": self._namespace}
|
|
311
|
+
|
|
312
|
+
client = self._http_pool.get_client()
|
|
313
|
+
try:
|
|
314
|
+
resp = await client.get(
|
|
315
|
+
url,
|
|
316
|
+
params=params,
|
|
317
|
+
headers=self._get_headers(),
|
|
318
|
+
timeout=self._timeout,
|
|
319
|
+
)
|
|
320
|
+
if resp.status_code == 404:
|
|
321
|
+
return None
|
|
322
|
+
if resp.status_code != 200:
|
|
323
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
324
|
+
vectors = resp.json().get("vectors", {})
|
|
325
|
+
if id not in vectors:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
item = vectors[id]
|
|
329
|
+
meta_raw = item.get("metadata") or {}
|
|
330
|
+
content = str(meta_raw.get("_content", ""))
|
|
331
|
+
orig_id = str(meta_raw.get("_orig_id") or id)
|
|
332
|
+
clean_meta = {k: v for k, v in meta_raw.items() if k not in RESERVED_META_KEYS}
|
|
333
|
+
values = item.get("values")
|
|
334
|
+
|
|
335
|
+
return SearchResult(
|
|
336
|
+
id=orig_id,
|
|
337
|
+
score=1.0,
|
|
338
|
+
metadata=clean_meta,
|
|
339
|
+
content=content,
|
|
340
|
+
vector=values,
|
|
341
|
+
)
|
|
342
|
+
except _HTTP_ERRORS as err:
|
|
343
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
344
|
+
|
|
345
|
+
async def delete(self, id: str) -> bool:
|
|
346
|
+
existing = await self.get(id)
|
|
347
|
+
if existing is None:
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
url = f"{self._base_url}/vectors/delete"
|
|
351
|
+
body = {"ids": [id], "namespace": self._namespace}
|
|
352
|
+
|
|
353
|
+
client = self._http_pool.get_client()
|
|
354
|
+
try:
|
|
355
|
+
resp = await client.post(
|
|
356
|
+
url,
|
|
357
|
+
json=body,
|
|
358
|
+
headers=self._get_headers(),
|
|
359
|
+
timeout=self._timeout,
|
|
360
|
+
)
|
|
361
|
+
if resp.status_code != 200:
|
|
362
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
363
|
+
return True
|
|
364
|
+
except _HTTP_ERRORS as err:
|
|
365
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
366
|
+
|
|
367
|
+
async def delete_batch(self, ids: Sequence[str]) -> int:
|
|
368
|
+
unique_ids = dedupe_preserve_order(ids)
|
|
369
|
+
if not unique_ids:
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
client = self._http_pool.get_client()
|
|
373
|
+
fetch_url = f"{self._base_url}/vectors/fetch"
|
|
374
|
+
params: list[tuple[str, str | int | float | bool | None]] = [("ids", item_id) for item_id in unique_ids]
|
|
375
|
+
params.append(("namespace", self._namespace))
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
fetch_resp = await client.get(
|
|
379
|
+
fetch_url,
|
|
380
|
+
params=params,
|
|
381
|
+
headers=self._get_headers(),
|
|
382
|
+
timeout=self._timeout,
|
|
383
|
+
)
|
|
384
|
+
if fetch_resp.status_code == 404:
|
|
385
|
+
return 0
|
|
386
|
+
if fetch_resp.status_code != 200:
|
|
387
|
+
handle_adapter_http_error(fetch_resp.status_code, fetch_resp.text, "pinecone")
|
|
388
|
+
vectors = fetch_resp.json().get("vectors", {})
|
|
389
|
+
except _HTTP_ERRORS as err:
|
|
390
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
391
|
+
|
|
392
|
+
existing_ids = [item_id for item_id in unique_ids if item_id in vectors]
|
|
393
|
+
if not existing_ids:
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
url = f"{self._base_url}/vectors/delete"
|
|
397
|
+
body = {"ids": existing_ids, "namespace": self._namespace}
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
resp = await client.post(
|
|
401
|
+
url,
|
|
402
|
+
json=body,
|
|
403
|
+
headers=self._get_headers(),
|
|
404
|
+
timeout=self._timeout,
|
|
405
|
+
)
|
|
406
|
+
if resp.status_code != 200:
|
|
407
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
408
|
+
return len(existing_ids)
|
|
409
|
+
except _HTTP_ERRORS as err:
|
|
410
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
411
|
+
|
|
412
|
+
async def count(self) -> int:
|
|
413
|
+
url = f"{self._base_url}/describe_index_stats"
|
|
414
|
+
client = self._http_pool.get_client()
|
|
415
|
+
try:
|
|
416
|
+
resp = await client.post(
|
|
417
|
+
url,
|
|
418
|
+
json={},
|
|
419
|
+
headers=self._get_headers(),
|
|
420
|
+
timeout=self._timeout,
|
|
421
|
+
)
|
|
422
|
+
if resp.status_code != 200:
|
|
423
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
424
|
+
stats = resp.json()
|
|
425
|
+
if self._namespace and "namespaces" in stats:
|
|
426
|
+
ns_info = stats["namespaces"].get(self._namespace, {})
|
|
427
|
+
return int(ns_info.get("vectorCount", 0))
|
|
428
|
+
return int(stats.get("totalVectorCount", 0))
|
|
429
|
+
except _HTTP_ERRORS as err:
|
|
430
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|
|
431
|
+
|
|
432
|
+
async def drop_collection(self, name: str) -> None:
|
|
433
|
+
validated = validate_collection_name(name)
|
|
434
|
+
url = f"https://api.pinecone.io/indexes/{encode_path_segment(validated)}"
|
|
435
|
+
client = self._http_pool.get_client()
|
|
436
|
+
try:
|
|
437
|
+
resp = await client.delete(
|
|
438
|
+
url,
|
|
439
|
+
headers=self._get_headers(),
|
|
440
|
+
timeout=self._timeout,
|
|
441
|
+
)
|
|
442
|
+
if resp.status_code not in (200, 202, 404):
|
|
443
|
+
handle_adapter_http_error(resp.status_code, resp.text, "pinecone")
|
|
444
|
+
except _HTTP_ERRORS as err:
|
|
445
|
+
raise AIConnectionError(f"[pinecone] Connection error: {err}") from err
|