From 5144378c7e1337454f40f5282d67b91955bf6155 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 10 Mar 2026 11:34:01 +0100 Subject: [PATCH] fix: routes --- app/api/ai-search/route.ts | 91 ++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/app/api/ai-search/route.ts b/app/api/ai-search/route.ts index 6158e820..b41cc555 100644 --- a/app/api/ai-search/route.ts +++ b/app/api/ai-search/route.ts @@ -3,6 +3,11 @@ import { searchProducts } from '../../../src/lib/qdrant'; import redis from '../../../src/lib/redis'; import { z } from 'zod'; 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 maxDuration = 60; // Max allowed duration (Vercel) @@ -108,12 +113,9 @@ Das ECHTE KLZ Team: .map((p: any) => p.payload?.content) .join('\n\n'); - const knowledgeDescriptions = searchResults - .filter((p) => p.payload?.type === 'knowledge') - .map((p: any) => p.payload?.content) - .join('\n\n'); - - contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}\n\nKABELWISSEN (Handbuch):\n${knowledgeDescriptions}`; + if (productDescriptions) { + contextStr = `KATALOG & PRODUKTE:\n${productDescriptions}`; + } foundProducts = searchResults .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 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." +- 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: -1. Prüfe den KONTEXT auf passende Kabel für das Kundenprojekt. -2. Nenne direkt 1-2 passende Produktserien aus dem Kontext, die für diesen Fall Sinn machen. -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?). -4. Wenn das Projekt klar ist und die Kabeltypen besprochen sind, frag nach, ob ein Kollege (z.B. Micha) ein konkretes Angebot machen soll. +1. Prüfe den KONTEXT auf passende Katalog-Kabel für das Kundenprojekt. +2. Wenn du tiefgehendes Wissen zu einem Kabeltyp brauchst (z.B. Biegeradius, Normen, Querschnitte), rufe das Kabelfachmann-Tool auf. +3. Nenne direkt 1-2 passende Produktserien aus dem Kontext oder der Tool-Abfrage, die für diesen Fall Sinn machen. +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: - PRIVAT-ANFRAGEN: B2B only. Private Hausinstallationen lehnen wir freundlich ab. @@ -161,51 +165,42 @@ ${contextStr || 'Kein Katalogkontext verfügbar.'} ${teamContextStr} `; - const mistralKey = process.env.MISTRAL_API_KEY; - if (!mistralKey) { - throw new Error('MISTRAL_API_KEY is not set'); + const openrouterApiKey = process.env.OPENROUTER_API_KEY; + if (!openrouterApiKey) { + throw new Error('OPENROUTER_API_KEY is not set'); } - // DSGVO: Mistral AI API direkt (EU/Frankreich) statt OpenRouter (US) - const fetchRes = await fetch('https://api.mistral.ai/v1/chat/completions', { - method: 'POST', - 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), - })), - ], - }), + const openrouter = createOpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: openrouterApiKey, }); - if (!fetchRes.ok) { - const errBody = await fetchRes.text(); - console.error('Mistral API Error:', errBody); - Sentry.captureException(new Error(`Mistral ${fetchRes.status}: ${errBody}`), { - tags: { context: 'ai-search-mistral' }, + let mcpTools: Record = {}; + const mcpUrl = process.env.KABELFACHMANN_MCP_URL || 'http://host.docker.internal:3007/sse'; + try { + const { tools } = await createMcpTools({ + name: 'kabelfachmann', + url: mcpUrl }); - - // Return user-friendly error based on status - const userMsg = - fetchRes.status === 429 - ? '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 }); + mcpTools = tools; + } catch (e) { + console.warn('Failed to load MCP tools', e); + Sentry.captureException(e, { tags: { context: 'ai-search-mcp' } }); } - const data = await fetchRes.json(); - const text = data.choices[0].message.content; + const { text } = await generateText({ + 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 NextResponse.json({