Newer
Older
navi-1 / navi / tools / memory_search.py
"""`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))