diff --git a/navi/core/agent.py b/navi/core/agent.py index 99980f9..27dc2b7 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -247,7 +247,21 @@ session.context.append(user_msg) await self._sessions.save(session) - ctx_injections = await self._ctx_builder._collect_context_injections(profile) + _injected_fact_ids: set[str] = set() + ctx_task = asyncio.create_task(self._ctx_builder._collect_context_injections(profile)) + mem_facts_task = asyncio.create_task( + self._ctx_builder._memory_facts_msg( + user_message, user_id=session.user_id, injected_ids=_injected_fact_ids + ) + ) + ctx_injections = await ctx_task + mem_facts = await mem_facts_task + if mem_facts: + ctx_injections.append(mem_facts) + log.debug("agent.memory_facts_injected", session_id=session_id, facts_msg_length=len(mem_facts.content or "")) + else: + log.debug("agent.memory_facts_none", session_id=session_id) + for iteration in range(profile.max_iterations): log.debug("agent.iteration", session_id=session_id, iteration=iteration) response = await llm.complete( @@ -710,7 +724,20 @@ _known_failed: frozenset[tuple[int, str]] = frozenset() _replan_msg: str | None = None - ctx_injections = await self._ctx_builder._collect_context_injections(profile) + _injected_fact_ids: set[str] = set() + ctx_task = asyncio.create_task(self._ctx_builder._collect_context_injections(profile)) + mem_facts_task = asyncio.create_task( + self._ctx_builder._memory_facts_msg( + user_message, user_id=session.user_id, injected_ids=_injected_fact_ids + ) + ) + ctx_injections = await ctx_task + mem_facts = await mem_facts_task + if mem_facts: + ctx_injections.append(mem_facts) + log.debug("agent.memory_facts_injected", session_id=session_id, facts_msg_length=len(mem_facts.content or "")) + else: + log.debug("agent.memory_facts_none", session_id=session_id) # Tool-calling loop — uses stream_complete() for every turn so thinking # is captured in real-time via ThinkingDelta/ThinkingEnd events. diff --git a/navi/core/context_builder.py b/navi/core/context_builder.py index b841838..06e2bd3 100644 --- a/navi/core/context_builder.py +++ b/navi/core/context_builder.py @@ -6,10 +6,14 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING +import structlog + from navi.llm.base import Message import navi.config as _config +log = structlog.get_logger() + if TYPE_CHECKING: from navi.context_providers._loader import ContextProviderRegistry from navi.memory.store import MemoryStore @@ -91,6 +95,52 @@ content=f"## What I remember about the user\n\n{summary}", ) + async def _memory_facts_msg( + self, + user_message: str, + user_id: str | None = None, + injected_ids: set[str] | None = None, + ) -> "Message | None": + if self._memory is None: + log.info("memory_facts_msg.skip", reason="no_memory_store") + return None + msg = user_message.strip() + if len(msg) <= 20: + log.info("memory_facts_msg.skip", reason="too_short", length=len(msg)) + return None + words = [w for w in msg.split() if any(c.isalpha() for c in w)] + if len(words) < 2: + log.info("memory_facts_msg.skip", reason="too_few_words", words=len(words)) + return None + if len(msg) < 50: + limit = 1 + elif len(msg) <= 150: + limit = 2 + else: + limit = 3 + log.info("memory_facts_msg.search", query=msg[:60], limit=limit, user_id=user_id) + facts = await self._memory.search_facts(msg, user_id=user_id, limit=limit) + log.info("memory_facts_msg.hits", count=len(facts) if facts else 0) + if not facts: + return None + if injected_ids is not None: + new_facts = [f for f in facts if f.get("id") not in injected_ids] + skipped = len(facts) - len(new_facts) + log.info("memory_facts_msg.dedup", new=len(new_facts), skipped=skipped) + if not new_facts: + return None + for f in new_facts: + injected_ids.add(f["id"]) + else: + new_facts = facts + lines = ["Known facts about the user:"] + for f in new_facts: + conf = f.get("confidence", 70) + lines.append(f"• [{f['category']}] {f['key']} → {f['value']} (confidence: {conf}%)") + result = "\n".join(lines) + log.info("memory_facts_msg.injected", lines=len(new_facts), length=len(result)) + return Message(role="system", content=result) + async def _collect_context_injections(self, profile: "AgentProfile") -> list[Message]: if self._cp_registry is None: return []