Files
mintel.me/apps/web/app/api/ai-search/route.ts
Marc Mintel 85d2d2c069
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
feat(ai): Implement AI agent contact form and fix local Qdrant network configs
2026-03-06 11:56:12 +01:00

141 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
}
}