import { NextResponse, NextRequest } from 'next/server'; import { searchPosts } from '../../../src/lib/qdrant'; import redis from '../../../src/lib/redis'; import * as Sentry from '@sentry/nextjs'; // Rate limiting constants const RATE_LIMIT_POINTS = 5; // 5 requests const RATE_LIMIT_DURATION = 60; // per 1 minute export async function POST(req: 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; 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'); await new Promise((resolve) => setTimeout(resolve, 3000)); return NextResponse.json({ answerText: 'Vielen Dank für Ihre Anfrage.', posts: [], }); } // 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); } 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: 'ai-search-rate-limit' } }); // Fail open if Redis is down } // 4. Fetch Context from Qdrant let contextStr = ''; let foundPosts: any[] = []; try { const searchResults = await searchPosts(latestMessage, 5); if (searchResults && searchResults.length > 0) { const postDescriptions = searchResults .map((p: any) => p.payload?.content) .join('\n\n'); contextStr = `BLOG-POSTS & WISSEN:\n${postDescriptions}`; foundPosts = searchResults .filter((p: any) => 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' } }); } // 5. Generate AI Response via OpenRouter (Mistral) const systemPrompt = `Du bist ein professioneller technischer Berater der Agentur "Mintel" – einer Full-Stack Digitalagentur spezialisiert auf Next.js, Payload CMS und moderne Web-Infrastruktur. Deine Aufgabe ist es, Besuchern bei technischen Fragen zu helfen, basierend auf den Blog-Artikeln und dem Fachwissen der Agentur. WICHTIGE REGELN: 1. ANTWORTE IMMER IN DER SPRACHE DES BENUTZERS. Wenn der Benutzer Deutsch spricht, antworte auf Deutsch. Bei Englisch, antworte auf Englisch. 2. Nutze das bereitgestellte BLOG-WISSEN unten, um deine Antworten zu fundieren. Verweise auf relevante Blog-Posts. 3. Sei hilfreich, präzise und technisch versiert. Du kannst Code-Beispiele geben wenn sinnvoll. 4. Wenn du keine passende Information findest, gib das offen zu und schlage vor, über das Kontaktformular direkt Kontakt aufzunehmen. 5. Antworte in Markdown-Format (Überschriften, Listen, Code-Blöcke sind erlaubt). 6. Halte Antworten kompakt aber informativ – maximal 3-4 Absätze. 7. Oute dich als AI-Assistent von Mintel. VERFÜGBARER KONTEXT: ${contextStr ? contextStr : 'Keine spezifischen Blog-Daten 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://mintel.me', 'X-Title': 'Mintel.me AI Search', }, 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 NextResponse.json({ answerText: text, posts: foundPosts, }); } 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 }); } }