001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.agentsflex.core.agent.route;
017
018import com.agentsflex.core.agent.IAgent;
019import com.agentsflex.core.message.AiMessage;
020import com.agentsflex.core.message.Message;
021import com.agentsflex.core.model.chat.ChatModel;
022import com.agentsflex.core.model.chat.ChatOptions;
023import com.agentsflex.core.prompt.MemoryPrompt;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import java.util.List;
028
029/**
030 * RouteAgent:负责路由用户输入到最合适的 IAgent。
031 * - 不实现 IAgent 接口
032 * - route() 方法返回 IAgent 实例,或 null(表示无匹配)
033 * - 不处理 Direct Answer,一律返回 null
034 * - 支持关键字快速匹配 + LLM 智能路由
035 */
036public class RoutingAgent {
037
038    private static final Logger log = LoggerFactory.getLogger(RoutingAgent.class);
039
040    private static final String DEFAULT_ROUTING_PROMPT_TEMPLATE =
041        "你是一个智能路由助手,请严格按以下规则响应:\n" +
042            "\n" +
043            "可用处理模块(Agent)及其能力描述:\n" +
044            "{agent_descriptions}\n" +
045            "\n" +
046            "规则:\n" +
047            "1. 如果用户问题属于某个模块的能力范围,请输出:Route: [模块名]\n" +
048            "2. 如果问题可以直接回答(如问候、常识、简单对话),请输出:Direct: [你的自然语言回答]\n" +
049            "3. 如果问题涉及多个模块,选择最核心的一个。\n" +
050            "4. 不要解释、不要输出其他内容,只输出上述两种格式之一。\n" +
051            "\n" +
052            "当前对话上下文(最近几轮):\n" +
053            "{conversation_context}\n" +
054            "\n" +
055            "用户最新问题:\n" +
056            "{user_input}";
057
058    private final ChatModel chatModel;
059    private final RoutingAgentRegistry routingAgentRegistry;
060    private final String userQuery;
061    private final MemoryPrompt memoryPrompt;
062
063    private String routingPromptTemplate = DEFAULT_ROUTING_PROMPT_TEMPLATE;
064    private ChatOptions chatOptions;
065    private boolean enableKeywordRouting = true;
066    private boolean enableLlmRouting = true;
067
068    public RoutingAgent(ChatModel chatModel, RoutingAgentRegistry routingAgentRegistry,
069                        String userQuery, MemoryPrompt memoryPrompt) {
070        this.chatModel = chatModel;
071        this.routingAgentRegistry = routingAgentRegistry;
072        this.userQuery = userQuery;
073        this.memoryPrompt = memoryPrompt;
074    }
075
076    /**
077     * 路由用户输入,返回匹配的 IAgent 实例。
078     * 仅当明确路由到某个 Agent 时才返回 IAgent,否则返回 null。
079     *
080     * @return IAgent 实例(仅 Route:xxx 场景),或 null(包括 Direct:、无匹配、异常等)
081     */
082    public IAgent route() {
083        try {
084            // 1. 关键字快速匹配
085            if (enableKeywordRouting) {
086                String agentName = routingAgentRegistry.findAgentByKeyword(userQuery);
087                if (agentName != null) {
088                    log.debug("关键字匹配命中 Agent: {}", agentName);
089                    return createAgent(agentName);
090                }
091            }
092
093            // 2. LLM 智能路由
094            if (enableLlmRouting) {
095                String contextSummary = buildContextSummary(memoryPrompt);
096                String agentDescriptions = routingAgentRegistry.getAgentDescriptions();
097                String prompt = routingPromptTemplate
098                    .replace("{agent_descriptions}", agentDescriptions)
099                    .replace("{conversation_context}", contextSummary)
100                    .replace("{user_input}", userQuery);
101
102                String decision = chatModel.chat(prompt, chatOptions);
103
104                if (decision != null && decision.startsWith("Route:")) {
105                    String agentName = decision.substring("Route:".length()).trim();
106                    return createAgent(agentName);
107                }
108            }
109
110            // 3. 无有效 Route,返回 null
111            log.debug("RouteAgent 未匹配到可路由的 Agent,返回 null。Query: {}", userQuery);
112            return null;
113
114        } catch (Exception e) {
115            log.error("RouteAgent 路由异常,返回 null", e);
116            return null;
117        }
118    }
119
120    private IAgent createAgent(String agentName) {
121        RoutingAgentFactory factory = routingAgentRegistry.getAgentFactory(agentName);
122        if (factory == null) {
123            log.warn("Agent 不存在: {}, 返回 null", agentName);
124            return null;
125        }
126        return factory.create(chatModel, userQuery, memoryPrompt);
127    }
128
129    private String buildContextSummary(MemoryPrompt history) {
130        List<Message> messages = history.getMessages();
131        if (messages == null || messages.isEmpty()) {
132            return "(无历史对话)";
133        }
134
135        int start = Math.max(0, messages.size() - 4);
136        StringBuilder sb = new StringBuilder();
137        for (int i = start; i < messages.size(); i++) {
138            Message msg = messages.get(i);
139            String role = msg instanceof AiMessage ? "AI" : "User";
140            String content = msg.getTextContent() != null ? msg.getTextContent() : "";
141            sb.append(role).append(": ").append(content.trim()).append("\n");
142        }
143        return sb.toString().trim();
144    }
145
146
147    public void setEnableKeywordRouting(boolean enable) {
148        this.enableKeywordRouting = enable;
149    }
150
151    public void setEnableLlmRouting(boolean enable) {
152        this.enableLlmRouting = enable;
153    }
154
155    public void setRoutingPromptTemplate(String routingPromptTemplate) {
156        if (routingPromptTemplate != null && !routingPromptTemplate.trim().isEmpty()) {
157            this.routingPromptTemplate = routingPromptTemplate;
158        }
159    }
160
161    public void setChatOptions(ChatOptions chatOptions) {
162        if (chatOptions != null) {
163            this.chatOptions = chatOptions;
164        }
165    }
166}