Przejdź do treści

Usługi ML

Cztery usługi tej platformy można wywołać samodzielnie, bez rozpoczynania rozmowy i bez uruchamiania agenta: analizę dokumentu, OCR, zamianę mowy na tekst i wykrywanie danych osobowych. To te same implementacje, z których korzystają agenty, tyle że sięga się do nich wprost — jeden zestaw parserów, jeden zestaw detektorów, jeden klient transkrypcji.

Istnieją, bo może ich potrzebować inny komponent. Kolejka, która ma odczytać zeskanowany wniosek, zadanie wsadowe redagujące eksport, usługa chcąca transkrypcji — żadne z nich nie chce okna czatu i żadne nie powinno udawać, że nim jest.

Co jest dostarczone, a co nie

Każda rodzina usług to jeden wiersz. served znaczy, że endpoint tego wdrożenia odpowiada na nią silnikiem wymienionym obok. dependency znaczy, że rodzina jest wymagana i czegoś brakuje, a notatka mówi czego. prepared to przygotowanie architektury: żaden silnik nie jest dostarczany, a szew, którym by przyszedł, jest nazwany.

GET /api/v1/ml/services odpowiada tą samą tabelą, więc integracja może ją odczytać, zamiast wierzyć stronie dokumentacji.

Usługa Wymagania Endpoint Stan Silnik
document_analysis FA-069, FA-070 POST /api/v1/ml/documents/analyze served LiteParse albo PyMuPDF, lokalnie
ocr FA-069, FA-071 POST /api/v1/ml/documents/ocr served OCR LiteParse: wbudowany Tesseract albo zarejestrowany serwer OCR
speech_to_text FA-069, FA-072 POST /api/v1/ml/audio/transcriptions served Własny endpoint transkrypcji organizacji
pii_detection FA-069, FA-073 POST /api/v1/ml/privacy/pii served Detektory wzorców, których używają guardrails
pii_named_entities FA-073, DA-007 — dependency Brak na tym wdrożeniu
image_analysis FA-074 — prepared Brak na tym wdrożeniu

Dwa wiersze mówią „nie" i oba mówią dlaczego. Nazwane encje — imię i nazwisko, adres pocztowy — nie mają kształtu wzorca, więc żadne wyrażenie regularne ich nie znajdzie: potrzeba modelu rozpoznawania encji dla każdego języka w zakresie. Endpoint wykrywania poniesie dodatkowe kategorie w dniu, w którym taki model się pojawi, a do tego czasu ich nie deklaruje. Analiza obrazu jest oznaczona jako zakres przyszły w samych wymaganiach.

Jak wywołać

Uwierzytelnianie, nagłówek organizacji i koperta błędu należą do API HTTP. Nie ma osobnego klucza, osobnego hosta ani drugiego wejścia, i jest to świadome: powierzchnia z własnymi drzwiami frontowymi to powierzchnia z własnymi pomyłkami.

Wszystkie cztery bramkuje jedno uprawnienie: ml:invoke. Świadomie nie jest to agents:run — integracja, która parsuje dokumenty, nie powinna przez to móc wydawać budżetu modelowego organizacji. Trzyma je każda rola poza Viewer.

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"

Odpowiedź niesie strony w kolejności, przygotowane chunki oraz rozmiar i skrót pliku:

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

Analiza dokumentu

POST /api/v1/ml/documents/analyze czyta to, co dokument już niesie. Pole parser wybiera między liteparse, który zachowuje układ i czyta formaty biurowe tam, gdzie zainstalowano LibreOffice, a pymupdf, który czyta PDF-y i jest szybszy. chunk_size, chunk_overlap i chunking_strategy kształtują przygotowane chunki; strategie to recursive, fixed i markdown, a czwarta pisownia zostaje odrzucona, zamiast po cichu potraktowana jak recursive.

chunk_overlap musi być mniejszy niż chunk_size. Równy jest odrzucany i nie chodzi o porządek: splitter to przyjmuje, a potem przesuwa się o mniej więcej jeden separator na chunk, zachowując prawie kompletną kopię poprzedniego - więc legalne wysłanie odpowiada dokumentem zwielokrotnionym wiele razy.

Dokument, z którego nic nie da się odczytać, zostaje odrzucony, a nie zwrócony z pustą listą stron, a odmowa mówi, żeby wywołać OCR — czego skan parsowany dla warstwy tekstowej potrzebuje zawsze.

OCR

POST /api/v1/ml/documents/ocr rozpoznaje tekst na każdej stronie, niezależnie od tego, czy strona niesie warstwę tekstową. Tym różni się od ingestii, która wykrywa to automatycznie i pomija rozpoznawanie tam, gdzie tekst już jest: kto poprosił o OCR, poprosił o odczytanie stron jako obrazów.

language to kod Tesseracta, czyli trzy litery — pol, nie pl. Dostarczany obraz instaluje eng i pol i tylko te dwa endpoint przyjmuje: kod bez pakietu językowego za sobą wywraca się wewnątrz parsowania, więc odmowa przychodzi już na granicy. Wdrożenie, które doinstaluje więcej pakietów, poszerza tę listę w tej samej zmianie.

ocr_service_id nazywa serwer OCR zarejestrowany wśród usług lokalnych, więc wdrożenie z własnym sidecarem rozpoznawania wysyła strony tam; pomiń je, a odczyta je silnik wbudowany w parser. Tak czy inaczej strony zostają w sieci samego wdrożenia.

.docx jest tu odrzucany, choć parser go czyta. Ingestia kieruje dokumenty biurowe do czytnika natywnego, zanim sięgnie po parser OCR, więc przyjęcie takiego pliku wyciągnęłoby istniejące akapity, pominęło zeskanowane strony i nic by o tej różnicy nie powiedziało. Przekonwertuj go na PDF.

Jedno wywołanie rozpoznaje najwyżej 200 stron i ma 120 sekund — oba węższe niż w ingestii, bo tutaj ktoś czeka, a na ingestię nie czeka nikt.

Zamiana mowy na tekst

POST /api/v1/ml/audio/transcriptions transkrybuje nagranie na kluczu z vaultu organizacji. provider i model nazywają, czego użyć; pominięcie obu bierze domyślne ustawienie zamiany mowy na tekst organizacji z karty Głos (Voice), a gdy go nie ma — pierwszą oferowaną parę wdrożenia; podanie dostawcy bez modelu bierze pierwszy model tego dostawcy. Czego nie robi nigdy, to nie wraca do domyślnego dostawcy, gdy dostawca został nazwany.

Nagranie podlega też własnemu limitowi 25 MB klienta transkrypcji, nawet gdy ML_MAX_UPLOAD_SIZE_MB jest wyższy, więc za duże nagranie zostaje odrzucone jako za duże, a nie dociera do silnika i wraca jako 503 o kluczu.

Klucz jest nazwany, nigdy wyszukiwany. secret_id wskazuje klucz z vaultu, z którym nagranie zostanie wysłane: klucz API, którego przeznaczeniem jest dostawca i który wywołujący widzi. Bez niego używany jest domyślny klucz organizacji, ale tylko dla dostawcy, dla którego go wybrano; każdy inny dostawca bez secret_id dostaje odmowę not_configured. Klucz innego dostawcy, innego rodzaju albo taki, którego wywołujący nie widzi, jest odrzucany na polu secret_id. OPENAI_API_KEY wdrożenia nie jest używany nigdy.

Każdy poziom konfigurujący zamianę mowy na tekst działa tak samo: model to wybór z katalogu zamiany mowy na tekst, a klucz to sekret z vaultu wskazany obok niego. Żaden profil modelu nie jest czytany — profile służą runom tekstowym — i nic nie wybiera klucza tylko dlatego, że jest jedynym dla danego dostawcy. Przy dwóch kluczach OpenAI w vaulcie płaci ten, który wskazano.

Nie ma jeszcze własnego serwera zamiany mowy na tekst

Każdy dostawca z katalogu jest osiągany pod własnym adresem, z kluczem. Serwer bez klucza w twojej sieci — Whisper za API zgodnym z OpenAI — nie jest dziś osiągalny. To planowany kolejny krok: usługa lokalna nowego rodzaju, speech, wskazywana zamiast klucza, tak jak kolekcje wskazują już serwer embeddingów albo OCR.

Odmowa mówi dlaczego w details.reason, tutaj i przy dyktowaniu tak samo. Własny tekst błędu dostawcy trafia do logu i nigdy nie jest zwracany, bo klient dostawcy wkłada do swojego komunikatu URL żądania razem z kluczem.

reason Status Co się stało
not_configured 503 Dla dostawcy nie wskazano użytecznego klucza — nie wybrano go, usunięto go albo ten poziom nie może go już używać; przy dyktowaniu także brak skonfigurowanego dyktowania na serwerze
empty 400 Silnik nie usłyszał mowy albo nagranie nie ma bajtów
unsupported_type 400 Tylko dyktowanie: nagranie nie jest WAV z 16-bitowym PCM 16 kHz mono — ani, gdy wdrożenie przyjmuje skompresowany dźwięk, WebM, Ogg czy MP4 — albo, na hostowanej stronie lub w widgecie, nie da się odczytać długości skompresowanego nagrania. details.accepted wymienia przyjmowane typy
too_large 400 Ponad 25 MB albo ponad limit dyktowania organizacji w bajtach lub w zmierzonej długości; details.limit_seconds podaje limit
provider_error 503 Dostawca odrzucił żądanie albo nie zdołał go obsłużyć
timeout 503 Dostawca nie odpowiedział w ciągu 60 sekund

Dyktowanie w czacie

POST /api/v1/agents/{agent_id}/dictation to mikrofon w polu wiadomości /chat, gdy organizacja transkrybuje na serwerze. Przyjmuje jedno nagranie jako file i odpowiada {"text": "..."}, które pole wiadomości wstawia do edycji; nic nie zostaje wysłane, dopóki osoba nie naciśnie Wyślij. Wywołujący nie nazywa dostawcy, modelu ani języka — rozwiązują się z agenta i organizacji — a osoba może dyktować do każdego agenta, którego może uruchomić, łącznie z osobą z rolą Viewer, która ma na nim grant use. Agent, którego nie może uruchomić, odpowiada 404, jak wszędzie indziej.

Przełącznik należy do agenta. Nagranie jest transkrybowane tylko wtedy, gdy opublikowany przełącznik Dyktowania agenta, speech_to_text.dictation, jest włączony; wyłączony oznacza, że kompozytor używa rozpoznawania mowy przeglądarki, a nagranie wysłane mimo to jest odrzucane jako not_configured, tak jak dla zarchiwizowanego agenta. Dostawca, model i klucz rozwiązują się wtedy z agenta i organizacji. Z organizacji, ustawione na karcie Głos (Voice) przez kogoś z org:settings, pochodzą speech_to_text_provider i speech_to_text_model (para z katalogu speech-to-text), speech_to_text_language (kod ISO-639-1 albo null, by silnik sam go rozpoznał) oraz dictation_max_seconds (od 10 do 600, domyślnie 300), które ustawia tylko organizacja.

GET /api/v1/agents/{agent_id}/dictation, z tym samym dostępem, mówi, jak mikrofon będzie transkrybował, zanim ktokolwiek zacznie nagrywać: engine (server albo browser), requested (przełącznik agenta), server_available, unavailable, max_seconds, provider i format. server_available wymaga pary, która rozwiązuje się dla tego agenta, katalogu, który nadal ją oferuje, oraz użytecznego klucza wskazanego obok pary — sprawdzanego pod kątem istnienia, nigdy nie otwieranego; engine to server, gdy to zachodzi, przełącznik jest włączony, a agent nie jest zarchiwizowany, i browser w przeciwnym razie.

Gdy jest fałszywe, unavailable mówi, czego brakuje: no_provider (żaden poziom nie wskazuje pary), not_offered (katalog ją wycofał) albo no_key (nie wskazano użytecznego klucza — wybierz go). Formularz embeda czyta server_available, bo hostowana strona i widget nie zależą od przełącznika agenta w /chat.

provider to ten, do którego trafiłoby nagranie, a format to, w czym dla niego nagrywać: wav, chyba że wdrożenie przyjmuje skompresowany dźwięk, a katalog speech-to-text mówi, że dostawca przyjmuje to, co tworzy MediaRecorder — wtedy webm. Rozstrzygane tutaj, więc żaden klient nie trzyma własnej listy. Agent bez niczego opublikowanego odpowiada według ustawień organizacji.

Domyślnie dyktowanie to surowy PCM: audio/wav (albo audio/x-wav, audio/wave) z 16-bitowym całkowitoliczbowym PCM 16 kHz mono, który każda przeglądarka nagrywa przez Web Audio. Plik jest czytany ściśle — jeden fragment fmt mówiący dokładnie to, jeden fragment data z pełnymi próbkami, rozmiary zgodne z plikiem — a każdy inny WAV, także stereo, 48 kHz, 24-bitowy czy zmiennoprzecinkowy, jest odrzucany jako unsupported_type. Dostawca dostaje policzone próbki za nagłówkiem zbudowanym przez serwer, nigdy nagłówek wywołującego ani inny fragment, który plik zawierał, jako recording.wav.

Gdy wdrożenie przyjmuje skompresowany dźwięk, przyjmowane są też audio/webm, audio/ogg i audio/mp4, z dopuszczalnym sufiksem kodeka, a audio/x-m4a i audio/m4a są czytane jako audio/mp4, a audio/opus i audio/x-ogg jako audio/ogg. To właśnie tworzy MediaRecorder przeglądarek, a dostawca otrzymuje je jako recording.webm, recording.ogg albo recording.m4a, według tej samej tabeli, która nazywa notatkę głosową z kanału voice.<ext>.

Trasa czyta najwyżej 25 MB treści żądania; limit bajtów organizacji — tyle bajtów, ile zajmuje PCM przez dictation_max_seconds plus trzy sekundy, 32 000 na sekundę, i 4 KB na kontener — jest stosowany do tego, co przeczytano. Treść, którą limit żądań wdrożenia odrzuca, zanim trasa ruszy, dostaje 413 bez reason, co pole wiadomości odczytuje jako too_large.

Potem długość jest mierzona, zanim dostawca zostanie wywołany, a nagranie dłuższe niż dictation_max_seconds jest odrzucane jako too_large, z trzema sekundami zapasu, bo pole wiadomości zatrzymuje nagranie przy takcie co sekundę. Dla PCM długość to policzone próbki, dokładnie. Długość skompresowanego nagrania jest odczytywana z jego własnego nagłówka — ostatniej strony Ogg, znaczników czasu bloków WebM, nagłówków filmu albo fragmentów MP4 — który plik zbudowany ręcznie może zaniżyć. Nagranie, którego długości nie da się odczytać, jest tu przyjmowane w granicach limitu bajtów; hostowana strona i widget je odrzucają.

Każda osoba może dyktować RATE_LIMIT_DICTATION_PER_MINUTE razy na minutę, domyślnie 10, na osobnym liczniku, więc dyktowanie nie zużywa limitu usług ML integracji. Każde dyktowanie zostawia jeden zapis wywołania, pod speech_to_text, liczony w sekundach dźwięku — dla PCM w policzonej długości. Pole wiadomości wysyła długość, którą nagrało, jako pole formularza duration obok file, używane tylko dla skompresowanego nagrania, którego długości nie podaje nic innego.

Dźwięk nigdy nie jest przechowywany: zostaje odrzucony, gdy wywołanie się kończy, a tekst istnieje tylko w odpowiedzi. Anulowanie w przeglądarce nie anuluje nagrania już wysłanego — dostawca i tak je transkrybuje, obciąża za nie konto organizacji, a wywołanie zostaje zapisane.

W Mistralu przeglądarka zawsze nagrywa WAV, a transkrybowanych języków jest mniej

Mistral dokumentuje dla Voxtrala wejście WAV i MP3, a nie WebM ani Opus, które nagrywają Chrome i Firefox. Dlatego gdy dostawcą, do którego rozwiązuje się nagranie, jest Mistral, pole wiadomości w każdej przeglądarce nagrywa WAV 16 kHz, mono, 16 bitów, nawet gdy wdrożenie przyjmuje skompresowany dźwięk. Przesyłany PCM jest mniej więcej dwa razy większy niż WebM, który zastępuje — 32 000 bajtów na sekundę — a limit długości jest ten sam.

Lista języków pochodzi z Whispera, którą akceptują OpenAI i Groq. Mistral podaje 13 języków dla Voxtrala, więc ustalony język, którego nie przyjmuje, wraca jako provider_error. W Mistralu zostaw język na automatycznym wykrywaniu i wypróbuj nagranie, zanim zaczniesz na nim polegać.

Do którego silnika trafia nagranie

Silnik zamiany mowy na tekst mogą wskazać trzy miejsca: karta Głos (Voice) organizacji, nadpisanie speech_to_text agenta, ustawiane na jego własnej karcie Głos (Voice) i działające po opublikowaniu, oraz bot kanału. Rozwiązują się w dwóch połowach:

Co Pochodzi z pierwszego, które to ustawia
Dostawca, model i klucz, razem Bot, potem agent, potem organizacja
Język Agent, potem organizacja, potem silnik sam go rozpoznaje

Klucz podróżuje ze swoją parą. Każdy poziom wskazuje klucz obok własnego dostawcy i modelu, a trójka rozwiązuje się jako całość: agent z własną parą wysyła ją z własnym kluczem, nigdy z kluczem organizacji. Klucz nigdy więc nie trafia do dostawcy, który go nie wydał. Jakie klucze może wskazać dany poziom:

Poziom Klucz musi być
Domyślne ustawienie organizacji Kluczem API dostawcy, widocznym dla całej organizacji — platforma wydaje go za wszystkich. Sprawdzane przy zapisie karty Głos i ponownie przy każdym nagraniu
Własna para bota kanału Tak samo: widocznym dla organizacji, bo za notatki głosowe bota płaci się w imieniu każdego, kto do niego pisze
Nadpisanie agenta Kluczem API dostawcy, który publikujący widzi — regułą, której podlega każdy klucz wskazany w specu agenta. Sprawdzane przy publikacji

Klucz, który po wybraniu na poziomie organizacji albo bota stał się prywatny, przestaje być używany, zamiast dalej płacić za cudze nagrania. Usunięty klucz czyści odwołanie na tych dwóch poziomach, a spec agenta zachowuje identyfikator do ponownej publikacji z innym: tak czy inaczej karta Głos i sprawdzenie gotowości mówią „wybierz klucz”, a nagrania są odrzucane jako not_configured, dopóki ktoś tego nie zrobi.

Polski agent w organizacji skonfigurowanej dla angielskiego ustawia więc language: pl i zachowuje dostawcę i klucz organizacji. Bot nie ma własnego języka; język agenta albo organizacji dotyczy także jego własnej pary. Gdy nic się nie rozwiązuje, nic nie jest transkrybowane: dyktowanie odpowiada not_configured, a bot mówi, że nie potrafi słuchać.

Dyktowanie czyta domyślną opublikowaną wersję agenta, tę, którą uruchamia /chat. Bot czyta agenta, którego obsługuje jego powiązanie — nigdy agenta, o którym ktoś wspomni przez @slug, bo nagranie jest transkrybowane, zanim wzmianki zostaną przekierowane — w wersji, którą przypina środowisko tego powiązania, tej, której użyje run. Agent w wersji roboczej albo zarchiwizowany nie ma wersji, którą ktokolwiek uruchamia, i odpowiada sama organizacja.

Wykrywanie danych osobowych

POST /api/v1/ml/privacy/pii przyjmuje JSON, a nie plik:

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]"
}

Raportowana jest każda zamówiona kategoria, także te, które nic nie dopasowały — „szukano i nie ma" i „nie szukano" to różne odpowiedzi. Kategorie to email, iban, credit_card, us_ssn i phone, a każda jest dopasowywana kształtem, a potem sprawdzana: Luhn dla karty, ISO 7064 dla IBAN-u, plan numeracji kraju i grupowanie cyfr dla numeru telefonu, więc ciąg cyfr nie zostaje zgłoszony jako rachunek ani numer.

Numer telefonu zapisany z + jest znajdowany niezależnie od kraju; krajowy, taki jak 415-555-0132, jest czytany względem domyślnych krajów guardrails, US, GB, DE, PL. Pole phone_regions, które zmienia tę listę, należy do guardrails agenta, nie do tego endpointu.

Wraca liczność i tekst po redakcji, a nie offsety poszczególnych dopasowań. Detektory odpowiadają przepisanym tekstem, a odzyskanie pozycji oznaczałoby skopiowanie ich tablicy wzorców i sum kontrolnych — kopia, która po cichu przestaje zgadzać się z oryginałem, jest gorsza niż węższy kontrakt.

Co jest zapisywane

Każde wywołanie zostawia wiersz: która usługa, która organizacja, kto poprosił, ile bajtów weszło, ile wyszło, jak długo trwało i jak się skończyło. GET /api/v1/ml/calls odczytuje je z powrotem, od najnowszych, a GET /api/v1/ml/calls/{id} odczytuje jedno.

Odrzucone wywołanie też jest zapisywane, a jego wiersz jest commitowany, zanim odmowa rozwinie żądanie — wiersz tylko dodany do tej transakcji zostałby wycofany przez samą odmowę, którą opisuje, a operator pytający, czemu integracja się psuje, usłyszałby, że tenant nie wykonał żadnych wywołań.

Żadna treść nie jest przechowywana. Wynik wraca w odpowiedzi i nie jest zapisywany, więc dokument tu sparsowany nie staje się dokumentem, który to wdrożenie trzyma, a tekst wysłany do sprawdzenia pod kątem danych osobowych nie zostaje w tabeli, o której nikt nie myślał jak o składnicy dokumentów. Wiersz wywołania innej organizacji jest nie do znalezienia, tak samo jak każdy inny wiersz na tym API.

Zużycie liczy się w jednostce, w której pracuje usługa — strony przy parsowaniu, znaki przy skanowaniu, sekundy dźwięku przy transkrypcji — a nie w pieniądzu. Dostarczone usługi działają albo na maszynach samego operatora, gdzie nie ma ceny dostawcy, albo na własnym koncie dostawcy organizacji, które rozlicza ją wprost. Liczba, której nikt nie pogodzi z fakturą, jest gorsza niż uczciwe zliczenie jednostek. Transkrypcja nie wlicza się też do budgetu agenta.

Transkrypcje

Do zamiany mowy na tekst prowadzą trzy drogi i każda zostawia ten sam wiersz: API (surface: api), dyktowanie (chat) i wiadomości głosowe bota kanału (channel). Wiersz dodaje provider i model, secret_id klucza z vaultu, z którym je wysłano — tak jak run zapisuje swój klucz, więc „który klucz kosztuje najwięcej” ma jedną odpowiedź — agent_id, dla którego było nagranie - agenta, do którego dyktowano, albo agenta obsługiwanego przez bota - channel_bot_id oraz audio_ms, długość nagrania.

Usunięcie klucza zostawia wiersz i czyści jego secret_id. requested_by wiadomości głosowej to osoba, z którą powiązany jest nadawca, dopóki jest członkiem organizacji, albo nikt. Zapisywane jest tylko nagranie przekazane providerowi; bot wyłączony albo bez silnika do odziedziczenia nikogo nie pytał.

Długość to pierwsze znane z poniższych:

Źródło Kto ją podaje
Samo nagranie Tylko dyktowanie: długość odczytana z kontenera, jak wyżej
Odpowiedź providera OpenAI whisper-1 (usage.seconds), Groq (duration, o które prosi response_format=verbose_json), Mistral (usage.prompt_audio_seconds)
Metadane platformy Notatka głosowa albo plik audio z Telegrama, klip ze Slacka
Przeglądarka Tylko dyktowanie: duration wysłane przez pole wiadomości, przycięte do najdłuższego dyktowania organizacji, bo to słowo klienta

gpt-4o-transcribe i gpt-4o-mini-transcribe od OpenAI podają tokeny, nie sekundy, a Mattermost nie podaje długości, więc wiadomość głosowa tam na tych modelach jej nie ma. Udany wiersz liczy wtedy 0 sekund przy pustym audio_ms - niezmierzone, a nie ciche.

GET /api/v1/ml/calls zawęża też po agent_id, channel_bot_id, surface, since i until, a nie tylko po ml_service, zawsze w organizacji wywołującego: id agenta innej organizacji nie pasuje do niczego. Agent, bot i droga — jako filtry i jako kolumny — wymagają runs:view, jak podsumowanie niżej. Z samym ml:invoke filtr po nich jest odrzucany z 403, a w każdym wierszu wszystkie trzy wracają jako null.

GET /api/v1/ml/calls/summary?days=30 sumuje udane transkrypcje w oknie czasu: seconds — własne units każdego wiersza, czyli nagranie zaokrąglone w górę do sekundy, zsumowane, więc podsumowanie zgadza się z listą — oraz count, count_unknown_length dla nagrań, których sekundy nie obejmują, oraz to samo według agenta, bota kanału i drogi (surface). Wymaga runs:view, jak GET /spend, a nie ml:invoke: podział wymienia agentów i boty, których Member może nie widzieć. Karta Głos (Voice) organizacji pokazuje to w jednej linijce.

Limity

Pojedyncze wywołanie przyjmuje do ML_MAX_UPLOAD_SIZE_MB megabajtów, domyślnie 25, i tylko tyle bajtów jest odczytywanych z ciała żądania — za duże wysłanie zostaje odrzucone, zanim w ogóle zostanie skopiowane do pamięci. Jedno skanowanie czyta najwyżej 200000 znaków, a skanowanie obejmujące phone najwyżej 10 000 cyfr: dopasowanie numerów telefonu kosztuje dziesiątki mikrosekund na cyfrę, więc dłuższy tekst zostaje odrzucony, a nie przeczytany. Wywołujący może wykonać RATE_LIMIT_ML_PER_MINUTE wywołań na minutę, domyślnie 30, liczonych na wywołującego, a nie na adres.

Limit tempa liczy starty i nie widzi tego, co wciąż trwa, co jest złym kształtem dla pracy mierzonej w minutach. Dlatego worker parsuje naraz najwyżej ML_MAX_CONCURRENT_PARSES dokumentów, domyślnie 4, a wywołanie trafiające na zajęte wszystkie sloty dostaje odmowę z Retry-After, a nie miejsce w kolejce: kto usłyszy „wróć za chwilę", może wrócić, a kto stoi za czterema skanami, już się poddał gdzieś, gdzie nikt tutaj tego nie widzi.

Wykonanie jest synchroniczne: odpowiedź jest wynikiem i nie ma kolejki do odpytywania. To jest uczciwe wobec tego, co tu jest, zamiast aspiracyjne — tryb kolejkowy dodałby własne stany do wiersza wywołania, i taki miałby kształt.

Wdrażanie ich osobno

Usługi skalują się inaczej niż konsola. Przebieg OCR to sekundy pracy procesora na wątku; serwowanie dashboardu nie jest ani jednym, ani drugim. Dlatego deploy/profiles/ml-services/ uruchamia obraz API drugi raz jako replikę, która odpowiada tylko na te ścieżki, z własnymi zasobami i własnym skalowaniem, a ingress przed nią kieruje do niej /api/v1/ml/.

To ten sam obraz i ta sama baza, i właśnie to sprawia, że jedna implementacja obsługuje zarówno agenty, jak i wywołujących wprost. Osobny jest proces, limity i restart — czyli to, o co prosi „wdrażane, aktualizowane i skalowane niezależnie".

Nakładkę i jej oczekiwania opisuje deploy/profiles/ml-services/README.md.