Files
breakpilot-core/pitch-deck/app/api/chat/route.ts
Benjamin Admin 978f0297eb feat(pitch): rewrite pitch content — Cloud SDK as core product
Restructure all pitch messaging: Cloud-based SDK platform with 65+ modules
is the CORE product. Mac Mini/Studio repositioned as side product for small
firms. Updated presenter scripts (20 slides), FAQ (35 entries), and chat
system prompt with new Kernbotschaften covering company compliance, Code/CE
scanning, EU AI hosting, Jira integration, and additional features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:10:33 +01:00

331 lines
14 KiB
TypeScript

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<string, { de: string; en: string }> = {
'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. Kern-Produkt: "Cloud-basierte SDK-Plattform mit 65+ Modulen. Kunden nutzen den SDK um KI DSGVO-konform einzusetzen und ihren CE-Risikobeurteilungsprozess zu optimieren."
2. Vollvernetzt: "Von der Kundenwebsite über die App in die Hardware-Elektronik und Standardprozesse. Erfasst alle Datenkategorien, Prozesse, Provider und Verarbeiter. Generiert automatisch AGB, DSE, Cookie Banner, Nutzungsbedingungen und alle Audit-Dokumente."
3. Code & CE: "SAST + DAST Scanning, vollständiger Pentest, CE Software-Risikobeurteilung (auch für Elektronik). Findings werden automatisch in Jira/Atlassian integriert mit exakten Codeänderungsvorschlägen, die die KI umsetzen kann."
4. EU-KI: "1000B LLM gehostet in Deutschland/Frankreich auf SysEleven (BSI-zertifiziert), OVH oder Hetzner. KEINE amerikanischen Anbieter. Isolierte Namespaces pro Kunde. Keinerlei Daten verlassen die Server."
5. Zielgruppen: "Maschinen- und Anlagenbauer, CE-Zertifizierer und ALLE produzierenden Firmen die Elektronik und Software entwickeln."
6. Zusatzfeatures: "Matrix-Chat + eigenes Jitsi (eigenes Teams), NVIDIA Meeting-Recorder schreibt Aufgaben direkt in Jira, ERPNext als Open-Source-ERP, Code-Assistent mit geschützten Namespaces."
7. Mac Mini/Studio: "Nebenprodukt für kleine Firmen (5-10 MA) — lokale, etwas langsamere Lösung mit vorinstalliertem RAG für Dokumentenverarbeitung und Q&A. Wir haben nur Wartungszugang und sehen die Daten nicht."
## 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", "BSI-zertifizierte EU-Cloud (SysEleven, OVH, Hetzner)", "Isolierte Kunden-Namespaces", "Enterprise-Grade Verschlüsselung".
## Erlaubt: Geschäftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (öffentlich), LLM-Größen (32b/40b/1000b), CE-Risikobeurteilung, Jira-Integration, Meeting-Recorder, Matrix/Jitsi.
## Team-Antworten (WICHTIG)
Wenn nach dem Team gefragt wird: IMMER die Namen, Rollen und Expertise der Gründer aus den bereitgestellten Daten nennen. NIEMALS vage Antworten wie "unser Team vereint Expertise" ohne Namen. Zitiere die konkreten Personen aus den Unternehmensdaten.
## 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<string> {
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<string, string> = {
'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 }
)
}
}