diff --git a/.env.example b/.env.example index 2bc737c..e85869d 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,9 @@ PERPLEXITY_API_KEY=pplx-your-perplexity-key # Apify API Key (required for LinkedIn scraping) APIFY_API_KEY=apify_api_your-apify-key +# NVIDIA API Key (Insights summary) +NVIDIA_API_KEY=nvapi-your-nvidia-key + # =========================================== # Supabase Database # =========================================== @@ -37,6 +40,9 @@ APIFY_ACTOR_ID=apimaestro~linkedin-profile-posts DEBUG=false LOG_LEVEL=INFO +# Insights +INSIGHTS_MANUAL_REFRESH_LIMIT_ENABLED=true + # =========================================== # Email Settings (for sending posts) # =========================================== diff --git a/src/agents/__init__.py b/src/agents/__init__.py index a1b94ee..a06a790 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -7,6 +7,7 @@ from src.agents.writer import WriterAgent from src.agents.critic import CriticAgent from src.agents.post_classifier import PostClassifierAgent from src.agents.post_type_analyzer import PostTypeAnalyzerAgent +from src.agents.strategy_importer import StrategyImporterAgent __all__ = [ "BaseAgent", @@ -17,4 +18,5 @@ __all__ = [ "CriticAgent", "PostClassifierAgent", "PostTypeAnalyzerAgent", + "StrategyImporterAgent", ] diff --git a/src/agents/strategy_importer.py b/src/agents/strategy_importer.py new file mode 100644 index 0000000..75bd915 --- /dev/null +++ b/src/agents/strategy_importer.py @@ -0,0 +1,69 @@ +"""Company strategy importer agent.""" +import json +from typing import Any, Dict, Optional + +from src.agents.base import BaseAgent + + +class StrategyImporterAgent(BaseAgent): + """Convert a strategy document into structured company strategy fields.""" + + def __init__(self) -> None: + super().__init__("StrategyImporter") + + async def process(self, document_text: str, company_name: Optional[str] = None) -> Dict[str, Any]: + system_prompt = ( + "Du bist ein erfahrener Strategy Analyst und strukturierst Unternehmensstrategien " + "für LinkedIn-Content. Du erhältst den Text eines Strategie-Dokuments und musst daraus " + "ein klares, ausführliches JSON im vorgegebenen Format erstellen." + ) + + user_prompt = f""" +Unternehmensname: {company_name or "Unbekannt"} + +Analysiere den folgenden Dokumenttext und extrahiere eine vollständige Unternehmensstrategie. +Die Antwort MUSS valides JSON sein und exakt dieses Schema enthalten: + +{{ + "mission": "String", + "vision": "String", + "brand_voice": "String", + "tone_guidelines": "String", + "target_audience": "String", + "content_pillars": ["String", "..."], + "dos": ["String", "..."], + "donts": ["String", "..."] +}} + +Anforderungen: +- Schreibe auf Deutsch. +- Fülle alle Felder so vollständig wie möglich. +- Nutze ausschließlich Informationen aus dem Dokument. Wenn etwas implizit ist, formuliere es vorsichtig. +- Content-Säulen, Do's und Don'ts sollen konkrete, umsetzbare Punkte sein. + +Dokumenttext: +{document_text} +""" + + response = await self.call_openai( + system_prompt=system_prompt, + user_prompt=user_prompt, + model="gpt-4o", + temperature=0.2, + response_format={"type": "json_object"} + ) + + parsed = json.loads(response) + if not isinstance(parsed, dict): + raise ValueError("Ungültiges JSON vom Modell.") + + return { + "mission": parsed.get("mission", ""), + "vision": parsed.get("vision", ""), + "brand_voice": parsed.get("brand_voice", ""), + "tone_guidelines": parsed.get("tone_guidelines", ""), + "target_audience": parsed.get("target_audience", ""), + "content_pillars": parsed.get("content_pillars", []) or [], + "dos": parsed.get("dos", []) or [], + "donts": parsed.get("donts", []) or [], + } diff --git a/src/config.py b/src/config.py index a3eac96..cff727f 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,7 @@ """Configuration management for LinkedIn Workflow.""" from typing import Optional from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field, AliasChoices from pathlib import Path @@ -61,6 +62,9 @@ class Settings(BaseSettings): # Token Encryption encryption_key: str = "" # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" + # NVIDIA (free model for insights summary) + nvidia_api_key: str = "" + # MOCO Integration moco_api_key: str = "" # Token für Authorization-Header moco_domain: str = "" # Subdomain: {domain}.mocoapp.com @@ -68,6 +72,13 @@ class Settings(BaseSettings): # Redis redis_url: str = "redis://redis:6379/0" scheduler_enabled: bool = False # True only on dedicated scheduler container + insights_manual_refresh_limit_enabled: bool = Field( + default=True, + validation_alias=AliasChoices( + "INSIGHTS_MANUAL_REFRESH_LIMIT_ENABLED", + "NSIGHTS_MANUAL_REFRESH_LIMIT_ENABLED" + ), + ) # YouTube (optional) youtube_cookies: str = "" # Raw Cookie header value for transcript fetching diff --git a/src/services/insights_summary_service.py b/src/services/insights_summary_service.py new file mode 100644 index 0000000..fd7761e --- /dev/null +++ b/src/services/insights_summary_service.py @@ -0,0 +1,82 @@ +"""Generate a short insights summary using NVIDIA-hosted LLM.""" +from __future__ import annotations + +from typing import Any, Dict, Optional + +from loguru import logger +from openai import OpenAI + +from src.config import settings + + +def _build_prompt(post_insights: Dict[str, Any]) -> str: + return ( + "Erstelle eine kurze, prägnante Zusammenfassung (2-4 Sätze) der wichtigsten " + "Insights aus den folgenden LinkedIn-Post-Metriken. Fokus: Trends, auffällige " + "Performance, was gut funktioniert.\n\n" + f"Posts: {post_insights.get('total_posts')}\n" + f"Ø Reaktionen/Post: {post_insights.get('avg_reactions')}\n" + f"Ø Likes: {post_insights.get('avg_likes')}\n" + f"Ø Comments: {post_insights.get('avg_comments')}\n" + f"Ø Shares: {post_insights.get('avg_shares')}\n" + f"Letzte 7 Tage Reaktionen: {post_insights.get('last_7_reactions')}\n" + f"Trend vs Vorwoche (%): {post_insights.get('trend_pct')}\n" + f"Bester Wochentag: {post_insights.get('best_weekday')}\n" + f"Posting-Kadenz/Woche: {post_insights.get('cadence_per_week')}\n" + ) + + +def _is_summary_complete(text: str) -> bool: + if not text: + return False + cleaned = text.strip() + if len(cleaned) < 60: + return False + if cleaned[-1] not in ".!?": + return False + return True + + +def generate_insights_summary(post_insights: Dict[str, Any]) -> Optional[str]: + """Generate a short summary for the insights page.""" + if not settings.nvidia_api_key: + return None + + try: + client = OpenAI( + base_url="https://integrate.api.nvidia.com/v1", + api_key=settings.nvidia_api_key, + ) + + completion = client.chat.completions.create( + model="deepseek-ai/deepseek-v3.2", + messages=[{"role": "user", "content": _build_prompt(post_insights)}], + temperature=0.6, + top_p=0.95, + max_tokens=300, + ) + + content = None + if completion and completion.choices: + content = completion.choices[0].message.content + if content: + text = content.strip() + if _is_summary_complete(text): + return text + + # Retry once with stricter settings if output looks incomplete + retry = client.chat.completions.create( + model="deepseek-ai/deepseek-v3.2", + messages=[{"role": "user", "content": _build_prompt(post_insights)}], + temperature=0.4, + top_p=0.9, + max_tokens=220, + ) + if retry and retry.choices: + text = (retry.choices[0].message.content or "").strip() + if _is_summary_complete(text): + return text + return None + except Exception as e: + logger.warning(f"Failed to generate insights summary: {e}") + return None diff --git a/src/web/templates/user/base.html b/src/web/templates/user/base.html index 7b0e4a7..e3d242c 100644 --- a/src/web/templates/user/base.html +++ b/src/web/templates/user/base.html @@ -157,16 +157,7 @@ - - - {% endif %} - {% if session and session.account_type == 'employee' %} - - - - + {% endif %} diff --git a/src/web/templates/user/company_strategy.html b/src/web/templates/user/company_strategy.html index 8416faa..bd60391 100644 --- a/src/web/templates/user/company_strategy.html +++ b/src/web/templates/user/company_strategy.html @@ -4,9 +4,55 @@ {% block content %}
Diese Strategie wird bei der Erstellung aller LinkedIn-Posts deiner Mitarbeiter berücksichtigt.
+ + +Diese Strategie wird bei der Erstellung aller LinkedIn-Posts deiner Mitarbeiter berücksichtigt.
+PDF hochladen, analysieren lassen und die Felder automatisch befüllen.
+ +Tägliche Auswertung deiner LinkedIn-Posts
-Verbinde deinen LinkedIn Account, damit wir täglich Post-Insights aktualisieren können.
- Zum LinkedIn Login -Noch keine Insights vorhanden. Der tägliche Import läuft in den nächsten 24 Stunden.
-Posts getrackt
-{{ post_insights.total_posts }}
-Ø Reaktionen/Post
-{{ post_insights.avg_reactions }}
-Likes {{ post_insights.avg_likes }} · Comments {{ post_insights.avg_comments }} · Shares {{ post_insights.avg_shares }}
-Letzte 7 Tage
-{{ post_insights.last_7_reactions }}
- {% if post_insights.trend_pct is not none %} -- {% if post_insights.trend_pct >= 0 %}+{% endif %}{{ post_insights.trend_pct }}% vs. Vorwoche -
- {% endif %} -{{ post.text[:180] }}{% if post.text|length > 180 %}...{% endif %}
-Diese Strategie wird bei der Erstellung deiner LinkedIn-Posts berücksichtigt.
- {% if session.company_name %} -Richtlinien von {{ session.company_name }}
- {% endif %} +Strukturierte Profil-Analyse basierend auf deinen LinkedIn-Posts.
+Dein Unternehmen hat noch keine Strategie definiert.
-Wende dich an deinen Administrator, um die Unternehmensstrategie einzurichten.
-Mission
-{{ strategy.mission }}
-Vision
-{{ strategy.vision }}
-Markenstimme
-{{ strategy.brand_voice }}
-Tonalität-Richtlinien
-{{ strategy.tone_guidelines }}
-{{ strategy.target_audience }}
-Die Hauptthemen, über die das Unternehmen postet.
-Noch keine Profil-Analyse vorhanden.
+Bitte wende dich an den Administrator, um eine Analyse zu starten.