From 675785ec932350da7ee38bdb79e239e914baee14 Mon Sep 17 00:00:00 2001 From: Linus Waldowsky Date: Sat, 10 Jan 2026 00:22:59 +0100 Subject: [PATCH] feat: replace Claude with GPT for AI chat integration - 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 --- CLAUDE.md | 20 ++- apps/client/src/app/(tabs)/chat.tsx | 6 +- apps/client/src/stores/ChatStore.ts | 2 +- apps/client/src/stores/index.ts | 6 +- apps/server/package.json | 2 +- apps/server/src/ai/ClaudeAdapter.ts | 21 --- apps/server/src/ai/GPTAdapter.ts | 116 +++++++++++++ apps/server/src/ai/index.ts | 2 +- apps/server/src/ai/utils/eventFormatter.ts | 29 ++++ apps/server/src/ai/utils/index.ts | 13 ++ apps/server/src/ai/utils/systemPrompt.ts | 37 +++++ apps/server/src/ai/utils/toolDefinitions.ts | 170 ++++++++++++++++++++ apps/server/src/ai/utils/toolExecutor.ts | 156 ++++++++++++++++++ apps/server/src/app.ts | 4 +- apps/server/src/services/ChatService.ts | 30 +++- package-lock.json | 43 ++--- package.json | 3 + 17 files changed, 599 insertions(+), 61 deletions(-) delete mode 100644 apps/server/src/ai/ClaudeAdapter.ts create mode 100644 apps/server/src/ai/GPTAdapter.ts create mode 100644 apps/server/src/ai/utils/eventFormatter.ts create mode 100644 apps/server/src/ai/utils/index.ts create mode 100644 apps/server/src/ai/utils/systemPrompt.ts create mode 100644 apps/server/src/ai/utils/toolDefinitions.ts create mode 100644 apps/server/src/ai/utils/toolExecutor.ts diff --git a/CLAUDE.md b/CLAUDE.md index d846f89..1b8c4b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ This is a fullstack TypeScript monorepo with npm workspaces. ### Root (monorepo) ```bash npm install # Install all dependencies for all workspaces +npm run format # Format all TypeScript files with Prettier ``` ### Client (apps/client) - Expo React Native app @@ -44,7 +45,7 @@ npm run start -w @caldav/server # Run compiled server (port 3000) | Backend | Express.js | Web framework | | | MongoDB | Database | | | Mongoose | ODM | -| | Claude (Anthropic) | AI/LLM for chat | +| | GPT (OpenAI) | AI/LLM for chat | | | JWT | Authentication | | Planned | iCalendar | Event export/import | @@ -134,7 +135,14 @@ src/ │ ├── MongoEventRepository.ts │ └── MongoChatRepository.ts ├── ai/ -│ └── ClaudeAdapter.ts # Implements AIProvider +│ ├── GPTAdapter.ts # Implements AIProvider using OpenAI GPT +│ ├── index.ts # Re-exports GPTAdapter +│ └── utils/ # Shared AI utilities (provider-agnostic) +│ ├── index.ts # Re-exports +│ ├── eventFormatter.ts # formatExistingEvents() for system prompt +│ ├── systemPrompt.ts # buildSystemPrompt() - German calendar assistant prompt +│ ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs +│ └── toolExecutor.ts # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents └── utils/ ├── jwt.ts # signToken(), verifyToken() ├── password.ts # hash(), compare() @@ -264,6 +272,8 @@ Server requires `.env` file in `apps/server/`: JWT_SECRET=your-secret-key JWT_EXPIRES_IN=1h MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin +OPENAI_API_KEY=sk-proj-... +USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI ``` ## Current Implementation Status @@ -284,15 +294,15 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin - `utils/eventFormatters`: getWeeksOverview(), getMonthOverview() with German localization - `utils/recurrenceExpander`: expandRecurringEvents() using rrule library for RRULE parsing - `ChatController`: getConversations(), getConversation() with cursor-based pagination support - - `ChatService`: getConversations(), getConversation(), processMessage() now persists user/assistant messages to DB, confirmEvent()/rejectEvent() update respondedAction and persist response messages + - `ChatService`: getConversations(), getConversation(), processMessage() uses real AI or test responses (via USE_TEST_RESPONSES), confirmEvent()/rejectEvent() update respondedAction and persist response messages - `MongoChatRepository`: Full CRUD implemented (getConversationsByUser, createConversation, getMessages with cursor pagination, createMessage, updateMessage) - `ChatRepository` interface: updateMessage() added for respondedAction tracking + - `GPTAdapter`: Full implementation with OpenAI GPT (gpt-5-mini model), function calling for calendar operations + - `ai/utils/`: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor, eventFormatter) - **Stubbed (TODO):** - `AuthMiddleware.authenticate()`: Currently uses fake user for testing - `AuthController`: refresh(), logout() - `AuthService`: refreshToken() -- **Not started:** - - `ClaudeAdapter` (AI integration - currently using test responses) **Shared:** Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type, and date utilities defined and exported. diff --git a/apps/client/src/app/(tabs)/chat.tsx b/apps/client/src/app/(tabs)/chat.tsx index d1a2498..2c56400 100644 --- a/apps/client/src/app/(tabs)/chat.tsx +++ b/apps/client/src/app/(tabs)/chat.tsx @@ -13,7 +13,11 @@ import Header from "../../components/Header"; import BaseBackground from "../../components/BaseBackground"; import { FlashList } from "@shopify/flash-list"; import { ChatService } from "../../services"; -import { useChatStore, chatMessageToMessageData, MessageData } from "../../stores"; +import { + useChatStore, + chatMessageToMessageData, + MessageData, +} from "../../stores"; import { ProposedEventChange } from "@caldav/shared"; import { ProposedEventCard } from "../../components/ProposedEventCard"; diff --git a/apps/client/src/stores/ChatStore.ts b/apps/client/src/stores/ChatStore.ts index 8e9f2a4..f818bf7 100644 --- a/apps/client/src/stores/ChatStore.ts +++ b/apps/client/src/stores/ChatStore.ts @@ -23,7 +23,7 @@ interface ChatState { export const useChatStore = create((set) => ({ messages: [], addMessages(messages) { - set((state) => ({messages: [...state.messages, ...messages]})) + set((state) => ({ messages: [...state.messages, ...messages] })); }, addMessage: (message: MessageData) => { set((state) => ({ messages: [...state.messages, message] })); diff --git a/apps/client/src/stores/index.ts b/apps/client/src/stores/index.ts index cdfafdb..a832c08 100644 --- a/apps/client/src/stores/index.ts +++ b/apps/client/src/stores/index.ts @@ -1,3 +1,7 @@ export { useAuthStore } from "./AuthStore"; -export { useChatStore, chatMessageToMessageData, type MessageData } from "./ChatStore"; +export { + useChatStore, + chatMessageToMessageData, + type MessageData, +} from "./ChatStore"; export { useEventsStore } from "./EventsStore"; diff --git a/apps/server/package.json b/apps/server/package.json index b28ab79..f3e9422 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -8,13 +8,13 @@ "start": "node dist/app.js" }, "dependencies": { - "@anthropic-ai/sdk": "^0.71.2", "@caldav/shared": "*", "bcrypt": "^6.0.0", "dotenv": "^16.4.7", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.1", + "openai": "^6.15.0", "rrule": "^2.8.1" }, "devDependencies": { diff --git a/apps/server/src/ai/ClaudeAdapter.ts b/apps/server/src/ai/ClaudeAdapter.ts deleted file mode 100644 index 18e55d6..0000000 --- a/apps/server/src/ai/ClaudeAdapter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Anthropic from "@anthropic-ai/sdk"; -import { AIProvider, AIContext, AIResponse } from "../services/interfaces"; - -export class ClaudeAdapter implements AIProvider { - private client: Anthropic; - private model: string; - - constructor(apiKey?: string, model: string = "claude-3-haiku-20240307") { - this.client = new Anthropic({ - apiKey: apiKey || process.env.ANTHROPIC_API_KEY, - }); - this.model = model; - } - - async processMessage( - message: string, - context: AIContext, - ): Promise { - throw new Error("Not implemented"); - } -} diff --git a/apps/server/src/ai/GPTAdapter.ts b/apps/server/src/ai/GPTAdapter.ts new file mode 100644 index 0000000..e8aeb5f --- /dev/null +++ b/apps/server/src/ai/GPTAdapter.ts @@ -0,0 +1,116 @@ +import OpenAI from "openai"; +import { ProposedEventChange } from "@caldav/shared"; +import { AIProvider, AIContext, AIResponse } from "../services/interfaces"; +import { + buildSystemPrompt, + TOOL_DEFINITIONS, + executeToolCall, + ToolDefinition, +} from "./utils"; + +/** + * Convert tool definitions to OpenAI format. + */ +function toOpenAITools( + defs: ToolDefinition[], +): OpenAI.Chat.Completions.ChatCompletionTool[] { + return defs.map((def) => ({ + type: "function" as const, + function: { + name: def.name, + description: def.description, + parameters: def.parameters, + }, + })); +} + +export class GPTAdapter implements AIProvider { + private client: OpenAI; + private model: string; + private tools: OpenAI.Chat.Completions.ChatCompletionTool[]; + + constructor(apiKey?: string, model: string = "gpt-5-mini") { + this.client = new OpenAI({ + apiKey: apiKey || process.env.OPENAI_API_KEY, + }); + this.model = model; + this.tools = toOpenAITools(TOOL_DEFINITIONS); + } + + async processMessage( + message: string, + context: AIContext, + ): Promise { + const systemPrompt = buildSystemPrompt(context); + + // Build messages array with conversation history + const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ + { role: "developer", content: systemPrompt }, + ]; + + // Add conversation history + for (const msg of context.conversationHistory) { + messages.push({ + role: msg.sender === "user" ? "user" : "assistant", + content: msg.content, + }); + } + + // Add current user message + messages.push({ role: "user", content: message }); + + let proposedChange: ProposedEventChange | undefined; + + // Tool calling loop + while (true) { + const response = await this.client.chat.completions.create({ + model: this.model, + messages, + tools: this.tools, + }); + + const assistantMessage = response.choices[0].message; + + // If no tool calls, return the final response + if ( + !assistantMessage.tool_calls || + assistantMessage.tool_calls.length === 0 + ) { + return { + content: + assistantMessage.content || "Ich konnte keine Antwort generieren.", + proposedChange, + }; + } + + // Process tool calls + for (const toolCall of assistantMessage.tool_calls) { + // Skip non-function tool calls + if (toolCall.type !== "function") continue; + + const { name, arguments: argsRaw } = toolCall.function; + const args = JSON.parse(argsRaw); + + const result = executeToolCall(name, args, context); + + // If the tool returned a proposedChange, capture it + if (result.proposedChange) { + proposedChange = result.proposedChange; + } + + // Add assistant message with tool call + messages.push({ + role: "assistant", + tool_calls: [toolCall], + }); + + // Add tool result + messages.push({ + role: "tool", + tool_call_id: toolCall.id, + content: result.content, + }); + } + } + } +} diff --git a/apps/server/src/ai/index.ts b/apps/server/src/ai/index.ts index 7938c6d..a286767 100644 --- a/apps/server/src/ai/index.ts +++ b/apps/server/src/ai/index.ts @@ -1 +1 @@ -export * from "./ClaudeAdapter"; +export * from "./GPTAdapter"; diff --git a/apps/server/src/ai/utils/eventFormatter.ts b/apps/server/src/ai/utils/eventFormatter.ts new file mode 100644 index 0000000..1f72f37 --- /dev/null +++ b/apps/server/src/ai/utils/eventFormatter.ts @@ -0,0 +1,29 @@ +import { CalendarEvent } from "@caldav/shared"; + +// German date/time formatting helpers +export const formatDate = (d: Date) => d.toLocaleDateString("de-DE"); +export const formatTime = (d: Date) => + d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); +export const formatDateTime = (d: Date) => + `${formatDate(d)} ${d.toLocaleTimeString("de-DE")}`; + +/** + * Format a list of events for display in the system prompt. + * Output is in German with date/time formatting. + */ +export function formatExistingEvents(events: CalendarEvent[]): string { + if (events.length === 0) { + return "Keine Termine vorhanden."; + } + + return events + .map((e) => { + const start = new Date(e.startTime); + const end = new Date(e.endTime); + const timeStr = `${formatTime(start)} - ${formatTime(end)}`; + const recurring = e.isRecurring ? " (wiederkehrend)" : ""; + const desc = e.description ? ` | ${e.description}` : ""; + return `- ${e.title} (ID: ${e.id}) | ${formatDate(start)} ${timeStr}${recurring}${desc}`; + }) + .join("\n"); +} diff --git a/apps/server/src/ai/utils/index.ts b/apps/server/src/ai/utils/index.ts new file mode 100644 index 0000000..f2fc3e5 --- /dev/null +++ b/apps/server/src/ai/utils/index.ts @@ -0,0 +1,13 @@ +export { + formatExistingEvents, + formatDate, + formatTime, + formatDateTime, +} from "./eventFormatter"; +export { buildSystemPrompt } from "./systemPrompt"; +export { + TOOL_DEFINITIONS, + type ToolDefinition, + type ParameterDef, +} from "./toolDefinitions"; +export { executeToolCall, type ToolResult } from "./toolExecutor"; diff --git a/apps/server/src/ai/utils/systemPrompt.ts b/apps/server/src/ai/utils/systemPrompt.ts new file mode 100644 index 0000000..c338f1c --- /dev/null +++ b/apps/server/src/ai/utils/systemPrompt.ts @@ -0,0 +1,37 @@ +import { AIContext } from "../../services/interfaces"; +import { formatExistingEvents } from "./eventFormatter"; + +/** + * Build the system prompt for the AI assistant. + * This prompt is provider-agnostic and can be used with any LLM. + */ +export function buildSystemPrompt(context: AIContext): string { + const currentDate = context.currentDate.toLocaleDateString("de-DE", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + + const eventsText = formatExistingEvents(context.existingEvents); + + return `Du bist ein hilfreicher Kalender-Assistent für die App "CalChat". +Du hilfst Benutzern beim Erstellen, Ändern und Löschen von Terminen. +Antworte immer auf Deutsch. + +Aktuelles Datum und Uhrzeit: ${currentDate} + +Wichtige Regeln: +- Nutze getDay() für relative Datumsberechnungen (z.B. "nächsten Freitag um 14 Uhr") +- Wenn der Benutzer einen Termin erstellen will, nutze proposeCreateEvent +- Wenn der Benutzer einen Termin ändern will, nutze proposeUpdateEvent mit der Event-ID +- Wenn der Benutzer einen Termin löschen will, nutze proposeDeleteEvent mit der Event-ID +- Du kannst NUR EINEN Event-Vorschlag pro Antwort machen +- Wenn der Benutzer nach seinen Terminen fragt, nutze die unten stehende Liste +- Nutze searchEvents um nach Terminen zu suchen, wenn du die genaue ID brauchst + +Existierende Termine des Benutzers: +${eventsText}`; +} diff --git a/apps/server/src/ai/utils/toolDefinitions.ts b/apps/server/src/ai/utils/toolDefinitions.ts new file mode 100644 index 0000000..f38c49a --- /dev/null +++ b/apps/server/src/ai/utils/toolDefinitions.ts @@ -0,0 +1,170 @@ +/** + * Parameter definition for tool parameters. + */ +export interface ParameterDef { + type: "string" | "number" | "boolean" | "object" | "array"; + description?: string; + enum?: string[]; +} + +/** + * Provider-agnostic tool definition format. + * Can be converted to OpenAI, Claude, or other provider formats. + */ +export interface ToolDefinition { + name: string; + description: string; + parameters: { + type: "object"; + properties: Record; + required: string[]; + }; +} + +/** + * All available tools for the calendar assistant. + */ +export const TOOL_DEFINITIONS: ToolDefinition[] = [ + { + name: "getDay", + description: + "Get a date for a specific weekday relative to today. Returns an ISO date string.", + parameters: { + type: "object", + properties: { + day: { + type: "string", + enum: [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ], + description: "The target weekday", + }, + offset: { + type: "number", + description: + "1 = next occurrence, 2 = the one after, -1 = last occurrence, etc.", + }, + hour: { + type: "number", + description: "Hour of day (0-23)", + }, + minute: { + type: "number", + description: "Minute (0-59)", + }, + }, + required: ["day", "offset", "hour", "minute"], + }, + }, + { + name: "getCurrentDateTime", + description: "Get the current date and time as an ISO string", + parameters: { + type: "object", + properties: {}, + required: [], + }, + }, + { + name: "proposeCreateEvent", + description: + "Propose creating a new calendar event. The user must confirm before it's saved. Call this when the user wants to create a new appointment.", + parameters: { + type: "object", + properties: { + title: { + type: "string", + description: "Event title", + }, + startTime: { + type: "string", + description: "Start time as ISO date string", + }, + endTime: { + type: "string", + description: "End time as ISO date string", + }, + description: { + type: "string", + description: "Optional event description", + }, + isRecurring: { + type: "boolean", + description: "Whether this is a recurring event", + }, + recurrenceRule: { + type: "string", + description: "RRULE format string for recurring events", + }, + }, + required: ["title", "startTime", "endTime"], + }, + }, + { + name: "proposeUpdateEvent", + description: + "Propose updating an existing event. The user must confirm. Use this when the user wants to modify an appointment.", + parameters: { + type: "object", + properties: { + eventId: { + type: "string", + description: "ID of the event to update", + }, + title: { + type: "string", + description: "New title (optional)", + }, + startTime: { + type: "string", + description: "New start time as ISO date string (optional)", + }, + endTime: { + type: "string", + description: "New end time as ISO date string (optional)", + }, + description: { + type: "string", + description: "New description (optional)", + }, + }, + required: ["eventId"], + }, + }, + { + name: "proposeDeleteEvent", + description: + "Propose deleting an event. The user must confirm. Use this when the user wants to remove an appointment.", + parameters: { + type: "object", + properties: { + eventId: { + type: "string", + description: "ID of the event to delete", + }, + }, + required: ["eventId"], + }, + }, + { + name: "searchEvents", + description: + "Search for events by title in the user's calendar. Returns matching events.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query to match against event titles", + }, + }, + required: ["query"], + }, + }, +]; diff --git a/apps/server/src/ai/utils/toolExecutor.ts b/apps/server/src/ai/utils/toolExecutor.ts new file mode 100644 index 0000000..48589b0 --- /dev/null +++ b/apps/server/src/ai/utils/toolExecutor.ts @@ -0,0 +1,156 @@ +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, + 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 = {}; + 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}` }; + } +} diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index d9db7a6..3bb4d04 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -10,7 +10,7 @@ import { MongoEventRepository, MongoChatRepository, } from "./repositories"; -import { ClaudeAdapter } from "./ai"; +import { GPTAdapter } from "./ai"; const app = express(); const port = process.env.PORT || 3000; @@ -43,7 +43,7 @@ const eventRepo = new MongoEventRepository(); const chatRepo = new MongoChatRepository(); // Initialize AI provider -const aiProvider = new ClaudeAdapter(); +const aiProvider = new GPTAdapter(); // Initialize services const authService = new AuthService(userRepo); diff --git a/apps/server/src/services/ChatService.ts b/apps/server/src/services/ChatService.ts index d973ee3..aad270a 100644 --- a/apps/server/src/services/ChatService.ts +++ b/apps/server/src/services/ChatService.ts @@ -265,12 +265,26 @@ export class ChatService { content: data.content, }); - const response = await getTestResponse( - responseIndex, - this.eventRepo, - userId, - ); - responseIndex++; + let response: TestResponse; + + if (process.env.USE_TEST_RESPONSES === "true") { + // Test mode: use static responses + response = await getTestResponse(responseIndex, this.eventRepo, userId); + responseIndex++; + } else { + // Production mode: use real AI + const events = await this.eventRepo.findByUserId(userId); + const history = await this.chatRepo.getMessages(conversationId, { + limit: 20, + }); + + response = await this.aiProvider.processMessage(data.content, { + userId, + conversationHistory: history, + existingEvents: events, + currentDate: new Date(), + }); + } // Save and then return assistant response const answerMessage = await this.chatRepo.createMessage(conversationId, { @@ -292,7 +306,9 @@ export class ChatService { updates?: UpdateEventDTO, ): Promise { // Update original message with respondedAction - await this.chatRepo.updateMessage(messageId, { respondedAction: "confirm" }); + await this.chatRepo.updateMessage(messageId, { + respondedAction: "confirm", + }); // Perform the actual event operation let content: string; diff --git a/package-lock.json b/package-lock.json index 787ef2e..f820ba7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,13 +62,13 @@ "name": "@caldav/server", "version": "1.0.0", "dependencies": { - "@anthropic-ai/sdk": "^0.71.2", "@caldav/shared": "*", "bcrypt": "^6.0.0", "dotenv": "^16.4.7", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.1", + "openai": "^6.15.0", "rrule": "^2.8.1" }, "devDependencies": { @@ -106,26 +106,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.71.2", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz", - "integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -11252,6 +11232,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.15.0.tgz", + "integrity": "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index f7fcb8e..7901575 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "apps/*", "packages/*" ], + "scripts": { + "format": "prettier --write \"apps/*/src/**/*.{ts,tsx}\" \"packages/*/src/**/*.ts\"" + }, "devDependencies": { "eslint": "^9.25.0", "prettier": "^3.7.4",