Skip to content

Implemented: AI plugin — AI/LLM capabilities in OFBiz services (OFBIZ-13408)#244

Open
patelanil wants to merge 72 commits into
apache:trunkfrom
patelanil:feature/ai-plugin
Open

Implemented: AI plugin — AI/LLM capabilities in OFBiz services (OFBIZ-13408)#244
patelanil wants to merge 72 commits into
apache:trunkfrom
patelanil:feature/ai-plugin

Conversation

@patelanil
Copy link
Copy Markdown
Contributor

Adds a new optional plugin (plugins/ai) that integrates LangChain4j 1.8.0 to bring
native AI/LLM capabilities to OFBiz services, Groovy scripts, and screen actions.

The plugin follows established OFBiz patterns throughout:

  • AiContainer implements the Container interface for lifecycle management,
    following the same pattern as BirtContainer in the birt plugin
  • AiFactory holds the singleton ChatModel instance, parallel to BirtFactory
  • AiWorker provides static generate() and generateStructured() utility methods,
    parallel to BirtWorker
  • AiServices exposes two standard OFBiz services: ai.generate and ai.generateStructured
  • Configuration via config/ai.properties, read with UtilProperties at startup

The design is provider-agnostic — a switch on ai.provider selects the LangChain4j
builder. The initial implementation supports OpenAI and any OpenAI-compatible endpoint
(Ollama, Groq, Together AI, Azure OpenAI) via ai.baseUrl. Additional providers
(Anthropic, Bedrock) can be added by extending the switch in AiContainer.java.

Dependencies added (both Apache 2.0):

  • dev.langchain4j:langchain4j:1.8.0
  • dev.langchain4j:langchain4j-open-ai:1.8.0

Smoke test verified end to end: ai.smokeTest service returned a live response
from OpenAI gpt-4o-mini.

patelanil added 13 commits May 8, 2026 20:33
- ofbiz-component.xml: registers plugin, container, service resource
- build.gradle: LangChain4j 1.8.0 dependencies (Apache 2.0)
- servicedef/services.xml: empty stub (populated in Step 6)
- config/ai.properties: gitignored, template only

Plugin compiles cleanly into root OFBiz jar.
Note: ai.properties is gitignored — not committed.

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- Implements Container interface following BirtContainer pattern
- init() stores name and configFile only
- start() reads provider-agnostic ai.properties:
  ai.provider, ai.model, ai.apiKey, ai.baseUrl, ai.timeout
- Validates apiKey — fails fast with clear error if not configured
- Provider switch builds ChatModel interface (not OpenAiChatModel)
- openai case covers OpenAI, Groq, Ollama, Azure via baseUrl
- Additional providers (anthropic, bedrock) can be added in switch
- Stores singleton via AiFactory.setChatModel() (Step 3)
- stop() calls AiFactory.destroy()
- Note: ai.properties is gitignored — not committed

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- Static factory parallel to BirtFactory pattern
- setChatModel(ChatModel) — called by AiContainer.start()
- getChatModel() — throws IllegalStateException if not initialized
- destroy() — called by AiContainer.stop()
- Compiles cleanly with AiContainer

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- generate(dctx, messages) → String
- generateStructured(dctx, messages, schema) → Map<String,Object>
- TYPE_BUILDERS map pattern — no switch, extensible
- JSON Schema vocabulary: string, number, integer, boolean, array, object
- toChatMessages() converts List<Map> to LangChain4j ChatMessage list
- buildJsonObjectSchema() + buildSchemaElement() for schema conversion
- ResponseFormatType.JSON (JSON_SCHEMA does not exist in LangChain4j 1.8.0)
- Jackson ObjectMapper for JSON response parsing
- dctx parameter present for future use (audit logging, delegator)
- GeneralException wraps all failures with clear message

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- generate(dctx, context) → calls AiWorker.generate, returns response
- generateStructured(dctx, context) → calls AiWorker.generateStructured,
  returns result Map
- ServiceUtil.returnSuccess/returnError pattern
- UtilGenerics.cast() for unchecked context parameter casts
- Pure delegation — no LangChain4j imports

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- ai.generate: messages (List IN) → response (String OUT)
- ai.generateStructured: messages (List IN) + schema (Map IN) → result (Map OUT)
- configName optional IN on both services (reserved for future use)
- engine=java, location=org.apache.ofbiz.ai.AiServices

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- AiTest.groovy: calls AiWorker.generate with test message
- ai.smokeTest service registered in services.xml
- Verified end to end: response 'Hello!' received from OpenAI
- Full stack confirmed: AiContainer → AiFactory → AiWorker → LangChain4j → OpenAI

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- What the plugin does and JIRA reference OFBIZ-13408
- Architecture table: AiContainer, AiFactory, AiWorker, AiServices
- Installation and configuration instructions
- Multiple provider support via ai.baseUrl
- Usage examples: generate() and generateStructured()
- Available services table with IN/OUT params
- Smoke test instructions
- Guide for adding new providers

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- ai.generate → aiGenerate
- ai.generateStructured → aiGenerateStructured
- ai.smokeTest → aiSmokeTest

Dot notation is not OFBiz convention for service names.
Updated services.xml and README.md.

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- AiStructuredTest.groovy: calls AiWorker.generateStructured with
  simple schema [word: 'string']
- aiSmokeTestStructured service registered in services.xml
- Verified end to end: response [word:Helloreceived from OpenAI
- Validates key presence in response Map

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
- AiTest.groovy: add final String MODULE = 'AiTest.groovy'
- AiStructuredTest.groovy: add final String MODULE = 'AiStructuredTest.groovy'
- Replace all Debug.log string literal module arguments with MODULE
- Follows OFBiz Groovy script convention (ArtifactInfo.groovy pattern)

Ref: patelanil/ofbiz-dev#1
OFBIZ-13408
@patelanil
Copy link
Copy Markdown
Contributor Author

Fixed the SonarCloud violation — replaced the 'AiStructuredTest' string
literal with a final String MODULE constant following the OFBiz Groovy
script convention (same pattern as ArtifactInfo.groovy).

Also applied the same fix to AiTest.groovy proactively.

patelanil added 16 commits May 16, 2026 14:24
Add langchain4j-anthropic:1.8.0 and langchain4j-ollama:1.8.0 to build.gradle.

Refactor AiContainer to support openai, anthropic, and ollama via ai.properties
config — replaces single-provider switch block with buildChatModel() if/else-if.
AiContainer now holds the static ChatModel field and exposes getChatModel(),
following the ServiceContainer pattern. AiFactory is deleted.

Refactor AiWorker to reference AiContainer directly. Align both generate() and
generateStructured() to fetch chatModel before the try block with consistent null
guards — fixes a pre-existing NPE risk in generateStructured().
Both files are safe to track — ai.properties now uses placeholder values
only, and CLAUDE.md contains no credentials.
Document all three supported providers (openai, anthropic, ollama) with
inline comments, model examples, and placeholder API key. Follows OFBiz
convention of committing properties files with placeholder values.
…, AiHttpClient

ProviderRegistry reads named-provider blocks from ai.properties and builds
ProviderConfig instances. ToolCatalog scans component ai/*.tools.xml files
and builds ToolDescriptor instances with JSON Schema from ModelService params.
AgentRegistry scans component ai/*.agent.xml files and builds AgentDefinition
instances, validating provider and tool references at startup. AiHttpClient
implements AiChatClient using java.net.http.HttpClient and Jackson for
OpenAI-compatible chat/completions requests.
… LangChain4j

Add AgentRunner implementing the agentic loop with tool-call execution
via OFBiz services. Rewrite AiContainer to hold ToolCatalog, AgentRegistry,
and ProviderRegistry (no LangChain4j). Rewrite AiWorker to use AiHttpClient
directly for ai.generate and ai.generateStructured services.
…s, persistence, offline tests

- Add entitydef/AiAgentEntities.xml with AiAgentRun and AiAgentToolCall entities
- Add data/AiAgentSeedData.xml with StatusType and StatusItem seed rows
- Register entity model and seed data in ofbiz-component.xml
- Add persistence to AgentRunner: create run record before loop, persist tool calls
  inline, update run record with token counts and status after loop completes;
  all delegator calls wrapped in try-catch so persistence failures never abort runs
- Add package-private test constructor to AgentRunner that bypasses AiContainer
- Add MockAiChatClient scripted FIFO test double for AiChatClient
- Add AgentRunnerTest with three offline tests: stop reason, max_iterations cap,
  and tool result truncation
The saveThreadMessages helper was not setting the userLoginId field
when creating a new AiConversationThread record, leaving ownership
information unpopulated despite the entity defining the field.
…equenceNum

- saveThreadMessages used +2L offset causing a gap on every call;
  correct offset is +1L since only one slot is consumed before user message
- replaced queryList with queryFirst when fetching max sequenceNum to
  avoid loading the full message history on each save
- added threadId empty-check guard to archiveConversationThread
@patelanil patelanil changed the title Implemented: AI plugin — LangChain4j integration for AI/LLM capabilities in OFBiz services (OFBIZ-13408) Implemented: AI plugin — AI/LLM capabilities in OFBiz services (OFBIZ-13408) May 28, 2026
patelanil added 23 commits June 2, 2026 17:02
Demonstrates the AI agent framework with a realistic ecommerce use case
using OFBiz demo data (Gizmos and Widgets product catalog).

Four tools wrapping standard OFBiz entity queries:
- getProductPriceSummary: reads ProductPrice (default, list, cost, promo, competitive)
- getProductInventorySummary: sums ProductFacility.lastInventoryCount across facilities
- getRecentOrderActivity: queries OrderHeader + OrderItem for last N days
- setProductPromoPrice: creates SPECIAL_PROMO_PRICE record (requires-approval=true)

PromoAdvisor agent uses all four tools to analyse a product and recommend
a promotional price, then applies it after manager approval via the
AiAgentProposal workflow.
ToolCatalog stores tool JSON schemas using Anthropic's "input_schema"
key. AiHttpClient was forwarding the schema node as-is to OpenAI, which
expects "parameters". OpenAI silently ignored the unknown key and called
every tool with empty arguments, causing ServiceValidationException on
every tool invocation.

Fix: deep-copy the schema node in buildRequestBody() and rename
"input_schema" -> "parameters" before serialising the OpenAI request.
ToolCatalog is unchanged — an Anthropic client can use "input_schema" natively.
Two fixes discovered during live testing of the PromoAdvisor agent:

1. agentRun silent failure: the catch block called e.getMessage() on a
   Groovy MultipleCompilationErrorsException whose line-source reader was
   null, so getMessage() itself threw NPE. The service threw an unchecked
   exception, OFBiz never populated errorMessageList, and the UI re-rendered
   the form silently with no explanation.
   Fix: added safeMessage() helper that wraps getMessage() in its own
   try/catch. Added catch(Exception) safety net so no unchecked exception
   can ever escape agentRun silently.

2. Groovy tool scripts used explicit GenericValue type declarations without
   importing org.apache.ofbiz.entity.GenericValue, causing compilation
   failures at tool invocation time. Fixed by replacing typed declarations
   with def in all four tool scripts.
The OFBIZ_AI entities have no security group data loaded, so
genericBasePermissionCheck always fails with 'Problem on permission
service definition'. The /ai webapp already enforces OFBTOOLS _CREATE /
_UPDATE / _DELETE at the screen level — the extra service-level permission
check is redundant and blocks all agent management operations.
Adds createChatClientForProvider() helper and wires it into both
run() and continueFromApproval() so Anthropic providers get
AnthropicChatClient while all others use AiHttpClient.
Also adds AnthropicChatClient implementation (Anthropic Messages API).
Add responseSchema parameter to AiChatClient.chat(), structuredResult
field to ChatResponse (with backward-compat 5-param constructor), and
structuredResult field/getter to AgentRunner.RunResult. MockAiChatClient
updated to match new interface signature.
…edResult in agentRun

- Add responseSchema param to runLoop() and pass agent.getResponseSchema() from both callers (run() and continueFromApproval())
- Pass responseSchema as 5th arg to chatClient.chat() inside runLoop()
- Populate structuredResult on RunResult from response.getStructuredResult() in the stop case
- Return structuredResult from agentRun() service method
- Add structuredResult OUT attribute (type Map, optional) to agentRun service definition
- Fix remaining 4-param chat() calls in AiWorker and rejectAgentProposal to pass null responseSchema
…structuredResult in UI

Live testing revealed OpenAI's strict json_schema mode rejects any schema that
does not declare additionalProperties:false on every object and list every
property in "required" — a cryptic 400 for hand-authored schemas. Setting
strict=false keeps output constrained to valid JSON matching the schema while
accepting any reasonable user-authored schema, consistent with the Anthropic path.

Also adds a structuredResult display field to the RunAgentResult screen so the
parsed Map is visible when running a structured-output agent through the /ai webapp.

Validated end-to-end on both providers with a TicketTriage agent:
- OpenAI gpt-4o-mini: structuredResult Map populated
- Anthropic claude-haiku-4-5: Map populated (markdown code fences stripped)
- Agent without responseSchema: free text, structuredResult null (backward compatible)
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant