// ============================================================================ // Step 05: Synthesize — Position Descriptions (Gemini Pro) // ============================================================================ import { llmJsonRequest } from "../llm-client.js"; import type { EstimationState, StepResult, PipelineConfig } from "../types.js"; import { DEFAULT_MODELS } from "../types.js"; export async function executeSynthesize( state: EstimationState, config: PipelineConfig, ): Promise { const models = { ...DEFAULT_MODELS, ...config.modelsOverride }; const startTime = Date.now(); if (!state.concept?.auditedFacts || !state.concept?.architecture?.sitemap) { return { success: false, error: "Missing audited facts or sitemap." }; } const facts = state.concept.auditedFacts; // Determine which positions are required const requiredPositions = [ "Das technische Fundament", (facts.selectedPages?.length || 0) + (facts.otherPages?.length || 0) > 0 ? "Individuelle Seiten" : null, facts.features?.length > 0 ? "System-Module (Features)" : null, facts.functions?.length > 0 ? "Logik-Funktionen" : null, facts.apiSystems?.length > 0 ? "Schnittstellen (API)" : null, facts.cmsSetup ? "Inhalts-Verwaltung" : null, facts.multilang ? "Mehrsprachigkeit" : null, "Inhaltliche Initial-Pflege", "Sorglos Betrieb", ].filter(Boolean); const systemPrompt = ` You are a Senior Solution Architect. Write position descriptions for a professional B2B quote. ### REQUIRED POSITIONS (STRICT — ONLY DESCRIBE THESE): ${requiredPositions.map((p) => `"${p}"`).join(", ")} ### RULES (STRICT): 1. NO FIRST PERSON: NEVER "Ich", "Mein", "Wir", "Unser". Lead with nouns or passive verbs. 2. QUANTITY PARITY: Description MUST list EXACTLY the number of items matching 'qty'. 3. CMS GUARD: If cmsSetup=false, do NOT mention "CMS", "Inhaltsverwaltung". Use "Plattform-Struktur". 4. TONE: "Erstellung von...", "Anbindung der...", "Bereitstellung von...". Technical, high-density. 5. PAGES: List actual page names. NO implementation notes in parentheses. 6. HARD SPECIFICS: Use industry terms from the briefing (e.g. "Kabeltiefbau", "110 kV"). 7. KEYS: Return EXACTLY the keys from REQUIRED POSITIONS. 8. NO AGB: NEVER mention "AGB" or "Geschäftsbedingungen". 9. Sorglos Betrieb: "Inklusive 1 Jahr technischer Betrieb, Hosting, SSL, Sicherheits-Updates, Monitoring und techn. Support." 10. Inhaltliche Initial-Pflege: Refers to DATENSÄTZE (datasets like products, references), NOT Seiten. Use "Datensätze" in the description, not "Seiten". 11. Mehrsprachigkeit: This is a +20% markup on the subtotal. NOT an API. NOT a Schnittstelle. ### EXAMPLES: - GOOD: "Erstellung der Seiten: Startseite, Über uns, Leistungen, Kontakt." - GOOD: "Native API-Anbindung an Google Maps mit individueller Standort-Visualisierung." - BAD: "Ich richte dir das CMS ein." - BAD: "Verschiedene Funktionen" (too generic — name the things!) ### DATA CONTEXT: ${JSON.stringify({ facts, sitemap: state.concept.architecture.sitemap, strategy: { briefingSummary: state.concept.strategy.briefingSummary } }, null, 2)} ### OUTPUT FORMAT: { "positionDescriptions": { "Das technische Fundament": string, ... } } `; try { const { data, usage } = await llmJsonRequest({ model: models.pro, systemPrompt, userPrompt: state.concept.briefing, apiKey: config.openrouterKey, }); return { success: true, data: data.positionDescriptions || data, usage: { step: "05-synthesize", model: models.pro, promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, cost: usage.cost, durationMs: Date.now() - startTime, }, }; } catch (err) { return { success: false, error: `Synthesize step failed: ${(err as Error).message}` }; } }