Zum Hauptinhalt springen

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​

OptionBeschreibung
--limit=100Max Profile (synchroner Modus)
--forceRe-Detection trotz vorhandener Daten
--profile=Einzelnes Profil via ID
--queueQueue-Modus (async via DetectProfileLanguage Jobs)
--queue-limit=500Max Profile pro Queue-Dispatch
--flushAlle pending Jobs aus ai-detection Queue löschen

Interne Schritte (Queue-Modus)​

  1. Hard-Cap prüfen: Queue-Tiefe gegen Limit prüfen. Überspringen wenn voll
  2. Profile laden: Sortiert nach Follower-Anzahl (populäre Profile zuerst)
  3. 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)
  4. Dispatch: DetectProfileLanguage Jobs in ai-detection Queue
  5. 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 via uniqueFor()) verhindert Duplikate

Interne Schritte (Synchron-Modus)​

  1. Profile laden (nach denselben Kriterien wie Queue-Modus)
  2. Denselben DetectProfileLanguage-Job pro Profil synchron ausführen
  3. 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 --force wird 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​

OptionBeschreibung
--days=7Anzahl Tage die Failed Logs behalten werden

Interne Schritte​

  1. AiDetectionLog Records mit status = 'error' selektieren
  2. Filter: created_at älter als angegebene Tage
  3. Records löschen
  4. 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 AiDetectionLog Tabelle 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​

OptionBeschreibung
--dry-runZeigt Chunks ohne zu dispatchen
--forceCooldown 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​

  1. Guard: Queue-Tiefe prüfen (ai-detection Queue, max = max_chunks * 3)
  2. Tags aggregieren: Alle ai_keywords aus social_profiles (JSONB) zählen
  3. Filtern: Blocked Tags, Stop Words, zu kurze Tags (< 3 Zeichen)
  4. Split: Tags in "established" (>= 5 Profile) und "candidates" (< 5 Profile) aufteilen
  5. Cooldown: Kürzlich konsolidierte Tags überspringen (90 Tage Default)
  6. Chunking: Candidates in Chunks aufteilen (max 50 Tags/Chunk, max 100 Chunks)
  7. Dispatch: ConsolidateTagChunk Jobs auf ai-detection Queue mit Batch-ID

Konsolidierungs-Aktionen​

Gemini analysiert jeden Chunk und schlägt Aktionen vor:

AktionBeschreibung
mergeTag in einen bestehenden "established" Tag mergen
suppressTag auf Blocklist setzen (irrelevant)
keepTag 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​

OptionTypDefaultBeschreibung
--limit=int50Maximale Anzahl Profile pro Lauf
--dry-runflag–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​

  1. Profile mit ai_detection_failed_at IS NOT NULL finden
  2. Filter: tracking_enabled = true, notExcluded() (nicht blocked/sanitized/archived)
  3. Queue-Tiefe prüfen: Überspringt wenn Queue ai-detection bereits > max($limit, 100) Jobs hat (verhindert Job-Snowballing)
  4. ai_detection_failed_at auf null zurücksetzen
  5. DetectProfileLanguage Job 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​

OptionTypDefaultBeschreibung
--threshold=int30Prozent-Schwelle für Stopword-Erkennung
--min-profiles=int100Mindestanzahl Profile bevor Analyse sinnvoll ist
--dry-runflag–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​

  1. Anzahl Profile mit Keywords zählen — überspringt wenn < --min-profiles
  2. Keywords via LOWER(jsonb_array_elements_text(ai_keywords)) extrahieren (PostgreSQL-spezifisch)
  3. Keywords in > threshold% der Profile als Stopwords identifizieren
  4. Dynamische Stopword-Liste in Redis cachen (keyword_dynamic_stopwords, TTL: 8 Tage)
  5. 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:

  1. detected_at auf NULL setzen → Profile erscheinen als "noch nicht erkannt"
  2. social:detect-languages --queue dispatcht 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.