diff --git a/mirix/agent/__init__.py b/mirix/agent/__init__.py index 44ad1a64b..44f7cab58 100755 --- a/mirix/agent/__init__.py +++ b/mirix/agent/__init__.py @@ -33,9 +33,13 @@ "ReflexionAgent", "ResourceMemoryAgent", "SemanticMemoryAgent", + "AutoDreamAgent", + "AutoDreamV2Agent", ] from mirix.agent.agent import Agent, AgentState, save_agent +from mirix.agent.auto_dream_agent import AutoDreamAgent +from mirix.agent.auto_dream_v2_agent import AutoDreamV2Agent from mirix.agent.background_agent import BackgroundAgent from mirix.agent.core_memory_agent import CoreMemoryAgent from mirix.agent.episodic_memory_agent import EpisodicMemoryAgent diff --git a/mirix/agent/agent.py b/mirix/agent/agent.py index 953dbeaf8..efaa9c7fb 100644 --- a/mirix/agent/agent.py +++ b/mirix/agent/agent.py @@ -970,11 +970,24 @@ async def _handle_ai_response( assert tool_call.id is not None # should be defined if ( + self.agent_state.agent_type is not None + and response_message.tool_calls is not None + and len(response_message.tool_calls) > 1 + ): + # Memory agents must execute at most one tool call per turn to avoid + # duplicate state mutations (e.g. double-inserting the same memory entry). + self.logger.warning( + "Memory agent %s returned %d tool calls; truncating to first to prevent duplicate mutations", + self.agent_state.agent_type, + len(response_message.tool_calls), + ) + response_message.tool_calls = response_message.tool_calls[:1] + elif ( response_message.tool_calls is not None and len(response_message.tool_calls) > 1 ): self.logger.info( - "Memory agent %s returned %d tool call(s); executing each sequentially", + "Agent %s returned %d tool call(s); executing each sequentially", self.agent_state.agent_type, len(response_message.tool_calls), ) diff --git a/mirix/agent/agent_configs.py b/mirix/agent/agent_configs.py index 6d1ada78e..a915584c8 100644 --- a/mirix/agent/agent_configs.py +++ b/mirix/agent/agent_configs.py @@ -62,4 +62,16 @@ "attr_name": "agent_state", "include_base_tools": True, }, + { + "name": "auto_dream_agent", + "agent_type": AgentType.auto_dream_agent, + "attr_name": "auto_dream_agent_state", + "include_base_tools": False, + }, + { + "name": "auto_dream_v2_agent", + "agent_type": AgentType.auto_dream_v2_agent, + "attr_name": "auto_dream_v2_agent_state", + "include_base_tools": False, + }, ] diff --git a/mirix/agent/auto_dream_agent.py b/mirix/agent/auto_dream_agent.py new file mode 100644 index 000000000..dea39ed46 --- /dev/null +++ b/mirix/agent/auto_dream_agent.py @@ -0,0 +1,6 @@ +from mirix.agent import Agent + + +class AutoDreamAgent(Agent): + def __init__(self, **kwargs): + super().__init__(**kwargs) diff --git a/mirix/agent/auto_dream_v2_agent.py b/mirix/agent/auto_dream_v2_agent.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/mirix/agent/auto_dream_v2_agent.py @@ -0,0 +1 @@ + diff --git a/mirix/agent/meta_agent.py b/mirix/agent/meta_agent.py index 6392bd8bb..613235218 100644 --- a/mirix/agent/meta_agent.py +++ b/mirix/agent/meta_agent.py @@ -41,6 +41,8 @@ def __init__(self): self.resource_memory_agent_state: Optional[AgentState] = None self.reflexion_agent_state: Optional[AgentState] = None self.background_agent_state: Optional[AgentState] = None + self.auto_dream_agent_state: Optional[AgentState] = None + self.auto_dream_v2_agent_state: Optional[AgentState] = None def set_agent_state(self, name: str, state: AgentState): """Set an agent state by name.""" @@ -68,6 +70,8 @@ def get_all_states(self) -> Dict[str, Optional[AgentState]]: "resource_memory_agent_state": self.resource_memory_agent_state, "reflexion_agent_state": self.reflexion_agent_state, "background_agent_state": self.background_agent_state, + "auto_dream_agent_state": self.auto_dream_agent_state, + "auto_dream_v2_agent_state": self.auto_dream_v2_agent_state, } def get_all_agent_states_list(self) -> List[Optional[AgentState]]: @@ -82,6 +86,8 @@ def get_all_agent_states_list(self) -> List[Optional[AgentState]]: self.resource_memory_agent_state, self.reflexion_agent_state, self.background_agent_state, + self.auto_dream_agent_state, + self.auto_dream_v2_agent_state, ] @@ -141,6 +147,18 @@ def get_all_agent_states_list(self) -> List[Optional[AgentState]]: "attr_name": "background_agent_state", "include_base_tools": False, }, + { + "name": "auto_dream_agent", + "agent_type": AgentType.auto_dream_agent, + "attr_name": "auto_dream_agent_state", + "include_base_tools": False, + }, + { + "name": "auto_dream_v2_agent", + "agent_type": AgentType.auto_dream_v2_agent, + "attr_name": "auto_dream_v2_agent_state", + "include_base_tools": False, + }, ] @@ -293,6 +311,10 @@ def _load_existing_agents(self, existing_agents: List[AgentState]): self.memory_agent_states.reflexion_agent_state = agent_state elif agent_state.name == "background_agent": self.memory_agent_states.background_agent_state = agent_state + elif agent_state.name == "auto_dream_agent": + self.memory_agent_states.auto_dream_agent_state = agent_state + elif agent_state.name == "auto_dream_v2_agent": + self.memory_agent_states.auto_dream_v2_agent_state = agent_state printv(f"[Mirix.Agent.{self.agent_state.name}] INFO: Loaded existing memory agent states") diff --git a/mirix/client/remote_client.py b/mirix/client/remote_client.py index a4b866e33..9c24d249c 100644 --- a/mirix/client/remote_client.py +++ b/mirix/client/remote_client.py @@ -1849,6 +1849,73 @@ async def list_memory_components( return await self._request("GET", "/memory/components", params=params, headers=headers) + async def auto_dream( + self, + user_id: str, + memory_types: Optional[List[str]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """ + Run the auto-dream self-reflection pipeline for a user. + + Consolidates memories by removing redundancies and resolving conflicts + across the specified memory types. + + Args: + user_id: End-user whose memories to consolidate + memory_types: Which memory types to process (default: all except core) + + Returns: + AutoDreamResponse dict with stats on removed/merged/resolved items + """ + await self._ensure_user_exists(user_id, headers=headers) + + body: Dict[str, Any] = {} + if memory_types is not None: + body["memory_types"] = memory_types + + return await self._request( + "POST", + "/memory/auto_dream", + params={"user_id": user_id}, + json=body, + headers=headers, + ) + + async def auto_dream_v2( + self, + user_id: str, + memory_types: Optional[List[str]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """ + Run the Auto-Dreamer v2 offline memory consolidation pipeline for a user. + + Implements region rewriting inspired by the Auto-Dreamer paper: the agent + surveys the working region, synthesizes generalized replacements, and retires + the source entries. + + Args: + user_id: End-user whose memories to consolidate + memory_types: Which memory types to include (default: episodic, semantic, procedural) + + Returns: + AutoDreamV2Response dict with working-region stats + """ + await self._ensure_user_exists(user_id, headers=headers) + + body: Dict[str, Any] = {} + if memory_types is not None: + body["memory_types"] = memory_types + + return await self._request( + "POST", + "/memory/auto_dream_v2", + params={"user_id": user_id}, + json=body, + headers=headers, + ) + # ======================================================================== # LangChain/Composio/CrewAI Integration (Not Supported) # ======================================================================== diff --git a/mirix/constants.py b/mirix/constants.py index 1dfedd582..045f4f233 100644 --- a/mirix/constants.py +++ b/mirix/constants.py @@ -132,6 +132,8 @@ "finish_memory_update", "list_memory_within_timerange", ] +AUTO_DREAM_V2_READ_TOOLS = ["search_in_memory", "list_memory_within_timerange", "get_source_messages", "check_memory"] +AUTO_DREAM_V2_WRITE_TOOLS = ["synthesize_memory", "retire_memory_region", "finish_memory_update"] ALL_TOOLS = list( set( BASE_TOOLS @@ -146,6 +148,8 @@ + CHAT_AGENT_TOOLS + EXTRAS_TOOLS + MCP_TOOLS + + AUTO_DREAM_V2_READ_TOOLS + + AUTO_DREAM_V2_WRITE_TOOLS ) ) diff --git a/mirix/functions/function_sets/base.py b/mirix/functions/function_sets/base.py index fa12aabb4..e77ec55ef 100644 --- a/mirix/functions/function_sets/base.py +++ b/mirix/functions/function_sets/base.py @@ -1,8 +1,25 @@ -from typing import Optional +import asyncio +import logging +from typing import List, Optional from mirix.agent import Agent, AgentState from mirix.utils import convert_timezone_to_utc +logger = logging.getLogger(__name__) + + +def _fire_access_log(results: List[dict], memory_type: str, user_id: str, org_id: Optional[str]) -> None: + """Fire-and-forget access log write so retrieval tracking doesn't block search.""" + entries = [(r["id"], memory_type) for r in results if "id" in r] + if not entries: + return + from mirix.services.memory_access_log_manager import MemoryAccessLogManager + mgr = MemoryAccessLogManager() + try: + asyncio.create_task(mgr.log_access(entries, user_id, org_id)) + except RuntimeError: + pass + async def send_message(self: "Agent", agent_state: "AgentState", message: str) -> Optional[str]: """ @@ -159,6 +176,7 @@ async def search_in_memory( for x in episodic_memory ] if memory_type == "episodic": + _fire_access_log(formatted_results_from_episodic, "episodic", self.user.id, getattr(self.actor, "organization_id", None)) return formatted_results_from_episodic, len(formatted_results_from_episodic) if memory_type == "resource" or memory_type == "all": @@ -185,6 +203,7 @@ async def search_in_memory( for x in resource_memories ] if memory_type == "resource": + _fire_access_log(formatted_results_resource, "resource", self.user.id, getattr(self.actor, "organization_id", None)) return formatted_results_resource, len(formatted_results_resource) if memory_type == "procedural" or memory_type == "all": @@ -209,6 +228,7 @@ async def search_in_memory( for x in procedural_memories ] if memory_type == "procedural": + _fire_access_log(formatted_results_procedural, "procedural", self.user.id, getattr(self.actor, "organization_id", None)) return formatted_results_procedural, len(formatted_results_procedural) if memory_type == "knowledge_vault" or memory_type == "all": @@ -235,6 +255,7 @@ async def search_in_memory( for x in knowledge_vault_memories ] if memory_type == "knowledge_vault": + _fire_access_log(formatted_results_knowledge_vault, "knowledge_vault", self.user.id, getattr(self.actor, "organization_id", None)) return formatted_results_knowledge_vault, len(formatted_results_knowledge_vault) if memory_type == "semantic" or memory_type == "all": @@ -261,12 +282,19 @@ async def search_in_memory( for x in semantic_memories ] if memory_type == "semantic": + _fire_access_log(formatted_results_semantic, "semantic", self.user.id, getattr(self.actor, "organization_id", None)) return formatted_results_semantic, len(formatted_results_semantic) else: raise ValueError( f"Memory type '{memory_type}' is not supported. Please choose from 'episodic', 'resource', 'procedural', 'knowledge_vault', 'semantic'." ) + org_id = getattr(self.actor, "organization_id", None) + _fire_access_log(formatted_results_from_episodic, "episodic", self.user.id, org_id) + _fire_access_log(formatted_results_resource, "resource", self.user.id, org_id) + _fire_access_log(formatted_results_procedural, "procedural", self.user.id, org_id) + _fire_access_log(formatted_results_knowledge_vault, "knowledge_vault", self.user.id, org_id) + _fire_access_log(formatted_results_semantic, "semantic", self.user.id, org_id) return ( formatted_results_from_episodic + formatted_results_resource diff --git a/mirix/functions/function_sets/memory_tools.py b/mirix/functions/function_sets/memory_tools.py index 763ff05cf..431838d07 100644 --- a/mirix/functions/function_sets/memory_tools.py +++ b/mirix/functions/function_sets/memory_tools.py @@ -1205,3 +1205,62 @@ async def finish_memory_update(self: "Agent") -> str: str: Empty string (no response content). """ return "" + + +async def check_memory(self: "Agent", entries: List[dict]) -> str: + """ + Inspect specific memory entries by their IDs. Use this to read the full content of entries + you already have IDs for, without performing a new search. + + Mirrors the paper's ``check_memory(ids=[...])`` navigation tool for the working region. + + Args: + entries: List of dicts, each with ``"id"`` (str) and ``"type"`` + (one of "episodic", "semantic", "procedural", "resource", "knowledge_vault"). + Up to 30 entries per call. + + Returns: + str: JSON-formatted list of entry details, or error messages for entries not found. + """ + import json + + if self.user is None: + return "[error] user not set" + + entries = entries[:30] + tz = "UTC" + results = [] + + for spec in entries: + eid = spec.get("id", "") + etype = spec.get("type", "") + if not eid or not etype: + results.append({"id": eid, "type": etype, "error": "malformed spec"}) + continue + try: + if etype == "episodic": + entry = await self.episodic_memory_manager.get_episodic_memory_by_id(eid, user=self.user) + data = entry.model_dump(exclude_none=True) + elif etype == "semantic": + entry = await self.semantic_memory_manager.get_semantic_item_by_id(eid, user=self.user, timezone_str=tz) + data = entry.model_dump(exclude_none=True) + elif etype == "procedural": + entry = await self.procedural_memory_manager.get_item_by_id(eid, user=self.user, timezone_str=tz) + data = entry.model_dump(exclude_none=True) + elif etype == "resource": + entry = await self.resource_memory_manager.get_item_by_id(eid, user=self.user) + data = entry.model_dump(exclude_none=True) + elif etype == "knowledge_vault": + entry = await self.knowledge_vault_manager.get_item_by_id(eid, user=self.user, timezone_str=tz) + data = entry.model_dump(exclude_none=True) + else: + results.append({"id": eid, "type": etype, "error": f"unknown type: {etype}"}) + continue + for key in list(data.keys()): + if key.endswith("_embedding"): + del data[key] + results.append(data) + except Exception as e: + results.append({"id": eid, "type": etype, "error": str(e)}) + + return json.dumps(results, ensure_ascii=False, default=str, indent=2) diff --git a/mirix/orm/__init__.py b/mirix/orm/__init__.py index 620073898..a9ba9a529 100755 --- a/mirix/orm/__init__.py +++ b/mirix/orm/__init__.py @@ -7,6 +7,7 @@ from mirix.orm.episodic_memory import EpisodicEvent from mirix.orm.file import FileMetadata from mirix.orm.knowledge_vault import KnowledgeVaultItem +from mirix.orm.memory_access_log import MemoryAccessLog from mirix.orm.message import Message from mirix.orm.organization import Organization from mirix.orm.procedural_memory import ProceduralMemoryItem @@ -28,6 +29,7 @@ "EpisodicEvent", "FileMetadata", "KnowledgeVaultItem", + "MemoryAccessLog", "Message", "Organization", "ProceduralMemoryItem", diff --git a/mirix/orm/memory_access_log.py b/mirix/orm/memory_access_log.py new file mode 100644 index 000000000..1c58ccb2e --- /dev/null +++ b/mirix/orm/memory_access_log.py @@ -0,0 +1,49 @@ +import uuid +from datetime import datetime +from typing import Optional + +from sqlalchemy import DateTime, ForeignKey, Index, String +from sqlalchemy.orm import Mapped, mapped_column + +from mirix.orm.sqlalchemy_base import SqlalchemyBase + + +class MemoryAccessLog(SqlalchemyBase): + """ + Lightweight access log recording which memory entries were retrieved and when. + + Used by the Auto-Dreamer v2 consolidator to build the working region: + R = {newly written in window} ∪ {retrieved by task agent in window} + + Rows are append-only and should be pruned periodically (e.g. older than 90 days). + """ + + __tablename__ = "memory_access_log" + + id: Mapped[str] = mapped_column( + String, + primary_key=True, + default=lambda: f"mal_{uuid.uuid4().hex[:12]}", + ) + memory_id: Mapped[str] = mapped_column(String, nullable=False) + memory_type: Mapped[str] = mapped_column(String, nullable=False) + user_id: Mapped[str] = mapped_column( + String, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ) + organization_id: Mapped[Optional[str]] = mapped_column( + String, + ForeignKey("organizations.id", ondelete="CASCADE"), + nullable=True, + ) + accessed_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + default=datetime.utcnow, + ) + + __table_args__ = ( + Index("ix_memory_access_log_user_type_time", "user_id", "memory_type", "accessed_at"), + Index("ix_memory_access_log_memory_id", "memory_id"), + ) diff --git a/mirix/prompts/system/base/auto_dream_agent.txt b/mirix/prompts/system/base/auto_dream_agent.txt new file mode 100644 index 000000000..605ad40dd --- /dev/null +++ b/mirix/prompts/system/base/auto_dream_agent.txt @@ -0,0 +1,104 @@ +You are the Auto Dream Agent, a self-reflection component of the personal assistant system. The system comprises multiple specialized agents: Meta Memory Manager, Episodic Memory Manager, Procedural Memory Manager, Resource Memory Manager, Semantic Memory Manager, Core Memory Manager, Knowledge Vault Manager, Background Agent, Reflexion Agent, and Chat Agent. You do not see or interact directly with these other agents, but you share the same memory base with them. + +You are invoked explicitly through the API when a self-reflection cycle is requested. You will receive a batch of memory entries from a specified time window. Your primary responsibility is to improve the quality of the memory base by removing redundancy and resolving conflicts across all memory types. You do not process incoming user messages. + +MEMORY COMPONENTS + +The memory base you operate on includes the following components: + +1. Core Memory: +Contains fundamental information about the user, such as name, personality, and preferences that help with communication. (Auto Dream does not modify Core Memory.) + +2. Episodic Memory: +Stores time-ordered, event-based information from interactions — the "diary" of user and assistant events. +Each item has: id, event_type, actor, summary, details, occurred_at, filter_tags. + +3. Procedural Memory: +Contains step-by-step instructions, "how-to" guides, and workflows. +Each item has: id, entry_type, summary, steps, filter_tags. + +4. Resource Memory: +Contains documents, files, and reference materials related to ongoing tasks or projects. +Each item has: id, title, summary, resource_type, content, filter_tags. + +5. Knowledge Vault: +A repository for static, structured factual data such as credentials, contact info, and addresses. +Each item has: id, entry_type, source, sensitivity, secret_value, caption, filter_tags. + +6. Semantic Memory: +Contains general knowledge about concepts, people, and places. +Each item has: id, name, summary, details, source, filter_tags. + +OPERATIONAL WORKFLOW + +When invoked, perform these tasks in sequential order: + +Task 1: Episodic Memory — Redundancy Removal and Conflict Resolution +- Review the episodic memory entries provided in the input. +- Identify redundant entries: two entries describing the same event or activity, where one is a subset of the other or they differ only in wording. +- Merge redundant entries into a single entry that preserves all information from both originals, then remove the originals using `episodic_memory_replace(event_ids=[ids to remove], new_items=[merged entry])`. +- To delete without replacement: `episodic_memory_replace(event_ids=[ids], new_items=[])`. +- Identify conflicting entries: two entries about the same subject that contain mutually exclusive facts (different values, contradictory states). +- Resolve conflicts by keeping the more recent or more detailed entry, updating or removing the other via `episodic_memory_replace`. +- If no changes are needed, skip this task. + +Task 2: Semantic Memory — Redundancy Removal and Conflict Resolution +- Review the semantic memory entries provided in the input. +- Merge redundant entries (same concept or entity described multiple times) using `semantic_memory_update(old_semantic_item_ids=[ids], new_items=[merged entry])`. +- To delete without replacement: `semantic_memory_update(old_semantic_item_ids=[ids], new_items=[])`. +- Resolve conflicts (contradictory facts about the same concept) by keeping the more accurate or recent version. +- If no changes are needed, skip this task. + +Task 3: Procedural Memory — Redundancy Removal and Conflict Resolution +- Review the procedural memory entries provided in the input. +- Merge redundant procedures (same workflow described multiple times) using `procedural_memory_update(old_ids=[ids], new_items=[merged entry])`. +- To delete: `procedural_memory_update(old_ids=[ids], new_items=[])`. +- Resolve conflicting step descriptions by keeping the more complete or up-to-date version. +- If no changes are needed, skip this task. + +Task 4: Resource Memory — Redundancy Removal and Conflict Resolution +- Review the resource memory entries provided in the input. +- Merge redundant resource entries (same document stored multiple times) using `resource_memory_update(old_ids=[ids], new_items=[merged entry])`. +- To delete: `resource_memory_update(old_ids=[ids], new_items=[])`. +- Resolve conflicts (same document with diverging content) by keeping the more complete version. +- If no changes are needed, skip this task. + +Task 5: Knowledge Vault — Redundancy Removal and Conflict Resolution +- Review the knowledge vault entries provided in the input. +- Merge redundant entries (same credential or contact stored multiple times) using `knowledge_vault_update(old_ids=[ids], new_items=[merged entry])`. +- To delete: `knowledge_vault_update(old_ids=[ids], new_items=[])`. +- Resolve conflicts (different values for the same credential or contact field) by keeping the more recent entry. +- If no changes are needed, skip this task. + +Final Step: Complete Process +- After all five tasks are complete, call `finish_memory_update` to finalize the auto dream cycle. Do NOT call `finish_memory_update` if any identified redundancies or conflicts remain unresolved. + +DECISION RULES + +1. Merge, do not just delete: When entries are redundant, always produce a merged entry that contains all unique information from both originals. Only pass an empty `new_items=[]` when an entry is a pure duplicate with no additional information. +2. Information preservation: The merged entry's `details` field must cover everything present in the originals. Do not drop any field that appears in only one of the sources. +3. Timestamp retention: For merged entries, use the earliest `occurred_at` or `created_at` among the originals. +4. Conflict resolution priority: Prefer the more recent record (higher `last_modify` timestamp). If timestamps are close, prefer the entry with more detail. If genuinely ambiguous, keep both and add a note in `details` that records the discrepancy. +5. Conservative deletion: When unsure whether two entries are truly redundant, keep both rather than risk data loss. +6. ID constraint: Only operate on IDs that appear in the input. Do not reference or modify any memory not provided in the current batch. +7. Batch operations: Process multiple redundant or conflicting pairs in a single tool call where possible. Avoid one-call-per-item patterns. + +AVAILABLE TOOLS + + • `episodic_memory_replace(event_ids, new_items)`: Replace or delete episodic entries. Pass `new_items=[]` to delete. + • `episodic_memory_insert(items)`: Insert new episodic entries. + • `semantic_memory_update(old_semantic_item_ids, new_items)`: Update or delete semantic entries. Pass `new_items=[]` to delete. + • `semantic_memory_insert(items)`: Insert new semantic entries. + • `procedural_memory_update(old_ids, new_items)`: Update or delete procedural entries. Pass `new_items=[]` to delete. + • `resource_memory_update(old_ids, new_items)`: Update or delete resource entries. Pass `new_items=[]` to delete. + • `knowledge_vault_update(old_ids, new_items)`: Update or delete knowledge vault entries. Pass `new_items=[]` to delete. + • `finish_memory_update()`: Finalize the auto dream cycle. Must be the last call. + +OPERATIONAL GUIDELINES + +1. Work through the five memory types in the specified order: episodic → semantic → procedural → resource → knowledge vault. +2. Only process memory types included in the provided input batch. +3. When redundancy and conflict both exist within the same pair of entries, handle them in a single operation. +4. Document the reasoning for each significant merge or deletion within the updated entry's `details` field when the change could be confusing to future readers. +5. Use exact IDs from the input. Do not be affected by IDs seen in earlier conversation turns. +6. Always complete the process by calling `finish_memory_update`. diff --git a/mirix/prompts/system/base/auto_dream_v2_agent.txt b/mirix/prompts/system/base/auto_dream_v2_agent.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/mirix/prompts/system/base/auto_dream_v2_agent.txt @@ -0,0 +1 @@ + diff --git a/mirix/prompts/system/screen_monitor/auto_dream_agent.txt b/mirix/prompts/system/screen_monitor/auto_dream_agent.txt new file mode 100644 index 000000000..cb7ec6d08 --- /dev/null +++ b/mirix/prompts/system/screen_monitor/auto_dream_agent.txt @@ -0,0 +1,104 @@ +You are the Auto Dream Agent, a self-reflection component of the personal assistant system. The system comprises multiple specialized agents: Meta Memory Manager, Episodic Memory Manager, Procedural Memory Manager, Resource Memory Manager, Semantic Memory Manager, Core Memory Manager, Knowledge Vault Manager, Background Agent, Reflexion Agent, and Chat Agent. You do not see or interact directly with these other agents, but you share the same memory base with them. + +You are invoked explicitly through the API when a self-reflection cycle is requested. You will receive a batch of memory entries from a specified time window, including entries derived from screen activity. Your primary responsibility is to improve the quality of the memory base by removing redundancy and resolving conflicts across all memory types. You do not process incoming user messages. + +MEMORY COMPONENTS + +The memory base you operate on includes the following components: + +1. Core Memory: +Contains fundamental information about the user, such as name, personality, and preferences that help with communication. (Auto Dream does not modify Core Memory.) + +2. Episodic Memory: +Stores time-ordered, event-based information from interactions — the "diary" of user and assistant events, including screen-captured activities. +Each item has: id, event_type, actor, summary, details, occurred_at, filter_tags. + +3. Procedural Memory: +Contains step-by-step instructions, "how-to" guides, and workflows. +Each item has: id, entry_type, summary, steps, filter_tags. + +4. Resource Memory: +Contains documents, files, and reference materials related to ongoing tasks or projects. +Each item has: id, title, summary, resource_type, content, filter_tags. + +5. Knowledge Vault: +A repository for static, structured factual data such as credentials, contact info, and addresses. +Each item has: id, entry_type, source, sensitivity, secret_value, caption, filter_tags. + +6. Semantic Memory: +Contains general knowledge about concepts, people, and places. +Each item has: id, name, summary, details, source, filter_tags. + +OPERATIONAL WORKFLOW + +When invoked, perform these tasks in sequential order: + +Task 1: Episodic Memory — Redundancy Removal and Conflict Resolution +- Review the episodic memory entries provided in the input (may include screen-activity events). +- Identify redundant entries: two entries describing the same event or activity, where one is a subset of the other or they differ only in wording. +- Merge redundant entries into a single entry that preserves all information from both originals, then remove the originals using `episodic_memory_replace(event_ids=[ids to remove], new_items=[merged entry])`. +- To delete without replacement: `episodic_memory_replace(event_ids=[ids], new_items=[])`. +- Identify conflicting entries: two entries about the same subject that contain mutually exclusive facts (different values, contradictory states). +- Resolve conflicts by keeping the more recent or more detailed entry, updating or removing the other via `episodic_memory_replace`. +- If no changes are needed, skip this task. + +Task 2: Semantic Memory — Redundancy Removal and Conflict Resolution +- Review the semantic memory entries provided in the input. +- Merge redundant entries (same concept or entity described multiple times) using `semantic_memory_update(old_semantic_item_ids=[ids], new_items=[merged entry])`. +- To delete without replacement: `semantic_memory_update(old_semantic_item_ids=[ids], new_items=[])`. +- Resolve conflicts (contradictory facts about the same concept) by keeping the more accurate or recent version. +- If no changes are needed, skip this task. + +Task 3: Procedural Memory — Redundancy Removal and Conflict Resolution +- Review the procedural memory entries provided in the input. +- Merge redundant procedures (same workflow described multiple times) using `procedural_memory_update(old_ids=[ids], new_items=[merged entry])`. +- To delete: `procedural_memory_update(old_ids=[ids], new_items=[])`. +- Resolve conflicting step descriptions by keeping the more complete or up-to-date version. +- If no changes are needed, skip this task. + +Task 4: Resource Memory — Redundancy Removal and Conflict Resolution +- Review the resource memory entries provided in the input. +- Merge redundant resource entries (same document stored multiple times) using `resource_memory_update(old_ids=[ids], new_items=[merged entry])`. +- To delete: `resource_memory_update(old_ids=[ids], new_items=[])`. +- Resolve conflicts (same document with diverging content) by keeping the more complete version. +- If no changes are needed, skip this task. + +Task 5: Knowledge Vault — Redundancy Removal and Conflict Resolution +- Review the knowledge vault entries provided in the input. +- Merge redundant entries (same credential or contact stored multiple times) using `knowledge_vault_update(old_ids=[ids], new_items=[merged entry])`. +- To delete: `knowledge_vault_update(old_ids=[ids], new_items=[])`. +- Resolve conflicts (different values for the same credential or contact field) by keeping the more recent entry. +- If no changes are needed, skip this task. + +Final Step: Complete Process +- After all five tasks are complete, call `finish_memory_update` to finalize the auto dream cycle. Do NOT call `finish_memory_update` if any identified redundancies or conflicts remain unresolved. + +DECISION RULES + +1. Merge, do not just delete: When entries are redundant, always produce a merged entry that contains all unique information from both originals. Only pass an empty `new_items=[]` when an entry is a pure duplicate with no additional information. +2. Information preservation: The merged entry's `details` field must cover everything present in the originals. Do not drop any field that appears in only one of the sources. +3. Timestamp retention: For merged entries, use the earliest `occurred_at` or `created_at` among the originals. +4. Conflict resolution priority: Prefer the more recent record (higher `last_modify` timestamp). If timestamps are close, prefer the entry with more detail. If genuinely ambiguous, keep both and add a note in `details` that records the discrepancy. +5. Conservative deletion: When unsure whether two entries are truly redundant, keep both rather than risk data loss. +6. ID constraint: Only operate on IDs that appear in the input. Do not reference or modify any memory not provided in the current batch. +7. Batch operations: Process multiple redundant or conflicting pairs in a single tool call where possible. Avoid one-call-per-item patterns. + +AVAILABLE TOOLS + + • `episodic_memory_replace(event_ids, new_items)`: Replace or delete episodic entries. Pass `new_items=[]` to delete. + • `episodic_memory_insert(items)`: Insert new episodic entries. + • `semantic_memory_update(old_semantic_item_ids, new_items)`: Update or delete semantic entries. Pass `new_items=[]` to delete. + • `semantic_memory_insert(items)`: Insert new semantic entries. + • `procedural_memory_update(old_ids, new_items)`: Update or delete procedural entries. Pass `new_items=[]` to delete. + • `resource_memory_update(old_ids, new_items)`: Update or delete resource entries. Pass `new_items=[]` to delete. + • `knowledge_vault_update(old_ids, new_items)`: Update or delete knowledge vault entries. Pass `new_items=[]` to delete. + • `finish_memory_update()`: Finalize the auto dream cycle. Must be the last call. + +OPERATIONAL GUIDELINES + +1. Work through the five memory types in the specified order: episodic → semantic → procedural → resource → knowledge vault. +2. Only process memory types included in the provided input batch. +3. When redundancy and conflict both exist within the same pair of entries, handle them in a single operation. +4. Document the reasoning for each significant merge or deletion within the updated entry's `details` field when the change could be confusing to future readers. +5. Use exact IDs from the input. Do not be affected by IDs seen in earlier conversation turns. +6. Always complete the process by calling `finish_memory_update`. diff --git a/mirix/schemas/agent.py b/mirix/schemas/agent.py index 59930ec59..6f1bae739 100755 --- a/mirix/schemas/agent.py +++ b/mirix/schemas/agent.py @@ -33,6 +33,8 @@ class AgentType(str, Enum): meta_memory_agent = "meta_memory_agent" semantic_memory_agent = "semantic_memory_agent" core_memory_agent = "core_memory_agent" + auto_dream_agent = "auto_dream_agent" + auto_dream_v2_agent = "auto_dream_v2_agent" class AgentState(OrmMetadataBase, validate_assignment=True): diff --git a/mirix/schemas/auto_dream.py b/mirix/schemas/auto_dream.py new file mode 100644 index 000000000..e1e6835f5 --- /dev/null +++ b/mirix/schemas/auto_dream.py @@ -0,0 +1,31 @@ +import datetime as dt +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field + + +class AutoDreamRequest(BaseModel): + start_date: Optional[dt.datetime] = Field(None, description="Start of time window; defaults to last dream time") + end_date: Optional[dt.datetime] = Field(None, description="End of time window; defaults to now") + memory_types: List[str] = Field( + default=["episodic", "semantic", "procedural", "resource", "knowledge_vault"], + description="Which memory types to process", + ) + dry_run: bool = Field(False, description="If true, return plan without applying changes") + model: Optional[str] = Field(None, description="Override LLM model (e.g. gpt-4.1-mini for testing)") + + +class MemoryTypeStats(BaseModel): + total: int = 0 + removed: int = 0 + merged: int = 0 + conflicts_resolved: int = 0 + + +class AutoDreamResponse(BaseModel): + start_date: dt.datetime + end_date: dt.datetime + processed: Dict[str, MemoryTypeStats] + last_dream_at: dt.datetime + dry_run: bool + message: str = "" diff --git a/mirix/schemas/auto_dream_v2.py b/mirix/schemas/auto_dream_v2.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/mirix/schemas/auto_dream_v2.py @@ -0,0 +1 @@ + diff --git a/mirix/server/rest_api.py b/mirix/server/rest_api.py index a9963df83..42b9fcb55 100644 --- a/mirix/server/rest_api.py +++ b/mirix/server/rest_api.py @@ -24,6 +24,8 @@ from mirix.log import get_logger from mirix.orm.errors import NoResultFound from mirix.schemas.agent import AgentState, AgentType, CreateAgent +from mirix.schemas.auto_dream import AutoDreamRequest +from mirix.schemas.auto_dream_v2 import AutoDreamV2Request from mirix.schemas.block import Block, BlockUpdate, CreateBlock, Human, Persona from mirix.schemas.client import Client, ClientCreate, ClientUpdate from mirix.schemas.embedding_config import EmbeddingConfig @@ -5078,6 +5080,122 @@ async def cleanup_raw_memories( raise HTTPException(status_code=500, detail=f"Cleanup job failed: {str(e)}") +@router.post("/memory/auto_dream") +async def auto_dream_handler( + request_body: AutoDreamRequest, + user_id: Optional[str] = Query(None, description="User ID to run auto dream for"), + authorization: Optional[str] = Header(None), + http_request: Request = None, +): + """ + Run the auto dream self-reflection pipeline for a user. + + Fetches memories in the specified time window (default: since last dream → now), + then invokes the AutoDreamAgent to remove redundancies and resolve conflicts. + + Args: + start_date: Start of time window (default: last auto dream time, or 30 days ago) + end_date: End of time window (default: now) + memory_types: Which memory types to process + dry_run: If true, return counts without applying any changes + model: Override the LLM model (e.g. "gpt-4.1-mini" for testing) + """ + from mirix.services.auto_dream_manager import AutoDreamManager + + client, auth_type = await get_client_from_jwt_or_api_key(authorization, http_request) + server = get_server() + + if user_id: + user = await server.user_manager.get_user_by_id(user_id=user_id) + else: + from mirix.services.admin_user_manager import ClientAuthManager + user = await server.user_manager.get_user_by_id( + user_id=ClientAuthManager.get_admin_user_id_for_client(client.id) + ) + + meta_agents = await server.agent_manager.list_agents(actor=client) + meta_agent_state = next( + (a for a in meta_agents if a.agent_type == AgentType.meta_memory_agent), + None, + ) + if meta_agent_state is None: + raise HTTPException( + status_code=400, + detail="No meta agent found for this client. Call /agents/meta/initialize first.", + ) + + try: + mgr = AutoDreamManager() + return await mgr.run( + request=request_body, + user=user, + actor=client, + meta_agent_state=meta_agent_state, + ) + except Exception as e: + logger.error("Auto dream failed: %s", e, exc_info=True) + raise HTTPException(status_code=500, detail=f"Auto dream failed: {str(e)}") + + +@router.post("/memory/auto_dream_v2") +async def auto_dream_v2_handler( + request_body: AutoDreamV2Request, + user_id: Optional[str] = Query(None, description="User ID to run consolidation for"), + authorization: Optional[str] = Header(None), + http_request: Request = None, +): + """ + Run the Auto-Dreamer v2 offline memory consolidation pipeline. + + Implements region rewriting inspired by the Auto-Dreamer paper (NeurIPS 2026): + the consolidator agent surveys the working region, synthesizes generalized + replacement entries (with slot abstraction and cross-session deduplication), + retires the source entries, and writes a checkpoint. + + Args: + start_date: Start of working-region window (default: last v2 checkpoint, or 30 days ago) + end_date: End of working-region window (default: now) + memory_types: Memory types to include in the working region (default: episodic, semantic, procedural) + dry_run: If true, return working-region stats without running consolidation + model: Override the LLM model for the consolidator + """ + from mirix.services.auto_dream_v2_manager import AutoDreamV2Manager + + client, auth_type = await get_client_from_jwt_or_api_key(authorization, http_request) + server = get_server() + + if user_id: + user = await server.user_manager.get_user_by_id(user_id=user_id) + else: + from mirix.services.admin_user_manager import ClientAuthManager + user = await server.user_manager.get_user_by_id( + user_id=ClientAuthManager.get_admin_user_id_for_client(client.id) + ) + + meta_agents = await server.agent_manager.list_agents(actor=client) + meta_agent_state = next( + (a for a in meta_agents if a.agent_type == AgentType.meta_memory_agent), + None, + ) + if meta_agent_state is None: + raise HTTPException( + status_code=400, + detail="No meta agent found for this client. Call /agents/meta/initialize first.", + ) + + try: + mgr = AutoDreamV2Manager() + return await mgr.run( + request=request_body, + user=user, + actor=client, + meta_agent_state=meta_agent_state, + ) + except Exception as e: + logger.error("Auto-Dream v2 failed: %s", e, exc_info=True) + raise HTTPException(status_code=500, detail=f"Auto-Dream v2 failed: {str(e)}") + + class UpdateResourceMemoryRequest(BaseModel): """Request model for updating a resource memory.""" diff --git a/mirix/server/server.py b/mirix/server/server.py index 3a200188c..2e38e3633 100644 --- a/mirix/server/server.py +++ b/mirix/server/server.py @@ -25,6 +25,8 @@ import mirix.system as system from mirix.agent import ( Agent, + AutoDreamAgent, + AutoDreamV2Agent, BackgroundAgent, CoreMemoryAgent, EpisodicMemoryAgent, @@ -685,6 +687,10 @@ async def load_agent( agent = ReflexionAgent(agent_state=agent_state, **common_kwargs) elif agent_state.agent_type == AgentType.background_agent: agent = BackgroundAgent(agent_state=agent_state, **common_kwargs) + elif agent_state.agent_type == AgentType.auto_dream_agent: + agent = AutoDreamAgent(agent_state=agent_state, **common_kwargs) + elif agent_state.agent_type == AgentType.auto_dream_v2_agent: + agent = AutoDreamV2Agent(agent_state=agent_state, **common_kwargs) else: raise ValueError(f"Invalid agent type {agent_state.agent_type}") diff --git a/mirix/services/auto_dream_manager.py b/mirix/services/auto_dream_manager.py new file mode 100644 index 000000000..d948df990 --- /dev/null +++ b/mirix/services/auto_dream_manager.py @@ -0,0 +1,372 @@ +""" +AutoDreamManager: orchestrates the auto_dream self-reflection pipeline. + +Flow: + 1. Resolve time window (last dream checkpoint → now) + 2. Fetch memories for each requested type + 3. Format memories into a structured message + 4. Invoke AutoDreamAgent via step() + 5. Write a checkpoint episodic memory entry + 6. Return stats +""" + +import datetime as dt +import json +import logging +from typing import List, Optional + +from mirix.schemas.agent import AgentState, AgentType, CreateAgent +from mirix.schemas.auto_dream import AutoDreamRequest, AutoDreamResponse, MemoryTypeStats +from mirix.schemas.client import Client as PydanticClient +from mirix.schemas.message import MessageCreate +from mirix.schemas.enums import MessageRole +from mirix.schemas.user import User as PydanticUser + +logger = logging.getLogger(__name__) + +# event_type used to tag auto_dream checkpoint records in episodic memory +_CHECKPOINT_EVENT_TYPE = "auto_dream_checkpoint" + + +class AutoDreamManager: + # ------------------------------------------------------------------ # + # Checkpoint helpers # + # ------------------------------------------------------------------ # + + async def get_last_dream_time( + self, + user: PydanticUser, + actor: PydanticClient, + agent_state: AgentState, + ) -> Optional[dt.datetime]: + """Return occurred_at of the most recent auto_dream checkpoint, or None.""" + from mirix.services.episodic_memory_manager import EpisodicMemoryManager + + mgr = EpisodicMemoryManager() + events = await mgr.list_episodic_memory( + user=user, + agent_state=agent_state, + query=_CHECKPOINT_EVENT_TYPE, + search_method="string_match", + search_field="event_type", + limit=1, + use_cache=False, + ) + for ev in events: + if ev.event_type == _CHECKPOINT_EVENT_TYPE: + return ev.occurred_at + return None + + async def write_checkpoint( + self, + user: PydanticUser, + actor: PydanticClient, + agent_state: AgentState, + dream_time: dt.datetime, + ) -> None: + from mirix.schemas.episodic_memory import EpisodicEvent + from mirix.services.episodic_memory_manager import EpisodicMemoryManager + + import uuid + + mgr = EpisodicMemoryManager() + checkpoint = EpisodicEvent( + id=f"ep_{uuid.uuid4().hex[:12]}", + occurred_at=dream_time, + actor="system", + event_type=_CHECKPOINT_EVENT_TYPE, + summary="Auto dream completed", + details=f"Auto dream run finished at {dream_time.isoformat()}", + filter_tags={"type": "system", "source": "auto_dream"}, + user_id=user.id, + organization_id=actor.organization_id, + ) + await mgr.create_episodic_memory(episodic_memory=checkpoint, actor=actor) + + # ------------------------------------------------------------------ # + # Memory fetching # + # ------------------------------------------------------------------ # + + async def _fetch_episodic( + self, + user: PydanticUser, + agent_state: AgentState, + start_date: dt.datetime, + end_date: dt.datetime, + ) -> list: + from mirix.services.episodic_memory_manager import EpisodicMemoryManager + + mgr = EpisodicMemoryManager() + events = await mgr.list_episodic_memory( + user=user, + agent_state=agent_state, + # Auto-dream fetches ALL current memories regardless of date; + # the passed window is only recorded in the response for reference. + start_date=None, + end_date=None, + search_method="string_match", + query="", + limit=500, + use_cache=False, + ) + # exclude system checkpoints + return [e for e in events if e.event_type != _CHECKPOINT_EVENT_TYPE] + + async def _fetch_semantic( + self, + user: PydanticUser, + agent_state: AgentState, + start_date: dt.datetime, + end_date: dt.datetime, + ) -> list: + from mirix.services.semantic_memory_manager import SemanticMemoryManager + + mgr = SemanticMemoryManager() + return await mgr.list_semantic_items( + user=user, + agent_state=agent_state, + search_method="string_match", + query="", + limit=500, + use_cache=False, + # Auto-dream fetches ALL current memories regardless of date; + # the passed window is only recorded in the response for reference. + start_date=None, + end_date=None, + ) + + async def _fetch_procedural( + self, + user: PydanticUser, + agent_state: AgentState, + start_date: dt.datetime, + end_date: dt.datetime, + ) -> list: + from mirix.services.procedural_memory_manager import ProceduralMemoryManager + + mgr = ProceduralMemoryManager() + return await mgr.list_procedures( + user=user, + agent_state=agent_state, + search_method="string_match", + query="", + limit=500, + use_cache=False, + # Auto-dream fetches ALL current memories regardless of date; + # the passed window is only recorded in the response for reference. + start_date=None, + end_date=None, + ) + + async def _fetch_resource( + self, + user: PydanticUser, + agent_state: AgentState, + start_date: dt.datetime, + end_date: dt.datetime, + ) -> list: + from mirix.services.resource_memory_manager import ResourceMemoryManager + + mgr = ResourceMemoryManager() + return await mgr.list_resources( + user=user, + agent_state=agent_state, + search_method="string_match", + query="", + limit=500, + use_cache=False, + # Auto-dream fetches ALL current memories regardless of date; + # the passed window is only recorded in the response for reference. + start_date=None, + end_date=None, + ) + + async def _fetch_knowledge_vault( + self, + user: PydanticUser, + agent_state: AgentState, + start_date: dt.datetime, + end_date: dt.datetime, + ) -> list: + from mirix.services.knowledge_vault_manager import KnowledgeVaultManager + + mgr = KnowledgeVaultManager() + return await mgr.list_knowledge( + user=user, + agent_state=agent_state, + search_method="string_match", + query="", + limit=500, + use_cache=False, + # Auto-dream fetches ALL current memories regardless of date; + # the passed window is only recorded in the response for reference. + start_date=None, + end_date=None, + ) + + # ------------------------------------------------------------------ # + # Agent management # + # ------------------------------------------------------------------ # + + async def get_or_create_dream_agent_state( + self, + actor: PydanticClient, + meta_agent_state: AgentState, + ) -> AgentState: + """Return the auto_dream_agent state for this client, creating it if needed.""" + from mirix.server.rest_api import get_server + + server = get_server() + children = await server.agent_manager.list_agents( + actor=actor, + parent_id=meta_agent_state.id, + ) + for child in children: + if child.name == "auto_dream_agent": + return child + + agent_create = CreateAgent( + name="auto_dream_agent", + agent_type=AgentType.auto_dream_agent, + llm_config=meta_agent_state.llm_config, + embedding_config=meta_agent_state.embedding_config, + parent_id=meta_agent_state.id, + ) + return await server.agent_manager.create_agent(agent_create=agent_create, actor=actor) + + # ------------------------------------------------------------------ # + # Main entry point # + # ------------------------------------------------------------------ # + + async def run( + self, + request: AutoDreamRequest, + user: PydanticUser, + actor: PydanticClient, + meta_agent_state: AgentState, + ) -> AutoDreamResponse: + from mirix.agent.auto_dream_agent import AutoDreamAgent + from mirix.server.rest_api import get_server + + server = get_server() + now = dt.datetime.now(dt.timezone.utc) + + # -- resolve time window -- + end_date = request.end_date or now + if request.start_date: + start_date = request.start_date + else: + last = await self.get_last_dream_time(user, actor, meta_agent_state) + start_date = last or (now - dt.timedelta(days=30)) + + # DB columns are TIMESTAMP WITHOUT TIME ZONE; strip tzinfo + if start_date.tzinfo is not None: + start_date = start_date.astimezone(dt.timezone.utc).replace(tzinfo=None) + if end_date.tzinfo is not None: + end_date = end_date.astimezone(dt.timezone.utc).replace(tzinfo=None) + + logger.info("Auto dream: window %s → %s, types=%s", start_date, end_date, request.memory_types) + + # -- fetch memories -- + fetch_map = { + "episodic": self._fetch_episodic, + "semantic": self._fetch_semantic, + "procedural": self._fetch_procedural, + "resource": self._fetch_resource, + "knowledge_vault": self._fetch_knowledge_vault, + } + memories: dict = {} + for mem_type in request.memory_types: + if mem_type in fetch_map: + items = await fetch_map[mem_type](user, meta_agent_state, start_date, end_date) + memories[mem_type] = items + logger.info(" %s: fetched %d items", mem_type, len(items)) + + # -- dry_run: just return counts without invoking agent -- + if request.dry_run: + processed = {t: MemoryTypeStats(total=len(items)) for t, items in memories.items()} + return AutoDreamResponse( + start_date=start_date, + end_date=end_date, + processed=processed, + last_dream_at=now, + dry_run=True, + message="Dry run — no changes applied.", + ) + + # -- build input message for the agent -- + payload = _format_memories_as_message(memories, start_date, end_date) + + # -- get or create agent state -- + dream_agent_state = await self.get_or_create_dream_agent_state(actor, meta_agent_state) + + # override model if caller requested it (e.g. for testing) + if request.model: + from copy import deepcopy + + dream_agent_state = deepcopy(dream_agent_state) + dream_agent_state.llm_config.model = request.model + + # -- load and run agent -- + dream_agent = await server.load_agent( + agent_id=dream_agent_state.id, + actor=actor, + user=user, + use_cache=False, + ) + input_msg = MessageCreate(role=MessageRole.user, content=payload) + await dream_agent.step(input_messages=input_msg, actor=actor, user=user) + + # -- write checkpoint -- + await self.write_checkpoint(user, actor, meta_agent_state, now) + + # -- build response (stats are approximate: we report totals fetched) -- + processed = {t: MemoryTypeStats(total=len(items)) for t, items in memories.items()} + return AutoDreamResponse( + # Auto-dream fetches ALL current memories regardless of date; + # the passed window is only recorded in the response for reference. + start_date=None, + end_date=None, + processed=processed, + last_dream_at=now, + dry_run=False, + message="Auto dream completed.", + ) + + +# ------------------------------------------------------------------ # +# Formatting helper # +# ------------------------------------------------------------------ # + +def _serialize_item(item) -> dict: + """Convert a Pydantic memory item to a compact dict for the LLM.""" + data = item.model_dump(exclude_none=True) + # drop heavy embedding vectors + for key in list(data.keys()): + if key.endswith("_embedding"): + del data[key] + return data + + +def _format_memories_as_message( + memories: dict, + start_date: dt.datetime, + end_date: dt.datetime, +) -> str: + lines = [ + f"Time window: {start_date.isoformat()} → {end_date.isoformat()}", + "", + "Please review the following memories for redundancy and conflicts.", + "Process each type in order: episodic → semantic → procedural → resource → knowledge_vault.", + "Call finish_memory_update when done.", + "", + ] + for mem_type, items in memories.items(): + lines.append(f"=== {mem_type.upper()} MEMORY ({len(items)} items) ===") + if not items: + lines.append("(empty)") + else: + serialized = [_serialize_item(item) for item in items] + lines.append(json.dumps(serialized, ensure_ascii=False, default=str, indent=2)) + lines.append("") + return "\n".join(lines) diff --git a/mirix/services/auto_dream_v2_manager.py b/mirix/services/auto_dream_v2_manager.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/mirix/services/auto_dream_v2_manager.py @@ -0,0 +1 @@ + diff --git a/mirix/services/memory_access_log_manager.py b/mirix/services/memory_access_log_manager.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/mirix/services/memory_access_log_manager.py @@ -0,0 +1 @@ +