- Replace ClaudeAdapter with GPTAdapter using OpenAI GPT (gpt-5-mini) - Implement function calling for calendar operations (getDay, proposeCreate/Update/Delete, searchEvents) - Add provider-agnostic AI utilities in ai/utils/ (systemPrompt, toolDefinitions, toolExecutor, eventFormatter) - Add USE_TEST_RESPONSES env var to toggle between real AI and test responses - Switch ChatService.processMessage to use real AI provider - Add npm run format command for Prettier - Update CLAUDE.md with new architecture
157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
import {
|
|
ProposedEventChange,
|
|
getDay,
|
|
Day,
|
|
DAY_TO_GERMAN,
|
|
} from "@caldav/shared";
|
|
import { AIContext } from "../../services/interfaces";
|
|
import { formatDate, formatTime, formatDateTime } from "./eventFormatter";
|
|
|
|
/**
|
|
* Result of executing a tool call.
|
|
*/
|
|
export interface ToolResult {
|
|
content: string;
|
|
proposedChange?: ProposedEventChange;
|
|
}
|
|
|
|
/**
|
|
* Execute a tool call and return the result.
|
|
* This function is provider-agnostic and can be used with any LLM.
|
|
*/
|
|
export function executeToolCall(
|
|
name: string,
|
|
args: Record<string, unknown>,
|
|
context: AIContext,
|
|
): ToolResult {
|
|
switch (name) {
|
|
case "getDay": {
|
|
const date = getDay(
|
|
args.day as Day,
|
|
args.offset as number,
|
|
args.hour as number,
|
|
args.minute as number,
|
|
);
|
|
const dayName = DAY_TO_GERMAN[args.day as Day];
|
|
return {
|
|
content: `${date.toISOString()} (${dayName}, ${formatDate(date)} um ${formatTime(date)} Uhr)`,
|
|
};
|
|
}
|
|
|
|
case "getCurrentDateTime": {
|
|
const now = context.currentDate;
|
|
return {
|
|
content: `${now.toISOString()} (${formatDateTime(now)})`,
|
|
};
|
|
}
|
|
|
|
case "proposeCreateEvent": {
|
|
const event = {
|
|
title: args.title as string,
|
|
startTime: new Date(args.startTime as string),
|
|
endTime: new Date(args.endTime as string),
|
|
description: args.description as string | undefined,
|
|
isRecurring: args.isRecurring as boolean | undefined,
|
|
recurrenceRule: args.recurrenceRule as string | undefined,
|
|
};
|
|
const dateStr = formatDate(event.startTime);
|
|
const startStr = formatTime(event.startTime);
|
|
const endStr = formatTime(event.endTime);
|
|
return {
|
|
content: `Event-Vorschlag erstellt: "${event.title}" am ${dateStr} von ${startStr} bis ${endStr} Uhr`,
|
|
proposedChange: {
|
|
action: "create",
|
|
event,
|
|
},
|
|
};
|
|
}
|
|
|
|
case "proposeUpdateEvent": {
|
|
const eventId = args.eventId as string;
|
|
const existingEvent = context.existingEvents.find(
|
|
(e) => e.id === eventId,
|
|
);
|
|
|
|
if (!existingEvent) {
|
|
return { content: `Event mit ID ${eventId} nicht gefunden.` };
|
|
}
|
|
|
|
const updates: Record<string, unknown> = {};
|
|
if (args.title) updates.title = args.title;
|
|
if (args.startTime)
|
|
updates.startTime = new Date(args.startTime as string);
|
|
if (args.endTime) updates.endTime = new Date(args.endTime as string);
|
|
if (args.description) updates.description = args.description;
|
|
|
|
// Build event object for display (merge existing with updates)
|
|
const displayEvent = {
|
|
title: (updates.title as string) || existingEvent.title,
|
|
startTime: (updates.startTime as Date) || existingEvent.startTime,
|
|
endTime: (updates.endTime as Date) || existingEvent.endTime,
|
|
description:
|
|
(updates.description as string) || existingEvent.description,
|
|
isRecurring: existingEvent.isRecurring,
|
|
};
|
|
|
|
return {
|
|
content: `Update-Vorschlag für "${existingEvent.title}" erstellt.`,
|
|
proposedChange: {
|
|
action: "update",
|
|
eventId,
|
|
updates,
|
|
event: displayEvent,
|
|
},
|
|
};
|
|
}
|
|
|
|
case "proposeDeleteEvent": {
|
|
const eventId = args.eventId as string;
|
|
const existingEvent = context.existingEvents.find(
|
|
(e) => e.id === eventId,
|
|
);
|
|
|
|
if (!existingEvent) {
|
|
return { content: `Event mit ID ${eventId} nicht gefunden.` };
|
|
}
|
|
|
|
return {
|
|
content: `Lösch-Vorschlag für "${existingEvent.title}" erstellt.`,
|
|
proposedChange: {
|
|
action: "delete",
|
|
eventId,
|
|
event: {
|
|
title: existingEvent.title,
|
|
startTime: existingEvent.startTime,
|
|
endTime: existingEvent.endTime,
|
|
description: existingEvent.description,
|
|
isRecurring: existingEvent.isRecurring,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
case "searchEvents": {
|
|
const query = (args.query as string).toLowerCase();
|
|
const matches = context.existingEvents.filter((e) =>
|
|
e.title.toLowerCase().includes(query),
|
|
);
|
|
|
|
if (matches.length === 0) {
|
|
return { content: `Keine Termine mit "${args.query}" gefunden.` };
|
|
}
|
|
|
|
const results = matches
|
|
.map((e) => {
|
|
const start = new Date(e.startTime);
|
|
return `- ${e.title} (ID: ${e.id}) am ${formatDate(start)} um ${formatTime(start)} Uhr`;
|
|
})
|
|
.join("\n");
|
|
|
|
return { content: `Gefundene Termine:\n${results}` };
|
|
}
|
|
|
|
default:
|
|
return { content: `Unbekannte Funktion: ${name}` };
|
|
}
|
|
}
|