Post Typen Verwalten + Strategy weight

This commit is contained in:
2026-02-14 14:48:03 +01:00
parent 1ebf50ab04
commit 31150000fd
14 changed files with 2624 additions and 43 deletions

View File

@@ -184,8 +184,10 @@ class PostClassifierAgent(BaseAgent):
system_prompt = """Du bist ein Content-Analyst, der LinkedIn-Posts in vordefinierte Kategorien einordnet.
WICHTIG: JEDER Post MUSS kategorisiert werden. Du MUSST für jeden Post den am besten passenden Post-Typ wählen, auch wenn die Übereinstimmung nicht perfekt ist.
Analysiere jeden Post und ordne ihn dem passendsten Post-Typ zu.
Wenn kein Typ wirklich passt, gib "null" als post_type_id zurück.
Es ist NICHT erlaubt, "null" als post_type_id zurückzugeben.
Bewerte die Zuordnung mit einer Confidence zwischen 0.3 und 1.0:
- 0.9-1.0: Sehr sicher, Post passt perfekt zum Typ
@@ -209,12 +211,14 @@ Gib ein JSON-Objekt zurück mit diesem Format:
"classifications": [
{{
"post_id": "uuid-des-posts",
"post_type_id": "uuid-des-typs oder null",
"post_type_id": "uuid-des-typs (IMMER erforderlich, niemals null)",
"confidence": 0.8,
"reasoning": "Kurze Begründung"
}}
]
}}"""
}}
WICHTIG: Jeder Post MUSS einen post_type_id bekommen. Wähle den am besten passenden Typ, auch wenn die Übereinstimmung nicht perfekt ist."""
try:
response = await self.call_openai(
@@ -230,6 +234,8 @@ Gib ein JSON-Objekt zurück mit diesem Format:
# Process and validate results
valid_results = []
unclassified_posts = list(posts) # Track which posts still need classification
for c in classifications:
post_id = c.get("post_id")
post_type_id = c.get("post_type_id")
@@ -244,7 +250,8 @@ Gib ein JSON-Objekt zurück mit diesem Format:
# Validate post_type_id
if post_type_id and post_type_id != "null" and post_type_id not in valid_type_ids:
logger.warning(f"Invalid post_type_id in classification: {post_type_id}")
continue
# Fallback: assign to first available type
post_type_id = list(valid_type_ids - {"null"})[0] if valid_type_ids - {"null"} else None
if post_type_id and post_type_id != "null":
valid_results.append({
@@ -253,6 +260,19 @@ Gib ein JSON-Objekt zurück mit diesem Format:
"classification_method": "semantic",
"classification_confidence": min(1.0, max(0.3, confidence))
})
unclassified_posts = [p for p in unclassified_posts if p.id != matching_post.id]
# FORCE CLASSIFY remaining unclassified posts
if unclassified_posts and post_types:
default_type = post_types[0] # Use first type as fallback
logger.warning(f"Force-classifying {len(unclassified_posts)} posts to default type: {default_type.name}")
for post in unclassified_posts:
valid_results.append({
"post_id": post.id,
"post_type_id": default_type.id,
"classification_method": "semantic_fallback",
"classification_confidence": 0.3
})
return valid_results

View File

@@ -22,7 +22,9 @@ class ResearchAgent(BaseAgent):
customer_data: Dict[str, Any],
example_posts: List[str] = None,
post_type: Any = None,
post_type_analysis: Dict[str, Any] = None
post_type_analysis: Dict[str, Any] = None,
company_strategy: Dict[str, Any] = None,
strategy_weight: float = 0.5
) -> Dict[str, Any]:
"""
Research new content topics.
@@ -34,6 +36,8 @@ class ResearchAgent(BaseAgent):
example_posts: List of the person's actual posts for style reference
post_type: Optional PostType object for targeted research
post_type_analysis: Optional post type analysis for context
company_strategy: Optional company strategy to align topics with
strategy_weight: Strategy weight (0.0-1.0) controlling strategy influence
Returns:
Research results with suggested topics
@@ -90,7 +94,9 @@ class ResearchAgent(BaseAgent):
example_posts=example_posts or [],
existing_topics=existing_topics,
post_type=post_type,
post_type_analysis=post_type_analysis
post_type_analysis=post_type_analysis,
company_strategy=company_strategy,
strategy_weight=strategy_weight
)
response = await self.call_openai(
@@ -379,7 +385,9 @@ QUALITÄTSKRITERIEN:
example_posts: List[str],
existing_topics: List[str],
post_type: Any = None,
post_type_analysis: Dict[str, Any] = None
post_type_analysis: Dict[str, Any] = None,
company_strategy: Dict[str, Any] = None,
strategy_weight: float = 0.5
) -> str:
"""Transform raw research into personalized, concrete topic suggestions."""
@@ -428,8 +436,11 @@ QUALITÄTSKRITERIEN:
post_type_section += "\n**WICHTIG:** Alle Themenvorschläge müssen zu diesem Post-Typ passen!\n"
# Build company strategy section
strategy_section = self._get_strategy_section(company_strategy, strategy_weight)
return f"""AUFGABE: Transformiere die Recherche-Ergebnisse in KONKRETE, PERSONALISIERTE Themenvorschläge.
{post_type_section}
{post_type_section}{strategy_section}
=== RECHERCHE-ERGEBNISSE (Rohdaten) ===
{raw_research}
@@ -492,6 +503,71 @@ WICHTIG:
- Hook-Ideen müssen zum Stil der Beispiel-Posts passen!
- Key Facts müssen aus der Recherche stammen (keine erfundenen Zahlen)"""
def _get_strategy_section(self, company_strategy: Dict[str, Any] = None, strategy_weight: float = 0.5) -> str:
"""Build company strategy section for research prompt with weighted influence."""
if not company_strategy:
return ""
# Extract strategy elements
mission = company_strategy.get("mission", "")
vision = company_strategy.get("vision", "")
content_pillars = company_strategy.get("content_pillars", [])
target_audience = company_strategy.get("target_audience", "")
dos = company_strategy.get("dos", [])
donts = company_strategy.get("donts", [])
# Build section only if there's meaningful content
if not any([mission, vision, content_pillars, dos, donts]):
return ""
# Determine importance level based on strategy_weight
if strategy_weight >= 0.8:
importance = "KRITISCH - STRIKT BEACHTEN"
instruction = "Alle Themen MÜSSEN sich an der Unternehmensstrategie orientieren! Nur Topics vorschlagen, die perfekt zur Strategie passen."
elif strategy_weight >= 0.6:
importance = "WICHTIG - BERÜCKSICHTIGEN"
instruction = "Priorisiere Topics die zur Unternehmensstrategie passen! Die Strategie soll klar erkennbar sein."
elif strategy_weight >= 0.4:
importance = "ORIENTIERUNG"
instruction = "Berücksichtige die Unternehmensstrategie wo sinnvoll - persönliche Expertise hat Priorität."
elif strategy_weight >= 0.2:
importance = "OPTIONAL"
instruction = "Die Strategie dient als Hintergrundinformation - freie Themenauswahl erlaubt."
else:
importance = "NUR REFERENZ"
instruction = "Ignoriere die Strategie weitgehend - Fokus auf persönliche Themen und Expertise."
section = f"""
=== UNTERNEHMENSSTRATEGIE ({importance}) ===
Strategy Weight: {strategy_weight:.1f} / 1.0
{instruction}
"""
if mission:
section += f"\nMISSION: {mission}"
if vision:
section += f"\nVISION: {vision}"
if content_pillars:
section += f"\nCONTENT PILLARS: {', '.join(content_pillars)}"
if target_audience:
section += f"\nZIELGRUPPE: {target_audience}"
if dos:
section += f"\n\nEMPFOHLENE THEMEN-RICHTUNGEN:\n" + "\n".join([f" + {d}" for d in dos])
if donts:
section += f"\n\nZU VERMEIDENDE THEMEN:\n" + "\n".join([f" - {d}" for d in donts])
# Weight-specific closing instruction
if strategy_weight >= 0.8:
section += "\n\n**KRITISCH:** Alle Topics müssen zur Unternehmensstrategie passen - keine Ausnahmen!"
elif strategy_weight >= 0.6:
section += "\n\n**WICHTIG:** Stelle sicher, dass die Mehrheit der Topics die Unternehmenswerte widerspiegelt!"
elif strategy_weight >= 0.4:
section += "\n\n**BEACHTE:** Integriere die Strategie subtil - persönliche Expertise bleibt wichtig!"
else:
section += "\n\n**REFERENZ:** Die Strategie dient nur als Orientierung - persönliche Topics haben Vorrang!"
return section + "\n"
def _get_structure_prompt(
self,
raw_research: str,

View File

@@ -30,7 +30,8 @@ class WriterAgent(BaseAgent):
post_type_analysis: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = "",
company_strategy: Optional[Dict[str, Any]] = None
company_strategy: Optional[Dict[str, Any]] = None,
strategy_weight: float = 0.5
) -> str:
"""
Write a LinkedIn post.
@@ -46,6 +47,7 @@ class WriterAgent(BaseAgent):
post_type: Optional PostType object for type-specific writing
post_type_analysis: Optional analysis of the post type
company_strategy: Optional company strategy to consider during writing
strategy_weight: Strategy weight (0.0-1.0) controlling how strongly the company strategy influences post generation
Returns:
Written LinkedIn post
@@ -65,7 +67,8 @@ class WriterAgent(BaseAgent):
post_type_analysis=post_type_analysis,
user_thoughts=user_thoughts,
selected_hook=selected_hook,
company_strategy=company_strategy
company_strategy=company_strategy,
strategy_weight=strategy_weight
)
else:
logger.info(f"Writing initial post for topic: {topic.get('title', 'Unknown')}")
@@ -90,7 +93,8 @@ class WriterAgent(BaseAgent):
post_type_analysis=post_type_analysis,
user_thoughts=user_thoughts,
selected_hook=selected_hook,
company_strategy=company_strategy
company_strategy=company_strategy,
strategy_weight=strategy_weight
)
else:
return await self._write_single_draft(
@@ -102,7 +106,8 @@ class WriterAgent(BaseAgent):
post_type_analysis=post_type_analysis,
user_thoughts=user_thoughts,
selected_hook=selected_hook,
company_strategy=company_strategy
company_strategy=company_strategy,
strategy_weight=strategy_weight
)
def _select_example_posts(
@@ -236,7 +241,8 @@ class WriterAgent(BaseAgent):
post_type_analysis: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = "",
company_strategy: Optional[Dict[str, Any]] = None
company_strategy: Optional[Dict[str, Any]] = None,
strategy_weight: float = 0.5
) -> str:
"""
Generate multiple drafts and select the best one.
@@ -249,6 +255,7 @@ class WriterAgent(BaseAgent):
post_type: Optional PostType object
post_type_analysis: Optional post type analysis
company_strategy: Optional company strategy to consider
strategy_weight: Strategy weight (0.0-1.0) for company strategy influence
Returns:
Best selected draft
@@ -256,7 +263,7 @@ 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)
system_prompt = self._get_system_prompt(profile_analysis, example_posts, learned_lessons, post_type, post_type_analysis, company_strategy, strategy_weight)
# Generate drafts in parallel with different temperatures/approaches
draft_configs = [
@@ -500,7 +507,8 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
post_type_analysis: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = "",
company_strategy: Optional[Dict[str, Any]] = None
company_strategy: Optional[Dict[str, Any]] = None,
strategy_weight: float = 0.5
) -> str:
"""Write a single draft (original behavior)."""
# Select examples if not already selected
@@ -515,7 +523,7 @@ 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)
system_prompt = self._get_system_prompt(profile_analysis, selected_examples, learned_lessons, post_type, post_type_analysis, company_strategy, 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
@@ -536,7 +544,8 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
learned_lessons: Optional[Dict[str, Any]] = None,
post_type: Any = None,
post_type_analysis: Optional[Dict[str, Any]] = None,
company_strategy: Optional[Dict[str, Any]] = None
company_strategy: Optional[Dict[str, Any]] = None,
strategy_weight: float = 0.5
) -> str:
"""Get system prompt for writer - orientiert an bewährten n8n-Prompts."""
# Extract key profile information
@@ -788,13 +797,13 @@ Vermeide IMMER diese KI-typischen Muster:
- Zu perfekte, glatte Formulierungen - echte Menschen schreiben mit Ecken und Kanten
{lessons_section}
{post_type_section}
{self._get_company_strategy_section(company_strategy)}
{self._get_company_strategy_section(company_strategy, strategy_weight)}
DEIN AUFTRAG: Schreibe den Post so, dass er für die Zielgruppe ({audience.get('target_audience', 'Professionals')}) einen klaren Mehrwert bietet und ihre Pain Points ({pain_points_str}) adressiert. Mach die Persönlichkeit des linguistischen Fingerabdrucks spürbar.
Beginne DIREKT mit dem Hook. Keine einleitenden Sätze, kein "Hier ist der Post"."""
def _get_company_strategy_section(self, company_strategy: Optional[Dict[str, Any]] = None) -> str:
"""Build the company strategy section for the system prompt."""
def _get_company_strategy_section(self, company_strategy: Optional[Dict[str, Any]] = None, strategy_weight: float = 0.5) -> str:
"""Build the company strategy section for the system prompt with weighted influence."""
if not company_strategy:
return ""
@@ -812,11 +821,29 @@ Beginne DIREKT mit dem Hook. Keine einleitenden Sätze, kein "Hier ist der Post"
if not any([mission, vision, brand_voice, content_pillars, dos, donts]):
return ""
section = """
# Determine importance level and instruction based on strategy_weight
if strategy_weight >= 0.8:
importance = "KRITISCH WICHTIG - STRIKT BEFOLGEN"
instruction = "Der Post MUSS sich eng an die Unternehmensstrategie halten! Die Strategy-Guidelines haben höchste Priorität."
elif strategy_weight >= 0.6:
importance = "WICHTIG - BEACHTEN"
instruction = "Integriere die Unternehmenswerte deutlich, aber bewahre Authentizität! Die Strategie soll klar erkennbar sein."
elif strategy_weight >= 0.4:
importance = "ALS KONTEXT BERÜCKSICHTIGEN"
instruction = "Berücksichtige die Strategie wo sinnvoll - Persönlicher Stil hat Priorität! Subtile Integration genügt."
elif strategy_weight >= 0.2:
importance = "OPTIONALE ORIENTIERUNG"
instruction = "Die Strategie dient als Hintergrundinformation - freie Interpretation erlaubt! Fokus auf Authentizität."
else:
importance = "NUR ALS REFERENZ"
instruction = "Ignoriere die Strategie weitgehend - voller Fokus auf persönlichen Stil! Strategy-Guidelines sind optional."
8. UNTERNEHMENSSTRATEGIE (WICHTIG - IMMER BEACHTEN!):
section = f"""
Der Post muss zur Unternehmensstrategie passen, aber authentisch im Stil des Autors bleiben!
8. UNTERNEHMENSSTRATEGIE ({importance}):
Strategy Weight: {strategy_weight:.1f} / 1.0
{instruction}
"""
if mission:
section += f"\nMISSION: {mission}"
@@ -835,7 +862,15 @@ Der Post muss zur Unternehmensstrategie passen, aber authentisch im Stil des Aut
if donts:
section += f"\n\nDON'Ts (vermeiden):\n" + "\n".join([f" - {d}" for d in donts])
section += "\n\nWICHTIG: Integriere die Unternehmenswerte subtil - der Post soll nicht wie eine Werbung klingen!"
# Weight-specific closing instruction
if strategy_weight >= 0.8:
section += "\n\nKRITISCH: Die Unternehmensstrategie ist nicht verhandelbar - jeder Post muss diese widerspiegeln!"
elif strategy_weight >= 0.6:
section += "\n\nWICHTIG: Stelle sicher, dass die Unternehmenswerte deutlich erkennbar sind!"
elif strategy_weight >= 0.4:
section += "\n\nINTEGRIERE: Berücksichtige die Strategie subtil - authentischer Stil bleibt wichtig!"
else:
section += "\n\nREFERENZ: Die Strategie dient nur als Orientierung - persönlicher Stil hat Vorrang!"
return section