Files
admin777 b0e89b6092 feat: 新增方案 C 的 Handoff 路由宿主,并让抽城市支持自然语言。
把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
2026-09-01 17:56:41 +08:00

107 lines
4.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using MAF1.Core.Agents;
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace MAF1.Agents.FileCity;
/// <summary>
/// 「读文件抽城市」Agent。学习路径:Create 注册工具和系统提示 → RunAsync 调模型 → Parse 把模型文本收成结构化结果。
/// CLI、网页系统节点、file-city 插件三处都调用同一套逻辑,避免各写一份 prompt。
/// </summary>
public static class FileCityAgent
{
public const string DefaultDescription = "从文件或用户描述中提取有效城市名。";
/// <summary>创建带 ReadTextFile 工具的聊天 Agent。模型必须先读文件,不能凭文件名猜内容。</summary>
public static AIAgent Create(
AgentFactory factory,
string name = "FileCityAgent",
string? description = null,
string? extraInstructions = null)
{
string instructions =
"""
你负责从用户材料里提取有效城市名。
用户可能给你两种输入,你必须自己判断:
1) 本地文件路径(如 Data/cities.txtC:\\a.txt)→ 必须先调用 ReadTextFile 读文件,不要猜内容。
2) 自然语言描述或城市名单(如「帮我查成都和杭州的天气」「明天去上海」)→ 不要调用 ReadTextFile,直接从用户消息里提取城市。
不要把描述、问句、城市名当成文件路径去读。
有效城市名:现实世界中真实存在的城市(如 成都、北京、Amsterdam)。
忽略空行、注释、人名、水果、随意单词等不是城市的内容。
城市可能出现在句子、列表或表格里,都要提取。
只输出一个 JSON 对象,不要 Markdown,不要其它说明:
{"hasValidCities": true, "cities": ["成都"], "reason": "说明"}
若没有有效城市:{"hasValidCities": false, "cities": [], "reason": "原因"}
cities 去重,保持出现顺序。
""";
if (!string.IsNullOrWhiteSpace(extraInstructions))
{
instructions = instructions + "\n" + extraInstructions;
}
return factory.CreateAgent(
name: name,
instructions: instructions,
description: string.IsNullOrWhiteSpace(description) ? DefaultDescription : description,
tools: [AIFunctionFactory.Create(FileTools.ReadTextFile)]);
}
/// <summary>跑一轮:读 filePath 输入,把解析后的 cities / hasValidCities 放进 Outputs。</summary>
public static async Task<AgentStepResult> RunAsync(
AIAgent agent,
IReadOnlyDictionary<string, object?> inputs,
CancellationToken cancellationToken = default)
{
string filePath = AgentInputs.ReadString(inputs, "filePath") ?? "Data/cities.txt";
AgentResponse response = await agent.RunAsync(
$"请读取这个文件并提取有效城市名:{filePath}",
cancellationToken: cancellationToken);
CityExtraction extraction = Parse(response.Text);
return new AgentStepResult
{
Message = response.Text,
Inputs = new Dictionary<string, object?> { ["filePath"] = filePath },
Outputs = new Dictionary<string, object?>
{
["hasValidCities"] = extraction.HasValidCities,
["cities"] = extraction.Cities,
["reason"] = extraction.Reason,
["raw"] = response.Text,
},
};
}
/// <summary>从模型原文里抠 JSON。模型偶尔会包 Markdown 代码块,所以先走 JsonText.UnwrapObject。</summary>
public static CityExtraction Parse(string text)
{
try
{
CityExtraction? result = JsonSerializer.Deserialize<CityExtraction>(
JsonText.UnwrapObject(text),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (result is null)
{
return Invalid("无法解析文件 Agent 的输出。");
}
result.Cities = result.Cities
.Where(city => !string.IsNullOrWhiteSpace(city))
.Select(city => city.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
result.HasValidCities = result.HasValidCities && result.Cities.Count > 0;
return result;
}
catch (JsonException ex)
{
return Invalid($"文件 Agent 没有返回合法 JSON{ex.Message}");
}
}
private static CityExtraction Invalid(string reason) =>
new() { HasValidCities = false, Reason = reason };
}