yau-plant-assistant/.env.example
Claude 45034eb4d1 Correct two counts that disagreed with the files they describe
The exam: eval/testset.jsonl holds 78 cases (A10 H28 L8 N5 P10 R10 T5 U2).
workflow-map.html called it a 75-question exam in the two places it states
the current total. Corrected. The two sentences describing how the exam
"grew from 67 questions to 75" are left alone - that step is historically
correct: 67 plus the eight live-model failures is 75, and the Phase 5
findings H26, H27 and H31 took it to 78 afterwards.

The env files: .env.example said TWO 0600 files under ~/ai/ and listed
pg-ai.env and api.env, but its own line 105 refers to langfuse.env, and
README.md, scripts/deploy.sh and compose/langfuse-compose.yml all use
three. The header was simply wrong; langfuse.env is now named in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 16:22:23 +10:00

165 lines
7.7 KiB
Text

# =============================================================================
# .env.example — every key, no values. Committed deliberately.
#
# On lin001 these live as THREE 0600 files under ~/ai/, never in Git:
# ~/ai/pg-ai.env the POSTGRES_* / AGENT_DB_* block
# ~/ai/langfuse.env the Langfuse SALT / NEXTAUTH_SECRET block (see below)
# ~/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
# --- Documents / upload UI (Phase 9) -----------------------------------------
# Two roles, and the split IS the safety boundary. db/005 carries a trigger that
# stops uploads_rw setting superseded = FALSE, so the web path can make a
# document less visible and never more. ingest_rw is the only one that writes
# chunks or restores a document.
UPLOADS_DB_USER=uploads_rw
UPLOADS_DB_PASSWORD=
INGEST_DB_USER=ingest_rw
INGEST_DB_PASSWORD=
DOCS_INBOX=/inbox
MAX_UPLOAD_MB=25
# authelia | demo
# authelia the actor is Remote-User from the forward-auth headers. The design.
# demo the actor is TYPED ON THE FORM. Self-asserted and unverified -
# exactly what the design forbids. Rows are written as `demo:<name>`
# with actor_groups = 'DEMO-UNVERIFIED' so they can never be mistaken
# for authenticated ones, and every screen says so.
DOC_IDENTITY_MODE=authelia
# Who may approve, withdraw or restore. Comma-separated. EMPTY MEANS NOBODY and
# every mutating endpoint 403s - that is the intended failure direction. This
# stands in for the AD group AI_DocPublishers; swapping to the group later is a
# config change, not a code change.
DOC_PUBLISHERS=
# --- 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