import { NextResponse, NextRequest } from 'next/server'; // Added NextRequest import { searchProducts } from '../../../src/lib/qdrant'; import redis from '../../../src/lib/redis'; import { z } from 'zod'; import * as Sentry from '@sentry/nextjs'; // Config and constants const RATE_LIMIT_POINTS = 5; // 5 requests const RATE_LIMIT_DURATION = 60 * 1; // per 1 minute // Removed requestSchema as it's replaced by direct parsing export async function POST(req: NextRequest) { // Changed req type to NextRequest try { const { messages, visitorId, honeypot } = await req.json(); // 1. Basic 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; const isBot = honeypot && honeypot.length > 0; // Check if the input itself is obviously spam/too long if (latestMessage.length > 500) { return NextResponse.json({ error: 'Message too long' }, { status: 400 }); } // 2. Honeypot check if (isBot) { console.warn('Honeypot triggered in AI search'); // Tarpit the bot await new Promise((resolve) => setTimeout(resolve, 3000)); return NextResponse.json({ answerText: 'Vielen Dank für Ihre Anfrage.', products: [], }); } // 3. Rate Limiting via Redis try { if (visitorId) { const requestCount = await redis.incr(`ai_search_rate_limit:${visitorId}`); if (requestCount === 1) { await redis.expire(`ai_search_rate_limit:${visitorId}`, RATE_LIMIT_DURATION); // Use constant } if (requestCount > RATE_LIMIT_POINTS) { // Use constant return NextResponse.json( { error: 'Rate limit exceeded. Please try again later.', }, { status: 429 }, ); } } } catch (redisError) { // Renamed variable for clarity console.error('Redis Rate Limiting Error:', redisError); // Changed to error for consistency Sentry.captureException(redisError, { tags: { context: 'ai-search-rate-limit' } }); // Fail open if Redis is down } // 4. Fetch Context from Qdrant based on the latest message let contextStr = ''; let foundProducts: any[] = []; try { const searchResults = await searchProducts(latestMessage, 5); if (searchResults && searchResults.length > 0) { const productDescriptions = searchResults .filter((p) => p.payload?.type === 'product' || !p.payload?.type) .map((p: any) => p.payload?.content) .join('\n\n'); const knowledgeDescriptions = searchResults .filter((p) => p.payload?.type === 'knowledge') .map((p: any) => p.payload?.content) .join('\n\n'); contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}\n\nKABELWISSEN (Handbuch):\n${knowledgeDescriptions}`; foundProducts = searchResults .filter((p) => (p.payload?.type === 'product' || !p.payload?.type) && p.payload?.data) .map((p: any) => p.payload?.data); } } catch (e) { console.error('Qdrant Search Error:', e); Sentry.captureException(e, { tags: { context: 'ai-search-qdrant' } }); // We can still proceed without context if Qdrant fails } // 5. Generate AI Response via OpenRouter (Mistral for DSGVO) const systemPrompt = `Du bist ein professioneller und extrem kompetenter Sales-Engineer / Consultant der Firma "KLZ Cables". Deine Aufgabe ist es, Kunden und Interessenten bei der Auswahl von Mittelspannungskabeln, Starkstromkabeln und Infrastrukturausrüstung beratend zur Seite zu stehen. WICHTIGE REGELN: 1. ANTWORTE IMMER IN DER SPRACHE DES BENUTZERS. Wenn der Benutzer Deutsch spricht, antworte auf Deutsch. 2. Wenn der Kunde vage ist (z.B. "Ich will einen Windpark bauen"), würge ihn NICHT ab. Stelle stattdessen gezielte, professionelle Rückfragen als Berater (z.B. "Für einen Windpark benötigen wir einige Rahmendaten: Reden wir über die Parkverkabelung (Mittelspannung, z.B. 20kV oder 33kV) oder die Netzanbindung? Welche Querschnitte oder Ströme erwarten Sie?"). 3. Nutze das bereitgestellte KABELWISSEN und KATALOG-Gedächtnis unten, um deine Antworten zu fundieren. 4. Bleibe stets professionell, lösungsorientiert und leicht technisch (Industrial Aesthetic). Du kannst humorvoll sein, wenn der Nutzer offensichtlich Quatsch fragt, aber lenke es immer elegant zurück zu Kabeln oder Energieinfrastruktur. 5. Antworte in reinem Text (kein Markdown für die Antwort, es sei denn es sind einfache Absätze oder Listen). 6. Wenn genügend Informationen vorhanden sind, präsentiere passende Kabel aus dem Katalog. 7. Oute dich als Berater von KLZ Cables. VERFÜGBARER KONTEXT: ${contextStr ? contextStr : 'Keine spezifischen Katalogdaten für diese Anfrage gefunden.'} `; 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://klz-cables.com', 'X-Title': 'KLZ Cables Search AI', }, body: JSON.stringify({ model: 'mistralai/mistral-large-2407', temperature: 0.3, messages: [ { role: 'system', content: systemPrompt }, ...messages.map((m: any) => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content), })), ], }), }); if (!fetchRes.ok) { const errBody = await fetchRes.text(); throw new Error(`OpenRouter API Error: ${errBody}`); } const data = await fetchRes.json(); const text = data.choices[0].message.content; // Return the AI's answer along with any found products return NextResponse.json({ answerText: text, products: foundProducts, }); } catch (error) { console.error('AI Search API Error:', error); Sentry.captureException(error, { tags: { context: 'ai-search-api' } }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }