feat: add conversational RollCalc assistant
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 🚀 RollCalc V14 + QoL Update - Deployment Guide
|
||||
|
||||
> Current repository note: direct roll-diameter calculations are generated server-side by `roll_calculation.py`, and PDF reports are rendered by `pdf_report.py`. Deploy both beside `app.py`, install the unchanged `requirements.txt`, and ensure nginx forwards authenticated `POST /api/calculations/roll`, `POST /api/calculations/roll/modify`, and `POST /api/reports/roll-calculation.pdf` requests to Flask. No browser, system PDF/font package, or additional Python dependency is required; the generated report is one A4 page under normal calculator conditions.
|
||||
|
||||
## 📦 Archive Contents
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
## Constrained Natural-Language Flow
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added deterministic, token-bounded product-family normalization in the article resolver for `Stex`/`Secutex`, `Bfix`/`Bentofix`, and `Sgrid`/`Secugrid`.
|
||||
- Added a minimal OpenWebUI Pipe that maps each OpenWebUI user/chat to the
|
||||
existing RollCalc conversation API, renders only deterministic messages, and
|
||||
exposes validated reports through a configurable browser-public URL.
|
||||
- Added friendly adapter errors, per-chat asynchronous request serialization,
|
||||
server-side Basic Auth without credential-bearing links, deployment/demo
|
||||
documentation, and mocked OpenWebUI Pipe tests without a RollCalc dependency.
|
||||
- Added a direct, dependency-free Ollama `/api/chat` client, defaulting to the configurable `qwen3.5:35B-A3B` model, with temperature `0`, thinking disabled, timeout handling, and JSON-schema-constrained output.
|
||||
- Added strict `new_calculation`, multi-field `modify_calculation`, and `unsupported` NLU validation; calculated and unknown fields are rejected.
|
||||
- Added process-local conversation state, deterministic German clarification/result templates, and temporary input-only PDF references.
|
||||
- Added authenticated conversation creation/message/report routes and a `python -m rollcalc_chat` terminal demo with optional diagnostics.
|
||||
- Added mocked Ollama, schema, conversational workflow, API, and authoritative conversational PDF tests; normal tests do not require Ollama.
|
||||
- Added `roll_calculation.py` as the authoritative headless service for direct diameter ranges, optional roll weight, article/core resolution, and warnings.
|
||||
- Added JSON-serializable calculation request/state/result models, structured clarification/failure statuses, and controlled mutation/recalculation.
|
||||
- Reject caller-supplied result fields in both requests and calculation state, and require clarification for duplicate exact article identifiers.
|
||||
- Added authenticated calculation and modification API boundaries for future tool use; no LLM integration was added.
|
||||
- Routed the active direct-diameter UI and PDF generation through the deterministic service and rejected caller-supplied engineering result fields.
|
||||
- Added archive-safe PDF filenames with date and `HHMMSS` time.
|
||||
- Added an authenticated **Download PDF** action for valid direct roll-diameter results.
|
||||
- Added compact, one-page A4 reports with article data, material inputs, minimum/average/maximum diameter, optional roll weight, active forklift warnings/notes, generation time, and build metadata.
|
||||
- Embedded repeated diagonal `INTERNAL USE ONLY` marks directly in the PDF page content stream. This is an internal-use marking and deterrent, not DRM or a tamper-proof control.
|
||||
- Added strict report-model validation and filesystem-safe archive filenames.
|
||||
- Added PDF model and endpoint tests without adding a new runtime dependency.
|
||||
- Renamed the PDF result section to **Calculation Results** and added the effective roll length as a fourth primary result card.
|
||||
|
||||
Reference in New Issue
Block a user