diff --git a/mirix/agent/__init__.py b/mirix/agent/__init__.py index 44ad1a64b..879fcd355 100755 --- a/mirix/agent/__init__.py +++ b/mirix/agent/__init__.py @@ -33,9 +33,11 @@ "ReflexionAgent", "ResourceMemoryAgent", "SemanticMemoryAgent", + "AutoDreamAgent", ] from mirix.agent.agent import Agent, AgentState, save_agent +from mirix.agent.auto_dream_agent import AutoDreamAgent 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_configs.py b/mirix/agent/agent_configs.py index 6d1ada78e..14b757578 100644 --- a/mirix/agent/agent_configs.py +++ b/mirix/agent/agent_configs.py @@ -62,4 +62,10 @@ "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, + }, ] 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/meta_agent.py b/mirix/agent/meta_agent.py index 6392bd8bb..80b01b7bf 100644 --- a/mirix/agent/meta_agent.py +++ b/mirix/agent/meta_agent.py @@ -41,6 +41,7 @@ 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 def set_agent_state(self, name: str, state: AgentState): """Set an agent state by name.""" @@ -68,6 +69,7 @@ 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, } def get_all_agent_states_list(self) -> List[Optional[AgentState]]: @@ -82,6 +84,7 @@ 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, ] @@ -141,6 +144,12 @@ 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, + }, ] @@ -293,6 +302,8 @@ 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 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..d225d3e62 100644 --- a/mirix/client/remote_client.py +++ b/mirix/client/remote_client.py @@ -1849,6 +1849,39 @@ 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, + ) + # ======================================================================== # LangChain/Composio/CrewAI Integration (Not Supported) # ======================================================================== 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/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..52d0aecb9 100755 --- a/mirix/schemas/agent.py +++ b/mirix/schemas/agent.py @@ -33,6 +33,7 @@ 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" 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/server/rest_api.py b/mirix/server/rest_api.py index a9963df83..eca0cc780 100644 --- a/mirix/server/rest_api.py +++ b/mirix/server/rest_api.py @@ -24,6 +24,7 @@ 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.block import Block, BlockUpdate, CreateBlock, Human, Persona from mirix.schemas.client import Client, ClientCreate, ClientUpdate from mirix.schemas.embedding_config import EmbeddingConfig @@ -5078,6 +5079,64 @@ 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.schemas.auto_dream import AutoDreamRequest, AutoDreamResponse + 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)}") + + class UpdateResourceMemoryRequest(BaseModel): """Request model for updating a resource memory.""" diff --git a/mirix/server/server.py b/mirix/server/server.py index 3a200188c..f8de97c3d 100644 --- a/mirix/server/server.py +++ b/mirix/server/server.py @@ -25,6 +25,7 @@ import mirix.system as system from mirix.agent import ( Agent, + AutoDreamAgent, BackgroundAgent, CoreMemoryAgent, EpisodicMemoryAgent, @@ -685,6 +686,8 @@ 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) 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)