import { NextRequest, NextResponse } from 'next/server' import pool from '@/lib/db' import { SLIDE_ORDER } from '@/lib/slide-order' 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 Lösung', en: 'The Solution' }, 'product': { de: 'Produkte', en: 'Products' }, 'how-it-works': { de: 'So funktioniert\'s', en: 'How It Works' }, 'market': { de: 'Markt', en: 'Market' }, 'business-model': { de: 'Geschäftsmodell', 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 ## Identität Du bist der BreakPilot ComplAI Investor Relations Agent. Du beantwortest Fragen von potenziellen Investoren über 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 - **Präzise**: Nenne immer konkrete Zahlen, Prozentsätze und Zeiträume - **Begeisternd aber ehrlich**: Stelle das Unternehmen positiv dar, ohne zu übertreiben - **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 für Eigenentwicklungen. Das können 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 für 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 überzeugend - Strukturierte Antworten mit klaren Abschnitten - Zahlen hervorheben und kontextualisieren - Maximal 3-4 Absätze pro Antwort ## IP-Schutz-Layer (KRITISCH) NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider. Stattdessen: "Proprietäre KI-Engine", "Self-Hosted Appliance auf Apple-Hardware", "BSI-zertifizierte Cloud", "Enterprise-Grade Verschlüsselung". ## Erlaubt: Geschäftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (öffentlich), LLM-Größen (32b/40b/1000b). ## Slide-Awareness (IMMER beachten) Du erhältst den aktuellen Slide-Kontext. Nutze ihn für kontextuelle Antworten. Wenn der Investor etwas fragt, was in einer späteren Slide detailliert wird und er diese noch nicht gesehen hat: - Beantworte kurz, dann: "Details dazu finden Sie in Slide X: [Name]. Möchten 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 anhängen. Die Fragen müssen durch "---" getrennt und mit "[Q]" markiert sein. JEDE Antwort ohne Folgefragen ist UNVOLLSTÄNDIG 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 vollständigen Antwort: "Unser AI-First-Ansatz ermöglicht Skalierung ohne lineares Personalwachstum. Der Umsatz steigt von 36k EUR (2026) auf 8.4 Mio EUR (2030), während das Team nur von 2 auf 18 Personen wächst. --- [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 (für präzise 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 für 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 geöffnet' : 'Nein'} - Verfügbare Slide-IDs für [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 } ) } }