初始提交
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MAF1.Agents;
|
||||
|
||||
public sealed class AgentStepResult
|
||||
{
|
||||
public string Message { get; init; } = "";
|
||||
public Dictionary<string, object?> Inputs { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public Dictionary<string, object?> Outputs { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static class AgentInputs
|
||||
{
|
||||
public static string? ReadString(IReadOnlyDictionary<string, object?> inputs, string key)
|
||||
{
|
||||
if (!inputs.TryGetValue(key, out object? value) || value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value is JsonElement el)
|
||||
{
|
||||
return el.ValueKind == JsonValueKind.String ? el.GetString() : el.ToString();
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
public static List<string> ReadStringList(IReadOnlyDictionary<string, object?> inputs, string key)
|
||||
{
|
||||
if (!inputs.TryGetValue(key, out object? value) || value is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (value is JsonElement el)
|
||||
{
|
||||
if (el.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return el.EnumerateArray()
|
||||
.Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() : item.ToString())
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(s => s!.Trim())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (el.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return Split(el.GetString());
|
||||
}
|
||||
}
|
||||
|
||||
if (value is IEnumerable<string> typed)
|
||||
{
|
||||
return typed.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToList();
|
||||
}
|
||||
|
||||
if (value is IEnumerable<object> objects)
|
||||
{
|
||||
return objects.Select(o => o?.ToString())
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(s => s!.Trim())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return Split(value.ToString());
|
||||
}
|
||||
|
||||
private static List<string> Split(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return text.Split(['、', ',', ';', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MAF1.Agents.FileCity;
|
||||
|
||||
public sealed class CityExtraction
|
||||
{
|
||||
[JsonPropertyName("hasValidCities")]
|
||||
public bool HasValidCities { get; set; }
|
||||
|
||||
[JsonPropertyName("cities")]
|
||||
public List<string> Cities { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = "";
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Text.Json;
|
||||
using MAF1.Tools;
|
||||
using MAF1.Utils;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace MAF1.Agents.FileCity;
|
||||
|
||||
public static class FileCityAgent
|
||||
{
|
||||
public static AIAgent Create(AgentFactory factory)
|
||||
{
|
||||
return factory.CreateAgent(
|
||||
name: "FileCityAgent",
|
||||
instructions:
|
||||
"""
|
||||
你负责读取用户指定的本地文件,并从中提取有效城市名。
|
||||
必须先调用 ReadTextFile 读取文件,不要猜测文件内容。
|
||||
有效城市名:现实世界中真实存在的城市(如 成都、北京、Amsterdam)。
|
||||
忽略空行、注释、人名、水果、随意单词等不是城市的内容。
|
||||
只输出一个 JSON 对象,不要 Markdown,不要其它说明:
|
||||
{"hasValidCities": true, "cities": ["成都"], "reason": "说明"}
|
||||
若没有有效城市:{"hasValidCities": false, "cities": [], "reason": "原因"}
|
||||
cities 去重,保持文件中的出现顺序。
|
||||
""",
|
||||
tools: [AIFunctionFactory.Create(FileTools.ReadTextFile)]);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using MAF1.Tools;
|
||||
using MAF1.Utils;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace MAF1.Agents.Weather;
|
||||
|
||||
public static class WeatherAgent
|
||||
{
|
||||
public static AIAgent Create(AgentFactory factory, WeatherTools weatherTools)
|
||||
{
|
||||
return factory.CreateAgent(
|
||||
name: "WeatherAgent",
|
||||
instructions:
|
||||
"""
|
||||
你负责查询天气。用户给出的每个城市都必须调用一次 GetWeather,不要编造天气。
|
||||
用中文汇总所有城市的天气。
|
||||
""",
|
||||
tools: [AIFunctionFactory.Create(weatherTools.GetWeather)]);
|
||||
}
|
||||
|
||||
public static async Task<AgentStepResult> RunAsync(
|
||||
AIAgent agent,
|
||||
IReadOnlyDictionary<string, object?> inputs,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<string> cities = AgentInputs.ReadStringList(inputs, "cities");
|
||||
if (cities.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("输入 cities 为空。请把上一节点的 cities 连到本节点的 cities。");
|
||||
}
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
$"请查询这些城市的天气:{string.Join("、", cities)}",
|
||||
cancellationToken: cancellationToken);
|
||||
return new AgentStepResult
|
||||
{
|
||||
Message = response.Text,
|
||||
Inputs = new Dictionary<string, object?> { ["cities"] = cities },
|
||||
Outputs = new Dictionary<string, object?> { ["summary"] = response.Text },
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user