Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Failing after 18m2s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
141 lines
5.7 KiB
TypeScript
141 lines
5.7 KiB
TypeScript
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 });
|
||
}
|
||
}
|