|
9 | 9 | from decimal import Decimal |
10 | 10 | from typing import Any, TYPE_CHECKING |
11 | 11 | from uuid import UUID, uuid4 |
| 12 | +import math |
| 13 | +import os |
12 | 14 | import structlog |
13 | 15 |
|
14 | 16 | from .models import ( |
|
31 | 33 |
|
32 | 34 |
|
33 | 35 | class MemoryService(ServiceBase): |
34 | | - """Main memory service orchestrating 5-tier memory hierarchy.""" |
| 36 | + """Main memory service orchestrating 5-tier memory hierarchy. |
| 37 | +
|
| 38 | + Tier-specific managers (WorkingMemoryManager, SessionMemoryManager, |
| 39 | + EpisodicMemoryManager, SemanticMemoryManager) are available in this |
| 40 | + package for advanced tier operations including token estimation, priority |
| 41 | + queuing, and knowledge graph. Current implementation uses Redis-backed |
| 42 | + dicts for T2/T3 and Postgres/Weaviate for T4/T5. Wire tier managers |
| 43 | + for advanced features post-MVP. |
| 44 | + """ |
35 | 45 |
|
36 | 46 | # Crisis keywords that force permanent retention (safety override) |
37 | 47 | CRISIS_KEYWORDS: frozenset[str] = frozenset({ |
@@ -105,6 +115,25 @@ async def shutdown(self) -> None: |
105 | 115 | pass |
106 | 116 | self._initialized = False |
107 | 117 |
|
| 118 | + async def _generate_embedding(self, text: str) -> list[float] | None: |
| 119 | + """Generate text embedding via OpenAI API. Returns None if unavailable.""" |
| 120 | + api_key = os.environ.get("OPENAI_API_KEY") |
| 121 | + if not api_key or not text.strip(): |
| 122 | + return None |
| 123 | + try: |
| 124 | + import httpx |
| 125 | + async with httpx.AsyncClient(timeout=10.0) as client: |
| 126 | + resp = await client.post( |
| 127 | + "https://api.openai.com/v1/embeddings", |
| 128 | + json={"model": "text-embedding-3-small", "input": text[:8000]}, |
| 129 | + headers={"Authorization": f"Bearer {api_key}"}, |
| 130 | + ) |
| 131 | + resp.raise_for_status() |
| 132 | + return resp.json()["data"][0]["embedding"] |
| 133 | + except Exception: |
| 134 | + logger.debug("embedding_generation_failed", text_len=len(text)) |
| 135 | + return None |
| 136 | + |
108 | 137 | async def store_memory(self, user_id: UUID, session_id: UUID | None, content: str, |
109 | 138 | content_type: str, tier: str, retention_category: str, |
110 | 139 | importance_score: Decimal, metadata: dict[str, Any]) -> StoreMemoryResult: |
@@ -138,11 +167,13 @@ async def store_memory(self, user_id: UUID, session_id: UUID | None, content: st |
138 | 167 | "tier_4_episodic": CollectionName.SESSION_SUMMARY.value, |
139 | 168 | "tier_5_semantic": CollectionName.USER_FACT.value, |
140 | 169 | } |
| 170 | + embedding = await self._generate_embedding(content) |
141 | 171 | vector_record = VectorRecord( |
142 | 172 | record_id=record.record_id, user_id=user_id, |
143 | 173 | session_id=session_id, content=content, |
144 | 174 | collection=_tier_to_collection.get(tier, CollectionName.CONVERSATION_MEMORY.value), |
145 | 175 | importance=float(importance_score), metadata=metadata, |
| 176 | + embedding=embedding or [], |
146 | 177 | ) |
147 | 178 | await self._weaviate_repo.store_vector(vector_record) |
148 | 179 | except Exception: |
@@ -247,9 +278,10 @@ async def assemble_context(self, user_id: UUID, session_id: UUID | None, |
247 | 278 | ) |
248 | 279 | context = self._build_basic_context(user_id, session_id, current_message, token_budget) |
249 | 280 | assembly_time_ms = int((time.perf_counter() - start_time) * 1000) |
| 281 | + estimated_tokens = len(context) // 4 # chars/4 matches ContextAssembler._estimate_tokens |
250 | 282 | return ContextAssemblyResult( |
251 | | - assembled_context=context, total_tokens=len(context.split()), |
252 | | - token_breakdown={"basic": len(context.split())}, sources_used=["working_memory"], |
| 283 | + assembled_context=context, total_tokens=estimated_tokens, |
| 284 | + token_breakdown={"basic": estimated_tokens}, sources_used=["working_memory"], |
253 | 285 | assembly_time_ms=assembly_time_ms, |
254 | 286 | ) |
255 | 287 |
|
|
0 commit comments