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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user