"""`memory_search` — one query over facts, session sketches and raw history."""
from __future__ import annotations
from navi.memory.search import UnifiedMemorySearch, format_hits
from navi.tools._internal.base import current_session_id, current_user_id
from ._internal.base import Tool, ToolContext, ToolResult
class MemorySearchTool(Tool):
name = "memory_search"
description = (
"Search everything this Navi remembers, in one call: curated facts about the user, "
"the session scratchpads, and verbatim excerpts of earlier turns that context "
"compression dropped. Use it when the user refers back to something you can no "
"longer see — 'as we discussed', 'the port we picked', 'that file', a name, a "
"number, a link, a decision — before saying you do not remember. "
"Every result carries its source: a fact (curated, rewritten by key), a raw excerpt "
"('session X, turn Y'), or a sketch. Prefer a newer fact over an older raw excerpt; "
"when the two disagree the result says which one is outdated."
)
parameters = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": (
"What to look for, in the words of the thing itself — a name, a file, "
"a command, a value, the topic. Full sentences work too."
),
},
"scope": {
"type": "string",
"enum": ["all", "facts", "raw", "sketches"],
"description": (
"Which store to read. 'all' (default) searches all three and returns "
"facts first, then relevant raw excerpts, then sketches. "
"'raw' = verbatim history, including turns compressed out of view."
),
},
"k": {
"type": "integer",
"description": "How many results per store to return (1-20, default 5).",
"minimum": 1,
"maximum": 20,
},
},
"required": ["query"],
}
def __init__(self, search: UnifiedMemorySearch) -> None:
self._search = search
async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
query = (params.get("query") or "").strip()
if not query:
return ToolResult(
success=False, output="query is required.", error="missing query"
)
scope = (params.get("scope") or "all").strip().lower()
if scope not in ("all", "facts", "raw", "sketches"):
scope = "all"
try:
k = int(params.get("k") or 5)
except (TypeError, ValueError):
k = 5
k = max(1, min(k, 20))
session_id = (ctx.session_id if ctx else None) or current_session_id.get()
user_id = (ctx.user_id if ctx else None)
if user_id is None:
user_id = current_user_id.get(None)
summary_at = await self._search.latest_summary_at(session_id) if session_id else None
hits = await self._search.search(
query,
scope=scope,
k=k,
user_id=user_id,
current_session_id=session_id,
summary_at=summary_at,
)
return ToolResult(success=True, output=format_hits(hits, query, scope))