Script-Time: Signal-Chat-Bot Teil 2
Stabiler Signal-Bot mit Auswahl der Gesprächspartner und variabler Aufgabe
Wenn man häufig von Personen kontaktiert wird, die glauben, man könne alle ihre Probleme lösen, und sie auch nach dem Hinweis, dass man im Urlaub ist, nicht damit aufhören, kann dies den Erholungseffekt erheblich beeinträchtigen.
Über meinen ersten Versuch, damit umzugehen, habe ich bereits berichtet:
Das Problem im ersten Ansatz war jedoch, dass die Chats kein Gedächtnis hatten. Somit hatte jede erzeugte Antwort auch keinen Bezug auf das eigentliche Gespräch. Das muss nun komplett überdacht werden.
Ein neuer Ansatz
Nachrichten abholen
Der Abruf neuer Signal-Nachrichten erfolgt, wie bisher, mittels folgendem Befehl:
signal-cli -u $MY_NUMBER -o json receive
Das JSON-Objekt speichere ich in einer temporären Datei /ramdisk/signal.txt
und zerlege es mittels jq
in seine einzelnen Chats (envelopes). Dabei interessieren mich nur Chats mit Textnachrichten. Lesebestätigungen und andere Meta-Informationen sollen nicht berücksichtigt werden:
cat /ramdisk/signal.txt |
jq -c '.envelope'
Zerlegen in die wichtigen Informationen
Aus diesen Chats extrahiere ich mir den Absender, den Empfänger und die Nachricht:
SOURCE=$(echo "$message" | jq -r '.source')
DESTINATION=$(echo "$message" | jq -r '.syncMessage.sentMessage.destination // .dataMessage.destination // empty')
MESSAGE=$(echo "$message" | jq -r '.syncMessage.sentMessage.message // .dataMessage.message // "null"')
Da die Nachrichten ja immer an mich gerichtet sind, oder eben von mir geschrieben wurden, ist entweder $SOURCE
oder $DESTINATION
identisch mit meiner Telefonnummer. Daher findet das Gespräch offensichtlich mit der Telefonnummer statt, die nicht meine eigene ist 😉.
Ein Gedächtnis erzeugen
Ich hänge die Nachricht, also $MESSAGE
, an eine Textdatei an, die den Chatverlauf mit dem Gesprächspartner enthält:
if [ "$SOURCE" == "$MY_NUMBER" ]; then
echo "Ich: $MESSAGE" >> "${DESTINATION}.txt"
else
echo "Mein Freund: $MESSAGE" >> "${SOURCE}.txt"
fi
Das führt zu folgenden Dateien:
Mit Inhalten, wie diesem:
Ich: Gut, mal schauen, was es mit Gitlab auf sich hat . Hoffentlich keine zu großen Überraschungen! 樂
Mein Freund: Hast du schon einmal mit git gearbeitet?
Ich: Natürlich habe ich schon mit git gearbeitet. Musste das für einen Kurs auch tun.
Mein Freund: Ist das schwer zu lernen?
Ich: Nein, git ist nicht besonders schwer zu lernen. Es erfordert etwas Zeit und Übung, aber es ist sehr nützlich für die Versionskontrolle von Code. Ich habe auch einen Kurs damit gemacht und fand es gut.
Mein Freund: Was für einen Kurs hast du da gemacht?
Ich: ...
"Ich:" ist also immer das, was ich bisher gesagt habe, oder was die KI für mich in den Chat geschrieben hat.
Personen auswählen, mit denen die KI chatten soll
Um nun herauszufinden, ob die KI eine Antwort generieren soll, muss im Array ${USERS[@]}
geschaut werden, ob die $SOURCE
mit einer dieser Nummern übereinstimmt:
for user in "${USERS[@]}"; do
if [ "$SOURCE" == "$user" ]; then
echo "Ich denke mir eine Antwort aus..."
...
Sollte die KI eine Antwort erstellen müssen, müssen wir ihr erklären, was wir von ihr wollen.
Die KI erstellt eine Antwort
Sie soll natürlich über den alten Gesprächsverlauf ($DIALOG
) bescheid wissen, damit bei kurzen Nachfragen, wie "Und wie kann man das lösen?", der Kontext bekannt ist.
Die eigentliche Antwort soll aber immer nur für die letzte Nachricht ($MESSAGE
) erstellt werden.
Damit die KI nicht versucht, das Gespräch am laufen zu halten, muss man ihr sagen, dass sie kein Assistent ist, sondern sich so verhalten soll, wie ich es im bisherigen Chatverlauf getan habe. Auf diese Weise wird unterschieden, ob das Gespräch eher förmlich war (weil vielleicht mit einem Geschäftspartner), oder eher lockerer (Kuhl, Alder!).
Auf keinen Fall darf die KI begründen, warum sie die Antwort so formuliert hat, was letztlich zu folgender Anweisung führt:
Das folgende Gespräch hat gerade stattgefunden:
$DIALOG
Antworte meinem Freund auf deutsch und so kurz wie möglich auf diese Nachricht:
$MESSAGE
Berücksichtige dabei, dass du kein Assistent bist, und das Gespräch nicht aufrecht erhalten musst.
Achte genau auf die Art und Weise, wie ich bisher in diesem Dialog gesprochen habe.
Verwende vergleichbaren Humor, ähnliche Wortwahl und Ausdrucksweise.
Smileys dürfen nur als Zeichen eingefügt werden. Die Verwendung von Smileybeschreibungen in Klammern ist verboten.
Gib nur die rohe Antwort aus. Ausser der reinen Antwort darf keine weitere Ausgabe oder Erklärung erfolgen.
Nachdem die unbequemen Zeichen aus dieser Anfrage umgewandelt wurden, wird sie schließlich an die KI übergeben:
# Ersetzen von Zeilenumbrüchen durch \n und Anführungszeichen durch \"
FRAGE_ESCAPED=$( echo $FRAGE | sed ':a;N;$!ba;s/\n/\\n/g;s/"/\\"/g' )
# Verwendung der Variablen im curl-Befehl
ANTWORT=$(curl -s http://ai-server:11434/api/generate --data-raw "{
\"model\": \"${AIMODEL}\",
\"prompt\": \"${FRAGE_ESCAPED}\",
\"stream\": false}" | jq -r .response)
Und wieder zurück an den Absender...
Die $ANTWORT
kann dann an die $SOURCE
zurückgeschickt werden:
signal-cli -u $MY_NUMBER send -m "$ANTWORT" "$SOURCE"
Dies war mein Test-Chat mit diesem Script. Den blau unterlegten Text hat die KI auf meine Nachrichten geantwortet:
Wie man sieht, erzeugt die KI relativ genau nachempfundene Redeweisen mit sinnvollen Inhalten.
Das fertige Script
Und so sieht das ganze Script dann aus:
!/bin/bash
# Message-Folder
cd /var/log/signal
TOKEN="254562456:AAE24562456662466534624562456CiDvnU"
CHAT_ID="345724577"
AIMODEL="mixtral:instruct"
# AIMODEL="phi3:mini"
# Meine Telefonnummer
MY_NUMBER="+4917ABCDEFG02"
# Array mit Telefonnummern der Partner, die von der KI beantwortet werden sollen.
USERS=(
"+4915277188298"
"+4917159876153"
"+4916168446581"
# Weitere Benutzer hinzufügen bei Bedarf
)
# Empfange die Nachrichten mit signal-cli und prüfe, ob das noch funktioniert.
/usr/local/bin/signal-cli -u $MY_NUMBER -o json receive > /ramdisk/signal.txt || curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" -d "chat_id=${CHAT_ID}&text=Hier gibts ein Problem mit der signal-cli auf $(hostname)"
cat /ramdisk/signal.txt |
jq -c '.envelope' |
while read -r message; do
# Extrahiere die User-ID (source) aus der Nachricht
SOURCE=$(echo "$message" | jq -r '.source')
# Extrahiere die Zielnummer (destination) aus der Nachricht
DESTINATION=$(echo "$message" | jq -r '.syncMessage.sentMessage.destination // .dataMessage.destination // empty')
# Extrahiere die eigentliche Nachricht
MESSAGE=$(echo "$message" | jq -r '.syncMessage.sentMessage.message // .dataMessage.message // "null"')
# Prüfe, ob MESSAGE nicht "null" ist
if [ "$MESSAGE" != "null" ] ; then
echo "$SOURCE: $MESSAGE"
# Erstelle den Dateinamen basierend auf der User-ID (source)
if [ "$SOURCE" == "$MY_NUMBER" ]; then
echo "Ich: $MESSAGE" >> "${DESTINATION}.txt"
else
echo "Mein Freund: $MESSAGE" >> "${SOURCE}.txt"
fi
# Überprüfe, ob die Nachricht von einem der Benutzer im USERS-Array kommt
for user in "${USERS[@]}"; do
if [ "$SOURCE" == "$user" ]; then
echo "Ich denke mir eine Antwort aus..."
echo
# Füge eine Chat-History hinzu, um die Frage zu formulieren
DIALOG="$(cat ${SOURCE}.txt)"
FRAGE=$(cat <<EOF
Das folgende Gespräch hat gerade stattgefunden:
$DIALOG
Antworte meinem Freund auf deutsch und so kurz wie möglich auf diese Nachricht:
$MESSAGE
Berücksichtige dabei, dass du kein Assistent bist, und das Gespräch nicht aufrecht erhalten musst.
Achte genau auf die Art und Weise, wie ich bisher in diesem Dialog gesprochen habe.
Verwende vergleichbaren Humor, ähnliche Wortwahl und Ausdrucksweise.
Smileys dürfen nur als Zeichen eingefügt werden. Die Verwendung von Smileybeschreibungen in Klammern ist verboten.
Gib nur die rohe Antwort aus. Ausser der reinen Antwort darf keine weitere Ausgabe erfolgen.
EOF
)
# FRAGE="$(cat ${SOURCE}.txt) ${GENERATERESPONSE}"
# Ersetzen von Zeilenumbrüchen durch \n und doppelten Anführungszeichen durch \"
FRAGE_ESCAPED=$( echo $FRAGE | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g')
# Verwendung der Variablen im curl-Befehl
ANTWORT=$(curl -s http://ai-server:11434/api/generate --data-raw "{
\"model\": \"${AIMODEL}\",
\"prompt\": \"${FRAGE_ESCAPED}\",
\"stream\": false}" | jq -r .response)
# Mit der generierten Antwort dem User antworten.
/usr/local/bin/signal-cli -u $MY_NUMBER send -m "${ANTWORT//\"/}" "$SOURCE" > /dev/null 2>&1
# Hänge die Antwort an den Dialog an
echo "Ich: ${ANTWORT//\"/}" >> ${SOURCE}.txt
fi
done
fi
done
rm /ramdisk/signal.txt
Ich rufe es per cron alle sechs Minuten auf. Der Test-Server, der aktuell die Antworten generiert, hat keine Grafikkarte. So kann es also eine Weile dauern, bis Antworten generiert sind:
Fazit
Dieses Script ist inzwischen recht stabil und kann nun auch auf weitere Ruhestörer ausgeweitet werden.
Aktuell probiere ich unterschiedliche Modelle für die Erzeugung der Antworten aus. Dabei ist mir aufgefallen, dass mixtral:instruct, wie auch weitere instruct Modelle, sehr an Fakten hängen und teils die gleiche Antwort wiederholen. Llama3 Varianten erkennen scheinbar Wiederholungen der Fragen und reagieren manchmal fast gereizt mit: "Das habe ich doch eben gerade erklärt:......!"
Ich denke, ich werde mit KI noch viel Spaß haben...