Skip to content

DriveWorks forms to React: An AI-Powered case study

Author: Pramod Sridhar

Executive summary

This case study presents an AI-driven approach to modernizing DriveWorks HTML forms into React components using a multi-agent architecture powered by DSPy, ChromaDB (RAG), and LLMs. The system intelligently parses, converts, validates, and assembles UI components using specialized agents, while leveraging a vector-based cache to reduce LLM usage and cost. This solution has shown early success in significantly reducing conversion time, improving reusability, and laying the foundation for scalable migration of legacy UI systems.

Introduction

DriveWorks is a proprietary configuration and automation tool widely used in manufacturing industries for creating dynamic forms. In our client project, DriveWorks has been employed to power the Hose Configurator application. However, its tightly coupled monolithic architecture with DriveWorks as the front end and direct SQL queries for the backend has posed several limitations. These included high vendor dependency, difficulty in scaling, lack of developer accessibility, and high maintenance costs.

With a growing need to modernize legacy systems, our team initiated a proof-of-concept (POC) project to convert DriveWorks forms into React components using AI technologies, with a long-term goal of enabling scalable, maintainable, and cost-efficient modernization.

Problem statement

The original Drive Works-based system has the following challenges:

  • Vendor Lock-in: No in-house expertise, must depend on vendor even for minor enhancements
  • Monolithic Design: No separation between UI, logic, and data. Unstructured Backend architecture.
  • Performance Bottlenecks: Slow rendering and interaction in complex forms. Very low parallel execution permitted.
  • Lack of Reusability: Manual reimplementation of similar UI patterns or business rules
  • High Maintenance Overhead: High cost and limited flexibility due to unsupported code repository and CI/CD processes

Objective of the POC

This POC was aimed to:

  • Automate the conversion of DriveWorks forms into React components
  • Maintain a knowledge base for reusable UI patterns
  • Establish a scalable, efficient and repeatable pipeline for future migrations
  • Demonstrate functional and visual parity with the original forms

Proposed architecture

React converter architecture

The solution architecture is modular, agent-driven, and powered by Retrieval-Augmented Generation (RAG):

  • Frontend: React
  • Backend: Python based API
  • LLM Provider: Groq (LLaMA3 70B)
  • Caching: ChromaDB for embedding-based similarity
  • Agent Workflow: DSPy for orchestration of Planner, Executor, Validator, and Regrouper
  • Validation: Pydantic for typed data integrity
Table & description

System workflow overview in steps:

DriveWorks Form: Accepts HTML containing various Driveworks tags

Planner Agent: Breaks down the HTML into a structured JSON blueprint for downstream processing.

RAG System: Reuses previously converted components from cache to reduce cost and speed up conversion.

Executor Agent: Reuses previously converted components from cache to reduce cost and speed up conversion.

Validator Agent: Ensures generated React code is correct, clean, and compliant with standards.

Regrouper Agent: Integrates validated components into a cohesive React layout that mirrors the original form.

WebSocket updates: Streams live updates to users, providing visibility into progress and debugging.

Final React Components: Produces final React code

Implementation highlights

To ensure maintainability, debuggability, and accuracy, the conversion pipeline was broken into four specialized agents:

1. DSPy agents for pipeline orchestration

Having a monolithic prompt or agent trying to parse, convert, validate, and assemble the React code would be an overkill and will consume considerable time and tokens to do so. Hence, we decided to separate the concerns into four specialized DSPy agents:

  • Planner: Analyzes the input DriveWorks HTML and converts it to structured JSON by parsing the XML structure. It extracts component definitions (like comboBox, input text etc), layout metadata (like X, Y positions), and styling attributes. This makes downstream agent tasks more deterministic and interpretable.

  • Executor: Converts each unknown component (not found in cache) into corresponding React code. This agent receives clearly defined component type, properties, and style guidelines from the planner, and returns JSX snippets using structured LLM prompting.

  • Validator: Checks and improves the React code for syntax errors, best practices, and logical issues. This helps catch cases where the LLM may have generated incomplete or invalid code.

  • Regrouper: Combines all the JSX snippets into a single React functional component. It uses layout and style metadata from the Planner to align and structure the output visually.

Why separate agents?

  • Modularity: Easier to debug, maintain, and upgrade each step independently.
  • Prompt Optimization: Different prompts can be fine-tuned for planning, execution, validation, and regrouping.
  • Clear Inputs/Outputs: Each agent’s result feeds directly and cleanly into the next.
  • Improved Reusability: Planner or Validator logic can be reused across future projects with different forms.
  • Enhanced Observability: We can trace exactly where failures or regressions occur.
  • Agent Evaluation: Allows for independent performance scoring of each phase using custom evaluation.

2. LLM Optimization through Batch Processing

One of the critical architectural optimizations was batching components for LLM calls instead of converting them individually. Consider the example DriveWorks form:

<Label Name=”lblProductionPlant” Text=”Production plant”/>

<ComboBox Name=”CBProductionPlant” Option1=”Plant A” Option2=”Plant B” />

<Label Name=”lblMedium” Text=”Medium”/>

<ComboBox Name=”CBMedium” Option1=”Water” Option2=”Oil” />

… (10+ repeated Label + ComboBox pairs)

If we process  each Label and ComboBox pair separately:

  • Each component (Label or ComboBox) triggers one LLM call each.
  • With 10 such pairs, that results in 20+ LLM calls which resulted in substantial API cost and delays.

Instead, we:

  • Parse all components upfront
  • Use RAG to filter out known components
  • Batch all unknown ones into a single prompt

     Impact:

  • Reduced latency and API overhead
  • Better prompt context, reducing LLM confusion
  • Minimizes token usage and cost significantly

3. RAG with ChromaDB

We implemented Retrieval-Augmented Generation (RAG) to avoid reprocessing known components. Here’s why and how:

Problem Before RAG:

Every form even if 80% of components were reused from previous forms, would go through expensive and redundant LLM calls.

Solution with RAG:

After a component is validated once, we save it into ChromaDB with:

  • Type (e.g., ComboBox)
  • Properties (e.g., options, default value)
  • Final React code
  1. We use MiniLM embeddings to convert component metadata into vector representations.
  2. On future runs, similar components are matched via vector similarity with a set threshold (<1 in our case)

Knowledge Base Contents

To make RAG effective, the system maintains a knowledge base of canonical component conversions. Each entry consists of a DriveWorks component type and its properties with a validated React snippet. This enables the pipeline to reuse known conversions instantly, ensuring consistency and reducing repeated LLM calls.

Example:

textbox_example:

  <input type=”text” className=”border p-2″ placeholder=”Enter value” />

metadata:

textbox:

component_type: TextBox

driveworks_props: “label=Name, required=true”

 combobox_example:

<select className=”border p-2″><option>Small</option><option>Medium</option><option>Large</option></select>

metadata:

combobox:

component_type: ComboBox

driveworks_props: “label=Size, options=Small|Medium|Large”

By storing both React snippets and DriveWorks properties, the system can quickly recognize when a similar component reappears and reuse it directly, avoiding unnecessary LLM calls.

Benefits:

  • Drastic reduction in LLM cost and latency
  • Self-learning behavior—system improves with every form processed
  • Persistent cache using ChromaDB’s disk-backed collections
  • Debuggable & Inspectable—we can audit matched cache entries

4. WebSocket Integration

We integrated a real-time WebSocket connection between the backend and the frontend interface. This allowed the pipeline to send live status updates to the user as each agent completes its phase.

  • Planner sends: “Parsed 12 components from form”
  • Executor sends: “Converting 4 new components”, “3 found in cache”
  • Validator sends: “All components validated successfully”
  • Regrouper sends: “Final React component assembled”

Why these matters:

  • Transparency: Users can observe the full agent journey and trust the AI pipeline
  • Debugging: Any errors can be caught in real time with the responsible agent clearly identified
  • UX: Maintains user engagement and reduces wait-time anxiety during conversion
Optimization Tools

Challenges faced & how they were solved

  • Challenge: Repeated LLM calls for similar UI components led to inefficiencies.
    Solution: Introduced ChromaDB-based RAG with MiniLM embeddings to reuse prior conversions.
  • Challenge: Unstructured prompting led to inconsistent component output.
    Solution: Used DSPy signatures and teleprompters for structured prompting and Chain-of-Thought workflows.
  • Challenge: Difficulty tracking LLM performance across components of multiple known or unknown types.
    Solution: Added batch performance metrics and logging at agent-level granularity.

Key code snippets

To bridge understanding across technical and non-technical readers, below are curated code blocks that illustrate critical parts of the implementation:

Phase 1: Pre-processing & planning

• Fast Cache Lookup Before LLM Call

def check_memory_cache(component_type: str, props_str: str) -> Optional[str]:

    cache_key = f”{component_type}|{props_str}”

    return IN_MEMORY_CACHE.get(cache_key)

This function performs a quick check in in-memory cache to see if a component has already been converted and stored. If found, the system skips LLM calls altogether, drastically reducing cost and latency.

Phase 2: Splitting + Batch preparation

• Splitting known and unknown components

cached, need_llm = split_known_unknown(components)

cache_hit_rate = len(cached) / len(components) if components else 0

This logic separates components that can be reused from cache and those that need LLM generation. The cache hit rate provides insight into system reuse efficiency.

Phase 3: Execution via LLM

• Batched LLM prompting for efficiency

exec_prompt = (

    PROMPTS[“executor”][“batch”]

    + “\n\n“`json\n”

    + json.dumps({ “components”: need_llm, “style_guide”: UI_MAPPING }, indent=2)

    + “\n“`\nReturn JSON [{type, code}]”

)

raw_exec = await call_llm(“Batch component conversion”, exec_prompt)

Instead of sending multiple prompts (one per component), this batch prompt sends all unknown components in a single request. This minimizes token usage and improves conversion time.

Phase 4: Per-component execution

• Component conversion via executor agent

exec_result = executor(

    component_data=component,

    component_type=component.get(“type”),

    style_guide=plan_result.style_guide

)

This code shows the Executor agent converting a component to JSX using structured component metadata and design rules. It separates business logic from UI rendering, enabling component-level reuse and traceability.

Phase 5: Validation

• Real-Time agent status via webSocket

await send_log_message(

    AgentLogMessage(

        session_id=session_id,

        agent=”Executor”,

        status=”working”,

        message=”Converting 4 new components…”,

        cache_miss=True

    )

)

This message is pushed from backend to frontend to update users on the current processing step. It helps users understand agent progress and diagnose issues in real-time.

• Cleaning LLM responses for safe parsing

def cleanup_json_block(raw: str) -> str:

    cleaned = re.sub(r”“`(?:json)?\\n?”, “”, raw).strip().rstrip(“`”)

    cleaned = re.sub(r”,\\s*]”, “]”, cleaned)

    cleaned = re.sub(r”,\\s*}”, “}”, cleaned)

    return cleaned

LLM responses may contain markdown wrappers or trailing commas that break JSON parsing. This utility strips unwanted formatting to ensure reliable downstream processing.

Phase 6: Caching new results

• Storing components in vector Cache and memory

cache_key = f”{canonical_type}|{props_str}”

IN_MEMORY_CACHE[cache_key] = tgt[“code”]

kb.add_conversion(

    canonical_type,

    props_str,

    tgt[“code”],

)

Once a component is converted and validated, it’s stored in both in-memory and ChromaDB vector store, enabling future reuse via similarity search.

Phase 7: Retrieval for RAG use

• ChromaDB RAG usage for similarity-based reuse

hit = kb.best_match(component_type, props_str, threshold=1)

if hit:

    reuse_conversion(hit[“text”])

else:

    send_to_llm()

This illustrates how the system determines whether a similar component has already been converted. If a match is found using ChromaDB’s vector similarity, the LLM is skipped, optimizing performance and cost.

Phase 8: Final layout assembly

• Regrouping the final react layout

regroup_prompt = (

    PROMPTS[“regrouper”][“system”]

    + “\n\nComponents:\n”

    + json.dumps(all_components, indent=2)

    + “\n\nLayout:\n”

    + json.dumps({ “structure”: “vertical” }, indent=2)

    + “\n\nStyleGuide:\n”

    + json.dumps(UI_MAPPING, indent=2)

)

raw_regroup = await call_llm(“Final React layout”, regroup_prompt)

After all components are validated, the regrouper agent combines them into a single structured React file, maintaining visual and layout consistency with the original DriveWorks form.

Prompt Templates (prompts.yml)

Each agent in the pipeline uses a structured prompt template, with context dynamically injected from the knowledge base. This keeps prompts concise while grounding them in real, validated examples.

Executor agent prompt example (from prompts.yml):

executor_agent:

  goal: “Convert unknown DriveWorks components into React + Tailwind.”

  context: “{{context}}”   # KB snippets are injected here

  instructions: |

    – Reuse patterns from context

    – Generate accessible React components with Tailwind classes

    – Keep props accurate, do not invent

  output_format: |

    “`tsx

    export const Generated = () => { … }

    “`

In practice, the top 2–3 most relevant KB snippets are passed into the {{context}} field. This allows the Executor to learn from past conversions and produce React code that is both consistent and aligned with the project’s standards.

Forecasted Business Value (based on initial metrics and internal estimation):

  • Cost Savings: €10,000–15,000 annually by eliminating DriveWorks dependency
  • Speed: 85% reduction in form conversion time
  • Developer Productivity: 3× faster onboarding with standardized React components
  • Consistency: High-quality, validated, and reusable code
  • Scalability: Capable of handling 100+ forms using the same pipeline

Note: These figures are projections based on our POC results and are subject to refinement in the final implementation.

Conclusion

This solution illustrates how agentic AI architectures, when combined with tools like DSPy and RAG, can enable legacy modernization at scale. While the full system is still under development, the early results are promising and offer a strong foundation for replacing proprietary UI platforms like DriveWorks with modern, intelligent, and scalable frameworks.

We believe this AI-driven transformation path will not only lower technical debt but also future-proof the UI modernization journey for large industrial applications.

Do you have any AI related challenges? We would love to hear from you.

Let’s connect over a cup of coffee. Please email us at  p.koning@devon.nl

Recommended Posts

No comment yet, add your voice below!


Add a Comment

Your email address will not be published. Required fields are marked *