import { NextResponse, NextRequest } from 'next/server'; import redis from '../../../src/lib/redis'; import * as Sentry from '@sentry/nextjs'; import { PRICING, initialState, PAGE_SAMPLES, FEATURE_OPTIONS, FUNCTION_OPTIONS, API_OPTIONS, ASSET_OPTIONS, DESIGN_OPTIONS, EMPLOYEE_OPTIONS, DEADLINE_LABELS, } from '../../../src/logic/pricing/constants'; // Rate limiting const RATE_LIMIT_POINTS = 10; const RATE_LIMIT_DURATION = 60; // Tool definitions for Mistral const TOOLS = [ { type: 'function' as const, function: { name: 'update_company_info', description: 'Aktualisiert Firmen-/Kontaktinformationen des Kunden. Nutze dieses Tool wenn der Nutzer seinen Namen, seine Firma oder Mitarbeiterzahl nennt.', parameters: { type: 'object', properties: { companyName: { type: 'string', description: 'Firmenname' }, name: { type: 'string', description: 'Name des Ansprechpartners' }, employeeCount: { type: 'string', enum: EMPLOYEE_OPTIONS.map((e) => e.id), description: 'Mitarbeiterzahl', }, existingWebsite: { type: 'string', description: 'URL der bestehenden Website' }, }, }, }, }, { type: 'function' as const, function: { name: 'update_project_type', description: 'Setzt den Projekttyp. Nutze dieses Tool wenn klar wird ob es eine Website oder Web-App wird.', parameters: { type: 'object', properties: { projectType: { type: 'string', enum: ['website', 'web-app'], description: 'Art des Projekts', }, }, required: ['projectType'], }, }, }, { type: 'function' as const, function: { name: 'show_page_selector', description: 'Zeigt dem Nutzer eine interaktive Auswahl der verfügbaren Seiten-Typen. Nutze dieses Tool wenn über die Struktur/Seiten der Website gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'array', items: { type: 'string' }, description: 'Bereits ausgewählte Seiten-IDs basierend auf dem Gespräch', }, }, }, }, }, { type: 'function' as const, function: { name: 'show_feature_selector', description: 'Zeigt dem Nutzer eine interaktive Auswahl der verfügbaren Features (Blog, Produkte, Jobs, Cases, Events). Nutze dieses Tool wenn über Inhalts-Bereiche gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'array', items: { type: 'string' }, description: 'Vorausgewählte Feature-IDs', }, }, }, }, }, { type: 'function' as const, function: { name: 'show_function_selector', description: 'Zeigt dem Nutzer eine interaktive Auswahl der technischen Funktionen (Suche, Filter, PDF, Formulare). Nutze dieses Tool wenn über technische Anforderungen gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'array', items: { type: 'string' }, description: 'Vorausgewählte Funktions-IDs', }, }, }, }, }, { type: 'function' as const, function: { name: 'show_api_selector', description: 'Zeigt dem Nutzer eine interaktive Auswahl der System-Integrationen (CRM, ERP, Payment, etc.). Nutze dieses Tool wenn über Drittanbieter-Anbindungen gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'array', items: { type: 'string' }, description: 'Vorausgewählte API-IDs', }, }, }, }, }, { type: 'function' as const, function: { name: 'show_asset_selector', description: 'Zeigt dem Nutzer eine Auswahl welche Assets bereits vorhanden sind (Logo, Styleguide, Bilder etc.). Nutze dieses Tool wenn über vorhandenes Material gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'array', items: { type: 'string' }, description: 'Vorausgewählte Asset-IDs', }, }, }, }, }, { type: 'function' as const, function: { name: 'show_design_picker', description: 'Zeigt dem Nutzer eine visuelle Design-Stil-Auswahl. Nutze dieses Tool wenn über das Design oder den visuellen Stil gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'string', description: 'Vorausgewählter Design-Stil' }, }, }, }, }, { type: 'function' as const, function: { name: 'show_timeline_picker', description: 'Zeigt dem Nutzer eine Timeline/Deadline-Auswahl. Nutze dieses Tool wenn über Zeitrahmen oder Deadlines gesprochen wird.', parameters: { type: 'object', properties: { preselected: { type: 'string', description: 'Vorausgewählte Deadline' }, }, }, }, }, { type: 'function' as const, function: { name: 'show_contact_fields', description: 'Zeigt dem Nutzer Eingabefelder für E-Mail-Adresse und optionale Nachricht. Nutze dieses Tool wenn es Zeit ist die Kontaktdaten zu sammeln, typischerweise gegen Ende des Gesprächs.', parameters: { type: 'object', properties: {}, }, }, }, { type: 'function' as const, function: { name: 'request_file_upload', description: 'Zeigt dem Nutzer einen Datei-Upload-Bereich. Nutze dieses Tool wenn der Nutzer Dateien teilen möchte (Briefing, Sitemap, Design-Referenzen etc.).', parameters: { type: 'object', properties: { label: { type: 'string', description: 'Beschriftung des Upload-Bereichs' }, }, }, }, }, { type: 'function' as const, function: { name: 'show_estimate_preview', description: 'Zeigt dem Nutzer eine Live-Kostenübersicht basierend auf dem aktuellen Konfigurationsstand. Nutze dieses Tool wenn genügend Informationen gesammelt wurden oder wenn der Nutzer nach Kosten fragt.', parameters: { type: 'object', properties: {}, }, }, }, { type: 'function' as const, function: { name: 'generate_estimate_pdf', description: 'Generiert ein PDF-Angebot basierend auf dem aktuellen Konfigurationsstand. Nutze dieses Tool wenn der Nutzer ein Angebot/PDF möchte oder das Gespräch abgeschlossen wird.', parameters: { type: 'object', properties: {}, }, }, }, { type: 'function' as const, function: { name: 'submit_inquiry', description: 'Sendet die Anfrage ab und benachrichtigt Marc Mintel. Nutze dieses Tool wenn der Nutzer explizit absenden möchte und mindestens Name + Email vorhanden sind.', parameters: { type: 'object', properties: {}, }, }, }, ]; // Available options for the system prompt const availableOptions = ` VERFÜGBARE SEITEN: ${PAGE_SAMPLES.map((p) => `${p.id} (${p.label})`).join(', ')} VERFÜGBARE FEATURES: ${FEATURE_OPTIONS.map((f) => `${f.id} (${f.label})`).join(', ')} VERFÜGBARE FUNKTIONEN: ${FUNCTION_OPTIONS.map((f) => `${f.id} (${f.label})`).join(', ')} VERFÜGBARE API-INTEGRATIONEN: ${API_OPTIONS.map((a) => `${a.id} (${a.label})`).join(', ')} VERFÜGBARE ASSETS: ${ASSET_OPTIONS.map((a) => `${a.id} (${a.label})`).join(', ')} VERFÜGBARE DESIGN-STILE: ${DESIGN_OPTIONS.map((d) => `${d.id} (${d.label})`).join(', ')} DEADLINES: ${Object.entries(DEADLINE_LABELS).map(([k, v]) => `${k} (${v})`).join(', ')} MITARBEITER: ${EMPLOYEE_OPTIONS.map((e) => `${e.id} (${e.label})`).join(', ')} PREISE (netto): - Basis Website: ${PRICING.BASE_WEBSITE}€ - Pro Seite: ${PRICING.PAGE}€ - Pro Feature: ${PRICING.FEATURE}€ - Pro Funktion: ${PRICING.FUNCTION}€ - API-Integration: ${PRICING.API_INTEGRATION}€ - CMS Setup: ${PRICING.CMS_SETUP}€ - Hosting monatlich: ${PRICING.HOSTING_MONTHLY}€ `; const SYSTEM_PROMPT = `Du bist ein professioneller Projektberater der Digitalagentur "Mintel" – spezialisiert auf Next.js, Payload CMS und moderne Web-Infrastruktur. DEINE AUFGABE: Du führst ein natürliches Beratungsgespräch, um alle Informationen für eine Website-/Web-App-Projektschätzung zu sammeln. Du bist freundlich, kompetent und effizient. GESPRÄCHSFÜHRUNG: 1. Begrüße den Nutzer und frage nach seinem Namen und Unternehmen. 2. Finde heraus, was für ein Projekt es wird (Website oder Web-App). 3. Sammle schrittweise die Anforderungen – NICHT alle auf einmal fragen! 4. Pro Nachricht maximal 1-2 Themen ansprechen. 5. Nutze die verfügbaren Tools um interaktive Auswahl-Widgets zu zeigen. 6. Wenn du genug Informationen hast, zeige eine Kostenübersicht. 7. Biete an, ein PDF-Angebot zu generieren. 8. Sammle am Ende Kontaktdaten und biete an die Anfrage abzusenden. WICHTIGE REGELN: - ANTWORTE IN DER SPRACHE DES NUTZERS (Deutsch/Englisch). - Halte Antworten kurz und natürlich (2-4 Sätze pro Nachricht). - Zeige Widgets über Tool-Calls – nicht als Text-Listen. - Wenn der Nutzer eine konkrete Auswahl trifft müssen wir das über die passenden UI-Tools machen, bestätige kurz und gehe zum nächsten Thema. - Du darfst mehrere Tools gleichzeitig aufrufen wenn es sinnvoll ist. - Sei proaktiv: Wenn der Nutzer sagt "ich brauche eine Website für mein Restaurant", sag nicht nur "ok", sondern schlage direkt passende Seiten vor (Home, About, Speisekarte, Kontakt, Impressum) und zeige den Seiten-Selektor. ${availableOptions} AKTUELLER FORMSTATE (wird vom Frontend mitgeliefert): Wird in jeder Nachricht als JSON übergeben.`; export async function POST(req: NextRequest) { try { const { messages, formState, visitorId, honeypot } = await req.json(); // Validation if (!messages || !Array.isArray(messages) || messages.length === 0) { return NextResponse.json({ error: 'Valid messages array is required' }, { status: 400 }); } const latestMessage = messages[messages.length - 1].content; // Honeypot if (honeypot && honeypot.length > 0) { await new Promise((resolve) => setTimeout(resolve, 3000)); return NextResponse.json({ message: 'Vielen Dank für Ihre Anfrage.', toolCalls: [], }); } // Rate Limiting try { if (visitorId) { const requestCount = await redis.incr(`agent_chat_rate_limit:${visitorId}`); if (requestCount === 1) { await redis.expire(`agent_chat_rate_limit:${visitorId}`, RATE_LIMIT_DURATION); } if (requestCount > RATE_LIMIT_POINTS) { return NextResponse.json( { error: 'Rate limit exceeded. Please try again later.' }, { status: 429 }, ); } } } catch (redisError) { console.error('Redis Rate Limiting Error:', redisError); Sentry.captureException(redisError, { tags: { context: 'agent-chat-rate-limit' } }); } // Build messages for OpenRouter const systemMessage = { role: 'system', content: `${SYSTEM_PROMPT}\n\nAKTUELLER FORMSTATE:\n${JSON.stringify(formState || initialState, null, 2)}`, }; const openRouterKey = process.env.OPENROUTER_API_KEY; if (!openRouterKey) { throw new Error('OPENROUTER_API_KEY is not set'); } const fetchRes = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${openRouterKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': process.env.NEXT_PUBLIC_BASE_URL || 'https://mintel.me', 'X-Title': 'Mintel.me Project Agent', }, body: JSON.stringify({ model: 'mistralai/mistral-large-2407', temperature: 0.4, tools: TOOLS, tool_choice: 'auto', messages: [ systemMessage, ...messages.map((m: any) => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content), ...(m.tool_calls ? { tool_calls: m.tool_calls } : {}), ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}), })), ], }), }); if (!fetchRes.ok) { const errBody = await fetchRes.text(); throw new Error(`OpenRouter API Error: ${errBody}`); } const data = await fetchRes.json(); const choice = data.choices[0]; const responseMessage = choice.message; // Extract tool calls const toolCalls = responseMessage.tool_calls?.map((tc: any) => ({ id: tc.id, name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}'), })) || []; return NextResponse.json({ message: responseMessage.content || '', toolCalls, rawToolCalls: responseMessage.tool_calls || [], }); } catch (error) { console.error('Agent Chat API Error:', error); Sentry.captureException(error, { tags: { context: 'agent-chat-api' } }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }