From 4998d0473965c7de6232ef57b0aced75c7285af2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 28 Feb 2026 18:12:58 -0500 Subject: [PATCH] Fix search_web over-triggering when user references existing notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Python fast-path regex (_PRIOR_WORK_REFS) in classify_intent that detects phrases like "research you did", "note you made", "using your research", "based on the research" etc. and returns no-tool immediately — saving the 19s intent LLM call and correctly letting the main model answer using search_notes/context rather than firing off a web search. Also tighten the intent prompt rules for search_web: explicitly prohibit using it for creative/brainstorming requests or when the user references existing notes, and add a rule that creative/ideation questions ("think of", "come up with", "brainstorm") always route to null (chat). Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/services/intent.py | 42 ++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/fabledassistant/services/intent.py b/src/fabledassistant/services/intent.py index e79b21e..5b106cd 100644 --- a/src/fabledassistant/services/intent.py +++ b/src/fabledassistant/services/intent.py @@ -100,9 +100,12 @@ Rules: - "read", "open", "show me", "what does X say", "display", "pull up" a specific note → use get_note with query=. - "list my notes", "show notes", "recent notes", "browse notes", "notes tagged X" → use list_notes (with optional q or tags). - "tag X with Y", "add tag Y to X", "untag Y from X", "remove tag Y from X" → use update_note with tags=[Y] and tag_mode="add" or "remove". -- search_web: user wants a quick factual answer retrieved from the web, without creating a note. - Use for brief lookups where a short summary suffices (current version numbers, quick facts, - "what is X", "look up X"). Do NOT use when the user wants a detailed written reference. +- search_web: user explicitly wants a quick factual answer from the web — current events, version + numbers, real-time facts ("what is the latest version of X", "who won the game last night"). + ONLY use when the information is clearly not in their notes and they need something fresh from + the internet. Do NOT use for creative questions, brainstorming, game design, writing help, or + when the user is building on content they already have. Do NOT use when the user references + their own notes or prior research — use search_notes instead. - research_topic: user wants a comprehensive, multi-section research note created from web sources. Use whenever the user wants to deeply understand, learn about, or get a full written reference on any subject — regardless of how they phrase it. The topic can be anything: technical subjects, @@ -110,12 +113,35 @@ Rules: The "topic" argument should capture the full subject matter of the request. Prefer this over search_web when the user's request implies wanting thorough coverage rather than a quick answer. +- For creative/ideation requests ("think of", "come up with", "ideas for", "help me design", + "imagine", "brainstorm") use null (chat) — the main model answers these directly. Only route + to a tool if the user also explicitly asks to search, look something up, or create/save something. +- If the user references notes or research the assistant previously created, prefer search_notes + (to retrieve the relevant note) or null (chat) so the main model can use its tools to find it. + Never use search_web when existing notes likely contain the answer. - "ack": one short, natural sentence confirming the action (tool path only). Vary phrasing — do not always start with "Let me". Omit (null) for chat-only responses. - Do NOT wrap the JSON in markdown code fences.""" +# Fast-path: "Research: " sent by the Research button. _RESEARCH_PREFIX = re.compile(r"^[Rr]esearch:\s+(.+)", re.DOTALL) +# When the user refers to work the assistant previously did, they want the main +# model to answer using existing context — not a web search. Skip intent and +# fall through to the streaming path so the model can call search_notes itself. +_PRIOR_WORK_REFS = re.compile( + r"\b(" + r"research (you|that you|we) did" + r"|research (you|that you|we) (made|created|compiled|wrote)" + r"|note (you|that you|we) (made|created|wrote)" + r"|notes (you|that you|we) (made|created|wrote)" + r"|using (your|the) research" + r"|based on (your|the) research" + r"|from (your|the) (research|note|notes)" + r")\b", + re.IGNORECASE, +) + async def classify_intent( user_message: str, @@ -149,6 +175,16 @@ async def classify_intent( ack=f"I'll research that and compile a comprehensive note.", ) + # Fast-path: user references prior assistant work ("research you did", "note you + # made", etc.) — this is a request to use existing content, not search the web. + # Return no-tool so the main model answers conversationally with search_notes + # available if it needs to retrieve the note. + if _PRIOR_WORK_REFS.search(user_message): + logger.info( + "Intent fast-path: prior-work reference detected → skipping tool dispatch" + ) + return IntentResult() + tool_summary = _build_tool_summary(tools) today = date_type.today().isoformat()