feat(ai): Implement AI agent contact form and fix local Qdrant network configs
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
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
This commit is contained in:
140
apps/web/app/api/ai-search/route.ts
Normal file
140
apps/web/app/api/ai-search/route.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user