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
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
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
- 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
- We use MiniLM embeddings to convert component metadata into vector representations.
- 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
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

No comment yet, add your voice below!