feat: add conversational RollCalc assistant
This commit is contained in:
+78
-5
@@ -4,7 +4,7 @@ Project-specific technical and domain knowledge for RollCalcPython.
|
||||
|
||||
## Purpose
|
||||
|
||||
RollCalcPython is an internal roll calculator for Naue roll products. The backend is a minimal Flask application that provides authentication, routing, static file delivery, and access logs. Most domain behavior runs in the browser inside `templates/roll_calculator.html`.
|
||||
RollCalcPython is an internal roll calculator for Naue roll products. The backend provides authentication, routing, static file delivery, access logs, and the authoritative direct roll-diameter calculation service. Other modes and the load optimizer still contain client-side behavior in `templates/roll_calculator.html`.
|
||||
|
||||
The tool is not intended as an unchecked source of truth. Inputs and outputs must be checked for plausibility before operational use.
|
||||
|
||||
@@ -19,6 +19,12 @@ Backend:
|
||||
- `/static/<path:filename>` is intended to serve static files behind Basic Auth.
|
||||
- `/api/health` returns health/version information.
|
||||
- `/api/user` returns the authenticated user.
|
||||
- `/api/calculations/roll` executes a `CalculationRequest` through the deterministic core.
|
||||
- `/api/calculations/roll/modify` applies controlled changes to `CalculationState` and recalculates.
|
||||
- `/api/reports/roll-calculation.pdf` recalculates a request and renders its authoritative result.
|
||||
- `/api/conversations` creates a process-local conversational session.
|
||||
- `/api/conversations/<id>/messages` runs constrained NLU followed by the existing deterministic calculation functions.
|
||||
- `/api/conversations/reports/<id>.pdf` recalculates a stored input-only request through the existing PDF path.
|
||||
- Access events are appended to `access_log.json`.
|
||||
- `build_info.json` is loaded at startup and exposed to all templates as `build_info`.
|
||||
|
||||
@@ -30,6 +36,47 @@ Frontend:
|
||||
- `window.APP_CONFIG` is currently defined inline for forklift rules.
|
||||
- A disclaimer modal blocks use until the user checks the confirmation checkbox.
|
||||
- The footer displays build metadata: version, branch, commit, and timestamp.
|
||||
- A valid direct diameter request/result is retained as `window.CURRENT_ROLL_REQUEST` / `window.CURRENT_ROLL_RESULT` until an input changes.
|
||||
|
||||
Domain service:
|
||||
|
||||
- `roll_calculation.py` owns `CalculationRequest`, `CalculationState`, `ArticleRepository`, `calculate_roll()`, and `modify_calculation()`.
|
||||
- Direct diameter ranges, optional weight, and direct-flow forklift warnings originate there.
|
||||
- Request parsing rejects calculated diameter, weight, warning, and calculation fields.
|
||||
- Natural-language clients may describe or modify a calculation request, but engineering results are generated exclusively by the deterministic Roll Calculator core.
|
||||
|
||||
Conversational adapter:
|
||||
|
||||
- `ollama_nlu.py` calls local Ollama directly through `/api/chat`, using `qwen3.5:35B-A3B`, temperature `0`, `think=false`, non-streaming output, and a strict JSON schema by default. `ROLLCALC_OLLAMA_MODEL` overrides the model name.
|
||||
- The NLU schema allows only `new_calculation`, `modify_calculation`, and `unsupported`. Modifications contain one or more explicitly allowed request-field changes, validated and applied atomically.
|
||||
- The model receives no formulas, product dataset, engineering results, warnings, or PDF content. After success it receives only a minimal current-input summary for referential follow-ups.
|
||||
- `conversation_service.py` owns `CalculationState`, the last deterministic result, typed pending clarification context, and temporary input-only report references. Ambiguous core context retains the canonical family and matching `CorePreset` candidates; invalid replies do not replace the previous valid state.
|
||||
- An identifier-free incomplete `new_calculation` from the model is converted to a follow-up only when a successful current state exists and at least one explicit modification-like value is present. Null fields inherit current inputs; a different article or explicit new-calculation phrase starts fresh.
|
||||
- User-facing answers and clarification messages are deterministic templates. Numeric output is formatted only from `CalculationResult`.
|
||||
- The adapter does not apply the browser's visible 150 mm core default. Current master data has no usable core value, so absent core information requires clarification.
|
||||
- Ollama/schema/timeout failures do not mutate calculation state.
|
||||
- The store is in-memory, process-local, non-persistent, and intended only for the initial demo.
|
||||
|
||||
OpenWebUI adapter:
|
||||
|
||||
- `integrations/openwebui/rollcalc_pipe.py` is a presentation/integration Pipe,
|
||||
not another NLU or calculation service. It calls only the existing
|
||||
conversation endpoints.
|
||||
- One process-local mapping from `(OpenWebUI user ID, chat ID)` to a RollCalc
|
||||
`conversation_id` preserves follow-up state without reconstructing it from the
|
||||
OpenWebUI transcript. Per-chat asynchronous locks preserve message order.
|
||||
- The Pipe renders only the deterministic API `message`. It validates the
|
||||
report path and creates Markdown links from `ROLLCALC_PUBLIC_BASE_URL`; it
|
||||
never exposes raw interpretation, result, state, or diagnostic objects.
|
||||
- `ROLLCALC_API_BASE_URL` is the server/container route, whereas
|
||||
`ROLLCALC_PUBLIC_BASE_URL` is the browser route. They are deliberately
|
||||
separate because container-local hostnames must not leak into PDF links.
|
||||
- Server-side API calls use configured Basic Auth. Credentials are never placed
|
||||
in PDF URLs, so users authenticate separately to the existing RollCalc PDF
|
||||
endpoint in their browser.
|
||||
- The Pipe's chat mapping is lost on OpenWebUI Function reload/restart. It is
|
||||
not shared by multiple OpenWebUI processes. Deployment and demo details live
|
||||
in `integrations/openwebui/README.md`.
|
||||
|
||||
## Build Info
|
||||
|
||||
@@ -79,7 +126,7 @@ Assumptions:
|
||||
- No air gaps.
|
||||
- Roll geometry is derived mathematically, not physically measured in the app.
|
||||
|
||||
Nominal diameter formula used by the Direct Calculation tab:
|
||||
Nominal diameter formula used by `roll_calculation.calculate_roll()` and returned to the Direct Calculation tab:
|
||||
|
||||
```text
|
||||
D = sqrt(d^2 + (4 * L * 1000 * t) / pi)
|
||||
@@ -117,7 +164,7 @@ Variables:
|
||||
|
||||
## Roll Weight
|
||||
|
||||
Roll weight is calculated client-side when width, area weight, and length are available:
|
||||
For the authoritative direct roll-diameter flow, roll weight is calculated server-side when requested and width, area weight, and length are available:
|
||||
|
||||
```text
|
||||
weight_kg = area_weight_g_m2 * length_m * width_m / 1000
|
||||
@@ -125,6 +172,30 @@ weight_kg = area_weight_g_m2 * length_m * width_m / 1000
|
||||
|
||||
Material thickness and area weight values are loaded from article data and are generated automatically. They are not guaranteed to be complete or reviewed.
|
||||
|
||||
## PDF Reports
|
||||
|
||||
The browser and PDF endpoints both submit a `CalculationRequest`. The PDF endpoint recalculates through `calculate_roll()` and passes its `CalculationResult` through `report_from_calculation_result()`; it never accepts client-supplied result fields. Minimum, average, maximum, effective roll length, optional weight, warnings, notes, effective inputs, and provenance therefore come from the same core.
|
||||
|
||||
`POST /api/reports/roll-calculation.pdf` is protected by the existing HTTP Basic Auth. Incomplete, conflicting, or invalid calculation requests return HTTP 400 rather than a document. Generated filenames include date and time without colon characters.
|
||||
|
||||
`pdf_report.py` uses no third-party package. It creates one A4 page with a compact header, result cards, two-column input area, warnings/notes, and footer. Repeated diagonal `INTERNAL USE ONLY` text is drawn into the actual page content stream with reduced opacity; it is a marking/deterrence mechanism and is not tamper-proof or DRM.
|
||||
|
||||
Direct-flow warnings are returned by the domain core and rendered by both consumers. `checkForklift()` remains only for the existing non-headless length/target UI modes. Build metadata and generation time are supplied server-side.
|
||||
|
||||
## Request Resolution and Clarification
|
||||
|
||||
- Article numbers are strings and exact matches take precedence.
|
||||
- Duplicate records for one exact article number require clarification; file order never decides the result.
|
||||
- A supplied name hint is validated conservatively; a clear mismatch produces `article_conflict`.
|
||||
- Article-name normalization in `roll_calculation.py` maps only the product-family tokens `Stex`/`Secutex`, `Bfix`/`Bentofix`, and `Sgrid`/`Secugrid` to their abbreviated canonical forms before deterministic matching. The mappings are token-bounded and do not alter `article-data.json`.
|
||||
- Unknown numbers/names produce `article_not_found`; duplicate exact names require clarification.
|
||||
- The service may extract one unambiguous width from standardized article-name dimensions and marks its source as `article_master_data`.
|
||||
- Required absent values produce `status = needs_clarification` with `missing` fields.
|
||||
- Canonical core presets live in `core_presets.py` and are rendered into the web UI as well as consumed by the headless resolver. The ordered list remains 133 mm steel, 150 mm PVC, 168 mm PVC, and 194 mm steel.
|
||||
- Complete preset labels and finite explicit German/English aliases resolve canonically. Generic `steel`/`Stahlkern` and `PVC`/plastic descriptions remain ambiguous because both families contain two presets; unknown descriptions never select a diameter.
|
||||
- `CalculationState` stores structured request fields. Mutations change only validated fields and always recalculate.
|
||||
- Unknown state fields, including caller-supplied calculation results, are rejected rather than ignored.
|
||||
|
||||
## Article Data
|
||||
|
||||
Active data source:
|
||||
@@ -156,7 +227,7 @@ Known domain caveat from existing project notes:
|
||||
|
||||
## Forklift and Heavy-Roll Rules
|
||||
|
||||
The active template currently evaluates forklift warnings through `checkForklift()` and inline `window.APP_CONFIG`.
|
||||
The direct roll-diameter service evaluates the following active behavior. Other UI modes still evaluate the equivalent inline `window.APP_CONFIG` rules through `checkForklift()`.
|
||||
|
||||
Current inline rule behavior:
|
||||
|
||||
@@ -235,5 +306,7 @@ Login/access logging remains part of RollCalc, but log viewing and article-data
|
||||
- Flask's default static route may conflict with the intended authenticated `/static/<path:filename>` behavior; verify effective routing before relying on protected static files.
|
||||
- Most active logic is concentrated in one large HTML template.
|
||||
- Multiple similar static JavaScript files exist; not all are necessarily active.
|
||||
- No automated test suite is visible.
|
||||
- The deterministic service, state mutation, template integration, PDF adapter/renderer, and Flask endpoints have standard-library `unittest` coverage under `tests/`.
|
||||
- Some docs and filenames in `docs/` may reflect earlier package/archive versions.
|
||||
- Conversation sessions and report references are lost on restart and are not shared across WSGI workers.
|
||||
- Ollama availability/model installation is external runtime state; normal tests use mocks and do not require it.
|
||||
|
||||
Reference in New Issue
Block a user