Files
Onyva-Postling/src/agents/researcher.py
Ruben Fischer f14515e9cf 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>
2026-02-11 11:30:20 +01:00

631 lines
25 KiB
Python

"""Research agent using Perplexity."""
import json
import random
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, List
from loguru import logger
from src.agents.base import BaseAgent
class ResearchAgent(BaseAgent):
"""Agent for researching new content topics using Perplexity."""
def __init__(self):
"""Initialize research agent."""
super().__init__("Researcher")
async def process(
self,
profile_analysis: Dict[str, Any],
existing_topics: List[str],
customer_data: Dict[str, Any],
example_posts: List[str] = None,
post_type: Any = None,
post_type_analysis: Dict[str, Any] = None
) -> Dict[str, Any]:
"""
Research new content topics.
Args:
profile_analysis: Profile analysis results
existing_topics: List of already covered topics
customer_data: Customer data (contains persona, style_guide, etc.)
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
Returns:
Research results with suggested topics
"""
logger.info("Starting research for new content topics")
if post_type:
logger.info(f"Targeting research for post type: {post_type.name}")
# Extract key information from profile analysis
audience_insights = profile_analysis.get("audience_insights", {})
topic_patterns = profile_analysis.get("topic_patterns", {})
industry = audience_insights.get("industry_context", "Business")
target_audience = audience_insights.get("target_audience", "Professionals")
content_pillars = topic_patterns.get("content_pillars", [])
pain_points = audience_insights.get("pain_points_addressed", [])
value_proposition = audience_insights.get("value_proposition", "")
# Extract customer-specific data
persona = customer_data.get("persona", "") if customer_data else ""
# STEP 1: Use Perplexity for REAL internet research (has live data!)
logger.info("Step 1: Researching with Perplexity (live internet data)")
perplexity_prompt = self._get_perplexity_prompt(
industry=industry,
target_audience=target_audience,
content_pillars=content_pillars,
existing_topics=existing_topics,
pain_points=pain_points,
persona=persona
)
# Dynamic system prompt for variety
system_prompts = [
"Du bist ein investigativer Journalist. Finde die neuesten, spannendsten Entwicklungen mit harten Fakten.",
"Du bist ein Branchen-Analyst. Identifiziere aktuelle Trends und Marktbewegungen mit konkreten Daten.",
"Du bist ein Trend-Scout. Spüre auf, was diese Woche wirklich neu und relevant ist.",
"Du bist ein Research-Spezialist. Finde aktuelle Studien, Statistiken und News mit Quellenangaben."
]
raw_research = await self.call_perplexity(
system_prompt=random.choice(system_prompts),
user_prompt=perplexity_prompt,
model="sonar-pro"
)
logger.info("Step 2: Transforming research into personalized topic ideas")
# STEP 2: Transform raw research into PERSONALIZED topic suggestions
transform_prompt = self._get_transform_prompt(
raw_research=raw_research,
target_audience=target_audience,
persona=persona,
content_pillars=content_pillars,
example_posts=example_posts or [],
existing_topics=existing_topics,
post_type=post_type,
post_type_analysis=post_type_analysis
)
response = await self.call_openai(
system_prompt=self._get_topic_creator_system_prompt(),
user_prompt=transform_prompt,
model="gpt-4o",
temperature=0.7, # Higher for creative topic angles
response_format={"type": "json_object"}
)
# Parse JSON response
result = json.loads(response)
suggested_topics = result.get("topics", [])
# STEP 3: Ensure diversity - filter out similar topics
suggested_topics = self._ensure_diversity(suggested_topics)
# Parse research results
research_results = {
"raw_response": response,
"suggested_topics": suggested_topics,
"industry": industry,
"target_audience": target_audience
}
logger.info(f"Research completed with {len(research_results['suggested_topics'])} topic suggestions")
return research_results
def _get_topic_creator_system_prompt(self) -> str:
"""Get system prompt for transforming research into personalized topics."""
return """Du bist ein LinkedIn Content-Stratege, der aus Recherche-Ergebnissen KONKRETE, PERSONALISIERTE Themenvorschläge erstellt.
WICHTIG: Du erstellst KEINE Schlagzeilen oder News-Titel!
Du erstellst KONKRETE CONTENT-IDEEN mit:
- Einem klaren ANGLE (Perspektive/Blickwinkel)
- Einer konkreten HOOK-IDEE
- Einem NARRATIV das die Person erzählen könnte
Der Unterschied:
❌ SCHLECHT (Schlagzeile): "KI verändert den Arbeitsmarkt"
✅ GUT (Themenvorschlag): "Warum ich als [Rolle] plötzlich 50% meiner Zeit mit KI-Prompts verbringe - und was das für mein Team bedeutet"
❌ SCHLECHT: "Neue Studie zu Remote Work"
✅ GUT: "3 Erkenntnisse aus der Stanford Remote-Studie, die mich als Führungskraft überrascht haben"
❌ SCHLECHT: "Fachkräftemangel in der IT"
✅ GUT: "Unpopuläre Meinung: Wir haben keinen Fachkräftemangel - wir haben ein Ausbildungsproblem. Hier ist was ich damit meine..."
Deine Themenvorschläge müssen:
1. ZUR PERSON PASSEN - Klingt wie etwas das diese spezifische Person posten würde
2. EINEN KONKRETEN ANGLE HABEN - Nicht "über X schreiben" sondern "diesen spezifischen Aspekt von X aus dieser Perspektive beleuchten"
3. EINEN HOOK VORSCHLAGEN - Eine konkrete Idee wie der Post starten könnte
4. HINTERGRUND-INFOS LIEFERN - Fakten/Daten aus der Recherche die die Person nutzen kann
5. ABWECHSLUNGSREICH SEIN - Verschiedene Formate und Kategorien
Antworte als JSON."""
def _get_system_prompt(self) -> str:
"""Get system prompt for research (legacy, kept for compatibility)."""
return """Du bist ein hochspezialisierter Trend-Analyst und Content-Researcher.
Deine Mission ist es, aktuelle, hochrelevante Content-Themen für LinkedIn zu identifizieren.
Du sollst:
1. Aktuelle Trends, News und Diskussionen der letzten 7-14 Tage recherchieren
2. Themen finden, die für die spezifische Zielgruppe relevant sind
3. Verschiedene Kategorien abdecken:
- Aktuelle News & Studien
- Schmerzpunkt-Lösungen
- Konträre Trends (gegen Mainstream-Meinung)
- Emerging Topics
Für jedes Thema sollst du bereitstellen:
- Einen prägnanten Titel
- Den Kern-Fakt (mit Daten, Quellen, Beispielen)
- Warum es relevant ist für die Zielgruppe
- Die Kategorie
Fokussiere dich auf Themen, die:
- AKTUELL sind (letzte 1-2 Wochen)
- KONKRET sind (mit Daten/Fakten belegt)
- RELEVANT sind für die Zielgruppe
- UNIQUE sind (nicht bereits behandelt)
Gib deine Antwort als JSON zurück."""
def _get_user_prompt(
self,
industry: str,
target_audience: str,
content_pillars: List[str],
existing_topics: List[str],
pain_points: List[str] = None,
value_proposition: str = "",
persona: str = ""
) -> str:
"""Get user prompt for research."""
pillars_text = ", ".join(content_pillars) if content_pillars else "Verschiedene Business-Themen"
existing_text = ", ".join(existing_topics[:20]) if existing_topics else "Keine"
pain_points_text = ", ".join(pain_points) if pain_points else "Nicht spezifiziert"
# Build persona section if available
persona_section = ""
if persona:
persona_section = f"""
**PERSONA DER PERSON (WICHTIG - Themen müssen zu dieser Expertise passen!):**
{persona[:800]}
"""
return f"""Recherchiere aktuelle LinkedIn-Content-Themen für folgendes Profil:
**KONTEXT:**
- Branche: {industry}
- Zielgruppe: {target_audience}
- Content-Säulen: {pillars_text}
- Pain Points der Zielgruppe: {pain_points_text}
- Value Proposition: {value_proposition or 'Mehrwert für die Zielgruppe bieten'}
{persona_section}
**BEREITS BEHANDELTE THEMEN (diese NICHT vorschlagen):**
{existing_text}
**AUFGABE:**
Finde 5-7 verschiedene aktuelle Themen, die:
1. ZUR EXPERTISE/PERSONA der Person passen
2. Die PAIN POINTS der Zielgruppe addressieren
3. AUTHENTISCH von dieser Person kommen könnten
4. NICHT generisch oder beliebig sind
Kategorien:
1. **News-Flash**: Aktuelle Nachrichten, Studien oder Entwicklungen
2. **Schmerzpunkt-Löser**: Probleme/Diskussionen, die die Zielgruppe aktuell beschäftigen
3. **Konträrer Trend**: Entwicklungen, die gegen die herkömmliche Meinung verstoßen
4. **Emerging Topic**: Neue Trends, die gerade an Fahrt gewinnen
WICHTIG: Themen müssen zur Person passen! Ein Experte für {industry} würde keine generischen "Productivity-Tips" posten, sondern spezifische Insights aus seinem Fachgebiet.
Fokus auf deutsche/DACH-Region relevante Themen.
Gib deine Antwort im folgenden JSON-Format zurück:
{{
"topics": [
{{
"title": "Prägnanter Arbeitstitel (spezifisch, nicht generisch!)",
"category": "News-Flash / Schmerzpunkt-Löser / Konträrer Trend / Emerging Topic",
"fact": "Detaillierte Zusammenfassung mit Daten, Fakten, Beispielen - SPEZIFISCH für diese Branche",
"relevance": "Warum ist das für {target_audience} wichtig und warum sollte DIESE Person darüber schreiben?",
"source": "Quellenangaben (Studien, Artikel, Statistiken)"
}}
]
}}"""
def _get_perplexity_prompt(
self,
industry: str,
target_audience: str,
content_pillars: List[str],
existing_topics: List[str],
pain_points: List[str] = None,
persona: str = ""
) -> str:
"""Get prompt for Perplexity research (optimized for live internet search)."""
pillars_text = ", ".join(content_pillars) if content_pillars else "Business-Themen"
existing_text = ", ".join(existing_topics[:20]) if existing_topics else "Keine bisherigen Themen"
pain_points_text = ", ".join(pain_points) if pain_points else "Allgemeine Business-Probleme"
# Current date for time-specific searches
today = datetime.now(timezone.utc)
date_str = today.strftime("%d. %B %Y")
week_ago = (today - timedelta(days=7)).strftime("%d. %B %Y")
persona_hint = ""
if persona:
persona_hint = f"\nEXPERTISE DER PERSON: {persona[:600]}\n"
# Randomize the research focus for variety
research_angles = [
{
"name": "Breaking News & Studien",
"focus": "Suche nach brandneuen Studien, Reports, Umfragen oder Nachrichten",
"examples": "Neue Statistiken, Forschungsergebnisse, Unternehmens-Announcements"
},
{
"name": "Kontroverse & Debatten",
"focus": "Suche nach aktuellen Kontroversen, Meinungsverschiedenheiten, heißen Diskussionen",
"examples": "Polarisierende Meinungen, Kritik an Trends, unerwartete Entwicklungen"
},
{
"name": "Technologie & Innovation",
"focus": "Suche nach neuen Tools, Technologien, Methoden die gerade aufkommen",
"examples": "Neue Software, AI-Entwicklungen, Prozess-Innovationen"
},
{
"name": "Markt & Wirtschaft",
"focus": "Suche nach wirtschaftlichen Entwicklungen, Marktveränderungen, Branchen-Shifts",
"examples": "Fusionen, Insolvenzen, Markteintritt, Regulierungen"
},
{
"name": "Menschen & Karriere",
"focus": "Suche nach Personalien, Karriere-Trends, Arbeitsmarkt-Entwicklungen",
"examples": "Führungswechsel, Hiring-Trends, Remote Work Updates, Skill-Demands"
},
{
"name": "Fails & Learnings",
"focus": "Suche nach öffentlichen Fehlern, Shitstorms, Lessons Learned",
"examples": "PR-Desaster, gescheiterte Launches, öffentliche Kritik"
}
]
# Pick 3-4 random angles for this research session
selected_angles = random.sample(research_angles, min(4, len(research_angles)))
angles_text = "\n".join([
f"- **{angle['name']}**: {angle['focus']} (z.B. {angle['examples']})"
for angle in selected_angles
])
# Random seed words for more variety
seed_variations = [
f"Was ist DIESE WOCHE ({week_ago} bis {date_str}) passiert in {industry}?",
f"Welche BREAKING NEWS gibt es HEUTE ({date_str}) oder diese Woche in {industry}?",
f"Was diskutiert die {industry}-Branche AKTUELL ({date_str})?",
f"Welche NEUEN Entwicklungen gibt es seit {week_ago} in {industry}?"
]
seed_question = random.choice(seed_variations)
return f"""AKTUELLES DATUM: {date_str}
{seed_question}
{persona_hint}
KONTEXT:
- Branche: {industry}
- Zielgruppe: {target_audience}
- Themen-Fokus: {pillars_text}
- Pain Points: {pain_points_text}
RECHERCHE-SCHWERPUNKTE FÜR DIESE SESSION:
{angles_text}
⛔ BEREITS BEHANDELTE THEMEN - NICHT NOCHMAL VORSCHLAGEN:
{existing_text}
=== DEINE AUFGABE ===
Recherchiere FAKTEN, DATEN und ENTWICKLUNGEN - keine fertigen Themenvorschläge!
Ich brauche ROHDATEN die ich dann in personalisierte Content-Ideen umwandeln kann.
Für jede Entwicklung/News sammle:
1. **Was genau ist passiert?** - Konkrete Fakten, nicht Interpretationen
2. **Zahlen & Daten** - Statistiken, Prozentsätze, Beträge, Veränderungen
3. **Wer ist beteiligt?** - Unternehmen, Personen, Organisationen
4. **Wann?** - Genaues Datum oder Zeitraum
5. **Quelle** - URL oder Publikationsname
6. **Kontext** - Warum ist das relevant? Was bedeutet es?
SUCHE NACH:
✅ Neue Studien/Reports mit konkreten Zahlen
✅ Unternehmens-Entscheidungen oder -Ankündigungen
✅ Marktveränderungen mit Daten
✅ Gesetzliche/Regulatorische Änderungen
✅ Kontroverse Aussagen von Branchenführern
✅ Überraschende Statistiken oder Trends
✅ Gescheiterte Projekte oder unerwartete Erfolge
FORMAT DEINER ANTWORT:
Liefere 8-10 verschiedene Entwicklungen/News mit möglichst vielen Fakten und Zahlen.
Formatiere sie klar und strukturiert.
QUALITÄTSKRITERIEN:
✅ AKTUALITÄT: Von dieser Woche oder letzter Woche
✅ KONKRETHEIT: Echte Zahlen, Namen, Daten (nicht "Experten sagen...")
✅ VERIFIZIERBARKEIT: Echte Quelle die man prüfen kann
✅ BRANCHENRELEVANZ: Spezifisch für {industry}
❌ VERMEIDE:
- Vage Aussagen ohne Daten ("KI wird wichtiger")
- Generische Trends ohne konkreten Aufhänger
- Alte News die jeder schon kennt
- Themen ohne verifizierbare Fakten"""
def _get_transform_prompt(
self,
raw_research: str,
target_audience: str,
persona: str,
content_pillars: List[str],
example_posts: List[str],
existing_topics: List[str],
post_type: Any = None,
post_type_analysis: Dict[str, Any] = None
) -> str:
"""Transform raw research into personalized, concrete topic suggestions."""
# Build example posts section
examples_section = ""
if example_posts:
examples_section = "\n\n=== SO SCHREIBT DIESE PERSON (Beispiel-Posts) ===\n"
for i, post in enumerate(example_posts[:5], 1):
post_preview = post[:600] + "..." if len(post) > 600 else post
examples_section += f"\n--- Beispiel {i} ---\n{post_preview}\n"
examples_section += "--- Ende Beispiele ---\n"
# Build pillars section
pillars_text = ", ".join(content_pillars[:5]) if content_pillars else "Keine spezifischen Säulen"
# Build existing topics section (to avoid)
existing_text = ", ".join(existing_topics[:15]) if existing_topics else "Keine"
# Build post type context section
post_type_section = ""
if post_type:
post_type_section = f"""
=== ZIEL-POST-TYP: {post_type.name} ===
{f"Beschreibung: {post_type.description}" if post_type.description else ""}
{f"Typische Hashtags: {', '.join(post_type.identifying_hashtags[:5])}" if post_type.identifying_hashtags else ""}
{f"Keywords: {', '.join(post_type.identifying_keywords[:10])}" if post_type.identifying_keywords else ""}
"""
if post_type.semantic_properties:
props = post_type.semantic_properties
if props.get("purpose"):
post_type_section += f"Zweck: {props['purpose']}\n"
if props.get("typical_tone"):
post_type_section += f"Tonalität: {props['typical_tone']}\n"
if props.get("target_audience"):
post_type_section += f"Zielgruppe: {props['target_audience']}\n"
if post_type_analysis and post_type_analysis.get("sufficient_data"):
post_type_section += "\n**Analyse-basierte Anforderungen:**\n"
if hooks := post_type_analysis.get("hooks"):
post_type_section += f"- Hook-Typen: {', '.join(hooks.get('hook_types', [])[:3])}\n"
if content := post_type_analysis.get("content_focus"):
post_type_section += f"- Hauptthemen: {', '.join(content.get('main_themes', [])[:3])}\n"
if content.get("target_emotion"):
post_type_section += f"- Ziel-Emotion: {content['target_emotion']}\n"
post_type_section += "\n**WICHTIG:** Alle Themenvorschläge müssen zu diesem Post-Typ passen!\n"
return f"""AUFGABE: Transformiere die Recherche-Ergebnisse in KONKRETE, PERSONALISIERTE Themenvorschläge.
{post_type_section}
=== RECHERCHE-ERGEBNISSE (Rohdaten) ===
{raw_research}
=== PERSON/EXPERTISE ===
{persona[:800] if persona else "Keine Persona definiert"}
=== CONTENT-SÄULEN DER PERSON ===
{pillars_text}
{examples_section}
=== BEREITS BEHANDELT (NICHT NOCHMAL!) ===
{existing_text}
=== DEINE AUFGABE ===
Erstelle 6-8 KONKRETE Themenvorschläge die:
1. ZU DIESER PERSON PASSEN - Basierend auf Expertise und Beispiel-Posts
2. EINEN KLAREN ANGLE HABEN - Nicht "über X schreiben" sondern eine spezifische Perspektive
3. FAKTEN AUS DER RECHERCHE NUTZEN - Konkrete Daten/Zahlen einbauen
4. ABWECHSLUNGSREICH SIND - Verschiedene Kategorien und Formate
KATEGORIEN (mindestens 3 verschiedene!):
- **Meinung/Take**: Deine Perspektive zu einem aktuellen Thema
- **Erfahrungsbericht**: "Was ich gelernt habe als..."
- **Konträr**: "Unpopuläre Meinung: ..."
- **How-To/Insight**: Konkrete Tipps basierend auf Daten
- **Story**: Persönliche Geschichte mit Business-Lesson
- **Analyse**: Daten/Trend analysiert durch deine Expertise-Brille
FORMAT DER THEMENVORSCHLÄGE:
{{
"topics": [
{{
"title": "Konkreter Thementitel (kein Schlagzeilen-Stil!)",
"category": "Meinung/Take | Erfahrungsbericht | Konträr | How-To/Insight | Story | Analyse",
"angle": "Der spezifische Blickwinkel/die Perspektive für diesen Post",
"hook_idea": "Konkrete Hook-Idee die zum Post passen würde (1-2 Sätze)",
"key_facts": ["Fakt 1 aus der Recherche", "Fakt 2 mit Zahlen", "Fakt 3"],
"why_this_person": "Warum passt dieses Thema zu DIESER Person und ihrer Expertise?",
"source": "Quellenangabe"
}}
]
}}
BEISPIEL EINES GUTEN THEMENVORSCHLAGS:
{{
"title": "Warum ich als Tech-Lead jetzt 30% meiner Zeit mit Prompt Engineering verbringe",
"category": "Erfahrungsbericht",
"angle": "Persönliche Erfahrung eines Tech-Leads mit der Veränderung seiner Rolle durch KI",
"hook_idea": "Vor einem Jahr habe ich Code geschrieben. Heute schreibe ich Prompts. Und ehrlich? Ich weiß noch nicht ob das gut oder schlecht ist.",
"key_facts": ["GitHub Copilot wird von 92% der Entwickler genutzt (Stack Overflow 2024)", "Durchschnittliche Zeitersparnis: 55%", "Aber: Code-Review-Zeit +40%"],
"why_this_person": "Als Tech-Lead hat die Person direkten Einblick in diese Veränderung und kann authentisch darüber berichten",
"source": "Stack Overflow Developer Survey 2024"
}}
WICHTIG:
- Jeder Vorschlag muss sich UNTERSCHEIDEN (anderer Angle, andere Kategorie)
- Keine generischen "Die Zukunft von X" Themen
- Hook-Ideen müssen zum Stil der Beispiel-Posts passen!
- Key Facts müssen aus der Recherche stammen (keine erfundenen Zahlen)"""
def _get_structure_prompt(
self,
raw_research: str,
target_audience: str,
persona: str = ""
) -> str:
"""Get prompt to structure Perplexity research into JSON (legacy)."""
return f"""Strukturiere die folgenden Recherche-Ergebnisse in ein sauberes JSON-Format.
RECHERCHE-ERGEBNISSE:
{raw_research}
AUFGABE:
Extrahiere die Themen und formatiere sie als JSON. Behalte ALLE Fakten, Quellen und Details bei.
Gib das Ergebnis in diesem Format zurück:
{{
"topics": [
{{
"title": "Prägnanter Titel des Themas",
"category": "News-Flash / Schmerzpunkt-Löser / Konträrer Trend / Emerging Topic",
"fact": "Die kompletten Fakten, Zahlen und Details aus der Recherche - NICHTS weglassen!",
"relevance": "Warum ist das für {target_audience} wichtig?",
"source": "Quellenangaben aus der Recherche"
}}
]
}}
WICHTIG:
- Behalte ALLE Fakten und Quellen aus der Recherche
- Erfinde NICHTS dazu
- Wenn etwas unklar ist, lass es weg
- Mindestens 5 Themen wenn vorhanden"""
def _ensure_diversity(self, topics: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Ensure topic suggestions are diverse (different categories, angles).
Args:
topics: List of topic suggestions
Returns:
Filtered list with diverse topics
"""
if len(topics) <= 3:
return topics
# Track categories used
category_counts = {}
diverse_topics = []
for topic in topics:
category = topic.get("category", "Unknown")
# Allow max 2 topics per category
if category_counts.get(category, 0) < 2:
diverse_topics.append(topic)
category_counts[category] = category_counts.get(category, 0) + 1
# If we filtered too many, add back some
if len(diverse_topics) < 5 and len(topics) >= 5:
for topic in topics:
if topic not in diverse_topics:
diverse_topics.append(topic)
if len(diverse_topics) >= 6:
break
logger.info(f"Diversity check: {len(topics)} -> {len(diverse_topics)} topics, categories: {category_counts}")
return diverse_topics
def _extract_topics_from_response(self, response: str) -> List[Dict[str, Any]]:
"""
Extract structured topics from Perplexity response.
Args:
response: Raw response from Perplexity
Returns:
List of structured topic dictionaries
"""
topics = []
# Simple parsing - split by topic markers
sections = response.split("[TITEL]:")
for section in sections[1:]: # Skip first empty section
try:
# Extract title
title_end = section.find("[KATEGORIE]:")
if title_end == -1:
title_end = section.find("\n")
title = section[:title_end].strip()
# Extract category
category = ""
if "[KATEGORIE]:" in section:
cat_start = section.find("[KATEGORIE]:") + len("[KATEGORIE]:")
cat_end = section.find("[DER FAKT]:")
if cat_end == -1:
cat_end = section.find("\n", cat_start)
category = section[cat_start:cat_end].strip()
# Extract fact
fact = ""
if "[DER FAKT]:" in section:
fact_start = section.find("[DER FAKT]:") + len("[DER FAKT]:")
fact_end = section.find("[WARUM RELEVANT]:")
if fact_end == -1:
fact_end = section.find("[QUELLE]:")
if fact_end == -1:
fact_end = len(section)
fact = section[fact_start:fact_end].strip()
# Extract relevance
relevance = ""
if "[WARUM RELEVANT]:" in section:
rel_start = section.find("[WARUM RELEVANT]:") + len("[WARUM RELEVANT]:")
rel_end = section.find("[QUELLE]:")
if rel_end == -1:
rel_end = len(section)
relevance = section[rel_start:rel_end].strip()
if title and fact:
topics.append({
"title": title,
"category": category or "Allgemein",
"fact": fact,
"relevance": relevance,
"source": "perplexity_research"
})
except Exception as e:
logger.warning(f"Failed to parse topic section: {e}")
continue
return topics