Package dyntabs.ai

Class AssistantBuilder<T>

java.lang.Object
dyntabs.ai.AssistantBuilder<T>
Type Parameters:
T - the assistant interface type

public class AssistantBuilder<T> extends Object
Builder for creating AI assistant proxies from annotated interfaces.

This builder creates a runtime proxy that forwards method calls to an AI model. You can optionally add tool objects (your existing Java services) that the AI can call, and document sources (RAG) that the AI can reference.

Use Case 1: Simple AI Assistant (No Tools)


 @EasyAIAssistant(systemMessage = "You are a helpful translator")
 public interface Translator {
     String translate(String text);
 }

 Translator t = EasyAI.assistant(Translator.class).build();
 String result = t.translate("Hello, how are you?");
 // result: "Bonjour, comment allez-vous?" (depending on system message)
 

Use Case 2: AI Assistant with Tools (AI Calls Your Java Code)

This is the most powerful feature. Pass your existing service objects to withTools(Object...), and the AI will call their @EasyTool-annotated methods when needed. Tools are opt-in: only annotated methods are exposed to the model; everything else stays uncallable.


 // Your existing service - annotate the methods the AI may call
 public class WeatherService {
     @EasyTool("Returns the current weather for a city")
     public String getWeather(String city) {
         return weatherApi.fetch(city).toString();
     }
 }

 @EasyAIAssistant(systemMessage = "You are a weather assistant")
 public interface WeatherBot {
     String ask(String question);
 }

 // Wire it together
 WeatherBot bot = EasyAI.assistant(WeatherBot.class)
     .withTools(new WeatherService())
     .build();

 // AI automatically calls WeatherService.getWeather("London") behind the scenes
 String answer = bot.ask("What's the weather like in London?");
 

Use Case 3: Multiple Tool Services


 SupportBot bot = EasyAI.assistant(SupportBot.class)
     .withTools(orderService, userService, inventoryService)
     .build();

 // AI can call methods from ANY of the three services
 bot.ask("What is the status of order #123 for user john@example.com?");
 

Use Case 4: Jakarta EJB Beans as Tools

EJB beans (@Stateless, @Stateful, @Singleton) injected via @Inject work as tools out of the box. EasyAI automatically detects the EJB proxy and reads the @EasyTool methods from the actual bean class. Method calls go through the container proxy, so transactions, security, and interceptors work normally.


 @Stateless
 public class OrderService {
     @PersistenceContext private EntityManager em;

     @EasyTool("Finds an order by its ID")
     public String findOrder(String orderId) {
         return em.find(Order.class, orderId).toString();
     }
 }

 // In your CDI bean:
 @Inject OrderService orderService;   // EJB proxy from the container

 SupportBot bot = EasyAI.assistant(SupportBot.class)
     .withTools(orderService)          // only its @EasyTool methods are callable
     .build();

 bot.ask("Where is order #123?");
 // AI calls orderService.findOrder("123") through the EJB proxy
 

Use Case 5: Override Settings Per-Assistant


 Translator t = EasyAI.assistant(Translator.class)
     .withModel("gpt-4o")           // use a specific model
     .withMemory(50)                 // remember 50 messages
     .withSystemMessage("Translate everything to Serbian")
     .build();
 
See Also:
  • Method Details

    • withTools

      public AssistantBuilder<T> withTools(Object... tools)
      Adds tool objects whose @EasyTool-annotated methods the AI can call.

      Opt-in (since 3.0.0): only methods you mark with @EasyTool become tools. Every other method — including state-mutating ones like cancelOrder or deleteUser — stays invisible to the model. Adding a method to a service is safe by default; it does nothing until you annotate it.

      Accepts plain POJOs and Jakarta EJB proxies (@Stateless, @Stateful, @Singleton) obtained via @Inject. EJB proxies are detected automatically — business methods are discovered from the actual bean class, while invocations go through the proxy so that container services (transactions, security, interceptors) work normally.

      Parameters:
      tools - one or more service objects (POJOs or injected EJB proxies)
      Returns:
      this builder
      See Also:
    • withAllPublicMethodsAsTools

      public AssistantBuilder<T> withAllPublicMethodsAsTools(Object... tools)
      Adds tool objects and exposes every public method on them to the AI — annotated or not.

      Unsafe escape hatch. This restores the pre-3.0.0 "expose everything" behavior. Because a tool channel is reachable by the model (whose input is not fully trusted), this lets a prompt-injected model invoke any public method on the beans you pass — a classic confused-deputy exposure. Reserve it for throwaway prototypes where nothing is sensitive; for anything real, prefer withTools(Object...) with @EasyTool on exactly the methods you intend.

      Parameters:
      tools - one or more service objects whose entire public surface becomes callable
      Returns:
      this builder
      See Also:
    • withMemory

      public AssistantBuilder<T> withMemory(int maxMessages)
    • withChatMemory

      public AssistantBuilder<T> withChatMemory(dev.langchain4j.memory.ChatMemory chatMemory)
      Uses a ChatMemory instance you own and keep, instead of letting the builder create a fresh one sized by withMemory(int).

      Why this exists

      withMemory(int) says "make me a memory of this size" — a brand-new, empty conversation buffer is created inside every build(). That is exactly what you don't want when you have to rebuild the assistant but keep the conversation going. The motivating case: an assistant whose tool set changes with context (e.g. a global panel that exposes order tools only while the user is on the Orders tab). Tools are fixed at build() time, so changing them means rebuilding — and with withMemory(int) each rebuild would wipe the chat history. Hand the builder a memory you hold onto instead, pass the same instance into every rebuild, and the conversation survives untouched.

      It is also the seam for a custom or persistent memory: supply a MessageWindowChatMemory with your own store, or any other ChatMemory implementation, and the assistant will read and append to it.

      Precedence: when set, this wins over withMemory(int) — the size hint is ignored because you are supplying the whole memory. Pass null to fall back to the size-based default.

      Familiar analogy: withMemory(int) is asking the office for a fresh, empty notebook each meeting; withChatMemory(...) is bringing your own notebook so the running notes carry over from one meeting to the next — even when the meeting room (the tool set) changes.

      Thread-safety: a shared memory is shared mutable state. Reuse one instance only within a single logical conversation (e.g. one session bean), not across concurrent users.

      Parameters:
      chatMemory - the memory to read from and append to; null restores the withMemory(int) default
      Returns:
      this builder
      See Also:
    • withSystemMessage

      public AssistantBuilder<T> withSystemMessage(String systemMessage)
    • withModel

      public AssistantBuilder<T> withModel(String modelName)
    • withApiKey

      public AssistantBuilder<T> withApiKey(String apiKey)
    • withRAG

      public AssistantBuilder<T> withRAG(String... sources)
      Enables RAG (document-powered AI) with the given document sources.

      This is the programmatic alternative to the @EasyRAG annotation. Use this when your document paths are not known at compile time, for example when they come from a database, user upload, or application configuration.

      In a web application, use file: prefix to point to documents on the server's file system, or pass any absolute path:

      
       // Documents on the server file system
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG("file:C:/app-data/docs/policy.pdf")
           .build();
      
       // Path from application config or database
       String docPath = appConfig.getDocumentPath();
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG(docPath)
           .build();
      
       // Multiple documents
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG("file:/data/policy.pdf", "file:/data/faq.pdf")
           .build();
       

      Supports the same path prefixes as @EasyRAG: classpath:, file:, or plain relative paths.

      Parameters:
      sources - one or more document paths
      Returns:
      this builder
      See Also:
    • withRAG

      public AssistantBuilder<T> withRAG(String[] sources, int maxResults, double minScore)
      Enables RAG with full control over retrieval parameters.
      
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG(
               new String[]{"file:/data/policy.pdf", "file:/data/terms.pdf"},
               5,     // return top 5 relevant segments
               0.7    // only segments with 70%+ relevance
           )
           .build();
       
      Parameters:
      sources - one or more document paths
      maxResults - maximum number of relevant segments to retrieve (default 3)
      minScore - minimum relevance score, 0.0 to 1.0 (default 0.5)
      Returns:
      this builder
    • withRAG

      public AssistantBuilder<T> withRAG(DocumentSource... sources)
      Enables RAG from in-memory document sources (byte arrays).

      Use this when your documents come from a DMS, database, REST API, or any source that provides content as byte[].

      
       // From a DMS
       byte[] pdfBytes = dmsClient.downloadDocument("DOC-12345");
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG(DocumentSource.of("policy.pdf", pdfBytes))
           .build();
      
       // From a database BLOB
       byte[] content = resultSet.getBytes("document_content");
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG(DocumentSource.of("terms.pdf", content))
           .build();
      
       // Plain text (no file needed)
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG(DocumentSource.ofText("policy", "All employees get 25 vacation days..."))
           .build();
      
       // Multiple documents from different sources
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withRAG(
               DocumentSource.of("policy.pdf", dmsClient.download("policy")),
               DocumentSource.of("faq.txt", restApi.getFaqBytes()),
               DocumentSource.ofText("extra-rules", additionalRulesText)
           )
           .build();
       
      Parameters:
      sources - one or more document sources with content as byte arrays
      Returns:
      this builder
      See Also:
    • withRAG

      public AssistantBuilder<T> withRAG(List<DocumentSource> sources, int maxResults, double minScore)
      Enables RAG from in-memory document sources with tuning parameters.
      Parameters:
      sources - document sources with content as byte arrays
      maxResults - maximum number of relevant segments to retrieve
      minScore - minimum relevance score, 0.0 to 1.0
      Returns:
      this builder
    • withMilvus

      public AssistantBuilder<T> withMilvus(String host, int port, String collectionName)
      Connects this assistant to a persistent Milvus collection for retrieval, using explicit connection settings.

      This is the read-side counterpart to EasyAI.indexer(). Unlike withRAG(String...) — which loads and embeds documents fresh on every build() into an in-memory store — withMilvus points at a collection that was populated earlier (and stays populated). Think "query the existing database" versus "load a file into memory for this one request."

      When set, Milvus takes precedence over withRAG(...) and @EasyRAG. Retrieval tuning uses maxResults/minScore defaults (3 / 0.5) unless you call withMilvus(MilvusConfig, int, double).

      
       PolicyBot bot = EasyAI.assistant(PolicyBot.class)
           .withMilvus("localhost", 19530, "documents")
           .build();
       
      Parameters:
      host - Milvus server hostname
      port - Milvus server port (typically 19530)
      collectionName - the collection to retrieve from
      Returns:
      this builder
    • withMilvus

      public AssistantBuilder<T> withMilvus(MilvusConfig config)
      Connects this assistant to a persistent Milvus collection using a fully built MilvusConfig (for non-default dimension or credentials).
      Parameters:
      config - the Milvus connection settings
      Returns:
      this builder
    • withMilvus

      public AssistantBuilder<T> withMilvus(MilvusConfig config, int maxResults, double minScore)
      Connects this assistant to a persistent Milvus collection with explicit retrieval tuning.
      Parameters:
      config - the Milvus connection settings
      maxResults - maximum relevant segments to retrieve per query
      minScore - minimum relevance score, 0.0 to 1.0
      Returns:
      this builder
    • withMilvus

      public AssistantBuilder<T> withMilvus()
      Connects this assistant to a persistent Milvus collection configured entirely from easyai.properties (keys easyai.milvus.*).
      Returns:
      this builder
      See Also:
    • withChatModel

      public AssistantBuilder<T> withChatModel(dev.langchain4j.model.chat.ChatModel model)
    • withActivityContext

      public AssistantBuilder<T> withActivityContext(ActivityContext activityContext)
      Makes this assistant ambient-activity aware: before each call to any of the assistant's methods, the given context is re-rendered and folded into the system message, so the model already knows what the user has recently been doing in the UI (and can resolve "this"/"that" without being told).

      Familiar analogy: giving your assistant a glance at your desk before every question — it sees the order you just opened, so "email the customer about it" needs no further explanation.

      
       SupportBot bot = EasyAI.assistant(SupportBot.class)
           .withTools(orderService)
           .withActivityContext(ActivityContext.of(activityStore)
               .forSession(sessionId).forTab(tabId).build())
           .build();
      
       bot.ask("Cancel this order");   // "this" resolved from recent activity
       

      The context is combined with any withSystemMessage(String) value by SystemMessageComposer; passing null simply leaves the feature off.

      Parameters:
      activityContext - the ambient-activity descriptor to inject, or null to disable
      Returns:
      this builder
      See Also:
    • withEventListener

      public AssistantBuilder<T> withEventListener(EasyAIListener eventListener)
      Registers a transport-agnostic EasyAIListener that receives a live stream of EasyAIEvents as the assistant calls your tools.

      This is the same observability hook the other capabilities expose (see AgentBuilder.withEventListener(EasyAIListener) and FlowBuilder.withEventListener(EasyAIListener)). Because an assistant's method is invoked directly by your code (there is no wrapper for EasyAI to bracket), the stream here is the per-tool-call slice of the lifecycle: for each tool the model decides to call, a EasyAIEvent.Phase.STEP_STARTED fires before it runs (a spinning row in a UI) and a EasyAIEvent.Phase.STEP fires after, with EasyAIEvent.Status.SUCCESS or EasyAIEvent.Status.ERROR. Events are emitted under EasyAIEvent.Source.ASSISTANT. An assistant with no tools emits nothing.

      Familiar analogy: a live camera over the assistant's hands — you see each tool it reaches for and what came back, even though the final spoken answer arrives through the normal return value of your assistant method.

      
       SupportBot bot = EasyAI.assistant(SupportBot.class)
           .withTools(orderService)
           .withEventListener(e -> log.info("{}", e))   // tool_call → tool_result per tool
           .build();
       
      Parameters:
      eventListener - the listener to receive the assistant's tool-call event stream (may be null)
      Returns:
      this builder
      See Also:
    • build

      public T build()