18. Interaktionssystem
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