Major updates: LinkedIn auto-posting, timezone fixes, and Docker improvements

Features:
- Add LinkedIn OAuth integration and auto-posting functionality
- Add scheduler service for automated post publishing
- Add metadata field to generated_posts for LinkedIn URLs
- Add privacy policy page for LinkedIn API compliance
- Add company management features and employee accounts
- Add license key system for company registrations

Fixes:
- Fix timezone issues (use UTC consistently across app)
- Fix datetime serialization errors in database operations
- Fix scheduling timezone conversion (local time to UTC)
- Fix import errors (get_database -> db)

Infrastructure:
- Update Docker setup to use port 8001 (avoid conflicts)
- Add SSL support with nginx-proxy and Let's Encrypt
- Add LinkedIn setup documentation
- Add migration scripts for schema updates

Services:
- Add linkedin_service.py for LinkedIn API integration
- Add scheduler_service.py for background job processing
- Add storage_service.py for Supabase Storage
- Add email_service.py improvements
- Add encryption utilities for token storage

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 11:30:20 +01:00
parent b50594dbfa
commit f14515e9cf
94 changed files with 21601 additions and 5111 deletions

View File

@@ -1,12 +1,13 @@
"""Base agent class."""
import asyncio
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional, Tuple
from openai import OpenAI
import httpx
from loguru import logger
from src.config import settings
from src.config import settings, estimate_cost
from src.database.client import db
class BaseAgent(ABC):
@@ -21,13 +22,59 @@ class BaseAgent(ABC):
"""
self.name = name
self.openai_client = OpenAI(api_key=settings.openai_api_key)
self._usage_logs: List[Dict[str, Any]] = []
self._user_id: Optional[str] = None
self._company_id: Optional[str] = None
self._operation: str = "unknown"
logger.info(f"Initialized {name} agent")
def set_tracking_context(
self,
operation: str,
user_id: Optional[str] = None,
company_id: Optional[str] = None
):
"""Set context for usage tracking."""
self._operation = operation
self._user_id = user_id
self._company_id = company_id
self._usage_logs = []
@abstractmethod
async def process(self, *args, **kwargs) -> Any:
"""Process the agent's task."""
pass
async def _log_usage(self, provider: str, model: str, prompt_tokens: int, completion_tokens: int, total_tokens: int):
"""Log API usage to database."""
cost = estimate_cost(model, prompt_tokens, completion_tokens)
usage = {
"provider": provider,
"model": model,
"operation": self._operation,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
"estimated_cost_usd": cost
}
self._usage_logs.append(usage)
try:
from uuid import UUID
await db.log_api_usage(
provider=provider,
model=model,
operation=self._operation,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
estimated_cost_usd=cost,
user_id=UUID(self._user_id) if self._user_id else None,
company_id=UUID(self._company_id) if self._company_id else None
)
except Exception as e:
logger.warning(f"Failed to log usage to DB: {e}")
async def call_openai(
self,
system_prompt: str,
@@ -74,6 +121,16 @@ class BaseAgent(ABC):
result = response.choices[0].message.content
logger.debug(f"[{self.name}] Received response (length: {len(result)})")
# Track usage
if response.usage:
await self._log_usage(
provider="openai",
model=model,
prompt_tokens=response.usage.prompt_tokens,
completion_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens
)
return result
async def call_perplexity(
@@ -117,4 +174,15 @@ class BaseAgent(ABC):
content = result["choices"][0]["message"]["content"]
logger.debug(f"[{self.name}] Received Perplexity response (length: {len(content)})")
# Track usage
usage = result.get("usage", {})
if usage:
await self._log_usage(
provider="perplexity",
model=model,
prompt_tokens=usage.get("prompt_tokens", 0),
completion_tokens=usage.get("completion_tokens", 0),
total_tokens=usage.get("prompt_tokens", 0) + usage.get("completion_tokens", 0)
)
return content

View File

@@ -1,7 +1,7 @@
"""Research agent using Perplexity."""
import json
import random
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, List
from loguru import logger
@@ -258,7 +258,7 @@ Gib deine Antwort im folgenden JSON-Format zurück:
pain_points_text = ", ".join(pain_points) if pain_points else "Allgemeine Business-Probleme"
# Current date for time-specific searches
today = datetime.now()
today = datetime.now(timezone.utc)
date_str = today.strftime("%d. %B %Y")
week_ago = (today - timedelta(days=7)).strftime("%d. %B %Y")

View File

@@ -27,7 +27,10 @@ class WriterAgent(BaseAgent):
critic_result: Optional[Dict[str, Any]] = None,
learned_lessons: Optional[Dict[str, Any]] = None,
post_type: Any = None,
post_type_analysis: Optional[Dict[str, Any]] = None
post_type_analysis: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = "",
company_strategy: Optional[Dict[str, Any]] = None
) -> str:
"""
Write a LinkedIn post.
@@ -42,6 +45,7 @@ class WriterAgent(BaseAgent):
learned_lessons: Optional lessons learned from past critic feedback
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
Returns:
Written LinkedIn post
@@ -58,12 +62,19 @@ class WriterAgent(BaseAgent):
critic_result=critic_result,
learned_lessons=learned_lessons,
post_type=post_type,
post_type_analysis=post_type_analysis
post_type_analysis=post_type_analysis,
user_thoughts=user_thoughts,
selected_hook=selected_hook,
company_strategy=company_strategy
)
else:
logger.info(f"Writing initial post for topic: {topic.get('title', 'Unknown')}")
if post_type:
logger.info(f"Using post type: {post_type.name}")
if user_thoughts:
logger.info(f"Including user thoughts ({len(user_thoughts)} chars)")
if selected_hook:
logger.info(f"Using selected hook: {selected_hook[:50]}...")
# Select example posts - use semantic matching if enabled
selected_examples = self._select_example_posts(topic, example_posts, profile_analysis)
@@ -76,7 +87,10 @@ class WriterAgent(BaseAgent):
example_posts=selected_examples,
learned_lessons=learned_lessons,
post_type=post_type,
post_type_analysis=post_type_analysis
post_type_analysis=post_type_analysis,
user_thoughts=user_thoughts,
selected_hook=selected_hook,
company_strategy=company_strategy
)
else:
return await self._write_single_draft(
@@ -85,7 +99,10 @@ class WriterAgent(BaseAgent):
example_posts=selected_examples,
learned_lessons=learned_lessons,
post_type=post_type,
post_type_analysis=post_type_analysis
post_type_analysis=post_type_analysis,
user_thoughts=user_thoughts,
selected_hook=selected_hook,
company_strategy=company_strategy
)
def _select_example_posts(
@@ -164,10 +181,14 @@ class WriterAgent(BaseAgent):
# If we still don't have enough, fill with top scored
while len(selected) < 3 and len(selected) < len(example_posts):
found = False
for item in scored_posts:
if item["post"] not in selected:
selected.append(item["post"])
found = True
break
if not found:
break # no more unique posts available
logger.info(f"Selected {len(selected)} example posts via semantic matching")
return selected
@@ -212,7 +233,10 @@ class WriterAgent(BaseAgent):
example_posts: List[str],
learned_lessons: Optional[Dict[str, Any]] = None,
post_type: Any = None,
post_type_analysis: Optional[Dict[str, Any]] = None
post_type_analysis: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = "",
company_strategy: Optional[Dict[str, Any]] = None
) -> str:
"""
Generate multiple drafts and select the best one.
@@ -224,6 +248,7 @@ class WriterAgent(BaseAgent):
learned_lessons: Lessons learned from past feedback
post_type: Optional PostType object
post_type_analysis: Optional post type analysis
company_strategy: Optional company strategy to consider
Returns:
Best selected draft
@@ -231,7 +256,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)
system_prompt = self._get_system_prompt(profile_analysis, example_posts, learned_lessons, post_type, post_type_analysis, company_strategy)
# Generate drafts in parallel with different temperatures/approaches
draft_configs = [
@@ -244,7 +269,7 @@ class WriterAgent(BaseAgent):
# Create draft tasks
async def generate_draft(config: Dict, draft_num: int) -> Dict[str, Any]:
user_prompt = self._get_user_prompt_for_draft(topic, draft_num, config["approach"])
user_prompt = self._get_user_prompt_for_draft(topic, draft_num, config["approach"], user_thoughts, selected_hook)
try:
draft = await self.call_openai(
system_prompt=system_prompt,
@@ -286,7 +311,9 @@ class WriterAgent(BaseAgent):
self,
topic: Dict[str, Any],
draft_num: int,
approach: str
approach: str,
user_thoughts: str = "",
selected_hook: str = ""
) -> str:
"""Get user prompt with slight variations for different drafts."""
# Different emphasis for each draft
@@ -305,8 +332,11 @@ class WriterAgent(BaseAgent):
if topic.get('angle'):
angle_section = f"\n**ANGLE/PERSPEKTIVE:**\n{topic.get('angle')}\n"
# User-selected hook takes priority
hook_section = ""
if topic.get('hook_idea'):
if selected_hook:
hook_section = f"\n**HOOK (VERWENDE DIESEN ALS EINSTIEG!):**\n\"{selected_hook}\"\n"
elif topic.get('hook_idea'):
hook_section = f"\n**HOOK-IDEE (als Inspiration):**\n\"{topic.get('hook_idea')}\"\n"
facts_section = ""
@@ -318,6 +348,34 @@ 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"
# User thoughts section
thoughts_section = ""
if user_thoughts:
thoughts_section = f"""
**PERSÖNLICHE GEDANKEN (UNBEDINGT einbauen!):**
{user_thoughts}
Diese Gedanken MÜSSEN im Post verarbeitet werden!
"""
# Adjust task based on hook
if selected_hook:
task_text = """**AUFGABE:**
Schreibe einen authentischen LinkedIn-Post, der:
1. Mit dem VORGEGEBENEN HOOK beginnt (exakt oder leicht angepasst)
2. Den Fakt/das Thema aufgreift und Mehrwert bietet
3. Die persönlichen Gedanken organisch einbaut
4. Die Key Facts verwendet wo es passt
5. Mit einem passenden CTA endet"""
else:
task_text = """**AUFGABE:**
Schreibe einen authentischen LinkedIn-Post, der:
1. Mit einem STARKEN, unerwarteten Hook beginnt (nutze die Hook-Idee als Inspiration, NICHT wörtlich!)
2. Den Fakt/das Thema aufgreift und Mehrwert bietet
3. Die persönlichen Gedanken organisch einbaut (falls vorhanden)
4. Die Key Facts einbaut wo es passt
5. Mit einem passenden CTA endet"""
return f"""Schreibe einen LinkedIn-Post zu folgendem Thema:
**THEMA:** {topic.get('title', 'Unbekanntes Thema')}
@@ -326,26 +384,19 @@ class WriterAgent(BaseAgent):
{angle_section}{hook_section}
**KERN-FAKT / INHALT:**
{topic.get('fact', topic.get('description', ''))}
{facts_section}
{facts_section}{thoughts_section}
**WARUM RELEVANT:**
{topic.get('relevance', 'Aktuelles Thema für die Zielgruppe')}
{why_section}
**DEIN ANSATZ FÜR DIESEN ENTWURF ({approach}):**
{emphasis}
**AUFGABE:**
Schreibe einen authentischen LinkedIn-Post, der:
1. Mit einem STARKEN, unerwarteten Hook beginnt (nutze die Hook-Idee als Inspiration, NICHT wörtlich!)
2. Den Fakt/das Thema aufgreift und Mehrwert bietet
3. Die Key Facts einbaut wo es passt
4. Eine persönliche Note oder Meinung enthält
5. Mit einem passenden CTA endet
{task_text}
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
- Die Hook-Idee ist nur INSPIRATION - mach etwas Eigenes daraus!
Gib NUR den fertigen Post zurück."""
@@ -446,7 +497,10 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
critic_result: Optional[Dict[str, Any]] = None,
learned_lessons: Optional[Dict[str, Any]] = None,
post_type: Any = None,
post_type_analysis: Optional[Dict[str, Any]] = None
post_type_analysis: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = "",
company_strategy: Optional[Dict[str, Any]] = None
) -> str:
"""Write a single draft (original behavior)."""
# Select examples if not already selected
@@ -461,8 +515,8 @@ 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)
user_prompt = self._get_user_prompt(topic, feedback, previous_version, critic_result)
system_prompt = self._get_system_prompt(profile_analysis, selected_examples, learned_lessons, post_type, post_type_analysis, company_strategy)
user_prompt = self._get_user_prompt(topic, feedback, previous_version, critic_result, user_thoughts, selected_hook)
# Lower temperature for more consistent style matching
post = await self.call_openai(
@@ -481,7 +535,8 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
example_posts: List[str] = None,
learned_lessons: Optional[Dict[str, Any]] = None,
post_type: Any = None,
post_type_analysis: Optional[Dict[str, Any]] = None
post_type_analysis: Optional[Dict[str, Any]] = None,
company_strategy: Optional[Dict[str, Any]] = None
) -> str:
"""Get system prompt for writer - orientiert an bewährten n8n-Prompts."""
# Extract key profile information
@@ -670,16 +725,65 @@ 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)}
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."""
if not company_strategy:
return ""
# Extract strategy elements
mission = company_strategy.get("mission", "")
vision = company_strategy.get("vision", "")
brand_voice = company_strategy.get("brand_voice", "")
tone_guidelines = company_strategy.get("tone_guidelines", "")
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, brand_voice, content_pillars, dos, donts]):
return ""
section = """
8. UNTERNEHMENSSTRATEGIE (WICHTIG - IMMER BEACHTEN!):
Der Post muss zur Unternehmensstrategie passen, aber authentisch im Stil des Autors bleiben!
"""
if mission:
section += f"\nMISSION: {mission}"
if vision:
section += f"\nVISION: {vision}"
if brand_voice:
section += f"\nBRAND VOICE: {brand_voice}"
if tone_guidelines:
section += f"\nTONE GUIDELINES: {tone_guidelines}"
if content_pillars:
section += f"\nCONTENT PILLARS: {', '.join(content_pillars)}"
if target_audience:
section += f"\nZIELGRUPPE: {target_audience}"
if dos:
section += f"\n\nDO's (empfohlen):\n" + "\n".join([f" + {d}" for d in dos])
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!"
return section
def _get_user_prompt(
self,
topic: Dict[str, Any],
feedback: Optional[str] = None,
previous_version: Optional[str] = None,
critic_result: Optional[Dict[str, Any]] = None
critic_result: Optional[Dict[str, Any]] = None,
user_thoughts: str = "",
selected_hook: str = ""
) -> str:
"""Get user prompt for writer."""
if feedback and previous_version:
@@ -727,8 +831,11 @@ Gib NUR den überarbeiteten Post zurück - keine Kommentare."""
if topic.get('angle'):
angle_section = f"\n**ANGLE/PERSPEKTIVE:**\n{topic.get('angle')}\n"
# User-selected hook takes priority over topic hook_idea
hook_section = ""
if topic.get('hook_idea'):
if selected_hook:
hook_section = f"\n**HOOK (VERWENDE DIESEN ALS EINSTIEG!):**\n\"{selected_hook}\"\n"
elif topic.get('hook_idea'):
hook_section = f"\n**HOOK-IDEE (als Inspiration):**\n\"{topic.get('hook_idea')}\"\n"
facts_section = ""
@@ -736,6 +843,34 @@ Gib NUR den überarbeiteten Post zurück - keine Kommentare."""
if key_facts and isinstance(key_facts, list) and len(key_facts) > 0:
facts_section = "\n**KEY FACTS (nutze diese!):**\n" + "\n".join([f"- {f}" for f in key_facts]) + "\n"
# User thoughts section
thoughts_section = ""
if user_thoughts:
thoughts_section = f"""
**PERSÖNLICHE GEDANKEN (UNBEDINGT einbauen!):**
{user_thoughts}
Diese Gedanken MÜSSEN im Post verarbeitet werden - sie spiegeln die echte Meinung der Person wider!
"""
# Adjust task based on whether hook is provided
if selected_hook:
task_section = """**AUFGABE:**
Schreibe einen authentischen LinkedIn-Post, der:
1. Mit dem VORGEGEBENEN HOOK beginnt (exakt oder leicht angepasst)
2. Den Fakt/das Thema aufgreift und Mehrwert bietet
3. Die persönlichen Gedanken organisch einbaut
4. Die Key Facts verwendet wo es passt
5. Mit einem passenden CTA endet"""
else:
task_section = """**AUFGABE:**
Schreibe einen authentischen LinkedIn-Post, der:
1. Mit einem STARKEN, unerwarteten Hook beginnt (nutze Hook-Idee als Inspiration!)
2. Den Fakt/das Thema aufgreift und Mehrwert bietet
3. Die persönlichen Gedanken organisch einbaut (falls vorhanden)
4. Die Key Facts verwendet wo es passt
5. Mit einem passenden CTA endet"""
return f"""Schreibe einen LinkedIn-Post zu folgendem Thema:
**THEMA:** {topic.get('title', 'Unbekanntes Thema')}
@@ -744,17 +879,11 @@ Gib NUR den überarbeiteten Post zurück - keine Kommentare."""
{angle_section}{hook_section}
**KERN-FAKT / INHALT:**
{topic.get('fact', topic.get('description', ''))}
{facts_section}
{facts_section}{thoughts_section}
**WARUM RELEVANT:**
{topic.get('relevance', 'Aktuelles Thema für die Zielgruppe')}
**AUFGABE:**
Schreibe einen authentischen LinkedIn-Post, der:
1. Mit einem STARKEN, unerwarteten Hook beginnt (nutze Hook-Idee als Inspiration!)
2. Den Fakt/das Thema aufgreift und Mehrwert bietet
3. Die Key Facts einbaut wo es passt
4. Eine persönliche Note oder Meinung enthält
5. Mit einem passenden CTA endet
{task_section}
WICHTIG:
- Vermeide KI-typische Formulierungen ("In der heutigen Zeit", "Tauchen Sie ein", etc.)
@@ -762,3 +891,263 @@ WICHTIG:
- Der Post soll SOFORT 85+ Punkte im Review erreichen
Gib NUR den fertigen Post zurück."""
async def generate_hooks(
self,
topic: Dict[str, Any],
profile_analysis: Dict[str, Any],
user_thoughts: str = "",
post_type: Any = None
) -> List[Dict[str, str]]:
"""
Generate 4 different hook options for a topic.
Args:
topic: Topic dictionary with title, fact, etc.
profile_analysis: Profile analysis for style matching
user_thoughts: User's personal thoughts about the topic
post_type: Optional PostType object
Returns:
List of {"hook": "...", "style": "..."} dictionaries
"""
logger.info(f"Generating hooks for topic: {topic.get('title', 'Unknown')}")
# Extract style info from profile
tone = profile_analysis.get('tone_analysis', {}).get('primary_tone', 'Professionell')
energy = profile_analysis.get('linguistic_fingerprint', {}).get('energy_level', 7)
address = profile_analysis.get('writing_style', {}).get('form_of_address', 'Du')
hook_phrases = profile_analysis.get('phrase_library', {}).get('hook_phrases', [])
# Build example hooks section
example_hooks = ""
if hook_phrases:
example_hooks = "\n\nBeispiel-Hooks dieser Person (zur Inspiration):\n" + "\n".join([f"- {h}" for h in hook_phrases[:5]])
system_prompt = f"""Du bist ein Hook-Spezialist für LinkedIn Posts. Deine Aufgabe ist es, 4 verschiedene, aufmerksamkeitsstarke Hooks zu generieren.
STIL DER PERSON:
- Tonalität: {tone}
- Energie-Level: {energy}/10 (höher = emotionaler, niedriger = sachlicher)
- Ansprache: {address}
{example_hooks}
GENERIERE 4 VERSCHIEDENE HOOKS:
1. **Provokant** - Eine kontroverse These oder überraschende Aussage
2. **Storytelling** - Beginn einer persönlichen Geschichte oder Anekdote
3. **Fakten-basiert** - Eine überraschende Statistik oder Fakt
4. **Neugier-weckend** - Eine Frage oder ein Cliffhanger
REGELN:
- Jeder Hook sollte 1-2 Sätze lang sein
- Hooks müssen zum Energie-Level und Ton der Person passen
- Keine KI-typischen Phrasen ("In der heutigen Zeit", "Stellen Sie sich vor")
- Die Hooks sollen SOFORT Aufmerksamkeit erregen
- Bei Energie 8+ darf es emotional/leidenschaftlich sein
- Bei Energie 5-7 eher sachlich-professionell
Antworte im JSON-Format:
{{"hooks": [
{{"hook": "Der Hook-Text hier", "style": "Provokant"}},
{{"hook": "Der Hook-Text hier", "style": "Storytelling"}},
{{"hook": "Der Hook-Text hier", "style": "Fakten-basiert"}},
{{"hook": "Der Hook-Text hier", "style": "Neugier-weckend"}}
]}}"""
# Build user prompt
thoughts_section = ""
if user_thoughts:
thoughts_section = f"\n\nPERSÖNLICHE GEDANKEN DER PERSON ZUM THEMA:\n{user_thoughts}\n\nIntegriere diese Perspektive in die Hooks!"
post_type_section = ""
if post_type:
post_type_section = f"\n\nPOST-TYP: {post_type.name}"
if post_type.description:
post_type_section += f"\n{post_type.description}"
user_prompt = f"""Generiere 4 Hooks für dieses Thema:
THEMA: {topic.get('title', 'Unbekanntes Thema')}
KATEGORIE: {topic.get('category', 'Allgemein')}
KERN-FAKT/INHALT:
{topic.get('fact', topic.get('description', 'Keine Details verfügbar'))}
{thoughts_section}{post_type_section}
Generiere jetzt die 4 verschiedenen Hooks im JSON-Format."""
response = await self.call_openai(
system_prompt=system_prompt,
user_prompt=user_prompt,
model="gpt-4o-mini",
temperature=0.8,
response_format={"type": "json_object"}
)
try:
result = json.loads(response)
hooks = result.get("hooks", [])
logger.info(f"Generated {len(hooks)} hooks successfully")
return hooks
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Failed to parse hooks response: {e}")
# Return fallback hooks
return [
{"hook": f"Was wäre, wenn {topic.get('title', 'dieses Thema')} alles verändert?", "style": "Neugier-weckend"},
{"hook": f"Letzte Woche habe ich etwas über {topic.get('title', 'dieses Thema')} gelernt, das mich nicht mehr loslässt.", "style": "Storytelling"},
{"hook": f"Die meisten unterschätzen {topic.get('title', 'dieses Thema')}. Ein Fehler.", "style": "Provokant"},
{"hook": f"Eine Zahl hat mich diese Woche überrascht: {topic.get('fact', 'Ein überraschender Fakt')}.", "style": "Fakten-basiert"}
]
async def generate_improvement_suggestions(
self,
post_content: str,
profile_analysis: Dict[str, Any],
critic_feedback: Optional[Dict[str, Any]] = None
) -> List[Dict[str, str]]:
"""
Generate improvement suggestions for an existing post based on critic feedback.
Args:
post_content: The current post content
profile_analysis: Profile analysis for style matching
critic_feedback: Optional feedback from the critic with improvements list
Returns:
List of {"label": "...", "action": "..."} dictionaries
"""
logger.info("Generating improvement suggestions for post")
# Extract style info from profile
tone = profile_analysis.get('tone_analysis', {}).get('primary_tone', 'Professionell')
energy = profile_analysis.get('linguistic_fingerprint', {}).get('energy_level', 7)
address = profile_analysis.get('writing_style', {}).get('form_of_address', 'Du')
# Build feedback context
feedback_context = ""
if critic_feedback:
feedback_text = critic_feedback.get('feedback', '')
improvements = critic_feedback.get('improvements', [])
strengths = critic_feedback.get('strengths', [])
score = critic_feedback.get('overall_score', 0)
feedback_context = f"""
LETZTES KRITIKER-FEEDBACK (Score: {score}/100):
{feedback_text}
IDENTIFIZIERTE VERBESSERUNGSPUNKTE:
{chr(10).join(['- ' + imp for imp in improvements]) if improvements else '- Keine spezifischen Verbesserungen genannt'}
STÄRKEN (diese beibehalten!):
{chr(10).join(['- ' + s for s in strengths]) if strengths else '- Keine spezifischen Stärken genannt'}
"""
system_prompt = f"""Du generierst kurze, klickbare Verbesserungsvorschläge für LinkedIn-Posts.
PROFIL DER PERSON:
- Tonalität: {tone}
- Energie: {energy}/10
- Ansprache: {address}
{feedback_context}
DEINE AUFGABE:
Generiere 4 KURZE Verbesserungsvorschläge (max 5-6 Worte pro Label).
Jeder Vorschlag muss:
1. Auf dem Kritiker-Feedback basieren (falls vorhanden)
2. Zum Stil der Person passen
3. Eine konkrete, umsetzbare Änderung beschreiben
FORMAT - Kurze Labels wie:
- "Hook emotionaler machen"
- "Persönliche Anekdote einbauen"
- "Absätze kürzen"
- "Call-to-Action verstärken"
Antworte im JSON-Format:
{{"suggestions": [
{{"label": "Kurzes Label (max 6 Worte)", "action": "Detaillierte Anweisung für die KI, was genau geändert werden soll"}},
...
]}}"""
user_prompt = f"""POST:
{post_content[:1500]}
Generiere 4 kurze, spezifische Verbesserungsvorschläge basierend auf dem Feedback und Profil."""
response = await self.call_openai(
system_prompt=system_prompt,
user_prompt=user_prompt,
model="gpt-4o-mini",
temperature=0.7,
response_format={"type": "json_object"}
)
try:
result = json.loads(response)
suggestions = result.get("suggestions", [])
logger.info(f"Generated {len(suggestions)} improvement suggestions")
return suggestions
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Failed to parse suggestions response: {e}")
return [
{"label": "Hook verstärken", "action": "Mache den ersten Satz emotionaler und aufmerksamkeitsstärker"},
{"label": "Storytelling einbauen", "action": "Füge eine kurze persönliche Anekdote oder Erfahrung hinzu"},
{"label": "Call-to-Action ergänzen", "action": "Füge am Ende eine Frage oder Handlungsaufforderung hinzu"},
{"label": "Struktur verbessern", "action": "Kürze lange Absätze und verbessere die Lesbarkeit"}
]
async def apply_suggestion(
self,
post_content: str,
suggestion: str,
profile_analysis: Dict[str, Any]
) -> str:
"""
Apply a specific improvement suggestion to a post.
Args:
post_content: The current post content
suggestion: The suggestion to apply
profile_analysis: Profile analysis for style matching
Returns:
The improved post content
"""
logger.info(f"Applying suggestion to post: {suggestion[:50]}...")
# Extract style info from profile
tone = profile_analysis.get('tone_analysis', {}).get('primary_tone', 'Professionell')
energy = profile_analysis.get('linguistic_fingerprint', {}).get('energy_level', 7)
address = profile_analysis.get('writing_style', {}).get('form_of_address', 'Du')
system_prompt = f"""Du bist ein LinkedIn-Experte. Deine Aufgabe ist es, einen Post basierend auf einem konkreten Verbesserungsvorschlag zu überarbeiten.
STIL DER PERSON:
- Tonalität: {tone}
- Energie-Level: {energy}/10
- Ansprache: {address}
WICHTIG:
- Behalte den Kern und die Hauptaussage des Posts bei
- Wende NUR die angegebene Verbesserung an
- Behalte die Länge ungefähr bei
- Antworte NUR mit dem überarbeiteten Post, keine Erklärungen"""
user_prompt = f"""ORIGINAL-POST:
{post_content}
ANZUWENDENDE VERBESSERUNG:
{suggestion}
Schreibe jetzt den überarbeiteten Post:"""
response = await self.call_openai(
system_prompt=system_prompt,
user_prompt=user_prompt,
model="gpt-4o-mini",
temperature=0.5
)
logger.info("Successfully applied suggestion to post")
return response.strip()