Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ public static void main(String[] args) {
public CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder) {

return args -> {
Map<String, String> supportRoutes = Map.of("billing",
Map<String, String> routeMap = Map.of(
"billing", "Billing and money related problem handling",
"technical", "Technical level problem handling",
"account", "Account security related problem handling",
"product", "Product and feature related problem handling"
);

Map<String, String> promptMap = Map.of("billing",
"""
You are a billing support specialist. Follow these guidelines:
1. Always start with "Billing Support Response:"
Expand Down Expand Up @@ -113,15 +120,25 @@ public CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder)
Best regards,
Mike""");

var routerWorkflow = new RoutingWorkflow(chatClientBuilder.build());
// Select a proper chat client for responses
ChatClient chatClient = chatClientBuilder.build();

// Select a proper chat client for routing task
ChatClient routingChatClient = chatClientBuilder.build();

var routerWorkflow = new RoutingWorkflow(routingChatClient);

int i = 1;
for (String ticket : tickets) {
System.out.println("\nTicket " + i++);
System.out.println("------------------------------------------------------------");
System.out.println(ticket);
System.out.println("------------------------------------------------------------");
System.out.println(routerWorkflow.route(ticket, supportRoutes));
String route = routerWorkflow.route(ticket, routeMap);
String prompt = promptMap.get(route);

String response = chatClient.prompt(prompt + "\nInput: " + ticket).call().content();
System.out.println(response);
}

};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2024 - 2024 the original author or authors.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* https://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -15,6 +15,8 @@
*/
package com.example.agentic;

import com.fasterxml.jackson.annotation.JsonPropertyDescription;

/**
* Record representing the response from the routing classification process.
*
Expand All @@ -32,15 +34,14 @@
* @see RoutingWorkflow
*/
public record RoutingResponse(
/**
* The reasoning behind the route selection, explaining why this particular
* route was chosen based on the input analysis.
*/

@JsonPropertyDescription(
"The reasoning behind the route selection, explaining why this particular " +
"route was chosen based on the input analysis. Consider factors like key terms, " +
"intent, and urgency level."
)
String reasoning,

/**
* The selected route name that will handle the input based on the
* classification analysis.
*/
@JsonPropertyDescription("The selected route key")
String selection) {
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.agentic;

import java.util.Map;
import java.util.Objects;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.util.Assert;

/**
* Implements the Routing workflow pattern that classifies input and directs it
* to specialized
* followup tasks. This workflow enables separation of concerns by routing
* different types
* of inputs to specialized prompts and processes optimized for specific
* categories.
*
* Routing workflow that uses an LLM to analyze input and select the most
* appropriate route from a set of available options. The workflow focuses on
* high-quality classification and returns the selected route key together with
* model reasoning (captured internally as {@link RoutingResponse}).
*
* <p>
* The routing workflow is particularly effective for complex tasks where:
* <ul>
Expand All @@ -38,42 +37,33 @@
* <li>Different types of input require different specialized processing or
* expertise</li>
* </ul>
*
*
* <p>
* Common use cases include:
* Key characteristics:
* <ul>
* <li>Customer support systems routing different types of queries (billing,
* technical, etc.)</li>
* <li>Content moderation systems routing content to appropriate review
* processes</li>
* <li>Query optimization by routing simple/complex questions to different model
* capabilities</li>
* <li>LLM-driven content analysis and classification</li>
* <li>Clear separation of concerns: classification yields a route key that
* downstream components can act on</li>
* <li>Extensible catalogue of routes defined by the caller</li>
* </ul>
*
* <p>
* This implementation allows for dynamic routing based on content
* classification,
* with each route having its own specialized prompt optimized for specific
* types of input.
*
* <p/>
* Implementation uses the <a href=
* The implementation uses the <a href=
* "https://docs.spring.io/spring-ai/reference/1.0/api/structured-output-converter.html">Spring
* AI Structure Output</a> to convert the chat client response into a structured
* {@link RoutingResponse} object.
*
* @author Christian Tzolov
* @see org.springframework.ai.chat.client.ChatClient
* AI Structured Output</a> feature to deserialize the model response into a
* {@link RoutingResponse}.
*
* @author Christian Tzolov, Joonas Vali
* @see ChatClient
* @see <a href=
* "https://docs.spring.io/spring-ai/reference/1.0/api/chatclient.html">Spring
* AI ChatClient</a>
* "https://docs.spring.io/spring-ai/reference/1.0/api/chatclient.html">Spring
* AI ChatClient</a>
* @see <a href=
* "https://www.anthropic.com/research/building-effective-agents">Building
* Effective Agents</a>
* "https://www.anthropic.com/research/building-effective-agents">Building
* Effective Agents</a>
* @see <a href=
* "https://docs.spring.io/spring-ai/reference/1.0/api/structured-output-converter.html">Spring
* AI Structure Output</a>
*
* "https://docs.spring.io/spring-ai/reference/1.0/api/structured-output-converter.html">Spring
* AI Structured Output</a>
*/
public class RoutingWorkflow {

Expand All @@ -84,56 +74,44 @@ public RoutingWorkflow(ChatClient chatClient) {
}

/**
* Routes input to a specialized prompt based on content classification. This
* method
* first analyzes the input to determine the most appropriate route, then
* processes
* the input using the specialized prompt for that route.
*
* Analyzes input with an LLM, evaluates the provided routes, and returns the
* selected route key.
*
* <p>
* The routing process involves:
* The method:
* <ol>
* <li>Content analysis to determine the appropriate category</li>
* <li>Selection of a specialized prompt optimized for that category</li>
* <li>Processing the input with the selected prompt</li>
* <li>Examines the input content and context</li>
* <li>Considers the available route options and their descriptions</li>
* <li>Generates reasoning and selects the best-matching route</li>
* </ol>
*
* <p>
* This approach allows for:
* <ul>
* <li>Better handling of diverse input types</li>
* <li>Optimization of prompts for specific categories</li>
* <li>Improved accuracy through specialized processing</li>
* </ul>
*
* @param input The input text to be routed and processed
* @param routes Map of route names to their corresponding specialized prompts
* @return Processed response from the selected specialized route
* @param input the input text to classify
* @param routes map of route keys to human-readable descriptions
* @return the selected route key
*/
public String route(String input, Map<String, String> routes) {
Assert.notNull(input, "Input text cannot be null");
Assert.notEmpty(routes, "Routes map cannot be null or empty");

// Determine the appropriate route for the input
String routeKey = determineRoute(input, routes.keySet());

// Get the selected prompt from the routes map
String selectedPrompt = routes.get(routeKey);
String routeKey = determineRoute(input, routes);

if (selectedPrompt == null) {
throw new IllegalArgumentException("Selected route '" + routeKey + "' not found in routes map");
if (!routes.containsKey(routeKey)) {
// LLM failure handling, retry, etc..
System.err.printf("Failed to detect the route, instead detected '%s', selecting fallback.%n", routeKey);
// Alternatively have fallback defined as a parameter or return Optional value...
return routes.keySet().stream().findFirst().orElseThrow();
}

// Process the input with the selected prompt
return chatClient.prompt(selectedPrompt + "\nInput: " + input).call().content();
return routeKey;
}

/**
* Analyzes the input content and determines the most appropriate route based on
* content classification. The classification process considers key terms,
* context,
* and patterns in the input to select the optimal route.
*
*
* <p>
* The method uses an LLM to:
* <ul>
Expand All @@ -144,29 +122,30 @@ public String route(String input, Map<String, String> routes) {
* </ul>
*
* @param input The input text to analyze for routing
* @param availableRoutes The set of available routing options
* @param availableRoutes The map of available routing options to their description
* @return The selected route key based on content analysis
*/
@SuppressWarnings("null")
private String determineRoute(String input, Iterable<String> availableRoutes) {
System.out.println("\nAvailable routes: " + availableRoutes);
private String determineRoute(String input, Map<String, String> availableRoutes) {
System.out.println("\nAvailable routes: " + availableRoutes.keySet());

String selectorPrompt = String.format("""
Analyze the input and select the most appropriate support team from these options: %s
First explain your reasoning, then provide your selection in this JSON format:

\\{
"reasoning": "Brief explanation of why this ticket should be routed to a specific team.
Consider key terms, user intent, and urgency level.",
"selection": "The chosen team name"
\\}
StringBuilder optionsWithDesc = new StringBuilder();
availableRoutes.forEach((k, v) -> {
optionsWithDesc.append(String.format("- %s: %s\n", k, v));
});

Input: %s""", availableRoutes, input);
String selectorPrompt = String.format("""
Analyze the input and select the most appropriate route from these options:

%s

First explain your reasoning, then provide your selection.

Input: %s""", optionsWithDesc, input);

RoutingResponse routingResponse = chatClient.prompt(selectorPrompt).call().entity(RoutingResponse.class);

System.out.println(String.format("Routing Analysis:%s\nSelected route: %s",
routingResponse.reasoning(), routingResponse.selection()));
System.out.printf("Routing Analysis:%s\nSelected route: %s%n",
Objects.requireNonNull(routingResponse).reasoning(), routingResponse.selection());

return routingResponse.selection();
}
Expand Down