mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
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:
parent
771db0a156
commit
9bb0df7df3
4 changed files with 63 additions and 16 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface StreamChunk {
|
|||
isComplete: boolean;
|
||||
error?: string;
|
||||
functionCall?: AIFunctionCall;
|
||||
shouldContinue?: boolean;
|
||||
}
|
||||
|
||||
export class StreamingService {
|
||||
|
|
|
|||
Loading…
Reference in a new issue