feat: 新增方案 C 的 Handoff 路由宿主,并让抽城市支持自然语言。
把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using System.Text.Json;
|
||||
using MAF1.Agents;
|
||||
|
||||
namespace MAF1.Route;
|
||||
|
||||
/// <summary>
|
||||
/// 方案 C 的图:节点 = 主管能调的专家白名单;边 = 允许的交接。
|
||||
/// 没有边时,名单上的节点可以按任意顺序调用。
|
||||
/// </summary>
|
||||
public sealed class RouteGraph
|
||||
{
|
||||
public List<RouteNode> Nodes { get; set; } = [];
|
||||
public List<RouteEdge> Edges { get; set; } = [];
|
||||
|
||||
public bool HasEdges => Edges.Count > 0;
|
||||
|
||||
/// <summary>主管第一跳只能到这些节点:无边时整张白名单;有边时取没有入边的入口。</summary>
|
||||
public IReadOnlyList<RouteNode> EntryNodes()
|
||||
{
|
||||
if (!HasEdges)
|
||||
{
|
||||
return Nodes;
|
||||
}
|
||||
|
||||
HashSet<string> targets = new(Edges.Select(e => e.To), StringComparer.OrdinalIgnoreCase);
|
||||
List<RouteNode> sources = Nodes.Where(n => !targets.Contains(n.Id)).ToList();
|
||||
return sources.Count > 0 ? sources : Nodes;
|
||||
}
|
||||
|
||||
public bool HasOutgoing(string nodeId)
|
||||
=> Edges.Any(e => e.From.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public RouteNode? Find(string nodeId)
|
||||
=> Nodes.FirstOrDefault(n => n.Id.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static JsonSerializerOptions JsonOptions { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
public static RouteGraph LoadFile(string path)
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
RouteGraph graph = JsonSerializer.Deserialize<RouteGraph>(json, JsonOptions)
|
||||
?? throw new InvalidOperationException($"无法解析路由图:{path}");
|
||||
graph.Validate();
|
||||
return graph;
|
||||
}
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Nodes.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("图里没有任何节点。方案 C 至少要放一个专家到白名单。");
|
||||
}
|
||||
|
||||
HashSet<string> ids = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (RouteNode node in Nodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.Id))
|
||||
{
|
||||
throw new InvalidOperationException("节点缺少 id。");
|
||||
}
|
||||
|
||||
if (!ids.Add(node.Id))
|
||||
{
|
||||
throw new InvalidOperationException($"节点 id 重复:{node.Id}");
|
||||
}
|
||||
|
||||
if (!SystemNodeTypes.IsKnown(node.Type))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"节点 `{node.Id}` 的类型 `{node.Type}` 不是系统专家。当前支持 {SystemNodeTypes.FileCity} / {SystemNodeTypes.Weather}。");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (RouteEdge edge in Edges)
|
||||
{
|
||||
if (Find(edge.From) is null)
|
||||
{
|
||||
throw new InvalidOperationException($"边的 from `{edge.From}` 不在图上。");
|
||||
}
|
||||
|
||||
if (Find(edge.To) is null)
|
||||
{
|
||||
throw new InvalidOperationException($"边的 to `{edge.To}` 不在图上。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RouteNode
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public string Title { get; set; } = "";
|
||||
public Dictionary<string, string> Config { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public sealed class RouteEdge
|
||||
{
|
||||
public string From { get; set; } = "";
|
||||
public string To { get; set; } = "";
|
||||
/// <summary>可选。hasValidCities / !hasValidCities / 空=只要拓扑允许就可以走。</summary>
|
||||
public string? When { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user