"""Memory tool — search, save, and forget facts about the user."""
from navi.memory.store import MemoryStore
from navi.tools.base import current_session_id
from .base import Tool, ToolResult
_VALID_CATEGORIES = {"profile", "preferences", "technical", "projects", "other"}
class MemoryTool(Tool):
name = "memory"
description = (
"Manage long-term memory about the user — facts that survive across sessions. "
"Actions: save (upsert a fact), search (find facts by query), forget (delete by key), list (all facts)."
)
parameters = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["save", "search", "forget", "list"],
"description": (
"save — upsert a fact (overwrites existing key). "
"search — find facts by keyword query. "
"forget — delete a fact by key. "
"list — return all stored facts."
),
},
"query": {
"type": "string",
"description": "search only: keywords describing what to look for.",
},
"category": {
"type": "string",
"enum": ["profile", "preferences", "technical", "projects", "other"],
"description": (
"save/forget: fact category. "
"profile=who they are, preferences=likes/dislikes, "
"technical=OS/tools/servers, projects=ongoing work, other=anything else."
),
},
"key": {
"type": "string",
"description": (
"save/forget: snake_case identifier unique within the category. "
"Examples: name, primary_os, home_server_ip, response_language."
),
},
"value": {
"type": "string",
"description": "save only: the fact as a concise plain-text statement.",
},
},
"required": ["action"],
}
def __init__(self, memory_store: MemoryStore) -> None:
self._store = memory_store
async def execute(self, params: dict) -> ToolResult:
action = params.get("action", "")
if action == "save":
return await self._save(params)
if action == "search":
return await self._search(params)
if action == "forget":
return await self._forget(params)
if action == "list":
return await self._list()
return ToolResult(success=False, output=f"Unknown action '{action}'.", error="invalid action")
async def _save(self, params: dict) -> ToolResult:
category = (params.get("category") or "").strip().lower()
key = (params.get("key") or "").strip()
value = (params.get("value") or "").strip()
if not category:
return ToolResult(success=False, output="category is required for save.", error="missing category")
if category not in _VALID_CATEGORIES:
return ToolResult(
success=False,
output=f"Invalid category '{category}'. Must be one of: {', '.join(sorted(_VALID_CATEGORIES))}",
error="invalid category",
)
if not key:
return ToolResult(success=False, output="key is required for save.", error="missing key")
if not value:
return ToolResult(success=False, output="value is required for save.", error="missing value")
session_id = current_session_id.get(None)
await self._store.upsert_fact(category, key, value, session_id)
return ToolResult(success=True, output=f"Saved [{category}] {key}: {value}")
async def _search(self, params: dict) -> ToolResult:
query = (params.get("query") or "").strip()
if not query:
return ToolResult(success=False, output="query is required for search.", error="missing query")
facts = await self._store.search_facts(query, limit=15)
if not facts:
return ToolResult(success=True, output="No matching facts found in memory.")
lines = [f"[{f['category']}] {f['key']}: {f['value']}" for f in facts]
return ToolResult(success=True, output=f"Found {len(facts)} fact(s):\n" + "\n".join(lines))
async def _forget(self, params: dict) -> ToolResult:
key = (params.get("key") or "").strip()
category = (params.get("category") or "").strip() or None
if not key:
return ToolResult(success=False, output="key is required for forget.", error="missing key")
deleted = await self._store.delete_fact(key, category)
if deleted == 0:
return ToolResult(success=False, output=f"No fact found with key '{key}'.", error="not found")
noun = "fact" if deleted == 1 else "facts"
return ToolResult(success=True, output=f"Deleted {deleted} {noun} with key '{key}'.")
async def _list(self) -> ToolResult:
facts = await self._store.get_all_facts()
if not facts:
return ToolResult(success=True, output="Memory is empty.")
lines = [f"[{f['category']}] {f['key']}: {f['value']}" for f in facts]
return ToolResult(success=True, output=f"{len(facts)} fact(s) in memory:\n" + "\n".join(lines))