using MAF1.Agents; using MAF1.Agents.FileCity; using MAF1.Agents.Weather; using MAF1.Tools; using MAF1.Utils; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Workflows; namespace MAF1.Route; /// /// 把方案 C 的图编译成 MAF Handoff:节点 = 白名单(主管可第一跳交给其中任何一个); /// 边 = 专家之间允许的交接。主管根据用户原话自己选第一跳,不必先经过拓扑入口。 /// public static class HandoffGraphBuilder { public static Workflow Build(AgentFactory factory, WeatherTools weatherTools, RouteGraph graph, RouteUserInput input) { AIAgent router = factory.CreateAgent( name: "router", description: "路由主管:根据用户任务把对话交接给图上允许的专家,自己不读文件、不查天气。", instructions: BuildRouterInstructions(graph, input)); Dictionary specialists = new(StringComparer.OrdinalIgnoreCase); foreach (RouteNode node in graph.Nodes) { specialists[node.Id] = CreateSpecialist(factory, weatherTools, node, graph, input); } HandoffWorkflowBuilder builder = AgentWorkflowBuilder.CreateHandoffBuilderWith(router); foreach (RouteNode node in graph.Nodes) { builder.WithHandoff(router, specialists[node.Id], HandoffReason(node, fromRouter: true, when: null)); } foreach (RouteEdge edge in graph.Edges) { builder.WithHandoff( specialists[edge.From], specialists[edge.To], HandoffReason(graph.Find(edge.To)!, fromRouter: false, when: edge.When)); } List mustHandoff = [router]; foreach (RouteNode node in graph.Nodes) { if (graph.HasOutgoing(node.Id)) { mustHandoff.Add(specialists[node.Id]); } } return builder .WithName("Route-Handoff") .EmitAgentResponseUpdateEvents() .EmitAgentResponseEvents() .WithAutonomousMode( turnLimit: 4, continuationPrompt: "你还没有完成交接。请立即调用 handoff 工具把对话交给下一位专家,不要自己结束本轮。", agents: mustHandoff) .Build(); } private static AIAgent CreateSpecialist( AgentFactory factory, WeatherTools weatherTools, RouteNode node, RouteGraph graph, RouteUserInput input) { string title = string.IsNullOrWhiteSpace(node.Title) ? node.Type : node.Title; string extra = BuildSpecialistExtra(node, graph, input); return node.Type switch { SystemNodeTypes.FileCity => FileCityAgent.Create( factory, name: node.Id, description: $"{title}:{FileCityAgent.DefaultDescription}", extraInstructions: extra), SystemNodeTypes.Weather => WeatherAgent.Create( factory, weatherTools, name: node.Id, description: $"{title}:{WeatherAgent.DefaultDescription}", extraInstructions: extra), _ => throw new InvalidOperationException($"未知节点类型 {node.Type}"), }; } private static string BuildRouterInstructions(RouteGraph graph, RouteUserInput input) { string entries = string.Join("、", graph.Nodes.Select(n => $"{n.Id}({n.Type})")); string edges = graph.HasEdges ? string.Join(";", graph.Edges.Select(e => $"{e.From}->{e.To}" + (string.IsNullOrWhiteSpace(e.When) ? "" : $"[{e.When}]"))) : "专家之间无边"; return $""" 你是交接图上的主管。禁止自己读文件或查询天气。 白名单专家:{entries} 收到用户任务后,必须立刻 handoff 给最合适的一位,不要经过用不上的专家。 判断规则: - 用户已经给出明确城市名、或只要查天气(如「拉斯维加斯」「帮我查成都天气」)→ 直接交给天气专家,不要先抽城市。 - 用户给的是文件路径、或一段需要从中识别城市的长文本 → 先交给抽城市专家。 专家之间的边:{edges} 不要向用户叙述交接过程,也不要在未交接时直接结束。 """; } private static string BuildSpecialistExtra(RouteNode node, RouteGraph graph, RouteUserInput input) { List lines = [ $"你是交接图上的节点 `{node.Id}`。", "完成自己的工作后:若有下游专家,必须调用框架注入的 handoff 工具把对话交出去;不要只输出结果就结束。", "若没有有效输入(例如抽不到城市),不要交接,用中文说明原因。", "不要向用户叙述交接过程。", ]; if (node.Type == SystemNodeTypes.FileCity) { lines.Add( """ 用户输入可能是文件路径,也可能是描述/问句/城市名单。你自己判断: - 像本地路径且需要读内容 → 调用 ReadTextFile。 - 像「帮我查成都天气」「明天去上海」→ 不要读文件,从用户消息提取城市。 不要把自然语言当成文件路径。 """); } List outgoing = graph.Edges .Where(e => e.From.Equals(node.Id, StringComparison.OrdinalIgnoreCase)) .ToList(); if (outgoing.Count == 0) { lines.Add("你没有下游专家。做完后直接回答用户,不要再交接。"); } else { lines.Add("下游:" + string.Join(";", outgoing.Select(e => { RouteNode to = graph.Find(e.To)!; string when = string.IsNullOrWhiteSpace(e.When) ? "" : $",条件 {e.When}"; return $"{to.Id}({to.Type}{when})"; }))); } return string.Join("\n", lines); } private static string HandoffReason(RouteNode target, bool fromRouter, string? when) { string title = string.IsNullOrWhiteSpace(target.Title) ? target.Type : target.Title; string role = target.Type switch { SystemNodeTypes.FileCity => FileCityAgent.DefaultDescription, SystemNodeTypes.Weather => WeatherAgent.DefaultDescription, _ => target.Type, }; string prefix = fromRouter ? target.Type switch { SystemNodeTypes.FileCity => "用户给了文件或需要从文本里识别城市时,交给这位专家。已经是明确城市名、只要查天气时不要交给他。", SystemNodeTypes.Weather => "用户已经给出城市名或明确要查天气时,直接交给这位专家,不必先抽城市。", _ => "把用户任务交给这位专家。", } : "把对话交给下一位专家。"; string condition = string.IsNullOrWhiteSpace(when) ? "" : when.Equals("hasValidCities", StringComparison.OrdinalIgnoreCase) ? " 仅当已经抽出有效城市时才交接。" : when.Equals("!hasValidCities", StringComparison.OrdinalIgnoreCase) ? " 仅当没有有效城市时才交接。" : $" 条件:{when}。"; return $"{prefix} 节点 {target.Id}({title}):{role}{condition}"; } }