Zum Inhalt

Die ML-Dienste

Vier Dienste dieser Plattform lassen sich für sich allein aufrufen, ohne eine Unterhaltung zu beginnen und ohne einen Agent auszuführen: Dokumentanalyse, OCR, Spracherkennung und Erkennung personenbezogener Daten. Es sind dieselben Implementierungen, die die Agents benutzen, nur direkt erreicht — ein Satz Parser, ein Satz Detektoren, ein Transkriptionsclient.

Es gibt sie, weil eine andere Komponente sie brauchen kann. Eine Warteschlange, die einen eingescannten Antrag lesen muss, ein Batch-Job, der einen Export schwärzt, ein Dienst, der ein Transkript will — keiner davon will ein Chat-Fenster, und keiner sollte so tun müssen, als wäre er eines.

Was geliefert ist und was nicht

Jede Dienstfamilie ist eine Zeile. served heißt, ein Endpunkt dieses Deployments beantwortet sie mit der daneben genannten Engine. dependency heißt, die Familie ist gefordert und etwas fehlt, und die Notiz sagt was. prepared ist Architekturvorbereitung: keine Engine wird ausgeliefert, und die Naht, durch die sie käme, ist benannt.

GET /api/v1/ml/services antwortet mit derselben Tabelle, sodass eine Integration sie lesen kann, statt einer Seite zu vertrauen.

Dienst Anforderungen Endpunkt Stand Engine
document_analysis FA-069, FA-070 POST /api/v1/ml/documents/analyze served LiteParse oder PyMuPDF, lokal
ocr FA-069, FA-071 POST /api/v1/ml/documents/ocr served LiteParse-OCR: mitgeliefertes Tesseract oder ein registrierter OCR-Server
speech_to_text FA-069, FA-072 POST /api/v1/ml/audio/transcriptions served Der eigene Transkriptionsendpunkt der Organisation
pii_detection FA-069, FA-073 POST /api/v1/ml/privacy/pii served Die Musterdetektoren, die auch die Guardrails benutzen
pii_named_entities FA-073, DA-007 — dependency Keine in diesem Deployment
image_analysis FA-074 — prepared Keine in diesem Deployment

Zwei Zeilen sagen nein, und beide sagen warum. Benannte Entitäten — der Name einer Person, eine Postanschrift — haben keine Musterform, also findet sie kein regulärer Ausdruck: dafür braucht es ein Named-Entity-Modell je Sprache im Umfang. Der Erkennungsendpunkt trägt die zusätzlichen Kategorien an dem Tag, an dem eines bereitgestellt wird, und bis dahin beansprucht er sie nicht. Bildanalyse ist in den Anforderungen selbst als künftiger Umfang markiert.

Einen aufrufen

Authentifizierung, der Organisations-Header und der Fehlerumschlag gehören der HTTP-API. Es gibt keinen eigenen Schlüssel, keinen eigenen Host und keinen zweiten Weg hinein, und das mit Absicht: eine Oberfläche mit eigener Haustür ist eine Oberfläche mit eigenen Fehlern.

Eine Berechtigung sichert alle vier: ml:invoke. Bewusst nicht agents:run — eine Integration, die Dokumente parst, sollte dadurch nicht auch das Modellbudget der Organisation ausgeben können. Jede Rolle außer Viewer hält sie.

curl -X POST "$BASE/api/v1/ml/documents/ocr" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Organization-Id: $ORG_ID" \
  -F "file=@scan.pdf" \
  -F "language=deu"

Die Antwort trägt die Seiten in Reihenfolge, die vorbereiteten Chunks sowie Größe und Hash der Datei:

{
  "filename": "scan.pdf",
  "filetype": "pdf",
  "byte_size": 184320,
  "content_hash": "9f2c…",
  "pages": [{"page_num": 1, "content": "Antrag auf …"}],
  "chunks": ["Antrag auf …"]
}

Dokumentanalyse

POST /api/v1/ml/documents/analyze liest, was ein Dokument bereits trägt. Das Feld parser wählt zwischen liteparse, das das Layout erhält und Office-Formate liest, wo LibreOffice installiert ist, und pymupdf, das PDFs liest und schneller ist. chunk_size, chunk_overlap und chunking_strategy formen die vorbereiteten Chunks; die Strategien sind recursive, fixed und markdown, und eine vierte Schreibweise wird abgelehnt statt still als recursive behandelt.

chunk_overlap muss kleiner als chunk_size sein. Gleich wird abgelehnt, und nicht der Ordnung halber: der Splitter nimmt es an und rückt dann um etwa einen Trenner je Chunk vor, während er eine fast vollständige Kopie des letzten behält - ein zulässiger Upload antwortet also mit einem vielfach vervielfältigten Dokument.

Ein Dokument, in dem nichts lesbar ist, wird abgelehnt statt mit einer leeren Seitenliste beantwortet, und die Ablehnung sagt, stattdessen OCR aufzurufen — was ein Scan, der auf seine Textebene hin geparst wird, immer braucht.

OCR

POST /api/v1/ml/documents/ocr erkennt den Text auf jeder Seite, ob die Seite eine Textebene trägt oder nicht. Das ist der Unterschied zur Ingestion, die das automatisch erkennt und die Erkennung überspringt, wo der Text schon da ist: wer OCR verlangt hat, hat verlangt, dass die Seiten als Bilder gelesen werden.

language ist ein Tesseract-Code, also drei Buchstaben — deu, nicht de. Das ausgelieferte Image installiert eng und pol, und genau diese zwei nimmt der Endpunkt an: ein Code ohne Sprachpaket dahinter scheitert mitten im Parse, also wird er schon an der Grenze abgelehnt. Ein Deployment, das weitere Pakete installiert, erweitert die Liste in derselben Änderung.

ocr_service_id benennt einen OCR-Server, der unter den lokalen Diensten registriert ist, sodass ein Deployment mit eigenem Erkennungs-Sidecar die Seiten dorthin schickt; lässt man es weg, liest sie die im Parser mitgelieferte Engine. So oder so bleiben die Seiten im eigenen Netz des Deployments.

Ein .docx wird hier abgelehnt, obwohl der Parser eines liest. Die Ingestion leitet Office-Dokumente an den nativen Leser, bevor sie den OCR-Parser befragt - eines anzunehmen würde also vorhandene Absätze herausziehen, gescannte Seiten überspringen und über den Unterschied schweigen. Konvertieren Sie es in PDF.

Ein Aufruf erkennt höchstens 200 Seiten und hat 120 Sekunden, beides enger als bei der Ingestion, weil hier jemand wartet und auf eine Ingestion niemand.

Spracherkennung

POST /api/v1/ml/audio/transcriptions transkribiert eine Aufnahme mit einem Schlüssel aus dem Vault der Organisation. provider und model benennen, was benutzt wird; lässt man beide weg, wird die Spracherkennungs-Voreinstellung der Organisation von der Voice-Karte genommen, ohne eine solche das erste angebotene Paar des Deployments, und nennt man einen Anbieter ohne Modell, dessen erstes Modell. Was nie passiert: auf den Standardanbieter zurückzufallen, obwohl ein Anbieter genannt wurde.

Eine Aufnahme unterliegt außerdem der eigenen 25-MB-Grenze des Transkriptionsclients, auch wenn ML_MAX_UPLOAD_SIZE_MB höher steht - eine zu große Aufnahme wird also als zu groß abgelehnt, statt die Engine zu erreichen und als 503 über einen Schlüssel zurückzukommen.

Der Schlüssel wird benannt, nie gesucht. secret_id benennt den Vault-Schlüssel, mit dem die Aufnahme gesendet wird: einen API-Schlüssel, dessen Zweck der Anbieter ist und den der Aufrufer sehen kann. Fehlt er, wird der Standardschlüssel der Organisation benutzt, aber nur für den Anbieter, für den er gewählt wurde; jeder andere Anbieter ohne secret_id wird als not_configured abgelehnt. Ein Schlüssel für einen anderen Anbieter, einer anderen Art oder einer, den der Aufrufer nicht sieht, wird am Feld secret_id abgelehnt. Der OPENAI_API_KEY des Deployments wird nie benutzt.

Jede Ebene, die Spracherkennung einrichtet, funktioniert gleich: Das Modell ist eine Wahl aus dem Spracherkennungskatalog, und der Schlüssel ist ein daneben benanntes Vault-Secret. Kein model profile wird gelesen - Profile sind das, was ein Text-Run nutzt -, und nichts wählt einen Schlüssel, nur weil er zufällig der einzige für einen Anbieter ist. Bei zwei OpenAI-Schlüsseln im Vault zahlt der benannte.

Noch kein selbst gehosteter Spracherkennungsserver

Jeder Anbieter im Katalog wird unter seiner eigenen Adresse erreicht, mit einem Schlüssel. Ein schlüsselloser Server im eigenen Netz - ein Whisper hinter einer OpenAI-kompatiblen API - ist heute nicht erreichbar. Das ist ein geplanter Folgeschritt: ein lokaler Dienst einer neuen Art, speech, benannt anstelle eines Schlüssels, so wie Collections schon einen Embedding- oder OCR-Server benennen.

Eine Ablehnung sagt in details.reason, warum, hier wie beim Diktieren. Der eigene Fehlertext des Anbieters wird protokolliert und nie zurückgegeben, denn ein Anbieter-Client schreibt die Anfrage-URL samt Schlüssel in seine Meldung.

reason Status Was passiert ist
not_configured 503 Für den Anbieter ist kein brauchbarer Schlüssel benannt - keiner gewählt, gelöscht oder auf dieser Ebene nicht mehr zulässig; beim Diktieren auch: Server-Diktat nicht eingerichtet
empty 400 Die Engine hat keine Sprache gehört, oder die Aufnahme hat keine Bytes
unsupported_type 400 Nur beim Diktieren: die Aufnahme ist kein WAV mit 16 kHz, Mono und 16-Bit-PCM - oder, wo das Deployment komprimiertes Audio annimmt, kein WebM, Ogg oder MP4 -, oder auf einer gehosteten Seite oder in einem Widget lässt sich die Länge einer komprimierten Aufnahme nicht lesen. details.accepted listet die angenommenen Typen
too_large 400 Über 25 MB oder über der Diktatgrenze der Organisation in Bytes oder in gemessener Länge; details.limit_seconds nennt die Grenze
provider_error 503 Der Anbieter hat die Anfrage abgelehnt oder ist daran gescheitert
timeout 503 Der Anbieter hat nicht innerhalb von 60 Sekunden geantwortet

Diktieren im Chat

POST /api/v1/agents/{agent_id}/dictation ist das Mikrofon im Eingabefeld von /chat, wenn die Organisation auf dem Server transkribiert. Es nimmt eine Aufnahme als file und antwortet mit {"text": "..."}, das das Eingabefeld zum Bearbeiten einsetzt; gesendet wird nichts, bis die Person auf Senden drückt. Der Aufrufer nennt weder Anbieter noch Modell noch Sprache — die lösen sich aus dem Agent und der Organisation auf — und die Person darf jedem Agent diktieren, den sie ausführen darf, ein Viewer mit einem use-Grant darauf eingeschlossen. Ein Agent, den sie nicht ausführen darf, antwortet mit 404, wie überall sonst.

Der Schalter gehört dem Agent. Eine Aufnahme wird nur transkribiert, solange der veröffentlichte Diktier-Schalter des Agents, speech_to_text.dictation, an ist; aus, nutzt der Composer die Spracherkennung des Browsers, und eine trotzdem gesendete Aufnahme wird als not_configured abgelehnt, wie für einen archivierten Agent. Anbieter, Modell und Schlüssel lösen sich dann aus Agent und Organisation auf. Aus der Organisation, eingestellt auf der Karte Stimme (Voice) von jemandem mit org:settings, kommen speech_to_text_provider und speech_to_text_model (ein Paar aus dem Speech-to-Text-Katalog), speech_to_text_language (ein ISO-639-1-Code, oder null, damit die Engine ihn erkennt) und dictation_max_seconds (10 bis 600, standardmäßig 300), das nur die Organisation festlegt.

GET /api/v1/agents/{agent_id}/dictation sagt mit demselben Zugriff, wie das Mikrofon transkribieren wird, bevor jemand aufnimmt: engine (server oder browser), requested (der Schalter des Agents), server_available, unavailable, max_seconds, provider und format. server_available verlangt ein Paar, das sich für diesen Agent auflöst, einen Katalog, der es noch anbietet, und einen brauchbaren Schlüssel, der neben dem Paar benannt ist - auf sein Vorhandensein geprüft, nie geöffnet; engine ist server, wenn das gilt, der Schalter an ist und der Agent nicht archiviert ist, und sonst browser.

Ist es falsch, sagt unavailable, was fehlt: no_provider (keine Ebene benennt ein Paar), not_offered (der Katalog hat es zurückgezogen) oder no_key (kein brauchbarer Schlüssel benannt - einen wählen). Das Embed-Formular liest server_available, denn eine gehostete Seite oder ein Widget hängt nicht am /chat-Schalter des Agents.

provider ist der Anbieter, den eine Aufnahme erreichen würde, und format, worin für ihn aufgenommen wird: wav, es sei denn, das Deployment nimmt komprimiertes Audio an und der Speech-to-Text-Katalog sagt, dass der Anbieter nimmt, was MediaRecorder erzeugt - dann webm. Das wird hier entschieden, sodass kein Client eine eigene Liste führt. Ein Agent ohne veröffentlichte Version antwortet aus den Einstellungen der Organisation.

Standardmäßig ist ein Diktat rohes PCM: audio/wav (oder audio/x-wav, audio/wave) mit 16 kHz, Mono und 16-Bit-Integer-PCM, das jeder Browser über Web Audio aufnimmt. Die Datei wird streng gelesen - ein fmt-Chunk, der genau das sagt, ein data-Chunk aus ganzen Samples, Größen, die zur Datei passen -, und jedes andere WAV, auch ein Stereo-, 48-kHz-, 24-Bit- oder Float-WAV, wird als unsupported_type abgelehnt. Der Anbieter bekommt die gezählten Samples hinter einem Header, den der Server baut, nie den Header des Aufrufers oder einen anderen Chunk der Datei, als recording.wav.

Wo das Deployment komprimiertes Audio annimmt, werden auch audio/webm, audio/ogg und audio/mp4 angenommen, ein Codec-Suffix ist erlaubt, und audio/x-m4a und audio/m4a werden als audio/mp4 gelesen, audio/opus sowie audio/x-ogg als audio/ogg. Sie erzeugt MediaRecorder in Browsern, und der Anbieter erhält sie als recording.webm, recording.ogg oder recording.m4a, nach derselben Tabelle, die eine Sprachnotiz aus einem Kanal voice.<ext> nennt.

Die Route liest höchstens 25 MB des Bodys; die Byte-Grenze der Organisation - die Bytes, die dictation_max_seconds plus drei Sekunden PCM belegen, 32.000 pro Sekunde, und 4 KB für den Container - gilt für das Gelesene. Ein Body, den die Anfragegrenze des Deployments ablehnt, bevor die Route läuft, bekommt 413 ohne reason, was das Eingabefeld als too_large liest.

Dann wird die Länge gemessen, bevor der Provider aufgerufen wird, und eine Aufnahme, die länger als dictation_max_seconds ist, wird als too_large abgelehnt, mit drei Sekunden Spielraum, weil das Eingabefeld im Sekundentakt stoppt. Bei PCM ist die Länge die gezählten Samples, exakt. Die einer komprimierten Aufnahme wird aus ihrem eigenen Header gelesen - der letzten Ogg-Seite, den Zeitstempeln der WebM-Blöcke, den Movie- oder Fragment-Headern von MP4 -, den eine von Hand gebaute Datei zu kurz angeben kann. Eine, deren Länge sich nicht lesen lässt, wird hier innerhalb der Byte-Grenze angenommen; eine gehostete Seite oder ein Widget lehnt sie ab.

Jede Person darf RATE_LIMIT_DICTATION_PER_MINUTE-mal pro Minute diktieren, standardmäßig 10, auf einem eigenen Zähler, sodass Diktieren nicht das ML-Kontingent einer Integration verbraucht. Jedes Diktat hinterlässt einen Aufrufdatensatz, unter speech_to_text und in Sekunden Audio gezählt - bei PCM die gezählte Länge. Das Eingabefeld schickt die aufgenommene Länge als Formularfeld duration neben file, das nur für eine komprimierte Aufnahme verwendet wird, für die nichts anderes eine Länge angibt.

Das Audio wird nie gespeichert: Es wird verworfen, wenn der Aufruf zurückkehrt, und der Text existiert nur in der Antwort. Abbrechen im Browser bricht eine bereits gesendete Aufnahme nicht ab — der Anbieter transkribiert sie trotzdem, stellt sie dem Konto der Organisation in Rechnung, und der Aufruf wird aufgezeichnet.

Bei Mistral nimmt der Browser immer WAV auf, und es werden weniger Sprachen transkribiert

Mistral dokumentiert für Voxtral WAV- und MP3-Eingaben, nicht WebM oder Opus, was Chrome und Firefox aufnehmen. Ist Mistral der Anbieter, auf den sich eine Aufnahme auflöst, nimmt das Eingabefeld deshalb in jedem Browser WAV mit 16 kHz, Mono und 16 Bit auf, auch wo das Deployment komprimiertes Audio annimmt. Ein PCM-Upload ist etwa doppelt so groß wie das WebM, das er ersetzt — 32.000 Bytes pro Sekunde —, und die Längengrenze ist dieselbe.

Die Sprachliste ist die von Whisper, die OpenAI und Groq akzeptieren. Mistral nennt 13 Sprachen für Voxtral, sodass eine festgelegte Sprache, die er nicht nimmt, als provider_error zurückkommt. Bei Mistral die Sprache auf automatischer Erkennung lassen und eine Aufnahme ausprobieren, bevor man sich darauf verlässt.

Welche Engine eine Aufnahme erreicht

Drei Stellen können eine Speech-to-Text-Engine nennen: die Karte Stimme (Voice) der Organisation, die speech_to_text-Überschreibung eines Agents, eingestellt auf seiner eigenen Karte Stimme (Voice) und live, sobald er veröffentlicht ist, und ein Kanal-Bot. Sie lösen sich in zwei Hälften auf:

Was Kommt von der ersten Stelle, die es setzt
Anbieter, Modell und Schlüssel, zusammen Der Bot, dann der Agent, dann die Organisation
Sprache Der Agent, dann die Organisation, dann erkennt die Engine sie

Der Schlüssel reist mit seinem Paar. Jede Ebene benennt einen Schlüssel neben ihrem eigenen Anbieter und Modell, und die drei lösen sich als Einheit auf: Ein Agent mit eigenem Paar sendet es mit seinem eigenen Schlüssel, nie mit dem der Organisation. Ein Schlüssel erreicht also nie einen Anbieter, der ihn nicht ausgestellt hat. Welche Schlüssel eine Ebene benennen darf:

Ebene Der Schlüssel muss sein
Die Voreinstellung der Organisation Ein API-Schlüssel für den Anbieter, für die ganze Organisation sichtbar - die Plattform gibt ihn für alle aus. Geprüft beim Speichern der Voice-Karte und erneut bei jeder Aufnahme
Das eigene Paar eines Kanal-Bots Ebenso: organisationsweit sichtbar, denn die Sprachnachrichten eines Bots werden für jeden bezahlt, der ihm schreibt
Die Überschreibung eines Agents Ein API-Schlüssel für den Anbieter, den der Veröffentlichende sehen kann - die Regel, der jeder Schlüssel in einer Agent-Spec folgt. Geprüft beim Veröffentlichen

Ein Schlüssel, der nach der Wahl auf Organisations- oder Bot-Ebene privat wird, wird nicht mehr benutzt, statt weiter für die Aufnahmen anderer zu zahlen. Ein gelöschter Schlüssel leert den Verweis auf diesen beiden Ebenen, und die Spec eines Agents behält die ID, bis sie mit einem anderen neu veröffentlicht wird: So oder so sagen die Voice-Karte und die Bereitschaftsprüfung „Schlüssel wählen“, und Aufnahmen werden als not_configured abgelehnt, bis jemand das tut.

Ein polnischer Agent in einer auf Englisch eingerichteten Organisation legt also language: pl fest und behält Anbieter und Schlüssel der Organisation. Ein Bot hat keine eigene Sprache; die des Agents oder der Organisation gilt auch für sein eigenes Paar. Löst sich nichts auf, wird nichts transkribiert: das Diktieren antwortet not_configured, und ein Bot sagt, dass er nicht zuhören kann.

Das Diktieren liest die veröffentlichte Standardversion des Agents, die, die /chat ausführt. Ein Bot liest den Agent, den seine Bindung bedient - nie einen Agent, den jemand mit @slug erwähnt, denn eine Aufnahme wird transkribiert, bevor Erwähnungen geroutet werden -, in der Version, die die Umgebung dieser Bindung festlegt, der, die der Run benutzen wird. Ein Agent im Entwurf oder archiviert hat keine Version, die jemand ausführt, und die Organisation antwortet allein.

Erkennung personenbezogener Daten

POST /api/v1/ml/privacy/pii nimmt JSON statt einer Datei:

curl -X POST "$BASE/api/v1/ml/privacy/pii" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Organization-Id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{"text": "write to ada@example.com", "categories": ["email"]}'
{
  "counts": [{"category": "email", "count": 1}],
  "total": 1,
  "redacted_text": "write to [redacted:email]"
}

Jede angeforderte Kategorie wird berichtet, auch die, die nichts getroffen haben — "gesucht und nicht vorhanden" und "nicht gesucht" sind verschiedene Antworten. Die Kategorien sind email, iban, credit_card, us_ssn und phone, und jede wird nach Form getroffen und dann geprüft: Luhn für eine Karte, ISO 7064 für eine IBAN, der Nummerierungsplan des Landes und die Gruppierung der Ziffern für eine Telefonnummer, damit eine Ziffernfolge nicht als Konto oder Nummer gemeldet wird.

Eine mit + geschriebene Telefonnummer wird unabhängig vom Land gefunden; eine nationale wie 415-555-0132 wird gegen die Standardländer der Guardrails gelesen, US, GB, DE, PL. Das Feld phone_regions, das diese Liste ändert, gehört zu den Guardrails eines Agenten, nicht zu diesem Endpunkt.

Zurück kommen Zählungen und der geschwärzte Text, nicht die Offsets der einzelnen Treffer. Die Detektoren antworten mit umgeschriebenem Text, und die Positionen zurückzugewinnen hieße, ihre Mustertabelle und ihre Prüfsummen zu kopieren — eine Kopie, die still aufhört, mit dem Original übereinzustimmen, ist schlimmer als ein engerer Vertrag.

Was aufgezeichnet wird

Jeder Aufruf hinterlässt eine Zeile: welcher Dienst, welche Organisation, wer gefragt hat, wie viele Bytes hineingingen, wie viel herauskam, wie lange es dauerte und wie es endete. GET /api/v1/ml/calls liest sie zurück, neueste zuerst, und GET /api/v1/ml/calls/{id} liest eine.

Ein abgelehnter Aufruf wird ebenfalls aufgezeichnet, und sein Datensatz wird festgeschrieben, bevor die Ablehnung die Anfrage abwickelt - eine Zeile, die der Transaktion nur hinzugefügt würde, nähme genau die Ablehnung zurück, die sie beschreibt, und einem Betreiber mit der Frage, warum eine Integration scheitert, würde gesagt, der Mandant habe gar keine Aufrufe gemacht.

Kein Inhalt wird aufbewahrt. Das Ergebnis geht in der Antwort zurück und wird nicht gespeichert, sodass ein hier geparstes Dokument kein Dokument wird, das dieses Deployment hält, und Text, der zur Prüfung auf personenbezogene Daten geschickt wurde, nicht in einer Tabelle bleibt, an die niemand als Dokumentenspeicher gedacht hat. Ein Aufrufdatensatz einer anderen Organisation ist nicht auffindbar, genau wie jede andere Zeile auf dieser API.

Nutzung wird in der Einheit gezählt, in der der Dienst arbeitet — Seiten bei einem Parse, Zeichen bei einem Scan, Sekunden Audio bei einer Transkription — und nicht in Geld. Die gelieferten Dienste laufen entweder auf den eigenen Maschinen des Betreibers, wo es keinen Anbieterpreis gibt, oder auf dem eigenen Anbieterkonto der Organisation, das ihr direkt in Rechnung stellt. Eine Zahl, die niemand mit einer Rechnung abgleichen kann, ist schlimmer als eine ehrliche Einheitenzählung. Auf das Budget eines Agents wird Transkription ebenfalls nicht angerechnet.

Transkriptionen

Sprache zu Text wird auf drei Wegen erreicht, und jeder hinterlässt dieselbe Zeile: die API (surface: api), Diktat (chat) und die Sprachnachrichten eines Kanal-Bots (channel). Die Zeile ergänzt provider und model, die secret_id des Vault-Schlüssels, mit dem sie gesendet wurde - so wie ein Run seinen Schlüssel festhält, damit „welcher Schlüssel kostet am meisten“ eine Antwort hat -, die agent_id, für die sie war - der Agent, in den diktiert wurde, oder der Agent, den der Bot bedient -, die channel_bot_id und audio_ms, die Länge der Aufnahme.

Das Löschen des Schlüssels behält die Zeile und leert ihre secret_id. Das requested_by einer Sprachnachricht ist die Person, mit der der Absender verknüpft ist, solange sie Mitglied der Organisation ist, oder niemand. Erfasst wird nur eine Aufnahme, die dem Provider übergeben wurde; ein ausgeschalteter Bot oder einer ohne Engine zum Erben hat niemanden gefragt.

Die Länge ist die erste bekannte von diesen:

Quelle Wer sie liefert
Die Aufnahme selbst Nur Diktat: die aus ihrem Container gelesene Länge, wie oben
Die Antwort des Providers OpenAI whisper-1 (usage.seconds), Groq (duration, angefordert mit response_format=verbose_json), Mistral (usage.prompt_audio_seconds)
Die Metadaten der Plattform Eine Telegram-Sprachnachricht oder Audiodatei, ein Slack-Clip
Der Browser Nur Diktat: die duration, die das Eingabefeld schickt, begrenzt auf das längste Diktat der Organisation, weil es das Wort des Clients ist

OpenAIs gpt-4o-transcribe und gpt-4o-mini-transcribe melden Tokens, keine Sekunden, und Mattermost gibt keine Länge an, sodass eine Sprachnachricht dort mit diesen Modellen keine hat. Eine erfolgreiche Zeile zählt dann 0 Sekunden mit leerem audio_ms - ungemessen, nicht still.

GET /api/v1/ml/calls filtert außer nach ml_service auch nach agent_id, channel_bot_id, surface, since und until, immer innerhalb der Organisation des Aufrufers: Die Agent-ID eines anderen Mandanten passt auf nichts. Agent, Bot und Weg - als Filter und als Spalten - verlangen runs:view, wie die Zusammenfassung unten. Mit ml:invoke allein wird ein Filter darauf mit 403 abgelehnt, und die drei kommen in jeder Zeile als null zurück.

GET /api/v1/ml/calls/summary?days=30 summiert die erfolgreichen Transkriptionen über ein Zeitfenster: seconds - die eigenen units jeder Zeile, also die Aufnahme auf die Sekunde aufgerundet, addiert, damit die Zusammenfassung zur Liste passt - und count, count_unknown_length für die Aufnahmen, die die Sekunden auslassen, und dasselbe nach Agent, nach Kanal-Bot und nach Weg (surface). Es verlangt runs:view, wie GET /spend, statt ml:invoke: Die Aufschlüsselung nennt Agents und Bots, die ein Member möglicherweise nicht sehen darf. Die Karte Stimme (Voice) der Organisation zeigt es in einer Zeile.

Grenzen

Ein einzelner Aufruf nimmt bis zu ML_MAX_UPLOAD_SIZE_MB Megabyte an, standardmäßig 25, und nur so viele Bytes werden aus dem Body gelesen - eine zu große Einreichung wird abgelehnt, ohne vorher in den Speicher kopiert worden zu sein. Ein Scan liest höchstens 200000 Zeichen, ein Scan mit phone höchstens 10.000 Ziffern: Die Telefonnummernsuche kostet Dutzende Mikrosekunden pro Ziffer, also wird ein längerer Text abgelehnt statt gelesen. Ein Aufrufer darf RATE_LIMIT_ML_PER_MINUTE Aufrufe pro Minute machen, standardmäßig 30, gezählt pro Aufrufer statt pro Adresse.

Ein Ratenlimit zählt Starts und sieht nicht, was noch läuft, was für Arbeit im Minutenbereich die falsche Form ist. Ein Worker parst deshalb höchstens ML_MAX_CONCURRENT_PARSES Dokumente gleichzeitig, standardmäßig 4, und ein Aufruf, der jeden Platz belegt vorfindet, wird mit einem Retry-After abgelehnt statt eingereiht: wem gesagt wird, gleich wiederzukommen, der kann das, und wer hinter vier Scans geparkt ist, hat längst irgendwo aufgegeben, wo es hier niemand sieht.

Die Ausführung ist synchron: die Antwort ist das Ergebnis, und es gibt keine Warteschlange zum Abfragen. Das ist ehrlich gegenüber dem, was hier ist, statt aspirativ — ein Warteschlangenmodus würde dem Aufrufdatensatz eigene Zustände hinzufügen, und in dieser Form käme er an.

Sie getrennt betreiben

Die Dienste skalieren anders als die Konsole. Ein OCR-Durchlauf sind CPU-gebundene Sekunden auf einem Thread; ein Dashboard auszuliefern ist keines von beidem. Also führt deploy/profiles/ml-services/ das API-Image ein zweites Mal als Replik aus, die nur diese Pfade beantwortet, mit eigenen Ressourcen und eigener Skalierung, und der Ingress davor leitet /api/v1/ml/ dorthin.

Es ist dasselbe Image und dieselbe Datenbank, und genau das hält eine Implementierung im Dienst sowohl der Agents als auch der direkten Aufrufer. Getrennt sind der Prozess, die Grenzen und der Neustart — also das, worum "unabhängig betrieben, aktualisiert und skaliert" bittet.

deploy/profiles/ml-services/README.md beschreibt das Overlay und was es erwartet.