hindsight-api 0.3.0__py3-none-any.whl → 0.4.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.
- hindsight_api/admin/cli.py +59 -0
- hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
- hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
- hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
- hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
- hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
- hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
- hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
- hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
- hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
- hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
- hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
- hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
- hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
- hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
- hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
- hindsight_api/api/http.py +1119 -93
- hindsight_api/api/mcp.py +11 -191
- hindsight_api/config.py +145 -45
- hindsight_api/engine/consolidation/__init__.py +5 -0
- hindsight_api/engine/consolidation/consolidator.py +859 -0
- hindsight_api/engine/consolidation/prompts.py +69 -0
- hindsight_api/engine/cross_encoder.py +114 -9
- hindsight_api/engine/directives/__init__.py +5 -0
- hindsight_api/engine/directives/models.py +37 -0
- hindsight_api/engine/embeddings.py +102 -5
- hindsight_api/engine/interface.py +32 -13
- hindsight_api/engine/llm_wrapper.py +505 -43
- hindsight_api/engine/memory_engine.py +2090 -1089
- hindsight_api/engine/mental_models/__init__.py +14 -0
- hindsight_api/engine/mental_models/models.py +53 -0
- hindsight_api/engine/reflect/__init__.py +18 -0
- hindsight_api/engine/reflect/agent.py +933 -0
- hindsight_api/engine/reflect/models.py +109 -0
- hindsight_api/engine/reflect/observations.py +186 -0
- hindsight_api/engine/reflect/prompts.py +483 -0
- hindsight_api/engine/reflect/tools.py +437 -0
- hindsight_api/engine/reflect/tools_schema.py +250 -0
- hindsight_api/engine/response_models.py +130 -4
- hindsight_api/engine/retain/bank_utils.py +79 -201
- hindsight_api/engine/retain/fact_extraction.py +81 -48
- hindsight_api/engine/retain/fact_storage.py +5 -8
- hindsight_api/engine/retain/link_utils.py +5 -8
- hindsight_api/engine/retain/orchestrator.py +1 -55
- hindsight_api/engine/retain/types.py +2 -2
- hindsight_api/engine/search/graph_retrieval.py +2 -2
- hindsight_api/engine/search/link_expansion_retrieval.py +164 -29
- hindsight_api/engine/search/mpfp_retrieval.py +1 -1
- hindsight_api/engine/search/retrieval.py +14 -14
- hindsight_api/engine/search/think_utils.py +41 -140
- hindsight_api/engine/search/trace.py +0 -1
- hindsight_api/engine/search/tracer.py +2 -5
- hindsight_api/engine/search/types.py +0 -3
- hindsight_api/engine/task_backend.py +112 -196
- hindsight_api/engine/utils.py +0 -151
- hindsight_api/extensions/__init__.py +10 -1
- hindsight_api/extensions/builtin/tenant.py +5 -1
- hindsight_api/extensions/operation_validator.py +81 -4
- hindsight_api/extensions/tenant.py +26 -0
- hindsight_api/main.py +16 -5
- hindsight_api/mcp_local.py +12 -53
- hindsight_api/mcp_tools.py +494 -0
- hindsight_api/models.py +0 -2
- hindsight_api/worker/__init__.py +11 -0
- hindsight_api/worker/main.py +296 -0
- hindsight_api/worker/poller.py +486 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/METADATA +12 -6
- hindsight_api-0.4.0.dist-info/RECORD +112 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/entry_points.txt +1 -0
- hindsight_api/engine/retain/observation_regeneration.py +0 -254
- hindsight_api/engine/search/observation_utils.py +0 -125
- hindsight_api/engine/search/scoring.py +0 -159
- hindsight_api-0.3.0.dist-info/RECORD +0 -82
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Prompts for the consolidation engine."""
|
|
2
|
+
|
|
3
|
+
CONSOLIDATION_SYSTEM_PROMPT = """You are a memory consolidation system. Your job is to convert facts into durable knowledge (observations) and merge with existing knowledge when appropriate.
|
|
4
|
+
|
|
5
|
+
You must output ONLY valid JSON with no markdown formatting, no code blocks, and no additional text.
|
|
6
|
+
|
|
7
|
+
## EXTRACT DURABLE KNOWLEDGE, NOT EPHEMERAL STATE
|
|
8
|
+
Facts often describe events or actions. Extract the DURABLE KNOWLEDGE implied by the fact, not the transient state.
|
|
9
|
+
|
|
10
|
+
Examples of extracting durable knowledge:
|
|
11
|
+
- "User moved to Room 203" -> "Room 203 exists" (location exists, not where user is now)
|
|
12
|
+
- "User visited Acme Corp at Room 105" -> "Acme Corp is located in Room 105"
|
|
13
|
+
- "User took the elevator to floor 3" -> "Floor 3 is accessible by elevator"
|
|
14
|
+
- "User met Sarah at the lobby" -> "Sarah can be found at the lobby"
|
|
15
|
+
|
|
16
|
+
DO NOT track current user position/state as knowledge - that changes constantly.
|
|
17
|
+
DO track permanent facts learned from the user's actions.
|
|
18
|
+
|
|
19
|
+
## PRESERVE SPECIFIC DETAILS
|
|
20
|
+
Keep names, locations, numbers, and other specifics. Do NOT:
|
|
21
|
+
- Abstract into general principles
|
|
22
|
+
- Generate business insights
|
|
23
|
+
- Make knowledge generic
|
|
24
|
+
|
|
25
|
+
GOOD examples:
|
|
26
|
+
- Fact: "John likes pizza" -> "John likes pizza"
|
|
27
|
+
- Fact: "Alice works at Google" -> "Alice works at Google"
|
|
28
|
+
|
|
29
|
+
BAD examples:
|
|
30
|
+
- "John likes pizza" -> "Understanding dietary preferences helps..." (TOO ABSTRACT)
|
|
31
|
+
- "User is at Room 203" -> "User is currently at Room 203" (EPHEMERAL STATE)
|
|
32
|
+
|
|
33
|
+
## MERGE RULES (when comparing to existing observations):
|
|
34
|
+
1. REDUNDANT: Same information worded differently → update existing
|
|
35
|
+
2. CONTRADICTION: Opposite information about same topic → update with history (e.g., "used to X, now Y")
|
|
36
|
+
3. UPDATE: New state replacing old state → update with history
|
|
37
|
+
|
|
38
|
+
## CRITICAL RULES:
|
|
39
|
+
- NEVER merge facts about DIFFERENT people
|
|
40
|
+
- NEVER merge unrelated topics (food preferences vs work vs hobbies)
|
|
41
|
+
- When merging contradictions, capture the CHANGE (before → after)
|
|
42
|
+
- Keep observations focused on ONE specific topic per person
|
|
43
|
+
- The "text" field MUST contain durable knowledge, not ephemeral state
|
|
44
|
+
- Do NOT include "tags" in output - tags are handled automatically"""
|
|
45
|
+
|
|
46
|
+
CONSOLIDATION_USER_PROMPT = """Analyze this new fact and consolidate into knowledge.
|
|
47
|
+
{mission_section}
|
|
48
|
+
NEW FACT: {fact_text}
|
|
49
|
+
|
|
50
|
+
EXISTING OBSERVATIONS:
|
|
51
|
+
{observations_text}
|
|
52
|
+
|
|
53
|
+
Instructions:
|
|
54
|
+
1. First, extract the DURABLE KNOWLEDGE from the fact (not ephemeral state like "user is at X")
|
|
55
|
+
2. Then compare with existing observations:
|
|
56
|
+
- If an observation covers the same topic: UPDATE it with the new knowledge
|
|
57
|
+
- If no observation covers the topic: CREATE a new one
|
|
58
|
+
|
|
59
|
+
Output JSON array of actions (ALWAYS an array, even for single action):
|
|
60
|
+
[
|
|
61
|
+
{{"action": "update", "learning_id": "uuid", "text": "updated durable knowledge", "reason": "..."}},
|
|
62
|
+
{{"action": "create", "text": "new durable knowledge", "reason": "..."}}
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
If NO consolidation is needed (fact is purely ephemeral with no durable knowledge):
|
|
66
|
+
[]
|
|
67
|
+
|
|
68
|
+
If no observations exist and fact contains durable knowledge:
|
|
69
|
+
[{{"action": "create", "text": "durable knowledge text", "reason": "new topic"}}]"""
|
|
@@ -130,13 +130,28 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
|
|
130
130
|
"Install it with: pip install sentence-transformers"
|
|
131
131
|
)
|
|
132
132
|
|
|
133
|
-
# Note: We use CPU even when GPU/MPS is available because:
|
|
134
|
-
# 1. The reranker model (MiniLM) is tiny (~22M params)
|
|
135
|
-
# 2. Batch sizes are small (~100-200 pairs)
|
|
136
|
-
# 3. Data transfer overhead to GPU outweighs compute benefit
|
|
137
|
-
# 4. CPU inference is actually faster for this workload
|
|
138
133
|
logger.info(f"Reranker: initializing local provider with model {self.model_name}")
|
|
139
|
-
|
|
134
|
+
|
|
135
|
+
# Determine device based on hardware availability.
|
|
136
|
+
# We always set low_cpu_mem_usage=False to prevent lazy loading (meta tensors)
|
|
137
|
+
# which can cause issues when accelerate is installed but no GPU is available.
|
|
138
|
+
# Note: We do NOT use device_map because CrossEncoder internally calls .to(device)
|
|
139
|
+
# after loading, which conflicts with accelerate's device_map handling.
|
|
140
|
+
import torch
|
|
141
|
+
|
|
142
|
+
# Check for GPU (CUDA) or Apple Silicon (MPS)
|
|
143
|
+
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
|
|
144
|
+
|
|
145
|
+
if has_gpu:
|
|
146
|
+
device = None # Let sentence-transformers auto-detect GPU/MPS
|
|
147
|
+
else:
|
|
148
|
+
device = "cpu"
|
|
149
|
+
|
|
150
|
+
self._model = CrossEncoder(
|
|
151
|
+
self.model_name,
|
|
152
|
+
device=device,
|
|
153
|
+
model_kwargs={"low_cpu_mem_usage": False},
|
|
154
|
+
)
|
|
140
155
|
|
|
141
156
|
# Initialize shared executor (limited workers naturally limits concurrency)
|
|
142
157
|
if LocalSTCrossEncoder._executor is None:
|
|
@@ -148,11 +163,101 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
|
|
148
163
|
else:
|
|
149
164
|
logger.info("Reranker: local provider initialized (using existing executor)")
|
|
150
165
|
|
|
166
|
+
def _is_xpc_error(self, error: Exception) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check if an error is an XPC connection error (macOS daemon issue).
|
|
169
|
+
|
|
170
|
+
On macOS, long-running daemons can lose XPC connections to system services
|
|
171
|
+
when the process is idle for extended periods.
|
|
172
|
+
"""
|
|
173
|
+
error_str = str(error).lower()
|
|
174
|
+
return "xpc_error_connection_invalid" in error_str or "xpc error" in error_str
|
|
175
|
+
|
|
176
|
+
def _reinitialize_model_sync(self) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Clear and reinitialize the cross-encoder model synchronously.
|
|
179
|
+
|
|
180
|
+
This is used to recover from XPC errors on macOS where the
|
|
181
|
+
PyTorch/MPS backend loses its connection to system services.
|
|
182
|
+
"""
|
|
183
|
+
logger.warning(f"Reinitializing reranker model {self.model_name} due to backend error")
|
|
184
|
+
|
|
185
|
+
# Clear existing model
|
|
186
|
+
self._model = None
|
|
187
|
+
|
|
188
|
+
# Force garbage collection to free resources
|
|
189
|
+
import gc
|
|
190
|
+
|
|
191
|
+
import torch
|
|
192
|
+
|
|
193
|
+
gc.collect()
|
|
194
|
+
|
|
195
|
+
# If using CUDA/MPS, clear the cache
|
|
196
|
+
if torch.cuda.is_available():
|
|
197
|
+
torch.cuda.empty_cache()
|
|
198
|
+
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
199
|
+
try:
|
|
200
|
+
torch.mps.empty_cache()
|
|
201
|
+
except AttributeError:
|
|
202
|
+
pass # Method might not exist in all PyTorch versions
|
|
203
|
+
|
|
204
|
+
# Reinitialize the model
|
|
205
|
+
try:
|
|
206
|
+
from sentence_transformers import CrossEncoder
|
|
207
|
+
except ImportError:
|
|
208
|
+
raise ImportError(
|
|
209
|
+
"sentence-transformers is required for LocalSTCrossEncoder. "
|
|
210
|
+
"Install it with: pip install sentence-transformers"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Determine device based on hardware availability
|
|
214
|
+
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
|
|
215
|
+
|
|
216
|
+
if has_gpu:
|
|
217
|
+
device = None # Let sentence-transformers auto-detect GPU/MPS
|
|
218
|
+
else:
|
|
219
|
+
device = "cpu"
|
|
220
|
+
|
|
221
|
+
self._model = CrossEncoder(
|
|
222
|
+
self.model_name,
|
|
223
|
+
device=device,
|
|
224
|
+
model_kwargs={"low_cpu_mem_usage": False},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
logger.info("Reranker: local provider reinitialized successfully")
|
|
228
|
+
|
|
229
|
+
def _predict_with_recovery(self, pairs: list[tuple[str, str]]) -> list[float]:
|
|
230
|
+
"""
|
|
231
|
+
Predict with automatic recovery from XPC errors.
|
|
232
|
+
|
|
233
|
+
This runs synchronously in the thread pool.
|
|
234
|
+
"""
|
|
235
|
+
max_retries = 1
|
|
236
|
+
for attempt in range(max_retries + 1):
|
|
237
|
+
try:
|
|
238
|
+
scores = self._model.predict(pairs, show_progress_bar=False)
|
|
239
|
+
return scores.tolist() if hasattr(scores, "tolist") else list(scores)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
# Check if this is an XPC error (macOS daemon issue)
|
|
242
|
+
if self._is_xpc_error(e) and attempt < max_retries:
|
|
243
|
+
logger.warning(f"XPC error detected in reranker (attempt {attempt + 1}): {e}")
|
|
244
|
+
try:
|
|
245
|
+
self._reinitialize_model_sync()
|
|
246
|
+
logger.info("Reranker reinitialized successfully, retrying prediction")
|
|
247
|
+
continue
|
|
248
|
+
except Exception as reinit_error:
|
|
249
|
+
logger.error(f"Failed to reinitialize reranker: {reinit_error}")
|
|
250
|
+
raise Exception(f"Failed to recover from XPC error: {str(e)}")
|
|
251
|
+
else:
|
|
252
|
+
# Not an XPC error or out of retries
|
|
253
|
+
raise
|
|
254
|
+
|
|
151
255
|
async def predict(self, pairs: list[tuple[str, str]]) -> list[float]:
|
|
152
256
|
"""
|
|
153
257
|
Score query-document pairs for relevance.
|
|
154
258
|
|
|
155
259
|
Uses a dedicated thread pool with limited workers to prevent CPU thrashing.
|
|
260
|
+
Automatically recovers from XPC errors on macOS by reinitializing the model.
|
|
156
261
|
|
|
157
262
|
Args:
|
|
158
263
|
pairs: List of (query, document) tuples to score
|
|
@@ -165,11 +270,11 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
|
|
165
270
|
|
|
166
271
|
# Use dedicated executor - limited workers naturally limits concurrency
|
|
167
272
|
loop = asyncio.get_event_loop()
|
|
168
|
-
|
|
273
|
+
return await loop.run_in_executor(
|
|
169
274
|
LocalSTCrossEncoder._executor,
|
|
170
|
-
|
|
275
|
+
self._predict_with_recovery,
|
|
276
|
+
pairs,
|
|
171
277
|
)
|
|
172
|
-
return scores.tolist() if hasattr(scores, "tolist") else list(scores)
|
|
173
278
|
|
|
174
279
|
|
|
175
280
|
class RemoteTEICrossEncoder(CrossEncoderModel):
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Pydantic models for directives."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Directive(BaseModel):
|
|
10
|
+
"""A directive is a hard rule injected into prompts.
|
|
11
|
+
|
|
12
|
+
Directives are user-defined rules that guide agent behavior. Unlike mental models
|
|
13
|
+
which are automatically consolidated from memories, directives are explicit
|
|
14
|
+
instructions that are always included in relevant prompts.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
- "Always respond in formal English"
|
|
18
|
+
- "Never share personal data with third parties"
|
|
19
|
+
- "Prefer conservative investment recommendations"
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
id: UUID = Field(description="Unique identifier")
|
|
23
|
+
bank_id: str = Field(description="Bank this directive belongs to")
|
|
24
|
+
name: str = Field(description="Human-readable name")
|
|
25
|
+
content: str = Field(description="The directive text to inject into prompts")
|
|
26
|
+
priority: int = Field(default=0, description="Higher priority directives are injected first")
|
|
27
|
+
is_active: bool = Field(default=True, description="Whether this directive is currently active")
|
|
28
|
+
tags: list[str] = Field(default_factory=list, description="Tags for filtering")
|
|
29
|
+
created_at: datetime = Field(
|
|
30
|
+
default_factory=lambda: datetime.now(timezone.utc), description="When this directive was created"
|
|
31
|
+
)
|
|
32
|
+
updated_at: datetime = Field(
|
|
33
|
+
default_factory=lambda: datetime.now(timezone.utc), description="When this directive was last updated"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
class Config:
|
|
37
|
+
from_attributes = True
|
|
@@ -128,20 +128,98 @@ class LocalSTEmbeddings(Embeddings):
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
logger.info(f"Embeddings: initializing local provider with model {self.model_name}")
|
|
131
|
-
|
|
132
|
-
#
|
|
131
|
+
|
|
132
|
+
# Determine device based on hardware availability.
|
|
133
|
+
# We always set low_cpu_mem_usage=False to prevent lazy loading (meta tensors)
|
|
134
|
+
# which can cause issues when accelerate is installed but no GPU is available.
|
|
135
|
+
import torch
|
|
136
|
+
|
|
137
|
+
# Check for GPU (CUDA) or Apple Silicon (MPS)
|
|
138
|
+
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
|
|
139
|
+
|
|
140
|
+
if has_gpu:
|
|
141
|
+
device = None # Let sentence-transformers auto-detect GPU/MPS
|
|
142
|
+
else:
|
|
143
|
+
device = "cpu"
|
|
144
|
+
|
|
133
145
|
self._model = SentenceTransformer(
|
|
134
146
|
self.model_name,
|
|
135
|
-
|
|
147
|
+
device=device,
|
|
148
|
+
model_kwargs={"low_cpu_mem_usage": False},
|
|
136
149
|
)
|
|
137
150
|
|
|
138
151
|
self._dimension = self._model.get_sentence_embedding_dimension()
|
|
139
152
|
logger.info(f"Embeddings: local provider initialized (dim: {self._dimension})")
|
|
140
153
|
|
|
154
|
+
def _is_xpc_error(self, error: Exception) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check if an error is an XPC connection error (macOS daemon issue).
|
|
157
|
+
|
|
158
|
+
On macOS, long-running daemons can lose XPC connections to system services
|
|
159
|
+
when the process is idle for extended periods.
|
|
160
|
+
"""
|
|
161
|
+
error_str = str(error).lower()
|
|
162
|
+
return "xpc_error_connection_invalid" in error_str or "xpc error" in error_str
|
|
163
|
+
|
|
164
|
+
def _reinitialize_model_sync(self) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Clear and reinitialize the embedding model synchronously.
|
|
167
|
+
|
|
168
|
+
This is used to recover from XPC errors on macOS where the
|
|
169
|
+
PyTorch/MPS backend loses its connection to system services.
|
|
170
|
+
"""
|
|
171
|
+
logger.warning(f"Reinitializing embedding model {self.model_name} due to backend error")
|
|
172
|
+
|
|
173
|
+
# Clear existing model
|
|
174
|
+
self._model = None
|
|
175
|
+
|
|
176
|
+
# Force garbage collection to free resources
|
|
177
|
+
import gc
|
|
178
|
+
|
|
179
|
+
import torch
|
|
180
|
+
|
|
181
|
+
gc.collect()
|
|
182
|
+
|
|
183
|
+
# If using CUDA/MPS, clear the cache
|
|
184
|
+
if torch.cuda.is_available():
|
|
185
|
+
torch.cuda.empty_cache()
|
|
186
|
+
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
187
|
+
try:
|
|
188
|
+
torch.mps.empty_cache()
|
|
189
|
+
except AttributeError:
|
|
190
|
+
pass # Method might not exist in all PyTorch versions
|
|
191
|
+
|
|
192
|
+
# Reinitialize the model (inline version of initialize() but synchronous)
|
|
193
|
+
try:
|
|
194
|
+
from sentence_transformers import SentenceTransformer
|
|
195
|
+
except ImportError:
|
|
196
|
+
raise ImportError(
|
|
197
|
+
"sentence-transformers is required for LocalSTEmbeddings. "
|
|
198
|
+
"Install it with: pip install sentence-transformers"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Determine device based on hardware availability
|
|
202
|
+
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
|
|
203
|
+
|
|
204
|
+
if has_gpu:
|
|
205
|
+
device = None # Let sentence-transformers auto-detect GPU/MPS
|
|
206
|
+
else:
|
|
207
|
+
device = "cpu"
|
|
208
|
+
|
|
209
|
+
self._model = SentenceTransformer(
|
|
210
|
+
self.model_name,
|
|
211
|
+
device=device,
|
|
212
|
+
model_kwargs={"low_cpu_mem_usage": False},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
logger.info("Embeddings: local provider reinitialized successfully")
|
|
216
|
+
|
|
141
217
|
def encode(self, texts: list[str]) -> list[list[float]]:
|
|
142
218
|
"""
|
|
143
219
|
Generate embeddings for a list of texts.
|
|
144
220
|
|
|
221
|
+
Automatically recovers from XPC errors on macOS by reinitializing the model.
|
|
222
|
+
|
|
145
223
|
Args:
|
|
146
224
|
texts: List of text strings to encode
|
|
147
225
|
|
|
@@ -150,8 +228,27 @@ class LocalSTEmbeddings(Embeddings):
|
|
|
150
228
|
"""
|
|
151
229
|
if self._model is None:
|
|
152
230
|
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
|
|
153
|
-
|
|
154
|
-
|
|
231
|
+
|
|
232
|
+
# Try encoding with automatic recovery from XPC errors
|
|
233
|
+
max_retries = 1
|
|
234
|
+
for attempt in range(max_retries + 1):
|
|
235
|
+
try:
|
|
236
|
+
embeddings = self._model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
|
|
237
|
+
return [emb.tolist() for emb in embeddings]
|
|
238
|
+
except Exception as e:
|
|
239
|
+
# Check if this is an XPC error (macOS daemon issue)
|
|
240
|
+
if self._is_xpc_error(e) and attempt < max_retries:
|
|
241
|
+
logger.warning(f"XPC error detected in embedding generation (attempt {attempt + 1}): {e}")
|
|
242
|
+
try:
|
|
243
|
+
self._reinitialize_model_sync()
|
|
244
|
+
logger.info("Model reinitialized successfully, retrying embedding generation")
|
|
245
|
+
continue
|
|
246
|
+
except Exception as reinit_error:
|
|
247
|
+
logger.error(f"Failed to reinitialize model: {reinit_error}")
|
|
248
|
+
raise Exception(f"Failed to recover from XPC error: {str(e)}")
|
|
249
|
+
else:
|
|
250
|
+
# Not an XPC error or out of retries
|
|
251
|
+
raise
|
|
155
252
|
|
|
156
253
|
|
|
157
254
|
class RemoteTEIEmbeddings(Embeddings):
|
|
@@ -160,14 +160,14 @@ class MemoryEngineInterface(ABC):
|
|
|
160
160
|
request_context: "RequestContext",
|
|
161
161
|
) -> dict[str, Any]:
|
|
162
162
|
"""
|
|
163
|
-
Get bank profile including disposition and
|
|
163
|
+
Get bank profile including disposition and mission.
|
|
164
164
|
|
|
165
165
|
Args:
|
|
166
166
|
bank_id: The memory bank ID.
|
|
167
167
|
request_context: Request context for authentication.
|
|
168
168
|
|
|
169
169
|
Returns:
|
|
170
|
-
Bank profile dict.
|
|
170
|
+
Bank profile dict with bank_id, name, disposition, and mission.
|
|
171
171
|
"""
|
|
172
172
|
...
|
|
173
173
|
|
|
@@ -190,25 +190,44 @@ class MemoryEngineInterface(ABC):
|
|
|
190
190
|
...
|
|
191
191
|
|
|
192
192
|
@abstractmethod
|
|
193
|
-
async def
|
|
193
|
+
async def merge_bank_mission(
|
|
194
194
|
self,
|
|
195
195
|
bank_id: str,
|
|
196
196
|
new_info: str,
|
|
197
197
|
*,
|
|
198
|
-
update_disposition: bool = True,
|
|
199
198
|
request_context: "RequestContext",
|
|
200
199
|
) -> dict[str, Any]:
|
|
201
200
|
"""
|
|
202
|
-
Merge new
|
|
201
|
+
Merge new mission information into bank profile.
|
|
203
202
|
|
|
204
203
|
Args:
|
|
205
204
|
bank_id: The memory bank ID.
|
|
206
|
-
new_info: New
|
|
207
|
-
update_disposition: Whether to infer disposition from background.
|
|
205
|
+
new_info: New mission information to merge.
|
|
208
206
|
request_context: Request context for authentication.
|
|
209
207
|
|
|
210
208
|
Returns:
|
|
211
|
-
Updated
|
|
209
|
+
Updated mission info.
|
|
210
|
+
"""
|
|
211
|
+
...
|
|
212
|
+
|
|
213
|
+
@abstractmethod
|
|
214
|
+
async def set_bank_mission(
|
|
215
|
+
self,
|
|
216
|
+
bank_id: str,
|
|
217
|
+
mission: str,
|
|
218
|
+
*,
|
|
219
|
+
request_context: "RequestContext",
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Set the bank's mission (replaces existing).
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
bank_id: The memory bank ID.
|
|
226
|
+
mission: The mission text.
|
|
227
|
+
request_context: Request context for authentication.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Dict with bank_id and mission.
|
|
212
231
|
"""
|
|
213
232
|
...
|
|
214
233
|
|
|
@@ -518,7 +537,7 @@ class MemoryEngineInterface(ABC):
|
|
|
518
537
|
bank_id: str,
|
|
519
538
|
*,
|
|
520
539
|
request_context: "RequestContext",
|
|
521
|
-
) ->
|
|
540
|
+
) -> dict[str, Any]:
|
|
522
541
|
"""
|
|
523
542
|
List async operations for a bank.
|
|
524
543
|
|
|
@@ -527,7 +546,7 @@ class MemoryEngineInterface(ABC):
|
|
|
527
546
|
request_context: Request context for authentication.
|
|
528
547
|
|
|
529
548
|
Returns:
|
|
530
|
-
|
|
549
|
+
Dict with 'total' (int) and 'operations' (list of operation dicts).
|
|
531
550
|
"""
|
|
532
551
|
...
|
|
533
552
|
|
|
@@ -561,16 +580,16 @@ class MemoryEngineInterface(ABC):
|
|
|
561
580
|
bank_id: str,
|
|
562
581
|
*,
|
|
563
582
|
name: str | None = None,
|
|
564
|
-
|
|
583
|
+
mission: str | None = None,
|
|
565
584
|
request_context: "RequestContext",
|
|
566
585
|
) -> dict[str, Any]:
|
|
567
586
|
"""
|
|
568
|
-
Update bank name and/or
|
|
587
|
+
Update bank name and/or mission.
|
|
569
588
|
|
|
570
589
|
Args:
|
|
571
590
|
bank_id: The memory bank ID.
|
|
572
591
|
name: New bank name (optional).
|
|
573
|
-
|
|
592
|
+
mission: New mission text (optional, replaces existing).
|
|
574
593
|
request_context: Request context for authentication.
|
|
575
594
|
|
|
576
595
|
Returns:
|