Package dyntabs.ai

Class EasyAI

java.lang.Object
dyntabs.ai.EasyAI

public final class EasyAI extends Object
Main entry point for the EasyAI library.

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 Type
    Method
    Description
    Starts building an EasyAgent that autonomously plans and executes multi-step tasks by orchestrating calls to your registered Java services.
    static <T> AssistantBuilder<T>
    assistant(Class<T> assistantInterface)
    Starts building an AI Assistant proxy for the given interface.
    Starts building a new Conversation for simple chat.
    static void
    Sets a global configuration that will be used as default for all new conversations and assistants (unless overridden per-builder).
    static <T> ExtractionBuilder<T>
    extract(Class<T> type)
    Starts a typed extraction: turn unstructured text or a document into a populated Java object (record or POJO).
    static String
    Extracts a clean, human-readable error message from an AI exception.
    Starts building a Flow: 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).
    Returns the global configuration, or loads from easyai.properties if not set.
    Starts building a document indexer that persists embeddings into a vector store (currently Milvus), so assistants can retrieve them later.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Method Details

    • chat

      public static ConversationBuilder chat()
      Starts building a new Conversation for 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

      public static <T> AssistantBuilder<T> assistant(Class<T> assistantInterface)
      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 @EasyAIAssistant for 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

      public static AgentBuilder agent()
      Starts building an EasyAgent that 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

      public static FlowBuilder flow()
      Starts building a Flow: 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

      public static <T> ExtractionBuilder<T> extract(Class<T> type)
      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 on
       

      Malformed 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

      public static IndexerBuilder 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) with withRAG(...) 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

      public static void configure(EasyAIConfig config)
      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

      public static EasyAIConfig getGlobalConfig()
      Returns the global configuration, or loads from easyai.properties if not set.
      Returns:
      the current global EasyAIConfig
    • extractErrorMessage

      public static String extractErrorMessage(Throwable t)
      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.message field. If the message is not JSON, it is returned as-is. If the exception is null, 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 key
      • dev.langchain4j.exception.RateLimitException — rate limit exceeded
      • dev.langchain4j.exception.InvalidRequestException — bad request, tool call failure
      • dev.langchain4j.exception.InternalServerException — provider server error
      Parameters:
      t - the exception thrown by an AI assistant or conversation call
      Returns:
      a clean, readable error message