feature/ai-search #2

Open
mmintel wants to merge 153 commits from feature/ai-search into main
Showing only changes of commit 5144378c7e - Show all commits

View File

@@ -3,6 +3,11 @@ import { searchProducts } from '../../../src/lib/qdrant';
import redis from '../../../src/lib/redis'; import redis from '../../../src/lib/redis';
import { z } from 'zod'; import { z } from 'zod';
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
// @ts-ignore
import { createMcpTools } from '@mintel/payload-ai/tools/mcpAdapter';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export const maxDuration = 60; // Max allowed duration (Vercel) export const maxDuration = 60; // Max allowed duration (Vercel)
@@ -108,12 +113,9 @@ Das ECHTE KLZ Team:
.map((p: any) => p.payload?.content) .map((p: any) => p.payload?.content)
.join('\n\n'); .join('\n\n');
const knowledgeDescriptions = searchResults if (productDescriptions) {
.filter((p) => p.payload?.type === 'knowledge') contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}`;
.map((p: any) => p.payload?.content) }
.join('\n\n');
contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}\n\nKABELWISSEN (Handbuch):\n${knowledgeDescriptions}`;
foundProducts = searchResults foundProducts = searchResults
.filter((p) => (p.payload?.type === 'product' || !p.payload?.type) && p.payload?.data) .filter((p) => (p.payload?.type === 'product' || !p.payload?.type) && p.payload?.data)
@@ -145,12 +147,14 @@ DEINE HAUPTAUFGABE: BERATEN, NICHT AUSFRAGEN!
- FRAGE NICHT nach abstrakten Dingen wie "Welchen Kabeltyp brauchst du?" -> DAS IST DEIN JOB, IHM DAS ZU SAGEN! - FRAGE NICHT nach abstrakten Dingen wie "Welchen Kabeltyp brauchst du?" -> DAS IST DEIN JOB, IHM DAS ZU SAGEN!
- FRAGE NICHT nach Längen oder genauen Trassen, es sei denn, der Kunde hat schon ganz klar gesagt, was er kaufen will. - FRAGE NICHT nach Längen oder genauen Trassen, es sei denn, der Kunde hat schon ganz klar gesagt, was er kaufen will.
- Biete aktiv Hilfe an: "Ich kann dir die passenden Querschnitte raussuchen, wenn du willst." - Biete aktiv Hilfe an: "Ich kann dir die passenden Querschnitte raussuchen, wenn du willst."
- Wenn technisches Wissen aus dem Kabelhandbuch benötigt wird, NUTZE UNBEDINGT eines der "kabelfachmann_*" Tools, anstatt zu raten oder zu behaupten du wüsstest es nicht! Das Tool weiss alles.
VORGEHEN: VORGEHEN:
1. Prüfe den KONTEXT auf passende Kabel für das Kundenprojekt. 1. Prüfe den KONTEXT auf passende Katalog-Kabel für das Kundenprojekt.
2. Nenne direkt 1-2 passende Produktserien aus dem Kontext, die für diesen Fall Sinn machen. 2. Wenn du tiefgehendes Wissen zu einem Kabeltyp brauchst (z.B. Biegeradius, Normen, Querschnitte), rufe das Kabelfachmann-Tool auf.
3. Biete eine konkrete Hilfestellung an (z.B. Leitungsberechnung, Verfügbarkeitsprüfung) ODER stelle EINE einzige fachliche Rückfrage, um das Kabel weiter einzugrenzen (z.B. Alu oder Kupfer?). 3. Nenne direkt 1-2 passende Produktserien aus dem Kontext oder der Tool-Abfrage, die für diesen Fall Sinn machen.
4. Wenn das Projekt klar ist und die Kabeltypen besprochen sind, frag nach, ob ein Kollege (z.B. Micha) ein konkretes Angebot machen soll. 4. Biete eine konkrete Hilfestellung an (z.B. Leitungsberechnung, Verfügbarkeitsprüfung) ODER stelle EINE einzige fachliche Rückfrage, um das Kabel weiter einzugrenzen (z.B. Alu oder Kupfer?).
5. Wenn das Projekt klar ist und die Kabeltypen besprochen sind, frag nach, ob ein Kollege (z.B. Micha) ein konkretes Angebot machen soll.
GRENZEN: GRENZEN:
- PRIVAT-ANFRAGEN: B2B only. Private Hausinstallationen lehnen wir freundlich ab. - PRIVAT-ANFRAGEN: B2B only. Private Hausinstallationen lehnen wir freundlich ab.
@@ -161,51 +165,42 @@ ${contextStr || 'Kein Katalogkontext verfügbar.'}
${teamContextStr} ${teamContextStr}
`; `;
const mistralKey = process.env.MISTRAL_API_KEY; const openrouterApiKey = process.env.OPENROUTER_API_KEY;
if (!mistralKey) { if (!openrouterApiKey) {
throw new Error('MISTRAL_API_KEY is not set'); throw new Error('OPENROUTER_API_KEY is not set');
} }
// DSGVO: Mistral AI API direkt (EU/Frankreich) statt OpenRouter (US) const openrouter = createOpenAI({
const fetchRes = await fetch('https://api.mistral.ai/v1/chat/completions', { baseURL: 'https://openrouter.ai/api/v1',
method: 'POST', apiKey: openrouterApiKey,
headers: {
Authorization: `Bearer ${mistralKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'ministral-8b-latest',
temperature: 0.3,
max_tokens: MAX_RESPONSE_TOKENS,
messages: [
{ role: 'system', content: systemPrompt },
...cappedMessages.map((m: any) => ({
role: m.role,
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
})),
],
}),
}); });
if (!fetchRes.ok) { let mcpTools: Record<string, any> = {};
const errBody = await fetchRes.text(); const mcpUrl = process.env.KABELFACHMANN_MCP_URL || 'http://host.docker.internal:3007/sse';
console.error('Mistral API Error:', errBody); try {
Sentry.captureException(new Error(`Mistral ${fetchRes.status}: ${errBody}`), { const { tools } = await createMcpTools({
tags: { context: 'ai-search-mistral' }, name: 'kabelfachmann',
url: mcpUrl
}); });
mcpTools = tools;
// Return user-friendly error based on status } catch (e) {
const userMsg = console.warn('Failed to load MCP tools', e);
fetchRes.status === 429 Sentry.captureException(e, { tags: { context: 'ai-search-mcp' } });
? 'Der KI-Service ist gerade überlastet. Bitte versuche es in ein paar Sekunden erneut.'
: fetchRes.status >= 500
? 'Der KI-Service ist vorübergehend nicht erreichbar. Bitte versuche es gleich nochmal.'
: 'Es gab ein Problem mit der KI-Anfrage. Bitte versuche es erneut.';
return NextResponse.json({ error: userMsg }, { status: 502 });
} }
const data = await fetchRes.json(); const { text } = await generateText({
const text = data.choices[0].message.content; model: openrouter('google/gemini-3.0-flash'),
system: systemPrompt,
messages: cappedMessages.map((m: any) => ({
role: m.role,
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
})),
tools: mcpTools,
// @ts-ignore
maxSteps: 3, // Allow the model to call the tool and then respond
temperature: 0.3,
maxTokens: MAX_RESPONSE_TOKENS,
});
// Return the AI's answer along with any found products // Return the AI's answer along with any found products
return NextResponse.json({ return NextResponse.json({