Class AssistantBuilder<T>
- Type Parameters:
T- the assistant interface type
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 Summary
Modifier and TypeMethodDescriptionbuild()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).withAllPublicMethodsAsTools(Object... tools) Adds tool objects and exposes every public method on them to the AI — annotated or not.withApiKey(String apiKey) withChatMemory(dev.langchain4j.memory.ChatMemory chatMemory) Uses aChatMemoryinstance you own and keep, instead of letting the builder create a fresh one sized bywithMemory(int).withChatModel(dev.langchain4j.model.chat.ChatModel model) withEventListener(EasyAIListener eventListener) Registers a transport-agnosticEasyAIListenerthat receives a live stream ofEasyAIEvents as the assistant calls your tools.withMemory(int maxMessages) Connects this assistant to a persistent Milvus collection configured entirely fromeasyai.properties(keyseasyai.milvus.*).withMilvus(MilvusConfig config) Connects this assistant to a persistent Milvus collection using a fully builtMilvusConfig(for non-default dimension or credentials).withMilvus(MilvusConfig config, int maxResults, double minScore) Connects this assistant to a persistent Milvus collection with explicit retrieval tuning.withMilvus(String host, int port, String collectionName) Connects this assistant to a persistent Milvus collection for retrieval, using explicit connection settings.withRAG(DocumentSource... sources) Enables RAG from in-memory document sources (byte arrays).Enables RAG (document-powered AI) with the given document sources.Enables RAG with full control over retrieval parameters.withRAG(List<DocumentSource> sources, int maxResults, double minScore) Enables RAG from in-memory document sources with tuning parameters.withSystemMessage(String systemMessage) Adds tool objects whose@EasyTool-annotated methods the AI can call.
-
Method Details
-
withTools
Adds tool objects whose@EasyTool-annotated methods the AI can call.Opt-in (since 3.0.0): only methods you mark with
@EasyToolbecome tools. Every other method — including state-mutating ones likecancelOrderordeleteUser— 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
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@EasyToolon exactly the methods you intend.- Parameters:
tools- one or more service objects whose entire public surface becomes callable- Returns:
- this builder
- See Also:
-
withMemory
-
withChatMemory
Uses aChatMemoryinstance you own and keep, instead of letting the builder create a fresh one sized bywithMemory(int).Why this exists
withMemory(int)says "make me a memory of this size" — a brand-new, empty conversation buffer is created inside everybuild(). 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 atbuild()time, so changing them means rebuilding — and withwithMemory(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
MessageWindowChatMemorywith your own store, or any otherChatMemoryimplementation, 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. Passnullto 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;nullrestores thewithMemory(int)default- Returns:
- this builder
- See Also:
-
withSystemMessage
-
withModel
-
withApiKey
-
withRAG
Enables RAG (document-powered AI) with the given document sources.This is the programmatic alternative to the
@EasyRAGannotation. 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
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 pathsmaxResults- 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
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
Enables RAG from in-memory document sources with tuning parameters.- Parameters:
sources- document sources with content as byte arraysmaxResults- maximum number of relevant segments to retrieveminScore- minimum relevance score, 0.0 to 1.0- Returns:
- this builder
-
withMilvus
Connects this assistant to a persistent Milvus collection for retrieval, using explicit connection settings.This is the read-side counterpart to
EasyAI.indexer(). UnlikewithRAG(String...)— which loads and embeds documents fresh on everybuild()into an in-memory store —withMilvuspoints 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 usesmaxResults/minScoredefaults (3 / 0.5) unless you callwithMilvus(MilvusConfig, int, double).PolicyBot bot = EasyAI.assistant(PolicyBot.class) .withMilvus("localhost", 19530, "documents") .build();- Parameters:
host- Milvus server hostnameport- Milvus server port (typically 19530)collectionName- the collection to retrieve from- Returns:
- this builder
-
withMilvus
Connects this assistant to a persistent Milvus collection using a fully builtMilvusConfig(for non-default dimension or credentials).- Parameters:
config- the Milvus connection settings- Returns:
- this builder
-
withMilvus
Connects this assistant to a persistent Milvus collection with explicit retrieval tuning.- Parameters:
config- the Milvus connection settingsmaxResults- maximum relevant segments to retrieve per queryminScore- minimum relevance score, 0.0 to 1.0- Returns:
- this builder
-
withMilvus
Connects this assistant to a persistent Milvus collection configured entirely fromeasyai.properties(keyseasyai.milvus.*).- Returns:
- this builder
- See Also:
-
withChatModel
-
withActivityContext
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 activityThe context is combined with any
withSystemMessage(String)value bySystemMessageComposer; passingnullsimply leaves the feature off.- Parameters:
activityContext- the ambient-activity descriptor to inject, ornullto disable- Returns:
- this builder
- See Also:
-
withEventListener
Registers a transport-agnosticEasyAIListenerthat receives a live stream ofEasyAIEvents as the assistant calls your tools.This is the same observability hook the other capabilities expose (see
AgentBuilder.withEventListener(EasyAIListener)andFlowBuilder.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, aEasyAIEvent.Phase.STEP_STARTEDfires before it runs (a spinning row in a UI) and aEasyAIEvent.Phase.STEPfires after, withEasyAIEvent.Status.SUCCESSorEasyAIEvent.Status.ERROR. Events are emitted underEasyAIEvent.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 benull)- Returns:
- this builder
- See Also:
-
build
-