Artikel

18. Interaktionssystem

Allgemein

Das Interaktionssystem verarbeitet Reaktionen auf Beiträge und findet begrenzt mögliche externe Interaktionen.

Es ist nicht für Reichweiten-Automatisierung gedacht. Es erzeugt Kandidaten, prüft Relevanz, schlägt Kommentare vor und legt diese in eine Freigabe-Queue.

Beitrag oder Kommentar finden
    ↓
Kandidat speichern
    ↓
Relevanz prüfen
    ↓
Risiko prüfen
    ↓
Kommentarvorschlag erzeugen
    ↓
Quality-Gate
    ↓
manuelle Freigabe
    ↓
optional ausführen

Für das MVP gilt:

Eigene Kommentare lesen: ja
Antwortvorschläge erzeugen: ja
Fremde Beiträge analysieren: begrenzt
Automatisch kommentieren: nein
Automatisch liken: nur stark begrenzt
Automatisch folgen: nein

18.1 Kandidaten finden

Ein Kandidat ist ein möglicher Anlass für eine Interaktion.

Kandidatentypen:

comment_reply
external_comment
like_candidate

Quellen:

Kommentare unter eigenen LinkedIn-Beiträgen
Kommentare unter eigenen Facebook-Beiträgen
sichtbare Fachbeiträge im LinkedIn-Feed
manuell gespeicherte URLs

Datenmodell:

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class InteractionCandidate:
    id: str
    platform: str
    interaction_type: str
    source_url: str
    source_author: str
    source_text: str
    relevance_score: float
    risk_score: float
    status: str
    suggested_text: str | None
    created_at: datetime
    updated_at: datetime

Statuswerte:

candidate
analyzed
suggested
approved
executed
rejected
failed
blocked

Beispiel:

{
  "id": "interaction_20260512_001",
  "platform": "linkedin",
  "interaction_type": "comment_reply",
  "source_url": "https://www.linkedin.com/feed/update/...",
  "source_author": "Max Beispiel",
  "source_text": "Interessanter Punkt zur Sichtbarkeit kleiner Anbieter.",
  "relevance_score": 0.0,
  "risk_score": 0.0,
  "status": "candidate",
  "suggested_text": null,
  "created_at": "2026-05-12T10:00:00+02:00",
  "updated_at": "2026-05-12T10:00:00+02:00"
}

ID-Erzeugung:

from datetime import datetime
from zoneinfo import ZoneInfo


class InteractionIdFactory:
    def create(self) -> str:
        now = datetime.now(ZoneInfo("Europe/Berlin"))

        return "interaction_" + now.strftime("%Y%m%d_%H%M%S_%f")

18.2 Beiträge analysieren

Analyse bedeutet:

Ist der Beitrag thematisch relevant?
Ist der Beitrag unproblematisch?
Ist eine Reaktion sinnvoll?
Gibt es einen konkreten fachlichen Anschluss?

Die Analyse sollte strukturiert erfolgen.

Ergebnis:

from dataclasses import dataclass


@dataclass(frozen=True)
class InteractionAnalysisResult:
    is_relevant: bool
    relevance_score: float
    risk_score: float
    should_comment: bool
    should_like: bool
    reason: str

OpenAI-Prompt:

Du analysierst einen Social-Media-Beitrag für einen kleinen deutschen E-Book-Verlag.

Beitrag:
{{ source_text }}

Verlagskontext:
{{ publisher_context }}

Prüfe:
- Bezug zu Büchern, Verlag, E-Books, KI, Automatisierung, Public Domain oder digitaler Kultur
- fachliche Anschlussfähigkeit
- Risiko durch politische, private, aggressive oder toxische Inhalte
- ob ein Kommentar sinnvoll wäre
- ob ein Like vertretbar wäre

Antworte ausschließlich im vorgegebenen JSON-Format.

Schema:

INTERACTION_ANALYSIS_SCHEMA: dict[str, object] = {
    "type": "object",
    "properties": {
        "is_relevant": {
            "type": "boolean"
        },
        "relevance_score": {
            "type": "number"
        },
        "risk_score": {
            "type": "number"
        },
        "should_comment": {
            "type": "boolean"
        },
        "should_like": {
            "type": "boolean"
        },
        "reason": {
            "type": "string"
        }
    },
    "required": [
        "is_relevant",
        "relevance_score",
        "risk_score",
        "should_comment",
        "should_like",
        "reason"
    ],
    "additionalProperties": False
}

Validator:

from typing import Any


class InteractionAnalysisValidator:
    def validate(self, payload: dict[str, Any]) -> InteractionAnalysisResult:
        is_relevant = payload.get("is_relevant")
        relevance_score = payload.get("relevance_score")
        risk_score = payload.get("risk_score")
        should_comment = payload.get("should_comment")
        should_like = payload.get("should_like")
        reason = payload.get("reason")

        if isinstance(is_relevant, bool) is False:
            raise RuntimeError("is_relevant must be boolean.")

        if isinstance(relevance_score, int) is False and isinstance(relevance_score, float) is False:
            raise RuntimeError("relevance_score must be number.")

        if isinstance(risk_score, int) is False and isinstance(risk_score, float) is False:
            raise RuntimeError("risk_score must be number.")

        if isinstance(should_comment, bool) is False:
            raise RuntimeError("should_comment must be boolean.")

        if isinstance(should_like, bool) is False:
            raise RuntimeError("should_like must be boolean.")

        if isinstance(reason, str) is False:
            raise RuntimeError("reason must be string.")

        return InteractionAnalysisResult(
            is_relevant=is_relevant,
            relevance_score=float(relevance_score),
            risk_score=float(risk_score),
            should_comment=should_comment,
            should_like=should_like,
            reason=reason,
        )

18.3 Relevanz bewerten

Relevanz ist kein Bauchgefühl, sondern eine technische Schwelle.

Geeignete Themen:

Verlag
Bücher
E-Books
KI
Automatisierung
Public Domain
digitale Produktion
Metadaten
Buchhandel
Kulturtechnik
Urheberrecht allgemein

Nicht geeignet:

Tagespolitik
Privates
Streit
Empörung
Religion
medizinische Themen
juristische Einzelfälle
Beziehungs- oder Lebensberatung
reine Selbstdarstellung anderer Accounts

Regel:

relevance_score >= 0.70
risk_score <= 0.25

Policy:

class InteractionDecisionPolicy:
    def can_suggest_comment(self, analysis: InteractionAnalysisResult) -> bool:
        if analysis.is_relevant is False:
            return False

        if analysis.should_comment is False:
            return False

        if analysis.relevance_score < 0.70:
            return False

        if analysis.risk_score > 0.25:
            return False

        return True

    def can_like(self, analysis: InteractionAnalysisResult) -> bool:
        if analysis.is_relevant is False:
            return False

        if analysis.should_like is False:
            return False

        if analysis.relevance_score < 0.80:
            return False

        if analysis.risk_score > 0.15:
            return False

        return True

18.4 Kommentarvorschlag erzeugen

Ein Kommentarvorschlag wird nur erzeugt, wenn die Analyse positiv ist.

Prompt:

Du erzeugst einen Kommentarvorschlag für einen kleinen deutschen E-Book-Verlag.

Beitrag:
{{ source_text }}

Verlagskontext:
{{ publisher_context }}

Ziel:
Der Kommentar soll einen konkreten fachlichen Gedanken ergänzen.

Regeln:
- maximal 500 Zeichen
- keine Emojis
- kein Verkaufslink
- keine Eigenwerbung
- keine generischen Komplimente
- keine politische Zuspitzung
- kein Angriff
- keine private Ansprache
- nur ein konkreter Gedanke

Ausgabe:
Wenn ein sinnvoller Kommentar möglich ist, gib nur den Kommentartext zurück.
Wenn kein sinnvoller Kommentar möglich ist, gib exakt zurück:
NO_COMMENT

Service:

from dataclasses import dataclass


@dataclass(frozen=True)
class CommentSuggestionResult:
    suggested_text: str | None
    status: str


class CommentSuggestionService:
    def __init__(
        self,
        openai_client: OpenAiClient,
        prompt_renderer: PromptTemplateRenderer,
        safety_identifier: str,
        model: str,
    ) -> None:
        self.openai_client = openai_client
        self.prompt_renderer = prompt_renderer
        self.safety_identifier = safety_identifier
        self.model = model

    def suggest(self, source_text: str, publisher_context: str) -> CommentSuggestionResult:
        input_text = self.prompt_renderer.render(
            "comment_suggestion.txt",
            {
                "source_text": source_text,
                "publisher_context": publisher_context,
            },
        )

        request = OpenAiTextRequest(
            model=self.model,
            instructions="Erzeuge sachliche Kommentarvorschläge für Social Media.",
            input_text=input_text,
            safety_identifier=self.safety_identifier,
            max_output_tokens=300,
        )

        result = self.openai_client.create_text(request)
        suggested_text = result.text.strip()

        if suggested_text == "NO_COMMENT":
            return CommentSuggestionResult(
                suggested_text=None,
                status="rejected",
            )

        if suggested_text == "":
            return CommentSuggestionResult(
                suggested_text=None,
                status="rejected",
            )

        if len(suggested_text) > 500:
            return CommentSuggestionResult(
                suggested_text=None,
                status="rejected",
            )

        return CommentSuggestionResult(
            suggested_text=suggested_text,
            status="suggested",
        )

18.5 Ausschlussregeln

Vor Analyse und Vorschlag laufen harte Ausschlussregeln.

class InteractionRejectPolicy:
    def __init__(self) -> None:
        self.blocked_fragments = [
            "afd",
            "cdu",
            "spd",
            "grüne",
            "fdp",
            "krieg",
            "impfung",
            "corona",
            "flüchtlinge",
            "israel",
            "palästina",
            "gaza",
            "ukraine",
            "russland",
            "trump",
            "putin",
            "hitler",
            "nazi",
        ]

    def assert_allowed(self, source_text: str) -> None:
        cleaned_text = source_text.strip()

        if cleaned_text == "":
            raise RuntimeError("Interaction source text must not be empty.")

        lowered_text = cleaned_text.lower()

        for blocked_fragment in self.blocked_fragments:
            if blocked_fragment in lowered_text:
                raise RuntimeError("Blocked interaction topic found: " + blocked_fragment)

        if len(cleaned_text) > 5000:
            raise RuntimeError("Interaction source text is too long.")

Kommentarvorschläge werden zusätzlich geprüft:

class CommentSuggestionValidator:
    def validate(self, suggested_text: str) -> None:
        cleaned_text = suggested_text.strip()

        if cleaned_text == "":
            raise RuntimeError("Suggested comment must not be empty.")

        if len(cleaned_text) > 500:
            raise RuntimeError("Suggested comment exceeds length limit.")

        blocked_fragments = [
            "jetzt kaufen",
            "hier klicken",
            "mein shop",
            "unbedingt lesen",
            "follow",
            "like",
            "dm",
            "pn",
            "privatnachricht",
        ]

        lowered_text = cleaned_text.lower()

        for blocked_fragment in blocked_fragments:
            if blocked_fragment in lowered_text:
                raise RuntimeError("Blocked comment fragment found: " + blocked_fragment)

        if "https://" in lowered_text:
            raise RuntimeError("Suggested comment must not contain links.")

18.6 Tageslimits

Tageslimits verhindern, dass die Maschine unkontrolliert handelt.

Empfohlene Werte:

max_comment_suggestions_per_day: 10
max_approved_comments_per_day: 3
max_executed_comments_per_day: 3
max_likes_per_day: 5
max_external_comments_per_day: 1

Konfiguration:

interaction_limits:
  max_comment_suggestions_per_day: 10
  max_approved_comments_per_day: 3
  max_executed_comments_per_day: 3
  max_likes_per_day: 5
  max_external_comments_per_day: 1

Limiter:

from datetime import datetime
from zoneinfo import ZoneInfo


class DailyInteractionLimiter:
    def __init__(self, interactions: list[dict[str, object]]) -> None:
        self.interactions = interactions

    def assert_comment_suggestion_allowed(self, max_per_day: int) -> None:
        count = self._count_today_by_status("suggested")

        if count >= max_per_day:
            raise RuntimeError("Daily comment suggestion limit reached.")

    def assert_like_allowed(self, max_per_day: int) -> None:
        count = self._count_today_by_type("like_candidate")

        if count >= max_per_day:
            raise RuntimeError("Daily like limit reached.")

    def _count_today_by_status(self, status: str) -> int:
        today = datetime.now(ZoneInfo("Europe/Berlin")).date().isoformat()
        count = 0

        for interaction in self.interactions:
            created_at = interaction.get("created_at")
            interaction_status = interaction.get("status")

            if isinstance(created_at, str) is False:
                continue

            if interaction_status != status:
                continue

            if created_at.startswith(today):
                count += 1

        return count

    def _count_today_by_type(self, interaction_type: str) -> int:
        today = datetime.now(ZoneInfo("Europe/Berlin")).date().isoformat()
        count = 0

        for interaction in self.interactions:
            created_at = interaction.get("created_at")
            current_type = interaction.get("interaction_type")

            if isinstance(created_at, str) is False:
                continue

            if current_type != interaction_type:
                continue

            if created_at.startswith(today):
                count += 1

        return count

18.7 Manuelle Freigabe

Kommentarvorschläge werden nicht direkt ausgeführt.

Freigabe per CLI:

python -m src.interaction_worker list --status suggested
python -m src.interaction_worker show interaction_20260512_001
python -m src.interaction_worker approve interaction_20260512_001
python -m src.interaction_worker reject interaction_20260512_001

Statuswechsel:

suggested → approved
suggested → rejected
approved → executed
approved → failed

Freigabe-Service:

class InteractionApprovalService:
    def __init__(self, repository: "InteractionRepository") -> None:
        self.repository = repository

    def approve(self, interaction_id: str) -> None:
        interaction = self.repository.find_by_id(interaction_id)

        if interaction.get("status") != "suggested":
            raise RuntimeError("Interaction must have status suggested.")

        interaction["status"] = "approved"
        interaction["approved_at"] = self._now()
        interaction["updated_at"] = self._now()

        self.repository.update(interaction)

    def reject(self, interaction_id: str) -> None:
        interaction = self.repository.find_by_id(interaction_id)

        if interaction.get("status") != "suggested":
            raise RuntimeError("Interaction must have status suggested.")

        interaction["status"] = "rejected"
        interaction["rejected_at"] = self._now()
        interaction["updated_at"] = self._now()

        self.repository.update(interaction)

    def _now(self) -> str:
        return datetime.now(ZoneInfo("Europe/Berlin")).isoformat()

18.8 Automatisches Liken nur nach Regeln

Automatisches Liken ist nur begrenzt sinnvoll.

Erlaubt nur, wenn:

Beitrag fachlich relevant
Risiko sehr niedrig
keine Politik
kein Streit
kein privates Thema
kein Verkaufs- oder Hype-Beitrag
Tageslimit nicht erreicht
Account nicht bereits mehrfach mit demselben Autor interagiert hat

Policy:

class LikePolicy:
    def can_like(self, analysis: InteractionAnalysisResult, source_text: str) -> bool:
        if analysis.should_like is False:
            return False

        if analysis.is_relevant is False:
            return False

        if analysis.relevance_score < 0.80:
            return False

        if analysis.risk_score > 0.15:
            return False

        lowered_text = source_text.lower()

        blocked_fragments = [
            "gewinnspiel",
            "giveaway",
            "politisch",
            "skandal",
            "wut",
            "hass",
            "krieg",
            "nazi",
        ]

        for blocked_fragment in blocked_fragments:
            if blocked_fragment in lowered_text:
                return False

        return True

Im MVP wird Like nur als Kandidat gespeichert, nicht automatisch ausgeführt.

{
  "id": "interaction_20260512_002",
  "platform": "linkedin",
  "interaction_type": "like_candidate",
  "source_url": "https://www.linkedin.com/feed/update/...",
  "source_author": "Beispiel Verlag",
  "source_text": "Beitrag über digitale Buchproduktion ...",
  "relevance_score": 0.91,
  "risk_score": 0.04,
  "status": "approved",
  "suggested_text": null
}

18.9 Beispiel: interaction_worker.py

Datei:

src/interaction_worker.py

import argparse
import json
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo


class InteractionWorkerError(Exception):
    pass


@dataclass(frozen=True)
class InteractionAnalysisResult:
    is_relevant: bool
    relevance_score: float
    risk_score: float
    should_comment: bool
    should_like: bool
    reason: str


@dataclass(frozen=True)
class CommentSuggestionResult:
    suggested_text: str | None
    status: str


class InteractionIdFactory:
    def create(self) -> str:
        now = datetime.now(ZoneInfo("Europe/Berlin"))

        return "interaction_" + now.strftime("%Y%m%d_%H%M%S_%f")


class InteractionStore:
    def __init__(self, file_path: Path) -> None:
        self.file_path = file_path

    def load(self) -> list[dict[str, Any]]:
        if self.file_path.exists() is False:
            return []

        with self.file_path.open("r", encoding="utf-8") as file:
            payload = json.load(file)

        if isinstance(payload, list) is False:
            raise InteractionWorkerError("Interaction JSON must contain a list.")

        interactions: list[dict[str, Any]] = []

        for item in payload:
            if isinstance(item, dict) is False:
                raise InteractionWorkerError("Interaction JSON contains non-object item.")

            interactions.append(item)

        return interactions

    def save(self, interactions: list[dict[str, Any]]) -> None:
        self.file_path.parent.mkdir(parents=True, exist_ok=True)

        with self.file_path.open("w", encoding="utf-8") as file:
            json.dump(interactions, file, ensure_ascii=False, indent=2)
            file.write("\n")


class InteractionRepository:
    def __init__(self, store: InteractionStore) -> None:
        self.store = store

    def append(self, interaction: dict[str, Any]) -> None:
        interactions = self.store.load()
        interactions.append(interaction)
        self.store.save(interactions)

    def list_by_status(self, status: str) -> list[dict[str, Any]]:
        interactions = self.store.load()
        result: list[dict[str, Any]] = []

        for interaction in interactions:
            if interaction.get("status") == status:
                result.append(interaction)

        return result

    def find_by_id(self, interaction_id: str) -> dict[str, Any]:
        interactions = self.store.load()

        for interaction in interactions:
            if interaction.get("id") == interaction_id:
                return interaction

        raise InteractionWorkerError("Interaction not found: " + interaction_id)

    def update(self, updated_interaction: dict[str, Any]) -> None:
        interactions = self.store.load()
        updated_interactions: list[dict[str, Any]] = []
        found = False

        updated_id = updated_interaction.get("id")

        for interaction in interactions:
            if interaction.get("id") == updated_id:
                updated_interactions.append(updated_interaction)
                found = True
            else:
                updated_interactions.append(interaction)

        if found is False:
            raise InteractionWorkerError("Interaction not found: " + str(updated_id))

        self.store.save(updated_interactions)


class InteractionRejectPolicy:
    def __init__(self) -> None:
        self.blocked_fragments = [
            "afd",
            "cdu",
            "spd",
            "grüne",
            "fdp",
            "krieg",
            "impfung",
            "corona",
            "flüchtlinge",
            "israel",
            "palästina",
            "gaza",
            "ukraine",
            "russland",
            "trump",
            "putin",
            "hitler",
            "nazi",
        ]

    def assert_allowed(self, source_text: str) -> None:
        cleaned_text = source_text.strip()

        if cleaned_text == "":
            raise InteractionWorkerError("Interaction source text must not be empty.")

        lowered_text = cleaned_text.lower()

        for blocked_fragment in self.blocked_fragments:
            if blocked_fragment in lowered_text:
                raise InteractionWorkerError("Blocked interaction topic found: " + blocked_fragment)

        if len(cleaned_text) > 5000:
            raise InteractionWorkerError("Interaction source text is too long.")


class InteractionDecisionPolicy:
    def can_suggest_comment(self, analysis: InteractionAnalysisResult) -> bool:
        if analysis.is_relevant is False:
            return False

        if analysis.should_comment is False:
            return False

        if analysis.relevance_score < 0.70:
            return False

        if analysis.risk_score > 0.25:
            return False

        return True

    def can_like(self, analysis: InteractionAnalysisResult) -> bool:
        if analysis.is_relevant is False:
            return False

        if analysis.should_like is False:
            return False

        if analysis.relevance_score < 0.80:
            return False

        if analysis.risk_score > 0.15:
            return False

        return True


class CommentSuggestionValidator:
    def validate(self, suggested_text: str) -> None:
        cleaned_text = suggested_text.strip()

        if cleaned_text == "":
            raise InteractionWorkerError("Suggested comment must not be empty.")

        if len(cleaned_text) > 500:
            raise InteractionWorkerError("Suggested comment exceeds length limit.")

        blocked_fragments = [
            "jetzt kaufen",
            "hier klicken",
            "mein shop",
            "unbedingt lesen",
            "follow",
            "like",
            "dm",
            "pn",
            "privatnachricht",
        ]

        lowered_text = cleaned_text.lower()

        for blocked_fragment in blocked_fragments:
            if blocked_fragment in lowered_text:
                raise InteractionWorkerError("Blocked comment fragment found: " + blocked_fragment)

        if "https://" in lowered_text:
            raise InteractionWorkerError("Suggested comment must not contain links.")


class DailyInteractionLimiter:
    def __init__(self, interactions: list[dict[str, Any]]) -> None:
        self.interactions = interactions

    def assert_comment_suggestion_allowed(self, max_per_day: int) -> None:
        count = self._count_today_by_status("suggested")

        if count >= max_per_day:
            raise InteractionWorkerError("Daily comment suggestion limit reached.")

    def assert_like_allowed(self, max_per_day: int) -> None:
        count = self._count_today_by_type("like_candidate")

        if count >= max_per_day:
            raise InteractionWorkerError("Daily like limit reached.")

    def _count_today_by_status(self, status: str) -> int:
        today = datetime.now(ZoneInfo("Europe/Berlin")).date().isoformat()
        count = 0

        for interaction in self.interactions:
            created_at = interaction.get("created_at")
            interaction_status = interaction.get("status")

            if isinstance(created_at, str) is False:
                continue

            if interaction_status != status:
                continue

            if created_at.startswith(today):
                count += 1

        return count

    def _count_today_by_type(self, interaction_type: str) -> int:
        today = datetime.now(ZoneInfo("Europe/Berlin")).date().isoformat()
        count = 0

        for interaction in self.interactions:
            created_at = interaction.get("created_at")
            current_type = interaction.get("interaction_type")

            if isinstance(created_at, str) is False:
                continue

            if current_type != interaction_type:
                continue

            if created_at.startswith(today):
                count += 1

        return count


class RuleBasedInteractionAnalyzer:
    def analyze(self, source_text: str) -> InteractionAnalysisResult:
        lowered_text = source_text.lower()

        relevant_fragments = [
            "buch",
            "bücher",
            "verlag",
            "ebook",
            "e-book",
            "ki",
            "automatisierung",
            "literatur",
            "public domain",
            "gemeinfrei",
            "metadaten",
            "digitalisierung",
        ]

        relevance_score = 0.0

        for fragment in relevant_fragments:
            if fragment in lowered_text:
                relevance_score += 0.15

        if relevance_score > 1.0:
            relevance_score = 1.0

        risk_score = 0.0

        risky_fragments = [
            "politik",
            "krieg",
            "skandal",
            "streit",
            "hass",
            "wut",
        ]

        for fragment in risky_fragments:
            if fragment in lowered_text:
                risk_score += 0.25

        if risk_score > 1.0:
            risk_score = 1.0

        is_relevant = relevance_score >= 0.30
        should_comment = relevance_score >= 0.70 and risk_score <= 0.25
        should_like = relevance_score >= 0.80 and risk_score <= 0.15

        return InteractionAnalysisResult(
            is_relevant=is_relevant,
            relevance_score=relevance_score,
            risk_score=risk_score,
            should_comment=should_comment,
            should_like=should_like,
            reason="rule_based_analysis",
        )


class RuleBasedCommentSuggestionService:
    def suggest(self, source_text: str) -> CommentSuggestionResult:
        lowered_text = source_text.lower()

        if "verlag" in lowered_text or "buch" in lowered_text or "literatur" in lowered_text:
            return CommentSuggestionResult(
                suggested_text=(
                    "Der Punkt zur Sichtbarkeit kleiner Anbieter ist im Buchmarkt ähnlich. "
                    "Technisch kann ein Angebot sehr gut sein, aber Auffindbarkeit entscheidet oft früher als Qualität."
                ),
                status="suggested",
            )

        return CommentSuggestionResult(
            suggested_text=None,
            status="rejected",
        )


class InteractionWorkerService:
    def __init__(
        self,
        repository: InteractionRepository,
        id_factory: InteractionIdFactory,
        reject_policy: InteractionRejectPolicy,
        decision_policy: InteractionDecisionPolicy,
        analyzer: RuleBasedInteractionAnalyzer,
        suggestion_service: RuleBasedCommentSuggestionService,
        comment_validator: CommentSuggestionValidator,
    ) -> None:
        self.repository = repository
        self.id_factory = id_factory
        self.reject_policy = reject_policy
        self.decision_policy = decision_policy
        self.analyzer = analyzer
        self.suggestion_service = suggestion_service
        self.comment_validator = comment_validator

    def create_candidate(
        self,
        platform: str,
        interaction_type: str,
        source_url: str,
        source_author: str,
        source_text: str,
    ) -> None:
        self.reject_policy.assert_allowed(source_text)

        now = self._now()

        interaction = {
            "id": self.id_factory.create(),
            "platform": platform,
            "interaction_type": interaction_type,
            "source_url": source_url,
            "source_author": source_author,
            "source_text": source_text,
            "relevance_score": 0.0,
            "risk_score": 0.0,
            "status": "candidate",
            "suggested_text": None,
            "created_at": now,
            "updated_at": now,
        }

        self.repository.append(interaction)

        print("CANDIDATE_CREATED")
        print(interaction["id"])

    def analyze_candidates(self) -> None:
        candidates = self.repository.list_by_status("candidate")
        analyzed_count = 0

        for candidate in candidates:
            source_text = candidate.get("source_text")

            if isinstance(source_text, str) is False:
                candidate["status"] = "failed"
                candidate["error_message"] = "source_text must be string"
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)
                continue

            try:
                self.reject_policy.assert_allowed(source_text)
                analysis = self.analyzer.analyze(source_text)

                candidate["relevance_score"] = analysis.relevance_score
                candidate["risk_score"] = analysis.risk_score
                candidate["analysis_reason"] = analysis.reason

                if self.decision_policy.can_suggest_comment(analysis) is True:
                    candidate["status"] = "analyzed"
                elif self.decision_policy.can_like(analysis) is True:
                    candidate["status"] = "approved"
                    candidate["interaction_type"] = "like_candidate"
                else:
                    candidate["status"] = "rejected"

                candidate["updated_at"] = self._now()
                self.repository.update(candidate)
                analyzed_count += 1
            except InteractionWorkerError as exception:
                candidate["status"] = "blocked"
                candidate["error_message"] = str(exception)
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)

        print("ANALYZED")
        print(str(analyzed_count))

    def suggest_comments(self) -> None:
        interactions = self.repository.store.load()
        limiter = DailyInteractionLimiter(interactions)
        limiter.assert_comment_suggestion_allowed(max_per_day=10)

        analyzed_candidates = self.repository.list_by_status("analyzed")
        suggested_count = 0

        for candidate in analyzed_candidates:
            source_text = candidate.get("source_text")

            if isinstance(source_text, str) is False:
                candidate["status"] = "failed"
                candidate["error_message"] = "source_text must be string"
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)
                continue

            suggestion = self.suggestion_service.suggest(source_text)

            if suggestion.status != "suggested":
                candidate["status"] = "rejected"
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)
                continue

            if suggestion.suggested_text is None:
                candidate["status"] = "rejected"
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)
                continue

            try:
                self.comment_validator.validate(suggestion.suggested_text)

                candidate["suggested_text"] = suggestion.suggested_text
                candidate["status"] = "suggested"
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)
                suggested_count += 1
            except InteractionWorkerError as exception:
                candidate["status"] = "rejected"
                candidate["error_message"] = str(exception)
                candidate["updated_at"] = self._now()
                self.repository.update(candidate)

        print("SUGGESTED")
        print(str(suggested_count))

    def list_by_status(self, status: str) -> None:
        interactions = self.repository.list_by_status(status)

        print("ID                              PLATFORM    TYPE              STATUS")
        print("-----------------------------------------------------------------------")

        for interaction in interactions:
            print(
                str(interaction.get("id", "")).ljust(31)
                + " "
                + str(interaction.get("platform", "")).ljust(11)
                + " "
                + str(interaction.get("interaction_type", "")).ljust(17)
                + " "
                + str(interaction.get("status", ""))
            )

    def show(self, interaction_id: str) -> None:
        interaction = self.repository.find_by_id(interaction_id)

        print("ID:       " + str(interaction.get("id")))
        print("Platform: " + str(interaction.get("platform")))
        print("Type:     " + str(interaction.get("interaction_type")))
        print("Status:   " + str(interaction.get("status")))
        print("Author:   " + str(interaction.get("source_author")))
        print("URL:      " + str(interaction.get("source_url")))
        print("Relevance:" + str(interaction.get("relevance_score")))
        print("Risk:     " + str(interaction.get("risk_score")))
        print("")
        print("Source:")
        print(str(interaction.get("source_text")))
        print("")
        print("Suggested:")
        print(str(interaction.get("suggested_text")))

    def approve(self, interaction_id: str) -> None:
        interaction = self.repository.find_by_id(interaction_id)

        if interaction.get("status") != "suggested":
            raise InteractionWorkerError("Interaction must have status suggested.")

        interaction["status"] = "approved"
        interaction["approved_at"] = self._now()
        interaction["updated_at"] = self._now()

        self.repository.update(interaction)

        print("APPROVED")
        print(interaction_id)

    def reject(self, interaction_id: str) -> None:
        interaction = self.repository.find_by_id(interaction_id)

        current_status = interaction.get("status")

        if current_status != "suggested" and current_status != "candidate" and current_status != "analyzed":
            raise InteractionWorkerError("Interaction status cannot be rejected manually.")

        interaction["status"] = "rejected"
        interaction["rejected_at"] = self._now()
        interaction["updated_at"] = self._now()

        self.repository.update(interaction)

        print("REJECTED")
        print(interaction_id)

    def _now(self) -> str:
        return datetime.now(ZoneInfo("Europe/Berlin")).isoformat()


def build_service(root_dir: Path) -> InteractionWorkerService:
    store = InteractionStore(root_dir / "data" / "interaction_candidates.json")
    repository = InteractionRepository(store)
    id_factory = InteractionIdFactory()
    reject_policy = InteractionRejectPolicy()
    decision_policy = InteractionDecisionPolicy()
    analyzer = RuleBasedInteractionAnalyzer()
    suggestion_service = RuleBasedCommentSuggestionService()
    comment_validator = CommentSuggestionValidator()

    return InteractionWorkerService(
        repository=repository,
        id_factory=id_factory,
        reject_policy=reject_policy,
        decision_policy=decision_policy,
        analyzer=analyzer,
        suggestion_service=suggestion_service,
        comment_validator=comment_validator,
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)

    create_parser = subparsers.add_parser("create-candidate")
    create_parser.add_argument("--platform", required=True)
    create_parser.add_argument("--type", required=True)
    create_parser.add_argument("--url", required=True)
    create_parser.add_argument("--author", required=True)
    create_parser.add_argument("--text", required=True)

    subparsers.add_parser("analyze")
    subparsers.add_parser("suggest-comments")

    list_parser = subparsers.add_parser("list")
    list_parser.add_argument("--status", required=True)

    show_parser = subparsers.add_parser("show")
    show_parser.add_argument("interaction_id")

    approve_parser = subparsers.add_parser("approve")
    approve_parser.add_argument("interaction_id")

    reject_parser = subparsers.add_parser("reject")
    reject_parser.add_argument("interaction_id")

    args = parser.parse_args()

    root_dir = Path(__file__).resolve().parent.parent
    service = build_service(root_dir)

    if args.command == "create-candidate":
        service.create_candidate(
            platform=args.platform,
            interaction_type=args.type,
            source_url=args.url,
            source_author=args.author,
            source_text=args.text,
        )
        return

    if args.command == "analyze":
        service.analyze_candidates()
        return

    if args.command == "suggest-comments":
        service.suggest_comments()
        return

    if args.command == "list":
        service.list_by_status(args.status)
        return

    if args.command == "show":
        service.show(args.interaction_id)
        return

    if args.command == "approve":
        service.approve(args.interaction_id)
        return

    if args.command == "reject":
        service.reject(args.interaction_id)
        return

    raise InteractionWorkerError("Unsupported command: " + str(args.command))


if __name__ == "__main__":
    main()

Beispielaufrufe

Kandidat manuell anlegen:

python -m src.interaction_worker create-candidate \
  --platform linkedin \
  --type external_comment \
  --url "https://www.linkedin.com/feed/update/example" \
  --author "Max Beispiel" \
  --text "Kleine Anbieter haben oft gute Produkte, aber zu wenig Sichtbarkeit."

Kandidaten analysieren:

python -m src.interaction_worker analyze

Kommentarvorschläge erzeugen:

python -m src.interaction_worker suggest-comments

Vorschläge anzeigen:

python -m src.interaction_worker list --status suggested

Einzelnen Vorschlag prüfen:

python -m src.interaction_worker show interaction_20260512_100000_123456

Freigeben:

python -m src.interaction_worker approve interaction_20260512_100000_123456

Ablehnen:

python -m src.interaction_worker reject interaction_20260512_100000_123456

Ergebnis dieses Kapitels

Das Interaktionssystem kann jetzt:

Interaktionskandidaten speichern
Beiträge regelbasiert analysieren
Relevanz und Risiko bewerten
Kommentarvorschläge erzeugen
Ausschlussregeln anwenden
Tageslimits prüfen
manuelle Freigabe abbilden
Like-Kandidaten nur nach strengen Regeln markieren
Interaktionen nachvollziehbar speichern

Kommentare werden weiterhin nicht automatisch veröffentlicht. Das System erzeugt eine kontrollierte Arbeitsliste statt einer unkontrollierten Kommentarautomatik.

0 Kommentare