fix(pitch-deck): eliminate SYSTEM_PROMPT placeholder leak and fix liquidity tax ordering
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m43s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 1m2s
CI / test-python-voice (push) Successful in 45s
CI / test-bqas (push) Successful in 41s
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m43s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 1m2s
CI / test-python-voice (push) Successful in 45s
CI / test-bqas (push) Successful in 41s
C3: Split SYSTEM_PROMPT into PART1/PART2/PART3 constants; Kernbotschaft #9 and VERSIONS-ISOLATION now concatenated directly at runtime instead of .replace() — a whitespace mismatch can no longer cause placeholder text to leak verbatim to the LLM. I2: Add second liquidity-chain pass (sumAus→ÜBERSCHUSS→rolling balance) after tax rows (Gewerbesteuer/Körperschaftsteuer) are written to fp_liquiditaet, so first-run LIQUIDITÄT figures include tax outflows without requiring a second engine invocation. I6: Warn when loadFpLiquiditaetSummary finds no fp_liquiditaet rows for a named scenario, surfacing scenario-name mismatches that would otherwise silently return empty context. I8: Sanitize console.error calls in chat/route.ts (3 sites) and data/route.ts; cap LiteLLM error body to 200 chars, use (error as Error).message for stream/handler errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,8 @@ const SLIDE_DISPLAY_NAMES: Record<string, { de: string; en: string }> = {
|
|||||||
|
|
||||||
const slideCount = SLIDE_ORDER.length
|
const slideCount = SLIDE_ORDER.length
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `# Investor Agent — BreakPilot ComplAI
|
// Static prefix: Identität through Kernbotschaft #8 — #9 and VERSIONS-ISOLATION injected at runtime
|
||||||
|
const SYSTEM_PROMPT_PART1 = `# Investor Agent — BreakPilot ComplAI
|
||||||
|
|
||||||
## Identität
|
## Identität
|
||||||
Du bist der BreakPilot ComplAI Investor Relations Agent. Du beantwortest Fragen von
|
Du bist der BreakPilot ComplAI Investor Relations Agent. Du beantwortest Fragen von
|
||||||
@@ -56,8 +57,10 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen.
|
|||||||
5. EU-Infrastruktur: "BSI-zertifizierte Cloud in Deutschland oder Frankreich. 100% Datensouveränität. KEINE US-Anbieter. Isolierte Namespaces."
|
5. EU-Infrastruktur: "BSI-zertifizierte Cloud in Deutschland oder Frankreich. 100% Datensouveränität. KEINE US-Anbieter. Isolierte Namespaces."
|
||||||
6. Zielgruppen: "Maschinen- und Anlagenbauer, Automobilindustrie, Zulieferer und alle produzierenden Unternehmen."
|
6. Zielgruppen: "Maschinen- und Anlagenbauer, Automobilindustrie, Zulieferer und alle produzierenden Unternehmen."
|
||||||
7. Geschäftsmodell: "SaaS, mitarbeiterbasiertes Pricing. Drei Tiers: Starter (3.600 EUR/Jahr), Professional (15.000-40.000 EUR/Jahr), Enterprise (ab 50.000 EUR/Jahr). Plus Beratung & Service (10.000-30.000 EUR/Monat). Kunden sparen mehr als sie zahlen — ROI ab Tag 1."
|
7. Geschäftsmodell: "SaaS, mitarbeiterbasiertes Pricing. Drei Tiers: Starter (3.600 EUR/Jahr), Professional (15.000-40.000 EUR/Jahr), Enterprise (ab 50.000 EUR/Jahr). Plus Beratung & Service (10.000-30.000 EUR/Monat). Kunden sparen mehr als sie zahlen — ROI ab Tag 1."
|
||||||
8. Team: "Lean-Team: 2 Gründer + 7 Mitarbeiter bis 2030 (9 Personen gesamt). Erste Einstellung: IT-Recht/Datenschutzjurist (50%). Dann: Security Engineer, Vertrieb, Backend, Kundenbetreuer, Marketing, DevOps. Jede Einstellung an konkreten Umsatzmeilenstein gekoppelt."
|
8. Team: "Lean-Team: 2 Gründer + 7 Mitarbeiter bis 2030 (9 Personen gesamt). Erste Einstellung: IT-Recht/Datenschutzjurist (50%). Dann: Security Engineer, Vertrieb, Backend, Kundenbetreuer, Marketing, DevOps. Jede Einstellung an konkreten Umsatzmeilenstein gekoppelt."`
|
||||||
9. Finanzplan: [SIEHE DYNAMISCHE VERSIONSDATEN — wird zur Laufzeit gesetzt]
|
|
||||||
|
// Static middle: Kommunikationsstil — injected between #9 and VERSIONS-ISOLATION
|
||||||
|
const SYSTEM_PROMPT_PART2 = `
|
||||||
|
|
||||||
## Kommunikationsstil
|
## Kommunikationsstil
|
||||||
- Antworte IMMER wie ein Mensch in einem persönlichen Gespräch — ausformulierte Sätze, natürlicher Redefluss
|
- Antworte IMMER wie ein Mensch in einem persönlichen Gespräch — ausformulierte Sätze, natürlicher Redefluss
|
||||||
@@ -66,10 +69,10 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen.
|
|||||||
- Nutze Übergangssätze wie "Der Grund dafür ist...", "Das haben wir bewusst so entschieden, weil...", "Besonders wichtig ist dabei..."
|
- Nutze Übergangssätze wie "Der Grund dafür ist...", "Das haben wir bewusst so entschieden, weil...", "Besonders wichtig ist dabei..."
|
||||||
- Zahlen und Fakten natürlich in den Text einbetten, nicht als Liste aufreihen
|
- Zahlen und Fakten natürlich in den Text einbetten, nicht als Liste aufreihen
|
||||||
- 3-5 Absätze pro Antwort, jeder Absatz ein eigenständiger Gedanke
|
- 3-5 Absätze pro Antwort, jeder Absatz ein eigenständiger Gedanke
|
||||||
- Der Text muss sich gut anhören wenn er vorgelesen wird (TTS-optimiert)
|
- Der Text muss sich gut anhören wenn er vorgelesen wird (TTS-optimiert)`
|
||||||
|
|
||||||
## VERSIONS-ISOLATION (ABSOLUT KRITISCH)
|
// Static suffix: everything after VERSIONS-ISOLATION
|
||||||
[WIRD ZUR LAUFZEIT GESETZT — enthält die exakten Zahlen dieser Investor-Version]
|
const SYSTEM_PROMPT_PART3 = `
|
||||||
|
|
||||||
## IP-Schutz-Layer (KRITISCH)
|
## IP-Schutz-Layer (KRITISCH)
|
||||||
NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider.
|
NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider.
|
||||||
@@ -123,7 +126,10 @@ async function loadFpLiquiditaetSummary(scenarioName: string): Promise<string> {
|
|||||||
ORDER BY l.sort_order`,
|
ORDER BY l.sort_order`,
|
||||||
[scenarioName]
|
[scenarioName]
|
||||||
)
|
)
|
||||||
if (rows.length === 0) return ''
|
if (rows.length === 0) {
|
||||||
|
if (scenarioName) console.warn('[chat] loadFpLiquiditaetSummary: no rows for scenario', JSON.stringify(scenarioName))
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const years = [2026, 2027, 2028, 2029, 2030]
|
const years = [2026, 2027, 2028, 2029, 2030]
|
||||||
const summary: Record<string, Record<number, number>> = {}
|
const summary: Record<string, Record<number, number>> = {}
|
||||||
@@ -333,9 +339,11 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const dynamicFinanzplanKernbotschaft = `9. Finanzplan: "Gründung August 2026. Pre-Seed über ${fundingStr}. ${customersStr} und ${revM} Umsatz bis 2030. ${employeesStr}."`
|
const dynamicFinanzplanKernbotschaft = `9. Finanzplan: "Gründung August 2026. Pre-Seed über ${fundingStr}. ${customersStr} und ${revM} Umsatz bis 2030. ${employeesStr}."`
|
||||||
|
|
||||||
let systemContent = SYSTEM_PROMPT
|
let systemContent = SYSTEM_PROMPT_PART1
|
||||||
.replace('9. Finanzplan: [SIEHE DYNAMISCHE VERSIONSDATEN — wird zur Laufzeit gesetzt]', dynamicFinanzplanKernbotschaft)
|
+ '\n' + dynamicFinanzplanKernbotschaft
|
||||||
.replace('## VERSIONS-ISOLATION (ABSOLUT KRITISCH)\n[WIRD ZUR LAUFZEIT GESETZT — enthält die exakten Zahlen dieser Investor-Version]', dynamicVersionIsolation)
|
+ SYSTEM_PROMPT_PART2
|
||||||
|
+ '\n\n' + dynamicVersionIsolation
|
||||||
|
+ SYSTEM_PROMPT_PART3
|
||||||
|
|
||||||
if (contextString) {
|
if (contextString) {
|
||||||
systemContent += '\n' + contextString
|
systemContent += '\n' + contextString
|
||||||
@@ -402,7 +410,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
if (!llmResponse.ok) {
|
if (!llmResponse.ok) {
|
||||||
const errorText = await llmResponse.text()
|
const errorText = await llmResponse.text()
|
||||||
console.error('LiteLLM error:', llmResponse.status, errorText)
|
console.error('LiteLLM error:', llmResponse.status, errorText.slice(0, 200))
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `LLM nicht erreichbar (Status ${llmResponse.status}).` },
|
{ error: `LLM nicht erreichbar (Status ${llmResponse.status}).` },
|
||||||
{ status: 502 }
|
{ status: 502 }
|
||||||
@@ -461,7 +469,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Stream read error:', error)
|
console.error('Stream read error:', (error as Error).message)
|
||||||
} finally {
|
} finally {
|
||||||
controller.close()
|
controller.close()
|
||||||
}
|
}
|
||||||
@@ -476,7 +484,7 @@ export async function POST(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Investor agent chat error:', error)
|
console.error('Investor agent chat error:', (error as Error).message)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
|
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
|
||||||
{ status: 503 }
|
{ status: 503 }
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export async function GET() {
|
|||||||
client.release()
|
client.release()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database query error:', error)
|
console.error('Database query error:', (error as Error).message)
|
||||||
// Return minimal stub in dev so the pitch renders without a DB connection
|
// Return minimal stub in dev so the pitch renders without a DB connection
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -623,6 +623,50 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
|||||||
liqKSt.values = v
|
liqKSt.values = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Second pass: tax rows (liqGewSt/liqKSt) were written after the rolling balance above.
|
||||||
|
// Recompute Summe AUSZAHLUNGEN → ÜBERSCHUSS chain → rolling balance so taxes are included
|
||||||
|
// even on the first engine run (when tax rows were previously zero).
|
||||||
|
if (sumAus) {
|
||||||
|
const s = emptyMonthly()
|
||||||
|
for (const row of liquid) {
|
||||||
|
if (row.row_type === 'auszahlung' && row.id !== sumAus.id) {
|
||||||
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id])
|
||||||
|
sumAus.values = s
|
||||||
|
}
|
||||||
|
if (uebVorInv && sumEin && sumAus) {
|
||||||
|
const s = emptyMonthly()
|
||||||
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((sumEin.values[`m${m}`] || 0) - (sumAus.values[`m${m}`] || 0))
|
||||||
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorInv.id])
|
||||||
|
uebVorInv.values = s
|
||||||
|
}
|
||||||
|
if (uebVorEnt && uebVorInv && liqInvest) {
|
||||||
|
const s = emptyMonthly()
|
||||||
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorInv.values[`m${m}`] || 0) - (liqInvest.values[`m${m}`] || 0))
|
||||||
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorEnt.id])
|
||||||
|
uebVorEnt.values = s
|
||||||
|
}
|
||||||
|
if (ueberschuss && uebVorEnt && entnahmen) {
|
||||||
|
const s = emptyMonthly()
|
||||||
|
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorEnt.values[`m${m}`] || 0) - (entnahmen.values[`m${m}`] || 0))
|
||||||
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), ueberschuss.id])
|
||||||
|
ueberschuss.values = s
|
||||||
|
}
|
||||||
|
if (kontostand && liquiditaet && ueberschuss) {
|
||||||
|
const ks = emptyMonthly()
|
||||||
|
const lq = emptyMonthly()
|
||||||
|
for (let m = 1; m <= MONTHS; m++) {
|
||||||
|
ks[`m${m}`] = m === 1 ? 0 : Math.round(lq[`m${m - 1}`])
|
||||||
|
lq[`m${m}`] = Math.round(ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0))
|
||||||
|
}
|
||||||
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ks), kontostand.id])
|
||||||
|
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(lq), liquiditaet.id])
|
||||||
|
kontostand.values = ks
|
||||||
|
liquiditaet.values = lq
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount },
|
personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount },
|
||||||
investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest },
|
investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest },
|
||||||
|
|||||||
Reference in New Issue
Block a user