remdb 0.3.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.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/__init__.py +2 -0
- rem/agentic/README.md +650 -0
- rem/agentic/__init__.py +39 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +8 -0
- rem/agentic/context.py +148 -0
- rem/agentic/context_builder.py +329 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +107 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +151 -0
- rem/agentic/providers/phoenix.py +674 -0
- rem/agentic/providers/pydantic_ai.py +572 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +396 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +231 -0
- rem/api/README.md +420 -0
- rem/api/main.py +324 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +536 -0
- rem/api/mcp_router/server.py +213 -0
- rem/api/mcp_router/tools.py +584 -0
- rem/api/routers/auth.py +229 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/completions.py +281 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +124 -0
- rem/api/routers/chat/streaming.py +185 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +26 -0
- rem/auth/middleware.py +100 -0
- rem/auth/providers/__init__.py +13 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +455 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +126 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +566 -0
- rem/cli/commands/configure.py +497 -0
- rem/cli/commands/db.py +493 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1302 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +245 -0
- rem/cli/commands/schema.py +183 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +96 -0
- rem/config.py +237 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +64 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +628 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +243 -0
- rem/models/entities/__init__.py +43 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +35 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +191 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/user.py +85 -0
- rem/py.typed +0 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +128 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +16 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +806 -0
- rem/services/content/service.py +676 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +336 -0
- rem/services/dreaming/moment_service.py +264 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +120 -0
- rem/services/embeddings/worker.py +421 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +686 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +575 -0
- rem/services/postgres/__init__.py +23 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
- rem/services/postgres/register_type.py +352 -0
- rem/services/postgres/repository.py +337 -0
- rem/services/postgres/schema_generator.py +379 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +354 -0
- rem/services/rem/README.md +304 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +145 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +527 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +6 -0
- rem/services/session/compression.py +360 -0
- rem/services/session/reload.py +77 -0
- rem/settings.py +1235 -0
- rem/sql/002_install_models.sql +1068 -0
- rem/sql/background_indexes.sql +42 -0
- rem/sql/install_models.sql +1038 -0
- rem/sql/migrations/001_install.sql +503 -0
- rem/sql/migrations/002_install_models.sql +1202 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +583 -0
- rem/utils/__init__.py +43 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +423 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/markdown.py +16 -0
- rem/utils/model_helpers.py +236 -0
- rem/utils/schema_loader.py +336 -0
- rem/utils/sql_types.py +348 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +330 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +5 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- remdb-0.3.0.dist-info/METADATA +1455 -0
- remdb-0.3.0.dist-info/RECORD +187 -0
- remdb-0.3.0.dist-info/WHEEL +4 -0
- remdb-0.3.0.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
|