mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
- Move parseFunctionCall/parseFunctionResponse to ResponseHelper - Enhance orphaned call/response filtering with detailed debug logs - Add toolId to conversation content for better tracking - Fix planning workflow execution mechanics and step numbering - Remove unused planning agent appendix and detailedAppendixForPlanningAgent - Add conversation save callbacks throughout AI controller loops - Improve multi-agent function handling to avoid exceptions - Update all tests to include toolId fields for proper filtering
89 lines
No EOL
3.5 KiB
TypeScript
89 lines
No EOL
3.5 KiB
TypeScript
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
|
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/Schemas/AIFunctionTypes";
|
|
import { StringTools } from "./StringTools";
|
|
import { Exception } from "./Exception";
|
|
|
|
// handle the rare event where a function call is also included in content (gemini sometimes does this)
|
|
export function sanitizeFunctionCallContent(content: string, functionCall: AIFunctionCall | null): string {
|
|
// Early returns for simple cases
|
|
if (!functionCall || !content.trim()) {
|
|
return content;
|
|
}
|
|
|
|
// If content has no JSON-like characters, return as-is
|
|
if (!content.includes('{') || !content.includes('}')) {
|
|
return content;
|
|
}
|
|
|
|
const functionCallString = functionCall.toConversationString();
|
|
let sanitized = content;
|
|
|
|
// Step 1: Remove markdown code blocks that might contain the function call
|
|
// Pattern matches ```json\n...\n``` or ```\n...\n```
|
|
sanitized = sanitized.replace(/```(?:json)?\s*\n?([\s\S]*?)\n?```/g, (match: string, codeContent: string) => {
|
|
// If the code block contains our function call, remove it entirely
|
|
if (codeContent.trim() === functionCallString.trim()) {
|
|
return '';
|
|
}
|
|
// Otherwise keep the code block
|
|
return match;
|
|
});
|
|
|
|
// Step 2: Remove exact JSON match (handles compact JSON)
|
|
sanitized = sanitized.replace(functionCallString, '').trim();
|
|
|
|
// Step 3: Handle pretty-printed variations by normalizing both strings
|
|
try {
|
|
const functionCallObj: unknown = JSON.parse(functionCallString);
|
|
const normalizedTarget = JSON.stringify(functionCallObj);
|
|
|
|
// Find and remove any JSON that matches when normalized
|
|
// This regex finds JSON objects/arrays in the text
|
|
const jsonPattern = /\{(?:[^{}]|(?:\{(?:[^{}]|(?:\{[^{}]*\}))*\}))*\}|\[(?:[^[\]]|(?:\[(?:[^[\]]|(?:\[[^[\]]*\]))*\]))*\]/g;
|
|
|
|
sanitized = sanitized.replace(jsonPattern, (match) => {
|
|
try {
|
|
const parsedMatch: unknown = JSON.parse(match);
|
|
const normalizedMatch = JSON.stringify(parsedMatch);
|
|
// Remove if it matches our function call when normalized
|
|
return normalizedMatch === normalizedTarget ? '' : match;
|
|
} catch {
|
|
// If it's not valid JSON, keep it
|
|
return match;
|
|
}
|
|
});
|
|
} catch {
|
|
// If function call string isn't valid JSON, we've done what we can
|
|
}
|
|
|
|
// Step 4: Clean up multiple consecutive whitespace/newlines left by removals
|
|
sanitized = sanitized.replace(/\n{3,}/g, '\n\n').trim();
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
export function parseFunctionCall(functionCallJson: string): StoredFunctionCall | null {
|
|
if (!StringTools.isValidJson(functionCallJson)) {
|
|
Exception.log(`Invalid JSON in functionCall field:\n${functionCallJson}`);
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(functionCallJson) as StoredFunctionCall;
|
|
} catch (error) {
|
|
Exception.log(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function parseFunctionResponse(responseJson: string): StoredFunctionResponse | null {
|
|
if (!StringTools.isValidJson(responseJson)) {
|
|
Exception.log(`Invalid JSON in function response content:\n${responseJson}`);
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(responseJson) as StoredFunctionResponse;
|
|
} catch (error) {
|
|
Exception.log(error);
|
|
return null;
|
|
}
|
|
} |