Über unsMediaKontaktImpressum
Joachim Zuckarelli 16. Juli 2024

Entwicklung eines KI-Assistenten mit der OpenAI Assistant API

Assistenten, die Fragen beantworten, Hilfestellung leisten, beraten oder kleinere Aufgaben erledigen, gehören zweifelsfrei zu den offensichtlichsten Anwendungsfällen künstlicher Intelligenz – und zu jenen, bei denen die Effizienzgewinne, die durch künstliche Intelligenz möglich sind, besonders deutlich hervortreten.

Ein Beispiel dafür liefert der schwedische Zahlungsdienstleister Klarna. Nach eigenen Angaben kann Klarna durch den Einsatz künstlicher Intelligenz im Customer Service Kundenanfragen bei gleichbleibender Kundenzufriedenheit deutlich schneller lösen als zuvor und mit einem erheblich geringeren Anteil von Rückfragen bzw. Wiederholungsanfragen. Die Arbeitsleistung der künstlichen Intelligenz entspricht nach Einschätzung von Klarna dabei ca. 700 Servicemitarbeitern und verspricht einen Gewinneffekt von ungefähr 40 Mio. US-Dollar im Jahr 2024. Natürlich können KI-basierte Assistenten nicht nur in der Beziehung zu Endkunden nützlich sein, auch im Einsatz für die eigenen Mitarbeiter spielen sie ihre Fähigkeiten aus.

Egal, ob Kunden oder eigene Mitarbeiter die Zielgruppe bilden – durch den geschickten Einsatz von Assistenten lassen sich eine Reihe vorteilhafter Effekte erzielen:

  1. Schnellere Beantwortung von Anfragen / weniger Wartezeit
  2. Steigerung der Zufriedenheit der Anfragenden
  3. Entlastung der zuständigen Kollegen von mühevoller und stressiger Support-Arbeit
  4. Ersparnis an Personalkosten (Ohne, dass die Support-Kollegen entlassen werden, schließlich können sie sich nun anderen, werthaltigeren und im Zweifel interessanteren Aufgaben widmen, was wiederum die Zufriedenheit dieser Mitarbeiter und die Wahrscheinlichkeit für den Verbleib im Unternehmen erhöht.)
  5. Entfall der Notwendigkeit von Recruiting und Vermeidung des Einarbeitungsaufwands neuer Support-Mitarbeiter.

Dank der Assistant API von OpenAI ist die Entwicklung von einfachen Assistenten, die sinnvolle Aufgaben effizient erledigen können, nicht schwer. 

In diesem Artikel werden wir einen einfachen Assistenten bauen, der auf Basis eines vorgegebenen unternehmensinternen Wissensbestandes aus Manuals, Anleitungen, FAQs oder anderen Dokumenten Fragen von internen oder externen Kunden beantworten kann. Durch die geschickte Berücksichtigung von Konfigurationsoptionen wird der Assistent dabei so gestaltet werden, dass er leicht auf andere Themen und mithin weitere Wissensbestände umgestellt werden kann; er lässt sich also mühelos für neue Anwendungsszenarien "kopieren".

Der gesamte (Python-)Code des Assistenten wird hier im Artikel schrittweise vorgestellt und besprochen und steht darüber hinaus als Ganzes in einem GitHub-Repository [1] zur Verfügung.

Das Problem des Information Retrievals

Prinzipiell ist es nicht schwer, die verfügbare OpenAI-Technologie (oder die eines anderen KI-Anbieters) gezielt Fragen auf Basis unternehmensinterner Wissensbestände beantworten zu lassen. Der einfachste Weg wäre, den Wissensbestand zusammen mit der Frage, die sich auf ihn bezieht, in einen ChatGPT-Prompt zu schreiben (oder in Form von Custom Instructions zu hinterlegen). Würde man, statt ChatGPT zu verwenden, eine selbstentwickelte Lösung auf Basis der OpenAI-API betreiben, könnte man der API den Wissensbestand als System-Nachricht mitgeben und die User-Nachricht auf die eigentliche Frage beschränken.

Beide Ansätze teilen jedoch einen schwerwiegenden Nachteil: Für jede neue Frage muss jedes Mal der gesamte Wissensbestand übergeben werden, da a priori nicht klar ist, welcher Teil davon für die Beantwortung der Frage des Benutzers tatsächlich relevant ist. Das dauert lange und ist teuer, denn die Benutzung der API wird nach Input- und Output-Tokens (nicht immer, aber oft identisch mit Wörtern) abgerechnet; aktuell kostet beim GPT-4o-Modell die Verarbeitung von 1 Mio. Input-Tokens 5 US-Dollar, die Ausgabe von 1 Mio. Output-Tokens 15 US-Dollar. Bei großen Wissensbeständen und vielen Nutzerfragen wäre mit erheblichen Kosten zu rechnen.

Deshalb geht man beim sogenannten Information Retrieval, wie es in Assistenten zum Einsatz kommt, einen anderen Weg:

  1. Die Tokens des Wissensbestandes werden in Vektoren von Embeddings übersetzt, also in eine Repräsentation entlang inhaltlicher Bedeutungs-Achsen (jedes Element des Vektors entspricht der Ausprägung auf einer dieser Bedeutungs-Achsen). Die Bedeutung wird dabei auch unter Berücksichtigung des Kontexts erschlossen (ob mit "Bank" ein Sitzmöbel oder ein Finanzinstitut gemeint ist, ist abhängig vom umgebenden Text, also vom Kontext). Wie gut die Repräsentation entlang der Bedeutungs-Achsen funktioniert, demonstriert ein berühmtes Beispiel, in dem gezeigt wird, dass sich relativ präzise das Embedding für das Wort "Königin" ergibt, wenn man vom Embedding-Vektor für "König" den des Wortes "Mann" abzieht und den des Wortes "Frau" addiert. Der bei der Kontextualisierung der Embeddings zum Einsatz kommende Ansatz wird als (Self-)Attention bezeichnet und geht auf Jakob Uszkoreit zurück, der diesen zusammen mit einigen Google-Mitstreitern 2017 im bahnbrechenden Paper "Attention Is All You Need" publizierte.
  2. Analog wird auch die Anfrage des Benutzers embedded und ihre Bedeutung auf diese Weise erfasst.
  3. Auf Basis der Embeddings von Wissensbestand und Anfrage wird der Wissensbestand nach Teilen durchsucht, die für die Beantwortung der Frage Bedeutung haben könnten.
  4. Nur die so gefundenen Teile werden anschließend tatsächlich zur Beantwortung der Frage herangezogen. Das reduziert die bei der Beantwortung der Frage zu verarbeitenden Input-Tokens unter Umständen drastisch (v.a., wenn der Wissensbestand sehr groß ist).

Die Assistant API von OpenAI

Natürlich könnten Assistenten auch ganz ohne eine spezielle API entwickelt werden, allein auf Basis der normalen API zum Zugriff auf die OpenAI-Modelle (wie etwa gpt-4o, gpt-4-turbo oder gpt-3.5). Ein wenig komfortabler wird es mit der populären Langchain-Bibliothek, die eine einigermaßen einheitliche Schnittstelle zu einer ganzen Reihe von KI-Modellen (darunter neben denen von OpenAI auch Claude von Anthropic und Gemini von Google) bereitstellt. Außerdem bietet sie etliche Funktionen, die die Implementierung von Information-Retrieval-Anwendungen erleichtern, wie etwa RetrievalQAWithSourcesChain(). Noch einfacher geht es aber mit der Assistant API von OpenAI. Die wichtigsten Vorteile der Verwendung der Assistant-API sind:

  • Die Assistant-API kümmert sich um das Management des Chat-Kontextfensters. Man muss also nicht manuell mit dem Problem umgehen, dass Eingaben des Benutzers und Ausgaben des Assistenten irgendwann aus der Kontextlänge "herauswachsen".
  • Die Assistant-API stellt extrem leicht zu handhabende Tools für Information Retrieval (wie wir weiter unten sehen werden), Arbeit mit Daten (Code Interpreter) und Bildgenerierung bereit.

Kernobjekte der Assistant API sind:

  • Der Thread: Eine zusammenhängende Unterhaltung zwischen Benutzer und Assistent, die in ein- und demselben Kontext abläuft.
  • Die Message: Ein Prompt des Benutzers oder eine Reaktion des Assistenten. Ein Thread kann eine oder mehrere solcher Nachrichten enthalten.
  • Der Run: Ein Aufruf des Assistenten, durch den die letzte Nachricht des Benutzers im Kontext der bisherigen Unterhaltung bearbeitet wird. Der Run fügt den Messages eine weitere mit der jüngsten Antwort des Assistenten hinzu.
  • Der Run Step: Ein Schritt in einem Run. Die Analyse der Steps ist besonders dann interessant, wenn man zu verstehen versucht, wie genau der Assistent bei der Beantwortung der Benutzeranfrage vorgeht und in welchem Verarbeitungsstatus sich eine laufende Anfrage gerade befindet.

Die Nutzung der Assistant API setzt (wie auch die Nutzung der übrigen OpenAI-APIs) ein Benutzerkonto voraus, über das die Kosten abgerechnet werden. Bezahlt wird mit Kreditkarte, wobei ein Guthaben vorausgezahlt wird, das dann verbraucht werden kann (die Einrichtung eines Auto-Recharge-Triggers ist möglich). Zur Kostenkontrolle stehen Auswertungen sowie die Möglichkeit zur Verfügung, ein monatliches Kosten-Budget zu setzen (bei dessen Überschreitung API-Requests zurückgewiesen werden) und eine Kosten-Schwelle zu definieren, bei deren Überschreitung man per E-Mail informiert wird.

Zur Authentifizierung nutzt die API Keys, von denen man für unterschiedliche Projekte auch mehrere anlegen kann. Die API-Keys sollte man unbedingt speichern, sie können über die OpenAI-Oberfläche nachträglich zwar noch gelöscht, aber nicht mehr eingesehen werden.

Die Wissensbasis des Assistenten – Vorbereitung des Vektor-Stores

Im ersten Schritt werden wir nun die Wissensbasis unseres Assistenten in Embeddings übersetzen und die vektorisierten Ergebnisse in einer Vektordatenbank speichern. Dieser Vorgang kann auch programmatisch über die vector_stores- und files-Endpunkte der OpenAI-API durchgeführt werden (was Sinn macht, wenn der Wissensbestand regelmäßig updatet werden muss); wir wollen uns aber hier der Einfachheit halber der OpenAI-Weboberfläche bedienen. Dort kann man, nachdem man sich in seinen Account eingeloggt hat, unter "Storage" einen neuen Vector Store erzeugen (Schritte 1 und 2 in Abbildung 1). Danach lassen sich dem Vector Store eine oder mehrere Dateien hinzufügen (Schritt 3), die dann praktisch sofort in Embedding-Vektoren übersetzt und im Vector Store gespeichert werden. Die unterstützten Dateiformate umfassen derzeit u.a. PDF, HTML und PPTX. Für den Betrieb des Vector Stores fallen Kosten in Höhe von aktuell 0,10 US-Dollar pro gespeichertem GB und Tag an, wobei das erste GB kostenlos ist. Um an dieser Stelle Kostenkontrolle auszuüben, kann man unter "Expiration Policy" einstellen, dass ein Vector Store automatisch nach einer bestimmten Zahl von Tagen deaktiviert wird. Wichtig ist die ID des Stores (Schritt 4 in Abbildung 1), die wir später benutzen werden, um aus unserem Code auf den Vector Store zuzugreifen.

Nachdem der Vector Store aufgebaut wurde, könnten wir über die Oberfläche einen Assistenten erzeugen und diesen dann im Playground ausprobieren. Das kann sich anbieten, um zum Beispiel mit unterschiedlichen System-Instruktionen und verschiedenen Werten für die übrigen Parameter (wie etwa die Temperature) zu experimentieren. Wir sparen uns das an dieser Stelle aber, weil die Assistentenanwendung, die wir hier entwickeln werden, es erlaubt, solcherlei Einstellungen bequem über die Weboberfläche des Assistenten zu modifizieren.

Struktur unseres Assistenten

Unser Assistent besitzt eine Web-Oberfläche, die mit Streamlit gebaut wird. Deshalb muss (sofern nicht schon geschehen) zunächst das Streamlit-Modul mit pip install streamlit installiert werden, analog für dasopenai-Modul von OpenAI, das den Zugriff auf die APIs erlaubt.

Der Assistent besteht nun im Kern aus den folgenden Dateien:

  • assistant.py: Die Hauptdatei, die den eigentlichen Python-Code enthält. Sie wird im folgenden Abschnitt eingehender besprochen.
  • config.py: Eine Konfigurationsdatei, in der als Python-Variablen verschiedene Parameter stehen, wie etwa der Titel des Assistenten, sein Logo, der Default-Wert für die Temperature (ein Parameter zwischen 0 und 2, der die "Kreativität" des Modells steuert), der Instruktionstext, der beim Aufruf der API als System-Message mitgegeben wird, die ID des Vector Stores und Pfade zu Avatar-Bildern, die im Chatbereich des Assistenten verwendet werden. Durch die Auslagerung dieser Parameter in eine Konfigurationsdatei bleibt der Assistent hinreichend allgemein und kann durch Austausch oder Anpassung der Konfigurationsdatei leicht für ein anderes Thema "umgerüstet" werden. Abbildung 2 zeigt die Konfigurationsdatei unseres Beispiels.
  • apikey.py: Diese Datei enthält lediglich eine Variable, openai_key, den API Key. Er wird sicherheitshalber in einer eigenen Datei gehalten.
  • models.json: Eine Datei mit Informationen über die Kosten der Modelle für Input und Output. Diese Daten werden später im Assistenten verwendet, um die Kosten der Assistentennutzung abzuschätzen. Abbildung 3 zeigt den Aufbau anhand eines Ausschnitts aus der Datei.

Listing assistant.py

#  Importe

import json
import streamlit as st
import time
from openai import OpenAI


# Konstanten und Parameter

from config import *
from apikey import *


# Initialisierung

if "loaded" not in st.session_state:
    st.session_state["client"] = OpenAI(api_key=openai_key)
    st.session_state["assistant"] = st.session_state["client"].beta.assistants.create(
        name=title,
        model=default_model,
        instructions=instruction_text,
        tools=[{"type": "file_search"}],
        tool_resources={"file_search": {"vector_store_ids": [vectorstore_id]}}
    )
    st.session_state["assistant_thread"] = st.session_state["client"].beta.threads.create()

    with open(base_path + "models.json","r") as file:
        st.session_state["models"] = json.load(file)

    st.session_state["messages"] = []
    st.session_state["prompt_tokens"] = 0
    st.session_state["completion_tokens"] = 0
    st.session_state["costs"] = 0
    st.session_state["loaded"] = True


# User interface

st.set_page_config(page_title=title)
if image:
    st.image(base_path + image, width=200)
st.title(title)

query = st.chat_input("Frage/Prompt")

st.sidebar.title("Einstellungen & Debugging")
st.sidebar.header("Einstellungen")

choice_model = st.sidebar.selectbox("Modell", options=list(st.session_state["models"].keys()), index=0)
choice_temp = st.sidebar.slider("Temperature", value=default_temperature, min_value=0.0, max_value=2.0, step=0.1)
instruction_text = st.sidebar.text_area(label="System-Message", value=instruction_text)

st.sidebar.divider()
st.sidebar.header("Debugging")
debug = st.sidebar.toggle("Debug-Modus", value = False)


# Hilfsfunktion zur Kalkulation der Kosten

def calculate_costs(tokens_prompt: int, tokens_completion: int, model: str) -> float:
    price_prompt = st.session_state["models"][model]["input"]["price"] / st.session_state["models"][model]["input"]["tokens"]
    price_completion = st.session_state["models"][model]["output"]["price"] / st.session_state["models"][model]["output"]["tokens"]
    return(tokens_prompt * price_prompt + tokens_completion * price_completion)


# Chat-Verlauf darstellen

for m in st.session_state["messages"]:    
    if m["role"] == "user" and avatar_path_user:
        avatar = base_path + avatar_path_user
    elif m["role"] == "assistant" and avatar_path_assistant:
        avatar = base_path + avatar_path_assistant
    else: avatar = None
    with st.chat_message(name=m["role"], avatar=avatar):
        st.write(m["content"])


# Event Handler

if query:
    with st.chat_message(name="user", avatar=base_path + avatar_path_user):
        st.write(query)
    
    status = st.status("Ermittle Antwort...", expanded=False)

    msg = st.session_state["client"].beta.threads.messages.create(
        st.session_state["assistant_thread"].id,
        role="user",
        content=query
    )
    assistant_run = st.session_state["client"].beta.threads.runs.create(
        thread_id = st.session_state["assistant_thread"].id,
        assistant_id = st.session_state["assistant"].id,
        model=choice_model,
        temperature=choice_temp,
        stream=False
    )

    assistant_run_retrieved = st.session_state["client"].beta.threads.runs.retrieve(thread_id=st.session_state["assistant_thread"].id, run_id=assistant_run.id)    
    while not assistant_run_retrieved.status in ["cancelled", "failed", "expired", "completed"]:
        time.sleep(0.5)
        assistant_run_retrieved = st.session_state["client"].beta.threads.runs.retrieve(thread_id=st.session_state["assistant_thread"].id, run_id=assistant_run.id)
    status.update(label="Fertig", state="complete", expanded=False)

    if assistant_run_retrieved.status == "completed":
        result = st.session_state["client"].beta.threads.messages.list(thread_id=st.session_state["assistant_thread"].id)        
        answer = result.data[0].content[0].text.value

        with st.chat_message(name="assistant", avatar=base_path + avatar_path_assistant):
            st.write(answer)

        st.session_state["messages"].append({"role": "user", "content": query, "tokens": assistant_run_retrieved.usage.prompt_tokens})
        st.session_state["messages"].append({"role": "assistant", "content": answer, "tokens": assistant_run_retrieved.usage.completion_tokens})
        st.session_state["prompt_tokens"] = st.session_state["prompt_tokens"] + assistant_run_retrieved.usage.prompt_tokens
        st.session_state["completion_tokens"] = st.session_state["completion_tokens"] + assistant_run_retrieved.usage.completion_tokens
        costs = calculate_costs(assistant_run_retrieved.usage.prompt_tokens, assistant_run_retrieved.usage.completion_tokens, choice_model)
        st.session_state["costs"] = st.session_state["costs"] + costs

        if debug:
            st.sidebar.subheader("Usage")
            st.sidebar.write("Prompt Tokens (letztes Prompt):", assistant_run_retrieved.usage.prompt_tokens)
            st.sidebar.write("Completion Tokens (letztes Prompt):", assistant_run_retrieved.usage.completion_tokens)
            st.sidebar.write("Total Tokens (letztes Prompt):", assistant_run_retrieved.usage.total_tokens)
            st.sidebar.write("Prompt Tokens (gesamte Session):", st.session_state["prompt_tokens"])
            st.sidebar.write("Completion Tokens (gesamte Session):", st.session_state["completion_tokens"])
            st.sidebar.write("Total Tokens (gesamte Session):", st.session_state["prompt_tokens"] + st.session_state["completion_tokens"])
            st.sidebar.subheader("Kosten")
            st.sidebar.write("Kosten (letztes Prompt, geschätzt):", costs)
            st.sidebar.write("Kosten (gesamte Session, geschätzt):", st.session_state["costs"])

            st.sidebar.subheader("Assistant API Run")
            st.sidebar.write(assistant_run_retrieved)

            st.sidebar.subheader("Assistant API Messages")
            st.sidebar.write(result)

Code im Detail

Zeilen 1-12:
Die benötigten Module werden geladen, darunter json (für das Lesen der Modell-Datei) und openai. Außerdem werden die Settings aus der Konfigurationsdatei und der API-Schlüssel als globale Variablen eingelesen.

Zeilen 17-35:
Dieser Block dient der Initialisierung der Anwendung und wird nur einmalig beim Start ausgeführt, dafür sorgt die Status-Variable loaded, deren Existenz zu Beginn überprüft wird. Im Rahmen der Initialisierung der Anwendung wird mit Hilfe des API-Keys eine Instanz der OpenAI-Klasse erzeugt und darauf aufbauend eine Assistenten-Instanz. Wichtige Eigenschaften, wie das Modell (zum Beispiel gpt-3.5) und die System-Instruktionen, die Verhaltensanweisungen für den Assistenten umfassen, werden hier mit den entsprechenden Vorgaben aus der Konfigurationsdatei vorbelegt; der Nutzer kann diese später über die Oberfläche ändern. Mit dem tools-Array wird festgelegt, welcher Werkzeuge sich der Assistent bedienen können soll. In unserem Fall der Information-Retrieval-Funktionalität file_search. Möglich wären hier auch code_interpreter für Datenanalysen und function für den Aufruf eigens definierter Funktionen, mit deren Hilfe der Assistent etwa auf andere Systeme zugreifen könnte. Das Dictionary tools_resources schließlich beschreibt die Ressourcen, die die Tools verwenden, in unserem Fall eine Liste der IDs von Vector Stores; hier könnten natürlich auch mehrere Vector Stores angegeben werden, wir verwenden jedoch nur einen.

Wir haben hier die Erzeugung des Assistenten in den Code mit aufgenommen, um diesen Vorgang zu demonstrieren, obwohl man in einer produktiven Anwendung den Assistenten nicht bei jedem Aufruf neu erzeugen, sondern ihn nur einmal aufsetzen würde (sei es mit einem Code wie diesem, sei es über die OpenAI-Oberfläche, was ebenfalls möglich ist, wie auch im Fall der Vector Stores); danach würde man einfach die ID des so erzeugten Assistenten im Code weiterverwenden.

Neben dem Assistenten wird ein Thread, also ein Kommunikationsverlauf initialisiert, der ein vom Assistenten grundsätzlich unabhängiges Objekt ist.

OpenAI-Objekt, Assistent und Thread werden im st.session-Dictionary gespeichert. Auf diese Weise stehen sie permanent während der Benutzer-Session zur Verfügung. Im Anschluss werden noch die JSON-Datei mit den (Kosten-)Daten zu den Modellen eingelesen sowie einige globale Variablen initialisiert, die für die Speicherung des Chat-Verlaufs (messages) und der Usage- bzw. Kostendaten dienen (prompt_tokens: Token-Anzahl der Benutzeranfrage, completion_tokens: Token-Anzahl des Modell-Outputs/der Antwort des Modells, costs: geschätzte Kosten der Token-Verarbeitung).

Zeilen 38-56:
Hier gestalten wir die Oberfläche des Assistenten, Streamlit-typisch bestehend aus dem Hauptbereich der Anwendung, in der sich die Chat-Komponente befindet, sowie einer Sidebar für Einstellungen (Modell, Temperature und Systeminstruktionen sind hier änderbar) und die Darstellung von Debug-Informationen. Der debug-Toggle gibt an, ob die Anwendung mehr “verbose” sein und beim Ansprechen der API detaillierte Informationen über Token-Anzahl, Kosten und die ausgetauschten Daten darstellen soll.

Zeilen 59-64:
Die Funktion calculate_costs() schätzt die Kosten einer Dialogrunde mit dem Assistenten, bestehend aus User-Anfrage (Prompt) und Antwort der API (Completion) auf Basis der Token-Zahlen sowie der Pro-Token-Kosten aus models.json.

Zeilen 67-76:
Jedes Mal, wenn Streamlit den Python-Code durchläuft, wird der Chat neu dargestellt. Dazu werden die einzelnen Nachrichten, die in der Liste messages im Streamlit-session_stategespeichert werden (das führen wir manuell weiter unten durch), mit dem entsprechenden Avatar dargestellt, sofern in der config.py-Datei die Pfade zu Avatar-Bildern angegeben wurden.

Zeilen 79-104:
Sendet der User seine Chat-Anfrage ab, wird diese im Chat dargestellt (Zeilen 81/82) und dann dem Thread hinzugefügt (Zeilen 87-91). Dabei wird die ID des zuvor erzeugten Threads verwendet und mit der Rolle user deutlich gemacht, dass es sich hier um eine Nachricht des Benutzers handelt. Im Anschluss wird ein Run des Threads erzeugt, also die Verarbeitung der User-Anfrage unter Berücksichtigung des bisherigen Gesprächsverlaufs (Zeilen 92-98). Dabei wird einerseits natürlich Bezug auf den Thread genommen, der verarbeitet werden soll, aber auch auf den Assistenten, der die Verarbeitung übernimmt. Über die Parameter model und temperature geben wir dem Run die vom Benutzer über die Oberfläche gewählten Einstellungen mit. Jetzt müssen nur noch die Ergebnisse des Runs aufgefangen werden. Das machen wir der Einfachheit halber ohne kontinuierliches Streaming (Zeile 97), indem der Status des Runs immer wieder im Abstand von 0,5 Sekunden mit runs.retrieve() abgefragt wird. Das Rückgabeobjekt enthält nicht nur das status-Feld, sondern auch Informationen zur Anzahl der verarbeiteten Tokens, die wir uns später zunutze machen.

Zeilen 106-118:
Wenn der Run erfolgreich war, wird dem Thread die Antwort der API auf die User-Anfrage angehängt. Diese fragen wir ab (Zeilen 107/108); die Funktion messages.list() listet dabei die Nachrichten im Thread standardmäßig in absteigender Reihenfolge, also die letzte zuerst. Die Antwort der API stellen wir dann zunächst im Chat-Verlauf dar (Zeilen 110/111) und ergänzen unseren Nachrichtenverlauf im Streamlit-session_state entsprechend mit dieser Nachricht sowie der vorangegangenen Anfrage des Benutzers, damit das nächste Mal, wenn Streamlit den Code ausführt, auch wieder der gesamte Chat-Verlauf dargestellt werden kann. Im Anschluss werden noch die Anzahl der Tokens und darauf aufbauend die Kosten der letzten Dialogrunde ermittelt und gespeichert.

Zeilen 120-Ende:
Wenn der Benutzer den Debug-Modus über den debug-Toggle angeschaltet hat, werden eine Reihe von Informationen zur verwendeten Token-Anzahl und den entstandenen Kosten angezeigt. Außerdem wird das gesamte assistant_run_retrieved-Objekt angezeigt.

Unser Assistent in Aktion

Der Assistent kann nun mit streamlit run assistant.py gestartet werden. Möchte man den Assistenten nicht nur auf der eigenen Maschine testen, sondern für ein größeres Publikum bereitstellen, bietet sich die Community Cloud auf Streamlit.io [2] an, über die die Applikation auf Basis eines (öffentlichen oder privaten) GitHub-Repositories deployed werden kann. Dabei sollte man das Secrets Management von Streamlit Community Cloud verwenden, um den OpenAI-API-Schlüssel zu speichern (nicht ins Repository committen!). Auch eine Benutzerauthentifizierung ist dabei möglich.

Abbildung 4 zeigt unseren Assistenten nach dem Start im Webbrowser.

Für produktive Anwendungen eignen sich Services wie Heroku oder Azure Web Apps. Allen diesen Formen des Deployments ist gemein, dass sie Continuous Integration (CI) erlauben, in dem Sinne, dass neue Commitments in dem der Anwendung zugrundeliegenden GitHub-Repository direkt in der produktiven App reflektiert werden.

Weiterentwicklungsmöglichkeiten

Natürlich stellt dieser Assistent nur eine sehr rudimentäre Grundlage dar.  Diese kann an vielen Stellen weiterentwickelt werden (und für den Produktivbetrieb auch weiterentwickelt werden sollte), etwa bzgl. des Fehler-Handlings oder der Integration von Voice-Input und -Output. Letztere muss man sich derzeit noch mit der Whisper-API (für die Transkription von Audio-Input) sowie der TTS-API (für die Erzeugung von Audio-Output) zusammenbauen. Zukünftig wird diese Multimodalität durch den neuen, nativ multimodalen Ansatz des GPT-4o-Modells sicher auch in die Assistant-API Einzug halten. Arbeiten kann man zudem an der Optimierung der Parameter-Werte, vor allem der Temperature, die die "Kreativität" des Modells steuert und den Systeminstruktionen, über die das Verhalten des Assistenten genauer spezifiziert werden kann.

Autor

Joachim Zuckarelli

Joachim Zuckarelli beschäftigt sich seit über 12 Jahren mit R. Heute ist er als Leiter Business Intelligence für einen Tierklinik-Betreiber tätig.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (1)
  • Peter Kleemann
    am 17.07.2024
    Leicht zu verarbeitender Artikel, danke.

    Dennoch: "Ohne, dass die Support-Kollegen entlassen werden, schließlich können sie sich nun anderen, werthaltigeren und im Zweifel interessanteren Aufgaben widmen"

    Als jemand, der jahrzehnte im Support - vor allem leitend - tätig war, bin ich mir sicher, dass dies so nicht der Fall sein wird.

    Support ist chronisch understaffed und jede Möglichkeit zur Einsparung geht primär in die Reduzierung des Headcounts.

    Selten sind Unternehmen ist daran interessiert, den Support durch werthaltige und interessante Aufgaben aufzuwerten. Ja, es gibt auch einzelne Ausnahmen - so lange, bis ein Investor mit ins Spiel kommt, dem es nicht um das Produkt oder die Dienstleistung geht, sondern um die Bilanz und den Gewinn.

    Support wird von 95% der Unternehmen als Costcenter betrieben. Würden mehr Unternehmen Support als Profitcenter verstehen, als Bestandteil von Presales und dem Productmanagement, könnte sich das ändern.

    Alleine aufgrund der Tatsache, dass genau dieses Argument mit diesem Text angereichert wurde (und deswegen alleine schon wegen der Wortzahl herausragt) bin ich mir außerdem sicher, dass der Autor beim Verfassen dieses Punktes den Gedanken hatte, derart kommentieren zu MÜSSEN.

    Beim Argument bleibt es aber ehrlicherweise genau so:
    * Ersparnis von Personalkosten (Punkt, nächster Abschnitt)

Neue Antwort auf Kommentar schreiben