Skip to main content

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.

Form Engine Architecture

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 @Cacheable caching 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 FormFlowNode hierarchy. Uses NodeTypeResolver to alternate between FIELD and OPTION nodes by depth (even depth = FIELD, odd depth = OPTION).
  • FieldDefinitionParser — parses individual field definitions into FieldDefinitionConfig objects 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) or OPTION (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:

ToolPurpose
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:

  1. Field Type Reference — a table of all field types with translated descriptions (DATE includes today's date)
  2. Form Structure — the hierarchical field list with show_when conditions 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:

ValidatorPurpose
RequiredField must have a non-empty value
MinLength / MaxLengthString length bounds
Min / MaxNumeric range bounds
PatternRegex pattern matching
MinAdvanceHoursBooking must be at least N hours in the future
MaxAdvanceDaysBooking 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 dtoField in the dtoFields list → FormDto.fields (main DTO)
  • Fields with dtoField NOT in the list → FormDto.extras (keyed by field name, not dtoField value)
  • dtoField is optional — when omitted from YAML, the field name is used automatically at runtime
  • SemanticValidator checks 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).