ADR-013: LangChain4j Integration for Intelligent CLI Workflow Routing
<!-- MADR 3.0 Template - Markdown Any Decision Records --> <!-- Reference: https://adr.github.io/madr/ -->
Status
Implemented
Date
2025-12-06
Deciders
- Development Team
Context and Problem Statement
The idempiere-cli currently uses traditional command-line parsing with Picocli where users must know exact command syntax. As the CLI grows with more commands (generators, registry, migration, etc.), discoverability becomes challenging. Users need to remember specific command names, flags, and argument orders.
We want to enable natural language command routing where users can describe what they want to accomplish, and an AI agent intelligently routes the request to the appropriate CLI command or workflow. This builds on our existing AI integration strategy (ADR-010, ADR-011) and leverages the Java ecosystem.
Decision Drivers
- User Experience: Enable natural language interaction for CLI tasks
- Java Ecosystem: Maintain consistency with existing Quarkus/Java stack
- Offline Capability: Support local LLM execution without cloud API dependency
- iDempiere Context: Leverage Application Dictionary knowledge for intelligent routing
- Extensibility: Allow easy addition of new tools/commands as routing targets
- Cost Control: Option to run entirely locally without API costs
Considered Options
- LangChain4j with Tool-based Routing - Java-native AI framework with @Tool annotations
- LangGraph (Python) via HTTP Bridge - Python implementation with Java HTTP client
- Custom Rule-Based Router - Pattern matching without LLM
- Direct OpenAI Function Calling - Raw API integration without framework
Decision Outcome
Chosen option: "LangChain4j with Tool-based Routing", because it provides native Java integration with our Quarkus stack, supports multiple LLM backends (including local models via Ollama), and offers a clean annotation-based tool definition that aligns with our existing Picocli command structure.
Confirmation
The decision is confirmed when:
- Natural language input is successfully routed to correct CLI commands
- Both cloud (Claude/OpenAI) and local (Ollama) LLM backends work
- Tool execution produces expected results
- Performance overhead is acceptable (<2s for routing decision)
Pros and Cons of the Options
Option 1: LangChain4j with Tool-based Routing
Native Java framework for LLM applications with structured tool calling.
- Good, because native Java/Quarkus integration
- Good, because supports multiple LLM providers (OpenAI, Claude, Ollama, Hugging Face)
- Good, because @Tool annotation mirrors our @Command pattern
- Good, because supports local LLM execution (no API costs)
- Good, because active development and community
- Neutral, because requires LLM for routing (even local)
- Bad, because adds dependency (~5MB)
Option 2: LangGraph via HTTP Bridge
Use Python LangGraph server with Java HTTP client.
- Good, because LangGraph has advanced workflow orchestration
- Good, because larger Python AI ecosystem
- Bad, because adds Python runtime dependency
- Bad, because HTTP bridge adds latency and complexity
- Bad, because mixed technology stack
Option 3: Custom Rule-Based Router
Pattern matching and keyword extraction without LLM.
- Good, because zero external dependencies
- Good, because deterministic behavior
- Good, because fast execution
- Bad, because limited natural language understanding
- Bad, because maintenance burden for rules
- Bad, because cannot handle complex or ambiguous requests
Option 4: Direct OpenAI Function Calling
Raw API integration without LangChain4j abstraction.
- Good, because minimal dependencies
- Good, because full control over implementation
- Bad, because vendor lock-in to OpenAI
- Bad, because no local LLM support
- Bad, because must implement tool orchestration from scratch
More Information
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ User Natural Language Input │
│ "List all tables with audit columns" │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────────┐
│ LangChain4j Agent │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ ChatLanguageModel (configurable backend) │ │
│ │ - OpenAiChatModel (gpt-4, gpt-3.5-turbo) │ │
│ │ - AnthropicChatModel (claude-3) │ │
│ │ - OllamaChatModel (llama3, mistral) ← LOCAL │ │
│ │ - HuggingFaceChatModel │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Routing Decision (Tool Selection) │ │
│ │ Based on: user intent, tool descriptions, AD context │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────────┐
│ CLI Tools (LangChain4j @Tool) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ RegistryTool │ │ GenerateTool │ │ MigrationTool│ │
│ │ │ │ │ │ │ │
│ │ @Tool │ │ @Tool │ │ @Tool │ │
│ │ listTables() │ │ model() │ │ create() │ │
│ │ getTable() │ │ process() │ │ apply() │ │
│ │ findColumns()│ │ window() │ │ rollback() │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TableTool │ │ ColumnTool │ │ DoctorTool │ │
│ │ │ │ │ │ │ │
│ │ @Tool │ │ @Tool │ │ @Tool │ │
│ │ create() │ │ add() │ │ diagnose() │ │
│ │ sync() │ │ modify() │ │ validate() │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────────┐
│ Existing CLI Services │
│ │
│ RegistryService, GeneratorRegistry, MigrationService, etc. │
└─────────────────────────────────────────────────────────────────┘
Implementation Notes
1. Maven Dependencies
<!-- LangChain4j Core -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.35.0</version>
</dependency>
<!-- Quarkus Integration -->
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-core</artifactId>
<version>0.20.0</version>
</dependency>
<!-- LLM Providers (choose based on needs) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>0.35.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>0.35.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>0.35.0</version>
</dependency>
2. CLI Tool Definitions
package org.idempiere.cli.ai.tools;
import dev.langchain4j.agent.tool.Tool;
import jakarta.inject.Inject;
/**
* LangChain4j tools wrapping CLI registry commands.
* Tool descriptions guide the LLM in selecting appropriate actions.
*/
public class RegistryTools {
@Inject
RegistryService registryService;
@Tool("Lists all tables in the Application Dictionary. " +
"Use when user wants to see available tables, search for tables, " +
"or explore the database schema.")
public String listTables(String pattern) {
return registryService.listTables(pattern);
}
@Tool("Gets detailed metadata for a specific table including columns, " +
"access level, and relationships. Use when user asks about " +
"a specific table structure.")
public String getTableDetails(String tableName) {
return registryService.getTableDetails(tableName);
}
@Tool("Finds columns matching a pattern across all tables. " +
"Use when user wants to find where a specific field is used.")
public String findColumns(String columnPattern) {
return registryService.findColumns(columnPattern);
}
@Tool("Lists all windows in the Application Dictionary. " +
"Use when user asks about UI windows or forms.")
public String listWindows(String pattern) {
return registryService.listWindows(pattern);
}
}
package org.idempiere.cli.ai.tools;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
import jakarta.inject.Inject;
/**
* LangChain4j tools wrapping CLI generator commands.
*/
public class GeneratorTools {
@Inject
GeneratorRegistry generatorRegistry;
@Tool("Generates a Java model class for an iDempiere table. " +
"Creates the M{TableName}.java file with getters/setters. " +
"Use when user wants to create model code for a table.")
public String generateModel(
@P("Table name (e.g., C_BPartner, M_Product)") String tableName,
@P("Output directory for generated file") String outputDir) {
return generatorRegistry.generate("model", tableName, outputDir);
}
@Tool("Generates a process class (SvrProcess) for iDempiere. " +
"Use when user wants to create a new background process or report.")
public String generateProcess(
@P("Process class name") String className,
@P("Package name") String packageName) {
return generatorRegistry.generate("process", className, packageName);
}
@Tool("Generates a window/tab/field configuration. " +
"Use when user wants to create UI components.")
public String generateWindow(
@P("Window name") String windowName,
@P("Base table name") String tableName) {
return generatorRegistry.generate("window", windowName, tableName);
}
}
3. AI Agent Service Interface
package org.idempiere.cli.ai;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
/**
* LangChain4j AI Service for CLI command routing.
* The agent uses tools to execute CLI operations based on natural language.
*/
@RegisterAiService(tools = {
RegistryTools.class,
GeneratorTools.class,
MigrationTools.class,
TableTools.class,
DoctorTools.class
})
public interface CliRouterAgent {
@SystemMessage("""
You are an iDempiere CLI assistant that helps users interact with
the Application Dictionary and generate code. You have access to
tools that wrap CLI commands.
When the user makes a request:
1. Understand what they want to accomplish
2. Select the appropriate tool(s) to fulfill the request
3. Execute the tools and provide helpful output
iDempiere context:
- Tables follow naming conventions: C_ (Customer), M_ (Material), AD_ (Dictionary)
- Common tables: C_BPartner (Business Partners), M_Product, C_Order, C_Invoice
- Application Dictionary (AD_*) tables define metadata
""")
String route(@UserMessage String userRequest);
}
4. Command Integration
package org.idempiere.cli.commands;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import dev.langchain4j.service.AiServices;
import org.idempiere.cli.ai.CliRouterAgent;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import jakarta.inject.Inject;
import java.util.concurrent.Callable;
@Command(name = "ask",
description = "Natural language interface to CLI commands",
mixinStandardHelpOptions = true)
public class AskCommand implements Callable<Integer> {
@Parameters(description = "Natural language request")
String request;
@Option(names = {"--provider", "-p"},
description = "LLM provider: ollama, anthropic, openai",
defaultValue = "ollama")
String provider;
@Option(names = {"--model", "-m"},
description = "Model name (e.g., llama3, claude-3-sonnet)")
String model;
@Inject
RegistryTools registryTools;
@Inject
GeneratorTools generatorTools;
@Override
public Integer call() {
ChatLanguageModel llm = createModel();
CliRouterAgent agent = AiServices.builder(CliRouterAgent.class)
.chatLanguageModel(llm)
.tools(registryTools, generatorTools)
.build();
String result = agent.route(request);
System.out.println(result);
return 0;
}
private ChatLanguageModel createModel() {
return switch (provider.toLowerCase()) {
case "ollama" -> OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName(model != null ? model : "llama3")
.build();
case "anthropic" -> AnthropicChatModel.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
.modelName(model != null ? model : "claude-3-sonnet-20240229")
.build();
case "openai" -> throw new UnsupportedOperationException(
"OpenAI support pending");
default -> throw new IllegalArgumentException(
"Unknown provider: " + provider);
};
}
}
5. Configuration (application.properties)
# LangChain4j / Quarkus AI configuration
quarkus.langchain4j.ollama.base-url=http://localhost:11434
quarkus.langchain4j.ollama.chat-model.model-id=llama3
quarkus.langchain4j.ollama.timeout=60s
# For cloud providers (optional)
quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY:}
quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY:}
# Default provider selection
idempiere.cli.ai.default-provider=ollama
idempiere.cli.ai.enabled=true
Usage Examples
# Natural language queries (using local Ollama)
idempiere-cli ask "list all tables with audit columns"
idempiere-cli ask "show me the structure of C_BPartner table"
idempiere-cli ask "generate a model class for M_Product"
idempiere-cli ask "what windows are available for managing orders?"
# Specify provider
idempiere-cli ask --provider=anthropic "find all columns named DocumentNo"
idempiere-cli ask --provider=ollama --model=mistral "create a new process"
# Complex multi-step requests
idempiere-cli ask "find all tables related to invoicing and show their relationships"
Local LLM Setup (Ollama)
For offline/local operation without API costs:
# Install Ollama
brew install ollama
# Pull a model (choose based on hardware)
ollama pull llama3 # Recommended for tool calling
ollama pull mistral # Alternative
ollama pull codellama # For code generation focus
# Start Ollama server (runs on localhost:11434)
ollama serve
Integration with Existing Architecture
┌─────────────────────────────────────────────────────────────────┐
│ idempiere-cli Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Traditional CLI │ │ AI-Powered CLI │ │
│ │ (Picocli) │ │ (LangChain4j) │ │
│ │ │ │ │ │
│ │ idempiere-cli │ │ idempiere-cli │ │
│ │ registry list │ │ ask "..." │ │
│ │ generate model │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ ┌────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Shared Service Layer │ │
│ │ │ │
│ │ RegistryService GeneratorRegistry MigrationService │ │
│ │ TableService ColumnService DoctorService │ │
│ └───────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼──────────────────────────────┐ │
│ │ Data Access Layer │ │
│ │ │ │
│ │ Direct JDBC (offline) │ REST API (ADR-009) │ │
│ │ cloudempiere.ai (ADR-011) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Future Enhancements
- Conversation Memory: Add chat history for multi-turn interactions
- RAG with AD Metadata: Embed Application Dictionary for better context
- Workflow Chaining: Support multi-step workflows with dependencies
- Custom Tool Discovery: Auto-register tools from plugin SPI
- Fine-tuning: Train models on iDempiere-specific patterns
Related ADRs
- ADR-010: MCP Server Architecture - MCP integration for Claude Code
- ADR-011: cloudempiere.ai Integration - Deep iDempiere AI context
- ADR-006: CLI as M2M API - Machine-to-machine interface
- ADR-008: Application Dictionary Registry - AD metadata access
- ADR-013 Appendix: Integration Analysis - Detailed integration strategy
Integration Analysis Summary
See 013-appendix-integration-analysis.md for detailed analysis. Key points:
| Related ADR | Relationship | Integration Strategy |
|---|---|---|
| ADR-008 (Registry) | Data Provider | LangChain4j tools consume RegistryService for AD metadata |
| ADR-010 (MCP Server) | Parallel Interface | Shared tool logic layer avoids duplication |
| ADR-011 (cloudempiere.ai) | Enhanced Backend | Optional secure backend with role-based access |
Key Architectural Decision: Create org.idempiere.cli.ai.shared package with tool logic that both ADR-010 MCP wrappers and ADR-013 LangChain4j wrappers can consume, avoiding code duplication.
References
- LangChain4j Documentation
- LangChain4j GitHub
- Quarkus LangChain4j Extension
- Ollama - Local LLM runner
- LangChain Concepts: Agents
- LangChain Routing
<!-- MADR 3.0 Guidelines:
- Required sections: Context and Problem Statement, Considered Options, Decision Outcome
- Recommended sections: Decision Drivers, Pros/Cons of Options, Confirmation
- Status values: Proposed, Accepted, Deprecated, Superseded
- ADRs are immutable - create new ADR to supersede, never edit existing
- Include diagrams where helpful (architecture, sequence, flow) -->