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.
Files changed (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. 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