把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
107 lines
4.7 KiB
C#
107 lines
4.7 KiB
C#
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.txt、C:\\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 };
|
||
}
|