From 9bb0df7df3e014ba09a89124b6d26451e30b98ef Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 11 Oct 2025 13:57:11 +0100 Subject: [PATCH] Add multi-turn conversation support with shouldContinue flag Implement continuation logic for AI responses that require multiple turns without explicit function calls. Add STOP_REASON_STOP constant to detect when conversation should naturally end versus when it needs continuation. Update streaming flow to handle both function calls and continuation flags, enabling more fluid multi-step conversations. --- AIClasses/Gemini/Gemini.ts | 5 +++++ AIClasses/SystemPrompt.ts | 34 +++++++++++++++++++++++++++++++ Components/ChatWindow.svelte | 39 +++++++++++++++++++++--------------- Services/StreamingService.ts | 1 + 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 4e1f87b..89ea7b1 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -13,6 +13,7 @@ import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunc export class Gemini implements IAIClass { private readonly REQUEST_WEB_SEARCH: string = "request_web_search"; + private readonly STOP_REASON_STOP: string = "STOP"; private readonly apiKey: string; private readonly aiPrompt: IPrompt = Resolve(Services.IPrompt); @@ -138,6 +139,9 @@ export class Gemini implements IAIClass { } const isComplete = !!candidate?.finishReason; + const finishReason = candidate?.finishReason; + + const shouldContinue = isComplete && finishReason !== this.STOP_REASON_STOP; // If streaming is complete and we have accumulated a function call, return it if (isComplete && this.accumulatedFunctionName) { @@ -151,6 +155,7 @@ export class Gemini implements IAIClass { content: text, isComplete: isComplete, functionCall: functionCall, + shouldContinue: shouldContinue, }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown parsing error"; diff --git a/AIClasses/SystemPrompt.ts b/AIClasses/SystemPrompt.ts index 43cec3a..be63413 100644 --- a/AIClasses/SystemPrompt.ts +++ b/AIClasses/SystemPrompt.ts @@ -40,6 +40,40 @@ You can help users with: Acknowledge you checked their notes, then provide general information. Example: "I didn't find any notes about gem prices in your vault. To help you with gem pricing, I'd need to know: [ask clarifying questions]" +## Working with Vault Structure and File Paths + +The user's directory structure is intentional and meaningful. File paths contain semantic information that should guide your responses. + +**Critical: Directory names are qualifiers and filters** + +When a user's query contains descriptive terms that match directory names, treat those directories as the PRIMARY search scope: + +**Examples:** +- "list my important templates" → ONLY show files from '/Important templates/' directory, not all templates +- "show recent meeting notes" → Prioritize files in '/Meetings/' or '/Recent/' directories +- "find urgent tasks" → Focus on '/Urgent/' folder if it exists +- "my work projects" → Look specifically in '/Work/' or '/Projects/Work/' directories + +**Filtering Process:** +1. Identify the descriptive qualifier in the query (e.g., "important", "recent", "urgent", "work") +2. Check if this qualifier matches any directory name in the vault structure +3. If yes, FILTER results to only show files from that directory path +4. If no matching directory exists, then search more broadly and filter by filename/content + +**Common Mistakes to Avoid:** +- ❌ Listing all templates when asked for "important templates" (ignoring directory filter) +- ❌ Showing everything with keyword "meeting" when "project meetings" folder exists +- ❌ Treating directory names as just metadata rather than semantic filters + +**When directory structure is ambiguous:** +If unsure whether a term refers to a directory or content category, list the available directories to help the user refine their query. + +**Implementation:** +- Always examine the full file paths returned by vault searches +- Parse directory structure as hierarchical semantic categories +- Match user's qualifying terms to directory names before filename matching +- A file in '/Important templates/weekly-report.md' should ONLY appear when queried for "important templates", not for generic "templates" + ## Response Guidelines **Natural Integration:** diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 8e4802c..1464403 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -106,23 +106,25 @@ scrollToBottom(); - let functionCall: AIFunctionCall | null = await streamRequestResponse(); - while (functionCall) { + let response = await streamRequestResponse(); + while (response.functionCall || response.shouldContinue) { - if ('user_message' in functionCall.arguments) { - currentThought = functionCall.arguments.user_message + if (response.functionCall) { + if ('user_message' in response.functionCall.arguments) { + currentThought = response.functionCall.arguments.user_message + } + + conversation.contents = [...conversation.contents, new ConversationContent( + Role.Assistant, response.functionCall.toConversationString(), new Date(), true)]; + await conversationService.saveConversation(conversation); + + const functionResponse: AIFunctionResponse = await aiFunctionService.performAIFunction(response.functionCall); + conversation.contents = [...conversation.contents, new ConversationContent( + Role.User, functionResponse.toConversationString(), new Date(), false, true)]; + await conversationService.saveConversation(conversation); } - conversation.contents = [...conversation.contents, new ConversationContent( - Role.Assistant, functionCall.toConversationString(), new Date(), true)]; - await conversationService.saveConversation(conversation); - - const functionResponse: AIFunctionResponse = await aiFunctionService.performAIFunction(functionCall); - conversation.contents = [...conversation.contents, new ConversationContent( - Role.User, functionResponse.toConversationString(), new Date(), false, true)]; - await conversationService.saveConversation(conversation); - - functionCall = await streamRequestResponse(); + response = await streamRequestResponse(); } } finally { currentThought = null; @@ -131,7 +133,7 @@ } } - async function streamRequestResponse(): Promise { + async function streamRequestResponse(): Promise<{ functionCall: AIFunctionCall | null, shouldContinue: boolean }> { // Create AI message placeholder const aiMessageIndex = conversation.contents.length; conversation.contents = [...conversation.contents, new ConversationContent(Role.Assistant, "")]; @@ -139,6 +141,7 @@ let accumulatedContent = ""; let capturedFunctionCall: AIFunctionCall | null = null; + let capturedShouldContinue = false; for await (const chunk of ai.streamRequest(conversation)) { if (chunk.error) { @@ -167,6 +170,10 @@ capturedFunctionCall = chunk.functionCall; } + if (chunk.shouldContinue) { + capturedShouldContinue = true; + } + if (chunk.isComplete) { isStreaming = false; // If there's a function call, remove the placeholder message @@ -187,7 +194,7 @@ } } - return capturedFunctionCall; + return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue }; } function handleKeydown(e: KeyboardEvent) { diff --git a/Services/StreamingService.ts b/Services/StreamingService.ts index fa3b300..9b8b04c 100644 --- a/Services/StreamingService.ts +++ b/Services/StreamingService.ts @@ -5,6 +5,7 @@ export interface StreamChunk { isComplete: boolean; error?: string; functionCall?: AIFunctionCall; + shouldContinue?: boolean; } export class StreamingService {