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.
This commit is contained in:
Andrew Beal 2025-10-11 13:57:11 +01:00
parent 771db0a156
commit 9bb0df7df3
4 changed files with 63 additions and 16 deletions

View file

@ -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";

View file

@ -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:**

View file

@ -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<AIFunctionCall | null> {
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) {

View file

@ -5,6 +5,7 @@ export interface StreamChunk {
isComplete: boolean;
error?: string;
functionCall?: AIFunctionCall;
shouldContinue?: boolean;
}
export class StreamingService {