<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Prasad's Notes]]></title><description><![CDATA[Prasad's Notes]]></description><link>https://blog.prasadgaikwad.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 09:12:22 GMT</lastBuildDate><atom:link href="https://blog.prasadgaikwad.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[LangGraph4j ReACT Agent: Explicit State Graphs for Tool Orchestration]]></title><description><![CDATA[When building LLM applications, there's a spectrum between "just call tools automatically" and "I need to see and control every step of the reasoning loop." LangChain4j's built-in tool calling does th]]></description><link>https://blog.prasadgaikwad.dev/langgraph4j-react-agent-explicit-state-graphs-for-tool-orchestration</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/langgraph4j-react-agent-explicit-state-graphs-for-tool-orchestration</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sun, 23 Aug 2026 18:32:04 GMT</pubDate><content:encoded><![CDATA[<p>When building LLM applications, there's a spectrum between "just call tools automatically" and "I need to see and control every step of the reasoning loop." LangChain4j's built-in tool calling does the former. LangGraph4j does the latter — and the difference is more useful than you'd think.</p>
<h2>The ReACT Pattern</h2>
<p>ReACT (Reason + Act) is a simple loop: the LLM reasons about what to do, picks a tool, observes the result, and decides whether to continue or answer. Most frameworks hide this loop behind a single <code>chat()</code> call. LangGraph4j exposes it as a first-class state graph.</p>
<p>The graph structure:</p>
<pre><code class="language-plaintext">__START__ → agent (LLM reasons) → action (tool executes) → agent → ... → __END__
</code></pre>
<p>Each node is named, traceable, and inspectable. You can see exactly when the LLM decided to call a tool, what tool it called, what result it got, and how it decided to stop.</p>
<h2>What We Built</h2>
<p>We integrated LangGraph4j's <code>AgentExecutor</code> into the demo project as a new orchestration endpoint alongside the existing agentic-services patterns (supervisor, chain, parallel, loop, conditional).</p>
<pre><code class="language-java">@Service
public class ReactAgentService {

    private final CompiledGraph&lt;AgentExecutor.State&gt; compiledGraph;

    public ReactAgentService(ChatModel chatModel, 
                             CalculatorTool calculatorTool,
                             DocumentSearchTool documentSearchTool,
                             WeatherTool weatherTool,
                             EmbeddingStoreStatsTool storeStatsTool) throws GraphStateException {
        StateGraph&lt;AgentExecutor.State&gt; graph = AgentExecutor.builder()
                .chatModel(chatModel)
                .toolsFromObject(calculatorTool, documentSearchTool, weatherTool, storeStatsTool)
                .build();
        this.compiledGraph = graph.compile();
    }

    public ReactResult run(String task) {
        // Stream to capture each graph node transition
        var generator = compiledGraph.stream(Map.of("messages", UserMessage.from(task)));
        for (var item : generator) {
            steps.add(item.node());  // "agent", "action", "agent", ...
        }
        
        // Get final state
        var finalState = compiledGraph.invoke(Map.of("messages", UserMessage.from(task)));
        String answer = finalState.get().finalResponse().orElse("No response");
        
        return new ReactResult(task, answer, steps, allMessages);
    }
}
</code></pre>
<h2>How It Differs from Built-in Tool Calling</h2>
<table>
<thead>
<tr>
<th></th>
<th>LangChain4j Tool Calling</th>
<th>LangGraph4j AgentExecutor</th>
</tr>
</thead>
<tbody><tr>
<td>Control</td>
<td>Implicit loop</td>
<td>Explicit state graph</td>
</tr>
<tr>
<td>Traceability</td>
<td>Final result only</td>
<td>Every node transition visible</td>
</tr>
<tr>
<td>Extension point</td>
<td>Limited</td>
<td>Full graph modification</td>
</tr>
<tr>
<td>Mental model</td>
<td>"Call this function"</td>
<td>"Build a state machine"</td>
</tr>
</tbody></table>
<p>The explicit graph approach means you can:</p>
<ul>
<li><p><strong>Debug reasoning</strong> — see exactly why the LLM chose tool A over tool B</p>
</li>
<li><p><strong>Add guardrails</strong> — insert nodes between agent and action</p>
</li>
<li><p><strong>Build complex patterns</strong> — conditional branching, human-in-the-loop (issues #236, #237)</p>
</li>
<li><p><strong>Monitor performance</strong> — track which tools are called, how many loops, where bottlenecks occur</p>
</li>
</ul>
<h2>The Pitfalls</h2>
<p>LangGraph4j's API has some rough edges:</p>
<ol>
<li><p><code>toolsFromObject()</code> <strong>takes</strong> <code>Object...</code> — not <code>List&lt;Object&gt;</code>. Easy to get wrong.</p>
</li>
<li><p><code>AsyncGenerator</code> <strong>doesn't have</strong> <code>forEachRemaining()</code> — use a for-each loop instead.</p>
</li>
<li><p><code>AgentExecutor.State</code> <strong>messages include ALL intermediate steps</strong> — the final state has the complete conversation history including every reasoning step, not just the initial prompt and final answer.</p>
</li>
</ol>
<h2>Running It</h2>
<pre><code class="language-bash"># CLI
./mvnw spring-boot:run
/react compute 2+2

# REST
curl -X POST http://localhost:8080/api/react \
  -H "Content-Type: application/json" \
  -d '{"message":"What is the weather in Tokyo and what is 15% of 340?"}'
</code></pre>
<p>The response includes the full trace:</p>
<pre><code class="language-json">{
  "task": "What is the weather in Tokyo and what is 15% of 340?",
  "answer": "The weather in Tokyo is 22°C and partly cloudy. 15% of 340 is 51.",
  "steps": ["agent", "action", "agent", "action", "agent"],
  "agentMessages": ["I need to check the weather and calculate 15% of 340.", ...]
}
</code></pre>
<h2>What's Next</h2>
<p>This is the foundation for two more patterns:</p>
<ul>
<li><p><strong>Stateful Pipeline</strong> — persist graph state across invocations, resume from checkpoint</p>
</li>
<li><p><strong>Human-in-the-Loop</strong> — pause the graph for human approval before executing sensitive tools</p>
</li>
</ul>
<p>The LangGraph4j integration is the most powerful orchestration pattern we've added — but also the most opinionated. For simple tool use, stick with <code>@AiService</code>. When you need visibility, control, and extensibility, reach for the graph.</p>
]]></content:encoded></item><item><title><![CDATA[Workflow Composition: Parallel, Loop, and Conditional Patterns with LangChain4j]]></title><description><![CDATA[The orchestration trilogy is complete. After sequential chaining (#227) and goal-oriented graph planning (#228), this round adds the third pattern: workflow composition — building complex pipelines fr]]></description><link>https://blog.prasadgaikwad.dev/workflow-composition-parallel-loop-and-conditional-patterns-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/workflow-composition-parallel-loop-and-conditional-patterns-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sun, 23 Aug 2026 18:29:23 GMT</pubDate><content:encoded><![CDATA[<p>The orchestration trilogy is complete. After sequential chaining (#227) and goal-oriented graph planning (#228), this round adds the third pattern: <strong>workflow composition</strong> — building complex pipelines from parallel, loop, and conditional primitives.</p>
<h2>The Three Building Blocks</h2>
<p>LangChain4j's <code>agentic</code> module provides three workflow patterns that can be nested inside each other:</p>
<h3>Parallel (<code>parallelBuilder</code>)</h3>
<p>Runs sub-agents concurrently, then merges results via an <code>output()</code> function:</p>
<pre><code class="language-java">UntypedAgent parallelResearch = AgenticServices.&lt;String&gt;parallelBuilder()
        .subAgents(researchAgent1, researchAgent2)
        .outputKey("research")
        .output(scope -&gt; {
            String r1 = scope.readState("research1", "");
            String r2 = scope.readState("research2", "");
            return r1 + "\n\n" + r2;
        })
        .build();
</code></pre>
<h3>Loop (<code>loopBuilder</code>)</h3>
<p>Runs sub-agents repeatedly until an exit condition is met:</p>
<pre><code class="language-java">UntypedAgent refinementLoop = AgenticServices.&lt;String&gt;loopBuilder()
        .subAgents(qualityScorer, improveAgent)
        .maxIterations(3)
        .exitCondition(scope -&gt; scope.readState("score", 0.0) &gt;= 0.8)
        .build();
</code></pre>
<h3>Conditional (<code>conditionalBuilder</code>)</h3>
<p>Routes to different sub-agents based on a predicate:</p>
<pre><code class="language-java">UntypedAgent conditionalFormatter = AgenticServices.&lt;String&gt;conditionalBuilder()
        .subAgents(
                scope -&gt; "technical".equals(scope.readState("category", "")),
                technicalFormat)
        .subAgents(
                scope -&gt; !"technical".equals(scope.readState("category", "")),
                generalFormat)
        .build();
</code></pre>
<h2>The Demo Pipeline</h2>
<p>The workflow generates a blog post with four phases:</p>
<pre><code class="language-mermaid">flowchart TD
    subgraph P["Phase 1: Parallel Research"]
        R1[ResearchAgent1&lt;br/&gt;research1]
        R2[ResearchAgent2&lt;br/&gt;research2]
    end

    subgraph L["Phase 3: Refinement Loop"]
        QS[QualityScorerAgent&lt;br/&gt;score]
        IA[ImproveAgent&lt;br/&gt;draft]
    end

    P --&gt; D[Phase 2: DraftAgent&lt;br/&gt;draft]
    D --&gt; L
    L --&gt; CA[Phase 4: CategoryAgent&lt;br/&gt;category]
    CA --&gt;|technical| TF[TechnicalFormatAgent&lt;br/&gt;formatted]
    CA --&gt;|general| GF[GeneralFormatAgent&lt;br/&gt;formatted]
</code></pre>
<table>
<thead>
<tr>
<th>Agent</th>
<th>Reads</th>
<th>Writes</th>
<th>Pattern</th>
</tr>
</thead>
<tbody><tr>
<td><code>ResearchAgent1</code></td>
<td><code>topic</code></td>
<td><code>research1</code></td>
<td>parallel</td>
</tr>
<tr>
<td><code>ResearchAgent2</code></td>
<td><code>topic</code></td>
<td><code>research2</code></td>
<td>parallel</td>
</tr>
<tr>
<td><code>WorkflowDraftAgent</code></td>
<td><code>topic</code>, <code>research1</code>, <code>research2</code></td>
<td><code>draft</code></td>
<td>sequential</td>
</tr>
<tr>
<td><code>QualityScorerAgent</code></td>
<td><code>draft</code></td>
<td><code>score</code></td>
<td>loop</td>
</tr>
<tr>
<td><code>ImproveAgent</code></td>
<td><code>draft</code></td>
<td><code>draft</code></td>
<td>loop</td>
</tr>
<tr>
<td><code>CategoryAgent</code></td>
<td><code>topic</code></td>
<td><code>category</code></td>
<td>sequential</td>
</tr>
<tr>
<td><code>TechnicalFormatAgent</code></td>
<td><code>draft</code></td>
<td><code>formatted</code></td>
<td>conditional</td>
</tr>
<tr>
<td><code>GeneralFormatAgent</code></td>
<td><code>draft</code></td>
<td><code>formatted</code></td>
<td>conditional</td>
</tr>
</tbody></table>
<p>All four patterns compose in a single sequence:</p>
<pre><code class="language-java">this.pipeline = AgenticServices.sequenceBuilder()
        .subAgents(parallelResearch, draftAgent, refinementLoop, categoryAgent)
        .subAgents(conditionalFormatter)
        .outputKey("formatted")
        .build();
</code></pre>
<h2>Gotchas</h2>
<p>Three things worth noting:</p>
<ol>
<li><p><strong>Builders must call</strong> <code>.build()</code><strong>.</strong> <code>parallelBuilder()</code>, <code>loopBuilder()</code>, and <code>conditionalBuilder()</code> return builder objects. You must call <code>.build()</code> to get the <code>UntypedAgent</code> that can be passed to <code>sequenceBuilder().subAgents()</code>. Forgetting <code>.build()</code> causes a cryptic "No agent method found" error.</p>
</li>
<li><p><strong>Parallel output() merges scope keys.</strong> The <code>output()</code> function on <code>parallelBuilder</code> reads sub-agent outputs from the scope and combines them. Set <code>outputKey()</code> to write the merged result back to the scope.</p>
</li>
<li><p><strong>Loop exitCondition is checked after each iteration.</strong> The predicate receives the live <code>AgenticScope</code>. The loop runs sub-agents sequentially within each iteration — so if you have <code>[scorer, improver]</code>, iteration 1 runs scorer then improver, and iteration 2 runs scorer again (checking the exit condition).</p>
</li>
</ol>
<h2>What We Learned</h2>
<p>The three workflow patterns cover the common orchestration cases:</p>
<ul>
<li><p><strong>Parallel</strong> for independent work that can run concurrently (research, brainstorming)</p>
</li>
<li><p><strong>Loop</strong> for iterative refinement (quality scoring, style review)</p>
</li>
<li><p><strong>Conditional</strong> for branching logic (formatting, routing)</p>
</li>
<li><p><strong>Sequence</strong> for chaining them together</p>
</li>
</ul>
<p>The key insight is that all three are composable. A loop can wrap a parallel workflow. A conditional can branch to a parallel or loop sub-graph. The <code>sequenceBuilder</code> orchestrates them in order.</p>
<p>This completes the orchestration feature set for the demo: crew (supervisor delegation), chain (sequential pipeline), graph (GOAP planning), and workflow (parallel/loop/conditional composition).</p>
<hr />
<p><strong>Next up:</strong> The demo now covers all core orchestration patterns. The remaining open idea is <strong>parallel agent execution</strong> — running multiple agents concurrently and merging their results, which is partially covered by the parallel pattern here.</p>
]]></content:encoded></item><item><title><![CDATA[Graph of Agents: Goal-Oriented Action Planning with LangChain4j]]></title><description><![CDATA[The previous round added two orchestration patterns: a supervisor-based crew (parallel delegation) and a chain-of-agents pipeline (sequential delegation). Both require you to know the execution order ]]></description><link>https://blog.prasadgaikwad.dev/graph-of-agents-goal-oriented-action-planning-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/graph-of-agents-goal-oriented-action-planning-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Fri, 21 Aug 2026 03:06:14 GMT</pubDate><content:encoded><![CDATA[<p>The previous round added two orchestration patterns: a supervisor-based crew (parallel delegation) and a chain-of-agents pipeline (sequential delegation). Both require you to know the execution order in advance. This round fills the gap with a <strong>goal-oriented agent graph</strong> — a pattern where typed sub-agents declare their required inputs and produced outputs, and a planner computes the shortest execution path automatically.</p>
<h2>What Is GOAP?</h2>
<p>GOAP stands for <strong>Goal-Oriented Action Planning</strong>. In LangChain4j, the <code>GoalOrientedPlanner</code> (from the <code>langchain4j-agentic-patterns</code> module) analyzes the dependency graph of your agents and uses A* search to find the shortest path from the current scope state to the goal.</p>
<p>Each agent declares:</p>
<ul>
<li><p><strong>Preconditions</strong> — what it needs from the scope (via <code>@V("key")</code> parameters)</p>
</li>
<li><p><strong>Postconditions</strong> — what it writes to the scope (via <code>.outputKey()</code>)</p>
</li>
</ul>
<p>The planner builds a directed graph from these declarations, then finds the shortest path that satisfies all dependencies. No LLM call is needed for planning — it's pure graph search.</p>
<h2>The Pipeline</h2>
<p>The demo uses a personalized blog post generation pipeline with six agents:</p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>Reads (preconditions)</th>
<th>Writes (postcondition)</th>
</tr>
</thead>
<tbody><tr>
<td><code>ExtractProfileAgent</code></td>
<td><code>prompt</code></td>
<td><code>profile</code></td>
</tr>
<tr>
<td><code>TopicSuggestionAgent</code></td>
<td><code>profile</code></td>
<td><code>topic</code></td>
</tr>
<tr>
<td><code>TopicOutlineAgent</code></td>
<td><code>topic</code></td>
<td><code>outline</code></td>
</tr>
<tr>
<td><code>TopicDraftAgent</code></td>
<td><code>topic</code>, <code>outline</code></td>
<td><code>draft</code></td>
</tr>
<tr>
<td><code>TopicEditorAgent</code></td>
<td><code>draft</code></td>
<td><code>edited</code></td>
</tr>
<tr>
<td><code>TopicWriteupAgent</code></td>
<td><code>profile</code>, <code>topic</code>, <code>outline</code>, <code>edited</code></td>
<td><code>writeup</code></td>
</tr>
</tbody></table>
<p>The goal is <code>writeup</code>. Given an initial scope containing <code>prompt</code>, the planner computes the shortest path:</p>
<pre><code class="language-plaintext">prompt → profile → topic → outline → draft → edited → writeup
</code></pre>
<pre><code class="language-mermaid">flowchart LR
    P["prompt"] --&gt; EA["ExtractProfile&lt;br/&gt;profile"]
    EA --&gt; TS["TopicSuggestion&lt;br/&gt;topic"]
    TS --&gt; TO["TopicOutline&lt;br/&gt;outline"]
    TS --&gt; TD["TopicDraft&lt;br/&gt;draft"]
    TO --&gt; TD
    TD --&gt; TE["TopicEditor&lt;br/&gt;edited"]
    TE --&gt; TW["TopicWriteup&lt;br/&gt;writeup"]
</code></pre>
<h2>The Code</h2>
<p>Each agent is a plain Java interface:</p>
<pre><code class="language-java">@SystemMessage("You are a profile extractor...")
@UserMessage("Extract a short profile from the following prompt...{{prompt}}")
public interface ExtractProfileAgent {
    @Agent(outputKey = "profile", description = "Extracts a user profile from a prompt")
    String extractProfile(@V("prompt") String prompt);
}
</code></pre>
<p>The <code>TopicWriteupAgent</code> reads four scope keys and writes the final output:</p>
<pre><code class="language-java">@SystemMessage("You are a blog post formatter...")
@UserMessage("""
        Create a personalized blog post using the following:
        - User profile: {{profile}}
        - Topic: {{topic}}
        - Outline: {{outline}}
        - Edited content: {{edited}}
        """)
public interface TopicWriteupAgent {
    @Agent(outputKey = "writeup", description = "Formats a personalized blog post")
    String createWriteup(@V("profile") String profile,
                         @V("topic") String topic,
                         @V("outline") String outline,
                         @V("edited") String edited);
}
</code></pre>
<p>The pipeline is assembled in <code>GraphOfAgentsService</code>:</p>
<pre><code class="language-java">this.pipeline = AgenticServices.plannerBuilder()
        .subAgents(extractProfile, topicSuggestion, outline, draft, editor, writeup)
        .outputKey("writeup")
        .planner(GoalOrientedPlanner::new)
        .build();
</code></pre>
<p>That's it. The planner does the rest.</p>
<h2>How the Planner Works</h2>
<p>When <code>pipeline.invoke(Map.of("prompt", "..."))</code> is called:</p>
<ol>
<li><p><strong>Graph construction.</strong> The <code>GoalOrientedSearchGraph</code> iterates all sub-agents, extracts their <code>@V</code> parameter names (preconditions) and <code>.outputKey()</code> values (postconditions), and builds a directed graph.</p>
</li>
<li><p><strong>Path search.</strong> The <code>DependencyGraphSearch</code> (A* algorithm) starts from the initial scope state (<code>{prompt}</code>) and finds the shortest path to the goal node (<code>writeup</code>).</p>
</li>
<li><p><strong>Execution.</strong> The planner invokes each agent in the computed order, writing results to the scope as it goes. Each agent's <code>@V</code> parameters are automatically populated from the scope.</p>
</li>
<li><p><strong>Result.</strong> The final output is the value of the goal key (<code>writeup</code>) from the scope.</p>
</li>
</ol>
<p>The key insight: <strong>the planner is algorithmic, not LLM-driven.</strong> No LLM call is needed to decide which agent runs next. The graph search is deterministic and fast.</p>
<h2>Gotchas</h2>
<p>Three things worth noting:</p>
<ol>
<li><p><code>langchain4j-agentic-patterns</code> <strong>is a separate module.</strong> It has its own <code>-betaNN</code> version and must be added to <code>pom.xml</code> alongside <code>langchain4j-agentic</code>. The GOAP classes live in <code>dev.langchain4j.agentic.patterns.goap</code>.</p>
</li>
<li><p><code>plannerBuilder()</code> <strong>has no</strong> <code>chatModel()</code> <strong>method.</strong> The <code>chatModel</code> is set on each individual agent builder, not on the planner builder. The planner is purely an orchestration layer — it doesn't invoke any LLM itself.</p>
</li>
<li><p><strong>Adding a new agent is declarative.</strong> Just add a new interface with <code>@V</code> parameters and <code>outputKey</code>, build it with <code>agentBuilder()</code>, and add it to the <code>plannerBuilder().subAgents(...)</code> list. The planner automatically discovers its dependencies and recalculates the path.</p>
</li>
</ol>
<h2>What We Learned</h2>
<p>The GOAP pattern completes the orchestration trilogy: crew (supervisor delegation), chain (sequential pipeline), and graph (goal-oriented planning). The key difference is that the graph pattern doesn't require you to specify the execution order — you just declare what each agent needs and produces, and the planner figures out the rest.</p>
<p>This is powerful when the pipeline might vary depending on what's already available. If the initial scope already contains a <code>topic</code>, the planner automatically skips <code>ExtractProfileAgent</code> and <code>TopicSuggestionAgent</code>. The same agents work regardless of which scope keys are pre-populated.</p>
<p>The <code>langchain4j-agentic-patterns</code> module also includes BDI, Blackboard, Debate, P2P, and Voting patterns — all built on the same <code>Planner</code> abstraction. Each pattern is composable: a GOAP sub-graph can wrap a loop, a conditional, or a parallel workflow as one of its agents.</p>
<hr />
<p><strong>Next up:</strong> The demo now has all three core orchestration patterns. The remaining open idea is <strong>parallel agent execution</strong> — running multiple agents concurrently and merging their results.</p>
]]></content:encoded></item><item><title><![CDATA[Chain of Agents: Sequential Prompt Chaining with LangChain4j]]></title><description><![CDATA[The previous round left the demo with two open ideas in its "Future Experiments" list: chain of agents (sequential prompt chaining) and streaming function calling (which shipped alongside the crew). E]]></description><link>https://blog.prasadgaikwad.dev/chain-of-agents-sequential-prompt-chaining-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/chain-of-agents-sequential-prompt-chaining-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Thu, 20 Aug 2026 02:26:50 GMT</pubDate><content:encoded><![CDATA[<p>The previous round left the demo with two open ideas in its "Future Experiments" list: <strong>chain of agents</strong> (sequential prompt chaining) and <strong>streaming function calling</strong> (which shipped alongside the crew). Everything before this ran agents either through a single AI service call or through a supervisor that routes to one worker at a time. This round fills the gap: a deterministic, ordered pipeline where each stage feeds its output into the next via a shared <code>AgenticScope</code>.</p>
<h2>What Is a Chain of Agents?</h2>
<p>LangChain4j's <code>AgenticServices.sequenceBuilder()</code> creates an <code>UntypedAgent</code> from a list of sub-agents that execute in order. Each sub-agent reads its input from the shared scope, writes its result to a named key, and the next agent picks up where the last one left off. There is no routing decision, no supervisor choosing which worker to call — it is a fixed, linear pipeline.</p>
<p>This is useful when the stages are known in advance and each one depends on the previous stage's output. The demo uses a blog-post generation pipeline:</p>
<ol>
<li><p><strong>OutlineAgent</strong> — takes a topic, produces a structured outline (<code>outputKey = "outline"</code>)</p>
</li>
<li><p><strong>DraftAgent</strong> — takes the outline, writes a full draft (<code>outputKey = "draft"</code>)</p>
</li>
<li><p><strong>EditorAgent</strong> — takes the draft, edits for clarity and flow (<code>outputKey = "edited"</code>)</p>
</li>
<li><p><strong>FormatAgent</strong> — takes the edited text, formats into a publish-ready Markdown post (<code>outputKey = "formatted"</code>)</p>
</li>
</ol>
<pre><code class="language-mermaid">flowchart LR
    T["topic input"] --&gt; OA["OutlineAgent&lt;br/&gt;outputKey=outline"]
    OA --&gt; DA["DraftAgent&lt;br/&gt;outputKey=draft"]
    DA --&gt; EA["EditorAgent&lt;br/&gt;outputKey=edited"]
    EA --&gt; FA["FormatAgent&lt;br/&gt;outputKey=formatted"]
    FA --&gt; OUT["formatted blog post"]
</code></pre>
<h2>The Code</h2>
<p>Each sub-agent is a plain Java interface with <code>@SystemMessage</code>, <code>@UserMessage</code>, and <code>@Agent(outputKey = "...")</code>:</p>
<pre><code class="language-java">@SystemMessage("You are a blog post outline specialist. Create a clear, structured outline "
        + "with a title, introduction, 3-5 main sections, and a conclusion.")
@UserMessage("Create a blog post outline for the following topic.\nTopic: {{topic}}")
public interface OutlineAgent {
    @Agent(outputKey = "outline", description = "Creates a structured blog post outline")
    String createOutline(@V("topic") String topic);
}
</code></pre>
<p><code>DraftAgent</code>, <code>EditorAgent</code>, and <code>FormatAgent</code> follow the same pattern, each with its own <code>outputKey</code> and system message tailored to its role.</p>
<p>The pipeline is assembled in <code>ChainOfAgentsService</code>:</p>
<pre><code class="language-java">OutlineAgent outlineAgent = AgenticServices.agentBuilder(OutlineAgent.class)
        .chatModel(chatModel).build();
DraftAgent draftAgent = AgenticServices.agentBuilder(DraftAgent.class)
        .chatModel(chatModel).build();
EditorAgent editorAgent = AgenticServices.agentBuilder(EditorAgent.class)
        .chatModel(chatModel).build();
FormatAgent formatAgent = AgenticServices.agentBuilder(FormatAgent.class)
        .chatModel(chatModel).build();

this.pipeline = AgenticServices.sequenceBuilder()
        .subAgents(outlineAgent, draftAgent, editorAgent, formatAgent)
        .outputKey("formatted")
        .build();
</code></pre>
<p>Two details:</p>
<ul>
<li><p><strong>Each sub-agent is built independently</strong> with its own <code>AgenticServices.agentBuilder()</code> call, then composed via <code>sequenceBuilder()</code>. This is different from the crew, where the supervisor and sub-agents are built together.</p>
</li>
<li><p><code>outputKey("formatted")</code> tells the pipeline which scope key becomes the top-level return value of <code>pipeline.invoke()</code>.</p>
</li>
</ul>
<p>To get the full trace (all intermediate outputs), use <code>invokeWithAgenticScope()</code>:</p>
<pre><code class="language-java">ResultWithAgenticScope&lt;String&gt; result =
        pipeline.invokeWithAgenticScope(Map.of("topic", topic));
String formatted = result.result();
String outline = result.agenticScope().readState("outline", (String) null);
String draft = result.agenticScope().readState("draft", (String) null);
</code></pre>
<p>The REST endpoint (<code>POST /api/chain</code>) and CLI command (<code>/chain &lt;topic&gt;</code>) both return the full trace, not just the final output.</p>
<h2>Gotchas</h2>
<p>Three things tripped us up during implementation:</p>
<ol>
<li><p><code>@SystemMessage</code> <strong>and</strong> <code>@UserMessage</code> <strong>go on the method, not the interface.</strong> Placing them at the interface level produces a compile error: <code>annotation interface not applicable to this kind of declaration</code>. This is the same rule as for crew sub-agents, but it is easy to forget when the interface has only one method.</p>
</li>
<li><p><code>UntypedAgent.invoke()</code> <strong>returns</strong> <code>Object</code><strong>, not</strong> <code>Map&lt;String, Object&gt;</code><strong>.</strong> The return value is the value of the configured <code>outputKey</code>, not the full scope map. To access intermediate outputs, you must use <code>invokeWithAgenticScope()</code> and <code>agenticScope().readState(key)</code>.</p>
</li>
<li><p><strong>Scope keys must be unique across all sub-agents.</strong> Each agent's <code>outputKey</code> writes to a different key in the shared scope. If two agents share a key, the second one silently overwrites the first.</p>
</li>
</ol>
<h2>Testing</h2>
<p>The <code>ChainOfAgentsServiceTest</code> uses a <code>ScriptedSequenceChatModel</code> — a fake <code>ChatModel</code> that returns canned responses in order, one per agent invocation. This makes the test fully offline with no API calls:</p>
<pre><code class="language-java">ScriptedSequenceChatModel chatModel = new ScriptedSequenceChatModel(
        List.of(OUTLINE, DRAFT, EDITED, FORMATTED));
ChainOfAgentsService service = new ChainOfAgentsService(chatModel);

ChainPipelineResult result = service.runWithTrace("Test Topic");

assertThat(result.outline()).isEqualTo(OUTLINE);
assertThat(result.draft()).isEqualTo(DRAFT);
assertThat(result.edited()).isEqualTo(EDITED);
assertThat(result.formatted()).isEqualTo(FORMATTED);
</code></pre>
<h2>What We Learned</h2>
<p>The <code>sequenceBuilder()</code> fills a real gap between "one agent, one call" and "supervisor with routing." When the pipeline is fixed — outline, draft, edit, format — there is no need for a supervisor to decide which agent runs next. The shared <code>AgenticScope</code> is the glue: each agent writes its result, the next agent reads it, and the final <code>outputKey</code> gives you the finished product.</p>
<p>This also reinforces the value of the <code>ModelRegistry</code> abstraction. Every agent in the chain shares the same <code>ChatModel</code> bean, so switching providers at runtime switches the entire pipeline in one move. The registry keeps paying for itself.</p>
<hr />
<p><strong>Next up:</strong> The demo now has a supervisor-based crew (parallel delegation) and a chain-of-agents pipeline (sequential delegation). The remaining open idea is <strong>parallel agent execution</strong> — running multiple agents concurrently and merging their results.</p>
]]></content:encoded></item><item><title><![CDATA[Advanced Orchestration: An Agentic Crew and Streaming Function Calling]]></title><description><![CDATA[The previous round left the demo with one open idea in its "Future Experiments" list: advanced agent orchestration with LangChain4j's agentic module, plus streaming function calling. Everything before]]></description><link>https://blog.prasadgaikwad.dev/advanced-orchestration-an-agentic-crew-and-streaming-function-calling</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/advanced-orchestration-an-agentic-crew-and-streaming-function-calling</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Wed, 19 Aug 2026 03:05:45 GMT</pubDate><content:encoded><![CDATA[<p>The previous round left the demo with one open idea in its "Future Experiments" list: <strong>advanced agent orchestration</strong> with LangChain4j's <code>agentic</code> module, plus <strong>streaming function calling</strong>. Everything before this ran agents through plain <code>AiServices</code> with <code>@Tool</code> methods — a single agent, one tool-calling round, one final answer. This round adds a real multi-agent system: a <em>supervisor</em> that decides which specialized sub-agent should handle a task, and a streaming AI service that can call tools while tokens are still flowing.</p>
<h2>The Two Experiments</h2>
<ol>
<li><p><strong>The crew.</strong> The <code>agentic</code> module (currently <code>1.18.0-beta28</code>) provides <code>AgenticServices.agentBuilder()</code> for standalone agents and <code>AgenticServices.supervisorBuilder()</code> for a supervisor that delegates to sub-agents. It isn't in the BOM, so <code>pom.xml</code> pins its own <code>-betaNN</code> version explicitly; it depends on <code>langchain4j</code> <code>1.18.0</code>, which the BOM's <code>1.18.1</code> overrides.</p>
</li>
<li><p><strong>Streaming function calling.</strong> <code>AiServices</code> already supports streaming services — a <code>TokenStream</code> returned from the method, tokens arriving via <code>onPartialResponse</code>. Binding <code>@Tool</code>s to a streaming service is no different from binding them to a blocking one. The two compose: <code>StreamingAgent.chat(...)</code> returns a <code>TokenStream</code>, and the tool is still available to the model mid-generation.</p>
</li>
</ol>
<h2>CrewService: A Supervisor With Three Workers</h2>
<p><code>CrewService</code> wires a supervisor to three sub-agents, each bound to one of the demo's existing <code>@Tool</code>s — the calculator, the weather lookup, and the document research tool:</p>
<pre><code class="language-java">CrewTaskAgent calculatorAgent = buildAgent(
        "Calculator", "Useful for arithmetic and any kind of math. Delegate calculations here.",
        chatModel, calculatorTool);
CrewTaskAgent weatherAgent = buildAgent(
        "Weather", "Useful for current weather in known cities. Delegate weather questions here.",
        chatModel, weatherTool);
CrewTaskAgent researchAgent = buildAgent(
        "Research", "Useful for questions about the indexed documents. Delegate document questions here.",
        chatModel, documentSearchTool);

this.supervisor = AgenticServices.supervisorBuilder()
        .name("Crew")
        .supervisorContext("You are the crew supervisor. Decide which agent is best suited for the task ...")
        .chatModel(chatModel)
        .chatMemoryProvider(memoryProvider)
        .subAgents(calculatorAgent, weatherAgent, researchAgent)
        .responseStrategy(SupervisorResponseStrategy.LAST)
        .maxAgentsInvocations(10)
        .build();
</code></pre>
<p>Two details worth highlighting:</p>
<ul>
<li><p><strong>Every agent shares one</strong> <code>ChatModel</code> — the <code>ModelRegistry</code> from the LLM-integration round. A sub-agent is just a <code>ChatModel</code> consumer, so switching providers with <code>/model chat</code> switches the whole crew in one move. The registry abstraction keeps paying for itself.</p>
</li>
<li><p><strong>Sub-agents must be typed.</strong> The supervisor hands a sub-agent an <code>arguments</code> map whose keys are matched to the sub-agent's parameters <em>by name</em>. That only works when the sub-agent declares its input as a <code>@V</code> parameter on a typed method with a <code>@UserMessage</code> template:</p>
</li>
</ul>
<pre><code class="language-java">public interface CrewTaskAgent {

    @SystemMessage("You are a specialist agent. Complete the task delegated to you using the tools "
            + "available. Return the final answer and nothing else.")
    @UserMessage("You have been delegated the following task. Use the tools available to complete it.\n"
            + "Delegated task: {{task}}")
    @Agent
    String run(@V("task") String task);
}
</code></pre>
<p>It was tempting to build sub-agents as untyped <code>AgenticServices.agentBuilder()</code> with a <code>userMessageProvider</code> extracting the <code>task</code> key. That fails silently: the provider never receives the delegated map (it gets the default memory id instead), so the worker ends up answering the literal string "default". The typed contract makes the hand-off explicit — the supervisor's <code>{"task": ...}</code> argument becomes the <code>run(...)</code> argument, and the template builds the worker's prompt from it.</p>
<h3>How the supervisor actually works</h3>
<p>The supervisor does <strong>not</strong> hand the model a JSON-tools list of its sub-agents. Its <code>SupervisorPlanner</code> asks the model for a decision in a specific shape — an <code>AgentInvocation</code> POJO with <code>agentName</code> and <code>arguments</code> — and the runtime invokes the matching sub-agent:</p>
<ul>
<li><p>Model returns <code>{"agentName": "Weather", "arguments": {...}}</code> → the Weather sub-agent runs, calls the model itself (and, if needed, its <code>getWeather</code> tool).</p>
</li>
<li><p>Model returns <code>{"agentName": "done"}</code> → the planner stops; with <code>SupervisorResponseStrategy.LAST</code>, the final answer is the last sub-agent's output.</p>
</li>
</ul>
<p>That protocol is what made the tests deterministic (more below).</p>
<h2>Streaming Function Calling</h2>
<p><code>StreamingAgent</code> is a one-method AI service:</p>
<pre><code class="language-java">public interface StreamingAgent {
    TokenStream chat(@MemoryId String memoryId, @UserMessage String message);
}
</code></pre>
<p>Its bean binds the existing <code>OpenAiStreamingChatModel</code> and the <code>CalculatorTool</code>:</p>
<pre><code class="language-java">AiServices.builder(StreamingAgent.class)
        .streamingChatModel(streamingChatModel)
        .chatMemoryProvider(createChatMemoryProvider(...))
        .tools(calculatorTool)
        .build();
</code></pre>
<p>The <code>/stream</code> command prints tokens as they arrive; the model can still decide to call <code>calculate</code> before producing its final text.</p>
<h2>The CLI Commands</h2>
<p>Two new commands complete the surface:</p>
<pre><code class="language-plaintext">/crew &lt;task&gt;     Execute a task with the agentic supervisor crew
/stream &lt;task&gt;   Stream a task with streaming function calling
</code></pre>
<pre><code class="language-java">TokenStream tokenStream = streamingAgent.chat(memoryId, task);
tokenStream
        .onPartialResponse(System.out::print)
        .onCompleteResponse(response -&gt; System.out.println())
        .onError(error -&gt; System.out.println("Stream error: " + error.getMessage()))
        .start();
</code></pre>
<h2>Testing It Offline</h2>
<p>Three new tests, all offline and deterministic:</p>
<ul>
<li><p><strong>Crew delegation protocol.</strong> A <code>ScriptedSupervisorChatModel</code> returns the three model replies in order: <code>{"agentName":"Weather",...}</code> (supervisor delegates), then a plain sub-agent answer, then <code>{"agentName":"done"}</code> (supervisor wraps up). The test asserts the returned answer is the sub-agent's text and that the supervisor made at least three calls.</p>
</li>
<li><p><strong>Delegated task delivery.</strong> The recorded second request — the Weather sub-agent's call — must contain the delegated task ("weather report") in its user message, proving the supervisor's <code>arguments</code> really reach the worker's prompt.</p>
</li>
<li><p><strong>Sub-agent tool exposure.</strong> The same recorded second request must carry the <code>getWeather</code> tool specification, proving the <code>@Tool</code> really is bound inside the crew.</p>
</li>
<li><p><strong>Streaming tokens + tools.</strong> A <code>FakeStreamingChatModel</code> (extended with <code>lastRequest()</code>) emits three tokens; the test collects them through <code>onPartialResponse</code>, asserts the full text on <code>onCompleteResponse</code>, and verifies the request's tool specifications contain <code>calculate</code>.</p>
</li>
</ul>
<p>Full suite: <strong>106 tests</strong>, all green.</p>
<h2>Design Notes</h2>
<ul>
<li><p><strong>The supervisor is a small protocol, not a tool call.</strong> Understanding that the model's output is parsed into an <code>AgentInvocation</code> was the key to both the implementation and the tests. Get the shape wrong and you get <code>OutputParsingException: Failed to parse ... into AgentInvocation</code>.</p>
</li>
<li><p><strong>Arguments are matched to sub-agent parameters by name.</strong> That is why the worker must be typed with <code>@V("task")</code>; a generic untyped <code>userMessageProvider</code> never receives the delegated arguments. The regression test asserting the delegated text in the worker's prompt would have caught this earlier.</p>
</li>
<li><p><code>LAST</code> <strong>keeps the loop short.</strong> <code>SupervisorResponseStrategy.LAST</code> returns the delegated result directly instead of scoring or summarizing it, which also makes the test's call sequence stable.</p>
</li>
<li><p><strong>Shared model = coherent crew.</strong> Because sub-agents take a <code>ChatModel</code>, the whole crew inherits runtime provider switching for free.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>The checklist and the "Future Experiments" list are now empty: memory, embeddings, RAG, agents, prompting, integration, tooling, evaluation, advanced features, multi-provider LLM integration, and now agentic orchestration plus streaming function calling. A natural follow-up is giving the crew more sub-agents (e.g., an image or audio worker) and a plan-then-execute workflow — the <code>agentic</code> module keeps growing.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://github.com/langchain4j/langchain4j/tree/main/langchain4j-agentic">LangChain4j agentic module</a></p>
</li>
<li><p><a href="https://docs.langchain4j.dev/tutorials/ai-services">LangChain4j Streaming</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/220">Advanced Orchestration Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>The last item on the board turned out to be the most mechanically involved. The <code>agentic</code> module moves orchestration out of your prompt and into typed primitives: agents as builders, delegation as a parsed decision, and a supervisor loop that knows when to stop. Combined with streaming function calling, the demo now shows both ends of the spectrum — a single streaming tool-calling round and a multi-agent crew that plans its own hand-off. And because everything shares the <code>ModelRegistry</code>, one command switches the entire crew to a different provider or model.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[LLM Integration: Multiple Providers, Runtime Switching, and Model Comparison]]></title><description><![CDATA[The demo's checklist has one item left: LLM integration — connect with different providers (Anthropic, Google, local models via Ollama), experiment with different models, and compare their performance]]></description><link>https://blog.prasadgaikwad.dev/llm-integration-multiple-providers-runtime-switching-and-model-comparison</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/llm-integration-multiple-providers-runtime-switching-and-model-comparison</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Wed, 19 Aug 2026 02:56:54 GMT</pubDate><content:encoded><![CDATA[<p>The demo's checklist has one item left: <strong>LLM integration</strong> — connect with different providers (Anthropic, Google, local models via Ollama), experiment with different models, and compare their performance. Previous rounds built a dependable yardstick (<code>/eval</code> with golden datasets) and a single, interface-based chat model wired into every AI service. This round exploits both: put a <em>registry</em> behind that one interface, and reuse the evaluation harness to compare models head-to-head.</p>
<h2>The Problem: One Bean, Many Providers</h2>
<p>Until now every AI service (assistant, RAG, agent, few-shot, judge) injected a <code>ChatModel</code> bean built from OpenAI only. Adding providers the obvious way — more <code>@Bean</code> methods, more constructor parameters — would ripple through the whole app. Instead, LangChain4j models all share one interface, <code>ChatModel</code>. So we can keep exactly one <code>ChatModel</code> bean and make it a <em>delegating registry</em>: whichever <code>provider:model</code> is selected is the one that actually answers every service's calls.</p>
<h3>LlmProvider</h3>
<p>A small enum encodes the four providers and each one's default model:</p>
<pre><code class="language-java">public enum LlmProvider {
    OPENAI("openai", "gpt-4o-mini"),
    ANTHROPIC("anthropic", "claude-haiku-4-5-20251001"),
    GEMINI("gemini", "gemini-2.5-flash"),
    OLLAMA("ollama", "llama3.2");
    // label() + defaultModelName() + fromLabel(String)
}
</code></pre>
<h3>ModelRegistry</h3>
<p>The registry implements <code>ChatModel</code>, holds the current selection, and lazily builds the real model on first use — cached per <code>provider:model</code> so switching back is free and starting the app never requires a key for a provider you aren't using:</p>
<pre><code class="language-java">public ChatModel currentChatModel() {
    return models.computeIfAbsent(key(currentProvider, currentModelName),
            ignored -&gt; buildChatModel(currentProvider, currentModelName));
}

private ChatModel buildChatModel(LlmProvider provider, String modelName) {
    return switch (provider) {
        case OPENAI    -&gt; OpenAiChatModel.builder().apiKey(System.getenv("OPENAI_API_KEY")).modelName(modelName).build();
        case ANTHROPIC -&gt; AnthropicChatModel.builder().apiKey(System.getenv("ANTHROPIC_API_KEY")).modelName(modelName).build();
        case GEMINI    -&gt; GoogleAiGeminiChatModel.builder().apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY")).modelName(modelName).build();
        case OLLAMA    -&gt; OllamaChatModel.builder().baseUrl(ollamaBaseUrl).modelName(modelName).build();
    };
}
</code></pre>
<p>Each provider is one case. The registry also reports which models are <em>available</em> — every provider whose key is present in the environment, plus Ollama, which is local. That list drives the comparison.</p>
<p>Because the registry is a <code>ChatModel</code> and the <em>only</em> <code>ChatModel</code> bean, <code>AiConfig</code> shrinks: the old OpenAI-specific bean disappears and services keep injecting <code>ChatModel</code> unchanged. Switching the registry switches the entire pipeline at once — including the LLM-as-a-judge metric used by <code>/eval</code>.</p>
<h2>Runtime Switching in the CLI</h2>
<p><code>/model</code> now covers both kinds of model. Chat switching accepts a provider with or without a model name:</p>
<pre><code class="language-plaintext">/model
  Chat model     : openai:gpt-4o-mini
  Embedding model: text-embedding-3-small

/model chat anthropic
  Switched chat model to 'anthropic:claude-haiku-4-5-20251001'. Every AI service now uses this model.

/model chat gemini:gemini-2.5-flash
</code></pre>
<p>A typo prints the available models with their status (<code>ready</code>, <code>no api key</code>, <code>local</code>), so the CLI doubles as a discovery tool.</p>
<h2>Comparing Models: <code>/eval compare</code></h2>
<p>The previous round's <code>EvaluationService</code> evaluates a golden dataset against an answer provider. Cross-model comparison is just running that evaluation once per available model and collecting the averages:</p>
<pre><code class="language-java">for (String model : modelRegistry.availableModels()) {
    modelRegistry.setModel(model);
    EvaluationReport report = evaluationService.evaluate(dataset, provider);
    rows.add(new ModelScore(model, report.averageScores()));
}
</code></pre>
<p>The original selection is restored in a <code>finally</code> block — switching models is a side effect the caller shouldn't be left with. The CLI prints a table:</p>
<pre><code class="language-plaintext">=== Model comparison: chat ===
Model                                exact  contains  f1  rougeL  embed  judge
openai:gpt-4o-mini                    0.33      0.67 ...
anthropic:claude-haiku-4-5-20251001   0.67      1.00 ...
ollama:llama3.2                       0.33      0.67 ...
</code></pre>
<p>This makes the third checklist item ("compare performance and capabilities") concrete: the same golden questions, the same metrics, one row per model.</p>
<h2>Testing It Offline</h2>
<p>Eight new tests, all offline and deterministic:</p>
<ul>
<li><p><strong>Registry routing</strong> — the registry has a test-only constructor pre-populated with <code>FakeChatModel</code>s keyed by <code>provider:model</code>. Tests assert the initial selection, <code>provider</code> and <code>provider:model</code> specs both switch correctly, unknown providers are rejected, and <code>doChat</code> delegates to the selected model.</p>
</li>
<li><p><strong>Comparison end-to-end</strong> — a real <code>EvaluationService</code> (with <code>FakeEmbeddingModel</code> for the embed metric and a fake judge) runs against a two-model registry. Tests verify one row per model, all six metric keys in range, that the answer provider observed each model being selected, and that the original selection is restored afterwards.</p>
</li>
<li><p><strong>Key-free startup</strong> — a real constructor test switches to Ollama and builds a model without any API key, proving startup never hard-requires keys.</p>
</li>
</ul>
<h2>Design Notes</h2>
<ul>
<li><p><strong>One interface, many implementations.</strong> Because LangChain4j models are interface-based, "support multiple providers" collapsed into a registry over a single bean type.</p>
</li>
<li><p><strong>Lazy builds, cached models.</strong> No provider is instantiated until used; switching back after a compare costs nothing.</p>
</li>
<li><p><strong>The judge is a model too.</strong> Cross-model comparison keeps its own metric, the LLM-as-a-judge, honest: the judge is the current model, so every row was scored by that model's own judgment.</p>
</li>
<li><p><strong>Deterministic test constructor.</strong> A package-private constructor accepting a pre-populated model map makes the registry testable without network or environment variables; <code>availableModels()</code> is pinned to that map in tests.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>The checklist is complete. The demo now spans memory, embeddings, RAG, agents, prompting, integration, tooling, evaluation, advanced features, and multi-provider LLM integration. The natural next experiment is advanced agent orchestration (LangChain4j's <code>agentic</code> module) — with a <code>ChatModel</code> registry that can point it at any provider.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/chat">LangChain4j Chat Models</a></p>
</li>
<li><p><a href="https://docs.langchain4j.dev/integration/language-models">LangChain4j Model Providers</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/194">LLM Integration Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>The final checklist item turned out to be mostly plumbing: LangChain4j's shared <code>ChatModel</code> interface meant supporting OpenAI, Anthropic, Gemini, and Ollama was a registry of four builder cases behind one bean. Runtime switching gives the demo a way to <em>experiment</em> with models interactively, and <code>/eval compare</code> turns that experimentation into numbers — the same golden datasets and metrics from the evaluation round, now applied per model. The demo began as a single chat command and ends with a switchboard for every major LLM provider plus a local one, all measured by the same yardstick.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Advanced Features: Multi-Modal, Function Calling, and Structured Output]]></title><description><![CDATA[The demo has covered memory, RAG, agents, prompting, integration, tooling, and evaluation. This round attacks the checklist's advanced features item: multi-modal capabilities, function calling, struct]]></description><link>https://blog.prasadgaikwad.dev/advanced-features-multi-modal-function-calling-and-structured-output</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/advanced-features-multi-modal-function-calling-and-structured-output</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sat, 15 Aug 2026 03:33:48 GMT</pubDate><content:encoded><![CDATA[<p>The demo has covered memory, RAG, agents, prompting, integration, tooling, and evaluation. This round attacks the checklist's <strong>advanced features</strong> item: multi-modal capabilities, function calling, structured output, and custom tools. The pattern that worked in earlier rounds holds: every capability is exercised offline in tests using fake models, while the real OpenAI models are wired for runtime use.</p>
<h2>Multi-Modal</h2>
<p>"Multi-modal" here means three separate capabilities, each backed by a different model interface in LangChain4j.</p>
<h3>Vision: images into the chat model</h3>
<p>A multimodal chat model (GPT-4o-mini) accepts image content alongside text. <code>VisionService</code> builds a <code>UserMessage</code> that mixes a <code>TextContent</code> (the question) with an <code>ImageContent</code>:</p>
<pre><code class="language-java">UserMessage.from(TextContent.from(question), ImageContent.from(URI.create(imageUrl)))
</code></pre>
<p>The image can come from a public URL or, more usefully, as raw bytes — the service base64-encodes them and passes a mime type, which is how an uploaded file reaches the model:</p>
<pre><code class="language-java">ImageContent.from(Base64.getEncoder().encodeToString(imageData), mimeType)
</code></pre>
<p><code>POST /api/describe</code> exposes this over HTTP (<code>{imageUrl | imageData, mimeType, question}</code>), and <code>/describe &lt;url&gt; [question]</code> works from the CLI.</p>
<h3>Image generation</h3>
<p><code>ImageGenerationService</code> wraps an <code>ImageModel</code> — OpenAI's <code>gpt-image-1</code> by default, configurable via <code>app.image.model-name</code>. The returned <code>Image</code> carries a URL, or base64 data plus a mime type, or a <code>revisedPrompt</code>. The CLI <code>/generate &lt;prompt&gt;</code> prints whichever form came back.</p>
<h3>Speech-to-text</h3>
<p><code>SpeechToTextService</code> wraps an <code>AudioTranscriptionModel</code> (whisper-1). It wraps raw audio bytes into an <code>Audio</code> value and calls <code>transcribeToText</code>:</p>
<pre><code class="language-java">Audio audio = Audio.builder()
        .base64Data(Base64.getEncoder().encodeToString(audioData))
        .mimeType(mimeType)
        .build();
return transcriptionModel.transcribeToText(audio);
</code></pre>
<p>CLI: <code>/transcribe &lt;file&gt;</code>. The wav/mp3/ogg/m4a extension is mapped to a mime type so the model knows what it's parsing.</p>
<h2>Function Calling and Custom Tools</h2>
<p>The agent from an earlier round already calls <code>@Tool</code> methods. This round pushes further on <strong>what a tool can be</strong>.</p>
<h3>Structured tool parameters</h3>
<p><code>WeatherTool.getWeather</code> takes a <code>WeatherRequest</code> record instead of a handful of scalars:</p>
<pre><code class="language-java">@Tool("Gets the current temperature for a city, returning it in the requested unit")
public String getWeather(WeatherRequest request) { ... }

@Description("Weather request parameters")
public record WeatherRequest(String city, TemperatureUnit unit) { }

public enum TemperatureUnit { CELSIUS, FAHRENHEIT }
</code></pre>
<p>LangChain4j derives a nested object schema from the record — <code>city</code> as a string, <code>unit</code> as an enum — so the model can fill in both fields in one call. The weather data is fake and deterministic, so the tool works offline. One subtlety surfaced in testing: for a POJO parameter named <code>request</code>, the model's arguments must be <code>{"request": {"city": "...", "unit": "..."}}</code>, keyed by the parameter name, not the bare object.</p>
<h3>Conversation-scoped tool state</h3>
<p><code>NoteTool</code> demonstrates a tool with state that is aware of <em>which conversation</em> it is running in, via <code>@ToolMemoryId</code>:</p>
<pre><code class="language-java">@Tool("Saves a note for the current conversation")
public String saveNote(@ToolMemoryId String memoryId, @P("The note text to save") String note) { ... }
</code></pre>
<p>LangChain4j injects the current conversation's memory id into the call, so notes saved in one conversation never leak into another — a real pattern for memory-adjacent tools.</p>
<h3>Dynamic tool selection with a ToolProvider</h3>
<p>The static agent registers its tools at build time with <code>.tools(...)</code>. The new <code>DynamicToolProvider</code> instead decides <strong>per request</strong> which tools are available, using <code>ToolProviderRequest</code> to inspect the user message:</p>
<pre><code class="language-java">@Override
public ToolProviderResult provideTools(ToolProviderRequest request) {
    List&lt;AiServiceTool&gt; tools = new ArrayList&lt;&gt;(ToolService.findTools(calculatorTool));
    tools.addAll(ToolService.findTools(noteTool));
    if (request.userMessage().singleText().toLowerCase().contains("weather")) {
        tools.addAll(ToolService.findTools(weatherTool));
    }
    return ToolProviderResult.builder().addAll(tools).build();
}
</code></pre>
<p>The calculator and note tools are always exposed; the weather tool only appears when the task is actually about the weather. That keeps the model's function-call surface — and the tokens describing it — as small as possible. <code>AiServices.builder(DynamicAgent.class).toolProvider(provider)</code> wires it up, and <code>/dynamic &lt;task&gt;</code> exercises it from the CLI.</p>
<h2>Structured Output at the Model Level</h2>
<p>The demo already had structured output through AI Service return types (<code>MovieReview</code>, enums, <code>List&lt;String&gt;</code>), where LangChain4j asks for JSON in the prompt and parses the reply. This round adds the <strong>model-level</strong> approach: attach the JSON schema <em>derived from the record</em> to the request as a response format, constraining the model to emit exactly that shape:</p>
<pre><code class="language-java">JsonSchema schema = JsonSchemas.jsonSchemaFrom(MovieReview.class)
        .orElseThrow(...);

ChatRequest request = ChatRequest.builder()
        .messages(List.of(SystemMessage.from(SYSTEM_PROMPT), UserMessage.from(text)))
        .parameters(ChatRequestParameters.builder()
                .responseFormat(schema)   // model is constrained to this schema
                .build())
        .build();
</code></pre>
<p><code>/schema &lt;text&gt;</code> runs this from the CLI. The guarantee is stronger than prompt-based extraction: with JSON mode the model's output is structurally enforced rather than merely requested.</p>
<h2>Testing It All Offline</h2>
<p>The suite grew to 92 tests, all offline:</p>
<ul>
<li><p><strong>Vision</strong> — a <code>FakeChatModel</code> captures the built <code>ChatRequest</code>; tests assert the user message contains exactly one <code>TextContent</code> and one <code>ImageContent</code>, and that the base64 variant carries the right mime type.</p>
</li>
<li><p><strong>Image generation / STT</strong> — dedicated fakes (<code>FakeImageModel</code>, <code>FakeAudioTranscriptionModel</code>) capture the prompt and the <code>Audio</code> payload respectively.</p>
</li>
<li><p><strong>Custom tools</strong> — direct unit tests for <code>WeatherTool</code> (unit conversion, unknown city) and <code>NoteTool</code> (notes scoped per conversation).</p>
</li>
<li><p><strong>Function calling end-to-end</strong> — <code>DynamicToolProviderTest</code> builds a real <code>DynamicAgent</code> over <code>AiServices</code> with a fake chat model that returns a <code>ToolExecutionRequest</code> on its first call and a plain answer on its second. The test watches the tool-specification list on each round: the weather tool is present only when the task mentions weather, the tool actually executes, and its result flows back into the second model call.</p>
</li>
<li><p><strong>Structured output</strong> — asserts the request's <code>responseFormat().jsonSchema()</code> is named <code>MovieReview</code> and that the model's JSON reply parses into the record.</p>
</li>
</ul>
<p>The agent-loop test deserves emphasis: it exercises the real <code>AiServices</code> machinery — schema generation, tool dispatch, the round-trip of results back into the model — with zero network calls, purely by scripting what the model would say.</p>
<h2>Design Notes</h2>
<ul>
<li><p><strong>Fake models everywhere.</strong> No capability ships without an offline test proving the request wiring is correct.</p>
</li>
<li><p><strong>Runtime configurability.</strong> Image and STT models are beans you can swap by property, matching the existing chat/embedding model pattern.</p>
</li>
<li><p><strong>Dynamic &gt; static where it matters.</strong> Exposing a tool per request beats registering every tool for every task — smaller prompts, fewer wrong calls.</p>
</li>
<li><p><strong>Nested tool schemas.</strong> A record parameter is the clean way to give a tool rich, typed inputs; just remember the arguments are keyed by the parameter name.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>The last checklist item is <strong>LLM integration</strong>: wiring alternative providers (Anthropic, Google, Ollama) and comparing models. The <code>/eval</code> harness from the previous round is exactly the yardstick that comparison needs.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/tools">LangChain4j Tools Documentation</a></p>
</li>
<li><p><a href="https://docs.langchain4j.dev/tutorials/chat">LangChain4j Multi-Modality</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/203">Advanced Features Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>The advanced-features round turned the demo from a text-only assistant into one that sees images, paints them, listens, calls tools with structured parameters, and emits schema-constrained JSON. Two ideas carried the round: fakes make even a tool-calling agent loop testable offline, and the interface-based model abstraction in LangChain4j (chat, image, audio) means each new modality was a small service rather than a rewrite.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Evaluation and Testing: Scoring LLM Output with Golden Datasets]]></title><description><![CDATA[A chat application that you can't measure is a wish — "it seems to work" scales poorly once there are dozens of features and you start swapping models. This round adds an evaluation harness to the dem]]></description><link>https://blog.prasadgaikwad.dev/evaluation-and-testing-scoring-llm-output-with-golden-datasets</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/evaluation-and-testing-scoring-llm-output-with-golden-datasets</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Fri, 14 Aug 2026 02:18:12 GMT</pubDate><content:encoded><![CDATA[<p>A chat application that you can't measure is a wish — "it seems to work" scales poorly once there are dozens of features and you start swapping models. This round adds an <strong>evaluation harness</strong> to the demo: golden datasets for each capability, a set of scoring metrics, and a <code>/eval</code> command that grades a live system question by question. The design constraint was that everything must run <strong>fully offline in tests</strong> — no API keys, no network, deterministic results — while still using the <em>real</em> retrieval and AI-service machinery.</p>
<h2>The Setup: Golden Datasets</h2>
<p>Evaluation needs a ground truth. Each <code>GoldenDataset</code> is a named list of <code>(question, expectedAnswer)</code> pairs, and we bundle three, one per capability:</p>
<pre><code class="language-java">public record GoldenDataset(String name, List&lt;GoldenQuestion&gt; goldenQuestions) {
    public static GoldenDataset rag() {
        return new GoldenDataset("rag", List.of(
                new GoldenQuestion(
                        "How does LangChain4j keep conversation memory?",
                        "LangChain4j offers MessageWindowChatMemory and TokenWindowChatMemory."),
                new GoldenQuestion(
                        "How does semantic search find similar texts?",
                        "Semantic search embeds the query and finds stored vectors with the highest cosine similarity."),
                new GoldenQuestion(
                        "What is LangChain4j?",
                        "LangChain4j is a Java framework that simplifies building applications with LLMs.")
        ));
    }
    // sentiment() and chat() follow the same shape
}
</code></pre>
<p>The RAG dataset's expected answers are written against the bundled <code>sample-data/</code> documents, so the questions are genuinely answerable by retrieval alone. Each dataset is deliberately small — three samples — because small and fast means we can run it on every test run and in CI.</p>
<h2>The Metrics</h2>
<p><code>Metric</code> is a one-method interface returning a score in <code>[0, 1]</code>:</p>
<pre><code class="language-java">public interface Metric {
    String name();
    double evaluate(String question, String expected, String actual);
}
</code></pre>
<p>The <code>Metrics</code> factory builds six of them:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>What it measures</th>
</tr>
</thead>
<tbody><tr>
<td><code>exact</code></td>
<td>Normalized string equality (case- and punctuation-insensitive)</td>
</tr>
<tr>
<td><code>contains</code></td>
<td>Whether the expected answer appears inside the produced answer</td>
</tr>
<tr>
<td><code>f1</code></td>
<td>Token F1 over the shared words — punishes missing facts (recall) and hallucinated extras (precision)</td>
</tr>
<tr>
<td><code>rougeL</code></td>
<td>F-measure of the longest common word <em>subsequence</em> — order-aware, unlike <code>f1</code></td>
</tr>
<tr>
<td><code>embed</code></td>
<td>Cosine similarity between the embeddings of expected and actual answers</td>
</tr>
<tr>
<td><code>judge</code></td>
<td>LLM-as-a-judge: the chat model rates faithfulness on 0–5, normalized to <code>[0,1]</code></td>
</tr>
</tbody></table>
<p>The first four are pure string math. <code>embed</code> reuses the app's embedding service, so <em>"v1.0 vectors with highest cosine similarity"</em> scores high against <em>"embeds the query and finds stored vectors with the highest cosine similarity"</em> even though only some words match — that's exactly the semantic tolerance you want from a search system.</p>
<h3>LLM-as-a-judge</h3>
<p>The judge metric is the only one that calls a model. It sends a strict system prompt plus the question, expected answer, and produced answer, then asks for a single integer 0–5:</p>
<pre><code class="language-java">ChatResponse response = judge.chat(ChatRequest.builder()
        .messages(List.of(
                SystemMessage.from(JUDGE_SYSTEM_PROMPT),
                UserMessage.from("Question: " + question + "\n"
                        + "Expected answer: " + expected + "\n"
                        + "Produced answer: " + actual)))
        .build());
return parseJudgeScore(response.aiMessage().text()) / 5.0;
</code></pre>
<p>LLMs are notoriously bad at formatting, so <code>parseJudgeScore</code> doesn't trust the reply: it extracts the first run of digits with a regex and clamps into 0–5. "Score: 5/5", "5", and "I'd say 5!" all parse to <code>1.0</code>. A reply with no digits — say the model went on a tangent — scores <code>0.0</code> rather than crashing the run.</p>
<h2>EvaluationService: Score a Provider</h2>
<p><code>EvaluationService</code> ties it together. It takes an <code>AnswerProvider</code> — a plain <code>String answer(String question)</code> function — runs the dataset through it, and scores every answer with every metric:</p>
<pre><code class="language-java">public EvaluationReport evaluate(GoldenDataset dataset, AnswerProvider provider, List&lt;Metric&gt; metrics) {
    // for each golden question:
    //   actual = provider.answer(question)
    //   scores = each metric.evaluate(question, expected, actual)
    // averages = per-metric mean, rounded to 2 decimals
}
</code></pre>
<p>The abstraction is what makes evaluation reusable. The <code>/eval</code> command binds each dataset to its real system with a one-line lambda:</p>
<pre><code class="language-java">case "rag" -&gt; {
    if (searchService.storeSize() == 0) { /* nudge user to /index first */ }
    provider = question -&gt; qaService.ask(memoryId, question);   // full RAG pipeline
}
case "chat" -&gt; provider = question -&gt; assistant.chat(memoryId, question);
case "sentiment" -&gt; provider = question -&gt; fewShotAssistant.classify(question).name();
</code></pre>
<p>The default metric set — <code>exact, contains, f1, rougeL, embed, judge</code> — is wired in <code>EvaluationService.defaultMetrics()</code>.</p>
<h2>The Report</h2>
<p><code>/eval</code> prints a per-question breakdown and the averages:</p>
<pre><code class="language-plaintext">=== Evaluation: sentiment ===
Metrics: exact, contains, f1, rougeL, embed, judge

[1] I absolutely loved this movie!
    Expected: POSITIVE
    Actual  : POSITIVE
    Scores  : exact=1.00  contains=1.00  f1=1.00  rougeL=1.00  embed=1.00  judge=1.00

[2] The service was okay, nothing special.
    Expected: NEUTRAL
    Actual  : NEUTRAL
    Scores  : exact=1.00  contains=1.00  f1=1.00  rougeL=1.00  embed=1.00  judge=1.00

Average: exact=1.00  contains=1.00  f1=1.00  rougeL=1.00  embed=1.00  judge=1.00
</code></pre>
<p>The multi-metric view is the point: <code>exact</code> and <code>contains</code> are brittle, <code>f1</code>/<code>rougeL</code> reward partial matches, <code>embed</code> tolerates rephrasing, and <code>judge</code> captures quality the string metrics can't. When one model replaces another, the averages shift in ways that tell you <em>which</em> failure mode changed — verbatim accuracy versus factual recall versus phrasing.</p>
<h2>Offline Testing: The Real Pipeline, Fake Models</h2>
<p>The golden datasets double as tests. <code>EvaluationServiceTest</code> runs the <strong>full RAG stack</strong> with real components — <code>AiServices.builder</code> creating the <code>QaAssistant</code> proxy, <code>MessageWindowChatMemory</code>, a <code>DefaultRetrievalAugmentor</code> with the real <code>SemanticSearchContentRetriever</code>, a temp document indexed into a real <code>InMemoryEmbeddingStore</code> — and only the two models are fakes:</p>
<pre><code class="language-java">QaAssistant qaAssistant = AiServices.builder(QaAssistant.class)
        .chatModel(fakeChatModel)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .retrievalAugmentor(DefaultRetrievalAugmentor.builder()
                .contentRetriever(new SemanticSearchContentRetriever(searchService))
                .build())
        .build();

EvaluationReport report = evaluationService.evaluate(
        GoldenDataset.rag(), question -&gt; qaService.ask("rag", question));
</code></pre>
<p>The fake chat model returns a canned answer for every prompt, so every RAG question scores <code>exact=1.0</code> — the test asserts the harness behaves end-to-end rather than testing the LLM (which you can't, offline). The metric math itself gets direct unit tests: ROUGE-L is order-aware (reversed word order scores lower than exact, but above zero), F1 handles empty-vs-empty as <code>1.0</code>, embedding similarity ranks identical &gt; related &gt; unrelated, and the judge parser clamps <code>"10"</code> down to <code>1.0</code> and <code>"no score"</code> down to <code>0.0</code>.</p>
<p>Two expectations in that first draft were flat-out wrong, which is the test suite working: cosine similarity of a vector with itself is <code>1.0000000000000002</code> (not exactly <code>1.0</code>), so the assertion became "close to 1.0 within 1e-6"; and my mental ROUGE-L on reversed word order was miscalculated — the LCS of <code>"a b c d"</code> and <code>"d c b a"</code> is 1, an F of 0.25, not the &gt;0.4 I'd guessed. The tests caught both before the PR shipped.</p>
<h2>Design Notes</h2>
<ul>
<li><p><strong>Offline by default.</strong> Every metric except the judge is deterministic string/vector math; tests swap in fake chat and embedding models, so the whole suite (now 73 tests) runs with no network and no key.</p>
</li>
<li><p><strong>Small golden sets.</strong> Three questions per dataset is enough to catch regressions and keep runs sub-second. Growing to dozens makes the suite flaky and slow.</p>
</li>
<li><p><strong>Metrics as interchangeable scores.</strong> A <code>List&lt;Metric&gt;</code> is passed to <code>evaluate</code>, so a future report could weight <code>judge</code> differently or drop <code>exact</code> entirely without touching the service.</p>
</li>
<li><p><strong>Defensive parsing.</strong> The judge prompt says "reply with only a single integer" because models don't; the regex+clamp makes the metric robust to anything it actually returns.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>Evaluation gives us a yardstick, which is the precondition for the remaining checklist items: <strong>advanced features</strong> (function calling, multi-modal, structured output) and <strong>LLM integration</strong> (swapping in Anthropic/Google/Ollama and comparing models). With <code>/eval</code> in place, those comparisons will have numbers instead of vibes.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev">LangChain4j documentation</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/202">Evaluation and Testing Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>A golden-dataset harness turns "the demo seems fine" into six numbers per capability. The most instructive part of building it was deciding what <em>not</em> to trust: an LLM judge needs regex parsing, string metrics need normalization, and your own intuitions about ROUGE-L need tests. With <code>/eval</code> reporting exact, contains, F1, ROUGE-L, embedding similarity, and judge scores, the next model swap will be measured, not guessed.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Developer Tooling: DevTools, Actuator, and Swagger UI]]></title><description><![CDATA[The demo is now a real web application with a dozen REST endpoints, streaming, and a database — which means it's time to make it comfortable to develop and easy to inspect. This short post covers the ]]></description><link>https://blog.prasadgaikwad.dev/developer-tooling-devtools-actuator-and-swagger-ui</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/developer-tooling-devtools-actuator-and-swagger-ui</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Mon, 10 Aug 2026 13:28:12 GMT</pubDate><content:encoded><![CDATA[<p>The demo is now a real web application with a dozen REST endpoints, streaming, and a database — which means it's time to make it comfortable to develop and easy to inspect. This short post covers the three pieces from our checklist's tooling round: <strong>Spring Boot DevTools</strong>, <strong>Actuator</strong>, and <strong>Swagger UI</strong>.</p>
<h2>The Dependencies</h2>
<p>Three additions to <code>pom.xml</code>:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;
    &lt;scope&gt;runtime&lt;/scope&gt;
    &lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springdoc&lt;/groupId&gt;
    &lt;artifactId&gt;springdoc-openapi-starter-webmvc-ui&lt;/artifactId&gt;
    &lt;version&gt;2.9.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>DevTools is scoped <code>runtime</code> and <code>optional</code> so it never ships in a production artifact. Springdoc 2.9.0 is the latest 2.x line — 3.x targets Spring Boot 4, and we're on 3.5.</p>
<h2>DevTools: Auto-Restart</h2>
<p>DevTools watches the classpath and restarts the app whenever a source file changes — the modern descendant of <code>spring-boot-devtools</code>' classic auto-restart. While editing the demo, saving a Java file triggers a restart automatically, so the old manual stop/<code>spring-boot:run</code> loop disappears. Nothing else to configure.</p>
<h2>Actuator: Monitoring</h2>
<p>Actuator exposes health and metrics without any code. The demo opts into a focused set — <code>health</code>, <code>info</code>, and <code>metrics</code> — rather than the kitchen sink:</p>
<pre><code class="language-properties">management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
management.info.env.enabled=true
info.app.name=langchain4j-demo
info.app.description=LangChain4j feature demo: memory, RAG, agents, prompting, streaming, WebSocket, DB
</code></pre>
<p><code>/actuator/health</code> now reports the app plus its H2 database component:</p>
<pre><code class="language-json">{"status":"UP","components":{"db":{"status":"UP","details":{"database":"H2"}},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"ssl":{"status":"UP"}}}
</code></pre>
<p>One configuration gotcha surfaced during this work: <code>management.info.env.enabled</code> is required to expose <code>info.*</code> properties — without it, <code>/actuator/info</code> returns <code>{}</code> no matter how many <code>info.app.*</code> keys you define. That's the kind of thing a one-line test (see below) would have caught on the first run.</p>
<h2>Swagger UI: Testing from the Browser</h2>
<p>springdoc scans the <code>@RestController</code>s and generates an OpenAPI document automatically — every endpoint, request record, and response record shows up with schemas. Open the UI at <code>/swagger-ui.html</code> and each of the thirteen <code>/api/**</code> paths becomes a clickable "Try it out" form:</p>
<pre><code class="language-plaintext">GET  /api/chat/stream  (SSE)
POST /api/chat  {"message": "..."}
POST /api/ask   {"question": "..."}
...and the prompt, search, index, store, and history endpoints
</code></pre>
<p><code>/v3/api-docs</code> serves the raw JSON if you want to feed it to other tools.</p>
<h3>Making the docs informative</h3>
<p>Auto-generated docs are accurate, but they start out terse — just parameter names and types. So each endpoint got explicit OpenAPI annotations: a <code>@Tag</code> per controller, an <code>@Operation</code> summary and description per method, <code>@Parameter</code> descriptions and examples for query/path params, <code>@Schema</code> descriptions on the request/response records, and <code>@ApiResponse</code> status codes. A tiny <code>OpenApiConfig</code> bean sets the title and description in the UI header.</p>
<p>The result reads like a hand-written API reference that can't drift:</p>
<pre><code class="language-java">@PostMapping("/chat")
@Operation(summary = "Chat with the memory-backed assistant",
        description = "Sends the message to the assistant with conversation memory. "
                + "Use a conversationId to keep a multi-turn conversation; a new id starts fresh.")
@ApiResponse(responseCode = "200", description = "The assistant's answer",
        content = @Content(schema = @Schema(implementation = ChatResponse.class)))
public ChatResponse chat(@RequestBody ChatRequest request) { ... }
</code></pre>
<p>with the request schema annotated on the record:</p>
<pre><code class="language-java">public record ChatRequest(
        @Schema(description = "Conversation (memory) id; a fresh id starts a new conversation",
                example = "web", defaultValue = "api")
        String conversationId,
        @Schema(description = "The user message or task to run", example = "Hello!")
        String message) { ... }
</code></pre>
<p>Every query parameter also carries a description and example, so "Try it out" comes pre-filled with sensible values.</p>
<h2>Testing the Tooling</h2>
<p><code>DevToolingTest</code> treats the tooling itself as a feature to verify:</p>
<pre><code class="language-java">@SpringBootTest(properties = "app.cli.enabled=false")
@AutoConfigureMockMvc
class DevToolingTest {

    @Test
    void actuatorHealthReportsUp() throws Exception {
        mockMvc.perform(get("/actuator/health"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("UP"));
    }

    @Test
    void openApiDocsDescribeTheRestApi() throws Exception {
        mockMvc.perform(get("/v3/api-docs"))
                .andExpect(content().string(containsString("/api/chat")))
                .andExpect(content().string(containsString("/api/chat/stream")));
    }
}
</code></pre>
<p>The OpenAPI test is genuinely useful: if someone renames a route, the docs test catches it. All 60 tests pass offline.</p>
<h2>Design Notes</h2>
<ul>
<li><p><strong>Expose the minimum.</strong> <code>health</code>, <code>info</code>, <code>metrics</code> cover monitoring without opening <code>env</code> or <code>beans</code> to the world.</p>
</li>
<li><p><strong>Let the docs come from the code.</strong> Every endpoint we documented by hand in the README previously had drifted (<code>/api/prompt/sentiment</code> was actually <code>/api/sentiment</code>). springdoc derives docs from the running code, so they can't drift — and it flagged the README table to fix.</p>
</li>
<li><p><strong>Tooling gets tests too.</strong> A 4-test class proves health, info, docs, and the UI redirect without a browser.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>Back to the feature checklist: <strong>evaluation</strong> (automated scoring of the typed outputs) and <strong>LLM integration</strong> (alternative providers and models). Both become more tractable now that every endpoint is testable from Swagger UI and observable through Actuator.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.spring.io/spring-boot/reference/actuator/index.html">Spring Boot Actuator Documentation</a></p>
</li>
<li><p><a href="https://springdoc.org">springdoc-openapi</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/211">Developer Tooling Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Tooling work is the least glamorous milestone in the demo, but it pays off immediately: DevTools removed the restart loop, Actuator made the app observable with four config lines, and Swagger UI turned "let me check the API" into a browser tab instead of a curl session. The best return, though, was the discovery that hand-maintained docs had already drifted — a good argument for generating documentation from the code that implements it.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Integration Features: REST, Streaming, WebSocket, and a Database]]></title><description><![CDATA[So far everything in this series has lived behind the command line. The model, the memory, the RAG pipeline, the agents — all real, but all reachable only through a REPL. Our checklist's next mileston]]></description><link>https://blog.prasadgaikwad.dev/integration-features-rest-streaming-websocket-and-a-database</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/integration-features-rest-streaming-websocket-and-a-database</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sun, 09 Aug 2026 23:34:51 GMT</pubDate><content:encoded><![CDATA[<p>So far everything in this series has lived behind the command line. The model, the memory, the RAG pipeline, the agents — all real, but all reachable only through a REPL. Our checklist's next milestone is <strong>integration features</strong>: exposing all of that as a web application. This post covers the four pieces — <strong>REST API</strong>, <strong>streaming</strong>, <strong>WebSocket</strong>, and <strong>database integration</strong> — and the one trick that lets all of them stay testable without a network connection.</p>
<h2>The Shape of It</h2>
<p>Spring Boot 3 already has the web layer; LangChain4j has the AI layer. The integration milestone is mostly about wiring the two together. The application now ships:</p>
<ul>
<li><p>a REST API under <code>/api</code> (chat, RAG, agent, prompt helpers, search, history)</p>
</li>
<li><p>a Server-Sent Events endpoint for token-by-token streaming</p>
</li>
<li><p>a WebSocket endpoint for the same, frame-by-frame</p>
</li>
<li><p>an H2 database, via Spring Data JPA, for conversation history</p>
</li>
</ul>
<p>The <code>pom.xml</code> gains four starters:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-websocket&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.h2database&lt;/groupId&gt;
    &lt;artifactId&gt;h2&lt;/artifactId&gt;
    &lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;
</code></pre>
<h2>REST API</h2>
<p>Controllers are thin: take a request record, call the service that already exists from the earlier milestones, return a response record. The chat endpoint reuses the memory-backed <code>Assistant</code> from the memory milestone, with <code>conversationId</code> defaulting to <code>"api"</code> for curl users:</p>
<pre><code class="language-java">@PostMapping("/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
    String answer = assistant.chat(request.conversationId(), request.message());
    return new ChatResponse(answer);
}
</code></pre>
<p>Because RAG (<code>/api/ask</code>) and the agent (<code>/api/agent</code>) were built as services in their own milestones, exposing them is a three-liner each. The prompt helpers from the last milestone (<code>/api/prompt/sentiment</code>, <code>/api/prompt/movie</code>, <code>/api/prompt/topics</code>) and semantic search (<code>/api/search</code>) get the same treatment. A static <code>index.html</code> lists every endpoint, and <code>chat.html</code> is a working browser client.</p>
<h2>Streaming with Server-Sent Events</h2>
<p>Streaming needed something we didn't have yet: a <code>StreamingChatModel</code>. That's a second bean in <code>AiConfig</code>, alongside the existing chat model:</p>
<pre><code class="language-java">@Bean
public StreamingChatModel streamingChatModel() {
    return OpenAiStreamingChatModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName("gpt-4o-mini")
            .build();
}
</code></pre>
<p>The streaming logic itself lives in a small service with no web-layer dependencies. That separation is deliberate — it means the same code serves both the SSE endpoint and the WebSocket handler, and it means we can unit-test it with a fake model:</p>
<pre><code class="language-java">@Service
public class ChatStreamingService {

    public void stream(String message, StreamConsumer consumer) {
        streamingChatModel.chat(ChatRequest.builder()
                .messages(List.of(
                        SystemMessage.from("You are a helpful assistant. Answer concisely."),
                        UserMessage.from(message)))
                .build(), new StreamingChatResponseHandler() {
                    @Override
                    public void onPartialResponse(String partialResponse) {
                        consumer.onToken(partialResponse);
                    }
                    @Override
                    public void onCompleteResponse(ChatResponse completeResponse) {
                        consumer.onComplete(completeResponse.aiMessage().text());
                    }
                    @Override
                    public void onError(Throwable error) {
                        consumer.onError(error);
                    }
                });
    }
}
</code></pre>
<p>The consumer is a plain interface: <code>onToken</code>, <code>onComplete</code>, <code>onError</code>. The SSE endpoint adapts it to an <code>SseEmitter</code>:</p>
<pre><code class="language-java">@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam String message) {
    SseEmitter emitter = new SseEmitter(60_000L);
    streamingService.stream(message, new ChatStreamingService.StreamConsumer() {
        @Override public void onToken(String token) {
            try {
                emitter.send(SseEmitter.event().data(token));
            } catch (IOException e) {
                emitter.completeWithError(e);
            }
        }
        @Override public void onComplete(String fullText) { emitter.complete(); }
        @Override public void onError(Throwable error) { emitter.completeWithError(error); }
    });
    return emitter;
}
</code></pre>
<p>The browser client reads the stream with a plain <code>EventSource</code> — no library needed.</p>
<h2>WebSocket</h2>
<p>The WebSocket handler is the same streaming service wearing a different transport. The client sends a JSON message; the server replies with one text frame per token and a final <code>[DONE]</code> frame:</p>
<pre><code class="language-java">@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        ChatMessagePayload payload;
        try {
            payload = objectMapper.readValue(message.getPayload(), ChatMessagePayload.class);
        } catch (IOException e) {
            sendQuietly(session, new TextMessage("[ERROR] Invalid message payload: " + e.getMessage()));
            return;
        }
        streamingService.stream(payload.message(), new ChatStreamingService.StreamConsumer() {
            @Override public void onToken(String token) {
                sendQuietly(session, new TextMessage(token));
            }
            @Override public void onComplete(String fullText) {
                sendQuietly(session, new TextMessage("[DONE]"));
            }
            @Override public void onError(Throwable error) {
                sendQuietly(session, new TextMessage("[ERROR] " + error.getMessage()));
            }
        });
    }
}
</code></pre>
<p><code>WebSocketConfig</code> registers the handler at <code>/ws/chat</code> and opens CORS for a browser client:</p>
<pre><code class="language-java">@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatWebSocketHandler, "/ws/chat").setAllowedOrigins("*");
    }
}
</code></pre>
<h2>Database Integration</h2>
<p>The last piece: conversation history moved out of memory and into a real store. The entity is a plain JPA <code>@Entity</code>; the repository is one interface with derived queries:</p>
<pre><code class="language-java">public interface ConversationEntryRepository extends JpaRepository&lt;ConversationEntry, Long&gt; {
    List&lt;ConversationEntry&gt; findByConversationIdOrderByTimestampAsc(String conversationId);
    List&lt;String&gt; findDistinctConversationIds();
    void deleteByConversationId(String conversationId);
}
</code></pre>
<p>The service wraps it with <code>@Transactional</code> — the derived <code>deleteByConversationId</code> needs a real transaction, something the test suite caught on the first run:</p>
<pre><code class="language-java">@Service
@Transactional
public class ConversationHistoryService {
    public ConversationEntry record(String conversationId, String role, String text) { ... }
    public List&lt;ConversationEntry&gt; history(String conversationId) { ... }
    public List&lt;String&gt; conversationIds() { ... }
    public void clear(String conversationId) { ... }
}
</code></pre>
<p>The REST layer exposes it as <code>GET /api/history</code>, <code>GET /api/history/{id}</code>, and <code>DELETE /api/history/{id}</code>. The database is in-memory H2 (<code>jdbc:h2:mem:demo</code>), so it resets on restart — but it's a real database with real JPA semantics, and swapping in Postgres would just be a JDBC URL and driver change.</p>
<h2>Keeping It Testable Offline</h2>
<p>The web layer added a second AI surface — streaming — which needed a fake just like <code>FakeChatModel</code>. <code>FakeStreamingChatModel</code> implements <code>StreamingChatModel</code> and plays back a fixed list of tokens:</p>
<pre><code class="language-java">class FakeStreamingChatModel implements StreamingChatModel {

    private final List&lt;String&gt; tokens;

    FakeStreamingChatModel(String... tokens) {
        this.tokens = List.of(tokens);
    }

    @Override
    public void doChat(ChatRequest request, StreamingChatResponseHandler handler) {
        StringBuilder full = new StringBuilder();
        for (String token : tokens) {
            handler.onPartialResponse(token);
            full.append(token);
        }
        handler.onCompleteResponse(ChatResponse.builder()
                .aiMessage(AiMessage.from(full.toString()))
                .build());
    }
}
</code></pre>
<p>With that in place the tests split cleanly by layer:</p>
<ul>
<li><p><code>ChatStreamingServiceTest</code> — token forwarding and completion, unit-level</p>
</li>
<li><p><code>ChatWebSocketHandlerTest</code> — frames arrive in order followed by <code>[DONE]</code>, and a bad payload yields <code>[ERROR]</code></p>
</li>
<li><p><code>ConversationHistoryServiceTest</code> — <code>@DataJpaTest</code>: ordering, empty history, distinct conversation ids, scoped deletes</p>
</li>
<li><p><code>ChatApiControllerTest</code> — <code>@SpringBootTest</code> + <code>@AutoConfigureMockMvc</code> with <code>@MockitoBean</code> stand-ins for the services; the SSE test uses <code>asyncDispatch</code> on the returned <code>MvcResult</code></p>
</li>
<li><p><code>HistoryApiControllerTest</code> — end-to-end through the real H2-backed service</p>
</li>
</ul>
<p>All 56 tests run offline.</p>
<h2>Design Notes</h2>
<ul>
<li><p><strong>Streaming is transport-agnostic.</strong> <code>ChatStreamingService</code> knows nothing about <code>SseEmitter</code> or <code>WebSocketSession</code>; both transports are thin adapters over the same <code>StreamConsumer</code>. That is what makes it unit-testable and gives SSE and WebSocket identical behavior for free.</p>
</li>
<li><p><strong>Reuse beats new abstractions.</strong> The controller didn't reinvent RAG or agents — it calls the existing <code>QaService</code> and <code>ChainService</code>.</p>
</li>
<li><p><strong>Transactions are not optional.</strong> Derived delete queries in Spring Data JPA require a transaction; the failing test on the first run was the cheapest possible proof.</p>
</li>
<li><p><strong>Fakes scale with the surface.</strong> One fake per AI boundary (<code>FakeChatModel</code>, <code>FakeEmbeddingModel</code>, now <code>FakeStreamingChatModel</code>) keeps every layer of the app testable without API keys.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>From our checklist: <strong>evaluation</strong> (now that prompts and outputs are typed, they're ready to be scored automatically) and <strong>LLM integration</strong> (swapping in other providers and models). Also worth exploring: multi-modal input and the new <code>agentic</code> module for advanced orchestration.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/ai-services">Official LangChain4j Streaming Documentation</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-framework/reference/web/webmvc.html">Spring Boot SSE and WebSocket</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/201">Integration Features Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Integration was the least "AI" milestone in the whole demo — no new prompting tricks, no model gymnastics. But it's the one that turns a library into an application: a browser can now chat, stream, and ask questions against every feature built so far, and conversation state lives in a real database. The design payoff was architectural: keeping streaming logic transport-free meant SSE and WebSocket both shipped as thin adapters, and keeping every fake at the model boundary meant all of it stays offline-testable.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Prompting Techniques with LangChain4j]]></title><description><![CDATA[Everything so far in this series has relied on a single well-written prompt. But prompts are code: they deserve templates, examples, and reliable output contracts. This post covers the three prompting]]></description><link>https://blog.prasadgaikwad.dev/prompting-techniques-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/prompting-techniques-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sat, 08 Aug 2026 00:23:55 GMT</pubDate><content:encoded><![CDATA[<p>Everything so far in this series has relied on a single well-written prompt. But prompts are code: they deserve templates, examples, and reliable output contracts. This post covers the three prompting techniques from our checklist — <strong>prompt templates</strong>, <strong>few-shot learning examples</strong>, and <strong>output parsers</strong> — and how LangChain4j makes each one a first-class, testable concept.</p>
<h2>Prompt Templates</h2>
<p>A prompt template is a prompt skeleton with <code>{{placeholders}}</code>. Instead of string-concatenating prompts in handlers all over the codebase, you define the template once and render it with variables.</p>
<p>LangChain4j's <code>PromptTemplate</code> is a pure, offline component — no model involved. It renders a template into a <code>Prompt</code>, which converts to a system or user message:</p>
<pre><code class="language-java">@Service
public class PromptService {

    private static final String SYSTEM_TEMPLATE = """
            You are a professional movie critic.
            Always write your reviews in a {{tone}} tone.
            """;

    private static final String USER_TEMPLATE = """
            Write a short review for the movie "{{movie}}" ({{year}}).
            Include a rating out of 10 in your review.
            """;

    public List&lt;ChatMessage&gt; buildMovieReviewMessages(String movie, int year, String tone) {
        Prompt systemPrompt = PromptTemplate.from(SYSTEM_TEMPLATE).apply(Map.of("tone", tone));
        Prompt userPrompt = PromptTemplate.from(USER_TEMPLATE).apply(Map.of("movie", movie, "year", year));
        return List.of(systemPrompt.toSystemMessage(), userPrompt.toUserMessage());
    }
}
</code></pre>
<p>The CLI command <code>/template</code> prints the fully rendered messages <em>without calling any API</em>:</p>
<pre><code class="language-plaintext">/template Inception
Rendered prompt template for "Inception" (year 2010, enthusiastic tone):

SYSTEM:
You are a professional movie critic.
Always write your reviews in an enthusiastic tone.

USER:
Write a short review for the movie "Inception" (2010).
Include a rating out of 10 in your review.
</code></pre>
<p>Because rendering is deterministic, it is fully unit-testable: same template + same variables → same messages. We assert exactly that in <code>PromptServiceTest</code>.</p>
<p>AI Services take templates one step further: <code>@SystemMessage</code> and <code>@UserMessage</code> accept templates whose variables are resolved from method parameters (via <code>@V</code>, or plain parameter names with Spring's <code>-parameters</code>):</p>
<pre><code class="language-java">@UserMessage("Write a review of {{movie}} from {{year}}")
String review(@V("movie") String movie, @V("year") int year);
</code></pre>
<h2>Few-Shot Learning Examples</h2>
<p>Sometimes a short instruction isn't enough — the model benefits from <em>seeing</em> correct input/output pairs. Few-shot prompting embeds a handful of labeled examples into the prompt. Our <code>FewShotAssistant</code> classifies sentiment into <code>POSITIVE</code> / <code>NEGATIVE</code> / <code>NEUTRAL</code> using a mini dataset in the system message:</p>
<pre><code class="language-java">public interface FewShotAssistant {

    @SystemMessage("""
            You are a sentiment classifier. Classify the sentiment of the given text
            as one of: POSITIVE, NEGATIVE, NEUTRAL. Reply with exactly one of these words.

            Examples:
            Text: "I absolutely loved this movie, best film of the year!"
            Sentiment: POSITIVE

            Text: "This restaurant is terrible, the food was cold and the service rude."
            Sentiment: NEGATIVE

            Text: "The package arrived on time. Nothing more to say."
            Sentiment: NEUTRAL
            """)
    Sentiment classify(@UserMessage String text);
}
</code></pre>
<p>Three well-chosen examples do double duty: they pin down the <em>format</em> (exactly one word) and illustrate the <em>decision boundary</em> (contrasting strong opinions with a neutral, factual statement). <code>AiServices</code> renders the system message with the examples on every call.</p>
<h2>Output Parsers (Structured Output)</h2>
<p>The most robust prompting technique is making the model return a <em>typed value</em> instead of free text. In LangChain4j this is done by choosing the AI Service's return type — the framework derives a schema, requests JSON matching it, and parses the reply into an object. The internal machinery is <code>OutputParser</code> (<code>EnumOutputParser</code>, <code>PojoOutputParser</code>, <code>StringListOutputParser</code>, and friends); we never touch it directly, we just pick the return type.</p>
<p>Returning an enum — the sentiment above returns <code>Sentiment</code>, parsed into the matching constant.</p>
<p>Returning a POJO — <code>MovieExtractor</code> returns a <code>MovieReview</code> record:</p>
<pre><code class="language-java">public record MovieReview(String title, int year, String director, double rating, String summary) {}

public interface MovieExtractor {
    @SystemMessage("""
            Extract information about the movie from the given text.
            Return the data as a JSON object with exactly these fields:
            title (string), year (integer), director (string), rating (number from 1 to 10), summary (string).
            """)
    MovieReview extract(@UserMessage String text);
}
</code></pre>
<p>The handler gets an object, not a string to hand-parse:</p>
<pre><code class="language-plaintext">/movie Inception is a 2010 film directed by Christopher Nolan about planting ideas in dreams.
Movie &gt; MovieReview[title=Inception, year=2010, director=Christopher Nolan, rating=9.0, summary=...]
</code></pre>
<p>Returning a collection — <code>TopicExtractor</code> returns <code>List&lt;String&gt;</code>. (Collection-of-strings output is parsed as one item per line, so the system message tells the model that format.)</p>
<h2>Testing Without a Live Model</h2>
<p>Prompt engineering is all about iteration, so offline testing is a big win. We added a second shared test helper, <code>FakeChatModel</code>, which mirrors <code>FakeEmbeddingModel</code>: it implements the <code>ChatModel</code> interface, captures the exact <code>ChatRequest</code> LangChain4j builds, and returns a canned reply. That lets tests verify two things at once:</p>
<ol>
<li><p><strong>What goes in</strong> — the generated system/user messages contain the few-shot examples and the interpolated template variables.</p>
</li>
<li><p><strong>What comes out</strong> — the canned reply is parsed into the correct enum constant, record, or list.</p>
</li>
</ol>
<pre><code class="language-java">@Test
void embedsFewShotExamplesInTheSystemMessage() {
    FakeChatModel chatModel = new FakeChatModel("POSITIVE");
    FewShotAssistant assistant = AiServices.builder(FewShotAssistant.class)
            .chatModel(chatModel)
            .build();

    Sentiment sentiment = assistant.classify("This movie is amazing!");

    assertThat(chatModel.lastSystemMessage())
            .contains("Examples:")
            .contains("Sentiment: NEGATIVE");
    assertThat(sentiment).isEqualTo(Sentiment.POSITIVE);
}

@Test
void parsesTheModelReplyIntoTheMovieReviewRecord() {
    FakeChatModel chatModel = new FakeChatModel("""
            {"title": "Inception", "year": 2010, "director": "Christopher Nolan",
             "rating": 9.0, "summary": "A thief enters dreams to plant an idea."}
            """);
    MovieReview review = AiServices.builder(MovieExtractor.class)
            .chatModel(chatModel)
            .build()
            .extract("Tell me about Inception.");

    assertThat(review.title()).isEqualTo("Inception");
    assertThat(review.rating()).isEqualTo(9.0);
}
</code></pre>
<p>These tests exercise the real <code>AiServices</code> proxy generation and the real output parsers — the only substitute is the model itself. Combined with <code>PromptServiceTest</code>, the whole prompting layer is verified without a single network call.</p>
<h2>Design Notes</h2>
<ul>
<li><p><strong>Templates are the API, not an afterthought.</strong> <code>PromptTemplate</code> and <code>@UserMessage</code>/<code>@SystemMessage</code> templates keep prompts next to their consumers and make variables explicit.</p>
</li>
<li><p><strong>Examples live in the system message.</strong> They ship with every call and double as both format and boundary guidance.</p>
</li>
<li><p><strong>Output contracts beat prompt begging.</strong> Telling the model "return JSON" is unreliable; returning <code>MovieReview</code> makes the schema explicit and the parsing automatic and typed.</p>
</li>
<li><p><strong>FakeChatModel closes the loop.</strong> It lets us assert on both the rendered prompt and the parsed result, which is where most prompt-engineering bugs actually live.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>From our checklist: <strong>LLM integration</strong> (pluggable providers/models), <strong>integration features</strong> (REST endpoints, streaming), and <strong>evaluation</strong> — now that outputs are typed (<code>Sentiment</code>, <code>MovieReview</code>, <code>List&lt;String&gt;</code>), they become much easier to evaluate automatically.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/ai-services">Official LangChain4j Prompting Documentation</a></p>
</li>
<li><p><a href="https://docs.langchain4j.dev/tutorials/structured-output">LangChain4j Structured Output</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/200">Prompting Techniques Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Prompting is the part of an LLM application you will iterate on most, so it pays to make it structured. Templates keep prompts DRY and inspectable; few-shot examples teach format and boundaries with zero extra code; typed return values turn model output from a string to parse into a contract to rely on. And because all three are pure or deterministic, they test cleanly offline — which is exactly what you want when the thing you're tuning changes every day.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Chains and Agents with LangChain4j]]></title><description><![CDATA[So far the demo's LLM calls have been single-shot: the chat model answers a question in one step. But many real tasks need more than that — a task may require arithmetic, a lookup in a knowledge base,]]></description><link>https://blog.prasadgaikwad.dev/chains-and-agents-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/chains-and-agents-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sat, 08 Aug 2026 00:16:07 GMT</pubDate><content:encoded><![CDATA[<p>So far the demo's LLM calls have been single-shot: the chat model answers a question in one step. But many real tasks need more than that — a task may require arithmetic, a lookup in a knowledge base, or a sequence of steps. This post brings in <strong>chains</strong> (deterministic pipelines we compose in Java) and <strong>agents</strong> (LLM-driven loops that decide to call <strong>tools</strong>).</p>
<h2>Two Kinds of "Chains"</h2>
<p>The word "chain" means two different things in the LLM world, and LangChain4j maps them to two different mechanisms:</p>
<ol>
<li><p><strong>Processing chains</strong> — <em>we</em> decide the steps, in code. Input flows through fixed stages: preprocess, route, transform, postprocess. Deterministic and testable.</p>
</li>
<li><p><strong>Agentic loops</strong> — <em>the model</em> decides the steps, calling functions in a loop ("reason, call tool, observe result") until the task is done. Non-deterministic; correctness is delegated to the model.</p>
</li>
</ol>
<p>LangChain4j implements the second one as <strong>function calling</strong> under the hood (OpenAI-style tool calls), and wraps it in <code>AiServices</code>. We implement the first one ourselves as plain Java composition.</p>
<h2>Tools: The Agent's Hands</h2>
<p>A <em>tool</em> is an ordinary method annotated with <code>@Tool</code>. The chat model sees a description of each tool and its parameters, and decides when to call it. <code>AiServices</code> executes the call and feeds the result back to the model — automatically, in a loop, with no glue code from us.</p>
<p>Our agent gets three tools. First, an arithmetic calculator. The trickiest part is <em>safety</em>: we never want to evaluate arbitrary model-produced strings as code, so we wrote a small recursive-descent parser that only accepts digits, <code>+ - * /</code>, parentheses, and dots:</p>
<pre><code class="language-java">@Component
public class CalculatorTool {

    @Tool("Calculates the result of an arithmetic expression using +, -, *, / and parentheses")
    public double calculate(@P("The arithmetic expression to evaluate, e.g. \"(1 + 2) * 3\"") String expression) {
        if (expression == null || expression.isBlank()) {
            throw new IllegalArgumentException("Expression must not be empty");
        }
        return new Evaluator(expression).evaluate();
    }
}
</code></pre>
<p><code>@Tool</code> marks the callable function; <code>@P</code> documents a single parameter (LangChain4j turns these into the model's function schema). Second, a document-search tool that gives the agent access to the same embedded knowledge base the RAG pipeline uses:</p>
<pre><code class="language-java">@Tool("Searches the indexed documents and returns the most relevant passages with their relevance scores")
public String searchDocuments(@P("The search query, e.g. \"what does the document say about RAG\"") String query) {
    List&lt;EmbeddingMatch&lt;TextSegment&gt;&gt; matches = searchService.search(query);
    if (matches.isEmpty()) {
        return "No matching documents found. Documents may need to be indexed first with /index.";
    }
    // ... format matches with scores
}
</code></pre>
<p>And third, a stats tool that reports the embedding store's model and size.</p>
<h2>The Agent: AiServices with Tools</h2>
<p>An agent is just an AI Service interface whose proxy gets built with <code>.tools(...)</code>:</p>
<pre><code class="language-java">public interface Agent {

    @SystemMessage("""
            You are an agent that accomplishes the user's task using the available tools.
            Use the "searchDocuments" tool when the task asks about the indexed documents or your own data.
            Use the "calculate" tool for arithmetic computations.
            If the task does not need a tool, answer directly. Be concise.
            """)
    String execute(@MemoryId String memoryId, @UserMessage String task);
}
</code></pre>
<p>Wired in <code>AiConfig</code> alongside the other AI services, reusing the same per-conversation memory provider:</p>
<pre><code class="language-java">@Bean
public Agent agent(ChatModel chatModel,
                   CalculatorTool calculatorTool,
                   DocumentSearchTool documentSearchTool,
                   EmbeddingStoreStatsTool storeStatsTool,
                   ChatMemoryRegistry chatMemoryRegistry,
                   ...) {
    return AiServices.builder(Agent.class)
            .chatModel(chatModel)
            .chatMemoryProvider(createChatMemoryProvider(chatMemoryRegistry, modelName, maxMessages, maxTokens))
            .tools(calculatorTool, documentSearchTool, storeStatsTool)
            .build();
}
</code></pre>
<p>The system message is important: it tells the model <em>which</em> tool fits <em>which</em> kind of task. Without it the model still sees the tool schemas, but good guidance reduces wasted calls.</p>
<h2>The Chain: Deterministic Routing</h2>
<p><code>ChainService</code> is our processing chain — a pipeline whose stages we control:</p>
<pre><code class="language-java">@Service
public class ChainService {

    private final Agent agent;
    private final CalculatorTool calculatorTool;

    public String ask(String memoryId, String task) {
        String normalized = task.trim();
        if (CalculatorTool.isArithmetic(normalized)) {
            return "Result: " + calculatorTool.calculate(normalized);
        }
        return agent.execute(memoryId, normalized);
    }
}
</code></pre>
<p>Stage 1 (preprocess + route): if the task is a <em>pure</em> numeric expression, resolve it locally — no model call, no tokens burned, and no chance of the LLM hallucinating a sum. Stage 2 (execute): everything else — including worded questions like "what is two plus two?" — goes to the agent, which can still decide to call the calculator itself.</p>
<p>This hybrid is a nice demonstration of why chains and agents are complements, not competitors: cheap, deterministic stages handle the obvious cases; the flexible, model-driven loop handles the rest.</p>
<h2>Trying It Out</h2>
<pre><code class="language-plaintext">/agent compute (20 * 5) - 8
Agent &gt; Result: 92.0
</code></pre>
<p>Pure arithmetic never touches the model — the chain resolves it. Now a worded version that needs the agent <em>and</em> its document-search tool (after <code>/index sample-data</code>):</p>
<pre><code class="language-plaintext">/agent what do the documents say about agents?
Agent &gt; The documents describe agents as programs that use tools to complete tasks:
        "Agents use tools to complete tasks to achieve a goal." (from langchain4j.txt)
</code></pre>
<p>Behind the scenes the model called <code>searchDocuments</code>, saw the retrieved passages, and answered from them — the RAG loop, but driven by the model's own decision instead of a fixed augmentor.</p>
<h2>Testing Without a Live Model</h2>
<p>The calculator and the chain are deterministic, so they get real unit tests — no mocks, no network:</p>
<pre><code class="language-java">@Test
void respectsOperatorPrecedence() {
    assertThat(calculatorTool.calculate("2 + 3 * 4")).isEqualTo(14.0);
}

@Test
void routesArithmeticToTheCalculatorWithoutTheAgent() {
    ChainService chain = new ChainService((memoryId, task) -&gt; "agent called: " + task, calculatorTool);
    assertThat(chain.ask("main", " 2 + 3 * 4 ")).isEqualTo("Result: 14.0");
}

@Test
void delegatesWordedTasksToTheAgent() {
    ChainService chain = new ChainService((memoryId, task) -&gt; "agent handled \"" + task + "\"", calculatorTool);
    assertThat(chain.ask("main", "What can you do?")).isEqualTo("agent handled \"What can you do?\"");
}
</code></pre>
<p>Because <code>Agent</code> is an interface, the chain test substitutes a lambda — asserting both the routing <em>and</em> that the memory id is passed through, entirely offline. The document-search tool is tested with the same <code>FakeEmbeddingModel</code> we use elsewhere: index real text, call the tool, assert it returns ranked passages.</p>
<h2>Design Notes</h2>
<ul>
<li><p><code>@Tool</code> <strong>on methods,</strong> <code>@P</code> <strong>on parameters.</strong> That's the whole declarative surface for function calling in LangChain4j. Parameters map to the OpenAI-style schema the model uses to format its calls.</p>
</li>
<li><p><strong>Tools are Spring beans.</strong> The three tool classes are <code>@Component</code>s with their own dependencies (the search tool reuses <code>SemanticSearchService</code>), so they compose cleanly with the rest of the context.</p>
</li>
<li><p><strong>Memory composes for free.</strong> The agent uses the same <code>ChatMemoryProvider</code> as the assistant and QA services, so it can hold a multi-turn, tool-using conversation with <code>@MemoryId</code> scoping.</p>
</li>
<li><p><strong>Safety matters.</strong> Never evaluate strings the model returns as code. Our evaluator parses a grammar, rejects unknown characters, throws on malformed input and division by zero, and is covered by tests for exactly those failure modes.</p>
</li>
</ul>
<h2>Next Steps</h2>
<p>The natural follow-ups from our checklist: <strong>prompting techniques</strong> (few-shot examples, templates) and <strong>evaluation</strong> (benchmarking whether the agent's tool choices are good ones). There is also a new, explicitly experimental <code>langchain4j-agentic</code> module in LangChain4j with structured "agent definitions" — worth a look once the stable <code>AiServices</code> + <code>@Tool</code> path has proven itself here.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/ai-services">Official LangChain4j AI Services Documentation</a></p>
</li>
<li><p><a href="https://docs.langchain4j.dev/tutorials/tools">LangChain4j Tools / Function Calling</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/198">Chains and Agents Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Chains and agents are two complementary ways to make an LLM application <em>do things</em> instead of just <em>say things</em>. Deterministic processing chains give us fast, testable, token-cheap routing for the cases we can predict; <code>AiServices</code> with <code>@Tool</code> gives the model the ability to call our own code when it decides that's needed — whether that's arithmetic, searching the knowledge base, or anything else we want to expose as a function. Both compose with the memory and RAG infrastructure we already built, which is exactly the point of a demo that grows feature by feature.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Document Processing with LangChain4j]]></title><description><![CDATA[In the last post we built a RAG pipeline that answers questions from indexed documents. But the pipeline is only as good as its input: real-world data lives in PDFs, mixed file types, and messy format]]></description><link>https://blog.prasadgaikwad.dev/document-processing-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/document-processing-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Thu, 06 Aug 2026 02:01:15 GMT</pubDate><content:encoded><![CDATA[<p>In the last post we built a RAG pipeline that answers questions from indexed documents. But the pipeline is only as good as its input: real-world data lives in PDFs, mixed file types, and messy formats — not clean <code>.txt</code> files. In this post we'll teach the demo to load PDFs, pick the right parser per file type, and split documents with configurable strategies.</p>
<h2>The Document Pipeline</h2>
<p>LangChain4j models the journey from raw file to searchable chunk as a chain of small steps:</p>
<ol>
<li><p><strong>Load</strong> — read bytes from a file or URL (<code>FileSystemDocumentLoader</code>).</p>
</li>
<li><p><strong>Parse</strong> — turn bytes into text using a <code>DocumentParser</code> chosen for the format (PDFBox for PDFs, plain text otherwise).</p>
</li>
<li><p><strong>Split</strong> — break the text into <code>TextSegment</code>s with a <code>DocumentSplitter</code>.</p>
</li>
<li><p><strong>Transform</strong> — enrich each segment (we prepend the source file name).</p>
</li>
</ol>
<p>Every step is swappable. We used to do this inside <code>SemanticSearchService</code> with hardcoded settings; now it lives in a dedicated <code>DocumentService</code> so the processing concerns are separate from the embedding concerns.</p>
<h2>Parsing PDFs</h2>
<p>PDF parsing needs a parser. LangChain4j has several; we added the Apache PDFBox one:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
    &lt;artifactId&gt;langchain4j-document-parser-apache-pdfbox&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<p>(The BOM manages its version, so no version tag is needed.)</p>
<p><code>ApachePdfBoxDocumentParser</code> extracts text from a PDF using PDFBox. <code>DocumentService</code> picks the right parser per extension — PDFs get PDFBox, everything else gets the plain-text parser:</p>
<pre><code class="language-java">private DocumentParser parserFor(Path path) {
    String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
    if (name.endsWith(".pdf")) {
        return new ApachePdfBoxDocumentParser();
    }
    return new TextDocumentParser();
}
</code></pre>
<p>And loads a single file or a whole directory:</p>
<pre><code class="language-java">public List&lt;TextSegment&gt; loadAndSplit(Path filePath) {
    return split(FileSystemDocumentLoader.loadDocument(filePath, parserFor(filePath)));
}

public List&lt;TextSegment&gt; loadAndSplitDirectory(Path directoryPath) {
    try (Stream&lt;Path&gt; files = Files.list(directoryPath)) {
        return files.filter(Files::isRegularFile)
                .filter(file -&gt; !file.getFileName().toString().startsWith("."))
                .flatMap(file -&gt; loadAndSplit(file).stream())
                .toList();
    } catch (IOException e) {
        throw new RuntimeException("Failed to list documents in " + directoryPath, e);
    }
}
</code></pre>
<p>Note that we skip hidden files (like macOS <code>.DS_Store</code>) — a small but necessary bit of processing hygiene. The loader attaches useful <code>Metadata</code> automatically, including <code>file_name</code> and the absolute directory path.</p>
<h2>Splitting Strategies</h2>
<p>LangChain4j ships several <code>DocumentSplitter</code>s out of the box:</p>
<ul>
<li><p><code>DocumentByParagraphSplitter</code> — chunks by paragraphs (blank-line separated blocks)</p>
</li>
<li><p><code>DocumentByLineSplitter</code> — chunks by lines</p>
</li>
<li><p><code>DocumentBySentenceSplitter</code> — chunks by sentences (OpenNLP sentence detection)</p>
</li>
<li><p><code>DocumentByWordSplitter</code> — chunks by words</p>
</li>
<li><p><code>DocumentByCharacterSplitter</code> — fixed-size character chunks</p>
</li>
<li><p><code>DocumentSplitters.recursive(...)</code> — paragraphs first, falling back to lines, then sentences, then words for anything still too big</p>
</li>
</ul>
<p>We wrapped these in a small enum so the strategy is configurable and switchable at runtime:</p>
<pre><code class="language-java">public enum DocumentSplitterType {
    RECURSIVE("recursive"),
    PARAGRAPH("paragraph"),
    LINE("line"),
    SENTENCE("sentence"),
    WORD("word"),
    CHARACTER("character");

    public DocumentSplitter create(int maxChunkSize, int maxOverlap) {
        return switch (this) {
            case RECURSIVE -&gt; DocumentSplitters.recursive(maxChunkSize, maxOverlap);
            case PARAGRAPH -&gt; new DocumentByParagraphSplitter(maxChunkSize, maxOverlap);
            case LINE -&gt; new DocumentByLineSplitter(maxChunkSize, maxOverlap);
            case SENTENCE -&gt; new DocumentBySentenceSplitter(maxChunkSize, maxOverlap);
            case WORD -&gt; new DocumentByWordSplitter(maxChunkSize, maxOverlap);
            case CHARACTER -&gt; new DocumentByCharacterSplitter(maxChunkSize, maxOverlap);
        };
    }
}
</code></pre>
<p><code>DocumentService</code> uses the selected strategy together with a configurable chunk size and overlap:</p>
<pre><code class="language-java">private List&lt;TextSegment&gt; split(Document document) {
    String fileName = document.metadata().getString(FILE_NAME);
    return splitterType.create(maxChunkSize, maxOverlap).split(document).stream()
            .map(segment -&gt; withFileNamePrefix(segment, fileName))
            .toList();
}
</code></pre>
<h3>Why the file-name prefix?</h3>
<p>Each splitter copies the document's metadata into every <code>TextSegment</code>. On top of that we prepend the file name to the segment <em>text</em>. This is a documented LangChain4j trick: retrieval improves when every chunk knows where it came from, because the embedding of e.g. <code>rag.pdf\nRetrieval Augmented Generation combines...</code> is closer to a query about RAG than the bare sentence would be.</p>
<h2>Wiring It In</h2>
<p><code>SemanticSearchService</code> now delegates the load-parse-split stage and keeps only the embed-and-store stage:</p>
<pre><code class="language-java">public int indexDocument(Path filePath) {
    return indexSegments(documentService.loadAndSplit(filePath));
}

public int indexDirectory(Path directoryPath) {
    return indexSegments(documentService.loadAndSplitDirectory(directoryPath));
}

private int indexSegments(List&lt;TextSegment&gt; segments) {
    if (segments.isEmpty()) {
        return 0;
    }
    int sizeBefore = embeddingStore.size();
    List&lt;Embedding&gt; embeddings = embeddingModel.embedAll(segments).content();
    embeddingStore.addAll(embeddings, segments);
    save();
    return embeddingStore.size() - sizeBefore;
}
</code></pre>
<p>A nice side effect: without an in-place splitter anymore, <code>SemanticSearchService</code> just embeds and stores whatever segments it is given — the splitter lives where it belongs, in <code>DocumentService</code>.</p>
<h2>Trying It Out</h2>
<p>We added a sample PDF (<code>sample-data/rag.pdf</code>) and a <code>/splitter</code> command to the CLI. Index the directory — PDFs and text files alike:</p>
<pre><code class="language-plaintext">/index sample-data
Indexed 7 segment(s). Store now holds 7 embedding(s).
</code></pre>
<p>Then ask a question grounded in the PDF:</p>
<pre><code class="language-plaintext">/ask how does RAG combine a vector database with a chat model?
RAG &gt; RAG combines a vector database with a chat model so the model can answer
      questions using your own documents instead of relying only on training data.
</code></pre>
<p>Switch splitting strategies and see how the chunking changes:</p>
<pre><code class="language-plaintext">/splitter
Splitter type: recursive
Max chunk size: 200 chars
Max overlap   : 20 chars

/splitter paragraph
Switched splitter to 'paragraph'. Re-index documents to re-chunk them with the new strategy.
</code></pre>
<h2>Testing Without a Live Model</h2>
<p>Processing is fully testable offline. The test suite generates a real PDF at runtime with PDFBox, then asserts the parser extracts the expected text:</p>
<pre><code class="language-java">@Test
void parsesPdfDocuments() throws Exception {
    Path pdf = tempDir.resolve("guide.pdf");
    createPdf(pdf, "Retrieval augmented generation lets a chat model answer from your own documents.");

    DocumentService service = new DocumentService("recursive", 200, 20);

    List&lt;TextSegment&gt; segments = service.loadAndSplit(pdf);

    assertThat(segments).isNotEmpty();
    assertThat(segments.get(0).text()).contains("Retrieval augmented generation");
}
</code></pre>
<p>Another test asserts the file-name prefix lands on every segment, and a third proves smaller chunk sizes yield more segments — all without a single network call.</p>
<h2>Configuration</h2>
<table>
<thead>
<tr>
<th>Property</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>app.document.splitter</code></td>
<td><code>recursive</code></td>
<td>Splitting strategy (<code>recursive</code>, <code>paragraph</code>, <code>line</code>, <code>sentence</code>, <code>word</code>, <code>character</code>)</td>
</tr>
<tr>
<td><code>app.document.max-chunk-size</code></td>
<td><code>200</code></td>
<td>Max segment size in characters</td>
</tr>
<tr>
<td><code>app.document.max-overlap</code></td>
<td><code>20</code></td>
<td>Overlap between segments in characters</td>
</tr>
</tbody></table>
<h2>Next Steps</h2>
<p>Now that documents are first-class, the RAG question-answering from the previous post gets much better input. Natural next explorations: <code>DocumentBySentenceSplitter</code> tuning, <code>EmbeddingStoreIngestor</code>'s <code>textSegmentTransformer</code> for richer metadata, and — later in our checklist — chains and agents that operate on the processed documents.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/rag">Official LangChain4j RAG Documentation</a></p>
</li>
<li><p><a href="https://pdfbox.apache.org">Apache PDFBox</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/197">Document Processing Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Document processing is where RAG becomes real: PDF parsing, per-format parsers, and configurable splitting turn arbitrary files into clean, retrievable chunks. With LangChain4j's <code>DocumentParser</code> and <code>DocumentSplitter</code> interfaces, each stage is a small pluggable component — and with a dedicated <code>DocumentService</code>, the demo now separates <em>how documents become segments</em> from <em>how segments become searchable embeddings</em>.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Retrieval Augmented Generation (RAG) with LangChain4j]]></title><description><![CDATA[In the last few posts we gave our chatbot a memory and the ability to search documents by meaning. Now it's time to combine the two. A large language model knows a lot, but it doesn't know your docume]]></description><link>https://blog.prasadgaikwad.dev/retrieval-augmented-generation-rag-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/retrieval-augmented-generation-rag-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Wed, 05 Aug 2026 02:52:44 GMT</pubDate><content:encoded><![CDATA[<p>In the last few posts we gave our chatbot a memory and the ability to search documents by meaning. Now it's time to combine the two. A large language model knows a lot, but it doesn't know <em>your</em> documents. <strong>Retrieval Augmented Generation (RAG)</strong> fixes that: retrieve the most relevant chunks for a question, stuff them into the prompt, and let the model answer from your own data. In this post we'll wire that pipeline together with LangChain4j's <code>RetrievalAugmentor</code>.</p>
<h2>The RAG Pipeline</h2>
<p>RAG adds three stages in front of a normal chat call:</p>
<ol>
<li><p><strong>Query transformation</strong> — optionally rewrite the user's question (e.g. to compress a follow-up into a standalone query).</p>
</li>
<li><p><strong>Retrieval</strong> — embed the question and pull the most similar chunks out of the vector store.</p>
</li>
<li><p><strong>Aggregation + injection</strong> — combine the hits and append them to the user message before it reaches the model.</p>
</li>
</ol>
<p>LangChain4j models this as an interface: <code>RetrievalAugmentor</code>. The default implementation, <code>DefaultRetrievalAugmentor</code>, composes pluggable pieces — a <code>QueryTransformer</code>, a <code>ContentRetriever</code>, a <code>ContentAggregator</code>, and a <code>ContentInjector</code>. We only need to supply the retriever; sensible defaults handle the rest.</p>
<h2>A ContentRetriever That Uses Our Search Service</h2>
<p>The natural place to plug RAG in is our <code>SemanticSearchService</code> from the embeddings post. It owns both the embedding model and the store, so the retriever just delegates to it:</p>
<pre><code class="language-java">public class SemanticSearchContentRetriever implements ContentRetriever {

    private final SemanticSearchService searchService;
    private final int maxResults;

    public SemanticSearchContentRetriever(SemanticSearchService searchService, int maxResults) {
        this.searchService = searchService;
        this.maxResults = maxResults;
    }

    @Override
    public List&lt;Content&gt; retrieve(Query query) {
        return searchService.search(query.text(), maxResults).stream()
                .map(this::toContent)
                .toList();
    }

    private Content toContent(EmbeddingMatch&lt;TextSegment&gt; match) {
        return Content.from(match.embedded(), Map.of(ContentMetadata.SCORE, match.score()));
    }
}
</code></pre>
<p>Delegating keeps a single source of truth: the store <em>and</em> the current embedding model live in the service, so a runtime <code>/model</code> switch is picked up by the RAG flow automatically. LangChain4j also ships a ready-made <code>EmbeddingStoreContentRetriever</code> if you'd rather wire the store directly.</p>
<h2>Assembling the Augmentor</h2>
<p>In <code>AiConfig</code> we expose the retriever as a bean, wrap it in a <code>RetrievalAugmentor</code>, and attach the augmentor to a dedicated question-answering AI service:</p>
<pre><code class="language-java">@Bean
public ContentRetriever contentRetriever(SemanticSearchService searchService,
                                         @Value("${app.rag.max-results:5}") int maxResults) {
    return new SemanticSearchContentRetriever(searchService, maxResults);
}

@Bean
public RetrievalAugmentor retrievalAugmentor(ContentRetriever contentRetriever) {
    return DefaultRetrievalAugmentor.builder()
            .contentRetriever(contentRetriever)
            .build();
}

@Bean
public QaAssistant qaAssistant(ChatModel chatModel,
                               RetrievalAugmentor retrievalAugmentor,
                               ...) {
    return AiServices.builder(QaAssistant.class)
            .chatModel(chatModel)
            .chatMemoryProvider(createChatMemoryProvider(...))
            .retrievalAugmentor(retrievalAugmentor)
            .build();
}
</code></pre>
<p>The QA assistant is a plain AI service interface, just like the chat assistant — the only difference is the augmentor:</p>
<pre><code class="language-java">public interface QaAssistant {

    @SystemMessage("""
            You are a question-answering assistant that answers only from the provided context.
            Answer the question using the information supplied in the user message. If the context
            does not contain the answer, respond with "I don't know". Keep the answer concise.
            """)
    String ask(@MemoryId String memoryId, @UserMessage String question);
}
</code></pre>
<p>The <code>@MemoryId</code> gives each conversation its own memory, so follow-up questions work across turns.</p>
<h2>How the Pieces Fit Together</h2>
<p>When <code>ask()</code> is called, <code>AiServices</code> runs the augmentor before hitting the model. The <code>DefaultRetrievalAugmentor</code>:</p>
<ul>
<li><p>passes the question through the default <code>QueryTransformer</code> unchanged,</p>
</li>
<li><p>hands it to our <code>SemanticSearchContentRetriever</code>, which embeds it and searches the store,</p>
</li>
<li><p>aggregates the results with the default <code>ContentAggregator</code>,</p>
</li>
<li><p>and injects them into the user message via the default <code>ContentInjector</code>, producing something like:</p>
</li>
</ul>
<pre><code class="language-plaintext">How does RAG work?

Answer using the following information:
Retrieval Augmented Generation combines a vector database with a chat model ...
</code></pre>
<p>Then the chat model answers using only that context — grounded in your documents, not its own guesses.</p>
<h2>Trying It Out</h2>
<p>The CLI gained an <code>/ask</code> command that chains retrieval and generation. With the bundled sample documents indexed:</p>
<pre><code class="language-plaintext">/index sample-data
Indexed 6 segment(s). Store now holds 6 embedding(s).

/ask what is a vector database?
RAG &gt; A vector database stores embeddings (numerical representations of text)
      so that similar meanings can be found quickly by vector similarity.
</code></pre>
<p>Ask a follow-up and the memory kicks in:</p>
<pre><code class="language-plaintext">/ask why does that matter for answering questions?
RAG &gt; It lets the model find relevant information from your own documents
      instead of relying only on what it learned during training.
</code></pre>
<p>Because retrieval happens per question, the answers are grounded in the indexed material — and with an empty store the augmentor returns nothing, so the assistant honestly says it doesn't know rather than hallucinating.</p>
<h2>Testing the Pipeline Without a Live Model</h2>
<p>The best part of separating retrieval from generation is that retrieval is fully testable offline. With the deterministic <code>FakeEmbeddingModel</code> from the embeddings post, we index a document, then assert that the augmentor retrieves the right chunk and injects it into the user message:</p>
<pre><code class="language-java">SemanticSearchService searchService = new SemanticSearchService(
        modelName -&gt; new FakeEmbeddingModel(), "test", null, 5);
searchService.indexDirectory(docs);

ContentRetriever retriever = new SemanticSearchContentRetriever(searchService, 5);
RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
        .contentRetriever(retriever)
        .build();

AugmentationResult result = augmentor.augment(new AugmentationRequest(
        UserMessage.from("How does RAG work?"),
        Metadata.from(question, "qa", List.of())));

assertThat(result.contents()).isNotEmpty();
assertThat(((UserMessage) result.chatMessage()).singleText())
        .contains("Retrieval Augmented Generation");
</code></pre>
<p>This exercises the whole RAG flow — transform, retrieve, aggregate, inject — with zero network calls, which is exactly the kind of test you want before paying for model invocations.</p>
<h2>Configuration</h2>
<table>
<thead>
<tr>
<th>Property</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>app.rag.max-results</code></td>
<td><code>5</code></td>
<td>Max document chunks retrieved for each question</td>
</tr>
</tbody></table>
<h2>Next Steps</h2>
<p>Our RAG is grounded, but it's the simplest flavor. LangChain4j has more pieces to explore: <code>CompressingQueryTransformer</code> (rewrites follow-up questions using chat memory), re-ranking aggregators, and dedicated vector databases like <code>pgvector</code> or Elasticsearch that replace the in-memory store at scale. Document processing (parsing PDFs, splitting smarter) is the other natural next step.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev/tutorials/rag">Official LangChain4j RAG Documentation</a></p>
</li>
<li><p><a href="https://github.com/langchain4j/langchain4j-examples">GitHub Examples Repository</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/199">RAG Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>RAG turns a general-purpose chatbot into one that answers from your own knowledge base. With LangChain4j, that's a <code>ContentRetriever</code>, a <code>RetrievalAugmentor</code>, and one line on the <code>AiServices</code> builder — and because the pipeline is composed of small interfaces, the retrieval half is testable without any API calls. Combined with conversation memory and semantic search from earlier posts, the demo is now a complete grounded question-answering system.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Embeddings and Semantic Search with LangChain4j]]></title><description><![CDATA[In previous posts we got LangChain4j up and running and gave our chatbot memory. But chatting only goes so far — what if you want to search through a pile of documents using meaning rather than keywor]]></description><link>https://blog.prasadgaikwad.dev/embeddings-and-semantic-search-with-langchain4j</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/embeddings-and-semantic-search-with-langchain4j</guid><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Mon, 03 Aug 2026 23:50:47 GMT</pubDate><content:encoded><![CDATA[<p>In previous posts we got LangChain4j up and running and gave our chatbot memory. But chatting only goes so far — what if you want to search through a pile of documents using <em>meaning</em> rather than keywords? That's where embeddings come in. In this post we'll build a semantic search tool that indexes text files, stores their embeddings, and answers queries by finding the most <em>conceptually similar</em> content.</p>
<h2>What Is an Embedding?</h2>
<p>An embedding is a vector (a list of numbers) that represents the meaning of a piece of text. Texts with similar meanings end up with similar vectors — they sit close together in a high-dimensional space. "How do I fix my computer" and "my laptop is broken" will have nearby vectors, even though they share almost no words.</p>
<p>Semantic search works in two phases:</p>
<ol>
<li><p><strong>Indexing</strong> — split documents into chunks, embed each chunk, and store the vectors in a vector store.</p>
</li>
<li><p><strong>Querying</strong> — embed the user's question and find stored vectors that are closest to it (using cosine similarity).</p>
</li>
</ol>
<p>LangChain4j gives us the building blocks: <code>EmbeddingModel</code> (embeds text), <code>EmbeddingStore</code> (stores vectors), <code>EmbeddingStoreIngestor</code> (runs the indexing pipeline), and <code>DocumentSplitters</code> (chunks documents).</p>
<h2>The Semantic Search Service</h2>
<p>We added a <code>SemanticSearchService</code> that ties these pieces together. It's a Spring component that holds an <code>EmbeddingModel</code> and an <code>InMemoryEmbeddingStore</code>, which we persist to a JSON file.</p>
<pre><code class="language-java">@Service
public class SemanticSearchService {

    @Autowired
    public SemanticSearchService(Function&lt;String, EmbeddingModel&gt; modelFactory,
                                 @Value("${app.embedding.model-name:text-embedding-3-small}") String modelName,
                                 @Value("${app.embedding.store-path:}") String storePath,
                                 @Value("${app.embedding.max-results:5}") int defaultMaxResults) {
        this.modelFactory = modelFactory;
        this.modelName = modelName;
        this.embeddingModel = modelFactory.apply(modelName);
        this.storePath = storePath == null || storePath.isBlank() ? null : Path.of(storePath);
        this.defaultMaxResults = defaultMaxResults;
        this.embeddingStore = loadStore();
    }

    public int indexDirectory(Path directoryPath) {
        return ingest(FileSystemDocumentLoader.loadDocuments(directoryPath));
    }

    private int ingest(List&lt;Document&gt; documents) {
        int sizeBefore = embeddingStore.size();
        EmbeddingStoreIngestor.builder()
                .documentSplitter(DocumentSplitters.recursive(200, 20))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build()
                .ingest(documents);
        save();
        return embeddingStore.size() - sizeBefore;
    }

    public List&lt;EmbeddingMatch&lt;TextSegment&gt;&gt; search(String query) {
        Response&lt;Embedding&gt; response = embeddingModel.embed(query);
        return embeddingStore.search(EmbeddingSearchRequest.builder()
                        .queryEmbedding(response.content())
                        .maxResults(defaultMaxResults)
                        .build())
                .matches();
    }
}
</code></pre>
<p>A few highlights:</p>
<ul>
<li><p><code>FileSystemDocumentLoader</code> reads every file in a directory as a <code>Document</code>.</p>
</li>
<li><p><code>DocumentSplitters.recursive(200, 20)</code> chunks documents into segments of at most 200 characters with 20 characters of overlap, so meaning isn't cut off at chunk boundaries.</p>
</li>
<li><p><code>EmbeddingStoreIngestor</code> is the pipeline: split → embed → store. It returns token usage so you can see how much a document cost to embed.</p>
</li>
<li><p><code>InMemoryEmbeddingStore</code> supports <code>serializeToFile</code> / <code>fromFile</code>, so the store survives restarts. We load it in the constructor if the file already exists.</p>
</li>
</ul>
<h2>Switching Embedding Models</h2>
<p>The issue's checklist asked us to try different embedding models, so we made the model switchable at runtime. In <code>AiConfig</code> we expose a small factory bean:</p>
<pre><code class="language-java">@Bean
public Function&lt;String, EmbeddingModel&gt; embeddingModelFactory() {
    return modelName -&gt; OpenAiEmbeddingModel.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .modelName(modelName)
            .build();
}
</code></pre>
<p><code>SemanticSearchService</code> uses this factory to build its model, so <code>setEmbeddingModel("text-embedding-3-large")</code> swaps it on the fly. We support the three OpenAI embedding models:</p>
<pre><code class="language-plaintext">text-embedding-3-small | text-embedding-3-large | text-embedding-ada-002
</code></pre>
<p>Note that switching models changes the vector dimensions, so re-index your documents after a switch — that's why the CLI reminds you.</p>
<h2>Trying It Out</h2>
<p>The CLI (our <code>ChatCli</code> from the memory post) gained embedding commands alongside chat. Index the bundled sample documents and search:</p>
<pre><code class="language-plaintext">/index sample-data
Indexed 4 segment(s). Store now holds 4 embedding(s).

/search what is a vector database?
Top 5 results for: "what is a vector database?"
1. [score 0.5678] Retrieval Augmented Generation (RAG) combines a vector database with a chat model. ...
2. [score 0.4321] Embeddings are dense vector representations of text that capture semantic meaning. ...
</code></pre>
<p>The query never contains the exact phrase "vector database" from the top hit's first words, yet the model ranked it first because the <em>meaning</em> matches.</p>
<p>You can also inspect a raw vector:</p>
<pre><code class="language-plaintext">/embed hello world
Embedding (1536 dimensions) of "hello world":
[0.0051, -0.0123, 0.0098, ...]
</code></pre>
<h2>Configuration</h2>
<table>
<thead>
<tr>
<th>Property</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>app.embedding.model-name</code></td>
<td><code>text-embedding-3-small</code></td>
<td>Embedding model for indexing and search</td>
</tr>
<tr>
<td><code>app.embedding.store-path</code></td>
<td><code>embedding-store.json</code></td>
<td>JSON file where the store is persisted</td>
</tr>
<tr>
<td><code>app.embedding.max-results</code></td>
<td><code>5</code></td>
<td>Default number of search results</td>
</tr>
</tbody></table>
<h2>Next Steps</h2>
<p>Embeddings are the foundation of <strong>Retrieval Augmented Generation (RAG)</strong>: retrieve the most relevant chunks for a question, then feed them to the chat model so it can answer from your own documents. That's a natural next exploration — and it builds directly on the memory, chat, and embedding pieces we now have.</p>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev">Official LangChain4j Documentation</a></p>
</li>
<li><p><a href="https://github.com/langchain4j/langchain4j-examples">GitHub Examples Repository</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/196">Embeddings Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Embeddings turn unstructured text into geometry, and semantic search is just a nearest-neighbor query over that geometry. With LangChain4j's <code>EmbeddingStoreIngestor</code> and <code>InMemoryEmbeddingStore</code>, the whole indexing pipeline is a few lines of code — and the store even persists to a plain JSON file. Combined with conversation memory, we're one step away from a full RAG application.</p>
<p>The full implementation lives in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>. Stay tuned for RAG!</p>
]]></content:encoded></item><item><title><![CDATA[Conversation Memory with LangChain4j: Keeping Context Across Turns]]></title><description><![CDATA[In our previous post, we built a simple command-line chat with LangChain4j. It worked, but there was one big limitation: the model had no memory. Every question was answered in isolation, as if the co]]></description><link>https://blog.prasadgaikwad.dev/conversation-memory-with-langchain4j-keeping-context-across-turns</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/conversation-memory-with-langchain4j-keeping-context-across-turns</guid><category><![CDATA[langchain4j]]></category><category><![CDATA[memory]]></category><category><![CDATA[#agent]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sun, 02 Aug 2026 02:26:00 GMT</pubDate><content:encoded><![CDATA[<p>In our <a href="https://blog.prasadgaikwad.dev/getting-started-with-langchain4j-building-your-first-ai-powered-java-application">previous post</a>, we built a simple command-line chat with LangChain4j. It worked, but there was one big limitation: the model had no memory. Every question was answered in isolation, as if the conversation had never happened. In this post, we'll fix that by adding conversation memory — and we'll explore two different memory strategies you can switch between at runtime.</p>
<h2>Why Does a Chatbot Need Memory?</h2>
<p>Large language models are stateless. Each request is processed independently, and the model has no idea what you asked it one minute ago. To hold a real conversation, the application must keep the history and send it along with every new question.</p>
<p>This is exactly what LangChain4j's memory module does. It stores the conversation, decides what to include on each call, and automatically manages the size of what's sent so you stay within the model's context window.</p>
<h2>Two Memory Strategies</h2>
<p>LangChain4j 1.18.1 ships two built-in <code>ChatMemory</code> implementations, each controlling the sliding window differently:</p>
<ol>
<li><p><strong>MessageWindowChatMemory</strong> — a buffer limited by the <em>number of messages</em>. When the window is full, the oldest messages are evicted.</p>
</li>
<li><p><strong>TokenWindowChatMemory</strong> — a window limited by the <em>token budget</em>. Messages are evicted until the history fits within a configured token count, using an actual tokenizer (in our case, OpenAI's).</p>
</li>
</ol>
<blockquote>
<p><strong>Note:</strong> Older LangChain4j versions listed "Summary" and "Vector" memory types. These are no longer part of the current API — the two sliding-window strategies above are what's available today, and they map nicely to the classic "buffer" and "context-window" ideas.</p>
</blockquote>
<h2>Setup</h2>
<p>We start from the getting-started project and add the core <code>langchain4j</code> module, which contains <code>AiServices</code> and the memory implementations:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
    &lt;artifactId&gt;langchain4j-open-ai&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- Adds AiServices, MessageWindowChatMemory, TokenWindowChatMemory, ... --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
    &lt;artifactId&gt;langchain4j&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<h2>Defining an AI Service</h2>
<p>The idiomatic way to add memory in LangChain4j is through an <strong>AI Service</strong> — a plain Java interface that <code>AiServices</code> turns into a working implementation.</p>
<pre><code class="language-java">public interface Assistant {

    @SystemMessage("You are a helpful assistant. Answer the question in a very concise way, only in 2 sentences maximum.")
    String chat(@MemoryId String memoryId, @UserMessage String message);
}
</code></pre>
<p>The annotations tell LangChain4j everything it needs:</p>
<ul>
<li><p><code>@SystemMessage</code> — the system prompt injected on every call.</p>
</li>
<li><p><code>@MemoryId</code> — selects which conversation's memory to use, so you can have one memory per user or per chat.</p>
</li>
<li><p><code>@UserMessage</code> — marks the parameter holding the user's input.</p>
</li>
</ul>
<h2>Wiring It Up with Spring</h2>
<p>Next we define the beans: a <code>ChatModel</code>, and the <code>Assistant</code> built with a <code>ChatMemoryProvider</code>. The provider is called by <code>AiServices</code> the first time a new memory ID is seen, so we can decide which memory type to create based on the ID itself. This is the trick that lets us switch memory types at runtime — the memory type is embedded in the memory ID.</p>
<pre><code class="language-java">@Configuration
public class AiConfig {

    @Bean
    public ChatModel chatModel(@Value("${app.chat.model-name:gpt-4o-mini}") String modelName) {
        return OpenAiChatModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .modelName(modelName)
                .build();
    }

    @Bean
    public Assistant assistant(ChatModel chatModel,
                               ChatMemoryRegistry chatMemoryRegistry,
                               @Value("${app.chat.model-name:gpt-4o-mini}") String modelName,
                               @Value("${app.memory.max-messages:10}") int maxMessages,
                               @Value("${app.memory.max-tokens:2000}") int maxTokens) {
        ChatMemoryProvider chatMemoryProvider = memoryId -&gt; {
            ChatMemory chatMemory = createMemory((String) memoryId, modelName, maxMessages, maxTokens);
            chatMemoryRegistry.register((String) memoryId, chatMemory);
            return chatMemory;
        };

        return AiServices.builder(Assistant.class)
                .chatModel(chatModel)
                .chatMemoryProvider(chatMemoryProvider)
                .build();
    }

    private ChatMemory createMemory(String memoryId, String modelName, int maxMessages, int maxTokens) {
        if (memoryId.startsWith(MemoryType.MESSAGE_WINDOW.label())) {
            return MessageWindowChatMemory.builder()
                    .id(memoryId)
                    .maxMessages(maxMessages)
                    .build();
        }
        return TokenWindowChatMemory.builder()
                .id(memoryId)
                .maxTokens(maxTokens, new OpenAiTokenCountEstimator(modelName))
                .build();
    }
}
</code></pre>
<p>The <code>MemoryType</code> enum encodes each strategy with a label and produces the memory ID:</p>
<pre><code class="language-java">public enum MemoryType {

    MESSAGE_WINDOW("message-window"),
    TOKEN_WINDOW("token-window");

    // ...

    public String memoryId(String conversationId) {
        return label + ":" + conversationId;
    }
}
</code></pre>
<p>So a conversation id of <code>main</code> becomes either <code>message-window:main</code> or <code>token-window:main</code>. When you switch strategies, the ID changes, <code>AiServices</code> sees a new memory ID, and the provider hands back a fresh memory of the new type. Switching types effectively starts a new conversation — which is exactly the behavior you'd want.</p>
<p><code>AiServices</code> retains the created memories itself, but it doesn't expose them. To let the CLI inspect and clear memory, we keep a small registry:</p>
<pre><code class="language-java">@Component
public class ChatMemoryRegistry {

    private final ConcurrentMap&lt;String, ChatMemory&gt; memories = new ConcurrentHashMap&lt;&gt;();

    public void register(String memoryId, ChatMemory memory) {
        memories.put(memoryId, memory);
    }

    public ChatMemory get(String memoryId) {
        return memories.get(memoryId);
    }
}
</code></pre>
<h2>The Command-Line Interface</h2>
<p>We moved the interactive loop out of the main application class into a dedicated <code>ChatCli</code> component, gated by a property so tests can load the Spring context without blocking on stdin.</p>
<pre><code class="language-properties">app.cli.enabled=true
app.chat.model-name=gpt-4o-mini
app.memory.max-messages=10
app.memory.max-tokens=2000
</code></pre>
<p>The CLI now understands a few commands alongside plain chat:</p>
<pre><code class="language-plaintext">/help                 Show this help
/memory               Show current memory type and state
/memory &lt;type&gt;        Switch memory type (message-window | token-window)
/clear                Clear the current conversation memory
quit                  Exit the application
</code></pre>
<p>Try it: ask <em>"My name is Alice"</em>, then follow up with <em>"What is my name?"</em>. With memory enabled, the model remembers. Then run <code>/memory token-window</code> and notice the conversation history was reset — a new memory of the new type starts fresh.</p>
<h2>Running the Application</h2>
<ol>
<li><p>Set your API key: <code>export OPENAI_API_KEY=...</code></p>
</li>
<li><p>Run it: <code>./mvnw spring-boot:run</code></p>
</li>
<li><p>Chat, and use <code>/memory</code> and <code>/clear</code> to explore how each memory type behaves.</p>
</li>
</ol>
<h2>Next Steps</h2>
<p>Conversation memory is the foundation for much more advanced features:</p>
<ol>
<li><p><strong>RAG (Retrieval Augmented Generation)</strong> — combine memory with document retrieval</p>
</li>
<li><p><strong>Chains and Agents</strong> — let the model use tools, with full conversation context</p>
</li>
<li><p><strong>Embeddings</strong> — semantic search over past conversations</p>
</li>
<li><p><strong>Streaming responses</strong> — stream tokens back to the user</p>
</li>
</ol>
<h2>Resources</h2>
<ul>
<li><p><a href="https://docs.langchain4j.dev">Official LangChain4j Documentation</a></p>
</li>
<li><p><a href="https://github.com/langchain4j/langchain4j-examples">GitHub Examples Repository</a></p>
</li>
<li><p><a href="https://github.com/prasadgaikwad/langchain4j-demo/issues/195">Conversation Memory Issue</a></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Adding memory transforms a stateless question-answer loop into a real conversation. With LangChain4j's AI Services and <code>@MemoryId</code>, it takes only a few lines of code, and the built-in <code>MessageWindowChatMemory</code> and <code>TokenWindowChatMemory</code> give you two ways to manage the context window — switchable at runtime.</p>
<p>Check out the full implementation in our <a href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a>. Stay tuned for more explorations — RAG, agents, and tool use are next on the list!</p>
]]></content:encoded></item><item><title><![CDATA[Supercharge Spring Data JPA: Dynamic Filtering & Performance Optimization with Slices]]></title><description><![CDATA[Building efficient search APIs in Spring Boot often leads to two common problems: infinite boilerplate code for dynamic filtering and performance bottlenecks caused by unnecessary count queries.
In this post, I'll share how we solved both using a Gen...]]></description><link>https://blog.prasadgaikwad.dev/supercharge-spring-data-jpa-dynamic-filtering-and-performance-optimization-with-slices</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/supercharge-spring-data-jpa-dynamic-filtering-and-performance-optimization-with-slices</guid><category><![CDATA[Spring]]></category><category><![CDATA[Spring Data Jpa]]></category><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Sun, 08 Feb 2026 23:12:18 GMT</pubDate><content:encoded><![CDATA[<p>Building efficient search APIs in Spring Boot often leads to two common problems: infinite boilerplate code for dynamic filtering and performance bottlenecks caused by unnecessary count queries.</p>
<p>In this post, I'll share how we solved both using a <strong>Generic Specification Repository</strong> and <strong>Slice-based Pagination</strong>.</p>
<h2 id="heading-1-the-problem-boilerplate-amp-heavy-queries"><strong>1. The Problem: Boilerplate &amp; Heavy Queries</strong></h2>
<h3 id="heading-the-boilerplate-trap"><strong>The Boilerplate Trap</strong></h3>
<p>Typically, allowing users to filter by <code>name</code>, <code>status</code>, <code>createdDate</code>, etc., requires writing endless custom repository methods or complex <code>CriteriaBuilder</code> logic.</p>
<h3 id="heading-the-count-query-killer"><strong>The Count Query Killer</strong></h3>
<p>Standard pagination (<code>Page&lt;T&gt;</code>) executing <code>findAll(Pageable)</code> triggers two queries:</p>
<ol>
<li><p>The actual data query (<code>SELECT ... LIMIT ? OFFSET ?</code>).</p>
</li>
<li><p>A total count query (<code>SELECT COUNT(*) ...</code>).</p>
</li>
</ol>
<p>For large tables, <strong>the count query is a performance killer</strong>. If you're building an "Infinite Scroll" or "Load More" feature, you don't even <em>need</em> the total count—you just need to know if there's a "next page".</p>
<hr />
<h2 id="heading-2-the-solution-generic-specification-repository"><strong>2. The Solution: Generic Specification Repository</strong></h2>
<p>We implemented a wrapper around <code>JpaSpecificationExecutor</code> that accepts a dynamic <code>SearchRequest</code>. This allows us to filter <strong>any entity</strong> without writing custom query code.</p>
<h3 id="heading-the-interface"><strong>The Interface</strong></h3>
<pre><code class="lang-java"><span class="hljs-meta">@NoRepositoryBean</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">SpecificationRepository</span>&lt;<span class="hljs-title">T</span>, <span class="hljs-title">ID</span>&gt; <span class="hljs-keyword">extends</span> <span class="hljs-title">JpaRepository</span>&lt;<span class="hljs-title">T</span>, <span class="hljs-title">ID</span>&gt;, <span class="hljs-title">SliceSpecificationExecutor</span>&lt;<span class="hljs-title">T</span>&gt; </span>{    
    <span class="hljs-comment">// Inherits findAll(Specification&lt;T&gt;, Pageable)    </span>
    <span class="hljs-comment">// Inherits findAllSliced(Specification&lt;T&gt;, Pageable)</span>
}
</code></pre>
<h3 id="heading-the-usage"><strong>The Usage</strong></h3>
<p>Now, any repository can extend this and instantly gain dynamic search powers:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ThreatActorRepository</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">SpecificationRepository</span>&lt;<span class="hljs-title">ThreatActor</span>, <span class="hljs-title">UUID</span>&gt; </span>{}
</code></pre>
<p>The Service layer simply builds the specification from a JSON request:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> Page&lt;ThreatActor&gt; <span class="hljs-title">search</span><span class="hljs-params">(SearchRequest request)</span> </span>{    
    Specification&lt;ThreatActor&gt; spec = GenericSpecificationBuilder.buildFromRequest(request);    
    <span class="hljs-keyword">return</span> repository.findAll(spec, pageable);
}
</code></pre>
<hr />
<h2 id="heading-3-the-optimization-slice-vs-page"><strong>3. The Optimization: Slice vs. Page</strong></h2>
<p>To solve the count query performance issue, we implemented <code>SliceSpecificationExecutor</code>.</p>
<h3 id="heading-what-is-a-slice"><strong>What is a Slice?</strong></h3>
<p>A <code>Slice</code> in Spring Data holds a chunk of data and knows if there is a next slice (<code>hasNext()</code>), but it <strong>does not</strong> know the total number of pages.</p>
<h3 id="heading-implementing-findallsliced"><strong>Implementing</strong> <code>findAllSliced</code></h3>
<p>We leveraged Spring Data's <code>Window</code> API (introduced recently) to fetch results efficiently:</p>
<pre><code class="lang-java">SliceSpecificationExecutor.<span class="hljs-function">javadefault Slice&lt;T&gt; <span class="hljs-title">findAllSliced</span><span class="hljs-params">(Specification&lt;T&gt; spec, Pageable pageable)</span> </span>{    <span class="hljs-comment">// Uses a "Window" to fetch size + 1 items to determine if a next page exists    // completely avoiding the SELECT COUNT(*) query.    Window&lt;T&gt; window = this.findBy(spec, ...);    return new SliceImpl&lt;&gt;(window.getContent(), pageable, window.hasNext());}</span>
</code></pre>
<h3 id="heading-the-performance-win"><strong>The Performance Win</strong></h3>
<p>By switching from <code>/search</code> (Page) to <code>/search-sliced</code> (Slice), we eliminate the heavy count query entirely.</p>
<p><strong>SQL Comparison:</strong></p>
<p><strong>Regular Page Request:</strong></p>
<pre><code class="lang-bash">sqlHibernate: select ... from threat_actors <span class="hljs-built_in">limit</span> ? offset ?Hibernate: select count(*) from threat_actors ... -- 🛑 EXPENSIVE on large tables
</code></pre>
<p><strong>Slice Request:</strong></p>
<pre><code class="lang-bash">sqlHibernate: select ... from threat_actors <span class="hljs-built_in">limit</span> ? offset ? -- ✅ ONLY ONE QUERY
</code></pre>
<hr />
<h2 id="heading-4-conclusion"><strong>4. Conclusion</strong></h2>
<p>By combining the <strong>Specification Pattern</strong> with <strong>Slice Pagination</strong>, we achieved:</p>
<ol>
<li><p><strong>Cleaner Code:</strong> Zero boilerplate for dynamic filters.</p>
</li>
<li><p><strong>Better Performance:</strong> 50% fewer queries for infinite scroll views.</p>
</li>
</ol>
<p>Check out the full implementation in the <a target="_blank" href="https://github.com/prasadgaikwad/spring-data-jpa-specification">repository</a>!</p>
<h3 id="heading-references"><strong>References</strong></h3>
<ul>
<li><p><a target="_blank" href="https://github.com/prasadgaikwad/spring-data-jpa-specification">https://github.com/prasadgaikwad/spring-data-jpa-specification</a></p>
</li>
<li><p><a target="_blank" href="https://gist.github.com/josergdev/06c82891a719eca4834410339885ad23">https://gist.github.com/josergdev/06c82891a719eca4834410339885ad23</a></p>
</li>
<li><p><a target="_blank" href="https://docs.spring.io/spring-data/jpa/reference/jpa/specifications.html">Spring Data JPA Specifications</a></p>
</li>
<li><p><a target="_blank" href="https://vladmihalcea.com/spring-data-jpa-specification/">Vlad Mihalcea: The best way to use the Spring Data JPA Specification</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Getting Started with LangChain4j: Building Your First AI-Powered Java Application]]></title><description><![CDATA[In this blog post, we'll walk through creating your first LangChain4j application using Spring Boot. We'll build a simple interactive command-line application that demonstrates the basic setup and usage of LangChain4j in a Java environment.
What is L...]]></description><link>https://blog.prasadgaikwad.dev/getting-started-with-langchain4j-building-your-first-ai-powered-java-application</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/getting-started-with-langchain4j-building-your-first-ai-powered-java-application</guid><category><![CDATA[langchain4j]]></category><category><![CDATA[Java]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Tue, 04 Nov 2025 04:14:03 GMT</pubDate><content:encoded><![CDATA[<p>In this blog post, we'll walk through creating your first LangChain4j application using Spring Boot. We'll build a simple interactive command-line application that demonstrates the basic setup and usage of LangChain4j in a Java environment.</p>
<h2 id="heading-what-is-langchain4j"><strong>What is LangChain4j?</strong></h2>
<p>LangChain4j is a powerful Java framework designed to simplify the development of applications powered by Large Language Models (LLMs). It provides a comprehensive set of tools and abstractions that make it easier to build sophisticated AI-powered applications.</p>
<h2 id="heading-project-setup"><strong>Project Setup</strong></h2>
<h3 id="heading-prerequisites"><strong>Prerequisites</strong></h3>
<ul>
<li><p>Java 17 or higher</p>
</li>
<li><p>Maven</p>
</li>
<li><p>Your favorite IDE (IntelliJ IDEA, Eclipse, or VS Code)</p>
</li>
<li><p>An API key from your chosen LLM provider (e.g., OpenAI)</p>
</li>
</ul>
<h3 id="heading-step-1-create-a-spring-boot-project"><strong>Step 1: Create a Spring Boot Project</strong></h3>
<p>Start by creating a new Spring Boot project using Spring Initializr or your IDE. We'll need the following dependencies:</p>
<ul>
<li><p>Spring Boot Starter</p>
</li>
<li><p>LangChain4j BOM (Bill of Materials)</p>
</li>
</ul>
<p>Here's our <code>pom.xml</code> configuration:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- Parent Spring Boot --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.springframework.boot<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spring-boot-starter-parent<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>3.5.7<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>

<span class="hljs-comment">&lt;!-- LangChain4j BOM for version management --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dependencyManagement</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>dev.langchain4j<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>langchain4j-bom<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>1.8.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">type</span>&gt;</span>pom<span class="hljs-tag">&lt;/<span class="hljs-name">type</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">scope</span>&gt;</span>import<span class="hljs-tag">&lt;/<span class="hljs-name">scope</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">dependencies</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dependencyManagement</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Spring Boot Starter --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.springframework.boot<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spring-boot-starter<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>

    <span class="hljs-comment">&lt;!-- LangChain4j OpenAI Integration --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>dev.langchain4j<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>langchain4j-open-ai<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dependencies</span>&gt;</span>
</code></pre>
<h3 id="heading-step-2-configuration"><strong>Step 2: Configuration</strong></h3>
<p>Create or update your <code>.env</code> file: (make sure to add this .env file to your .gitignore to keep your API key secure)</p>
<pre><code class="lang-plaintext"># OpenAI API Configuration
OPENAI_API_KEY=your-api-key-here
</code></pre>
<h3 id="heading-step-3-creating-the-application"><strong>Step 3: Creating the Application</strong></h3>
<p>We'll create a simple command-line application that:</p>
<ol>
<li><p>Asks for user's question</p>
</li>
<li><p>Answers concisely using the OpenAI GPT-4o-mini model</p>
</li>
<li><p>Continues interaction until they type 'quit'</p>
</li>
</ol>
<p>The implementation uses Spring's CommandLineRunner for the interactive loop.</p>
<h2 id="heading-understanding-the-code"><strong>Understanding the Code</strong></h2>
<p>Let's break down the key components:</p>
<ol>
<li><strong>The Main Application Class</strong></li>
</ol>
<pre><code class="lang-java">
<span class="hljs-meta">@SpringBootApplication</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LangChain4jDemoApplication</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">CommandLineRunner</span> </span>{

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{
        SpringApplication.run(LangChain4jDemoApplication.class, args);
    }

    /**
     * A simple command-line application that interacts with the user,
     * takes a question as input, and provides a concise answer using
     * the OpenAI GPT-<span class="hljs-number">4</span>o-mini model.
     */
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">(String... args)</span> </span>{
        ChatModel model = OpenAiChatModel.builder()
                .apiKey(System.getenv(<span class="hljs-string">"OPENAI_API_KEY"</span>))
                .modelName(<span class="hljs-string">"gpt-4o-mini"</span>)
                .build();

        <span class="hljs-keyword">try</span> (Scanner scanner = <span class="hljs-keyword">new</span> Scanner(System.in)) {
            <span class="hljs-keyword">while</span> (<span class="hljs-keyword">true</span>) {
                System.out.print(<span class="hljs-string">"Please enter your question (type 'quit' to exit): "</span>);
                String question = scanner.nextLine();

                <span class="hljs-keyword">if</span> (<span class="hljs-string">"quit"</span>.equalsIgnoreCase(question)) {
                    System.out.println(<span class="hljs-string">"Goodbye!"</span>);
                    <span class="hljs-keyword">break</span>;
                }

                String response = model.chat(<span class="hljs-string">"You are an helpful assistant. "</span> +
                        <span class="hljs-string">"Answer this question in very concise way, only in 2 sentences maximum. Question: "</span> + question);
                System.out.println(<span class="hljs-string">"Answer: "</span> + response);
                System.out.println(); <span class="hljs-comment">// Add a blank line for better readability</span>
            }
        }
    }
}
</code></pre>
<h2 id="heading-running-the-application"><strong>Running the Application</strong></h2>
<p>To run the application:</p>
<ol>
<li><p>Ensure you have set your API key in <code>.env</code> file</p>
</li>
<li><p>Run the following command:</p>
<pre><code class="lang-plaintext"> ./mvnw spring-boot:run
</code></pre>
</li>
</ol>
<h2 id="heading-next-steps"><strong>Next Steps</strong></h2>
<p>This basic setup provides a foundation for exploring more advanced LangChain4j features:</p>
<ol>
<li><p><strong>Adding LLM Integration</strong></p>
<ul>
<li><p>Implement chat completions</p>
</li>
<li><p>Add streaming responses</p>
</li>
<li><p>Experiment with different models</p>
</li>
</ul>
</li>
<li><p><strong>Implementing Memory</strong></p>
<ul>
<li><p>Add conversation history</p>
</li>
<li><p>Implement different memory types</p>
</li>
</ul>
</li>
<li><p><strong>Creating Chains</strong></p>
<ul>
<li><p>Build processing pipelines</p>
</li>
<li><p>Add prompt templates</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<p>For more information and advanced features, check out:</p>
<ul>
<li><p><a target="_blank" href="https://docs.langchain4j.dev/">Official LangChain4j Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/langchain4j/langchain4j-examples">GitHub Examples Repository</a></p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>This simple example demonstrates how easy it is to get started with LangChain4j. The framework's integration with Spring Boot makes it particularly attractive for Java developers looking to build AI-powered applications.</p>
<p>Stay tuned for more blog posts where we'll explore advanced features like:</p>
<ul>
<li><p>Working with different LLM providers</p>
</li>
<li><p>Implementing RAG (Retrieval Augmented Generation)</p>
</li>
<li><p>Building custom agents and tools</p>
</li>
<li><p>Creating sophisticated conversation chains</p>
</li>
</ul>
<p>Remember to check out our <a target="_blank" href="https://github.com/prasadgaikwad/langchain4j-demo">GitHub repository</a> for the complete source code and future updates!</p>
]]></content:encoded></item><item><title><![CDATA[How I created my blog in 10 minutes!]]></title><description><![CDATA[Note: The steps mentioned here are specifically for Google DNS, a .dev domain and Hashnode.

So I wanted to create my blog and online profile for quite some time but was not sure about following:

How to start the blog?
How to register a domain name ...]]></description><link>https://blog.prasadgaikwad.dev/how-i-created-my-blog-in-10-minutes</link><guid isPermaLink="true">https://blog.prasadgaikwad.dev/how-i-created-my-blog-in-10-minutes</guid><category><![CDATA[Developer Blogging]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Blogging]]></category><category><![CDATA[blog]]></category><category><![CDATA[Hashnode]]></category><dc:creator><![CDATA[Prasad Gaikwad]]></dc:creator><pubDate>Fri, 01 Jan 2021 16:18:34 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>Note: The steps mentioned here are specifically for Google DNS, a .dev domain and Hashnode.</p>
</blockquote>
<p>So I wanted to create my blog and online profile for quite some time but was not sure about following:</p>
<ol>
<li>How to start the blog?</li>
<li>How to register a domain name and setup?</li>
<li>Should I use any blogging platform or build my own blog?</li>
<li>Which blogging platform to use?</li>
</ol>
<p>And after researching for a while, I decided to create my blog today to check this item off my list on very first day of the year :)</p>
<p>Here is a short tutorial on how to create a blog, setup your own domain name and use the domain name to redirect to your blog, all about in 10 minutes. (off course it took some time for me to research on the options for blogging platforms, domain name service providers etc, but actual process of blog setup took about 10 minutes!)</p>
<h1 id="my-choices">My Choices</h1>
<ul>
<li>I chose <a target="_blank" href="https://domains.google.com/">Google Domains</a> as my domain name service (DNS) provider to avoid getting too much into the weeds for searching a DNS provider. During my initial research, I found it has features which I required to get started quickly. </li>
<li>I picked <a target="_blank" href="https://hashnode.com/">Hashnode</a> as my blogging platform since I found that it provides a lot of useful features in addition to the ease of creating and maintaining a blog.</li>
</ul>
<h1 id="prerequisites">Prerequisites</h1>
<ol>
<li>You need a google/gmail account to buy domain name from google.</li>
<li>Create an account with Hashnode to get you started (you don't need steps for this, believe me ;)) </li>
</ol>
<h1 id="steps">Steps</h1>
<ol>
<li>Search for your required domain name at <a target="_blank" href="https://domains.google.com/">Google Domains</a>, add it to the cart and proceed.
You should see something like following on next screen:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1609529870646/WLq97Uwyg.png" alt="image.png" /></li>
<li>Provide required contact information on checkout screen. 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1609530136994/ZtlCq1VK2.png" alt="image.png" /></li>
<li>Provide payment information and checkout. That's it! You own the domain name now! You may have to verify your email address for the domain purchase.</li>
<li>On your Hashnode blog dashboard page, add your domain name (in my case I used a subdomain blog.prasadgaikwad.dev) under DOMAIN tab and click update. You will see next steps on the same page as shown below:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1609530572227/rxmEuzi2WJ.png" alt="image.png" /></li>
<li>Go back to your Google Domains dashboard and select the newly created domain name to go into its settings.</li>
<li>Go to DNS section and scroll to the "Custom resource records" section. Add CNAME record as shown below:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1609530956828/y2Idz3sBS.png" alt="image.png" />
(Note that I am using a subdomain "blog" since Google Domains does not support CNAME record for root domain. You can create any subdomain)</li>
<li>Wait for few minutes and go to your subdomain url, you should see your Hashnode blog!
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1609531345453/e9u7ckSJo.png" alt="image.png" /></li>
<li>Wait for it...you may also want to redirect your root domain (in my case  <a target="_blank" href="https://prasadgaikwad.dev">prasadgaikwad.dev</a>  and  <a target="_blank" href="https://www.prasadgaikwad.dev">www.prasadgaikwad.dev</a>), then go the Website section of your Google Domains page and add Forward domain details. (In my case I forwarded my root domain to my blog subdomain for now!)
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1609531593479/FKspWiQib.png" alt="image.png" /></li>
<li>You may have to hard refresh (command + r) browser page with your domain url, to delete any cached pages or errors.</li>
<li>And that's all you need to get your own developer blog started on your custom domain.</li>
</ol>
<h1 id="references">References:</h1>
<ol>
<li><a target="_blank" href="https://hashnode.com/post/how-to-set-up-a-custom-domain-on-devblog-cjvoymax9001u8xs1i9f3077e">How to Set Up a Custom Domain on Devblog</a> </li>
<li><a target="_blank" href="https://www.freecodecamp.org/news/devblog-launch-your-developer-blog-own-domain/">Hashnode: How to Launch Your Own Developer Blog on Your Own Domain in Minutes</a> </li>
</ol>
<p>(I created this blog post to document steps which I could not find in reference blogs, so that it might be useful if someone decides to use these platforms like me) </p>
]]></content:encoded></item></channel></rss>