remdb 0.3.242__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.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,435 @@
1
+ """
2
+ Embedding Worker - Background task processor for generating embeddings.
3
+
4
+ Design:
5
+ - Asyncio-based task queue (asyncio.Queue)
6
+ - Worker pool processes embedding tasks in background
7
+ - Non-blocking: batch_upsert returns immediately, embeddings generated async
8
+ - Batching: Groups tasks for efficient API calls to OpenAI/Anthropic
9
+ - Error handling: Retries with exponential backoff
10
+
11
+ Flow:
12
+ 1. batch_upsert() queues embedding tasks
13
+ 2. Worker pool picks up tasks from queue
14
+ 3. Workers batch tasks and call embedding API
15
+ 4. Workers upsert embeddings to embeddings_<table>
16
+
17
+ Future:
18
+ - Replace with Celery/RQ for production (multi-process, Redis backend)
19
+ - Add monitoring and metrics (task latency, queue depth, error rate)
20
+ - Support multiple embedding providers (OpenAI, Cohere, local models)
21
+ """
22
+
23
+ import asyncio
24
+ import os
25
+ from typing import Any, Optional
26
+ import hashlib
27
+ import uuid
28
+ from uuid import uuid4
29
+
30
+ import httpx
31
+ from loguru import logger
32
+ from pydantic import BaseModel
33
+
34
+
35
+ class EmbeddingTask(BaseModel):
36
+ """
37
+ Embedding task for background processing.
38
+
39
+ Each task represents one field of one entity that needs embedding.
40
+ """
41
+
42
+ task_id: str
43
+ entity_id: str
44
+ table_name: str
45
+ field_name: str
46
+ content: str
47
+ provider: str = "openai"
48
+ model: str = "text-embedding-3-small"
49
+
50
+
51
+ # Global singleton worker instance
52
+ _global_worker: Optional["EmbeddingWorker"] = None
53
+
54
+
55
+ def get_global_embedding_worker(postgres_service: Any = None) -> "EmbeddingWorker":
56
+ """
57
+ Get or create global embedding worker singleton.
58
+
59
+ Worker lives independently of any single PostgresService instance.
60
+ Multiple services can share the same worker.
61
+
62
+ Args:
63
+ postgres_service: PostgresService instance (required on first call)
64
+
65
+ Returns:
66
+ Global EmbeddingWorker instance
67
+ """
68
+ global _global_worker
69
+
70
+ if _global_worker is None:
71
+ if postgres_service is None:
72
+ raise RuntimeError("Must provide postgres_service on first call to get_global_embedding_worker")
73
+ _global_worker = EmbeddingWorker(postgres_service=postgres_service)
74
+ logger.debug("Created global EmbeddingWorker singleton")
75
+
76
+ return _global_worker
77
+
78
+
79
+ class EmbeddingWorker:
80
+ """
81
+ Background worker for generating embeddings.
82
+
83
+ Uses asyncio.Queue for task management and worker pool pattern.
84
+ Workers consume tasks, batch them, and call embedding APIs.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ postgres_service: Any,
90
+ num_workers: int = 2,
91
+ batch_size: int = 10,
92
+ batch_timeout: float = 1.0,
93
+ openai_api_key: Optional[str] = None,
94
+ ):
95
+ """
96
+ Initialize embedding worker.
97
+
98
+ Args:
99
+ postgres_service: PostgresService instance for upserting embeddings
100
+ num_workers: Number of concurrent worker tasks
101
+ batch_size: Number of tasks to batch for API call
102
+ batch_timeout: Max seconds to wait before processing partial batch
103
+ openai_api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
104
+ """
105
+ self.postgres_service = postgres_service
106
+ self.num_workers = num_workers
107
+ self.batch_size = batch_size
108
+ self.batch_timeout = batch_timeout
109
+
110
+ self.task_queue: asyncio.Queue = asyncio.Queue()
111
+ self.workers: list[asyncio.Task] = []
112
+ self.running = False
113
+ self._in_flight_count = 0 # Track tasks being processed (not just in queue)
114
+
115
+ # Store API key for direct HTTP requests
116
+ from ...settings import settings
117
+ self.openai_api_key = openai_api_key or settings.llm.openai_api_key
118
+ if not self.openai_api_key:
119
+ logger.warning(
120
+ "No OpenAI API key provided - embeddings will use zero vectors"
121
+ )
122
+
123
+ logger.debug(
124
+ f"Initialized EmbeddingWorker: {num_workers} workers, "
125
+ f"batch_size={batch_size}, timeout={batch_timeout}s"
126
+ )
127
+
128
+ async def start(self) -> None:
129
+ """Start worker pool."""
130
+ if self.running:
131
+ logger.debug("EmbeddingWorker already running")
132
+ return
133
+
134
+ self.running = True
135
+ logger.debug(f"Starting {self.num_workers} embedding workers")
136
+
137
+ for i in range(self.num_workers):
138
+ worker = asyncio.create_task(self._worker_loop(i))
139
+ self.workers.append(worker)
140
+
141
+ logger.debug("EmbeddingWorker started")
142
+
143
+ async def stop(self) -> None:
144
+ """Stop worker pool gracefully - processes remaining queue before stopping."""
145
+ if not self.running:
146
+ return
147
+
148
+ queue_size = self.task_queue.qsize()
149
+ in_flight = self._in_flight_count
150
+ logger.debug(f"Stopping EmbeddingWorker (queue={queue_size}, in_flight={in_flight})")
151
+
152
+ # Wait for both queue to drain AND in-flight tasks to complete
153
+ max_wait = 30 # 30 seconds max
154
+ waited = 0.0
155
+ while (not self.task_queue.empty() or self._in_flight_count > 0) and waited < max_wait:
156
+ await asyncio.sleep(0.5)
157
+ waited += 0.5
158
+
159
+ if not self.task_queue.empty() or self._in_flight_count > 0:
160
+ remaining = self.task_queue.qsize() + self._in_flight_count
161
+ logger.warning(
162
+ f"EmbeddingWorker timeout: {remaining} tasks remaining after {max_wait}s"
163
+ )
164
+ raise RuntimeError(
165
+ f"{remaining} embedding tasks did not complete within {max_wait}s timeout"
166
+ )
167
+
168
+ self.running = False
169
+
170
+ # Cancel all workers
171
+ for worker in self.workers:
172
+ worker.cancel()
173
+
174
+ # Wait for workers to finish
175
+ await asyncio.gather(*self.workers, return_exceptions=True)
176
+
177
+ self.workers.clear()
178
+ logger.debug("EmbeddingWorker stopped")
179
+
180
+ async def queue_task(self, task: EmbeddingTask) -> None:
181
+ """
182
+ Queue embedding task for background processing.
183
+
184
+ Returns immediately (non-blocking).
185
+
186
+ Args:
187
+ task: Embedding task to process
188
+ """
189
+ await self.task_queue.put(task)
190
+ logger.debug(
191
+ f"Queued embedding task: {task.table_name}.{task.field_name} "
192
+ f"(queue size: {self.task_queue.qsize()})"
193
+ )
194
+
195
+ async def _worker_loop(self, worker_id: int) -> None:
196
+ """
197
+ Worker loop: consume tasks, batch, and generate embeddings.
198
+
199
+ Args:
200
+ worker_id: Unique worker identifier
201
+ """
202
+ logger.debug(f"Worker {worker_id} started")
203
+
204
+ while self.running:
205
+ try:
206
+ # Collect batch of tasks
207
+ batch = await self._collect_batch()
208
+
209
+ if not batch:
210
+ continue
211
+
212
+ # Track in-flight tasks
213
+ self._in_flight_count += len(batch)
214
+
215
+ logger.debug(f"Worker {worker_id} processing batch of {len(batch)} tasks")
216
+
217
+ try:
218
+ # Generate embeddings for batch
219
+ await self._process_batch(batch)
220
+ logger.debug(f"Worker {worker_id} completed batch")
221
+ finally:
222
+ # Always decrement in-flight count, even on error
223
+ self._in_flight_count -= len(batch)
224
+
225
+ except asyncio.CancelledError:
226
+ logger.debug(f"Worker {worker_id} cancelled")
227
+ break
228
+ except Exception as e:
229
+ logger.error(f"Worker {worker_id} error: {e}", exc_info=True)
230
+ # Continue processing (don't crash worker on error)
231
+ await asyncio.sleep(1)
232
+
233
+ logger.debug(f"Worker {worker_id} stopped")
234
+
235
+ async def _collect_batch(self) -> list[EmbeddingTask]:
236
+ """
237
+ Collect batch of tasks from queue.
238
+
239
+ Waits for first task, then collects up to batch_size or until timeout.
240
+
241
+ Returns:
242
+ List of tasks to process
243
+ """
244
+ batch = []
245
+
246
+ try:
247
+ # Wait for first task
248
+ first_task = await asyncio.wait_for(
249
+ self.task_queue.get(), timeout=self.batch_timeout
250
+ )
251
+ batch.append(first_task)
252
+
253
+ # Collect additional tasks (up to batch_size)
254
+ while len(batch) < self.batch_size:
255
+ try:
256
+ task = await asyncio.wait_for(
257
+ self.task_queue.get(), timeout=0.1 # Quick timeout
258
+ )
259
+ batch.append(task)
260
+ except asyncio.TimeoutError:
261
+ # No more tasks available quickly
262
+ break
263
+
264
+ except asyncio.TimeoutError:
265
+ # No tasks available (timeout on first task)
266
+ pass
267
+
268
+ return batch
269
+
270
+ async def _process_batch(self, batch: list[EmbeddingTask]) -> None:
271
+ """
272
+ Process batch of embedding tasks.
273
+
274
+ Generates embeddings via API and upserts to database.
275
+
276
+ Args:
277
+ batch: List of tasks to process
278
+ """
279
+ if not batch:
280
+ return
281
+
282
+ # Group by provider/model for efficient batching
283
+ # Future enhancement: group heterogeneous batches by provider/model
284
+ provider = batch[0].provider
285
+ model = batch[0].model
286
+
287
+ # Extract text content for embedding
288
+ texts = [task.content for task in batch]
289
+
290
+ try:
291
+ # Generate embeddings
292
+ embeddings = await self._generate_embeddings_api(
293
+ texts=texts, provider=provider, model=model
294
+ )
295
+
296
+ # Upsert to database
297
+ logger.debug(f"Upserting {len(embeddings)} embeddings to database...")
298
+ await self._upsert_embeddings(batch, embeddings)
299
+
300
+ logger.debug(
301
+ f"Successfully generated and stored {len(embeddings)} embeddings "
302
+ f"(provider={provider}, model={model})"
303
+ )
304
+
305
+ except Exception as e:
306
+ logger.error(f"Failed to process embedding batch: {e}", exc_info=True)
307
+ # Re-raise to signal failure - embeddings MUST NOT fail silently
308
+ raise RuntimeError(
309
+ f"Embedding generation failed for {len(batch)} resources: {e}"
310
+ ) from e
311
+
312
+ async def _generate_embeddings_api(
313
+ self, texts: list[str], provider: str, model: str
314
+ ) -> list[list[float]]:
315
+ """
316
+ Generate embeddings via external API.
317
+
318
+ Args:
319
+ texts: List of text strings to embed
320
+ provider: Embedding provider (openai, cohere, etc.)
321
+ model: Model name
322
+
323
+ Returns:
324
+ List of embedding vectors (1536 dimensions for text-embedding-3-small)
325
+ """
326
+ if provider == "openai" and self.openai_api_key:
327
+ try:
328
+ logger.debug(
329
+ f"Generating OpenAI embeddings for {len(texts)} texts using {model}"
330
+ )
331
+
332
+ # Call OpenAI embeddings API using httpx
333
+ async with httpx.AsyncClient() as client:
334
+ response = await client.post(
335
+ "https://api.openai.com/v1/embeddings",
336
+ headers={
337
+ "Authorization": f"Bearer {self.openai_api_key}",
338
+ "Content-Type": "application/json",
339
+ },
340
+ json={"input": texts, "model": model},
341
+ timeout=60.0,
342
+ )
343
+ response.raise_for_status()
344
+
345
+ # Extract embeddings from response
346
+ data = response.json()
347
+ embeddings = [item["embedding"] for item in data["data"]]
348
+
349
+ logger.debug(
350
+ f"Successfully generated {len(embeddings)} embeddings from OpenAI"
351
+ )
352
+ return embeddings
353
+
354
+ except Exception as e:
355
+ logger.error(
356
+ f"Failed to generate embeddings from OpenAI: {e}", exc_info=True
357
+ )
358
+ # Fall through to zero vectors
359
+
360
+ # Fallback to zero vectors if no API key or error
361
+ logger.warning(
362
+ f"Using zero-vector embeddings for {len(texts)} texts "
363
+ f"(provider={provider}, model={model})"
364
+ )
365
+ dimension = 1536 # text-embedding-3-small dimension
366
+ return [[0.0] * dimension for _ in texts]
367
+
368
+ async def _upsert_embeddings(
369
+ self, tasks: list[EmbeddingTask], embeddings: list[list[float]]
370
+ ) -> None:
371
+ """
372
+ Upsert embeddings to database.
373
+
374
+ Args:
375
+ tasks: List of embedding tasks
376
+ embeddings: Generated embedding vectors
377
+ """
378
+ if len(tasks) != len(embeddings):
379
+ raise ValueError(
380
+ f"Task count ({len(tasks)}) != embedding count ({len(embeddings)})"
381
+ )
382
+
383
+ for task, embedding in zip(tasks, embeddings):
384
+ table_name = f"embeddings_{task.table_name}"
385
+
386
+ # Generate deterministic ID from key fields (entity_id, field_name, provider)
387
+ key_string = f"{task.entity_id}:{task.field_name}:{task.provider}"
388
+ embedding_id = str(uuid.UUID(hashlib.md5(key_string.encode()).hexdigest()))
389
+
390
+ # Build upsert SQL - conflict on deterministic ID
391
+ sql = f"""
392
+ INSERT INTO {table_name} (
393
+ id,
394
+ entity_id,
395
+ field_name,
396
+ provider,
397
+ model,
398
+ embedding,
399
+ created_at,
400
+ updated_at
401
+ )
402
+ VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
403
+ ON CONFLICT (id)
404
+ DO UPDATE SET
405
+ model = EXCLUDED.model,
406
+ embedding = EXCLUDED.embedding,
407
+ updated_at = CURRENT_TIMESTAMP;
408
+ """
409
+
410
+ try:
411
+ # Convert embedding list to PostgreSQL array format
412
+ embedding_str = "[" + ",".join(str(x) for x in embedding) + "]"
413
+
414
+ await self.postgres_service.execute(
415
+ sql,
416
+ (
417
+ embedding_id,
418
+ task.entity_id,
419
+ task.field_name,
420
+ task.provider,
421
+ task.model,
422
+ embedding_str, # pgvector expects string format
423
+ ),
424
+ )
425
+
426
+ logger.debug(
427
+ f"Upserted embedding: {task.table_name}.{task.entity_id}.{task.field_name}"
428
+ )
429
+
430
+ except Exception as e:
431
+ logger.error(
432
+ f"Failed to upsert embedding for {task.entity_id}: {e}",
433
+ exc_info=True,
434
+ )
435
+ raise