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
This commit is contained in:
20
CLAUDE.md
20
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.
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ interface ChatState {
|
||||
export const useChatStore = create<ChatState>((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] }));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<AIResponse> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
116
apps/server/src/ai/GPTAdapter.ts
Normal file
116
apps/server/src/ai/GPTAdapter.ts
Normal file
@@ -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<AIResponse> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from "./ClaudeAdapter";
|
||||
export * from "./GPTAdapter";
|
||||
|
||||
29
apps/server/src/ai/utils/eventFormatter.ts
Normal file
29
apps/server/src/ai/utils/eventFormatter.ts
Normal file
@@ -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");
|
||||
}
|
||||
13
apps/server/src/ai/utils/index.ts
Normal file
13
apps/server/src/ai/utils/index.ts
Normal file
@@ -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";
|
||||
37
apps/server/src/ai/utils/systemPrompt.ts
Normal file
37
apps/server/src/ai/utils/systemPrompt.ts
Normal file
@@ -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}`;
|
||||
}
|
||||
170
apps/server/src/ai/utils/toolDefinitions.ts
Normal file
170
apps/server/src/ai/utils/toolDefinitions.ts
Normal file
@@ -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<string, ParameterDef>;
|
||||
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"],
|
||||
},
|
||||
},
|
||||
];
|
||||
156
apps/server/src/ai/utils/toolExecutor.ts
Normal file
156
apps/server/src/ai/utils/toolExecutor.ts
Normal file
@@ -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<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}` };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -265,12 +265,26 @@ export class ChatService {
|
||||
content: data.content,
|
||||
});
|
||||
|
||||
const response = await getTestResponse(
|
||||
responseIndex,
|
||||
this.eventRepo,
|
||||
userId,
|
||||
);
|
||||
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<ChatResponse> {
|
||||
// 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;
|
||||
|
||||
43
package-lock.json
generated
43
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user