← jason fetzerShift Investigatordocs
Back to the investigatorGolden evalsSynthetic data / read-only tools

How a question becomes evidence and a chart

This describes the implemented code as reviewed September 7, 2026. It replaces the original proposed first-slice design. See the agent contract for permissions, metrics for formulas, and the archived September 7 review for the defects it found, all since repaired.

Three execution paths

  1. Production day / Offline: the application parses a supported question pattern and runs a predetermined bounded tool plan. Since issue #20 every such view is a URL (URLS): the browser sends the question as GET /q?…&q=, the server redirects to the canonical address and answers it, and the address bar is kept equal to it.
  2. Production day / the analyst: Anthropic, the default, proposes one plan that application code checks step by step before any query runs (ANTHROPIC_DEMO); OpenAI, the optional provider, chooses one function at a time. Either way application code validates scope, executes queries, and formats the numerical answer.
  3. Week, month, accounting: the server calls PeriodReports directly. There is no model call, natural-language planning, or provider permission on this path.

Choosing a live provider does not add new calculation tools, dates, or production authority. It changes who chooses the next permitted tool and adds a bounded narrative after successful evidence retrieval.

Follow one request

In the UI, choose September 14, shift 2, Selected shift only, Production day, and Offline. Ask "Why was this shift slow? Chart downtime by reason."

In offline mode the browser sends GET /q?day=2026-09-14&shift=2&scope=shift&period=day&q=Why+was+this+shift+slow%3F… with Accept: application/json; the server parses the question with the offline router, answers 302 Location: /corr/1/day/2026-09-14/shift/2?measure=overview, and that address returns the answer (the browser then pushes it with history.pushState). No token and no session: the state is the address. A live round POSTs JSON to /api/ask with question, day, shift, scope, period, mode, corrupt, and optional session_id, with the workbench token in a header; on the loopback workbench a key you paste travels in X-Anthropic-Key or X-OpenAI-Key, kept out of the JSON and cleared from the field on submit; the public demo ignores both headers and uses its own key.

The server validates the request before selecting a dataset: the day must be one of the store's Monday–Friday production days (a weekend or out-of-range date is a 400 with the reason). RecordStore.day_analysis builds that day's private in-memory DuckDB tables from the record families. Up to three day/scenario Analysis objects are retained by the server.

Data flow: records → store → day tables → tools

  1. synth.generate_days() lays out the rolling year (Mon–Fri, from 365 days before as_of to as_of; each day from its own date-keyed draws, issue #35) as four record families — dry-end setups with customer blocks and reject lines, wet ends, shifts, stops (RECORDS) — byte-identical for the same seed, with a content hash.
  2. RecordStore (record_store.py) builds them once per process (about 12 s for the year: 6 s generating, 6 s loading and deriving shift_kpis; about 200 ms from the image's prebuilt folder) into one in-memory DuckDB with external access disabled: setups, customers, rejects, wet_ends, shifts, stops. The server and the period reports share the process-wide default_store(); the eval harness reads the golden window's store (evals/run.py golden_store()), which is that same store when the windows match.
  3. day_analysis(production_day, corrupt) validates each setup record of that day (finite non-negative quantities, web width and grade, gross = lineal × width / 12, waste within gross, interval and knife positions, reject lines reconciling) and loads the SAME per-day tables the daily tools always read — shifts, setups, stops, orders, rejects — through Analysis.from_rows. A shift-split setup (change_code S) lands in the shift its half falls in; a failed record is excluded with its raw values and makes its shift's coverage incomplete; corrupt=True injects the fixture's bad-trim record into the first setup of shift 2. The nine daily tools, the offline planner, the PDF and the period reports are unchanged above this line.
  4. The six period tools query the store directly with bound window dates (and an optional shift), so "this month versus last" is one fixed read over the whole dataset rather than a loop over days.

The offline plan for this question requests KPIs, downtime by reason, wet-end performance, notes, and the two corresponding charts. The result has numeric summaries, chart/table payloads, trace entries, coverage, context, usage, and a run ID.

The model path can choose a different sequence. That is a capability and an evaluation responsibility: the application currently validates legal tool use, not whether the sequence fully answers the user's question.

Actual tool contracts

All data tools take an integer shift in {1,2,3}; the current Analysis object owns the date. Do not put production_day in tool arguments. Extra arguments are rejected.

ToolAdditional argumentsResult
get_shift_kpisNoneOne row of calculated KPIs and coverage
get_shift_overviewNoneKPIs, largest losses and three chart evidence components; four fixed read queries
get_order_matrixNoneOrder fragments: dimensions, outs, cuts, demand, production, overrun and remaining quantities
get_downtime_breakdownkind: null/Maintenance/Operator; group_by: reason/place/kindGrouped stop minutes, shift percentages and source IDs
get_wet_end_performanceNoneWet-end ID, grade, target, actual elapsed-time speed and source IDs
get_quality_breakdownNoneDry-end reject area by reason
get_setup_matrixNoneSetup intervals, counts, allocated stop minutes and performance
get_shift_notesNoneShift note plus individual downtime observations
get_rankingsmetric: speed/downtime/qualityUp to ten rows; empty when coverage is incomplete
render_chartresult_id only; no shift argumentChart rows copied from successful chartable evidence in this run

The live provider schemas require all declared properties, including null kind when no filter applies. The application methods allow omitted optional arguments for offline/internal calls. Ranking metric and grouping values are validated in the data methods. The dispatcher enforces tool names, required/extra arguments, integer shifts, current scope and chart provenance.

Example model-selected tool request:

{"tool":"get_downtime_breakdown","arguments":{"shift":2,"kind":null,"group_by":"reason"}}

For the unfiltered request, the actual query shape is:

SELECT reason AS category,
       sum(seconds)/60 AS down_minutes,
       sum(seconds)/28800*100 AS shift_percent,
       list(id ORDER BY id) AS source_ids
FROM stops
WHERE shift = ?
GROUP BY reason
ORDER BY down_minutes DESC, category

The bound value is 2. Python chooses the grouping identifier from an allowlist; values remain parameters. A kind filter adds AND kind = ? with its own bound parameter. No visitor SQL is accepted.

Validation before analysis

The intake accepts only the project's version-1 synthetic XML and fixed-demo-local clock. UTF-8 XML is limited to 1 MB, depth 8 and 3,000 elements; DTDs and entity declarations are rejected. There must be three fixed eight-hour shifts.

Setup intervals partition each shift. Knife geometry, gross area, trim, shear, reject-sheet conversion and grade targets reconcile. Bad setup quantities are retained in excluded_records with original attributes, and the whole setup is omitted. Structural calendar errors, overlapping stops and cross-shift events reject the input rather than being silently resolved.

There is no web upload endpoint or live plant connector. The server generates its own dataset; the intake is not a general OEM importer.

Evidence and tracing

Each data result contains production_day, shift, dataset_version (SHA-256 of XML), rows, coverage, excluded_records, and SQL/parameters/timing. The agent wraps it in a run-specific result_id, tool name, arguments, success/error indicator, and elapsed time.

render_chart accepts only this investigation's successful downtime, wet-end, or quality result. Its rows are a deep copy of that result. Browser rendering uses text nodes and trusted SVG elements. A heatmap is separately constructed by PeriodReports from aggregated daily results.

The trace is inspectable execution evidence, not model chain-of-thought. It is not a durable audit log. Sessions and reports disappear when the process exits.

Context and follow-ups

The context holds the selected day, focus shift, scope, dataset hash, and up to four question/shifts/intent entries. It does not retain complete prior answers or evidence rows between investigations. The model receives that bounded history plus current-run compact tool results.

Visible date, shift, scope, mode, period and corrupt-record changes reset the UI conversation. A changed dataset hash clears agent history. An all-shift comparison is allowed only when the scope control permits it.

The agent now preserves the selected control shift after comparisons. A server round-trip regression verifies retained history under unchanged controls. All-shifts scope covers all three shifts by default; explicitly named shifts narrow a daily question.

The optional OpenAI provider: boundary and cost

The Anthropic analyst's boundary, guards and cost are in ANTHROPIC_DEMO; this section is the optional OpenAI provider's tool loop.

OpenAIPlanner constructs strict Responses function schemas and requires one function call per response. A finish call supplies successful evidence IDs; application code formats calculated summaries. A separate write_narrative function returns bounded prose and field references, validated by narrative.py. Numeric placeholders are filled from evidence, then shown as Model interpretation with citations. Prose is not certified by reference validation. A cannot_answer call now displays an application-owned unavailable message with supported next steps.

The key is request-scoped unless supplied through the server environment. Before each provider attempt the adapter reserves spending in a local SQLite budget. The request/output/call limits and caveats are documented in OPENAI_BYOK. Automated provider tests use scripted responses and make no real model calls.

The model's process instructions discourage unsupported claims and scope changes. Application scope/tool restrictions are enforceable; semantic quality and causal interpretation are not guaranteed by those restrictions. A request explicitly naming a chart/graph/plot gets partial status if no chart was produced. Overview evidence automatically supplies three charts per shift. Semantic fulfillment beyond that remains an evaluation concern (R4).

Period reports and heatmap

PeriodReports reads daily KPI and note snapshots, closes each Analysis database, then caches at most 60 snapshots for the clean/corrupt demo month. It constructs weighted totals, recurrence tables, shift ranks, daily rows, notes and charts. Missing dates never enter totals as zero. Comparison deltas and ranks are withheld when the necessary coverage is incomplete.

Heatmap rows contain the date, availability, output, percentages, counts, and heavy hitter. The browser normalizes available estimated-good output into alpha 0.22–1, sets a direct rgba background, and marks a cell deep (white day number) when the blended colour's relative luminance over the light surface falls below 0.179. Mouse, keyboard focus and click show the same detail text. It does not navigate to the clicked production date.

Delivery and limitations

The server retains the latest result for up to 32 live sessions. Save text contains narrative sections and limitations, not all evidence tables. The PDF of an addressed (offline) answer is the same address with format=pdf, rendered on request on any instance; a live answer's PDF uses the retained session/run reference. Neither makes a model call for export. The heatmap title fallback is repaired; daily and period PDF byte/access regression tests pass. Visual layout certification remains pending.

No email, schedule write, equipment action, web search or external market feed exists. Public deployment requires a separate implementation pass. The current local server, tokens, cache and rate limit are not a multi-user production service.

Rendered from docs/AGENT_EXECUTION.md in the repository.