Kostenlos testen
Handbuch

slash-commands

Slash-Commands

User-getriggerte Delegation an einen Assistant, direkt aus der Chat-Eingabe:

/gutscheine versandkostenfrei 10€

heisst: nimm assistants.gutscheine, gib ihm versandkostenfrei 10€ als Task, zeig die Antwort im Chat. Das ist dieselbe Mechanik wie invoke_assistant — nur vom Nutzer ausgeloest statt vom Master-LLM.

Ein Command IST ein Assistant-Slug

Es gibt kein zusaetzliches Config-Feld. Jeder Assistant des Tenants ist automatisch als Command aufrufbar, der Slug ist der Command-Name:

Assistant-Slug Command
gutscheine /gutscheine
kpi /kpi
amazon_files /amazon_files

Damit gibt es nur eine Quelle der Wahrheit: legt der Builder einen Assistant an, existiert der Command sofort mit — ohne zweiten Eintrag, der veralten koennte.

Nicht verwendet wird das Feld name — das ist der Anzeigename und enthaelt Leerzeichen, Umlaute und Sonderzeichen (Amazon-Dateien auflisten, Artikel & Preise), taugt also nicht als Command.

Built-ins

Zwei Commands gehoeren zu keinem Assistant und gelten in jedem Tenant:

Command Wirkung
/help Liste aller Commands des Tenants
/clear Setzt den Chat zurueck — Pendant zum Reset-Button der Web-UI

Built-ins gewinnen gegen gleichnamige Assistants: ein Tenant-Assistant namens clear wuerde das Zuruecksetzen sonst unerreichbar machen. Sie stehen auch dann in /help, wenn der Tenant gar keine Assistants hat.

Warum /clear

In der Web-UI und in der Flutter-App gibt es einen Reset-Button (GET /chat/reset). In den Messaging-Kanaelen gibt es keine UI — ohne /clear liesse sich ein WhatsApp-Thread nie zuruecksetzen und sein Kontext wuechse unbegrenzt weiter.

/clear macht exakt dasselbe wie der Button (gemeinsamer ChatResetService): alle Messages des Chats auf deleted, hochgeladene Dateien von der Platte, ToolState-Files und Cancel-Flag weg. Die chat_id bleibt bestehen.

Zwei Feinheiten:

  • Der Command laeuft vor dem Persistieren der User-Message. Die Bestaetigung wird bewusst nicht persistiert — sonst waere der geleerte Chat sofort wieder einen Eintrag lang und der naechste Turn saehe eine Assistant-Message ohne User-Message davor. Sie geht nur an den Sink, ist also im Web sichtbar und wird per Channel-Adapter als WhatsApp-Nachricht zugestellt.
  • In Kanaelen hat der Ingest die /clear-Message bereits in chat_messages geschrieben. Der Reset raeumt sie mit ab, weil er alle nicht-geloeschten Rows des Chats erfasst.

Ausfuehrung: direkt, ohne Master-LLM

Ein Slash-Command ueberspringt den Master-Turn komplett. Der Nutzer hat den Ziel-Assistenten explizit benannt — es gibt nichts zu entscheiden, also entscheidet auch kein LLM. Das spart einen Roundtrip und macht das Verhalten deterministisch.

Konsequenz: der Sub-Assistant sieht keinen Verlauf des Master-Chats. Er bekommt nur seinen eigenen System-Prompt und den Task. Ein Nachfassen ("und jetzt nochmal mit 15€") landet wieder beim Master-Assistant, nicht beim Sub-Assistant.

Verhalten im Detail

Eingabe Ergebnis
/gutscheine versandkostenfrei 10€ Sub-Chat gegen gutscheine, Antwort im Master-Chat
/gutscheine (ohne Argument) Greeting + suggestions/toggles/inputs des Assistenten
/help Liste aller Commands des Tenants
/clear Chat zurueckgesetzt, Bestaetigung als einzige Ausgabe
/gutschein 10€ (Tippfehler) Fehlermeldung mit Vorschlag /gutscheine
reset das mal kein Command — geht normal ans LLM
kannst du /clear machen kein Command — / steht nicht am Zeilenanfang

Guards

Striktes Matching. Nur ^/slug am Zeilenanfang, nur [a-z0-9_]. Kein Fuzzy-Matching, kein Substring-Match. Historischer Hintergrund: der alte Reset-Regex im ChatController matchte auf Substrings und loeschte bei "reset das mal" ueberraschend Chats — er wurde am 2026-08-01 deaktiviert (ChatController.php, Kommentar "Der Chat-Text darf nicht mehr als Trigger dienen"). Diese Lehre gilt hier weiter: /clear ersetzt den alten Regex durch einen expliziten Trigger — reset das mal loest nach wie vor nichts aus, nur ein /clear am Zeilenanfang.

Unbekannte Commands raten nicht. Trifft ein Slug keinen Assistant, kommt eine Fehlermeldung mit Aehnlichkeitsvorschlag — es wird nie ein anderer Assistant "hilfsweise" ausgefuehrt.

Uploads gewinnen. Eine Message mit Bild ist nie ein Command, auch wenn der Text mit / beginnt.

In Sub-Chats deaktiviert. Dort ist die "User"-Message der Task eines Master-Assistants, kein Nutzer-Tippen. Ein LLM, das seinen Task mit / beginnt, loest keinen Command aus. Erkennung ueber den sub_-Prefix der Chat-ID.

Tool-Allowlist. Fuer die Dauer des Commands wird die Allowlist des ToolRunners eng auf invoke_assistant gesetzt und danach exakt wiederhergestellt — auch wenn das Tool eine Exception wirft.

Recursion-Guard. Weil der Command ueber invoke_assistant laeuft, gilt dieselbe MAX_DEPTH = 2 wie bei der LLM-getriebenen Delegation.

Autocomplete

GET /chat/commands?tenant=X liefert die Command-Liste aus der Tenant-Config (inkl. Built-ins, jeweils mit builtin: true|false), damit ein Frontend ein Menue rendern kann, ohne eine eigene Kopie zu pflegen. Anonyme Nutzer bekommen eine leere Liste, weil Commands an interne Assistants delegieren.

Stand heute hat der Endpoint keinen Consumer — in keinem der ausgelieferten Frontends (chat.js, chat-internal.js, chat-icon.js, Flutter) existiert ein Autocomplete-Menue. Getippte Commands funktionieren ueberall, nur eben ohne Vorschlagsliste. In Kanaelen ohne UI ist /help der Weg, die verfuegbaren Commands zu erfahren.

Sichtbarkeit vs. Erreichbarkeit

hidden => true-Assistants (z.B. config_reviewer) tauchen nicht in /help und nicht im Autocomplete auf, bleiben aber per Direkteingabe aufrufbar. Die Liste ist Kuratierung, keine Zugriffskontrolle — der eigentliche Schutz sind die Tool-Allowlists der Assistants.

Code

Datei Rolle
src/Service/Chat/SlashCommandResolver.php Parsing, Aufloesung, Built-ins, /help-Rendering
src/Service/Chat/SlashCommandRunner.php Ausfuehrung, Sink-Events, Fehlerbehandlung
src/Service/Chat/ChatResetService.php Reset-Logik fuer /clear, Reset-Button und resetChat
src/Service/Chat/ChatService.php Interception am Anfang von processMessage()
src/Controller/ChatController.php commands()-Endpoint, reset()-Action

Die Interception sitzt bewusst im ChatService, nicht im Controller — so gilt sie fuer /chat (JSON) und /chat/stream (SSE) gleichermassen, ohne Duplikat.

Wer darf Commands nutzen (fail-closed)

Ein Command startet jeden Assistenten des Workspaces samt Tools, /help listet alle Assistenten, /clear loescht den Verlauf. Deshalb fuehrt ChatService Commands nur aus, wenn der Aufrufer es ausdruecklich erlaubt (allowSlashCommands, Default aus). Die Regeln stehen in App\Service\Chat\SlashCommandPolicy (Sicherheits-Fix 03.10.2026):

Kanal Commands
Web-Chat / Flutter-App, eingeloggt ja (ChatController, forWebRequest)
Public-Widget (anonym oder anonymisiert) nein
public-Assistent (Slug public oder public: true) nein, auch eingeloggt
WhatsApp-Plattformnummer, registrierter Absender auf fester Route (routing.senders) oder eigener Wahl (/tenant, /assistant) ja fuer /<assistant> (autoReplyChannel setzt slash_commands aus der Routing-Entscheidung). /help und /clear beantwortet dort schon der ConversationRouter (/clear = /neu: Verlaufsgrenze, kein Loeschen), siehe docs/WhatsAppRouting.md.
WhatsApp Default-Route, CTWA-Demo, FB Messenger, Instagram nein
E-Mail / Inbox-Entwuerfe, eingehende Webhooks, Scheduled Tasks nein

Ohne Freigabe ist ein Text mit / eine normale Nachricht an den aktuellen Assistenten. HeadlessAssistantRunner nimmt die Option slash_commands (Default aus) und sperrt public-Assistenten zusaetzlich selbst.

In den Messaging-Kanaelen wird die Antwort vom ChannelAdapter::send() des jeweiligen Kanals zugestellt — fuer /clear also die Bestaetigung.

Tests

vendor/bin/phpunit tests/TestCase/Service/Chat/SlashCommandResolverTest.php
vendor/bin/phpunit tests/TestCase/Service/Chat/SlashCommandRunnerTest.php
vendor/bin/phpunit tests/TestCase/Service/Chat/ChatServiceTest.php
vendor/bin/phpunit tests/TestCase/Service/Chat/SlashCommandPolicyTest.php
vendor/bin/phpunit tests/TestCase/Service/Chat/HeadlessAssistantRunnerTest.php

Verwandte Doks