Spaces:
Sleeping
Sleeping
| """ | |
| Public facade for the memory system. | |
| Import surface (backward-compatible): | |
| from memory.memory_manager import MemoryManager | |
| from memory.memory_manager import ShortTermMemory # used by eval_agent.py | |
| from memory.memory_manager import WorkingMemory # used by eval_agent.py | |
| from memory.memory_manager import LongTermMemory # used by eval_agent.py | |
| from memory.memory_manager import EpisodicMemory # used by eval_agent.py | |
| """ | |
| from memory.backends import _using_supabase | |
| from memory.stores import ( | |
| ShortTermMemory, | |
| WorkingMemory, | |
| LongTermMemory, | |
| EpisodicMemory, | |
| ) | |
| # Re-exported so existing callers (eval_agent.py) can still import directly | |
| # from memory.memory_manager without touching their import lines. | |
| __all__ = [ | |
| "MemoryManager", | |
| "ShortTermMemory", | |
| "WorkingMemory", | |
| "LongTermMemory", | |
| "EpisodicMemory", | |
| ] | |
| class MemoryManager: | |
| """ | |
| The only class the orchestrator talks to. | |
| The orchestrator calls three methods: | |
| build_memory_context() - before analysis starts | |
| save_completed_analysis() - after analysis finishes | |
| reset_short_term() - at the start of each new run | |
| Everything else (which DB, which embedding model, fallback logic) | |
| is hidden inside the four memory classes in memory.stores. | |
| This is the stable-interface / swappable-implementation pattern. | |
| The orchestrator has never changed even as the memory backend went | |
| from SQLite to ChromaDB to Supabase. | |
| """ | |
| def __init__(self): | |
| self.short_term = ShortTermMemory() | |
| self.long_term = LongTermMemory() | |
| self.episodic = EpisodicMemory() | |
| self.working = WorkingMemory() | |
| if _using_supabase(): | |
| print("[memory] Backend: Supabase (persistent)") | |
| else: | |
| print("[memory] Backend: SQLite local fallback " | |
| "(set SUPABASE_URL + SUPABASE_KEY for persistence)") | |
| def build_memory_context(self, ticker: str, user_id: str) -> str: | |
| """ | |
| Assemble the full memory block injected into every agent's system prompt. | |
| Order of priority (most -> least specific): | |
| 1. User's investment profile (preferences they've set) | |
| 2. History of THIS ticker specifically | |
| 3. Snippet from the last analysis of this ticker | |
| 4. Semantically similar past analyses (cross-ticker) | |
| 5. Current conversation context (short-term) | |
| """ | |
| parts = [] | |
| profile = self.long_term.get_user_profile(user_id) | |
| if profile: | |
| parts.append(f"## User investment profile:\n{profile}") | |
| history = self.episodic.get_ticker_history(user_id, ticker) | |
| if history: | |
| parts.append(history) | |
| past = self.long_term.recall_past_analyses(ticker, user_id, n=1) | |
| if past: | |
| parts.append(f"## Last analysis of {ticker}:\n{past[0]}") | |
| similar = self.long_term.semantic_search( | |
| f"stock analysis {ticker} {self._sector_hint(ticker)}", user_id, n=2 | |
| ) | |
| if similar: | |
| parts.append("## Similar past analyses:\n" + "\n---\n".join(similar)) | |
| conv = self.short_term.get_context() | |
| if conv: | |
| parts.append(conv) | |
| if not parts: | |
| return "" | |
| return ( | |
| "## MEMORY CONTEXT\n" | |
| + "\n\n".join(parts) | |
| + "\n\nUse this context to personalise analysis. " | |
| "Reference past calls by date when relevant.\n---\n" | |
| ) | |
| def save_completed_analysis(self, ticker: str, user_id: str, | |
| report: str, recommendation: str, | |
| confidence: int, price: float): | |
| """Persist a completed, human-approved analysis to both memory stores.""" | |
| self.long_term.store_analysis( | |
| ticker, user_id, report, recommendation, confidence | |
| ) | |
| self.episodic.record( | |
| user_id, ticker, recommendation, confidence, price, report | |
| ) | |
| def reset_short_term(self): | |
| """Call at the start of each new analysis run.""" | |
| self.short_term.clear() | |
| self.working.reset() | |
| def _sector_hint(self, ticker: str) -> str: | |
| """Quick sector lookup for better semantic search queries.""" | |
| try: | |
| import yfinance as yf | |
| return yf.Ticker(ticker).info.get("sector", "") | |
| except Exception: | |
| return "" | |
| # Convenience pass-throughs ----------------------------------------------- | |
| def store_preference(self, user_id: str, preference: str): | |
| """Save a user preference e.g. 'risk averse', 'prefers UK stocks'""" | |
| self.long_term.store_preference(user_id, preference) | |
| def get_all_history(self, user_id: str) -> list[dict]: | |
| """For the Streamlit sidebar - all past recommendations.""" | |
| return self.episodic.get_all_history(user_id) | |
| def update_outcome(self, episode_id: int, outcome: str): | |
| """Record what actually happened after a call - builds track record.""" | |
| self.episodic.update_outcome(episode_id, outcome) | |
| def is_using_supabase(self) -> bool: | |
| """Let the UI know which backend is active.""" | |
| return _using_supabase() | |