Form Engine
This is the most detailed architectural diagram, covering the internal design of the form engine — from YAML configuration loading through parsing, tree construction, tool-based interaction, validation, and the conversation state machine.
Config Loading — Decorator Pattern
Form configuration starts as YAML files: customers/*.yaml provides per-customer form definitions, and translations/{locale}.yaml holds locale-specific text.
FormConfigLoader is the interface, with two implementations:
- YamlFormConfigLoader — reads and parses YAML files from the classpath
- CachedFormConfigLoader — a decorator that adds
@Cacheablecaching with checksum-based hot-reload, so config changes are picked up without a restart
Loaded configuration passes through 7 ConfigValidators (ordered by @Order) to check structure, semantics, node types, field references, validation rules, field properties, and translation keys. For a complete guide to writing YAML configs, see the Configuration section.
Parsing Pipeline
Two parsers convert raw config into the internal model:
- FormFlowParser — recursively walks the YAML tree and builds a
FormFlowNodehierarchy. UsesNodeTypeResolverto alternate between FIELD and OPTION nodes by depth (even depth = FIELD, odd depth = OPTION). - FieldDefinitionParser — parses individual field definitions into
FieldDefinitionConfigobjects containing type, validation rules, prompt keys, and option lists.
Form Flow Tree — Composite Pattern
The parsed tree is a composite structure of FormFlowNode objects. Each node has:
- A NodeType — either
FIELD(a question to answer) orOPTION(a branch selected by the user's choice) - A QuestionFieldType — one of TEXT, EMAIL, DATE, TIME, NUMBER, SELECT, SMART_SELECT, TEXTAREA, BOOLEAN, or INFO
- Links to children (sub-questions), options (branches for SELECT fields), and a parent reference
This creates a tree where the root's children are top-level questions, SELECT questions branch into options, and each option can have its own sub-questions — enabling conditional form flows.
Tool-Based Interaction
The LLM interacts with the form engine through FormTools, which exposes @Tool annotated methods:
| Tool | Purpose |
|---|---|
getFieldInfo(fieldName) | Get type, prompt, options, validation rules, description |
submitAnswer(fieldName, value) | Validate and store answer; reports cascading clears on branch switch |
clearAnswer(fieldName) | Clear answer + cascading clears for branch descendants |
isLegallyComplete() | Check if all mandatory fields are answered |
buildSummary() | Generate confirmation summary of all answers |
addExtraInfo(key, value) | Capture contextual info not in form fields (e.g., pet info, access codes) |
resetForm() | Clear all answers and start over |
The inner LLM reads the form structure via getFormSchema() (produced by FormSchemaRenderer) and current progress via getFormState() to decide which field to ask about next. Value extraction happens inline — the LLM reads field type info and submits correctly formatted values directly via submitAnswer().
Schema Rendering
FormSchemaRenderer produces a flat, DFS-ordered representation with two sections:
- Field Type Reference — a table of all field types with translated descriptions (DATE includes today's date)
- Form Structure — the hierarchical field list with
show_whenconditions for conditional branches, mandatory markers, and INFO annotations
This flat format is designed for LLM consumption — research confirms flat structures outperform nested ones for LLM reasoning.
Validation — Strategy Pattern
When an answer is submitted via submitAnswer(), AnswerValidator first coerces the raw string to the expected type, then applies all matching validation rules for the field.
ValidationRuleRegistry auto-discovers validators via Spring and maps rule names to implementations. The 8 built-in validators are:
| Validator | Purpose |
|---|---|
| Required | Field must have a non-empty value |
| MinLength / MaxLength | String length bounds |
| Min / Max | Numeric range bounds |
| Pattern | Regex pattern matching |
| MinAdvanceHours | Booking must be at least N hours in the future |
| MaxAdvanceDays | Booking must be at most N days in the future |
DTO Building & dtoField Mapping
When all mandatory fields are answered and the user confirms, buildDto() assembles a FormDto:
- Fields with
dtoFieldin thedtoFieldslist →FormDto.fields(main DTO) - Fields with
dtoFieldNOT in the list →FormDto.extras(keyed by field name, not dtoField value) dtoFieldis optional — when omitted from YAML, the field name is used automatically at runtimeSemanticValidatorchecks for dtoField path collisions via DFS to prevent silent overwrites
State Machine
The conversation lifecycle is managed by two enums:
ConversationPhase tracks the high-level flow:
GREETING → FORM_FILLING → PRE_CONFIRMATION → CONFIRMATION → COMPLETED
↓
ABANDONED (on timeout or exit)
SessionStatus tracks the session itself: ACTIVE, PAUSED, RESUMED, COMPLETE, or EXPIRED. This is independent of the conversation phase — a session can be PAUSED during any phase and RESUMED later.
Translation
TranslationService (implemented by YamlTranslationService) provides locale-specific text throughout the system. It loads {locale}.yaml files, resolves nested dot-notation keys (e.g. form.fields.name.prompt), and falls back to English (en) when a key is missing in the requested locale.
Translation is used both at config time (resolving promptKey and labelKey in field definitions) and at runtime (generating user-facing prompts, form schema descriptions, and error messages).