11 KiB
Roll Calculator Architecture
Authority Boundary
Natural-language clients may describe or modify a calculation request, but engineering results are generated exclusively by the deterministic Roll Calculator core.
External callers can provide decision inputs only. CalculationRequest.from_dict() rejects diameter results, roll-weight results, warnings, and complete calculation objects. Missing decision-relevant inputs produce clarification instead of defaults, and article/core conflicts are returned explicitly.
Direct Roll-Diameter Flow
Browser or future tool client
|
| CalculationRequest (inputs only)
v
ArticleRepository.resolve()
|
v
roll_calculation.calculate_roll()
|
| CalculationResult
+----------------------> browser rendering
|
+--> report_from_calculation_result()
|
v
render_roll_report()
roll_calculation.py is authoritative for the direct roll-diameter range, effective roll length, optional roll weight, known-core resolution, and direct-flow forklift warnings. Canonical core presets live in core_presets.py; both the Jinja-rendered browser buttons and roll_calculation.py consume that same ordered tuple. The active browser direct-diameter mode calls /api/calculations/roll; it has no independent implementation of that diameter formula.
The PDF endpoint accepts the same calculation request, recalculates it through the core, then adapts the authoritative result to the established one-page PDF layout. It does not accept calculated values from the browser.
generate_calculation_pdf is the equivalent opt-in MCP adapter. It accepts only
original roll-calculation inputs and uses the shared report service after
deterministic recalculation. Under FastMCP Streamable HTTP, the MCP host owns a
short-lived local store and its /reports/<256-bit-capability>.pdf download
route. Flask has no role in this path, which avoids cross-host filesystem
coupling and binary PDF content in tool responses. The browser-reachable origin
is configured independently with ROLLCALC_MCP_ARTIFACT_PUBLIC_BASE_URL.
Installed MCPO 0.0.20 does not retain native MCP resources/resource links, so
the tool returns an ordinary browser URL rather than an MCP resource.
Production-Machine Feasibility
config/machines.yaml is the runtime source for configured production-machine
limits; data/source/LineLimitations_RollCalc.csv is retained as the reference
source document. machine_constraints.py loads and validates the YAML through
MachineRepository, resolves only stable IDs/names/configured aliases, and
evaluates independent constraints through check_production_feasibility().
V1 compares material-only roll weight, nominal/average diameter, calculated maximum diameter, core diameter, and product width. All configured V1 inputs are required before a feasible result may be claimed. Nominal diameter is used for a configured minimum; an average diameter over the maximum fails, whereas an average fit whose calculated maximum exceeds the maximum is a retained warning. Line speed remains configured but is outside the initial roll-only scope. Flask and MCP are thin adapters; no machine constraints are duplicated in those layers or in browser code.
calculate_machine_max_product_length() composes the same domains for target
length questions. It resolves the machine only through MachineRepository,
uses calculate_product_length() with the configured maximum diameter, and
maps its average length to the nominal maximum and its minimum length to the
conservative/no-warning maximum. Material-weight maxima use the shared
calculate_material_length_for_weight() inverse; final material weights use
calculate_material_weight(). The domain chooses governing constraints,
including deterministic ties, and verifies selected lengths through the
existing direct roll and feasibility services. It never resolves articles,
reads YAML in adapters, duplicates roll mathematics, applies a manufacturing
increment, or implements conditional production rules. Flask and MCP expose
thin adapters; the browser UI remains unchanged.
Constrained Natural-Language Flow
German/English utterance
|
v
OllamaNLUClient (qwen3.5:35B-A3B by default)
|
| strict input-only NLU JSON
v
application schema validation
|
+--> new_calculation --> calculate_roll()
|
+--> modify_calculation --> modify_calculation()
|
+--> unsupported
|
v
deterministic response/clarification template
|
+--> input-only temporary report reference
|
v
calculate_roll() --> existing PDF renderer
The local LLM is an NLU adapter only. Engineering calculations remain deterministic. For continuity, Ollama receives a minimal summary of the last successful input state (article reference, length, width, core, and weight-request flag), but no formula, product/master-data dataset, warning rule, calculated result, or PDF payload. Its output is constrained to new_calculation, a multi-field modify_calculation, or unsupported, then independently validated by ollama_nlu.validate_nlu_payload().
ConversationService owns structured state and does not replay a transcript to reconstruct technical parameters. It records the current CalculationState, last deterministic result, and a typed pending clarification. Ambiguous core-family context contains the canonical family and candidate CorePreset objects, from which allowed diameters and deterministic messages are derived. Failed candidate validation leaves the prior valid state and pending context unchanged. All clarification and success prose is produced by fixed application templates.
If a successful state exists and the model returns an identifier-free new_calculation containing only explicit modification-like values, ConversationService applies those non-null values as a narrow patch to the current state. Null/omitted values never erase inherited inputs, and include_roll_weight is added only when true. A different article identifier or an explicit new-calculation phrase bypasses inheritance. Failed NLU or deterministic validation preserves the previous valid state.
Successful responses create a random report reference that stores the validated input request only. Downloading the report performs a fresh authoritative calculation and uses the same report_from_calculation_result() / render_roll_report() path as the browser PDF endpoint.
The current InMemoryConversationStore is an MVP boundary. It is process-local, is cleared on restart, is not shared across WSGI workers, has no retention policy, and should be replaced before a multi-process OpenWebUI deployment.
Models and Statuses
CalculationRequest contains explicit request fields such as article identifier/name hint, roll length, width, thickness/stddev, area weight, core diameter/type, category/site, and whether optional weight is requested.
CalculationState wraps that request for conversational workflows and is included in calculation responses. modify_calculation() applies an explicit field patch, validates the resulting request, and recalculates exactly once. A failed or ambiguous patch does not replace the prior successful state; pending clarification stores the complete requested patch structurally and combines it with the selected clarification before that one recalculation. Coupled core changes clear the old counterpart so a phrase such as steel cannot silently retain an unrelated diameter.
Successful results include:
- resolved article;
- effective inputs with
user,article_master_data, or other explicit provenance; - effective roll length;
- minimum, average, and maximum diameter;
- optional roll weight;
- warnings and notes;
- calculator/build provenance.
Non-success statuses are structured as article_not_found, article_conflict, needs_clarification, or invalid_parameter. Missing and ambiguous fields are separate arrays.
Article and Core Resolution
Article numbers remain strings and exact article-number matching takes precedence. A supplied name hint is checked conservatively. Name-only resolution first uses normalized full/base-name matching (case, whitespace, punctuation, R501/R 501/R-501, trailing dimensions, and parenthetical descriptors). That existing normalization contains the only supported bidirectional product-family aliases: Stex/Secutex, Bfix/Bentofix, and Sgrid/Secugrid; aliases are token-bounded and map to the abbreviated canonical token before matching. The optional fuzzy stage ranks repository candidates only; it resolves only a unique, high-confidence candidate with a safe margin and otherwise returns a clarification. Duplicate master-data records for the same exact article number also require clarification instead of being selected by file order. Width extraction is limited to one recognized, unambiguous dimension in the master-data name and records master-data provenance.
Known cores are defined once in core_presets.py: 133 mm steel, 150 mm PVC, 168 mm PVC, and 194 mm steel. app.py passes this ordered tuple to the active Jinja template, and the headless resolver imports the same tuple. Complete labels and a finite explicit alias set resolve deterministically to a canonical label and diameter. Generic steel/Stahlkern and PVC/plastic descriptions remain ambiguous because each existing family has two presets. Custom positive diameters remain permitted because the existing UI permits them.
Scope Boundary
The conversational adapter currently covers the normal direct roll-diameter operation and its optional weight/warnings/PDF flow. The product-length, target-diameter, extrapolation, and load-optimizer modes remain client-side and unsupported by NLU. No RAG, embedding, vector database, agent framework, autonomous loop, or arbitrary tool execution is present.
OpenWebUI Presentation Boundary
OpenWebUI chat (RollCalc Pipe)
|
| user message + application-owned chat/session key
v
OpenWebUI Pipe in-memory mapping
|
| existing RollCalc conversation_id
v
POST /api/conversations/<id>/messages
|
+--> deterministic user-facing message
|
+--> validated relative report path
|
v
browser-public RollCalc URL + existing authenticated PDF endpoint
integrations/openwebui/rollcalc_pipe.py is intentionally a Pipe instead of an
LLM Tool. The Pipe is itself the selectable OpenWebUI model and performs one
direct asynchronous API call, so OpenWebUI cannot decide whether to invoke the
calculator or generate an alternative engineering answer. It contains no
calculation, resolver, clarification, core-preset, warning, or report-rendering
logic.
The process-local mapping key combines the OpenWebUI user ID and chat ID. A per-chat lock serializes clarification/follow-up messages while separate chats remain independent. Missing stable identifiers are rejected rather than mapped to a global session. Unknown RollCalc conversations are discarded without replaying the current follow-up against empty state.
Internal API and external report routing are configured separately. API Basic Auth is server-side; report Markdown links contain no credentials and retain the existing browser-facing Basic-Auth challenge. The MVP does not provide shared SSO, persistent/cross-worker mappings, report tokens, or streaming.