fix: routes
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user