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