import { NextRequest, NextResponse } from 'next/server' import pool from '@/lib/db' import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation' const LITELLM_URL = process.env.LITELLM_URL || 'https://llm-dev.meghsakha.com' const LITELLM_MODEL = process.env.LITELLM_MODEL || 'gpt-oss-120b' const LITELLM_API_KEY = process.env.LITELLM_API_KEY || '' // Build SLIDE_NAMES dynamically from SLIDE_ORDER const SLIDE_DISPLAY_NAMES: Record = { 'intro-presenter': { de: 'Intro', en: 'Intro' }, 'cover': { de: 'Cover', en: 'Cover' }, 'problem': { de: 'Das Problem', en: 'The Problem' }, 'solution': { de: 'Die Loesung', en: 'The Solution' }, 'product': { de: 'Produkte', en: 'Products' }, 'how-it-works': { de: 'So funktionierts', en: 'How It Works' }, 'market': { de: 'Markt', en: 'Market' }, 'business-model': { de: 'Geschaeftsmodell', en: 'Business Model' }, 'traction': { de: 'Traction', en: 'Traction' }, 'competition': { de: 'Wettbewerb', en: 'Competition' }, 'team': { de: 'Team', en: 'Team' }, 'financials': { de: 'Finanzen', en: 'Financials' }, 'the-ask': { de: 'The Ask', en: 'The Ask' }, 'ai-qa': { de: 'KI Q&A', en: 'AI Q&A' }, 'annex-assumptions': { de: 'Anhang: Annahmen', en: 'Appendix: Assumptions' }, 'annex-architecture': { de: 'Anhang: Architektur', en: 'Appendix: Architecture' }, 'annex-gtm': { de: 'Anhang: Go-to-Market', en: 'Appendix: Go-to-Market' }, 'annex-regulatory': { de: 'Anhang: Regulatorik', en: 'Appendix: Regulatory' }, 'annex-engineering': { de: 'Anhang: Engineering', en: 'Appendix: Engineering' }, 'annex-aipipeline': { de: 'Anhang: KI-Pipeline', en: 'Appendix: AI Pipeline' }, } const slideCount = SLIDE_ORDER.length const SYSTEM_PROMPT = `# Investor Agent — BreakPilot ComplAI ## Identitaet Du bist der BreakPilot ComplAI Investor Relations Agent. Du beantwortest Fragen von potenziellen Investoren ueber das Unternehmen, das Produkt, den Markt und die Finanzprognosen. Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen. ## Kernprinzipien - **Datengetrieben**: Beziehe dich immer auf die bereitgestellten Unternehmensdaten - **Praezise**: Nenne immer konkrete Zahlen, Prozentsaetze und Zeitraeume - **Begeisternd aber ehrlich**: Stelle das Unternehmen positiv dar, ohne zu uebertreiben - **Zweisprachig**: Antworte in der Sprache, in der die Frage gestellt wird ## Kernbotschaften (IMMER betonen wenn passend) 1. Zielmarkt: "Maschinen- und Anlagenbauer (VDMA ~3.600 Mitglieder in DE, ~5.000 DACH) die eigene Software/Firmware entwickeln." 2. USP: "Nicht nur organisatorische Compliance, sondern auch Code-Security und Risikoanalyse fuer Eigenentwicklungen. Das koennen Proliance, DataGuard und heyData NICHT." 3. Produkt-Architektur: "Mac Mini/Studio lokal im Serverraum macht die Vorarbeit (Scanning, Analyse). Das BSI-zertifizierte 1000B Cloud-LLM in Deutschland implementiert Fixes und ist fuer alle Mitarbeiter nutzbar." 4. Regulatorik: "Cyber Resilience Act (CRA) verpflichtet Hersteller, Software in Produkten abzusichern — unser Kern-Use-Case. Plus DSGVO, AI Act und NIS2." 5. Skalierbarkeit: "AI-First — 10x Kunden ≠ 10x Personal. 380 Kunden in 2030 bei 5.5 Mio EUR Umsatz." 6. Marktchance: "8.7 Mrd EUR TAM, SOM 7.2 Mio EUR (500 DACH-Maschinenbauer x 14.400 EUR/Jahr)." ## Kommunikationsstil - Professionell, knapp und ueberzeugend - Strukturierte Antworten mit klaren Abschnitten - Zahlen hervorheben und kontextualisieren - Maximal 3-4 Absaetze pro Antwort ## IP-Schutz-Layer (KRITISCH) NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider. Stattdessen: "Proprietaere KI-Engine", "Self-Hosted Appliance auf Apple-Hardware", "BSI-zertifizierte Cloud", "Enterprise-Grade Verschluesselung". ## Erlaubt: Geschaeftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (oeffentlich), LLM-Groessen (32b/40b/1000b). ## Slide-Awareness (IMMER beachten) Du erhaeltst den aktuellen Slide-Kontext. Nutze ihn fuer kontextuelle Antworten. Wenn der Investor etwas fragt, was in einer spaeteren Slide detailliert wird und er diese noch nicht gesehen hat: - Beantworte kurz, dann: "Details dazu finden Sie in Slide X: [Name]. Moechten Sie dorthin springen? [GOTO:slide-id]" - Verwende [GOTO:slide-id] mit der Slide-ID (z.B. [GOTO:financials], [GOTO:competition]) ## FOLLOW-UP FRAGEN — KRITISCHE PFLICHT Du MUSST am Ende JEDER einzelnen Antwort exakt 3 Folgefragen anhaengen. Die Fragen muessen durch "---" getrennt und mit "[Q]" markiert sein. JEDE Antwort ohne Folgefragen ist UNVOLLSTAENDIG und FEHLERHAFT. EXAKTES FORMAT (keine Abweichung erlaubt): [Deine Antwort hier] --- [Q] Erste Folgefrage passend zum Thema? [Q] Zweite Folgefrage die tiefer geht? [Q] Dritte Folgefrage zu einem verwandten Aspekt? KONKRETES BEISPIEL einer vollstaendigen Antwort: "Unser AI-First-Ansatz ermoeglicht Skalierung ohne lineares Personalwachstum. Der Umsatz steigt von 36k EUR (2026) auf 8.4 Mio EUR (2030), waehrend das Team nur von 2 auf 18 Personen waechst. --- [Q] Wie sieht die Kostenstruktur im Detail aus? [Q] Welche Unit Economics erreicht ihr in 2030? [Q] Wie vergleicht sich die Personaleffizienz mit Wettbewerbern?" WICHTIG: Vergiss NIEMALS die Folgefragen! Sie sind PFLICHT.` async function loadPitchContext(): Promise { try { const client = await pool.connect() try { const [company, team, financials, market, products, funding, features] = await Promise.all([ client.query('SELECT * FROM pitch_company LIMIT 1'), client.query('SELECT name, role_de, equity_pct, expertise FROM pitch_team ORDER BY sort_order'), client.query('SELECT year, revenue_eur, costs_eur, mrr_eur, customers_count, employees_count, arr_eur FROM pitch_financials ORDER BY year'), client.query('SELECT market_segment, value_eur, growth_rate_pct, source FROM pitch_market'), client.query('SELECT name, hardware, hardware_cost_eur, monthly_price_eur, llm_size, llm_capability_de, operating_cost_eur FROM pitch_products ORDER BY sort_order'), client.query('SELECT round_name, amount_eur, use_of_funds, instrument FROM pitch_funding LIMIT 1'), client.query('SELECT feature_name_de, breakpilot, proliance, dataguard, heydata, is_differentiator FROM pitch_features WHERE is_differentiator = true'), ]) return ` ## Unternehmensdaten (fuer praezise Antworten nutzen) ### Firma ${JSON.stringify(company.rows[0], null, 2)} ### Team ${JSON.stringify(team.rows, null, 2)} ### Finanzprognosen (5-Jahres-Plan) ${JSON.stringify(financials.rows, null, 2)} ### Markt (TAM/SAM/SOM) ${JSON.stringify(market.rows, null, 2)} ### Produkte ${JSON.stringify(products.rows, null, 2)} ### Finanzierung ${JSON.stringify(funding.rows[0], null, 2)} ### Differenzierende Features (nur bei ComplAI) ${JSON.stringify(features.rows, null, 2)} ` } finally { client.release() } } catch (error) { console.warn('Could not load pitch context from DB:', error) return '' } } export async function POST(request: NextRequest) { try { const body = await request.json() const { message, history = [], lang = 'de', slideContext, faqAnswer } = body if (!message || typeof message !== 'string') { return NextResponse.json({ error: 'Message is required' }, { status: 400 }) } // FAQ shortcut: if client sends a pre-cached FAQ answer, stream it directly (no LLM call) if (faqAnswer && typeof faqAnswer === 'string') { const encoder = new TextEncoder() const stream = new ReadableStream({ start(controller) { // Stream the FAQ answer in chunks for consistent UX const words = faqAnswer.split(' ') let i = 0 const interval = setInterval(() => { if (i < words.length) { const chunk = (i === 0 ? '' : ' ') + words[i] controller.enqueue(encoder.encode(chunk)) i++ } else { clearInterval(interval) controller.close() } }, 30) }, }) return new NextResponse(stream, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }) } const pitchContext = await loadPitchContext() let systemContent = SYSTEM_PROMPT if (pitchContext) { systemContent += '\n' + pitchContext } // Slide context for contextual awareness if (slideContext) { const visited: number[] = slideContext.visitedSlides || [] const currentSlideId = slideContext.currentSlide const currentSlideName = SLIDE_DISPLAY_NAMES[currentSlideId]?.[lang] || currentSlideId const notYetSeen = SLIDE_ORDER .map((id, idx) => ({ id, idx, name: SLIDE_DISPLAY_NAMES[id]?.[lang] || id })) .filter(s => !visited.includes(s.idx)) .map(s => `${s.idx + 1}. ${s.name}`) systemContent += `\n\n## Slide-Kontext (WICHTIG fuer kontextuelle Antworten) - Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von ${slideCount}) - Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_DISPLAY_NAMES[SLIDE_ORDER[i]]?.[lang] || SLIDE_ORDER[i]).filter(Boolean).join(', ')} - Noch nicht gesehene Slides: ${notYetSeen.join(', ')} - Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Investor hat gerade erst den Pitch geoeffnet' : 'Nein'} - Verfuegbare Slide-IDs fuer [GOTO:id]: ${SLIDE_ORDER.join(', ')} ` } systemContent += `\n\n## Aktuelle Sprache: ${lang === 'de' ? 'Deutsch' : 'English'}\nAntworte in ${lang === 'de' ? 'Deutsch' : 'English'}.` const messages = [ { role: 'system', content: systemContent }, ...history.slice(-10).map((h: { role: string; content: string }) => ({ role: h.role === 'user' ? 'user' : 'assistant', content: h.content, })), { role: 'user', content: message + '\n\n(Erinnerung: Beende deine Antwort IMMER mit "---" gefolgt von 3 Folgefragen im Format "[Q] Frage?")' }, ] // LiteLLM (OpenAI-compatible API) const headers: Record = { 'Content-Type': 'application/json', } if (LITELLM_API_KEY) { headers['Authorization'] = `Bearer ${LITELLM_API_KEY}` } const llmResponse = await fetch(`${LITELLM_URL}/v1/chat/completions`, { method: 'POST', headers, body: JSON.stringify({ model: LITELLM_MODEL, messages, stream: true, temperature: 0.4, max_tokens: 4096, }), signal: AbortSignal.timeout(120000), }) if (!llmResponse.ok) { const errorText = await llmResponse.text() console.error('LiteLLM error:', llmResponse.status, errorText) return NextResponse.json( { error: `LLM nicht erreichbar (Status ${llmResponse.status}).` }, { status: 502 } ) } // Parse SSE stream from LiteLLM and emit plain text to client const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { const reader = llmResponse.body!.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') // Keep the last (potentially incomplete) line in the buffer buffer = lines.pop() || '' for (const line of lines) { const trimmed = line.trim() if (!trimmed || !trimmed.startsWith('data: ')) continue const data = trimmed.slice(6) if (data === '[DONE]') continue try { const json = JSON.parse(data) const content = json.choices?.[0]?.delta?.content if (content) { controller.enqueue(encoder.encode(content)) } } catch { // Partial JSON, skip } } } // Process any remaining buffer if (buffer.trim()) { const trimmed = buffer.trim() if (trimmed.startsWith('data: ') && trimmed.slice(6) !== '[DONE]') { try { const json = JSON.parse(trimmed.slice(6)) const content = json.choices?.[0]?.delta?.content if (content) { controller.enqueue(encoder.encode(content)) } } catch { // Ignore } } } } catch (error) { console.error('Stream read error:', error) } finally { controller.close() } }, }) return new NextResponse(stream, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }) } catch (error) { console.error('Investor agent chat error:', error) return NextResponse.json( { error: 'Verbindung zum LLM fehlgeschlagen.' }, { status: 503 } ) } }