"""Profile analyzer agent.""" import json from typing import Dict, Any, List from collections import Counter from loguru import logger from src.agents.base import BaseAgent from src.database.models import LinkedInProfile, LinkedInPost class ProfileAnalyzerAgent(BaseAgent): """Agent for analyzing LinkedIn profiles and extracting writing patterns.""" def __init__(self): """Initialize profile analyzer agent.""" super().__init__("ProfileAnalyzer") async def process( self, profile: LinkedInProfile, posts: List[LinkedInPost], customer_data: Dict[str, Any] ) -> Dict[str, Any]: """ Analyze LinkedIn profile and extract writing patterns. Args: profile: LinkedIn profile data posts: List of LinkedIn posts customer_data: Additional customer data from input file Returns: Comprehensive profile analysis """ logger.info(f"Analyzing profile for: {profile.name}") # Prepare analysis data profile_summary = { "name": profile.name, "headline": profile.headline, "summary": profile.summary, "industry": profile.industry, "location": profile.location } # Prepare posts with engagement data - use up to 30 posts posts_with_engagement = self._prepare_posts_for_analysis(posts[:15]) # Also identify top performing posts by engagement top_posts = self._get_top_performing_posts(posts, limit=5) system_prompt = self._get_system_prompt() user_prompt = self._get_user_prompt(profile_summary, posts_with_engagement, top_posts, customer_data) response = await self.call_openai( system_prompt=system_prompt, user_prompt=user_prompt, model="gpt-4o", temperature=0.3, response_format={"type": "json_object"} ) # Parse JSON response analysis = json.loads(response) # Add N-gram analysis post_texts = [p.post_text for p in posts if p.post_text] ngram_patterns = self._extract_ngram_patterns(post_texts) analysis["ngram_patterns"] = ngram_patterns logger.info("Profile analysis completed successfully (with N-gram analysis)") return analysis def _prepare_posts_for_analysis(self, posts: List[LinkedInPost]) -> List[Dict[str, Any]]: """Prepare posts with engagement data for analysis.""" prepared = [] for i, post in enumerate(posts): if not post.post_text: continue prepared.append({ "index": i + 1, "text": post.post_text, "likes": post.likes or 0, "comments": post.comments or 0, "shares": post.shares or 0, "engagement_total": (post.likes or 0) + (post.comments or 0) * 2 + (post.shares or 0) * 3 }) return prepared def _get_top_performing_posts(self, posts: List[LinkedInPost], limit: int = 5) -> List[Dict[str, Any]]: """Get top performing posts by engagement.""" posts_with_engagement = [] for post in posts: if not post.post_text or len(post.post_text) < 50: continue engagement = (post.likes or 0) + (post.comments or 0) * 2 + (post.shares or 0) * 3 posts_with_engagement.append({ "text": post.post_text, "likes": post.likes or 0, "comments": post.comments or 0, "shares": post.shares or 0, "engagement_score": engagement }) # Sort by engagement and return top posts sorted_posts = sorted(posts_with_engagement, key=lambda x: x["engagement_score"], reverse=True) return sorted_posts[:limit] def _get_system_prompt(self) -> str: """Get system prompt for profile analysis.""" return """Du bist ein hochspezialisierter AI-Analyst für LinkedIn-Profile und Content-Strategie. Deine Aufgabe ist es, aus LinkedIn-Profildaten und Posts ein umfassendes Content-Analyse-Profil zu erstellen, das als BLAUPAUSE für das Schreiben neuer Posts dient. WICHTIG: Extrahiere ECHTE BEISPIELE aus den Posts! Keine generischen Beschreibungen. Das Profil soll folgende Dimensionen analysieren: 1. **Schreibstil & Tonalität** - Wie schreibt die Person? (formal, locker, inspirierend, provokativ, etc.) - Welche Perspektive wird genutzt? (Ich, Wir, Man) - Wie ist die Ansprache? (Du, Sie, neutral) - Satzdynamik und Rhythmus 2. **Phrasen-Bibliothek (KRITISCH!)** - Hook-Phrasen: Wie beginnen Posts? Extrahiere 5-10 ECHTE Beispiele! - Übergangs-Phrasen: Wie werden Absätze verbunden? - Emotionale Ausdrücke: Ausrufe, Begeisterung, etc. - CTA-Phrasen: Wie werden Leser aktiviert? - Signature Phrases: Wiederkehrende Markenzeichen 3. **Struktur-Templates** - Analysiere die STRUKTUR der Top-Posts - Erstelle 2-3 konkrete Templates (z.B. "Hook → Flashback → Erkenntnis → CTA") - Typische Satzanfänge für jeden Abschnitt 4. **Visuelle Muster** - Emoji-Nutzung (welche, wo, wie oft) - Unicode-Formatierung (fett, kursiv) - Strukturierung (Absätze, Listen, etc.) 5. **Audience Insights** - Wer ist die Zielgruppe? - Welche Probleme werden adressiert? - Welcher Mehrwert wird geboten? Gib deine Analyse als strukturiertes JSON zurück.""" def _get_user_prompt( self, profile_summary: Dict[str, Any], posts_with_engagement: List[Dict[str, Any]], top_posts: List[Dict[str, Any]], customer_data: Dict[str, Any] ) -> str: """Get user prompt with data for analysis.""" # Format all posts with engagement data all_posts_text = "" for post in posts_with_engagement: all_posts_text += f"\n--- Post {post['index']} (Likes: {post['likes']}, Comments: {post['comments']}, Shares: {post['shares']}) ---\n" all_posts_text += post['text'][:2000] # Limit each post to 2000 chars all_posts_text += "\n" # Format top performing posts top_posts_text = "" if top_posts: for i, post in enumerate(top_posts, 1): top_posts_text += f"\n--- TOP POST {i} (Engagement Score: {post['engagement_score']}, Likes: {post['likes']}, Comments: {post['comments']}) ---\n" top_posts_text += post['text'][:2000] top_posts_text += "\n" return f"""Bitte analysiere folgendes LinkedIn-Profil BASIEREND AUF DEN ECHTEN POSTS: **PROFIL-INFORMATIONEN:** - Name: {profile_summary.get('name', 'N/A')} - Headline: {profile_summary.get('headline', 'N/A')} - Branche: {profile_summary.get('industry', 'N/A')} - Location: {profile_summary.get('location', 'N/A')} - Summary: {profile_summary.get('summary', 'N/A')} **ZUSÄTZLICHE KUNDENDATEN (Persona, Style Guide, etc.):** {json.dumps(customer_data, indent=2, ensure_ascii=False)} **TOP-PERFORMING POSTS (die erfolgreichsten Posts - ANALYSIERE DIESE BESONDERS GENAU!):** {top_posts_text if top_posts_text else "Keine Engagement-Daten verfügbar"} **ALLE POSTS ({len(posts_with_engagement)} Posts mit Engagement-Daten):** {all_posts_text} --- WICHTIG: Analysiere die ECHTEN POSTS sehr genau! Deine Analyse muss auf den tatsächlichen Mustern basieren, nicht auf Annahmen. Extrahiere WÖRTLICHE ZITATE wo möglich! Achte besonders auf: 1. Die TOP-PERFORMING Posts - was macht sie erfolgreich? 2. Wiederkehrende Phrasen und Formulierungen - WÖRTLICH extrahieren! 3. Wie beginnen die Posts (Hooks)? - ECHTE BEISPIELE sammeln! 4. Wie enden die Posts (CTAs)? 5. Emoji-Verwendung (welche, wo, wie oft) 6. Länge und Struktur der Absätze 7. Typische Satzanfänge und Übergänge Erstelle eine umfassende Analyse im folgenden JSON-Format: {{ "writing_style": {{ "tone": "Beschreibung der Tonalität basierend auf den echten Posts", "perspective": "Ich/Wir/Man/Gemischt - mit Beispielen aus den Posts", "form_of_address": "Du/Sie/Neutral - wie spricht die Person die Leser an?", "sentence_dynamics": "Kurze Sätze? Lange Sätze? Mischung? Fragen?", "average_post_length": "Kurz/Mittel/Lang", "average_word_count": 0 }}, "linguistic_fingerprint": {{ "energy_level": 0, "shouting_usage": "Beschreibung mit konkreten Beispielen aus den Posts", "punctuation_patterns": "Beschreibung (!!!, ..., ?, etc.)", "signature_phrases": ["ECHTE Phrasen aus den Posts", "die wiederholt vorkommen"], "narrative_anchors": ["Storytelling-Elemente", "die die Person nutzt"] }}, "phrase_library": {{ "hook_phrases": [ "ECHTE Hook-Sätze aus den Posts wörtlich kopiert", "Mindestens 5-8 verschiedene Beispiele", "z.B. '𝗞𝗜-𝗦𝘂𝗰𝗵𝗲 𝗶𝘀𝘁 𝗱𝗲𝗿 𝗲𝗿𝘀𝘁𝗲 𝗦𝗰𝗵𝗿𝗶𝘁𝘁 𝗶𝗺 𝗦𝗮𝗹𝗲𝘀 𝗙𝘂𝗻𝗻𝗲𝗹.'" ], "transition_phrases": [ "ECHTE Übergangssätze zwischen Absätzen", "z.B. 'Und wisst ihr was?', 'Aber Moment...', 'Was das mit X zu tun hat?'" ], "emotional_expressions": [ "Ausrufe und emotionale Marker", "z.B. 'Halleluja!', 'Sorry to say!!', 'Galopp!!!!'" ], "cta_phrases": [ "ECHTE Call-to-Action Formulierungen", "z.B. 'Was denkt ihr?', 'Seid ihr dabei?', 'Lasst uns darüber sprechen.'" ], "filler_expressions": [ "Typische Füllwörter und Ausdrücke", "z.B. 'Ich meine...', 'Wisst ihr...', 'Ok, ok...'" ] }}, "structure_templates": {{ "primary_structure": "Die häufigste Struktur beschreiben, z.B. 'Unicode-Hook → Persönliche Anekdote → Erkenntnis → Bullet Points → CTA'", "template_examples": [ {{ "name": "Storytelling-Post", "structure": ["Fetter Hook mit Zitat", "Flashback/Anekdote", "Erkenntnis/Lesson", "Praktische Tipps", "CTA-Frage"], "example_post_index": 1 }}, {{ "name": "Insight-Post", "structure": ["Provokante These", "Begründung", "Beispiel", "Handlungsaufforderung"], "example_post_index": 2 }} ], "typical_sentence_starters": [ "ECHTE Satzanfänge aus den Posts", "z.B. 'Ich glaube, dass...', 'Was mir aufgefallen ist...', 'Das Verrückte ist...'" ], "paragraph_transitions": [ "Wie werden Absätze eingeleitet?", "z.B. 'Und...', 'Aber:', 'Das bedeutet:'" ] }}, "tone_analysis": {{ "primary_tone": "Haupttonalität basierend auf den Posts", "emotional_range": "Welche Emotionen werden angesprochen?", "authenticity_markers": ["Was macht den Stil einzigartig?", "Erkennbare Merkmale"] }}, "topic_patterns": {{ "main_topics": ["Hauptthemen aus den Posts"], "content_pillars": ["Content-Säulen"], "expertise_areas": ["Expertise-Bereiche"], "expertise_level": "Anfänger/Fortgeschritten/Experte" }}, "audience_insights": {{ "target_audience": "Wer wird angesprochen?", "pain_points_addressed": ["Probleme die adressiert werden"], "value_proposition": "Welchen Mehrwert bietet die Person?", "industry_context": "Branchenkontext" }}, "visual_patterns": {{ "emoji_usage": {{ "emojis": ["Liste der tatsächlich verwendeten Emojis"], "placement": "Anfang/Ende/Inline/Zwischen Absätzen", "frequency": "Selten/Mittel/Häufig - pro Post durchschnittlich X" }}, "unicode_formatting": "Wird ✓, →, •, 𝗙𝗲𝘁𝘁 etc. verwendet? Wo?", "structure_preferences": "Absätze/Listen/Einzeiler/Nummeriert" }}, "content_strategy": {{ "hook_patterns": "Wie werden Posts KONKRET eröffnet? Beschreibung des Musters", "cta_style": "Wie sehen die CTAs aus? Frage? Aufforderung? Keine?", "storytelling_approach": "Persönliche Geschichten? Metaphern? Case Studies?", "post_structure": "Hook → Body → CTA? Oder anders?" }}, "best_performing_patterns": {{ "what_works": "Was machen die Top-Posts anders/besser?", "successful_hooks": ["WÖRTLICHE Beispiel-Hooks aus Top-Posts"], "engagement_drivers": ["Was treibt Engagement?"] }} }} KRITISCH: Bei phrase_library und structure_templates müssen ECHTE, WÖRTLICHE Beispiele aus den Posts stehen! Keine generischen Beschreibungen!""" def _extract_ngram_patterns(self, post_texts: List[str]) -> Dict[str, Any]: """ Extract N-gram patterns from posts for better style fingerprinting. Args: post_texts: List of post texts Returns: Dictionary with bigrams, trigrams, and signature patterns """ if not post_texts or len(post_texts) == 0: return { "typical_bigrams": [], "typical_trigrams": [], "signature_combinations": [], "available": False } try: import nltk from nltk import ngrams, word_tokenize # Download required NLTK data if not available try: nltk.data.find('tokenizers/punkt') except LookupError: logger.info("Downloading NLTK punkt tokenizer...") nltk.download('punkt', quiet=True) # Combine all text all_text = " ".join(post_texts) # Tokenize (try German tokenizer, fallback to default) try: words = word_tokenize(all_text.lower(), language='german') except: words = word_tokenize(all_text.lower()) # Remove punctuation and very short words words = [w for w in words if w.isalnum() and len(w) > 2] # Extract bigrams (2-word combinations) bigrams = list(ngrams(words, 2)) bigram_freq = Counter(bigrams) # Extract trigrams (3-word combinations) trigrams = list(ngrams(words, 3)) trigram_freq = Counter(trigrams) # Get top bigrams and trigrams top_bigrams = [" ".join(bg) for bg, _ in bigram_freq.most_common(50)] top_trigrams = [" ".join(tg) for tg, _ in trigram_freq.most_common(30)] # Find signature patterns (unique combinations that appear multiple times) signature_patterns = self._find_signature_patterns(bigram_freq, trigram_freq) logger.info(f"Extracted {len(top_bigrams)} bigrams, {len(top_trigrams)} trigrams, " f"{len(signature_patterns)} signature patterns") return { "typical_bigrams": top_bigrams, "typical_trigrams": top_trigrams, "signature_combinations": signature_patterns, "bigram_count": len(bigrams), "trigram_count": len(trigrams), "available": True } except Exception as e: logger.error(f"N-gram extraction failed: {e}") return { "typical_bigrams": [], "typical_trigrams": [], "signature_combinations": [], "available": False, "error": str(e) } def _find_signature_patterns( self, bigram_freq: Counter, trigram_freq: Counter ) -> List[str]: """ Find signature patterns - unique combinations that are not generic. Args: bigram_freq: Counter of bigram frequencies trigram_freq: Counter of trigram frequencies Returns: List of signature pattern strings """ # Generic German words to filter out generic_words = { 'und', 'die', 'der', 'das', 'ist', 'sind', 'ein', 'eine', 'zu', 'den', 'für', 'mit', 'auf', 'von', 'in', 'des', 'dem', 'im', 'zum', 'zur', 'am', 'bei', 'hat', 'haben', 'wird', 'werden', 'kann', 'können', 'soll', 'müssen', 'auch', 'nur', 'noch', 'schon', 'sehr', 'mehr', 'aber', 'oder', 'wenn', 'dann', 'als', 'wie', 'nach', 'über' } signature_patterns = [] # Find non-generic bigrams that appear at least 3 times for (w1, w2), count in bigram_freq.most_common(100): if count >= 3: # Must appear multiple times # Filter out generic combinations if w1 not in generic_words or w2 not in generic_words: pattern = f"{w1} {w2}" if pattern not in signature_patterns: signature_patterns.append(pattern) # Find non-generic trigrams that appear at least 2 times for (w1, w2, w3), count in trigram_freq.most_common(50): if count >= 2: # At least one word should not be generic non_generic_count = sum(1 for w in [w1, w2, w3] if w not in generic_words) if non_generic_count >= 2: pattern = f"{w1} {w2} {w3}" if pattern not in signature_patterns: signature_patterns.append(pattern) # Limit to top 20 most distinctive patterns return signature_patterns[:20]