diff --git a/src/agents/writer.py b/src/agents/writer.py index 595b98e..eba0491 100644 --- a/src/agents/writer.py +++ b/src/agents/writer.py @@ -263,7 +263,12 @@ class WriterAgent(BaseAgent): num_drafts = min(max(settings.writer_multi_draft_count, 2), 5) # Clamp between 2-5 logger.info(f"Generating {num_drafts} drafts for selection") - system_prompt = self._get_system_prompt(profile_analysis, example_posts, learned_lessons, post_type, post_type_analysis, company_strategy, strategy_weight) + system_prompt = self._get_compact_system_prompt( + profile_analysis=profile_analysis, + example_posts=example_posts, + company_strategy=company_strategy, + strategy_weight=strategy_weight + ) # Generate drafts in parallel with different temperatures/approaches draft_configs = [ @@ -385,8 +390,13 @@ class WriterAgent(BaseAgent): why_section = f"\n**WARUM DU DARÜBER SCHREIBEN SOLLTEST:**\n{topic.get('why_this_person')}\n" plan_section = "" - if topic.get("post_plan"): - plan_section = f"\n**POST-PLAN (VERBINDLICH):**\n{json.dumps(topic.get('post_plan'), ensure_ascii=False, indent=2)}\n" + if topic.get("content_plan"): + plan = topic.get("content_plan") or {} + alignment = plan.get("company_alignment") or [] + alignment_section = "" + if isinstance(alignment, list) and alignment: + alignment_section = "\n**STRATEGIE-ANFORDERUNGEN (VERBINDLICH):**\n" + "\n".join([f"- {a}" for a in alignment]) + "\n" + plan_section = f"\n**CONTENT-PLAN (VERBINDLICH):**\n{json.dumps(plan, ensure_ascii=False, indent=2)}\n{alignment_section}" # User thoughts section thoughts_section = "" @@ -416,6 +426,16 @@ Schreibe einen authentischen LinkedIn-Post, der: 4. Die Key Facts einbaut wo es passt 5. Mit einem passenden CTA endet""" + content_plan_note = "" + if topic.get("content_plan"): + plan = topic.get("content_plan") or {} + tier = plan.get("quality_tier") + required = plan.get("required_elements") or [] + req_text = ", ".join(required[:6]) if isinstance(required, list) else "" + tier_text = f" | Qualitätsziel: {tier}" if tier else "" + req_note = f" | Pflichtelemente: {req_text}" if req_text else "" + content_plan_note = f"\n- CONTENT-PLAN ist verbindlich: Nutze NUR diese Fakten, keine neuen hinzufügen{tier_text}{req_note}" + return f"""Schreibe einen LinkedIn-Post zu folgendem Thema: **THEMA:** {topic.get('title', 'Unbekanntes Thema')} @@ -437,6 +457,7 @@ WICHTIG: - Vermeide KI-typische Formulierungen ("In der heutigen Zeit", "Tauchen Sie ein", etc.) - Schreibe natürlich und menschlich - Der Post soll SOFORT 85+ Punkte im Review erreichen +{content_plan_note} Gib NUR den fertigen Post zurück.""" @@ -556,7 +577,12 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format: elif len(selected_examples) > 3: selected_examples = random.sample(selected_examples, 3) - system_prompt = self._get_system_prompt(profile_analysis, selected_examples, learned_lessons, post_type, post_type_analysis, company_strategy, strategy_weight) + system_prompt = self._get_compact_system_prompt( + profile_analysis=profile_analysis, + example_posts=selected_examples, + company_strategy=company_strategy, + strategy_weight=strategy_weight + ) user_prompt = self._get_user_prompt(topic, feedback, previous_version, critic_result, user_thoughts, selected_hook) # OPTIMIERT: Niedrigere Temperature (0.5 statt 0.6) für konsistenteren Stil @@ -570,6 +596,104 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format: logger.info("Post written successfully") return post.strip() + def _build_style_card(self, profile_analysis: Dict[str, Any]) -> str: + """Build a compact, deterministic style card.""" + def _ensure_dict(value): + return value if isinstance(value, dict) else {} + + writing_style = _ensure_dict(profile_analysis.get("writing_style", {})) + linguistic = _ensure_dict(profile_analysis.get("linguistic_fingerprint", {})) + tone_analysis = _ensure_dict(profile_analysis.get("tone_analysis", {})) + phrase_library = _ensure_dict(profile_analysis.get("phrase_library", {})) + structure_templates = _ensure_dict(profile_analysis.get("structure_templates", {})) + visual = _ensure_dict(profile_analysis.get("visual_patterns", {})) + audience = _ensure_dict(profile_analysis.get("audience_insights", {})) + + def _top(items, n): + if not isinstance(items, list): + return [] + return [str(i) for i in items[:n]] + + hook_phrases = _top(phrase_library.get("hook_phrases", []), 3) + cta_phrases = _top(phrase_library.get("cta_phrases", []), 2) + signature_phrases = _top(linguistic.get("signature_phrases", []), 3) + sentence_starters = _top(structure_templates.get("typical_sentence_starters", []), 3) + paragraph_transitions = _top(structure_templates.get("paragraph_transitions", []), 2) + + emoji_usage = visual.get("emoji_usage", {}) + emoji_list = emoji_usage.get("emojis", []) + emoji_str = " ".join(_top(emoji_list, 5)) if isinstance(emoji_list, list) else str(emoji_list) + + return f"""STYLE CARD (kompakt, verbindlich): +- Perspektive: {writing_style.get('perspective', 'Ich')} +- Ansprache: {writing_style.get('form_of_address', 'Du')} +- Ton: {tone_analysis.get('primary_tone', writing_style.get('tone', 'Professionell'))} +- Energie: {linguistic.get('energy_level', 7)}/10 +- Satz-Dynamik: {writing_style.get('sentence_dynamics', 'Mix')} +- Hook-Stil Beispiele: {', '.join(hook_phrases) if hook_phrases else 'keine'} +- CTA-Stil Beispiele: {', '.join(cta_phrases) if cta_phrases else 'keine'} +- Signature Phrases: {', '.join(signature_phrases) if signature_phrases else 'keine'} +- Typische Satzanfänge: {', '.join(sentence_starters) if sentence_starters else 'keine'} +- Übergänge: {', '.join(paragraph_transitions) if paragraph_transitions else 'keine'} +- Emojis: {emoji_str or 'keine'} | Platzierung: {emoji_usage.get('placement', 'Ende')} | Frequenz: {emoji_usage.get('frequency', 'Mittel')} +- Zielgruppe: {audience.get('target_audience', 'Professionals')} | Branche: {audience.get('industry_context', 'Business')} +""" + + def _get_compact_system_prompt( + self, + profile_analysis: Dict[str, Any], + example_posts: List[str] = None, + company_strategy: Optional[Dict[str, Any]] = None, + strategy_weight: float = 0.5 + ) -> str: + """Short, high-signal system prompt to enforce style without overload.""" + style_card = self._build_style_card(profile_analysis) + + examples_section = "" + if example_posts: + sample = example_posts[:2] + examples_section = "\nECHTE BEISPIELE (nur Stil, nicht kopieren):\n" + for i, post in enumerate(sample, 1): + post_text = post[:800] + "..." if len(post) > 800 else post + examples_section += f"\n--- Beispiel {i} ---\n{post_text}\n" + + strategy_section = "" + if company_strategy and strategy_weight >= 0.2: + content_pillars = company_strategy.get("content_pillars", []) + brand_voice = company_strategy.get("brand_voice", "") + tone_guidelines = company_strategy.get("tone_guidelines", "") + dos = company_strategy.get("dos", []) + donts = company_strategy.get("donts", []) + + if strategy_weight >= 0.8: + weight_note = "KRITISCH: Strategie dominiert Inhalt und Stil" + elif strategy_weight >= 0.6: + weight_note = "WICHTIG: Strategie klar erkennbar" + elif strategy_weight >= 0.4: + weight_note = "MODERAT: Strategie subtil einweben" + else: + weight_note = "LEICHT: Strategie nur als Kontext" + + strategy_section = f""" +UNTERNEHMENSKONTEXT ({weight_note}): +- Brand Voice: {brand_voice} +- Tone Guidelines: {tone_guidelines} +- Content Pillars: {', '.join(content_pillars) if content_pillars else 'keine'} +- Do: {', '.join(dos[:3]) if isinstance(dos, list) and dos else 'keine'} +- Don't: {', '.join(donts[:3]) if isinstance(donts, list) and donts else 'keine'} +""" + + return f"""Du bist Ghostwriter für LinkedIn. Schreibe einen Post, der exakt wie die Person klingt. +{style_card} +{strategy_section} +{examples_section} +REGELN: +- Inhalt strikt an den Content-Plan halten, keine neuen Fakten erfinden +- Perspektive und Ansprache strikt einhalten +- Keine KI-Phrasen ("In der heutigen Zeit", "Stellen Sie sich vor", etc.) +- Keine Markdown-Fettung (kein **), keine Trennlinien (---) +- Ausgabe: Nur der fertige Post""" + def _get_system_prompt( self, profile_analysis: Dict[str, Any], @@ -991,8 +1115,13 @@ Strategy Weight: {strategy_weight:.1f} / 1.0 score_text = f"**AKTUELLER SCORE:** {critic_result.get('overall_score', 'N/A')}/100\n\n" if critic_result else "" plan_section = "" - if topic.get("post_plan"): - plan_section = f"\n**POST-PLAN (VERBINDLICH):**\n{json.dumps(topic.get('post_plan'), ensure_ascii=False, indent=2)}\n" + if topic.get("content_plan"): + plan = topic.get("content_plan") or {} + alignment = plan.get("company_alignment") or [] + alignment_section = "" + if isinstance(alignment, list) and alignment: + alignment_section = "\n**STRATEGIE-ANFORDERUNGEN (VERBINDLICH):**\n" + "\n".join([f"- {a}" for a in alignment]) + "\n" + plan_section = f"\n**CONTENT-PLAN (VERBINDLICH):**\n{json.dumps(plan, ensure_ascii=False, indent=2)}\n{alignment_section}" return f"""ÜBERARBEITE den Post basierend auf dem Feedback. @@ -1059,8 +1188,8 @@ Gib NUR den überarbeiteten Post zurück - keine Kommentare.""" source_section = f"\n**QUELLE:** {source_title} {source_url}\n" plan_section = "" - if topic.get("post_plan"): - plan_section = f"\n**POST-PLAN (VERBINDLICH):**\n{json.dumps(topic.get('post_plan'), ensure_ascii=False, indent=2)}\n" + if topic.get("content_plan"): + plan_section = f"\n**CONTENT-PLAN (VERBINDLICH):**\n{json.dumps(topic.get('content_plan'), ensure_ascii=False, indent=2)}\n" # User thoughts section thoughts_section = "" @@ -1090,6 +1219,16 @@ Schreibe einen authentischen LinkedIn-Post, der: 4. Die Key Facts verwendet wo es passt 5. Mit einem passenden CTA endet""" + content_plan_note = "" + if topic.get("content_plan"): + plan = topic.get("content_plan") or {} + tier = plan.get("quality_tier") + required = plan.get("required_elements") or [] + req_text = ", ".join(required[:6]) if isinstance(required, list) else "" + tier_text = f" | Qualitätsziel: {tier}" if tier else "" + req_note = f" | Pflichtelemente: {req_text}" if req_text else "" + content_plan_note = f"\n- CONTENT-PLAN ist verbindlich: Nutze NUR diese Fakten, keine neuen hinzufügen{tier_text}{req_note}" + return f"""Schreibe einen LinkedIn-Post zu folgendem Thema: **THEMA:** {topic.get('title', 'Unbekanntes Thema')} @@ -1108,6 +1247,7 @@ WICHTIG: - Vermeide KI-typische Formulierungen ("In der heutigen Zeit", "Tauchen Sie ein", etc.) - Schreibe natürlich und menschlich - Der Post soll SOFORT 85+ Punkte im Review erreichen +{content_plan_note} Gib NUR den fertigen Post zurück.""" diff --git a/src/orchestrator.py b/src/orchestrator.py index b2043c3..6ba4382 100644 --- a/src/orchestrator.py +++ b/src/orchestrator.py @@ -48,6 +48,51 @@ class WorkflowOrchestrator: ] logger.info("WorkflowOrchestrator initialized with quality check & refiner agents") + def _get_effective_strategy_weight( + self, + base_weight: float, + post_type: Optional[Any] = None, + post_type_analysis: Optional[Dict[str, Any]] = None + ) -> float: + """ + Adjust strategy weight based on post type semantics. + Heuristic: company/announcement types increase, personal/story types decrease. + """ + weight = float(base_weight or 0.0) + + text_parts: List[str] = [] + if post_type: + text_parts.append(str(getattr(post_type, "name", "") or "")) + text_parts.append(str(getattr(post_type, "description", "") or "")) + if post_type_analysis and isinstance(post_type_analysis, dict): + content_focus = post_type_analysis.get("content_focus") or {} + if isinstance(content_focus, dict): + text_parts.append(" ".join(content_focus.get("main_themes", []) or [])) + text_parts.append(str(content_focus.get("value_proposition", "") or "")) + writing_guidelines = post_type_analysis.get("writing_guidelines") or {} + if isinstance(writing_guidelines, dict): + text_parts.append(" ".join(writing_guidelines.get("dos", []) or [])) + text_parts.append(" ".join(writing_guidelines.get("donts", []) or [])) + hooks = post_type_analysis.get("hooks") or {} + if isinstance(hooks, dict): + text_parts.append(" ".join(hooks.get("hook_types", []) or [])) + + text = " ".join([t for t in text_parts if t]).lower() + + boost = 0.0 + if any(k in text for k in ["announcement", "launch", "produkt", "product", "case study", "update", "news", "company", "unternehmen"]): + boost += 0.1 + if any(k in text for k in ["personal", "story", "anekdote", "opinion", "lessons", "behind the scenes", "fehler", "learned"]): + boost -= 0.1 + + # Clamp adjustment for extreme weights + if weight >= 0.8: + boost = max(boost, -0.05) + if weight <= 0.3: + boost = min(boost, 0.05) + + return max(0.0, min(1.0, weight + boost)) + def _set_tracking(self, operation: str, user_id: Optional[str] = None, company_id: Optional[str] = None): """Set tracking context on all agents.""" @@ -56,6 +101,20 @@ class WorkflowOrchestrator: for agent in self._all_agents: agent.set_tracking_context(operation=operation, user_id=uid, company_id=comp_id) + def _normalize_critic_scores(self, critic_result: Dict[str, Any]) -> Dict[str, Any]: + """Ensure overall_score equals the sum of category scores.""" + scores = critic_result.get("scores") or {} + if not isinstance(scores, dict): + return critic_result + total = ( + int(scores.get("authenticity_and_style", 0)) + + int(scores.get("content_quality", 0)) + + int(scores.get("logic_and_coherence", 0)) + + int(scores.get("technical_execution", 0)) + ) + critic_result["overall_score"] = total + return critic_result + async def _resolve_tracking_ids(self, user_id: UUID) -> dict: """Resolve company_id from a user_id for tracking.""" try: @@ -600,8 +659,13 @@ class WorkflowOrchestrator: if post_type: if post_type.analysis: post_type_analysis = post_type.analysis - strategy_weight = post_type.strategy_weight # Extract strategy weight from post type - logger.info(f"Using post type '{post_type.name}' with strategy weight {strategy_weight:.1f}") + strategy_weight = post_type.strategy_weight # Base strategy weight from post type + strategy_weight = self._get_effective_strategy_weight( + base_weight=strategy_weight, + post_type=post_type, + post_type_analysis=post_type_analysis + ) + logger.info(f"Using post type '{post_type.name}' with strategy weight {strategy_weight:.2f}") # Load user's real posts as style examples # If post_type_id is specified, only use posts of that type @@ -639,8 +703,8 @@ class WorkflowOrchestrator: company_strategy = company.company_strategy logger.info(f"Loaded company strategy for post creation: {company.name}") - # Build a structured post plan (used to guide writing) - post_plan = await self._build_post_plan( + # Build a structured content plan (content only, used to guide writing) + content_plan = await self._build_content_plan( topic=topic, profile_analysis=profile_analysis.full_analysis, company_strategy=company_strategy, @@ -649,8 +713,12 @@ class WorkflowOrchestrator: user_thoughts=user_thoughts, post_type_analysis=post_type_analysis ) - if post_plan: - topic = {**topic, "post_plan": post_plan} + if content_plan: + topic = { + **topic, + "content_plan": content_plan, + "content_quality": content_plan.get("quality_tier") + } # Writer-Critic loop while iteration < max_iterations and not approved: @@ -710,6 +778,7 @@ class WorkflowOrchestrator: iteration=iteration, max_iterations=max_iterations ) + critic_result = self._normalize_critic_scores(critic_result) # Style/Strategy scoring (enforce thresholds based on strategy_weight) style_strategy = await self._score_style_strategy( @@ -818,6 +887,10 @@ class WorkflowOrchestrator: # Save generated post from src.database.models import GeneratedPost + metadata = { + "content_plan": content_plan, + "strategy_weight_used": strategy_weight + } generated_post = GeneratedPost( user_id=user_id, topic_title=topic.get("title", "Unknown"), @@ -826,7 +899,8 @@ class WorkflowOrchestrator: writer_versions=writer_versions, critic_feedback=critic_feedback_list, status=status, - post_type_id=post_type_id + post_type_id=post_type_id, + metadata=metadata ) saved_post = await db.save_generated_post(generated_post) @@ -884,7 +958,102 @@ class WorkflowOrchestrator: "readability_check": readability_result } - async def _build_post_plan( + def _derive_content_quality( + self, + topic: Dict[str, Any], + user_thoughts: str = "", + strategy_weight: float = 0.5, + post_type_analysis: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Derive a deterministic content quality tier and requirements.""" + key_facts = topic.get("key_facts") or [] + if not isinstance(key_facts, list): + key_facts = [] + has_summary = bool(topic.get("summary")) + has_extended_summary = bool(topic.get("extended_summary")) + thoughts_len = len((user_thoughts or "").strip()) + + score = 0 + score += 2 if has_extended_summary else 0 + score += 1 if has_summary else 0 + if len(key_facts) >= 3: + score += 2 + elif len(key_facts) >= 2: + score += 1 + if thoughts_len >= 200: + score += 2 + elif thoughts_len >= 80: + score += 1 + + if score >= 5: + tier = "premium" + required = [ + "3 klare Kernpunkte", + "1 Gegenpunkt oder Einwand + kurze Einordnung", + "1 Mini-Story oder Beispiel", + "mind. 1 Fakt/Zahl/Zitat (falls vorhanden)", + "CTA als Diskussionsfrage" + ] + elif score >= 3: + tier = "standard" + required = [ + "3 klare Kernpunkte", + "1 Beispiel oder Anwendung", + "1 klarer Takeaway", + "CTA als Frage oder Aufforderung" + ] + else: + tier = "basic" + required = [ + "1 zentrale These", + "2 Kernpunkte", + "1 klarer Takeaway", + "CTA als Frage" + ] + + if thoughts_len > 0: + required.append("Persönliche Gedanken der Person integrieren") + required.append("Eigener Abschnitt: 'Meine Gedanken dazu' oder ähnliche Formulierung") + + # Post type influence: ensure content format alignment when available + post_type_required = [] + if post_type_analysis and isinstance(post_type_analysis, dict): + hooks = post_type_analysis.get("hooks") or {} + patterns = [] + if isinstance(hooks, dict): + patterns = hooks.get("hook_types") or [] + if patterns: + post_type_required.append(f"Hook-Typ bevorzugt: {patterns[0]}") + formats = post_type_analysis.get("content_formats") or post_type_analysis.get("formats") or [] + if isinstance(formats, list) and formats: + post_type_required.append(f"Format-Orientierung: {formats[0]}") + + # Strategy influence: enforce alignment progressively + strategy_required = [] + if strategy_weight >= 0.8: + strategy_required = [ + "Mindestens 2 Content-Pillars integrieren", + "Brand Voice explizit erkennbar" + ] + elif strategy_weight >= 0.6: + strategy_required = [ + "Mindestens 1 Content-Pillar integrieren", + "Marken-Ton subtil erkennbar" + ] + elif strategy_weight >= 0.4: + strategy_required = [ + "1 Pillar ODER 1 Do berücksichtigen" + ] + + combined_required = required + post_type_required + strategy_required + + return { + "quality_tier": tier, + "required_elements": combined_required, + "strategy_required": strategy_required + } + + async def _build_content_plan( self, topic: Dict[str, Any], profile_analysis: Dict[str, Any], @@ -894,28 +1063,44 @@ class WorkflowOrchestrator: user_thoughts: str = "", post_type_analysis: Optional[Dict[str, Any]] = None ) -> Optional[Dict[str, Any]]: - """Create a structured plan to guide the writer.""" + """Create a structured content plan (content only, no style).""" try: + quality = self._derive_content_quality( + topic, + user_thoughts=user_thoughts, + strategy_weight=strategy_weight, + post_type_analysis=post_type_analysis + ) + company_alignment_requirements: List[str] = [] + if company_strategy and strategy_weight >= 0.4: + content_pillars = company_strategy.get("content_pillars") or [] + if isinstance(content_pillars, list) and content_pillars: + if strategy_weight >= 0.8: + company_alignment_requirements.append(f"Mindestens 2 dieser Pillars nutzen: {', '.join(content_pillars[:3])}") + else: + company_alignment_requirements.append(f"Mindestens 1 dieser Pillars nutzen: {', '.join(content_pillars[:3])}") + brand_voice = company_strategy.get("brand_voice") + if brand_voice and strategy_weight >= 0.6: + company_alignment_requirements.append(f"Brand Voice erkennbar: {brand_voice}") + dos = company_strategy.get("dos") or [] + if isinstance(dos, list) and dos and strategy_weight >= 0.6: + company_alignment_requirements.append(f"Mindestens 1 Do berücksichtigen: {', '.join(dos[:2])}") system_prompt = ( - "Du erstellst einen kompakten Schreibplan für einen LinkedIn-Post. " - "Der Plan muss Stil der Person UND Unternehmensstrategie berücksichtigen. " - "Antwort nur als JSON." + "Du erstellst einen kompakten CONTENT-PLAN für einen LinkedIn-Post. " + "Fokus NUR auf Inhalt, Logik, Fakten und Struktur. " + "KEINE Stil-Anweisungen. Antwort nur als JSON." ) user_prompt = f""" Thema: {topic.get('title')} Kern-Fakt: {topic.get('fact') or topic.get('summary','')} Relevanz: {topic.get('relevance')} +Zusammenfassung: {topic.get('summary') or topic.get('extended_summary','')} +Key Facts: {topic.get('key_facts') or []} Ausgewählter Hook: {selected_hook or "keiner"} Persönliche Gedanken: {user_thoughts or "keine"} -Profil-Analyse (Kurz): -Tone: {profile_analysis.get('writing_style', {}).get('tone')} -Perspektive: {profile_analysis.get('writing_style', {}).get('perspective')} -Ansprache: {profile_analysis.get('writing_style', {}).get('form_of_address')} -Signature Phrases: {profile_analysis.get('linguistic_fingerprint', {}).get('signature_phrases', [])} - Unternehmensstrategie (Kurz): Mission: {company_strategy.get('mission') if company_strategy else ''} Brand Voice: {company_strategy.get('brand_voice') if company_strategy else ''} @@ -924,14 +1109,22 @@ Do: {company_strategy.get('dos') if company_strategy else []} Don't: {company_strategy.get('donts') if company_strategy else []} Strategy Weight: {strategy_weight:.1f} +Qualitätsziel: {quality.get('quality_tier')} +Pflichtelemente: {quality.get('required_elements')} +Strategie-Anforderungen (verbindlich falls vorhanden): {company_alignment_requirements or 'keine'} Gib JSON im Format: {{ - "hook_type": "Story/Fakten/These/Frage/...", - "structure": ["Hook", "Body", "CTA"], - "style_requirements": ["2-4 konkrete Stil-Elemente/Signaturphrasen"], - "strategy_requirements": ["2-4 Strategie-Elemente/Content-Pillar/Do"], - "cta_type": "Frage/Aufforderung/Diskussion/..." + "quality_tier": "basic/standard/premium", + "required_elements": ["..."], + "hook_idea": "1-2 Sätze (Inhaltlich, nicht stilistisch)", + "thesis": "Kernaussage in einem Satz", + "key_points": ["Punkt 1", "Punkt 2", "Punkt 3"], + "example_or_story": "Kurzes Beispiel oder Mini-Story zur Stützung", + "cta": "Konkreter CTA (Frage oder Aufforderung)", + "structure": ["Hook", "These", "Punkt 1", "Punkt 2", "Punkt 3", "Beispiel", "CTA"], + "constraints": ["Keine neuen Fakten erfinden", "Fakten müssen aus Quelle oder user_thoughts kommen"], + "company_alignment": ["Erforderliche Strategy-Elemente aus strategy_required falls vorhanden"] }} """ raw = await self.writer.call_openai( @@ -941,7 +1134,21 @@ Gib JSON im Format: temperature=0.2, response_format={"type": "json_object"} ) - return json.loads(raw) + plan = json.loads(raw) + # Ensure required fields exist + if "quality_tier" not in plan: + plan["quality_tier"] = quality.get("quality_tier") + if "required_elements" not in plan: + plan["required_elements"] = quality.get("required_elements") + if "company_alignment" not in plan and quality.get("strategy_required"): + plan["company_alignment"] = quality.get("strategy_required") + if company_alignment_requirements: + existing = plan.get("company_alignment") or [] + if not isinstance(existing, list): + existing = [] + merged = existing + [r for r in company_alignment_requirements if r not in existing] + plan["company_alignment"] = merged + return plan except Exception as e: logger.warning(f"Post plan generation failed: {e}") return None @@ -1005,8 +1212,8 @@ Gib JSON im Format: user_thoughts: str = "", post_type_analysis: Optional[Dict[str, Any]] = None ) -> Optional[Dict[str, Any]]: - """Public wrapper to build a post plan.""" - return await self._build_post_plan( + """Public wrapper to build a content plan.""" + return await self._build_content_plan( topic=topic, profile_analysis=profile_analysis, company_strategy=company_strategy, diff --git a/src/web/templates/post_detail.html b/src/web/templates/post_detail.html index 041d2d7..d67d770 100644 --- a/src/web/templates/post_detail.html +++ b/src/web/templates/post_detail.html @@ -422,48 +422,14 @@
{{ style_card }}
+ {{ style_card }}
+ {{ style_card }}
+