AI Enhancement Commands
Commands für AI-gesteuerte Profil-Anreicherung: Spracherkennung, Kategorisierung und Tag-Konsolidierung.
Alle AI-Commands nutzen die Gemini API und teilen sich die ai-detection Queue mit Rate-Limiting.
social:detect-languages
Erkennt Sprache, Land und Kategorie von Social Profiles via Gemini AI.
Verarbeitet Profile entweder synchron oder dispatcht Jobs asynchron in die ai-detection Queue.
social:detect-languages
{--limit=100 : Maximale Anzahl Profile (synchron)}
{--force : Auch bereits erkannte Profile neu erkennen}
{--profile= : Bestimmte Profile-ID}
{--queue : Jobs in Queue dispatchen statt synchron}
{--queue-limit=500 : Maximale Profile pro Queue-Run}
{--flush : Alle pending ai-detection Jobs entfernen}
Beispiele
# Synchrone Verarbeitung (Development/Debugging)
php artisan social:detect-languages --limit=50
# Asynchron in Queue dispatchen (Production)
php artisan social:detect-languages --queue --queue-limit=100
# Bestimmtes Profil force-redetecten
php artisan social:detect-languages --profile=12345 --force
# Queue komplett leeren
php artisan social:detect-languages --flush
Optionen
| Option | Beschreibung |
|---|---|
--limit=100 | Max Profile (synchroner Modus) |
--force | Re-Detection trotz vorhandener Daten |
--profile= | Einzelnes Profil via ID |
--queue | Queue-Modus (async via DetectProfileLanguage Jobs) |
--queue-limit=500 | Max Profile pro Queue-Dispatch |
--flush | Alle pending Jobs aus ai-detection Queue löschen |
Interne Schritte (Queue-Modus)
- Hard-Cap prüfen: Queue-Tiefe gegen Limit prüfen. Überspringen wenn voll
- Profile laden: Sortiert nach Follower-Anzahl (populäre Profile zuerst)
- Filter: Nur Profile mit Description, denen mindestens eines der fünf erkannten Felder fehlt (
needsAiEnrichment()Scope), außerhalb Cooldown-Periode. Geblockte, sanitized und archivierte Profile werden ausgeschlossen (notExcluded()Scope) - Dispatch:
DetectProfileLanguageJobs inai-detectionQueue - Rate-Limiting: Job-Middleware
RateLimited('gemini-api')begrenzt auf 15 Requests/Minute. Rate-limited Jobs werden mit Delay zurueck in die Queue gelegt (nicht geloescht).ShouldBeUnique(30-Min Lock viauniqueFor()) verhindert Duplikate
Interne Schritte (Synchron-Modus)
- Profile laden (nach denselben Kriterien wie Queue-Modus)
- Denselben
DetectProfileLanguage-Job pro Profil synchron ausführen - 250ms Pause zwischen Requests (4 RPS max)
⚠️ Seit v2.11.0 führt der synchrone Modus denselben Job aus wie der Queue-Modus. Vorher
hatte er eine eigene Speicher-Logik, die nur detected_country und detected_language
schrieb — Kategorie, Keywords und Beschreibung aus derselben Gemini-Antwort gingen dabei
verloren. Zwei Speicher-Pfade für ein Ergebnis waren genau die Drift, die das verursacht hat.
Welche Profile ausgewählt werden — und warum das einmal falsch war
Der Enhancer liefert fünf Felder: detected_language, detected_country, ai_category,
ai_keywords und ai_description. Ein Profil wird ausgewählt, sobald eines davon fehlt.
Ein manuell gesetztes Feld (ai_manual_*) zählt dabei als vorhanden — dieselbe Vorrangregel,
die der Public Explorer bei der Aufnahme anwendet.
⚠️ Bis v2.11.0 fragte die Auswahl nur nach Sprache und Land. Ein Profil kam nur dran, wenn ihm eines von beiden fehlte. Grosse, gepflegte Kanäle bekommen jedoch beides von der Plattform mitgeliefert — sie wurden deshalb nie ausgewählt, blieben dauerhaft ohne Kategorie, Keywords und Beschreibung und fielen damit aus dem Public Explorer, der genau diese drei als Pflichtfelder liest. Je besser ein Kanal gepflegt war, desto sicherer griff der Ausschluss.
Die Regel liegt jetzt an einer Stelle (SocialProfile::needsAiEnrichment() als Scope plus
hasCompleteAiEnrichment() für die Einzelzeile) statt in vier handkopierten Fassungen. Ein
Test über 32 Feld-Kombinationen hält beide Fassungen gegeneinander.
Cooldown-System
Profile die kürzlich erkannt wurden, werden übersprungen:
- Default Cooldown: 365 Tage (
services.gemini.cooldown_days) - Mit
--forcewird der Cooldown ignoriert
⚠️ --force erreichte den Job bis v2.11.0 nicht. Der Schalter weitete nur die Auswahl im
Befehl aus; der Job stieg danach an seinem eigenen Riegel wieder aus, und ein erzwungener
Queue-Lauf war wirkungslos — ohne dass etwas rot wurde. Der Job nimmt force jetzt entgegen
und überspringt damit sowohl den Cooldown als auch die Vollständigkeitsprüfung.
Dasselbe galt für den Knopf „AI-Enhancer starten" auf der Watcher-Detailseite: Er setzte
detected_at auf null, um den Cooldown zu umgehen. Das löschte eine echte Information und
öffnete nur einen der beiden Riegel — der Knopf war ausgerechnet für die Profile wirkungslos,
für die man ihn drückt. Er nutzt jetzt dasselbe force.
Schedule
Alle 15 Minuten mit --queue und dynamischem --queue-limit (= hourly_limit / 4).
Das Hourly-Limit kommt aus AI_ENHANCER_HOURLY_LIMIT (Default: 100).
Location: app/Console/Commands/DetectChannelLanguages.php
ai:prune-logs
Löscht fehlgeschlagene AI Detection Logs nach dem Retention-Fenster. Erfolgreiche Detections bleiben permanent erhalten, da sie wertvolle Sprach-/Land-Daten enthalten.
ai:prune-logs
{--days=7 : Aufbewahrungsdauer für fehlgeschlagene Logs}
Beispiele
# Standard: Failed Logs älter als 7 Tage löschen
php artisan ai:prune-logs
# Kürzere Retention
php artisan ai:prune-logs --days=3
# Längere Retention
php artisan ai:prune-logs --days=14
Optionen
| Option | Beschreibung |
|---|---|
--days=7 | Anzahl Tage die Failed Logs behalten werden |
Interne Schritte
AiDetectionLogRecords mitstatus = 'error'selektieren- Filter:
created_atälter als angegebene Tage - Records löschen
- Gelöschte Anzahl ausgeben
Hinweise
- Nur
error-Logs werden gelöscht -- erfolgreiche Detections bleiben permanent - Typische Fehler: API-Timeouts, Rate-Limiting, ungültige Responses
- Die
AiDetectionLogTabelle speichert Request/Response für Debugging
Schedule
2x täglich: 02:30 und 14:30 UTC mit .withoutOverlapping().
Location: app/Console/Commands/PruneAiDetectionLogs.php
tags:consolidate
Wöchentliche AI-gesteuerte Tag-Konsolidierung via Gemini. Aggregiert alle AI-generierten Tags, identifiziert Low-Use-Tags und dispatcht Chunks zur Gemini-Analyse.
tags:consolidate
{--dry-run : Vorschau ohne Dispatch}
{--force : Cooldown ignorieren, alle Tags verarbeiten}
{--chunk-size= : Chunk-Größe überschreiben}
{--max-chunks= : Maximale Chunk-Anzahl überschreiben}
Beispiele
# Standard-Lauf (nur Low-Use Tags, Cooldown beachten)
php artisan tags:consolidate
# Vorschau: welche Chunks würden dispatched
php artisan tags:consolidate --dry-run
# Alle Tags verarbeiten, Cooldown ignorieren
php artisan tags:consolidate --force
# Angepasste Chunk-Größe
php artisan tags:consolidate --chunk-size=30 --max-chunks=50
Optionen
| Option | Beschreibung |
|---|---|
--dry-run | Zeigt Chunks ohne zu dispatchen |
--force | Cooldown ignorieren, alle Kandidaten verarbeiten |
--chunk-size= | Override für postbox.tag_consolidation.chunk_size (Default: 50) |
--max-chunks= | Override für postbox.tag_consolidation.max_chunks (Default: 100) |
Interne Schritte
- Guard: Queue-Tiefe prüfen (
ai-detectionQueue, max = max_chunks * 3) - Tags aggregieren: Alle
ai_keywordsaussocial_profiles(JSONB) zählen - Filtern: Blocked Tags, Stop Words, zu kurze Tags (< 3 Zeichen)
- Split: Tags in "established" (>= 5 Profile) und "candidates" (< 5 Profile) aufteilen
- Cooldown: Kürzlich konsolidierte Tags überspringen (90 Tage Default)
- Chunking: Candidates in Chunks aufteilen (max 50 Tags/Chunk, max 100 Chunks)
- Dispatch:
ConsolidateTagChunkJobs aufai-detectionQueue mit Batch-ID
Konsolidierungs-Aktionen
Gemini analysiert jeden Chunk und schlägt Aktionen vor:
| Aktion | Beschreibung |
|---|---|
merge | Tag in einen bestehenden "established" Tag mergen |
suppress | Tag auf Blocklist setzen (irrelevant) |
keep | Tag behalten (keine Aktion) |
High-Confidence Merges werden automatisch ausgeführt; Low-Confidence gehen an Admin-Review.
Konfiguration
// config/postbox.php → tag_consolidation
'chunk_size' => env('TAG_CONSOLIDATION_CHUNK_SIZE', 50),
'max_chunks' => env('TAG_CONSOLIDATION_MAX_CHUNKS', 100),
'established_threshold' => 5,
'min_tag_length' => 3,
'consolidation_cooldown_days' => 90,
'rate_per_minute' => 10,
Schedule
Sonntags 18:00 UTC (Low-Traffic Window) mit .withoutOverlapping().
Location: app/Console/Commands/ConsolidateTags.php
ai:retry-failed
Setzt permanent fehlgeschlagene AI-Detections zurück und dispatched sie erneut.
Profile mit ai_detection_failed_at Marker werden für einen neuen Versuch freigegeben.
ai:retry-failed
{--limit=50 : Maximale Anzahl Profile pro Retry}
{--dry-run : Vorschau ohne Dispatch}
Optionen
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--limit= | int | 50 | Maximale Anzahl Profile pro Lauf |
--dry-run | flag | – | Nur Vorschau, keine Jobs dispatchen |
Beispiele
# Standard: bis zu 50 fehlgeschlagene Profile retrien
php artisan ai:retry-failed
# Größerer Batch
php artisan ai:retry-failed --limit=200
# Vorschau: welche Profile wären betroffen?
php artisan ai:retry-failed --dry-run
Interne Logik
- Profile mit
ai_detection_failed_at IS NOT NULLfinden - Filter:
tracking_enabled = true,notExcluded()(nicht blocked/sanitized/archived) - Queue-Tiefe prüfen: Überspringt wenn Queue
ai-detectionbereits >max($limit, 100)Jobs hat (verhindert Job-Snowballing) ai_detection_failed_ataufnullzurücksetzenDetectProfileLanguageJob dispatchen
Schedule
Wöchentlich sonntags um 08:00 UTC.
Location: app/Console/Commands/RetryFailedAiDetection.php
keywords:update-stopwords
Analysiert Keyword-Verteilung und aktualisiert die dynamische Stopword-Liste. Keywords die in zu vielen Profilen vorkommen (z.B. "Official", "Music") werden als Stopwords markiert.
keywords:update-stopwords
{--threshold=30 : Prozent-Schwelle — Keywords in mehr als X% der Profile werden Stopwords}
{--min-profiles=100 : Mindestanzahl Profile mit Keywords bevor die Analyse startet}
{--dry-run : Vorschau ohne Speicherung}
Optionen
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--threshold= | int | 30 | Prozent-Schwelle für Stopword-Erkennung |
--min-profiles= | int | 100 | Mindestanzahl Profile bevor Analyse sinnvoll ist |
--dry-run | flag | – | Nur Vorschau, keine Cache-Aktualisierung |
Beispiele
# Standard: Stopwords für Keywords in > 30% aller Profile
php artisan keywords:update-stopwords
# Strengerer Schwellwert (> 20%)
php artisan keywords:update-stopwords --threshold=20
# Vorschau der erkannten Stopwords
php artisan keywords:update-stopwords --dry-run
Interne Logik
- Anzahl Profile mit Keywords zählen — überspringt wenn <
--min-profiles - Keywords via
LOWER(jsonb_array_elements_text(ai_keywords))extrahieren (PostgreSQL-spezifisch) - Keywords in >
threshold% der Profile als Stopwords identifizieren - Dynamische Stopword-Liste in Redis cachen (
keyword_dynamic_stopwords, TTL: 8 Tage) - Heartbeat in Cache für Health-Monitoring setzen
Hinweise
- PostgreSQL-spezifisch: Verwendet
jsonb_array_elements_text()— funktioniert nicht mit SQLite - Die dynamischen Stopwords ergänzen die statische Liste in der Keyword-Extraktion
- 8-Tage TTL stellt sicher, dass die Liste auch bei verpasstem Wochenlauf noch gültig ist
Schedule
Wöchentlich sonntags um 02:00 UTC.
Location: app/Console/Commands/UpdateKeywordStopwords.php
AI Enhancer Re-Run für gezielte Profile
Anleitung zum gezielten Re-Run der AI Enhancement auf bestimmte Profilgruppen (z.B. PRO-Profile, Favoriten). Nützlich nach Feature-Updates (neue Felder, bessere Prompts), um bestehende Profile mit der aktuellen AI-Logik neu zu verarbeiten.
Konzept
Die AI Enhancement wird über das detected_at Feld gesteuert. Profile mit detected_at innerhalb des Cooldowns (default 365 Tage) werden übersprungen. Um ein Re-Run zu erzwingen:
detected_ataufNULLsetzen → Profile erscheinen als "noch nicht erkannt"social:detect-languages --queuedispatcht sie in die Queue
Re-Run auf PRO-Profile
PRO-Profile sind via social_profiles.pro_enabled = true markiert.
-- Vorschau: Wie viele PRO-Profile betroffen?
SELECT COUNT(*) FROM social_profiles
WHERE pro_enabled = true
AND tracking_enabled = true
AND blocked_at IS NULL
AND sanitized_at IS NULL
AND archived_at IS NULL
AND detected_at IS NOT NULL;
-- Reset: detected_at auf NULL setzen
UPDATE social_profiles
SET detected_at = NULL, updated_at = NOW()
WHERE pro_enabled = true
AND tracking_enabled = true
AND blocked_at IS NULL
AND sanitized_at IS NULL
AND archived_at IS NULL
AND detected_at IS NOT NULL;
Re-Run auf Favoriten-Profile
Favoriten-Profile liegen in Workspaces mit name = 'Favoriten' (dem automatisch erstellten Favorites-Workspace pro User).
-- Vorschau: Wie viele Favoriten-Profile betroffen?
SELECT COUNT(DISTINCT sp.id) FROM social_profiles sp
JOIN watcher_sources ws ON ws.social_profile_id = sp.id
JOIN watchers w ON w.id = ws.watcher_id
JOIN workspaces wk ON wk.id = w.workspace_id
WHERE wk.name = 'Favoriten'
AND sp.tracking_enabled = true
AND sp.blocked_at IS NULL
AND sp.sanitized_at IS NULL
AND sp.archived_at IS NULL
AND sp.detected_at IS NOT NULL;
-- Reset: detected_at auf NULL setzen
UPDATE social_profiles
SET detected_at = NULL, updated_at = NOW()
WHERE id IN (
SELECT DISTINCT sp.id FROM social_profiles sp
JOIN watcher_sources ws ON ws.social_profile_id = sp.id
JOIN watchers w ON w.id = ws.watcher_id
JOIN workspaces wk ON wk.id = w.workspace_id
WHERE wk.name = 'Favoriten'
AND sp.tracking_enabled = true
AND sp.blocked_at IS NULL
AND sp.sanitized_at IS NULL
AND sp.archived_at IS NULL
AND sp.detected_at IS NOT NULL
);
Re-Run auf PRO + Favoriten kombiniert
UPDATE social_profiles
SET detected_at = NULL, updated_at = NOW()
WHERE tracking_enabled = true
AND blocked_at IS NULL
AND sanitized_at IS NULL
AND archived_at IS NULL
AND detected_at IS NOT NULL
AND (
pro_enabled = true
OR id IN (
SELECT DISTINCT ws.social_profile_id
FROM watcher_sources ws
JOIN watchers w ON w.id = ws.watcher_id
JOIN workspaces wk ON wk.id = w.workspace_id
WHERE wk.name = 'Favoriten'
)
);
Nach dem SQL-Reset
Der reguläre 15-Minuten-Scheduler (social:detect-languages --queue) pickt die Profile automatisch auf. Für schnellere Verarbeitung:
# Sofort einen größeren Batch dispatchen
php artisan social:detect-languages --queue --queue-limit=500
# Oder mit --force für alle (auch nicht-resetted) Profile
php artisan social:detect-languages --queue --queue-limit=200 --force
Monitoring
# Queue-Tiefe prüfen
php artisan queue:monitor ai-detection
# AI Detection Stats im Admin Dashboard
# → /admin/ai-enhancements
Hinweis: Das Daily Limit (AI_ENHANCER_DAILY_LIMIT, default 1400) gilt weiterhin. Bei großen Re-Runs verteilt sich die Verarbeitung über mehrere Tage.
AI Queue Worker
Alle drei AI-Commands teilen sich den ai-detection Queue Worker:
php8.4 artisan queue:work database --sleep=5 --daemon --quiet --timeout=60 --tries=3 --queue=ai-detection
Rate-Limiting erfolgt ueber Job-Middleware (RateLimited('gemini-api') mit 15 RPM). Rate-limited Jobs werden automatisch mit Delay zurueck in die Queue gelegt und bei naechster Gelegenheit verarbeitet. ShouldBeUnique mit 30-Minuten-Lock verhindert, dass der Scheduler Duplikate fuer dasselbe Profil dispatcht, waehrend ein verzoegerter Job noch aussteht.