Phase 9's operator path, built ahead of Phase 8 at the customer's direction and live at api.yokogawa.tech/documents. Upload, convert, review, approve, withdraw and restore. The pool screen is explicitly out of scope. Served by ai-api rather than ai-web, and mounted at /documents rather than /docs. ai.yokogawa.tech is SCADA-only since 2026-08-28 and passes through no Authelia, so it has no identity to record; publishers arrive on api.yokogawa.tech where the forward-auth headers still do. /docs stays with Swagger, which the customer is keeping - two things under one prefix with two different access policies is what gets misread during a later edit. Conversion is text extraction, not document parsing: pypdf, python-docx and openpyxl. Docling would be better at this and pulls torch, which lin001 has neither the memory to install nor the business running next to the demo plant's PLC. The cost is real - no layout, no table structure, and a scan cannot be read at all, so it is refused rather than stored empty. It is acceptable only because the converted text is shown to a person before the document can be cited, which is the same safety net the design already required for the header. convert.py is the one file to change if that stops being true. Chunking is mirrored from ingest.py rather than shared, because the two live in different images. They must stay identical: if they drift, the same document chunks differently depending on who loaded it, and the assistant answers or fails to answer depending on that. The step-sequence rule is locked by a test. Identity is self-asserted for the demo - the actor is typed on the form, which section 16 forbids, and the publisher list is one name with no password. Rows are written as `demo:<name>` with actor_groups = 'DEMO-UNVERIFIED' so that when real auth goes on, a name somebody typed stays tellable from a name Authelia proved. doc_actions cannot be deleted from, so an ambiguity there would be permanent. Two rules the code enforces rather than documents: uploading is open to anyone who reaches the page, because uploading changes nothing an operator can see - approving does, and that is what is gated; and an empty publisher list means nobody, not everybody. Verified on the host end to end: withdraw as a non-publisher 403s, with a short reason 400s, and as admin flips 5 chunks and writes a complete audit row; restore puts them back and keeps both rows. The corpus is unchanged afterwards. Requirements are split so the document dependencies install in their own layer - a change there costs four small wheels instead of re-resolving fastapi, langgraph and langfuse on a 2 vCPU shared host. The five divergences from section 16 are recorded in section 14. The one with teeth: files published through the UI stay in the inbox, so `ai-ingest --all` cannot see them and the two paths must not be used on the same document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 lines
1.1 KiB
Docker
28 lines
1.1 KiB
Docker
# ai-api — FastAPI. Built on the host from ~/ai/api.
|
|
FROM python:3.12-slim
|
|
|
|
# Non-root. The container has no reason to be root and one good reason not to
|
|
# be: it shares a host with live control.
|
|
RUN useradd --create-home --uid 10001 appuser
|
|
|
|
WORKDIR /app
|
|
|
|
# Requirements first so a code change does not re-resolve the dependency tree.
|
|
COPY requirements.txt .
|
|
RUN pip install --no-cache-dir --requirement requirements.txt
|
|
|
|
# The document-upload dependencies go in a SECOND layer, so changing them does
|
|
# not invalidate the one above. Four pure-Python wheels, a few megabytes, no
|
|
# compiler. See requirements-docs.txt for what belongs here and what does not.
|
|
COPY requirements-docs.txt .
|
|
RUN pip install --no-cache-dir --requirement requirements-docs.txt
|
|
|
|
COPY . .
|
|
|
|
USER appuser
|
|
EXPOSE 8000
|
|
|
|
# No published port in ai-compose.yml - Caddy reaches this on the proxy network.
|
|
# Single worker: the PoC is one operator at a time, and a second worker doubles
|
|
# the connection count against imh for no benefit.
|
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|