diff --git a/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/Application.java b/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/Application.java index 6807c510..d304e3bc 100644 --- a/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/Application.java +++ b/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/Application.java @@ -39,7 +39,14 @@ public static void main(String[] args) { public CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder) { return args -> { - Map supportRoutes = Map.of("billing", + Map 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 promptMap = Map.of("billing", """ You are a billing support specialist. Follow these guidelines: 1. Always start with "Billing Support Response:" @@ -113,7 +120,13 @@ 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) { @@ -121,7 +134,11 @@ public CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder) 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); } }; diff --git a/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingResponse.java b/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingResponse.java index b7101965..220a75fe 100644 --- a/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingResponse.java +++ b/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingResponse.java @@ -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. @@ -15,6 +15,8 @@ */ package com.example.agentic; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + /** * Record representing the response from the routing classification process. * @@ -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) { } diff --git a/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingWorkflow.java b/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingWorkflow.java index 28cc0a16..084c35f0 100644 --- a/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingWorkflow.java +++ b/agentic-patterns/routing-workflow/src/main/java/com/example/agentic/RoutingWorkflow.java @@ -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}). + * *

* The routing workflow is particularly effective for complex tasks where: *

    @@ -38,42 +37,33 @@ *
  • Different types of input require different specialized processing or * expertise
  • *
- * + * *

- * Common use cases include: + * Key characteristics: *

    - *
  • Customer support systems routing different types of queries (billing, - * technical, etc.)
  • - *
  • Content moderation systems routing content to appropriate review - * processes
  • - *
  • Query optimization by routing simple/complex questions to different model - * capabilities
  • + *
  • LLM-driven content analysis and classification
  • + *
  • Clear separation of concerns: classification yields a route key that + * downstream components can act on
  • + *
  • Extensible catalogue of routes defined by the caller
  • *
- * *

- * This implementation allows for dynamic routing based on content - * classification, - * with each route having its own specialized prompt optimized for specific - * types of input. - * *

- * Implementation uses the Spring - * AI Structure Output 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 feature to deserialize the model response into a + * {@link RoutingResponse}. + * + * @author Christian Tzolov, Joonas Vali + * @see ChatClient * @see Spring - * AI ChatClient + * "https://docs.spring.io/spring-ai/reference/1.0/api/chatclient.html">Spring + * AI ChatClient * @see Building - * Effective Agents + * "https://www.anthropic.com/research/building-effective-agents">Building + * Effective Agents * @see Spring - * AI Structure Output - * + * "https://docs.spring.io/spring-ai/reference/1.0/api/structured-output-converter.html">Spring + * AI Structured Output */ public class RoutingWorkflow { @@ -84,48 +74,36 @@ 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. + * *

- * The routing process involves: + * The method: *

    - *
  1. Content analysis to determine the appropriate category
  2. - *
  3. Selection of a specialized prompt optimized for that category
  4. - *
  5. Processing the input with the selected prompt
  6. + *
  7. Examines the input content and context
  8. + *
  9. Considers the available route options and their descriptions
  10. + *
  11. Generates reasoning and selects the best-matching route
  12. *
* - *

- * This approach allows for: - *

    - *
  • Better handling of diverse input types
  • - *
  • Optimization of prompts for specific categories
  • - *
  • Improved accuracy through specialized processing
  • - *
- * - * @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 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; } /** @@ -133,7 +111,7 @@ public String route(String input, Map routes) { * content classification. The classification process considers key terms, * context, * and patterns in the input to select the optimal route. - * + * *

* The method uses an LLM to: *

    @@ -144,29 +122,30 @@ public String route(String input, Map routes) { *
* * @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 availableRoutes) { - System.out.println("\nAvailable routes: " + availableRoutes); + private String determineRoute(String input, Map 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(); }