Dynamic tabs and AI for Jakarta EE applications — without the boilerplate.
TabForge AI is a Jakarta EE library with two independent modules:
- DynTabs — multi-tab PrimeFaces UI with a proper CDI scope per tab
- EasyAI — add AI assistants and chatbots to your app in minutes
Both are designed for real Jakarta EE applications: CDI, EJB, PrimeFaces, GlassFish, WildFly, Payara.
Building a multi-tab UI in PrimeFaces means managing tab state, lifecycle, navigation, and component IDs manually. @ViewScoped doesn't work per-tab. CDI @Observes can't route events to individual tab instances. Duplicate tabs, stale data, and ID collisions are constant problems.
Define tabs as annotated CDI beans. DynTabs handles everything else.
@Named
@TabScoped // one bean instance per open tab
@DynTab(name = "OrdersDynTab",
title = "Orders",
includePage = "/WEB-INF/include/orders/orders.xhtml")
public class OrdersBean extends BaseDyntabCdiBean {
@Inject
private OrderService orderService;
@Override
protected void accessPointMethod(Map parameters) {
// called when the tab opens — load your data here
orders = orderService.findAll();
}
}Open the tab from a menu item:
<p:menuitem value="Orders" action="uishell:Orders"/>That's it. DynTabs opens the tab, creates an isolated OrdersBean instance in @TabScoped, calls accessPointMethod(), and prevents duplicates if the user clicks again.
@TabScoped— custom CDI scope with one bean instance per open tab. Open the same tab twice and get two completely independent instances.- Tab lifecycle —
accessPointMethod()on open,exitPointMethod()on close - Inter-tab messaging —
sendMessageToAllAppModules(payload)/onApplicationMessage() - Workflow pattern — child tab closes and returns a value to the parent with
closeAndReturnValueToCaller() - Dynamic tabs — open tabs programmatically with parameters at runtime
- In-tab navigation — switch XHTML pages inside a tab without opening a new one
- Declarative security —
@DynTab(securedResource=true, allowedRoles={"ADMIN"}) - Repeatable
@DynTab— one bean can serve as multiple different tabs with different parameters
Adding AI to a Jakarta EE application with LangChain4J means learning ChatModel, AiServices, ToolSpecification, EmbeddingStore, ContentRetriever, and more — before writing a single line of business logic. Spring AI has the same problem.
Six things cover 95% of use cases:
// 1. Simple chat
Conversation chat = EasyAI.chat()
.withMemory(20)
.withSystemMessage("You are a helpful Java tutor.")
.build();
String answer = chat.send("What is a HashMap?");// 2. Assistant that calls your Java services (no @Tool annotations needed)
@EasyAIAssistant(systemMessage = "You are an e-commerce support bot.")
public interface SupportBot {
String ask(String question);
}
// OrderService is a plain POJO or @Stateless EJB — no changes needed
SupportBot bot = EasyAI.assistant(SupportBot.class)
.withTools(orderService, userService)
.build();
bot.ask("Where is my order #12345?");
// AI calls orderService.findOrder("12345") automatically// 3. AI that answers from your documents (PDF, DOCX, TXT)
@EasyRAG(source = "classpath:company-policy.pdf")
@EasyAIAssistant(systemMessage = "Answer based on the company policy.")
public interface PolicyBot {
String ask(String question);
}
PolicyBot bot = EasyAI.assistant(PolicyBot.class).build();
bot.ask("How many vacation days do employees get?");// 4. Autonomous multi-step agent — plans and executes sequences of tool calls
EasyAgent agent = EasyAI.agent()
.withServices(orderService, paymentService, shippingService)
.withMaxSteps(10)
.withPlanningPrompt(true)
.withStepListener(step -> log.info("Step {}: {} → {}", step.stepNumber(), step.toolName(), step.result()))
.build();
String result = agent.execute("Process order #42: verify stock, charge the card, and schedule delivery");
// Agent autonomously calls the right services in the right order// 5. Persistent RAG — index documents once into a vector store, query them forever
EasyAI.indexer()
.toMilvus("localhost", 19530, "company_kb")
.index("file:/data/handbook.pdf"); // also accepts byte[] from a DMS, DB BLOB, or upload
@EasyAIAssistant(systemMessage = "Answer from the company knowledge base.")
public interface KbBot { String ask(String question); }
KbBot bot = EasyAI.assistant(KbBot.class)
.withMilvus("localhost", 19530, "company_kb") // survives restarts, shared across sessions/nodes
.build();
bot.ask("How many vacation days do employees get?");
// No documents are loaded or embedded at request time — they were indexed once, up front// 6. Structured extraction — turn unstructured text or a document into a typed Java object
record Invoice(String vendor, String invoiceNumber, LocalDate date,
BigDecimal total, List<LineItem> items) {}
// From an email body, or straight from a PDF's bytes (parsed + extracted in one call)
Invoice inv = EasyAI.extract(Invoice.class)
.from(DocumentSource.of("invoice.pdf", pdfBytes));
em.persist(inv); // no AI from here on — it's just a typed object your code already understands- Zero-annotation tools — pass any POJO or
@Inject-ed EJB to.withTools(). EasyAI discovers methods via reflection. No@Tool, no schema, no config. - EJB proxy support —
@Stateless,@Stateful,@Singletonbeans work transparently. Container services (transactions, security, interceptors) are preserved. - RAG from any source — classpath, file path, or
byte[]from a DMS, database BLOB, REST API, or user upload - Persistent vector store —
EasyAI.indexer().toMilvus(...).index(...)writes embeddings to Milvus once;.withMilvus(...)lets any assistant query them. Survives restarts, shared across sessions and server nodes. Local embedding model included (no API key, runs offline) - Structured extraction —
EasyAI.extract(Invoice.class).from(text or PDF)returns a populated record/POJO. Parses the document, extracts, retries on malformed output, and optionally runs Jakarta Bean Validation — in one call - Autonomous agent —
EasyAI.agent()executes multi-step tasks across your services without manual orchestration. Built-in step limit, step listener, and planning prompt. - Live observability (new in 2.1) —
.withEventListener(...)streams anEasyAIEventfor every moment of an operation (started → tool call → result → finished). Transport-agnostic: log it, meter it, or push it to a live UI. See below. - CDI integration — assistants are injectable with
@Inject. Tool beans are auto-wired viatools = {...}on the annotation. - Global config + per-call override — set API key once with
EasyAI.configure(), override per assistant if needed - Clean error messages —
EasyAI.extractErrorMessage(e)parses JSON error responses from OpenAI-compatible providers
EasyAI normally works silently and hands you a final answer. Add one method — .withEventListener(listener) — and it will instead narrate itself as it runs: an EasyAIEvent for "I started", "I'm calling tool X", "tool X returned", "I'm on document 7 of 200", "I finished". It works on EasyAI.chat(), EasyAI.agent(), EasyAI.indexer(), and EasyAI.extract().
EasyAgent agent = EasyAI.agent()
.withServices(orderService, paymentService)
.withEventListener(event ->
log.info("[{}] {} — {}", event.source(), event.phase(), event.title()))
.build();
agent.execute("Order 2 laptops for U123, charge the card, schedule delivery");
// [AGENT] STARTED — Planning task
// [AGENT] STEP_STARTED — checkStock
// [AGENT] STEP — checkStock
// [AGENT] STEP_STARTED — processPayment
// ...
// [AGENT] FINISHED — Task completeThe key design choice: EasyAIEvent knows nothing about HTTP, SSE, WebSockets, or any UI's JSON schema. It is a plain immutable value (source, phase, status, title, detail, toolName, sequence, timestamp). You decide what to do with it — so the same event stream can feed a log file, a metrics counter, or a real-time dashboard without EasyAI ever depending on any of them.
For the full "wow" — every chat turn and agent tool call rendered live in a browser Activity panel — the starter ships the ~150 lines of (entirely app-side) Server-Sent-Events plumbing that maps EasyAIEvent → UI. You never write that mapping into your library code; it stays in your app, exactly where transport belongs.
If you are building on Jakarta EE, Spring AI is simply the wrong tool — it requires Spring Boot, Spring context, and Spring beans throughout your application. EasyAI is designed for the Jakarta EE runtime you already have.
| EasyAI | Spring AI | |
|---|---|---|
| Target runtime | Jakarta EE — CDI, EJB, GlassFish, WildFly, Payara | Spring Boot / Spring context |
| Tool registration | Zero annotations — pass any POJO or EJB, all public methods become tools | @Tool on each method, or a per-method MethodToolCallback |
| EJB bean as tool | Built-in — @Stateless, @Stateful, @Singleton work as-is, container services preserved |
Not supported |
| AI config | Single easyai.properties file + EasyAI.configure() |
application.properties + Spring bean wiring |
| RAG from byte[] | DocumentSource.of("name.pdf", bytes) — one line that parses, splits, and embeds |
TikaDocumentReader(ByteArrayResource) exists, but you assemble the reader → splitter → embedding → store pipeline yourself |
| Persistent vector store (Milvus) | Two one-liners — indexer().toMilvus(...).index(...) to write, .withMilvus(...) to query; local embedding model included |
MilvusVectorStore bean (needs a MilvusServiceClient + EmbeddingModel) wired manually, plus the ETL pipeline |
| Structured extraction | EasyAI.extract(T.class).from(text or PDF) — parses the document, extracts, retries, optional Bean Validation, in one call |
.entity(T.class) converts text to an object, but document parsing, retries, and validation are on you |
| CDI injection | @Inject SupportBot bot |
@Autowired (Spring context only) |
| Simple chat | EasyAI.chat().build() — one line, no interface needed |
ChatClient builder + a ChatModel bean |
| Multi-step agent | EasyAI.agent() — built-in, fully managed |
Tool calls auto-execute, but no first-class agent (no step cap, typed trace, or planning toggle) |
| Agent safety limit | withMaxSteps(n) — hard cap on tool calls, returns a final answer when reached |
No built-in cap on automatic tool-calling — needs a custom advisor or a manual loop |
| Agent execution trace | withStepListener(step -> ...) — typed callback with tool name, args, and result per step |
Via custom advisors or Micrometer observation — no typed per-step callback out of the box |
| Agent planning prompt | withPlanningPrompt(true) — instructs the model to plan before acting |
Write your own system prompt — no built-in toggle |
Comparison reflects Spring AI 2.0.0-M8 (May 2026). Spring AI is a capable, broad framework; the point here is fit for the Jakarta EE runtime and the amount of wiring each task takes — not that Spring AI can't do these things. The tool-call cap gap is tracked upstream in spring-ai#3333.
EasyAI also works outside Jakarta EE — plain Java, unit tests, standalone apps. Just call .build() directly, no container needed.
- Java 21+
- Jakarta EE 11+ (CDI 4, EJB 4)
- PrimeFaces 13+ (DynTabs module only)
- LangChain4J 1.15.1 (included transitively)
Quickest start: clone the TabForge AI Starter — a pre-configured Maven WAR project for Eclipse with everything already set up. Open it, deploy, and start writing tab beans immediately.
Add to an existing project: add the dependency to your pom.xml (packaging must be war):
<dependency>
<groupId>io.github.tabforgeai</groupId>
<artifactId>tabforge-ai</artifactId>
<version>2.0.0</version>
</dependency>Then follow the setup guides — DynTabs takes 6 steps (faces-config, template include, beans.xml, etc.), EasyAI takes 2 (dependency + easyai.properties). Full instructions with copy-paste examples are in the guides below.
- EasyAI Developer Guide — full API reference, all use cases, configuration, tips
- DynTabs Developer Guide — setup, all use cases, annotations reference, tips
- JavaDoc — API reference
- Demo Application — working example with DynTabs and EasyAI
Apache License 2.0
