docs: 为 CLI、网页编排器和插件协议补充学习向注释
在核心类型、图执行器、stdio 插件和前端入口写清职责与对照路径,不改运行行为。
This commit is contained in:
@@ -2,6 +2,10 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace MAF1.Agents;
|
namespace MAF1.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一次 Agent 执行的统一结果。网页编排器和进程外插件都用这套字段,方便画布把输出接到下一节点。
|
||||||
|
/// Message 是给日志看的原文;Outputs 才是下游节点真正读取的端口数据。
|
||||||
|
/// </summary>
|
||||||
public sealed class AgentStepResult
|
public sealed class AgentStepResult
|
||||||
{
|
{
|
||||||
public string Message { get; init; } = "";
|
public string Message { get; init; } = "";
|
||||||
@@ -9,8 +13,12 @@ public sealed class AgentStepResult
|
|||||||
public Dictionary<string, object?> Outputs { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, object?> Outputs { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从图 JSON / 插件 stdin 读入参。值可能是 string、数组,也可能是 System.Text.Json 反序列化后的 JsonElement。
|
||||||
|
/// </summary>
|
||||||
public static class AgentInputs
|
public static class AgentInputs
|
||||||
{
|
{
|
||||||
|
/// <summary>按端口名读一个字符串;没有该键则返回 null。</summary>
|
||||||
public static string? ReadString(IReadOnlyDictionary<string, object?> inputs, string key)
|
public static string? ReadString(IReadOnlyDictionary<string, object?> inputs, string key)
|
||||||
{
|
{
|
||||||
if (!inputs.TryGetValue(key, out object? value) || value is null)
|
if (!inputs.TryGetValue(key, out object? value) || value is null)
|
||||||
@@ -26,6 +34,7 @@ public static class AgentInputs
|
|||||||
return value.ToString();
|
return value.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>读城市列表:支持 JSON 数组、["a","b"] 风格,或用顿号/逗号分隔的一串文字。</summary>
|
||||||
public static List<string> ReadStringList(IReadOnlyDictionary<string, object?> inputs, string key)
|
public static List<string> ReadStringList(IReadOnlyDictionary<string, object?> inputs, string key)
|
||||||
{
|
{
|
||||||
if (!inputs.TryGetValue(key, out object? value) || value is null)
|
if (!inputs.TryGetValue(key, out object? value) || value is null)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace MAF1.Agents.FileCity;
|
namespace MAF1.Agents.FileCity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FileCity Agent 约定输出的 JSON 形状。hasValidCities 会驱动边条件,也会触发「缺城市时等人确认」。
|
||||||
|
/// </summary>
|
||||||
public sealed class CityExtraction
|
public sealed class CityExtraction
|
||||||
{
|
{
|
||||||
[JsonPropertyName("hasValidCities")]
|
[JsonPropertyName("hasValidCities")]
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ using Microsoft.Extensions.AI;
|
|||||||
|
|
||||||
namespace MAF1.Agents.FileCity;
|
namespace MAF1.Agents.FileCity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 「读文件抽城市」Agent。学习路径:Create 注册工具和系统提示 → RunAsync 调模型 → Parse 把模型文本收成结构化结果。
|
||||||
|
/// CLI、网页系统节点、file-city 插件三处都调用同一套逻辑,避免各写一份 prompt。
|
||||||
|
/// </summary>
|
||||||
public static class FileCityAgent
|
public static class FileCityAgent
|
||||||
{
|
{
|
||||||
|
/// <summary>创建带 ReadTextFile 工具的聊天 Agent。模型必须先读文件,不能凭文件名猜内容。</summary>
|
||||||
public static AIAgent Create(AgentFactory factory)
|
public static AIAgent Create(AgentFactory factory)
|
||||||
{
|
{
|
||||||
return factory.CreateAgent(
|
return factory.CreateAgent(
|
||||||
@@ -26,6 +31,7 @@ public static class FileCityAgent
|
|||||||
tools: [AIFunctionFactory.Create(FileTools.ReadTextFile)]);
|
tools: [AIFunctionFactory.Create(FileTools.ReadTextFile)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>跑一轮:读 filePath 输入,把解析后的 cities / hasValidCities 放进 Outputs。</summary>
|
||||||
public static async Task<AgentStepResult> RunAsync(
|
public static async Task<AgentStepResult> RunAsync(
|
||||||
AIAgent agent,
|
AIAgent agent,
|
||||||
IReadOnlyDictionary<string, object?> inputs,
|
IReadOnlyDictionary<string, object?> inputs,
|
||||||
@@ -50,6 +56,7 @@ public static class FileCityAgent
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>从模型原文里抠 JSON。模型偶尔会包 Markdown 代码块,所以先走 JsonText.UnwrapObject。</summary>
|
||||||
public static CityExtraction Parse(string text)
|
public static CityExtraction Parse(string text)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ using Microsoft.Extensions.AI;
|
|||||||
|
|
||||||
namespace MAF1.Agents.Weather;
|
namespace MAF1.Agents.Weather;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 「按城市查天气」Agent。真正的 HTTP 查询在 WeatherTools.GetWeather,模型只负责决定调几次工具并写成中文摘要。
|
||||||
|
/// </summary>
|
||||||
public static class WeatherAgent
|
public static class WeatherAgent
|
||||||
{
|
{
|
||||||
|
/// <summary>创建带 GetWeather 工具的 Agent。instructions 要求每个城市都调工具,禁止编造气温。</summary>
|
||||||
public static AIAgent Create(AgentFactory factory, WeatherTools weatherTools)
|
public static AIAgent Create(AgentFactory factory, WeatherTools weatherTools)
|
||||||
{
|
{
|
||||||
return factory.CreateAgent(
|
return factory.CreateAgent(
|
||||||
@@ -19,6 +23,7 @@ public static class WeatherAgent
|
|||||||
tools: [AIFunctionFactory.Create(weatherTools.GetWeather)]);
|
tools: [AIFunctionFactory.Create(weatherTools.GetWeather)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>需要上游把 cities 连到本节点。空列表直接抛错,避免无意义地打 LLM。</summary>
|
||||||
public static async Task<AgentStepResult> RunAsync(
|
public static async Task<AgentStepResult> RunAsync(
|
||||||
AIAgent agent,
|
AIAgent agent,
|
||||||
IReadOnlyDictionary<string, object?> inputs,
|
IReadOnlyDictionary<string, object?> inputs,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ using MAF1.Agents;
|
|||||||
|
|
||||||
namespace MAF1.Decisions;
|
namespace MAF1.Decisions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作流暂停时发给人或 UI 的确认请求。必须带 DefaultOptionId,超时就采用它。
|
||||||
|
/// CLI 和 Web 共用这一套,避免两套文案分叉。
|
||||||
|
/// </summary>
|
||||||
public sealed class DecisionRequest
|
public sealed class DecisionRequest
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
@@ -14,6 +18,7 @@ public sealed class DecisionRequest
|
|||||||
public DateTimeOffset Deadline { get; set; }
|
public DateTimeOffset Deadline { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>一个可选项。RequiresText=true 时前端会显示输入框(例如改填城市)。</summary>
|
||||||
public sealed class DecisionOption
|
public sealed class DecisionOption
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
@@ -24,6 +29,7 @@ public sealed class DecisionOption
|
|||||||
public bool IsDefault { get; set; }
|
public bool IsDefault { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>人的选择或超时结果。TimedOut=true 时一律按默认方案处理,忽略 Text。</summary>
|
||||||
public sealed class DecisionAnswer
|
public sealed class DecisionAnswer
|
||||||
{
|
{
|
||||||
public string OptionId { get; set; } = "";
|
public string OptionId { get; set; } = "";
|
||||||
@@ -31,12 +37,14 @@ public sealed class DecisionAnswer
|
|||||||
public bool TimedOut { get; set; }
|
public bool TimedOut { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>缺城市场景下的两个方案 id,前后端都硬编码这两个字符串。</summary>
|
||||||
public static class DecisionOptionIds
|
public static class DecisionOptionIds
|
||||||
{
|
{
|
||||||
public const string Stop = "stop";
|
public const string Stop = "stop";
|
||||||
public const string QueryCities = "query-cities";
|
public const string QueryCities = "query-cities";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>工厂:生成「没有读到城市」这一类确认。网页 runner 和 CLI executor 都调用 Create。</summary>
|
||||||
public static class EmptyCityDecision
|
public static class EmptyCityDecision
|
||||||
{
|
{
|
||||||
public static DecisionRequest Create(string nodeId, string? reason, int timeoutSeconds)
|
public static DecisionRequest Create(string nodeId, string? reason, int timeoutSeconds)
|
||||||
@@ -72,6 +80,7 @@ public static class EmptyCityDecision
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>用户选「改查其它城市」且解析出至少一个城市名时返回 true。</summary>
|
||||||
public static bool TryContinueWithCities(DecisionAnswer answer, out List<string> cities)
|
public static bool TryContinueWithCities(DecisionAnswer answer, out List<string> cities)
|
||||||
{
|
{
|
||||||
cities = [];
|
cities = [];
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace MAF1.PluginContract;
|
namespace MAF1.PluginContract;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每个插件目录里的 plugin.json。网页宿主扫描这个文件来画节点端口,不会加载插件 DLL。
|
||||||
|
/// Id 会出现在画布节点的 Type 上。
|
||||||
|
/// </summary>
|
||||||
public sealed class PluginManifest
|
public sealed class PluginManifest
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
@@ -16,12 +20,14 @@ public sealed class PluginManifest
|
|||||||
public List<PluginPort> Outputs { get; set; } = [];
|
public List<PluginPort> Outputs { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>如何启动插件进程。Command 可以是相对插件目录的 exe,或 dotnet + dll。</summary>
|
||||||
public sealed class PluginLaunch
|
public sealed class PluginLaunch
|
||||||
{
|
{
|
||||||
public string Command { get; set; } = "";
|
public string Command { get; set; } = "";
|
||||||
public List<string> Args { get; set; } = [];
|
public List<string> Args { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>画布上的一个输入或输出端口。Name 必须和代码里读写的字段一致。</summary>
|
||||||
public sealed class PluginPort
|
public sealed class PluginPort
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
@@ -30,6 +36,7 @@ public sealed class PluginPort
|
|||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>插件声明「我需要哪种凭据」。宿主按 Type 注入,Key 不进流程图 JSON。</summary>
|
||||||
public sealed class PluginCredentialNeed
|
public sealed class PluginCredentialNeed
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
@@ -38,6 +45,7 @@ public sealed class PluginCredentialNeed
|
|||||||
public string Description { get; set; } = "";
|
public string Description { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>宿主写入插件 stdin 的整包请求:业务输入 + 凭据。</summary>
|
||||||
public sealed class PluginRequest
|
public sealed class PluginRequest
|
||||||
{
|
{
|
||||||
public Dictionary<string, object?> Inputs { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, object?> Inputs { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -46,6 +54,7 @@ public sealed class PluginRequest
|
|||||||
public Dictionary<string, PluginCredentialPayload> Credentials { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, PluginCredentialPayload> Credentials { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>实际的 endpoint / apiKey / model。只在宿主→插件进程之间传递。</summary>
|
||||||
public sealed class PluginCredentialPayload
|
public sealed class PluginCredentialPayload
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
@@ -56,6 +65,7 @@ public sealed class PluginCredentialPayload
|
|||||||
public Dictionary<string, string> Extra { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, string> Extra { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>插件 JSON 统一 camelCase,和网页前端字段名对齐。</summary>
|
||||||
public static class PluginJson
|
public static class PluginJson
|
||||||
{
|
{
|
||||||
public static readonly System.Text.Json.JsonSerializerOptions Options = new()
|
public static readonly System.Text.Json.JsonSerializerOptions Options = new()
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ using Microsoft.Extensions.Configuration;
|
|||||||
|
|
||||||
namespace MAF1.PluginContract;
|
namespace MAF1.PluginContract;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程外插件的 stdio 协议:宿主把 PluginRequest JSON 写入 stdin,插件把输出 JSON 写到 stdout。
|
||||||
|
/// 学习时对照 PluginProcessRunner:那边启动进程、写 stdin、读 stdout。
|
||||||
|
/// </summary>
|
||||||
public static class PluginStdio
|
public static class PluginStdio
|
||||||
{
|
{
|
||||||
|
/// <summary>插件入口第一步:读完 stdin 再干活。宿主写完会关闭 stdin。</summary>
|
||||||
public static async Task<PluginRequest> ReadRequestAsync(CancellationToken cancellationToken = default)
|
public static async Task<PluginRequest> ReadRequestAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using Stream stdin = Console.OpenStandardInput();
|
using Stream stdin = Console.OpenStandardInput();
|
||||||
@@ -20,6 +25,7 @@ public static class PluginStdio
|
|||||||
return request ?? new PluginRequest();
|
return request ?? new PluginRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把 Outputs 写成一行 JSON。不要往 stdout 打日志,日志请走 stderr。</summary>
|
||||||
public static async Task WriteOutputsAsync(Dictionary<string, object?> outputs, CancellationToken cancellationToken = default)
|
public static async Task WriteOutputsAsync(Dictionary<string, object?> outputs, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
string json = JsonSerializer.Serialize(outputs, PluginJson.Options);
|
string json = JsonSerializer.Serialize(outputs, PluginJson.Options);
|
||||||
@@ -27,6 +33,7 @@ public static class PluginStdio
|
|||||||
await Console.Out.FlushAsync(cancellationToken);
|
await Console.Out.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把宿主注入的 LLM 凭据写进环境变量,这样 AgentFactory.Load 能读到 Key。</summary>
|
||||||
public static void ApplyCredentialsToEnvironment(IReadOnlyDictionary<string, PluginCredentialPayload> credentials)
|
public static void ApplyCredentialsToEnvironment(IReadOnlyDictionary<string, PluginCredentialPayload> credentials)
|
||||||
{
|
{
|
||||||
foreach (KeyValuePair<string, PluginCredentialPayload> pair in credentials)
|
foreach (KeyValuePair<string, PluginCredentialPayload> pair in credentials)
|
||||||
@@ -49,6 +56,9 @@ public static class PluginStdio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插件自己读天气等配置时用。优先 MAF1_CONTENT_ROOT(宿主 exe 目录),否则向上找 appsettings.json。
|
||||||
|
/// </summary>
|
||||||
public static IConfiguration LoadHostConfiguration()
|
public static IConfiguration LoadHostConfiguration()
|
||||||
{
|
{
|
||||||
string contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT");
|
string contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT");
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MAF1.Tools;
|
namespace MAF1.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 给 LLM 调用的本地读文件工具。Description 属性会进工具 schema,模型据此决定何时调用。
|
||||||
|
/// 路径解析会试当前目录、exe 目录、以及 MAF1_CONTENT_ROOT(插件进程由宿主注入)。
|
||||||
|
/// </summary>
|
||||||
public static class FileTools
|
public static class FileTools
|
||||||
{
|
{
|
||||||
[Description("Read the full UTF-8 text of a local file. Always call this before judging city names.")]
|
[Description("Read the full UTF-8 text of a local file. Always call this before judging city names.")]
|
||||||
@@ -28,6 +32,7 @@ public static class FileTools
|
|||||||
return File.ReadAllText(resolved, Encoding.UTF8);
|
return File.ReadAllText(resolved, Encoding.UTF8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>相对路径时依次试 cwd、exe 目录、内容根、再往上两级父目录(适配 bin/Debug/netX)。</summary>
|
||||||
private static string? Resolve(string path)
|
private static string? Resolve(string path)
|
||||||
{
|
{
|
||||||
if (Path.IsPathRooted(path) && File.Exists(path))
|
if (Path.IsPathRooted(path) && File.Exists(path))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
namespace MAF1.Tools;
|
namespace MAF1.Tools;
|
||||||
|
|
||||||
|
/// <summary>对应 appsettings.json 的 Weather 段。CLI 和 Web 各自有一份配置文件。</summary>
|
||||||
public sealed class WeatherOptions
|
public sealed class WeatherOptions
|
||||||
{
|
{
|
||||||
public string Provider { get; set; } = "Wttr";
|
public string Provider { get; set; } = "Wttr";
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace MAF1.Tools;
|
namespace MAF1.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 天气 HTTP 实现。Agent 通过 GetWeather 间接调用这里,而不是自己拼 URL。
|
||||||
|
/// Provider=Wttr 免费无需 Key;OpenWeather 需要 ApiKey。
|
||||||
|
/// </summary>
|
||||||
public sealed class WeatherTools(WeatherOptions options, HttpClient http)
|
public sealed class WeatherTools(WeatherOptions options, HttpClient http)
|
||||||
{
|
{
|
||||||
[Description("Look up live weather for a city or location. Always call this instead of guessing.")]
|
[Description("Look up live weather for a city or location. Always call this instead of guessing.")]
|
||||||
@@ -82,6 +86,7 @@ public sealed class WeatherTools(WeatherOptions options, HttpClient http)
|
|||||||
: $"{name}:{description},气温 {temp:0.#}°C,湿度 {humidity}%";
|
: $"{name}:{description},气温 {temp:0.#}°C,湿度 {humidity}%";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把 appsettings 里的 URL 模板换成真实地址。城市名要做 Uri.EscapeDataString。</summary>
|
||||||
private string Expand(string template, string location, string? apiKey)
|
private string Expand(string template, string location, string? apiKey)
|
||||||
{
|
{
|
||||||
return template
|
return template
|
||||||
@@ -90,6 +95,7 @@ public sealed class WeatherTools(WeatherOptions options, HttpClient http)
|
|||||||
.Replace("{apiKey}", apiKey ?? "", StringComparison.OrdinalIgnoreCase);
|
.Replace("{apiKey}", apiKey ?? "", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>带超时和 User-Agent。部分天气站点会拒绝没有 UA 的请求。</summary>
|
||||||
public static HttpClient CreateHttpClient()
|
public static HttpClient CreateHttpClient()
|
||||||
{
|
{
|
||||||
HttpClient http = new() { Timeout = TimeSpan.FromSeconds(20) };
|
HttpClient http = new() { Timeout = TimeSpan.FromSeconds(20) };
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ using OpenAI.Chat;
|
|||||||
|
|
||||||
namespace MAF1.Utils;
|
namespace MAF1.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建聊天 Agent 的工厂。根据 Endpoint 判断走 Azure OpenAI 还是 OpenAI 兼容接口(DeepSeek 等)。
|
||||||
|
/// 密钥优先级:appsettings Llm → 环境变量。浏览器永远拿不到 Key。
|
||||||
|
/// </summary>
|
||||||
public sealed class AgentFactory
|
public sealed class AgentFactory
|
||||||
{
|
{
|
||||||
private readonly ChatClient _chatClient;
|
private readonly ChatClient _chatClient;
|
||||||
@@ -48,11 +52,13 @@ public sealed class AgentFactory
|
|||||||
.GetChatClient(model);
|
.GetChatClient(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把 ChatClient 包成 Microsoft.Agents.AI 的 AIAgent,并挂上 tools。</summary>
|
||||||
public AIAgent CreateAgent(string name, string instructions, IList<AITool>? tools = null)
|
public AIAgent CreateAgent(string name, string instructions, IList<AITool>? tools = null)
|
||||||
{
|
{
|
||||||
return _chatClient.AsAIAgent(instructions: instructions, name: name, tools: tools);
|
return _chatClient.AsAIAgent(instructions: instructions, name: name, tools: tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>合并配置文件和环境变量。插件进程里环境变量通常由宿主写入。</summary>
|
||||||
public static LlmOptions Load(IConfiguration config)
|
public static LlmOptions Load(IConfiguration config)
|
||||||
{
|
{
|
||||||
LlmOptions options = config.GetSection("Llm").Get<LlmOptions>() ?? new LlmOptions();
|
LlmOptions options = config.GetSection("Llm").Get<LlmOptions>() ?? new LlmOptions();
|
||||||
@@ -85,6 +91,7 @@ public sealed class AgentFactory
|
|||||||
|| host.Contains("services.ai.azure.com", StringComparison.OrdinalIgnoreCase);
|
|| host.Contains("services.ai.azure.com", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>OpenAI 兼容 API 要求 base URL 以 /v1 结尾,用户常只填到域名。</summary>
|
||||||
private static Uri ToOpenAICompatibleEndpoint(string endpoint)
|
private static Uri ToOpenAICompatibleEndpoint(string endpoint)
|
||||||
{
|
{
|
||||||
string trimmed = endpoint.TrimEnd('/');
|
string trimmed = endpoint.TrimEnd('/');
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace MAF1.Utils;
|
namespace MAF1.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从模型回复里抽出 JSON 对象。模型常包 ```json ... ```,或在前后加说明文字。
|
||||||
|
/// </summary>
|
||||||
public static class JsonText
|
public static class JsonText
|
||||||
{
|
{
|
||||||
public static string UnwrapObject(string text)
|
public static string UnwrapObject(string text)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
namespace MAF1.Utils;
|
namespace MAF1.Utils;
|
||||||
|
|
||||||
|
/// <summary>LLM 连接信息。不要把真实 ApiKey 提交进 git。</summary>
|
||||||
public sealed class LlmOptions
|
public sealed class LlmOptions
|
||||||
{
|
{
|
||||||
public string ApiKey { get; set; } = "";
|
public string ApiKey { get; set; } = "";
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MAF1.Utils;
|
namespace MAF1.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows 控制台默认代码页容易把中文打成乱码。启动时切到 UTF-8,并打开 VT 序列(彩色输出用)。
|
||||||
|
/// </summary>
|
||||||
public static class WindowsConsole
|
public static class WindowsConsole
|
||||||
{
|
{
|
||||||
private const uint Utf8CodePage = 65001;
|
private const uint Utf8CodePage = 65001;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
|
|
||||||
namespace MAF1.Plugins;
|
namespace MAF1.Plugins;
|
||||||
|
|
||||||
|
/// <summary>内存中的完整凭据。只给插件进程用,不要序列化给浏览器。</summary>
|
||||||
public sealed class CredentialRecord
|
public sealed class CredentialRecord
|
||||||
{
|
{
|
||||||
public string Id { get; init; } = "";
|
public string Id { get; init; } = "";
|
||||||
@@ -15,6 +16,7 @@ public sealed class CredentialRecord
|
|||||||
public Dictionary<string, string> Extra { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, string> Extra { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>GET /api/credentials 的形状:有没有 Key 用布尔值表示,不返回 Key 本身。</summary>
|
||||||
public sealed class CredentialPublicView
|
public sealed class CredentialPublicView
|
||||||
{
|
{
|
||||||
public string Id { get; init; } = "";
|
public string Id { get; init; } = "";
|
||||||
@@ -25,6 +27,9 @@ public sealed class CredentialPublicView
|
|||||||
public bool HasApiKey { get; init; }
|
public bool HasApiKey { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Llm 段和 Credentials 段组装凭据。节点 config.credentialId 默认 llm-default。
|
||||||
|
/// </summary>
|
||||||
public sealed class CredentialStore
|
public sealed class CredentialStore
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
@@ -64,6 +69,7 @@ public sealed class CredentialStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>给前端的列表。Endpoint 目前未打码(学习项目);ApiKey 绝不会出现在这里。</summary>
|
||||||
public IReadOnlyList<CredentialPublicView> ListPublic()
|
public IReadOnlyList<CredentialPublicView> ListPublic()
|
||||||
=> _named.Values
|
=> _named.Values
|
||||||
.OrderBy(item => item.Id, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(item => item.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using MAF1.Web;
|
|||||||
|
|
||||||
namespace MAF1.Plugins;
|
namespace MAF1.Plugins;
|
||||||
|
|
||||||
|
/// <summary>一次目录快照:系统节点 + 插件节点合并后的 Nodes 列表给画布用。</summary>
|
||||||
public sealed class NodeCatalogSnapshot
|
public sealed class NodeCatalogSnapshot
|
||||||
{
|
{
|
||||||
public string PluginsRoot { get; init; } = "";
|
public string PluginsRoot { get; init; } = "";
|
||||||
@@ -13,6 +14,7 @@ public sealed class NodeCatalogSnapshot
|
|||||||
public IReadOnlyList<LoadedPlugin> LoadedPlugins { get; init; } = [];
|
public IReadOnlyList<LoadedPlugin> LoadedPlugins { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把扫描结果和 AgentCatalog 拼在一起。同名时插件覆盖系统实现。</summary>
|
||||||
public sealed class NodeCatalogService(PluginScanner scanner)
|
public sealed class NodeCatalogService(PluginScanner scanner)
|
||||||
{
|
{
|
||||||
public NodeCatalogSnapshot Load()
|
public NodeCatalogSnapshot Load()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
|
|
||||||
namespace MAF1.Plugins;
|
namespace MAF1.Plugins;
|
||||||
|
|
||||||
|
/// <summary>Plugins:Directory 相对网页 exe,不是源码树里的 plugins/ 文件夹。</summary>
|
||||||
public sealed class PluginOptions
|
public sealed class PluginOptions
|
||||||
{
|
{
|
||||||
public string Directory { get; set; } = "plugins";
|
public string Directory { get; set; } = "plugins";
|
||||||
@@ -21,6 +22,7 @@ public sealed class PluginOptions
|
|||||||
=> config.GetSection("Plugins").Get<PluginOptions>() ?? new PluginOptions();
|
=> config.GetSection("Plugins").Get<PluginOptions>() ?? new PluginOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>扫描成功后的一个插件:目录 + 已解析的 plugin.json。</summary>
|
||||||
public sealed class LoadedPlugin
|
public sealed class LoadedPlugin
|
||||||
{
|
{
|
||||||
public required string FolderName { get; init; }
|
public required string FolderName { get; init; }
|
||||||
@@ -28,6 +30,7 @@ public sealed class LoadedPlugin
|
|||||||
public required PluginContract.PluginManifest Manifest { get; init; }
|
public required PluginContract.PluginManifest Manifest { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>扫描问题,前端左侧「扫描说明」会显示。error 的插件不会进画布。</summary>
|
||||||
public sealed class CatalogIssue
|
public sealed class CatalogIssue
|
||||||
{
|
{
|
||||||
public string Level { get; init; } = "error";
|
public string Level { get; init; } = "error";
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ using MAF1.Utils;
|
|||||||
|
|
||||||
namespace MAF1.Plugins;
|
namespace MAF1.Plugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动子进程跑插件:stdin 写 PluginRequest,stdout 读输出 JSON,超时 Kill。
|
||||||
|
/// 这是「进程外 Agent」的完整示例,对照 FileCityPlugin/Program.cs 一起看。
|
||||||
|
/// </summary>
|
||||||
public sealed class PluginProcessRunner(PluginOptions options, CredentialStore credentials)
|
public sealed class PluginProcessRunner(PluginOptions options, CredentialStore credentials)
|
||||||
{
|
{
|
||||||
public async Task<Dictionary<string, object?>> RunAsync(
|
public async Task<Dictionary<string, object?>> RunAsync(
|
||||||
@@ -97,6 +101,7 @@ public sealed class PluginProcessRunner(PluginOptions options, CredentialStore c
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>工作目录=插件文件夹;环境变量带上 OPENAI_* 和 MAF1_CONTENT_ROOT。</summary>
|
||||||
private ProcessStartInfo CreateStartInfo(LoadedPlugin plugin, Dictionary<string, PluginCredentialPayload> payload)
|
private ProcessStartInfo CreateStartInfo(LoadedPlugin plugin, Dictionary<string, PluginCredentialPayload> payload)
|
||||||
{
|
{
|
||||||
string command = plugin.Manifest.Launch.Command;
|
string command = plugin.Manifest.Launch.Command;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ using MAF1.PluginContract;
|
|||||||
|
|
||||||
namespace MAF1.Plugins;
|
namespace MAF1.Plugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描 exe 旁边的 plugins/*/plugin.json。源码目录 Plugins/ 不会被运行时直接读到,要先 build 复制到输出目录。
|
||||||
|
/// </summary>
|
||||||
public sealed class PluginScanner(PluginOptions options)
|
public sealed class PluginScanner(PluginOptions options)
|
||||||
{
|
{
|
||||||
public PluginScanResult Scan()
|
public PluginScanResult Scan()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 网页入口:Minimal API + wwwroot 静态文件。浏览器只调 REST,不接触 API Key。
|
||||||
|
// 学习顺序建议:/api/catalog → 画布 → POST /api/run → 若 needsDecision 再 POST /api/run/{id}/decide。
|
||||||
using MAF1.Decisions;
|
using MAF1.Decisions;
|
||||||
using MAF1.Utils;
|
using MAF1.Utils;
|
||||||
using MAF1.Web;
|
using MAF1.Web;
|
||||||
@@ -16,6 +18,7 @@ WebApplication app = builder.Build();
|
|||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
// 左侧节点列表:系统内置 + 扫描到的插件。
|
||||||
app.MapGet("/api/catalog", (AgentRuntime runtime) =>
|
app.MapGet("/api/catalog", (AgentRuntime runtime) =>
|
||||||
{
|
{
|
||||||
var snapshot = runtime.Catalog.Load();
|
var snapshot = runtime.Catalog.Load();
|
||||||
@@ -28,14 +31,17 @@ app.MapGet("/api/catalog", (AgentRuntime runtime) =>
|
|||||||
issues = snapshot.Issues,
|
issues = snapshot.Issues,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
// 给下拉框用的凭据列表(不含原始 Key)。
|
||||||
app.MapGet("/api/credentials", (AgentRuntime runtime) => runtime.Credentials.ListPublic());
|
app.MapGet("/api/credentials", (AgentRuntime runtime) => runtime.Credentials.ListPublic());
|
||||||
app.MapGet("/api/status", (AgentRuntime runtime) => new
|
app.MapGet("/api/status", (AgentRuntime runtime) => new
|
||||||
{
|
{
|
||||||
weatherProvider = runtime.WeatherProvider,
|
weatherProvider = runtime.WeatherProvider,
|
||||||
pluginsRoot = runtime.PluginsRoot,
|
pluginsRoot = runtime.PluginsRoot,
|
||||||
});
|
});
|
||||||
|
// 提交画布 JSON。可能一次跑完,也可能返回 status=needsDecision。
|
||||||
app.MapPost("/api/run", (WorkflowGraph graph, AgentRuntime runtime, CancellationToken cancellationToken)
|
app.MapPost("/api/run", (WorkflowGraph graph, AgentRuntime runtime, CancellationToken cancellationToken)
|
||||||
=> runtime.Runner.RunAsync(graph, cancellationToken));
|
=> runtime.Runner.RunAsync(graph, cancellationToken));
|
||||||
|
// 超时后前端轮询最终结果。
|
||||||
app.MapGet("/api/run/{runId}", (string runId, AgentRuntime runtime) =>
|
app.MapGet("/api/run/{runId}", (string runId, AgentRuntime runtime) =>
|
||||||
{
|
{
|
||||||
WorkflowRunResult? result = runtime.Runner.Get(runId);
|
WorkflowRunResult? result = runtime.Runner.Get(runId);
|
||||||
@@ -43,6 +49,7 @@ app.MapGet("/api/run/{runId}", (string runId, AgentRuntime runtime) =>
|
|||||||
? Results.NotFound(new WorkflowRunResult { Ok = false, Status = WorkflowRunStatus.Failed, Error = "找不到这次运行。" })
|
? Results.NotFound(new WorkflowRunResult { Ok = false, Status = WorkflowRunStatus.Failed, Error = "找不到这次运行。" })
|
||||||
: Results.Ok(result);
|
: Results.Ok(result);
|
||||||
});
|
});
|
||||||
|
// 用户点确认:optionId=stop 或 query-cities(可带 text 城市名)。
|
||||||
app.MapPost("/api/run/{runId}/decide", (string runId, DecisionAnswer answer, AgentRuntime runtime, CancellationToken cancellationToken)
|
app.MapPost("/api/run/{runId}/decide", (string runId, DecisionAnswer answer, AgentRuntime runtime, CancellationToken cancellationToken)
|
||||||
=> runtime.Runner.DecideAsync(runId, answer, cancellationToken));
|
=> runtime.Runner.DecideAsync(runId, answer, cancellationToken));
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using MAF1.PluginContract;
|
|||||||
|
|
||||||
namespace MAF1.Web;
|
namespace MAF1.Web;
|
||||||
|
|
||||||
|
/// <summary>画布上一个端口的元数据,前端用来渲染圆点和检查器文案。</summary>
|
||||||
public sealed class PortInfo
|
public sealed class PortInfo
|
||||||
{
|
{
|
||||||
public string Name { get; init; } = "";
|
public string Name { get; init; } = "";
|
||||||
@@ -10,6 +11,9 @@ public sealed class PortInfo
|
|||||||
public bool Required { get; init; }
|
public bool Required { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一种可放到画布上的节点类型。Origin=system 走进程内 Handler;Origin=plugin 走独立进程。
|
||||||
|
/// </summary>
|
||||||
public sealed class AgentTypeInfo
|
public sealed class AgentTypeInfo
|
||||||
{
|
{
|
||||||
public string Type { get; init; } = "";
|
public string Type { get; init; } = "";
|
||||||
@@ -30,6 +34,7 @@ public sealed class AgentTypeInfo
|
|||||||
public IReadOnlyList<PluginCredentialNeed> Credentials { get; init; } = [];
|
public IReadOnlyList<PluginCredentialNeed> Credentials { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>系统节点绑定到哪个 C# 实现。插件节点 Handler 保持 None。</summary>
|
||||||
public enum SystemHandler
|
public enum SystemHandler
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
@@ -37,6 +42,9 @@ public enum SystemHandler
|
|||||||
Weather,
|
Weather,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写死在宿主里的两种系统节点。插件 id 若与 Type 冲突,运行时优先插件进程。
|
||||||
|
/// </summary>
|
||||||
public static class AgentCatalog
|
public static class AgentCatalog
|
||||||
{
|
{
|
||||||
public static AgentTypeInfo? FindSystem(string type)
|
public static AgentTypeInfo? FindSystem(string type)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ using Microsoft.Extensions.Configuration;
|
|||||||
|
|
||||||
namespace MAF1.Web;
|
namespace MAF1.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 网页进程的组合根:把 LLM、两个系统 Agent、插件扫描/启动子进程、图执行器绑在一起。
|
||||||
|
/// ASP.NET 把它注册成 Singleton,一次请求里不要 new 第二份。
|
||||||
|
/// </summary>
|
||||||
public sealed class AgentRuntime
|
public sealed class AgentRuntime
|
||||||
{
|
{
|
||||||
public AgentRuntime(IConfiguration config)
|
public AgentRuntime(IConfiguration config)
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ using Microsoft.Agents.AI;
|
|||||||
|
|
||||||
namespace MAF1.Web;
|
namespace MAF1.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 网页自己的图执行器(不是 Microsoft Agents AI Workflow)。
|
||||||
|
/// 流程:拓扑排序 → 按边接线组装输入 → 跑系统节点或插件进程 → 若抽城市失败则暂停等人。
|
||||||
|
/// 和 CLI 对照:这里用 DAG + when 条件,CLI 用 WorkflowBuilder 的 Executor 图。
|
||||||
|
/// </summary>
|
||||||
public sealed class ConfigurableWorkflowRunner(
|
public sealed class ConfigurableWorkflowRunner(
|
||||||
AIAgent fileCityAgent,
|
AIAgent fileCityAgent,
|
||||||
AIAgent weatherAgent,
|
AIAgent weatherAgent,
|
||||||
@@ -17,6 +22,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
WorkflowRunStore runs,
|
WorkflowRunStore runs,
|
||||||
int decisionTimeoutSeconds)
|
int decisionTimeoutSeconds)
|
||||||
{
|
{
|
||||||
|
/// <summary>这些 config 键不是业务输入端口,接线时不要当 filePath 那样传给 Agent。</summary>
|
||||||
private static readonly HashSet<string> ReservedConfigKeys = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> ReservedConfigKeys = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"credentialId",
|
"credentialId",
|
||||||
@@ -24,6 +30,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
"decisionTimeoutSeconds",
|
"decisionTimeoutSeconds",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>开始一次运行。后面若暂停,用同一 runId 继续 DecideAsync。</summary>
|
||||||
public async Task<WorkflowRunResult> RunAsync(WorkflowGraph graph, CancellationToken cancellationToken)
|
public async Task<WorkflowRunResult> RunAsync(WorkflowGraph graph, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -62,6 +69,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
return await ResolveAsync(session, answer, cancellationToken, session.Pending?.Id);
|
return await ResolveAsync(session, answer, cancellationToken, session.Pending?.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>从 session.NextIndex 接着跑。条件不满足的节点记 Skipped,不调用 LLM。</summary>
|
||||||
private async Task<WorkflowRunResult> ContinueAsync(WorkflowSession session, CancellationToken cancellationToken)
|
private async Task<WorkflowRunResult> ContinueAsync(WorkflowSession session, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -107,6 +115,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>返回 needsDecision,并在后台倒计时到点后按默认方案 ResolveAsync。</summary>
|
||||||
private WorkflowRunResult PauseForEmptyCities(WorkflowSession session, WorkflowNode node, NodeRunLog log)
|
private WorkflowRunResult PauseForEmptyCities(WorkflowSession session, WorkflowNode node, NodeRunLog log)
|
||||||
{
|
{
|
||||||
string? reason = ReadString(log.Outputs, "reason");
|
string? reason = ReadString(log.Outputs, "reason");
|
||||||
@@ -167,6 +176,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把确认结果写回该节点的 outputs(例如改写 cities),然后继续后续节点。</summary>
|
||||||
private async Task<WorkflowRunResult> ResolveAsync(
|
private async Task<WorkflowRunResult> ResolveAsync(
|
||||||
WorkflowSession session,
|
WorkflowSession session,
|
||||||
DecisionAnswer answer,
|
DecisionAnswer answer,
|
||||||
@@ -319,6 +329,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
Steps = steps ?? [],
|
Steps = steps ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>节点输出了 hasValidCities=false,且后面还有节点时,默认要弹确认。config 可关。</summary>
|
||||||
private static bool ShouldAskEmptyCities(WorkflowNode node, NodeRunLog log)
|
private static bool ShouldAskEmptyCities(WorkflowNode node, NodeRunLog log)
|
||||||
{
|
{
|
||||||
if (log.Skipped)
|
if (log.Skipped)
|
||||||
@@ -340,6 +351,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
return !ReadBool(log.Outputs, "hasValidCities");
|
return !ReadBool(log.Outputs, "hasValidCities");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>插件 id 优先于系统节点。系统节点按 Handler 调 FileCityAgent / WeatherAgent。</summary>
|
||||||
private async Task<NodeRunLog> RunNodeAsync(
|
private async Task<NodeRunLog> RunNodeAsync(
|
||||||
NodeCatalogSnapshot snapshot,
|
NodeCatalogSnapshot snapshot,
|
||||||
WorkflowNode node,
|
WorkflowNode node,
|
||||||
@@ -439,6 +451,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Kahn 算法拓扑排序。有环则无法确定执行顺序。</summary>
|
||||||
private static List<string> TopologicalOrder(WorkflowGraph graph)
|
private static List<string> TopologicalOrder(WorkflowGraph graph)
|
||||||
{
|
{
|
||||||
Dictionary<string, int> indegree = graph.Nodes.ToDictionary(n => n.Id, _ => 0);
|
Dictionary<string, int> indegree = graph.Nodes.ToDictionary(n => n.Id, _ => 0);
|
||||||
@@ -473,6 +486,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>先填节点 Config,再用入边把上游输出端口覆盖到下游输入端口(连线优先)。</summary>
|
||||||
private static Dictionary<string, object?> ResolveInputs(
|
private static Dictionary<string, object?> ResolveInputs(
|
||||||
WorkflowGraph graph,
|
WorkflowGraph graph,
|
||||||
WorkflowNode node,
|
WorkflowNode node,
|
||||||
@@ -505,6 +519,7 @@ public sealed class ConfigurableWorkflowRunner(
|
|||||||
return inputs;
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>入边 When 不满足则跳过本节点。当前只实现 hasValidCities 这一类条件。</summary>
|
||||||
private static bool PassEdgeConditions(
|
private static bool PassEdgeConditions(
|
||||||
WorkflowGraph graph,
|
WorkflowGraph graph,
|
||||||
WorkflowNode node,
|
WorkflowNode node,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ using MAF1.Decisions;
|
|||||||
|
|
||||||
namespace MAF1.Web;
|
namespace MAF1.Web;
|
||||||
|
|
||||||
|
/// <summary>浏览器 POST /api/run 的图:节点 + 连线。和前端 state.nodes / state.edges 一一对应。</summary>
|
||||||
public sealed class WorkflowGraph
|
public sealed class WorkflowGraph
|
||||||
{
|
{
|
||||||
public List<WorkflowNode> Nodes { get; set; } = [];
|
public List<WorkflowNode> Nodes { get; set; } = [];
|
||||||
public List<WorkflowEdge> Edges { get; set; } = [];
|
public List<WorkflowEdge> Edges { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>画布节点。Config 里可写 filePath、credentialId、decisionTimeoutSeconds 等固定输入。</summary>
|
||||||
public sealed class WorkflowNode
|
public sealed class WorkflowNode
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
@@ -18,6 +20,9 @@ public sealed class WorkflowNode
|
|||||||
public Dictionary<string, string> Config { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, string> Config { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一条边:FromPort → ToPort 传数据。When 是运行条件(hasValidCities / !hasValidCities / 空=始终)。
|
||||||
|
/// </summary>
|
||||||
public sealed class WorkflowEdge
|
public sealed class WorkflowEdge
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
@@ -28,6 +33,7 @@ public sealed class WorkflowEdge
|
|||||||
public string? When { get; set; }
|
public string? When { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>一次运行的状态机:completed / needsDecision / failed。</summary>
|
||||||
public static class WorkflowRunStatus
|
public static class WorkflowRunStatus
|
||||||
{
|
{
|
||||||
public const string Completed = "completed";
|
public const string Completed = "completed";
|
||||||
@@ -35,6 +41,7 @@ public static class WorkflowRunStatus
|
|||||||
public const string Failed = "failed";
|
public const string Failed = "failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>返回给前端的运行快照。暂停时带 Decision,方便弹出确认框。</summary>
|
||||||
public sealed class WorkflowRunResult
|
public sealed class WorkflowRunResult
|
||||||
{
|
{
|
||||||
public bool Ok { get; set; }
|
public bool Ok { get; set; }
|
||||||
@@ -46,6 +53,7 @@ public sealed class WorkflowRunResult
|
|||||||
public List<NodeRunLog> Steps { get; set; } = [];
|
public List<NodeRunLog> Steps { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>单个节点的执行日志,显示在页面底部「运行结果」。</summary>
|
||||||
public sealed class NodeRunLog
|
public sealed class NodeRunLog
|
||||||
{
|
{
|
||||||
public string NodeId { get; set; } = "";
|
public string NodeId { get; set; } = "";
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ using MAF1.Plugins;
|
|||||||
|
|
||||||
namespace MAF1.Web;
|
namespace MAF1.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一次可暂停的运行。NextIndex 记住停在第几个节点;Pending 是当前确认请求。
|
||||||
|
/// Mutex 防止「用户点击」和「超时自动确认」同时 Continue。
|
||||||
|
/// </summary>
|
||||||
internal sealed class WorkflowSession
|
internal sealed class WorkflowSession
|
||||||
{
|
{
|
||||||
public required string RunId { get; init; }
|
public required string RunId { get; init; }
|
||||||
@@ -22,6 +26,7 @@ internal sealed class WorkflowSession
|
|||||||
public object Sync { get; } = new();
|
public object Sync { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>进程内运行字典。服务重启后 runId 会失效,学习项目不做持久化。</summary>
|
||||||
public sealed class WorkflowRunStore
|
public sealed class WorkflowRunStore
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, WorkflowSession> _sessions = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, WorkflowSession> _sessions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* 编排器布局:顶栏、三栏(节点 / 画布 / 检查器)、底部日志、确认弹层。 */
|
||||||
:root {
|
:root {
|
||||||
--bg: #0f1419;
|
--bg: #0f1419;
|
||||||
--panel: #171e26;
|
--panel: #171e26;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
<!-- 单页编排器:左节点列表、中画布、右检查器、下日志。逻辑全在 /js/app.js。 -->
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
<h2>运行结果</h2>
|
<h2>运行结果</h2>
|
||||||
<pre id="log">尚未运行。</pre>
|
<pre id="log">尚未运行。</pre>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- 缺城市时的确认层,对应后端 status=needsDecision -->
|
||||||
<div id="decisionModal" class="modal hidden" aria-hidden="true">
|
<div id="decisionModal" class="modal hidden" aria-hidden="true">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h2>需要你确认</h2>
|
<h2>需要你确认</h2>
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 可视化编排器前端(无框架)。
|
||||||
|
*
|
||||||
|
* 学习路径:
|
||||||
|
* 1. init → loadCatalog:拉系统节点和插件节点
|
||||||
|
* 2. 示例图 / 拖节点、点端口连线;图数据在 state.nodes / state.edges
|
||||||
|
* 3. runGraph POST /api/run;若 status=needsDecision 弹出确认框
|
||||||
|
* 4. 确认走 /api/run/{id}/decide;超时则 pollRun 等服务端自动采用默认方案
|
||||||
|
*
|
||||||
|
* selected / pending 只用于点击交互。发给后端的 JSON 不含 API Key,只有 credentialId。
|
||||||
|
*/
|
||||||
const state = {
|
const state = {
|
||||||
catalog: [],
|
catalog: [],
|
||||||
system: [],
|
system: [],
|
||||||
@@ -44,6 +55,7 @@ function pickNode(list, inputName) {
|
|||||||
return (list || []).find((item) => item.inputs?.some((p) => p.name === inputName));
|
return (list || []).find((item) => item.inputs?.some((p) => p.name === inputName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 生成「抽城市 → 有城市才查天气」的示例图。连线 when=hasValidCities。 */
|
||||||
function exampleGraph() {
|
function exampleGraph() {
|
||||||
const fileInfo = pickNode(state.system, "filePath") || pickNode(state.catalog, "filePath");
|
const fileInfo = pickNode(state.system, "filePath") || pickNode(state.catalog, "filePath");
|
||||||
const weatherInfo = pickNode(state.plugins, "cities") || pickNode(state.system, "cities") || pickNode(state.catalog, "cities");
|
const weatherInfo = pickNode(state.plugins, "cities") || pickNode(state.system, "cities") || pickNode(state.catalog, "cities");
|
||||||
@@ -110,6 +122,7 @@ function addNode(type) {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据 state 重画节点 DOM;连线是 SVG,在 drawWires。 */
|
||||||
function render() {
|
function render() {
|
||||||
canvas.innerHTML = "";
|
canvas.innerHTML = "";
|
||||||
for (const node of state.nodes) {
|
for (const node of state.nodes) {
|
||||||
@@ -172,6 +185,7 @@ function startDrag(e, node) {
|
|||||||
window.addEventListener("mouseup", up);
|
window.addEventListener("mouseup", up);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 先点输出端口记下 pending,再点输入端口生成一条边。 */
|
||||||
function onPort(nodeId, port, dir) {
|
function onPort(nodeId, port, dir) {
|
||||||
if (dir === "out") {
|
if (dir === "out") {
|
||||||
state.pending = { nodeId, port };
|
state.pending = { nodeId, port };
|
||||||
@@ -293,6 +307,7 @@ function renderInspector() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 把画布图交给后端。可能直接完成,也可能弹出 decisionModal。 */
|
||||||
async function runGraph() {
|
async function runGraph() {
|
||||||
closeDecision();
|
closeDecision();
|
||||||
logEl.textContent = "运行中…";
|
logEl.textContent = "运行中…";
|
||||||
@@ -379,6 +394,7 @@ function syncDecisionText() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 倒计时到 0 后去 GET 运行结果,因为服务端会自己按默认方案继续。 */
|
||||||
function startDecisionTimer(deadline) {
|
function startDecisionTimer(deadline) {
|
||||||
if (decisionTimer) {
|
if (decisionTimer) {
|
||||||
clearInterval(decisionTimer);
|
clearInterval(decisionTimer);
|
||||||
@@ -447,6 +463,7 @@ decisionSubmit.onclick = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 拉节点目录和凭据列表,刷新左侧面板。 */
|
||||||
async function loadCatalog() {
|
async function loadCatalog() {
|
||||||
const [catalog, credentials] = await Promise.all([
|
const [catalog, credentials] = await Promise.all([
|
||||||
(await fetch("/api/catalog")).json(),
|
(await fetch("/api/catalog")).json(),
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ using Microsoft.Extensions.Configuration;
|
|||||||
|
|
||||||
namespace MAF1;
|
namespace MAF1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 命令行宿主:读配置、创建两个 Agent、按 node/edge 选一张工作流图。
|
||||||
|
/// node = 判断写在 CityGate 节点里;edge = 判断写在 AddEdge 的 condition 上。两条路缺城市时都会等人确认。
|
||||||
|
/// </summary>
|
||||||
internal static class CliHost
|
internal static class CliHost
|
||||||
{
|
{
|
||||||
public static async Task RunAsync(string[] args)
|
public static async Task RunAsync(string[] args)
|
||||||
@@ -39,6 +43,7 @@ internal static class CliHost
|
|||||||
Console.WriteLine($"决策等待: {decisionTimeoutSeconds} 秒(超时采用默认方案:结束查询)");
|
Console.WriteLine($"决策等待: {decisionTimeoutSeconds} 秒(超时采用默认方案:结束查询)");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 同一套 Agent,两张不同的图。对比源码时重点看 CityWeatherWorkflow 里 AddEdge 的差异。
|
||||||
Workflow workflow = mode == "edge"
|
Workflow workflow = mode == "edge"
|
||||||
? CityWeatherWorkflow.BuildEdgeCondition(fileCityAgent, weatherAgent, decisionTimeoutSeconds)
|
? CityWeatherWorkflow.BuildEdgeCondition(fileCityAgent, weatherAgent, decisionTimeoutSeconds)
|
||||||
: CityWeatherWorkflow.BuildNodeCondition(fileCityAgent, weatherAgent, decisionTimeoutSeconds);
|
: CityWeatherWorkflow.BuildNodeCondition(fileCityAgent, weatherAgent, decisionTimeoutSeconds);
|
||||||
@@ -48,6 +53,7 @@ internal static class CliHost
|
|||||||
await WorkflowOrchestration.RunAsync(workflow, label, filePath);
|
await WorkflowOrchestration.RunAsync(workflow, label, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>无参数时默认 node + Data/cities.txt。--cli 是旧兼容前缀,可忽略。</summary>
|
||||||
private static bool TryParseArgs(string[] args, out string mode, out string filePath)
|
private static bool TryParseArgs(string[] args, out string mode, out string filePath)
|
||||||
{
|
{
|
||||||
mode = "node";
|
mode = "node";
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ using MAF1.Decisions;
|
|||||||
|
|
||||||
namespace MAF1.Orchestration;
|
namespace MAF1.Orchestration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CLI 版确认框。回车 / 1 = 结束查询;2 = 再输入城市;也可以直接打「成都」。
|
||||||
|
/// ReadLine 和超时赛跑:到期返回 TimedOut,工作流走默认方案。
|
||||||
|
/// </summary>
|
||||||
internal static class ConsoleDecisionPrompt
|
internal static class ConsoleDecisionPrompt
|
||||||
{
|
{
|
||||||
public static async Task<DecisionAnswer> WaitAsync(DecisionRequest request)
|
public static async Task<DecisionAnswer> WaitAsync(DecisionRequest request)
|
||||||
@@ -70,6 +74,7 @@ internal static class ConsoleDecisionPrompt
|
|||||||
return new DecisionAnswer { OptionId = DecisionOptionIds.Stop };
|
return new DecisionAnswer { OptionId = DecisionOptionIds.Stop };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>超时返回 null。注意:超时后后台的 ReadLine 仍可能阻塞,学习项目可接受。</summary>
|
||||||
private static async Task<string?> ReadLineOrTimeoutAsync(DateTimeOffset deadline)
|
private static async Task<string?> ReadLineOrTimeoutAsync(DateTimeOffset deadline)
|
||||||
{
|
{
|
||||||
TimeSpan remain = deadline - DateTimeOffset.UtcNow;
|
TimeSpan remain = deadline - DateTimeOffset.UtcNow;
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ using Microsoft.Agents.AI.Workflows;
|
|||||||
|
|
||||||
namespace MAF1.Orchestration;
|
namespace MAF1.Orchestration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把 Workflow 事件打到控制台。核心循环:WatchStreamAsync。
|
||||||
|
/// 遇到 RequestInfoEvent 说明图在 RequestPort 上等人,这里弹出 ConsoleDecisionPrompt 再 SendResponseAsync。
|
||||||
|
/// </summary>
|
||||||
public static class WorkflowOrchestration
|
public static class WorkflowOrchestration
|
||||||
{
|
{
|
||||||
public static async Task RunAsync(Workflow workflow, string modeLabel, string filePath)
|
public static async Task RunAsync(Workflow workflow, string modeLabel, string filePath)
|
||||||
@@ -13,6 +17,7 @@ public static class WorkflowOrchestration
|
|||||||
|
|
||||||
string prompt = $"请读取这个文件并提取有效城市名:{filePath}";
|
string prompt = $"请读取这个文件并提取有效城市名:{filePath}";
|
||||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, prompt);
|
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, prompt);
|
||||||
|
// TurnToken 通知 Agent Executor「可以开始一轮」;emitEvents 才会推送流式文本。
|
||||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||||
|
|
||||||
string? lastExecutorId = null;
|
string? lastExecutorId = null;
|
||||||
@@ -21,6 +26,7 @@ public static class WorkflowOrchestration
|
|||||||
switch (evt)
|
switch (evt)
|
||||||
{
|
{
|
||||||
case RequestInfoEvent requestEvent:
|
case RequestInfoEvent requestEvent:
|
||||||
|
// 人机环:工作流挂起 → 控制台询问 → 把 DecisionAnswer 送回 RequestPort。
|
||||||
DecisionRequest decision = ReadDecision(requestEvent.Request);
|
DecisionRequest decision = ReadDecision(requestEvent.Request);
|
||||||
DecisionAnswer answer = await ConsoleDecisionPrompt.WaitAsync(decision);
|
DecisionAnswer answer = await ConsoleDecisionPrompt.WaitAsync(decision);
|
||||||
await run.SendResponseAsync(requestEvent.Request.CreateResponse(answer));
|
await run.SendResponseAsync(requestEvent.Request.CreateResponse(answer));
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,6 @@
|
|||||||
using MAF1;
|
// 控制台入口。学习时从这里开始:参数解析 → 组装 Agent → 跑 Microsoft Agents AI Workflow。
|
||||||
|
// 可视化编排在另一个项目 MAF1.Web,本进程不听 HTTP。
|
||||||
|
using MAF1;
|
||||||
using MAF1.Utils;
|
using MAF1.Utils;
|
||||||
|
|
||||||
WindowsConsole.EnableUtf8();
|
WindowsConsole.EnableUtf8();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.Agents.AI.Workflows;
|
|||||||
namespace MAF1.Workflows;
|
namespace MAF1.Workflows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 边条件模式:没有城市时不在边上直接结束,而是发出决策请求。
|
/// 边条件模式:没有城市时不在边上直接结束,而是发出 DecisionRequest,进入 RequestPort 等人。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AskCityDecisionExecutor(int timeoutSeconds) : Executor<CityExtraction>("AskCityDecision")
|
internal sealed class AskCityDecisionExecutor(int timeoutSeconds) : Executor<CityExtraction>("AskCityDecision")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ using Microsoft.Extensions.AI;
|
|||||||
|
|
||||||
namespace MAF1.Workflows;
|
namespace MAF1.Workflows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// node 模式的闸门:解析 FileCity 文本,有城市就改写成查天气的用户消息;没有就发出确认请求。
|
||||||
|
/// 判断发生在这个类内部,所以叫「条件写在节点里」。
|
||||||
|
/// </summary>
|
||||||
internal sealed class CityGateExecutor(int decisionTimeoutSeconds) : ChatProtocolExecutor(
|
internal sealed class CityGateExecutor(int decisionTimeoutSeconds) : ChatProtocolExecutor(
|
||||||
"CityGate",
|
"CityGate",
|
||||||
new ChatProtocolExecutorOptions { AutoSendTurnToken = false })
|
new ChatProtocolExecutorOptions { AutoSendTurnToken = false })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.AI;
|
|||||||
namespace MAF1.Workflows;
|
namespace MAF1.Workflows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 边条件模式:这里只解析,不做 if。走哪条边由 AddEdge 的 condition 决定。
|
/// 边条件模式:这里只解析 FileCity 文本,不做 if。走哪条边由 AddEdge 的 condition 决定。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class CityParseExecutor() : ChatProtocolExecutor(
|
internal sealed class CityParseExecutor() : ChatProtocolExecutor(
|
||||||
"CityParse",
|
"CityParse",
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ using MAF1.Agents.FileCity;
|
|||||||
|
|
||||||
namespace MAF1.Workflows;
|
namespace MAF1.Workflows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用 Microsoft.Agents.AI.Workflows 拼「抽城市 → 查天气」两张对照图。
|
||||||
|
/// 建议对照 BuildNodeCondition / BuildEdgeCondition:前者 if 在 CityGate 里,后者 if 在边上。
|
||||||
|
/// RequestPort 是官方的「向外要一次人工输入」端口,对应网页的 needsDecision。
|
||||||
|
/// </summary>
|
||||||
public static class CityWeatherWorkflow
|
public static class CityWeatherWorkflow
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 节点内判断:FileCity → CityGate。有城市则 Gate 直接催 Weather;没有则发 DecisionRequest。
|
||||||
|
/// </summary>
|
||||||
public static Workflow BuildNodeCondition(AIAgent fileCityAgent, AIAgent weatherAgent, int decisionTimeoutSeconds)
|
public static Workflow BuildNodeCondition(AIAgent fileCityAgent, AIAgent weatherAgent, int decisionTimeoutSeconds)
|
||||||
{
|
{
|
||||||
(ExecutorBinding fileCity, ExecutorBinding weather) = BindAgents(fileCityAgent, weatherAgent);
|
(ExecutorBinding fileCity, ExecutorBinding weather) = BindAgents(fileCityAgent, weatherAgent);
|
||||||
@@ -27,6 +35,9 @@ public static class CityWeatherWorkflow
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 边条件:Parse 之后两条边,condition 看 HasValidCities。没有城市走 Ask → RequestPort,而不是直接结束。
|
||||||
|
/// </summary>
|
||||||
public static Workflow BuildEdgeCondition(AIAgent fileCityAgent, AIAgent weatherAgent, int decisionTimeoutSeconds)
|
public static Workflow BuildEdgeCondition(AIAgent fileCityAgent, AIAgent weatherAgent, int decisionTimeoutSeconds)
|
||||||
{
|
{
|
||||||
(ExecutorBinding fileCity, ExecutorBinding weather) = BindAgents(fileCityAgent, weatherAgent);
|
(ExecutorBinding fileCity, ExecutorBinding weather) = BindAgents(fileCityAgent, weatherAgent);
|
||||||
@@ -49,6 +60,7 @@ public static class CityWeatherWorkflow
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>把 AIAgent 嵌进 Workflow。ForwardIncomingMessages=false 避免把上游闲聊原样转给下游。</summary>
|
||||||
private static (ExecutorBinding FileCity, ExecutorBinding Weather) BindAgents(AIAgent fileCityAgent, AIAgent weatherAgent)
|
private static (ExecutorBinding FileCity, ExecutorBinding Weather) BindAgents(AIAgent fileCityAgent, AIAgent weatherAgent)
|
||||||
{
|
{
|
||||||
AIAgentHostOptions agentOptions = new()
|
AIAgentHostOptions agentOptions = new()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 进程外 FileCity 插件。宿主启动本 exe,stdin 给 JSON,stdout 只回输出对象。
|
||||||
|
// 业务逻辑全部复用 MAF1.Core 里的 FileCityAgent,这里只做协议适配。
|
||||||
using MAF1.Agents.FileCity;
|
using MAF1.Agents.FileCity;
|
||||||
using MAF1.PluginContract;
|
using MAF1.PluginContract;
|
||||||
using MAF1.Utils;
|
using MAF1.Utils;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// 进程外 Weather 插件。同样只做 stdio 适配,天气实现仍在 MAF1.Core。
|
||||||
using MAF1.Agents.Weather;
|
using MAF1.Agents.Weather;
|
||||||
using MAF1.PluginContract;
|
using MAF1.PluginContract;
|
||||||
using MAF1.Tools;
|
using MAF1.Tools;
|
||||||
|
|||||||
Reference in New Issue
Block a user