Class EasyAI
EasyAI is a simple abstraction layer over LangChain4J. It hides all the low-level details (ChatModel, ChatMemory, AiServices, ToolSpecification, EmbeddingStore...) behind a clean builder-pattern API that any Java developer can use in minutes.
Quick Start
Step 1: Add your API key to easyai.properties on the classpath:
easyai.provider=openai easyai.api-key=sk-YOUR-KEY easyai.model-name=gpt-4o-mini
Step 2: Start chatting!
Three Ways to Use EasyAI
1. Simple Chat (chat())
Send messages to AI and get responses. Optionally remembers conversation history.
Conversation chat = EasyAI.chat()
.withMemory(20) // remember last 20 messages
.withSystemMessage("You are a helpful tutor") // set AI personality
.build();
String answer = chat.send("What is Java?");
String follow = chat.send("Give me an example"); // AI remembers the context
2. AI Assistant with Tools (assistant(Class))
Define an interface, give it your existing service classes as "tools", and the AI will call your Java methods when needed.
// 1. Define assistant interface
@EasyAIAssistant(systemMessage = "You are an e-commerce support bot")
public interface SupportBot {
String ask(String question);
}
// 2. Your existing service — mark callable methods with @EasyTool (opt-in)
public class OrderService {
@EasyTool("Finds an order by its ID")
public String findOrder(String orderId) {
return database.findById(orderId).toString();
}
}
// 3. Wire it together
SupportBot bot = EasyAI.assistant(SupportBot.class)
.withTools(orderService, userService) // only @EasyTool methods are exposed
.build();
// 4. The AI will automatically call orderService.findOrder("12345")
String answer = bot.ask("Where is my order #12345?");
3. Document-Powered Assistant (RAG)
Let the AI answer questions based on your PDF, DOCX, or TXT files.
@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();
String answer = bot.ask("What is the vacation policy?");
// AI reads the PDF and answers based on its content
Overriding Configuration Per-Call
You can override any config property when building:
Conversation chat = EasyAI.chat()
.withProvider("ollama") // use local Ollama
.withModel("llama3") // specific model
.withBaseUrl("http://localhost:11434/v1/") // custom endpoint
.withTemperature(0.3) // less creative
.withMaxTokens(500) // shorter answers
.build();
CDI / Jakarta EE Integration
In a Jakarta EE application, assistants are automatically injectable:
@Inject SupportBot bot; // no manual build() needed
- See Also:
-
Method Summary
Modifier and TypeMethodDescriptionstatic AgentBuilderagent()Starts building anEasyAgentthat autonomously plans and executes multi-step tasks by orchestrating calls to your registered Java services.static <T> AssistantBuilder<T> Starts building an AI Assistant proxy for the given interface.static ConversationBuilderchat()Starts building a newConversationfor simple chat.static voidconfigure(EasyAIConfig config) Sets a global configuration that will be used as default for all new conversations and assistants (unless overridden per-builder).static <T> ExtractionBuilder<T> Starts a typed extraction: turn unstructured text or a document into a populated Java object (record or POJO).static StringExtracts a clean, human-readable error message from an AI exception.static FlowBuilderflow()Starts building aFlow: a deterministic, developer-authored pipeline of named steps where you fix the order in plain Java and the LLM is called only at the edges you declare (typically "understand the request" first, "summarize the outcome" last).static EasyAIConfigReturns the global configuration, or loads fromeasyai.propertiesif not set.static IndexerBuilderindexer()Starts building a document indexer that persists embeddings into a vector store (currently Milvus), so assistants can retrieve them later.
-
Method Details
-
chat
Starts building a newConversationfor simple chat.Conversation chat = EasyAI.chat() .withMemory(20) .withSystemMessage("You are a helpful assistant") .build(); String answer = chat.send("Hello!");- Returns:
- a new
ConversationBuilder
-
assistant
Starts building an AI Assistant proxy for the given interface.The interface should have one or more methods that accept a String and return a String. Annotate it with
@EasyAIAssistantfor a system message.@EasyAIAssistant(systemMessage = "You are a code reviewer") public interface CodeReviewer { String review(String code); } CodeReviewer reviewer = EasyAI.assistant(CodeReviewer.class).build(); String feedback = reviewer.review("public void foo() { ... }");- Type Parameters:
T- the assistant interface type- Parameters:
assistantInterface- the interface class to create a proxy for- Returns:
- a new
AssistantBuilder
-
agent
Starts building anEasyAgentthat autonomously plans and executes multi-step tasks by orchestrating calls to your registered Java services.Unlike
assistant(Class), which answers a single question, an agent receives a complex task and breaks it into steps — calling your service methods in the right order, using the result of each step to decide what to do next, and adapting if a step fails.EasyAgent agent = EasyAI.agent() .withServices(inventoryService, paymentService, orderService) .withMaxSteps(10) .withPlanningPrompt(true) .withStepListener(step -> log.info("[AGENT] Step {}: {}({}) -> {}", step.stepNumber(), step.toolName(), step.arguments(), step.result())) .build(); String result = agent.execute( "Order 2 laptops for user U123, apply loyalty credit, " + "use fallback warehouse WH-EU if out of stock." );- Returns:
- a new
AgentBuilder
-
flow
Starts building aFlow: a deterministic, developer-authored pipeline of named steps where you fix the order in plain Java and the LLM is called only at the edges you declare (typically "understand the request" first, "summarize the outcome" last).This is the deliberate counterpart to
agent(). An agent lets the model decide which service to call next — right when the path is genuinely unknown, but non-deterministic and hard to test. A flow is for a known business process (check stock → take payment → create order → ship): one correct order, the same every time, each step a plain, testable function. The motto: LLM for language, Java for logic.FlowContext out = EasyAI.flow() .step("understand", ctx -> EasyAI.extract(OrderRequest.class).from(ctx.inputText())) // LLM .step("checkStock", ctx -> inventory.checkStock(ctx.get("understand", OrderRequest.class))) .step("pay", ctx -> payment.charge(ctx.get("understand", OrderRequest.class))) .step("createOrder",ctx -> orders.create(ctx.get("understand", OrderRequest.class))) .step("ship", ctx -> shipping.schedule(ctx.get("createOrder", Order.class))) .step("summarize", ctx -> EasyAI.chat().build() .send("Tell the user what happened:\n" + ctx.trail())) // LLM .build() .run("Order 3 blue watches, ship home."); String reply = (String) out.result(); // the "summarize" step's output- Returns:
- a new
FlowBuilder - See Also:
-
extract
Starts a typed extraction: turn unstructured text or a document into a populated Java object (record or POJO).This is the bridge from the unstructured/AI world into your normal typed-Java world. Once
.from(...)returns, no AI is involved any more — you hold a plain object that your existing services, JPA entities, and PrimeFaces forms already understand.record Invoice(String vendor, String invoiceNumber, LocalDate date, BigDecimal total, List<LineItem> items) {} // From free text: Invoice inv = EasyAI.extract(Invoice.class).from(emailBody); // From a document's bytes - parses (Tika) and extracts in one call: Invoice inv = EasyAI.extract(Invoice.class) .from(DocumentSource.of("invoice.pdf", pdfBytes)); em.persist(inv); // it is just data from here onMalformed model output is retried automatically; call
.validate()to also run Jakarta Bean Validation on the result.- Type Parameters:
T- the type to extract- Parameters:
type- the class to extract (a record or POJO)- Returns:
- a new
ExtractionBuilder - See Also:
-
indexer
Starts building a document indexer that persists embeddings into a vector store (currently Milvus), so assistants can retrieve them later.This is the write side of RAG. Where
assistant(Class)withwithRAG(...)builds an ephemeral, in-memory index that vanishes when the JVM stops,indexer()writes to a persistent store you populate once and reuse — think "save the documents to the database" rather than "load them for this request."// One-time (or scheduled) ingestion: int indexed = EasyAI.indexer() .toMilvus("localhost", 19530, "documents") .index("file:/data/policy.pdf", "classpath:faq.txt"); // Later, any assistant reads from the same collection: PolicyBot bot = EasyAI.assistant(PolicyBot.class) .withMilvus("localhost", 19530, "documents") .build();- Returns:
- a new
IndexerBuilder - See Also:
-
configure
Sets a global configuration that will be used as default for all new conversations and assistants (unless overridden per-builder).EasyAI.configure(EasyAIConfig.builder() .provider("openai") .apiKey("sk-...") .modelName("gpt-4o") .build());- Parameters:
config- the global configuration
-
getGlobalConfig
Returns the global configuration, or loads fromeasyai.propertiesif not set.- Returns:
- the current global
EasyAIConfig
-
extractErrorMessage
Extracts a clean, human-readable error message from an AI exception.LangChain4J exceptions (especially from OpenAI-compatible providers like Groq, Azure OpenAI, and OpenAI itself) often carry raw JSON in their message, for example:
{"error":{"message":"Failed to call a function...","type":"invalid_request_error",...}}This method parses the JSON and extracts only the
error.messagefield. If the message is not JSON, it is returned as-is. If the exception isnull, an empty string is returned.Typical usage in a JSF backing bean or REST endpoint:
try { return bot.ask(userQuestion); } catch (Exception e) { log.error("AI call failed", e); return "Sorry, something went wrong: " + EasyAI.extractErrorMessage(e); }Common LangChain4J exception types to catch separately if needed:
dev.langchain4j.exception.AuthenticationException— wrong API keydev.langchain4j.exception.RateLimitException— rate limit exceededdev.langchain4j.exception.InvalidRequestException— bad request, tool call failuredev.langchain4j.exception.InternalServerException— provider server error
- Parameters:
t- the exception thrown by an AI assistant or conversation call- Returns:
- a clean, readable error message
-