yau-plant-assistant/.env.example
Claude dcfb411c3b Store the document title and authorising role at ingest
ProcedureIdentity requires a title and an authorising role, and neither was
stored anywhere. The answer writer was asked for both, read them off whatever
chunk retrieval happened to return, and returned "" whenever the header chunk
was not among them.

They belong in the row for the same reason doc_number and revision do: they
are facts about the controlled document, established once when a human
confirms the header, not something to re-derive per question from whatever
text was retrieved. Denormalised onto every chunk exactly as the existing
header fields are - ingest replaces every chunk of a source_file in one
transaction, so they cannot drift within a document.

complete() deliberately still requires only doc_number, revision and
effective_date. A missing title makes an answer less useful; a wrong revision
sends somebody to the wrong document. --assume-yes must keep refusing on the
second and tolerate the first.

controlled_copy_location is NOT in the schema. It is a site fact, identical on
every row, and the one field where an invented value sends a person to a place
that does not exist. It is CONTROLLED_COPY_LOCATION in api.env, defaulting to
a string that names who to ask.

The authorising-role pattern requires the colon: without it the lazy gap
swallowed the field name and captured "role: Station Maintenance Supervisor"
as the value, which the first run caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:54:01 +10:00

139 lines
6.4 KiB
Text

# =============================================================================
# .env.example — every key, no values. Committed deliberately.
#
# On lin001 these live as TWO 0600 files under ~/ai/, never in Git:
# ~/ai/pg-ai.env the POSTGRES_* / AGENT_DB_* block
# ~/ai/api.env everything else
# Follow the ~/authelia/authelia.env precedent: chmod 0600, owned by azureuser.
# =============================================================================
# --- pg-ai (~/ai/pg-ai.env) --------------------------------------------------
POSTGRES_PASSWORD=
AGENT_DB_USER=agent_ro
AGENT_DB_PASSWORD=
# --- imh (PENDING — leave blank until Phase 4) -------------------------------
IMH_HOST=yau-sls-poc-imh
IMH_PORT=1433
IMH_DB=
IMH_USER=svc_agent_ro
IMH_PASSWORD=
USE_FIXTURES=true # flip to false when imh is live
# --- local Postgres ----------------------------------------------------------
PGHOST=pg-ai
PGPORT=5432
PGDATABASE=plant
PGUSER=agent_ro
PGPASSWORD=
# --- Azure OpenAI ------------------------------------------------------------
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_API_VERSION=
CHAT_DEPLOYMENT= # flagship — final prose only
CHEAP_DEPLOYMENT= # nano/mini — classifier, entities, tool selection
EMBED_DEPLOYMENT= # text-embedding-3-small
# --- no-LLM stub mode --------------------------------------------------------
# OFF for anything real. With it on, no model is called: the class comes from
# the caller instead of the classifier and the prose is a fixed placeholder.
# Every answer carries stub_mode: true and a banner. See api/stub.py for what
# it proves and what it does not.
NO_LLM_STUB=false
# --- behaviour ---------------------------------------------------------------
CLASSIFIER_CONFIDENCE_THRESHOLD=0.7
# Where the CONTROLLED copy of a procedure actually lives - a site fact, the
# same for every document, so it is not in doc_chunks. Set this to the real
# DMS location. The default names who to ask, which is always true and never
# sends anybody to a place that does not exist.
CONTROLLED_COPY_LOCATION=
SITE_TIMEZONE=Australia/Sydney # storage UTC; convert once, in Cube
MAX_ROWS_RETURNED=5000
QUERY_TIMEOUT_SECONDS=30
MAX_OUTPUT_TOKENS=1200
# --- Cube --------------------------------------------------------------------
CUBEJS_API_SECRET=
CUBEJS_API_URL=http://cube:4000/cubejs-api/v1
# The upstream source Cube reads. ai-compose.yml says these come from this
# file, and this file did not list them - so the first person to deploy Cube
# had to work out from the compose comments and db/003_roles.sql what the
# service actually needed. While USE_FIXTURES=true the fixtures live in pg-ai,
# so this points at pg-ai with the cube_rw credentials from pg-ai.env.
# At Phase 4 the whole block becomes CUBEJS_DB_TYPE=mssql against imh.
CUBEJS_DB_TYPE=postgres
CUBEJS_DB_HOST=pg-ai
CUBEJS_DB_PORT=5432
CUBEJS_DB_NAME=plant
CUBEJS_DB_USER=cube_rw # CUBE_DB_USER in pg-ai.env
CUBEJS_DB_PASS= # CUBE_DB_PASSWORD in pg-ai.env
# NOT CUBEJS_EXT_DB_*. Cube v1 will not use Postgres as an external
# pre-aggregation store; cubestore does that job and needs no keys here.
# See the note above the cube service in compose/ai-compose.yml.
# --- Langfuse ----------------------------------------------------------------
# PROJECT keys, created in the Langfuse UI - not the server's own secrets
# (SALT, NEXTAUTH_SECRET), which live in langfuse.env and are a different
# thing. The two must be a MATCHED PAIR from the same key: a public key from
# one pair with a secret from another authenticates as neither.
#
# Langfuse shows the secret ONCE, at creation, and stores only a hash - the
# database keeps a masked form (sk-lf-...abcd) and nothing can recover it. If
# it is lost, generate a new pair; there is no way to read the old one back.
#
# Getting this wrong is silent in both directions. Keys absent -> _langfuse()
# returns None and every question is simply untraced. Keys present but wrong ->
# a client is built, Langfuse rejects it, and main.py swallows the exception on
# purpose, because observability must never break the answer path. Neither case
# logs anything. Confirm traces are ARRIVING; do not infer it from config.
LANGFUSE_HOST=http://langfuse:3000
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_SALT=
LANGFUSE_NEXTAUTH_SECRET=
LANGFUSE_DB_PASSWORD=
# --- ingest ------------------------------------------------------------------
AI_DOCS_ROOT=/datadisk/ai-docs
CHUNK_TOKEN_TARGET=800
# --- document upload (Phase 9) -----------------------------------------------
# Two more roles, because the component reachable from the internet must not be
# the component that can write doc_chunks. See db/004_doc_uploads.sql.
UPLOADS_DB_USER=uploads_rw # ai-api: the queue only, never doc_chunks
UPLOADS_DB_PASSWORD=
INGEST_DB_USER=ingest_rw # ai-docs-worker AND the ai-ingest CLI:
# doc_chunks + the queue. PGUSER is agent_ro
# and cannot INSERT - see BUILD-AI-CONTAINERS §16.1.
INGEST_DB_PASSWORD=
AI_DOCS_INBOX=/datadisk/ai-docs-inbox # writable staging; NOT the ingest root
AI_DOCS_WITHDRAWN=/datadisk/ai-docs-withdrawn # withdrawn files are moved, not deleted
MAX_UPLOAD_MB=50
UPLOAD_DISK_LIMIT_PCT=90 # refuse uploads above this on /datadisk
ALLOWED_UPLOAD_EXTENSIONS=.pdf,.docx,.md,.txt
# DIRECT membership only — Authelia does not resolve nested groups.
DOC_PUBLISHER_GROUP=AI_DocPublishers
# Who may curate the retrieval pool and run trimmed-pool demos. Defaults to the
# publisher group; point it at a narrower AD group if that should be separate.
DOC_ADMIN_GROUP=AI_DocPublishers
ALLOW_SELF_APPROVAL=false # uploader approving their own document
# Withdrawal (superseded = TRUE) is always available to the publisher group and
# is reversible. PURGE deletes chunks and is not. Leave it off unless there is a
# document that must not be in the database at all.
ALLOW_PURGE=false
# Retrieval pool. pool_enabled is orthogonal to superseded - see db/006_doc_pool.sql.
# Below this share of documents enabled, retrieval drops to an exact scan: the
# HNSW index is built over ALL embeddings and filters afterwards, so a heavily
# trimmed pool can return almost nothing. This bites in exactly the demo that
# trims the pool. Rehearse it.
POOL_EXACT_SCAN_BELOW_PCT=50
WORKER_POLL_SECONDS=10
WORKER_LEASE_MINUTES=30 # an `ingesting` row older than this is a dead worker