Los servicios de ML¶
Cuatro servicios de esta plataforma se pueden llamar por su cuenta, sin empezar una conversación y sin ejecutar un agent: análisis de documentos, OCR, transcripción de voz y detección de datos personales. Son las mismas implementaciones que usan los agents, alcanzadas directamente — un conjunto de parsers, un conjunto de detectores, un cliente de transcripción.
Existen porque otro componente puede necesitarlos. Una cola que tiene que leer un formulario escaneado, un proceso por lotes que redacta una exportación, un servicio que quiere una transcripción — ninguno quiere una ventana de chat, y ninguno debería tener que fingir que lo es.
Qué está entregado y qué no¶
Cada familia de servicios es una fila. served significa que un endpoint de este despliegue la responde con el motor nombrado al lado. dependency significa que la familia es obligatoria y falta algo, y la nota dice qué. prepared es preparación de arquitectura: no se entrega ningún motor, y la costura por la que llegaría está nombrada.
GET /api/v1/ml/services responde con esta misma tabla, de modo que una
integración puede leerla en lugar de fiarse de una página.
| Servicio | Requisitos | Endpoint | Estado | Motor |
|---|---|---|---|---|
document_analysis |
FA-069, FA-070 | POST /api/v1/ml/documents/analyze |
served | LiteParse o PyMuPDF, en local |
ocr |
FA-069, FA-071 | POST /api/v1/ml/documents/ocr |
served | OCR de LiteParse: Tesseract incluido, o un servidor OCR registrado |
speech_to_text |
FA-069, FA-072 | POST /api/v1/ml/audio/transcriptions |
served | El endpoint de transcripción propio de la organización |
pii_detection |
FA-069, FA-073 | POST /api/v1/ml/privacy/pii |
served | Los detectores de patrones que usan los guardrails |
pii_named_entities |
FA-073, DA-007 | — | dependency | Ninguno en este despliegue |
image_analysis |
FA-074 | — | prepared | Ninguno en este despliegue |
Dos filas dicen que no, y ambas dicen por qué. Las entidades nombradas — el nombre de una persona, una dirección postal, un número de teléfono — no tienen forma de patrón, así que ninguna expresión regular las encuentra: eso requiere un modelo de reconocimiento de entidades por cada idioma del alcance. El endpoint de detección llevará las categorías adicionales el día que se provea uno, y hasta entonces no las reclama. El análisis de imagen está marcado como alcance futuro en los propios requisitos.
Cómo llamar a uno¶
La autenticación, la cabecera de organización y el sobre de error son los de la API HTTP. No hay clave aparte, ni host aparte, ni una segunda entrada, y es deliberado: una superficie con su propia puerta principal es una superficie con sus propios errores.
Un permiso controla los cuatro: ml:invoke. Deliberadamente no es
agents:run — una integración que parsea documentos no debería por ello poder
gastar el presupuesto de modelo de la organización. Lo tienen todos los roles
salvo 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"
La respuesta lleva las páginas en orden, los chunks preparados, y el tamaño y el hash del archivo:
{
"filename": "scan.pdf",
"filetype": "pdf",
"byte_size": 184320,
"content_hash": "9f2c…",
"pages": [{"page_num": 1, "content": "Antrag auf …"}],
"chunks": ["Antrag auf …"]
}
Análisis de documentos¶
POST /api/v1/ml/documents/analyze lee lo que el documento ya lleva. El campo
parser elige entre liteparse, que conserva la maquetación y lee formatos de
ofimática donde LibreOffice está instalado, y pymupdf, que lee PDF y es más
rápido. chunk_size, chunk_overlap y chunking_strategy dan forma a los
chunks preparados; las estrategias son recursive, fixed y markdown, y una
cuarta grafía se rechaza en vez de tratarse en silencio como recursive.
chunk_overlap tiene que ser menor que chunk_size. Igual se rechaza, y no
por prolijidad: el splitter lo acepta y entonces avanza más o menos un separador
por chunk mientras conserva una copia casi completa del anterior, así que una
subida legítima responde con un documento multiplicado muchas veces.
Un documento del que no se puede leer nada se rechaza en lugar de responderse con una lista de páginas vacía, y el rechazo dice que se llame a OCR — que es lo que un escaneo parseado por su capa de texto siempre necesita.
OCR¶
POST /api/v1/ml/documents/ocr reconoce el texto de cada página, lleve o no la
página una capa de texto. Esa es la diferencia con la ingesta, que lo
autodetecta y se salta el reconocimiento donde el texto ya está: quien pidió OCR
pidió que las páginas se leyeran como imágenes.
language es un código de Tesseract, es decir tres letras — pol, no pl. La
imagen que se distribuye instala eng y pol, y esos dos son los que el
endpoint acepta: un código sin paquete de idioma detrás falla dentro del parseo,
así que se rechaza en la frontera. Un despliegue que instale más paquetes amplía
la lista en el mismo cambio.
ocr_service_id nombra un servidor OCR registrado entre los
servicios locales, de modo que un despliegue con su propio
sidecar de reconocimiento envía allí las páginas; omítelo y las lee el motor
incluido con el parser. En cualquier caso las páginas se quedan en la red del
propio despliegue.
Un .docx se rechaza aquí, aunque el parser lea uno. La ingesta encamina los
documentos de ofimática al lector nativo antes de consultar el parser de OCR, así
que aceptar uno extraería sus párrafos existentes, se saltaría sus páginas
escaneadas y no diría nada de la diferencia. Conviértelo a PDF.
Una llamada reconoce como mucho 200 páginas y tiene 120 segundos, ambos más estrechos que los de la ingesta, porque aquí alguien espera y a una ingesta no la espera nadie.
Transcripción de voz¶
POST /api/v1/ml/audio/transcriptions transcribe una grabación con las
credenciales propias de la organización. provider y model nombran qué usar;
omitir ambos toma el primer par ofrecido por el despliegue, y nombrar un
proveedor sin modelo toma el primer modelo de ese proveedor. Lo que nunca hace es
recurrir al proveedor por defecto cuando se nombró un proveedor: así es como una
grabación destinada a un motor autoalojado acaba en un proveedor externo.
Una grabación se somete además al techo propio de 25 MB del cliente de
transcripción aunque ML_MAX_UPLOAD_SIZE_MB sea mayor, de modo que una grabación
demasiado grande se rechaza por ser demasiado grande y no llega al motor para
volver como un 503 sobre credenciales.
El motor es el endpoint que nombre el model profile de la organización para ese proveedor. Esa es la respuesta para un despliegue que no puede enviar audio a un proveedor: apunta el profile a un servidor propio que hable la misma API y el endpoint de aquí no cambia. Una organización sin credenciales utilizables recibe un rechazo que lo dice — nada se da por existente si un operador no lo ha configurado.
Detección de datos personales¶
POST /api/v1/ml/privacy/pii toma JSON en lugar de un archivo:
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]"
}
Se informa de cada categoría pedida, incluidas las que no encontraron nada —
"buscada y ausente" y "no buscada" son respuestas distintas. Las categorías son
email, iban, credit_card y us_ssn, y cada una se empareja por forma y
después se comprueba: Luhn para una tarjeta, ISO 7064 para un IBAN, de modo que
una tira de dígitos no se reporte como una cuenta.
Lo que vuelve son recuentos y el texto redactado, no los desplazamientos de cada coincidencia. Los detectores responden con texto reescrito, y recuperar las posiciones supondría copiar su tabla de patrones y sus sumas de comprobación — una copia que deja de coincidir en silencio con el original es peor que un contrato más estrecho.
Qué queda registrado¶
Cada llamada deja una fila: qué servicio, qué organización, quién pidió, cuántos
bytes entraron, cuánto salió, cuánto tardó y cómo terminó.
GET /api/v1/ml/calls las lee de vuelta, las más recientes primero, y
GET /api/v1/ml/calls/{id} lee una.
Una llamada rechazada también queda registrada, y su fila se confirma antes de que el rechazo desenrolle la petición: una fila meramente añadida a esa transacción sería deshecha por el propio rechazo que describe, y a un operador que pregunta por qué falla una integración se le diría que el tenant no hizo ninguna llamada.
No se guarda ningún contenido. El resultado vuelve en la respuesta y no se almacena, así que un documento parseado aquí no se convierte en un documento que este despliegue guarda, y el texto enviado para buscar datos personales no se retiene en una tabla que nadie pensó como almacén de documentos. El registro de una llamada hecha por otra organización no se encuentra, igual que cualquier otra fila de esta API.
El uso se cuenta en la unidad en la que trabaja el servicio — páginas en un parseo, caracteres en un escaneo — y no en dinero. Los servicios entregados corren o bien en las máquinas del propio operador, donde no hay precio de proveedor, o bien en la cuenta de proveedor propia de la organización, que le factura directamente. Una cifra que nadie puede conciliar con una factura es peor que un recuento honesto de unidades.
Límites¶
Una sola llamada acepta hasta ML_MAX_UPLOAD_SIZE_MB megabytes, 25 por defecto,
y solo esa cantidad de bytes se lee del cuerpo: una entrega demasiado grande se
rechaza sin haberse copiado antes a memoria. Un escaneo lee como mucho 200000
caracteres. Un llamante puede hacer RATE_LIMIT_ML_PER_MINUTE llamadas por
minuto, 30 por defecto, contadas por llamante y no por dirección.
Un límite de tasa cuenta arranques y no ve lo que sigue en marcha, que es la
forma equivocada para un trabajo medido en minutos. Por eso un worker parsea como
mucho ML_MAX_CONCURRENT_PARSES documentos a la vez, 4 por defecto, y una llamada
que llega con todas las plazas ocupadas se rechaza con un Retry-After en lugar
de encolarse: a quien se le dice que vuelva en un momento puede volver, y quien
queda aparcado detrás de cuatro escaneos ya se ha rendido en algún sitio que aquí
nadie ve.
La ejecución es síncrona: la respuesta es el resultado, y no hay cola que consultar. Eso es honesto sobre lo que hay aquí en lugar de aspiracional — un modo encolado añadiría sus propios estados al registro de llamada, que es la forma en la que llegaría.
Desplegarlos por separado¶
Los servicios escalan de forma distinta a la consola. Una pasada de OCR son
segundos de CPU en un hilo; servir un dashboard no es ni lo uno ni lo otro. Así
que deploy/profiles/ml-services/ ejecuta la imagen de la API una segunda vez
como una réplica que solo responde a estas rutas, con sus propios recursos y su
propio escalado, y el ingress de delante enruta /api/v1/ml/ hacia ella.
Es la misma imagen y la misma base de datos, que es lo que mantiene una sola implementación al servicio tanto de los agents como de quien llama directamente. Lo que está separado es el proceso, los límites y el reinicio — que es lo que pide "desplegado, actualizado y escalado de forma independiente".
deploy/profiles/ml-services/README.md describe el overlay y lo que espera.