Spaces:
Running
Running
| # backend/memory/consolidator.py | |
| import os | |
| import json | |
| import logging | |
| from .working_memory import WorkingMemory | |
| from .episodic_memory import EpisodicMemory | |
| from .semantic_memory import SemanticMemory | |
| from .procedural_memory import ProceduralMemory | |
| try: | |
| from google import genai | |
| from google.genai import types | |
| from config import GEMINI_API_KEY, GEMINI_MODEL | |
| except ImportError: | |
| pass | |
| class MemoryConsolidator: | |
| def __init__(self): | |
| # Resolve persistence directory from environment variable provided by Tauri | |
| base_dir = os.environ.get("JARVIS_APP_DATA_DIR") | |
| if not base_dir: | |
| # Fallback to local if not set | |
| base_dir = os.path.join(os.path.expanduser("~"), ".jarvis_omega") | |
| mem_dir = os.path.join(base_dir, "memory") | |
| os.makedirs(mem_dir, exist_ok=True) | |
| self.episodic = EpisodicMemory(os.path.join(mem_dir, "episodic")) | |
| self.semantic = SemanticMemory(os.path.join(mem_dir, "semantic.db")) | |
| self.procedural = ProceduralMemory(os.path.join(mem_dir, "procedural.db")) | |
| self._client = None | |
| try: | |
| self._client = genai.Client(api_key=GEMINI_API_KEY) | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| async def _ask_llm_json(self, prompt: str, system: str) -> dict: | |
| if not self._client: | |
| return {} | |
| try: | |
| resp = await self._client.aio.models.generate_content( | |
| model=GEMINI_MODEL, | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| system_instruction=system, | |
| temperature=0.1, | |
| response_mime_type="application/json" | |
| ), | |
| ) | |
| raw = resp.text.strip() | |
| if raw.startswith("```"): | |
| import re | |
| raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.MULTILINE) | |
| return json.loads(raw) | |
| except Exception as e: | |
| logging.error(f"Consolidator LLM Error: {e}") | |
| return {} | |
| async def consolidate(self, working_memory: WorkingMemory): | |
| """Runs at session end to consolidate working memory.""" | |
| if not working_memory.messages: | |
| return | |
| # 1. Summarize into a single episode | |
| convo_text = "" | |
| for msg in working_memory.messages: | |
| convo_text += f"{msg['role'].upper()}: {msg['content']}\n" | |
| summary_prompt = f"Summarize this conversation into a single descriptive paragraph:\n\n{convo_text}" | |
| summary_sys = 'You are a memory consolidation engine. Return a JSON object: {"summary": "..."}' | |
| summary_result = await self._ask_llm_json(summary_prompt, summary_sys) | |
| episode_summary = summary_result.get("summary", "Conversation recorded.") | |
| # Call EpisodicMemory.add() | |
| await self.episodic.add(episode_summary, {"type": "session_summary", "msg_count": len(working_memory.messages)}) | |
| # 2. Extract semantic triples | |
| triple_prompt = f"Extract key factual knowledge from this conversation as subject-predicate-object triples.\nConversation:\n{convo_text}" | |
| triple_sys = """Return a JSON array of triples: | |
| { | |
| "triples": [ | |
| {"subject": "Tony Stark", "predicate": "likes", "object": "cheeseburgers", "confidence": 0.9} | |
| ] | |
| }""" | |
| triple_result = await self._ask_llm_json(triple_prompt, triple_sys) | |
| triples = triple_result.get("triples", []) | |
| for t in triples: | |
| if "subject" in t and "predicate" in t and "object" in t: | |
| await self.semantic.add_fact( | |
| subject=t["subject"], | |
| predicate=t["predicate"], | |
| obj=t["object"], | |
| confidence=t.get("confidence", 1.0) | |
| ) | |
| # 3. Extract procedural patterns (if any new tasks were successfully completed) | |
| proc_prompt = f"Did the AI successfully complete any multi-step task in this conversation? If so, extract the pattern.\nConversation:\n{convo_text}" | |
| proc_sys = """Return JSON format: | |
| { | |
| "tasks": [ | |
| { | |
| "task_name": "clear_temp_files", | |
| "steps": [{"action": "run_shell_command", "args": {"command": "rm -rf /tmp/*"}}], | |
| "triggers": ["clear temp", "empty trash"] | |
| } | |
| ] | |
| } | |
| If none, return {"tasks": []}""" | |
| proc_result = await self._ask_llm_json(proc_prompt, proc_sys) | |
| tasks = proc_result.get("tasks", []) | |
| for t in tasks: | |
| if "task_name" in t and "steps" in t: | |
| # Update ProceduralMemory (async — must be awaited or the Mongo | |
| # write is silently dropped, same class of bug as add_fact above). | |
| await self.procedural.record_success(t["task_name"], t["steps"]) | |
| # Finally, clear working memory | |
| working_memory.clear() | |
| logging.info("Memory consolidation complete.") | |