attempt to improve post creation

This commit is contained in:
2026-03-16 13:49:07 +01:00
parent 17799a05ce
commit d4bf3fe25d
5 changed files with 377 additions and 18 deletions

View File

@@ -59,11 +59,25 @@ class CriticAgent(BaseAgent):
def _get_system_prompt(self, profile_analysis: Dict[str, Any], example_posts: Optional[List[str]] = None, iteration: int = 1, max_iterations: int = 3) -> str:
"""Get system prompt for critic - orientiert an bewährten n8n-Prompts."""
writing_style = profile_analysis.get("writing_style", {})
linguistic = profile_analysis.get("linguistic_fingerprint", {})
tone_analysis = profile_analysis.get("tone_analysis", {})
phrase_library = profile_analysis.get("phrase_library", {})
def _ensure_dict(value: Any) -> Dict[str, Any]:
return value if isinstance(value, dict) else {}
def _extract_from_list(items: Any, key: str) -> Optional[str]:
if not isinstance(items, list):
return None
key_lower = key.lower()
for item in items:
if isinstance(item, str) and item.lower().startswith(f"{key_lower}:"):
return item.split(":", 1)[1].strip()
return None
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 = profile_analysis.get("structure_templates", {})
structure_templates_dict = _ensure_dict(structure_templates)
audience_insights = _ensure_dict(profile_analysis.get("audience_insights", {}))
# Build example posts section for style comparison
examples_section = ""
@@ -84,7 +98,10 @@ class CriticAgent(BaseAgent):
cta_phrases = phrase_library.get('cta_phrases', [])
# Extract structure info
primary_structure = structure_templates.get('primary_structure', 'Hook → Body → CTA')
if isinstance(structure_templates, list):
primary_structure = _extract_from_list(structure_templates, "primary_structure") or "Hook → Body → CTA"
else:
primary_structure = structure_templates_dict.get('primary_structure', 'Hook → Body → CTA')
# Iteration-aware guidance
iteration_guidance = ""
@@ -118,7 +135,7 @@ ITERATION {iteration}/{max_iterations} - Fortschritt anerkennen:
REFERENZ-PROFIL (Der Maßstab):
Branche: {profile_analysis.get('audience_insights', {}).get('industry_context', 'Business')}
Branche: {audience_insights.get('industry_context', 'Business')}
Perspektive: {writing_style.get('perspective', 'Ich-Perspektive')}
Ansprache: {writing_style.get('form_of_address', 'Du/Euch')}
Energie-Level: {linguistic.get('energy_level', 7)}/10 (1=sachlich, 10=explosiv)

View File

@@ -384,6 +384,10 @@ class WriterAgent(BaseAgent):
if topic.get('why_this_person'):
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"
# User thoughts section
thoughts_section = ""
if user_thoughts:
@@ -420,7 +424,7 @@ Schreibe einen authentischen LinkedIn-Post, der:
{angle_section}{hook_section}
**KERN-FAKT / INHALT:**
{topic.get('fact', topic.get('description', ''))}
{summary_section}{extended_summary_section}{outline_section}{key_points_section}{facts_section}{quotes_section}{source_section}{thoughts_section}
{summary_section}{extended_summary_section}{outline_section}{key_points_section}{facts_section}{quotes_section}{source_section}{plan_section}{thoughts_section}
**WARUM RELEVANT:**
{topic.get('relevance', 'Aktuelles Thema für die Zielgruppe')}
{why_section}
@@ -587,6 +591,37 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
phrase_library = profile_analysis.get("phrase_library", {})
structure_templates = profile_analysis.get("structure_templates", {})
def _ensure_dict(value):
return value if isinstance(value, dict) else {}
def _extract_from_list(items, key):
if not isinstance(items, list):
return None
prefix = f"{key}:"
for item in items:
text = str(item)
if text.lower().startswith(prefix):
return text.split(":", 1)[1].strip()
return None
def _extract_list_from_list(items, key):
value = _extract_from_list(items, key)
if not value:
return []
return [part.strip() for part in value.split(",") if part.strip()]
def _extract_emojis(text: str) -> list:
if not text:
return []
pattern = r"[\U0001F300-\U0001FAFF]"
matches = re.findall(pattern, text)
return matches or []
# Normalize optional sections
visual_dict = _ensure_dict(visual)
content_strategy_dict = _ensure_dict(content_strategy)
structure_templates_dict = _ensure_dict(structure_templates)
# Build example posts section (OPTIMIERT: mehr Kontext, weniger kürzen)
examples_section = ""
if example_posts and len(example_posts) > 0:
@@ -598,7 +633,13 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
examples_section += "--- Ende Beispiele ---\n"
# Safe extraction of nested values
emoji_list = visual.get('emoji_usage', {}).get('emojis', ['🚀'])
if isinstance(visual, list):
emojis = []
for item in visual:
emojis.extend(_extract_emojis(str(item)))
emoji_list = emojis if emojis else ['🚀']
else:
emoji_list = visual_dict.get('emoji_usage', {}).get('emojis', ['🚀'])
emoji_str = ' '.join(emoji_list) if isinstance(emoji_list, list) else str(emoji_list)
sig_phrases = linguistic.get('signature_phrases', [])
narrative_anchors = linguistic.get('narrative_anchors', [])
@@ -622,9 +663,22 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
return '\n - '.join(selected)
# Extract structure templates
primary_structure = structure_templates.get('primary_structure', 'Hook → Body → CTA')
sentence_starters = structure_templates.get('typical_sentence_starters', [])
paragraph_transitions = structure_templates.get('paragraph_transitions', [])
if isinstance(structure_templates, list):
primary_structure = _extract_from_list(structure_templates, "primary_structure") or "Hook → Body → CTA"
sentence_starters = _extract_list_from_list(structure_templates, "typical_sentence_starters")
paragraph_transitions = _extract_list_from_list(structure_templates, "paragraph_transitions")
else:
primary_structure = structure_templates_dict.get('primary_structure', 'Hook → Body → CTA')
sentence_starters = structure_templates_dict.get('typical_sentence_starters', [])
paragraph_transitions = structure_templates_dict.get('paragraph_transitions', [])
if isinstance(content_strategy, list):
content_strategy_dict = {
"cta_style": _extract_from_list(content_strategy, "cta_style") or "Interaktive Frage an die Community",
"hook_patterns": _extract_from_list(content_strategy, "hook_patterns"),
"post_structure": _extract_from_list(content_strategy, "post_structure"),
"storytelling_approach": _extract_from_list(content_strategy, "storytelling_approach"),
}
# Extract N-gram patterns if available (NEU!)
ngram_patterns = profile_analysis.get("ngram_patterns", {})
@@ -799,20 +853,20 @@ Zielgruppe: {audience.get('target_audience', 'Professionals')}
4. VISUELLE REGELN:
Unicode-Fettung: Nutze für den ersten Satz (Hook) fette Unicode-Zeichen (z.B. 𝗪𝗶𝗰𝗵𝘁𝗶𝗴𝗲𝗿 𝗦𝗮𝘁𝘇), sofern das zur Person passt: {visual.get('unicode_formatting', 'Fett für Hooks')}
Unicode-Fettung: Nutze für den ersten Satz (Hook) fette Unicode-Zeichen (z.B. 𝗪𝗶𝗰𝗵𝘁𝗶𝗴𝗲𝗿 𝗦𝗮𝘁𝘇), sofern das zur Person passt: {visual_dict.get('unicode_formatting', _extract_from_list(visual, 'unicode_formatting') if isinstance(visual, list) else 'Fett für Hooks')}
Emoji-Logik: Verwende diese Emojis: {emoji_str}
Platzierung: {visual.get('emoji_usage', {}).get('placement', 'Ende')}
Häufigkeit: {visual.get('emoji_usage', {}).get('frequency', 'Mittel')}
Platzierung: {visual_dict.get('emoji_usage', {}).get('placement', _extract_from_list(visual, 'emoji_usage') if isinstance(visual, list) else 'Ende')}
Häufigkeit: {visual_dict.get('emoji_usage', {}).get('frequency', 'Mittel')}
Erzähl-Anker: Baue Elemente ein wie: {narrative_str}
(Falls 'PS-Zeilen', 'Dialoge' oder 'Flashbacks' genannt sind, integriere diese wenn es passt.)
Layout: {visual.get('structure_preferences', 'Kurze Absätze, mobil-optimiert')}
Layout: {visual_dict.get('structure_preferences', _extract_from_list(visual, 'structure_preferences') if isinstance(visual, list) else 'Kurze Absätze, mobil-optimiert')}
Länge: Ca. {writing_style.get('average_word_count', 300)} Wörter
CTA: Beende den Post mit einer Variante von: {content_strategy.get('cta_style', 'Interaktive Frage an die Community')}
CTA: Beende den Post mit einer Variante von: {content_strategy_dict.get('cta_style', 'Interaktive Frage an die Community')}
5. GUARDRAILS (VERBOTE!):
@@ -936,6 +990,10 @@ Strategy Weight: {strategy_weight:.1f} / 1.0
# Revision mode with structured feedback
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"
return f"""ÜBERARBEITE den Post basierend auf dem Feedback.
**VORHERIGE VERSION:**
@@ -945,6 +1003,7 @@ Strategy Weight: {strategy_weight:.1f} / 1.0
{feedback}
{specific_changes_text}
{improvements_text}
{plan_section}
**DEINE AUFGABE:**
1. Führe die Änderungen durch wie im Feedback beschrieben
2. Behalte alles bei was gut funktioniert
@@ -999,6 +1058,10 @@ Gib NUR den überarbeiteten Post zurück - keine Kommentare."""
source_url = topic.get('source') or ""
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"
# User thoughts section
thoughts_section = ""
if user_thoughts:
@@ -1035,7 +1098,7 @@ Schreibe einen authentischen LinkedIn-Post, der:
{angle_section}{hook_section}
**KERN-FAKT / INHALT:**
{topic.get('fact', topic.get('description', ''))}
{summary_section}{extended_summary_section}{outline_section}{key_points_section}{facts_section}{quotes_section}{source_section}{thoughts_section}
{summary_section}{extended_summary_section}{outline_section}{key_points_section}{facts_section}{quotes_section}{source_section}{plan_section}{thoughts_section}
**WARUM RELEVANT:**
{topic.get('relevance', 'Aktuelles Thema für die Zielgruppe')}

View File

@@ -1,4 +1,5 @@
"""Main orchestrator for the LinkedIn workflow."""
import json
from collections import Counter
from typing import Dict, Any, List, Optional, Callable
from uuid import UUID
@@ -638,6 +639,19 @@ 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(
topic=topic,
profile_analysis=profile_analysis.full_analysis,
company_strategy=company_strategy,
strategy_weight=strategy_weight,
selected_hook=selected_hook,
user_thoughts=user_thoughts,
post_type_analysis=post_type_analysis
)
if post_plan:
topic = {**topic, "post_plan": post_plan}
# Writer-Critic loop
while iteration < max_iterations and not approved:
iteration += 1
@@ -697,13 +711,45 @@ class WorkflowOrchestrator:
max_iterations=max_iterations
)
# Style/Strategy scoring (enforce thresholds based on strategy_weight)
style_strategy = await self._score_style_strategy(
post=current_post,
profile_analysis=profile_analysis.full_analysis,
company_strategy=company_strategy,
strategy_weight=strategy_weight
)
style_score = style_strategy.get("style_score", 0)
strategy_score = style_strategy.get("strategy_score", 0)
style_threshold = int(60 + (1 - strategy_weight) * 30)
strategy_threshold = int(60 + strategy_weight * 30)
if not company_strategy:
strategy_score = 100
strategy_threshold = 0
critic_result["style_score"] = style_score
critic_result["strategy_score"] = strategy_score
critic_result["style_threshold"] = style_threshold
critic_result["strategy_threshold"] = strategy_threshold
needs_style_fix = style_score < style_threshold
needs_strategy_fix = strategy_score < strategy_threshold
if needs_style_fix or needs_strategy_fix:
critic_result["approved"] = False
issues = []
if needs_style_fix:
issues.append(f"Stil-Match zu niedrig ({style_score}/100, Ziel: {style_threshold}).")
if needs_strategy_fix:
issues.append(f"Strategie-Match zu niedrig ({strategy_score}/100, Ziel: {strategy_threshold}).")
feedback_note = " ".join(issues)
extra = style_strategy.get("feedback", "")
critic_result["feedback"] = f"{critic_result.get('feedback','')}\n\n{feedback_note}\n{extra}".strip()
critic_feedback_list.append(critic_result)
approved = critic_result.get("approved", False)
score = critic_result.get("overall_score", 0)
# Auto-approve on last iteration if score is decent (>= 80)
if iteration == max_iterations and not approved and score >= 80:
if iteration == max_iterations and not approved and score >= 80 and not (needs_style_fix or needs_strategy_fix):
approved = True
critic_result["approved"] = True
logger.info(f"Auto-approved on final iteration with score {score}")
@@ -838,6 +884,138 @@ class WorkflowOrchestrator:
"readability_check": readability_result
}
async def _build_post_plan(
self,
topic: Dict[str, Any],
profile_analysis: Dict[str, Any],
company_strategy: Optional[Dict[str, Any]],
strategy_weight: float,
selected_hook: str = "",
user_thoughts: str = "",
post_type_analysis: Optional[Dict[str, Any]] = None
) -> Optional[Dict[str, Any]]:
"""Create a structured plan to guide the writer."""
try:
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."
)
user_prompt = f"""
Thema: {topic.get('title')}
Kern-Fakt: {topic.get('fact') or topic.get('summary','')}
Relevanz: {topic.get('relevance')}
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 ''}
Content Pillars: {company_strategy.get('content_pillars') if company_strategy else []}
Do: {company_strategy.get('dos') if company_strategy else []}
Don't: {company_strategy.get('donts') if company_strategy else []}
Strategy Weight: {strategy_weight:.1f}
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/..."
}}
"""
raw = await self.writer.call_openai(
system_prompt=system_prompt,
user_prompt=user_prompt,
model="gpt-4o",
temperature=0.2,
response_format={"type": "json_object"}
)
return json.loads(raw)
except Exception as e:
logger.warning(f"Post plan generation failed: {e}")
return None
async def _score_style_strategy(
self,
post: str,
profile_analysis: Dict[str, Any],
company_strategy: Optional[Dict[str, Any]],
strategy_weight: float
) -> Dict[str, Any]:
"""Score style and strategy match separately."""
try:
system_prompt = (
"Bewerte einen LinkedIn-Post in zwei Dimensionen: Stil-Match zur Person "
"und Strategie-Match zur Unternehmensstrategie. Antwort nur als JSON."
)
user_prompt = f"""
POST:
\"\"\"{post}\"\"\"
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 ''}
Content Pillars: {company_strategy.get('content_pillars') if company_strategy else []}
Do: {company_strategy.get('dos') if company_strategy else []}
Don't: {company_strategy.get('donts') if company_strategy else []}
Gib JSON im Format:
{{
"style_score": 0-100,
"strategy_score": 0-100,
"feedback": "Kurzes, konkretes Verbesserungshinweis-Text (2-4 Sätze)"
}}
"""
raw = await self.critic.call_openai(
system_prompt=system_prompt,
user_prompt=user_prompt,
model="gpt-4o",
temperature=0.2,
response_format={"type": "json_object"}
)
return json.loads(raw)
except Exception as e:
logger.warning(f"Style/Strategy scoring failed: {e}")
return {"style_score": 0, "strategy_score": 0, "feedback": ""}
async def generate_post_plan(
self,
topic: Dict[str, Any],
profile_analysis: Dict[str, Any],
company_strategy: Optional[Dict[str, Any]],
strategy_weight: float,
selected_hook: str = "",
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(
topic=topic,
profile_analysis=profile_analysis,
company_strategy=company_strategy,
strategy_weight=strategy_weight,
selected_hook=selected_hook,
user_thoughts=user_thoughts,
post_type_analysis=post_type_analysis
)
async def _extract_recurring_feedback(self, user_id: UUID) -> Dict[str, Any]:
"""
Extract recurring feedback patterns from past generated posts.

View File

@@ -0,0 +1,71 @@
"""Normalize profile analysis structures for consistent access."""
from typing import Any, Dict, List
def _extract_from_list(items: Any, key: str) -> str:
if not isinstance(items, list):
return ""
prefix = f"{key}:"
for item in items:
text = str(item)
if text.lower().startswith(prefix):
return text.split(":", 1)[1].strip()
return ""
def _extract_list_from_list(items: Any, key: str) -> List[str]:
value = _extract_from_list(items, key)
if not value:
return []
return [part.strip() for part in value.split(",") if part.strip()]
def _ensure_dict(value: Any) -> Dict[str, Any]:
return value if isinstance(value, dict) else {}
def normalize_profile_analysis(profile_analysis: Dict[str, Any]) -> Dict[str, Any]:
"""Ensure nested fields are dicts with expected keys to avoid crashes."""
if not isinstance(profile_analysis, dict):
return {}
normalized = dict(profile_analysis)
visual = profile_analysis.get("visual_patterns", {})
content_strategy = profile_analysis.get("content_strategy", {})
structure_templates = profile_analysis.get("structure_templates", {})
if isinstance(visual, list):
normalized["visual_patterns"] = {
"unicode_formatting": _extract_from_list(visual, "unicode_formatting"),
"structure_preferences": _extract_from_list(visual, "structure_preferences"),
"emoji_usage": {
"placement": _extract_from_list(visual, "emoji_usage"),
"frequency": _extract_from_list(visual, "emoji_usage"),
"emojis": []
}
}
else:
normalized["visual_patterns"] = _ensure_dict(visual)
if isinstance(content_strategy, list):
normalized["content_strategy"] = {
"cta_style": _extract_from_list(content_strategy, "cta_style"),
"hook_patterns": _extract_from_list(content_strategy, "hook_patterns"),
"post_structure": _extract_from_list(content_strategy, "post_structure"),
"storytelling_approach": _extract_from_list(content_strategy, "storytelling_approach"),
}
else:
normalized["content_strategy"] = _ensure_dict(content_strategy)
if isinstance(structure_templates, list):
normalized["structure_templates"] = {
"primary_structure": _extract_from_list(structure_templates, "primary_structure"),
"template_examples": _extract_list_from_list(structure_templates, "template_examples"),
"paragraph_transitions": _extract_list_from_list(structure_templates, "paragraph_transitions"),
"typical_sentence_starters": _extract_list_from_list(structure_templates, "typical_sentence_starters"),
}
else:
normalized["structure_templates"] = _ensure_dict(structure_templates)
return normalized

View File

@@ -4373,6 +4373,16 @@ async def chat_generate_post(request: Request):
"relevance": "User-specified content"
}
plan = await orchestrator.generate_post_plan(
topic=topic,
profile_analysis=profile_analysis.full_analysis,
company_strategy=company_strategy,
strategy_weight=getattr(post_type, 'strategy_weight', 0.5),
user_thoughts=message
)
if plan:
topic["post_plan"] = plan
# Generate post
post_content = await writer.process(
topic=topic,
@@ -4485,6 +4495,16 @@ async def chat_refine_post(request: Request):
"relevance": "User refinement request"
}
plan = await orchestrator.generate_post_plan(
topic=topic,
profile_analysis=full_analysis,
company_strategy=company_strategy,
strategy_weight=getattr(post_type, 'strategy_weight', 0.5),
user_thoughts=message
)
if plan:
topic["post_plan"] = plan
# Use writer's revision capability
refined_post = await writer.process(
topic=topic,
@@ -4794,6 +4814,16 @@ async def company_chat_generate_post(request: Request):
)
topic = {"title": message[:100], "fact": message, "relevance": "Company-created content"}
plan = await orchestrator.generate_post_plan(
topic=topic,
profile_analysis=profile_analysis.full_analysis,
company_strategy=company_strategy,
strategy_weight=getattr(post_type, 'strategy_weight', 0.5),
user_thoughts=message
)
if plan:
topic["post_plan"] = plan
post_content = await writer.process(
topic=topic,
profile_analysis=profile_analysis.full_analysis,