mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: use promptContent for user messages across AI providers
Standardize message extraction logic to use promptContent field for user roles and content field for assistant roles. Update all AI provider implementations (Claude, OpenAI, Gemini) and corresponding tests. Remove trailing space insertion in ChatInput and clean up minor formatting issues.
This commit is contained in:
parent
a3d0b55100
commit
ba8db87ffd
8 changed files with 72 additions and 63 deletions
|
|
@ -11,6 +11,7 @@ import type AIAgentPlugin from "main";
|
|||
import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
|
||||
import { isValidJson } from "Helpers/Helpers";
|
||||
import type { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
|
||||
export class Claude implements IAIClass {
|
||||
|
||||
|
|
@ -153,11 +154,12 @@ export class Claude implements IAIClass {
|
|||
return conversationContent.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "")
|
||||
.map(content => {
|
||||
const contentBlocks: any[] = [];
|
||||
const contentToExtract = content.role == Role.User ? content.promptContent : content.content;
|
||||
|
||||
if (content.content.trim() !== "" && !content.isFunctionCallResponse) {
|
||||
if (contentToExtract.trim() !== "" && !content.isFunctionCallResponse) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: content.content
|
||||
text: contentToExtract
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +177,7 @@ export class Claude implements IAIClass {
|
|||
} catch (error) {
|
||||
console.error("Failed to parse function call:", error);
|
||||
// Fall back to treating as text
|
||||
if (content.content.trim() === "") {
|
||||
if (contentToExtract.trim() === "") {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: "Error parsing function call"
|
||||
|
|
@ -185,7 +187,7 @@ export class Claude implements IAIClass {
|
|||
} else {
|
||||
console.error("Invalid JSON in functionCall field");
|
||||
// Fall back to treating as text
|
||||
if (content.content.trim() === "") {
|
||||
if (contentToExtract.trim() === "") {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: "Error parsing function call"
|
||||
|
|
@ -195,10 +197,10 @@ export class Claude implements IAIClass {
|
|||
}
|
||||
|
||||
// Add function response if present
|
||||
if (content.isFunctionCallResponse && content.content.trim() !== "") {
|
||||
if (isValidJson(content.content)) {
|
||||
if (content.isFunctionCallResponse && contentToExtract.trim() !== "") {
|
||||
if (isValidJson(contentToExtract)) {
|
||||
try {
|
||||
const parsedContent = JSON.parse(content.content);
|
||||
const parsedContent = JSON.parse(contentToExtract);
|
||||
contentBlocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: parsedContent.id,
|
||||
|
|
@ -208,14 +210,14 @@ export class Claude implements IAIClass {
|
|||
console.error("Failed to parse function response:", error);
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: content.content
|
||||
text: contentToExtract
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid JSON in function response content");
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: content.content
|
||||
text: contentToExtract
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,29 +159,30 @@ export class Gemini implements IAIClass {
|
|||
return conversationContent.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "")
|
||||
.map(content => {
|
||||
const parts: any[] = [];
|
||||
const contentToExtract = content.role == Role.User ? content.promptContent : content.content;
|
||||
|
||||
if (content.content.trim() !== "") {
|
||||
if (contentToExtract.trim() !== "") {
|
||||
if (content.isFunctionCallResponse) {
|
||||
if (isValidJson(content.content)) {
|
||||
if (isValidJson(contentToExtract)) {
|
||||
try {
|
||||
const parsedContent = JSON.parse(content.content);
|
||||
const parsedContent = JSON.parse(contentToExtract);
|
||||
if (parsedContent.functionResponse) {
|
||||
parts.push({
|
||||
functionResponse: parsedContent.functionResponse
|
||||
});
|
||||
} else {
|
||||
parts.push({ text: content.content });
|
||||
parts.push({ text: contentToExtract });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse function response:", error);
|
||||
parts.push({ text: content.content });
|
||||
parts.push({ text: contentToExtract });
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid JSON in function response content");
|
||||
parts.push({ text: content.content });
|
||||
parts.push({ text: contentToExtract });
|
||||
}
|
||||
} else {
|
||||
parts.push({ text: content.content });
|
||||
parts.push({ text: contentToExtract });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,9 @@ export class OpenAI implements IAIClass {
|
|||
content: systemPrompt
|
||||
},
|
||||
...conversation.contents
|
||||
.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "")
|
||||
.map(content => {
|
||||
.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "")
|
||||
.map(content => {
|
||||
const contentToExtract = content.role == Role.User ? content.promptContent : content.content;
|
||||
// Handle function call
|
||||
if (content.isFunctionCall && content.functionCall.trim() !== "") {
|
||||
if (isValidJson(content.functionCall)) {
|
||||
|
|
@ -61,7 +62,7 @@ export class OpenAI implements IAIClass {
|
|||
const parsedContent = JSON.parse(content.functionCall);
|
||||
return {
|
||||
role: content.role,
|
||||
content: content.content.trim() !== "" ? content.content : null,
|
||||
content: contentToExtract.trim() !== "" ? contentToExtract : null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: parsedContent.functionCall.id,
|
||||
|
|
@ -78,23 +79,23 @@ export class OpenAI implements IAIClass {
|
|||
// Fall back to regular message
|
||||
return {
|
||||
role: content.role,
|
||||
content: content.content || "Error parsing function call"
|
||||
content: contentToExtract || "Error parsing function call"
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid JSON in functionCall field");
|
||||
return {
|
||||
role: content.role,
|
||||
content: content.content || "Error parsing function call"
|
||||
content: contentToExtract || "Error parsing function call"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle function response
|
||||
if (content.isFunctionCallResponse && content.content.trim() !== "") {
|
||||
if (isValidJson(content.content)) {
|
||||
if (content.isFunctionCallResponse && contentToExtract.trim() !== "") {
|
||||
if (isValidJson(contentToExtract)) {
|
||||
try {
|
||||
const parsedContent = JSON.parse(content.content);
|
||||
const parsedContent = JSON.parse(contentToExtract);
|
||||
return {
|
||||
role: "tool",
|
||||
tool_call_id: parsedContent.id,
|
||||
|
|
@ -105,14 +106,14 @@ export class OpenAI implements IAIClass {
|
|||
// Fall back to regular message
|
||||
return {
|
||||
role: content.role,
|
||||
content: content.content
|
||||
content: contentToExtract
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid JSON in function response content");
|
||||
return {
|
||||
role: content.role,
|
||||
content: content.content
|
||||
content: contentToExtract
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +121,7 @@ export class OpenAI implements IAIClass {
|
|||
// Regular text message
|
||||
return {
|
||||
role: content.role,
|
||||
content: content.content
|
||||
content: contentToExtract
|
||||
};
|
||||
})
|
||||
];
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
import ChatSearchResults from "./ChatSearchResults.svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { InputService } from "Services/InputService";
|
||||
import { textAreaNode } from "happy-dom/lib/PropertySymbol";
|
||||
|
||||
export let hasNoApiKey: boolean;
|
||||
export let isSubmitting: boolean;
|
||||
|
|
@ -88,7 +87,7 @@
|
|||
}
|
||||
|
||||
inputService.deleteTextRange(position - 1, position, textareaElement);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +130,6 @@
|
|||
|
||||
inputService.deleteTextRange($searchState.position, inputService.getCursorPosition(textareaElement), textareaElement);
|
||||
inputService.insertElementAtCursor(node, textareaElement);
|
||||
inputService.insertTextAtCursor(" ", textareaElement);
|
||||
}
|
||||
searchStateStore.resetSearch();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class ChatService {
|
|||
|
||||
const inputMessages = conversation.contents
|
||||
.filter(message => message.role === Role.User && !message.isFunctionCallResponse)
|
||||
.map(message => message.content)
|
||||
.map(message => message.promptContent)
|
||||
.join("\n");
|
||||
|
||||
const outputMessages = conversation.contents
|
||||
|
|
@ -143,8 +143,7 @@ export class ChatService {
|
|||
private async streamRequestResponse(
|
||||
conversation: Conversation, allowDestructiveActions: boolean, callbacks: IChatServiceCallbacks
|
||||
): Promise<{ functionCall: AIFunctionCall | null, shouldContinue: boolean }> {
|
||||
// this should never happen
|
||||
if (!this.ai) {
|
||||
if (!this.ai) { // this should never happen
|
||||
return { functionCall: null, shouldContinue: false };;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ describe('Claude', () => {
|
|||
describe('extractContents', () => {
|
||||
it('should convert simple text content to Claude message format', () => {
|
||||
const contents = [
|
||||
new ConversationContent(Role.User, 'Hello'),
|
||||
new ConversationContent(Role.User, 'Hello', 'Hello'), // content, promptContent
|
||||
new ConversationContent(Role.Assistant, 'Hi there')
|
||||
];
|
||||
|
||||
|
|
@ -301,14 +301,16 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function response to tool_result format', () => {
|
||||
const responseContent = JSON.stringify({
|
||||
id: 'call_123',
|
||||
functionResponse: {
|
||||
response: ['file1.txt', 'file2.txt']
|
||||
}
|
||||
});
|
||||
const functionResponseContent = new ConversationContent(
|
||||
Role.User,
|
||||
JSON.stringify({
|
||||
id: 'call_123',
|
||||
functionResponse: {
|
||||
response: ['file1.txt', 'file2.txt']
|
||||
}
|
||||
})
|
||||
responseContent,
|
||||
responseContent // promptContent should also be set for User role
|
||||
);
|
||||
functionResponseContent.isFunctionCallResponse = true;
|
||||
|
||||
|
|
@ -351,7 +353,8 @@ describe('Claude', () => {
|
|||
|
||||
const invalidContent = new ConversationContent(
|
||||
Role.User,
|
||||
'invalid json {'
|
||||
'invalid json {',
|
||||
'invalid json {' // promptContent for User role
|
||||
);
|
||||
invalidContent.isFunctionCallResponse = true;
|
||||
|
||||
|
|
@ -369,9 +372,9 @@ describe('Claude', () => {
|
|||
|
||||
it('should filter out empty content', () => {
|
||||
const contents = [
|
||||
new ConversationContent(Role.User, 'Hello'),
|
||||
new ConversationContent(Role.User, 'Hello', 'Hello'),
|
||||
new ConversationContent(Role.Assistant, ''), // Empty
|
||||
new ConversationContent(Role.User, 'World')
|
||||
new ConversationContent(Role.User, 'World', 'World')
|
||||
];
|
||||
|
||||
const result = (claude as any).extractContents(contents);
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ describe('Gemini', () => {
|
|||
|
||||
it('should format system instruction as parts array', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Test'));
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Test', 'Test'));
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
|
|
@ -357,14 +357,16 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should convert function response to Gemini format', () => {
|
||||
const responseContent = JSON.stringify({
|
||||
functionResponse: {
|
||||
name: 'search_files',
|
||||
response: ['file1.txt', 'file2.txt']
|
||||
}
|
||||
});
|
||||
const functionResponseContent = new ConversationContent(
|
||||
Role.User,
|
||||
JSON.stringify({
|
||||
functionResponse: {
|
||||
name: 'search_files',
|
||||
response: ['file1.txt', 'file2.txt']
|
||||
}
|
||||
})
|
||||
responseContent,
|
||||
responseContent // promptContent for User role
|
||||
);
|
||||
functionResponseContent.isFunctionCallResponse = true;
|
||||
|
||||
|
|
@ -407,7 +409,8 @@ describe('Gemini', () => {
|
|||
|
||||
const invalidContent = new ConversationContent(
|
||||
Role.User,
|
||||
'invalid json {'
|
||||
'invalid json {',
|
||||
'invalid json {' // promptContent for User role
|
||||
);
|
||||
invalidContent.isFunctionCallResponse = true;
|
||||
|
||||
|
|
@ -424,9 +427,9 @@ describe('Gemini', () => {
|
|||
|
||||
it('should filter out empty content', () => {
|
||||
const contents = [
|
||||
new ConversationContent(Role.User, 'Hello'),
|
||||
new ConversationContent(Role.User, 'Hello', 'Hello'),
|
||||
new ConversationContent(Role.Assistant, ''), // Empty
|
||||
new ConversationContent(Role.User, 'World')
|
||||
new ConversationContent(Role.User, 'World', 'World')
|
||||
];
|
||||
|
||||
const result = (gemini as any).extractContents(contents);
|
||||
|
|
|
|||
|
|
@ -358,14 +358,16 @@ describe('OpenAI', () => {
|
|||
|
||||
it('should convert function response to role:tool format', async () => {
|
||||
const conversation = new Conversation();
|
||||
const responseContent = JSON.stringify({
|
||||
id: 'call_123',
|
||||
functionResponse: {
|
||||
response: ['file1.txt', 'file2.txt']
|
||||
}
|
||||
});
|
||||
const functionResponseContent = new ConversationContent(
|
||||
Role.User,
|
||||
JSON.stringify({
|
||||
id: 'call_123',
|
||||
functionResponse: {
|
||||
response: ['file1.txt', 'file2.txt']
|
||||
}
|
||||
})
|
||||
responseContent,
|
||||
responseContent // promptContent for User role
|
||||
);
|
||||
functionResponseContent.isFunctionCallResponse = true;
|
||||
conversation.contents.push(functionResponseContent);
|
||||
|
|
@ -425,7 +427,7 @@ describe('OpenAI', () => {
|
|||
const invalidContent = new ConversationContent(
|
||||
Role.User,
|
||||
'invalid json {',
|
||||
false,
|
||||
'invalid json {', // promptContent for User role
|
||||
''
|
||||
);
|
||||
invalidContent.isFunctionCallResponse = true;
|
||||
|
|
@ -451,9 +453,9 @@ describe('OpenAI', () => {
|
|||
|
||||
it('should filter out empty content', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Hello'));
|
||||
conversation.contents.push(new ConversationContent(Role.Assistant, '', false, ''));
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'World'));
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Hello', 'Hello'));
|
||||
conversation.contents.push(new ConversationContent(Role.Assistant, '', '', ''));
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'World', 'World'));
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
|
|
|
|||
Loading…
Reference in a new issue