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:
Andrew Beal 2025-10-31 18:32:40 +00:00
parent a3d0b55100
commit ba8db87ffd
8 changed files with 72 additions and 63 deletions

View file

@ -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
});
}
}

View file

@ -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 });
}
}

View file

@ -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
};
})
];

View file

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

View file

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

View file

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

View file

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

View file

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