portfolio-optimiser/DEPLOY.md
Kjell Tore Guttormsen a3300ab0f6 feat(5): overleveringspakke for eksterne — git archive HEAD + DEPLOY.md
Én zip en mottakende organisasjon deployer uten å klone repoet eller ha konto her.
Arkivet er git archive HEAD (tracked files only), som er SAMME tre den målte
docker-build-konteksten bruker — og grunnen til at STATE.md/*.local.md/.env ikke kan
komme inn: de er gitignorert, ikke filtrert bort av et filter vi må vedlikeholde.

DEPLOY.md svarer mottakerens tre første spørsmål: hvem gjør hva (plattform-operatør,
bestiller, fagperson), prosessen ende-til-ende, og hvorfor det ikke finnes et
chat-grensesnitt. Den navngir også deploy-kravet 4e målte men aldri skrev ned:
pakket model_map.json bærer REPLACE-WITH-*, så uten PORTFOLIO_MODEL_MAP starter
containeren, svarer på /readiness og feiler hver invocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeW1LhH5TtXxKZPe9JkqL1
2026-08-14 10:43:28 +02:00

9 KiB
Raw Blame History

Deploying and using this package

This archive is a complete, self-contained copy of portfolio-optimiser. You do not need to clone anything or have an account anywhere to deploy it. Unzip it and follow this document.

It is written in English because its readers are implementing developers and platform operators. The two documents that describe the human side of the loop are written in Norwegian and ship in this same archive: docs/bestille-en-kjoring.md (ordering a run) and docs/ekspert-svar.md (giving the verdict).


1. What this is — and what it is not

It finds cost savings inside a single project: agents propose candidate measures, a deterministic validator decides whether the numbers hold, a second agent (the "checker") gates the reasoning, and a domain expert gives the final verdict. The system learns from those verdicts.

There is no chat interface, and that is a design decision rather than a missing feature.

The hosted surface is a task API — POST /invocations with a JSON body, GET /readiness — not a conversation. The system is deliberately not exposed as a Microsoft Foundry agent via as_agent(), because the validator, the cost-baseline anchoring, the checker gate and the savings ledger all live outside the agent graph. Wrapping it as a chat agent would route requests around precisely the components that make an answer trustworthy, and you would get a fluent assistant instead of a validated result.

The dialogue with humans happens in two places instead, both file-shaped and both auditable:

  • before a run, through a mandate file (what should be considered), and
  • after a run, through a verdict file (what a qualified person decided).

If you want an interactive front end, build it on top of POST /invocations — but keep the verdict step human. An approval loop that a model can close by itself is not an approval loop.


2. Who does what

The package has three distinct audiences. Only the first one is technical.

Role What they do Where it is documented
Platform operator Deploys the container into Microsoft Foundry, sets two environment variables, verifies with the offline preflight. One-time work. This document, sections 35.
Ordering domain expert (bestiller) Writes the mandate file: the objective, which approaches are worth trying, and why. The why is the part the system cannot read out of cost figures. docs/bestille-en-kjoring.md
Reviewing domain expert (fagperson) Receives a validated proposal and answers approved or rejected with a written rationale. That verdict is what the system learns from. docs/ekspert-svar.md

The ordering expert steers what is considered, never what is approved. If you ask for something the numbers do not support, the deterministic validator rejects it and the rejection comes back with its reason. That property is the point of the whole design; do not build around it.


3. The process, end to end

  1. Prepare a knowledge base for the project — cost documents, technical sources, prior experience — as a document bundle. A working five-document example ships in shared/examples/bygg-energi-mikro, and docs/knowledge-base-recipe.md describes how to build your own.
  2. Order a run with a mandate file (optional but recommended). See docs/bestille-en-kjoring.md.
  3. Run it — from the CLI, or by POST /invocations against the deployed container (section 5). The run navigates the knowledge base, debates candidate measures, validates the numbers against the project's actual cost baseline, and returns either a validated proposal or a rejection with a reason. A rejection is a successful run: the negative outcome belongs to the answer, not to the transport, and the API returns HTTP 200 for it.
  4. A domain expert judges the outcome and writes a verdict — either inline during the run, or as a JSON file dropped into an inbox directory afterwards, possibly days later. See docs/ekspert-svar.md.
  5. Approved verdicts can be promoted into the knowledge base, so the next run reads them as context and does not re-propose something the organisation has already rejected. Promotion is gated: only an approved verdict can be promoted, and raw agent output never enters the knowledge base on its own.

Steps 4 and 5 are what make this a learning loop rather than a report generator.


4. Prerequisites in your own tenant

You need, in Microsoft Foundry:

  1. A Foundry resource with project management enabled and a project inside it. A Foundry project is required — an Azure OpenAI resource (kind: OpenAI) is a different client surface and will not work.
  2. At least one model deployment (any current chat model; a small one is enough).
  3. The Foundry User role (role definition id 53ca6127-db72-4b80-b1b0-d745d6d5456d) for the identity that will run it, assigned at project scope. Azure Owner and Contributor grant management permissions only, not the data-plane permissions this needs.

5. Configuration — the two variables that matter

5.1 The project endpoint. Resolved as the first non-empty of:

PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT     # yours; wins if set
FOUNDRY_PROJECT_ENDPOINT               # injected by the Foundry hosting platform

Inside a hosted Foundry container the platform injects the second one and you need to set nothing. Outside it — a laptop, a pipeline, a container you run yourself — set the first. The value looks like https://<resource>.services.ai.azure.com/api/projects/<project>.

5.2 The model map — this is the one that catches people. The packaged src/portfolio_optimiser/data/model_map.json ships placeholders:

"azure": { "default": "REPLACE-WITH-FOUNDRY-DEPLOYMENT", ... }

Deployment names are tenant-specific, so they are deliberately never committed. The code fail-fasts on a REPLACE-WITH- value rather than guessing. Point PORTFOLIO_MODEL_MAP at your own file:

{
  "local":  { "default": "qwen3:4b",     "proposer": "qwen3:4b",     "checker": "qwen3:4b" },
  "azure":  { "default": "my-deployment", "proposer": "my-deployment", "checker": "my-deployment" }
}
export PORTFOLIO_MODEL_MAP=/path/to/model_map.json

If you skip this, the container starts and answers /readiness — and fails every invocation. That is the single most likely first-deployment failure, which is why it has its own section.

5.3 Verify before you pay for a model call:

uv run python -m portfolio_optimiser.preflight --profile azure

Exit code 0 means every offline-checkable precondition holds: endpoint shape, model-map structure, no placeholders left. It is necessary but not sufficient — RBAC (403), token or tenant problems (401) and a well-formed but non-existent deployment (404) can only surface at the live call.


6. Deploying

The image must be built for x86_64 regardless of your workstation's architecture:

git archive HEAD | docker build --platform linux/amd64 -t portfolio-optimiser -

If you unzipped this package rather than cloning it, build from the directory instead:

docker build --platform linux/amd64 -t portfolio-optimiser .

azure.yaml describes the hosted-agent deployment (azd). The container serves port 8088 (override with PORT), answers GET /readiness, accepts POST /invocations, and shuts down cleanly on SIGTERM.

The invocations payload is whitelisted against the run signature — unknown fields are refused by name with HTTP 400, never silently ignored:

Field Required Notes
project_id yes The project to analyse.
docs_dir yes Source documents.
verdict_input yes The expert verdict for this run.
bundle_dir no Knowledge base to navigate.
profile no Defaults to azure in the hosted container.
max_rounds no Debate round cap.
max_tokens no Per-invocation token ceiling. Set it. The built-in default is 100 000 tokens per run, which is a ceiling nobody chose for your workload.
top_k no Retrieval breadth.

HTTP 400 means the request was malformed; 500 means the run itself failed, with error_type and error in the body. A rejected proposal is 200 — see section 3.


7. Honest limits

  • This framework has never been run against a live model in our environment. Everything above is measured against tests, an offline simulation and container builds. The first live run in your tenant is the first live run, full stop.
  • The system is a technical framework. Data protection assessments, lawful basis and risk ownership belong to whoever deploys it — see README.md and SECURITY.md.
  • Readiness does not respond during CPU-bound stretches of a run (the optimisation solve).
  • Chunked request bodies are not supported.
  • Verdict promotion writes to the knowledge base non-atomically; it assumes a single writing process.