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

166 lines
5.3 KiB
C#

using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace MAF1.Route;
/// <summary>
/// 跑 Handoff。工具日志按 callId 去重:同一调用会同时出现在流式事件、完整回复、结束输出里,只打一遍。
/// </summary>
public static class HandoffRun
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
public static async Task RunAsync(Workflow workflow, string task)
{
HashSet<string> printedCalls = new(StringComparer.Ordinal);
List<ChatMessage> messages = [new(ChatRole.User, task)];
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
string? lastSummary = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
switch (evt)
{
case ExecutorFailedEvent failed:
Console.WriteLine();
Console.WriteLine($"[失败] {failed.ExecutorId}: {failed.Data}");
break;
case AgentResponseUpdateEvent update:
if (update.ExecutorId != lastExecutorId)
{
Console.WriteLine();
Console.Write($"{Friendly(update.ExecutorId)}: ");
lastExecutorId = update.ExecutorId;
}
if (!string.IsNullOrEmpty(update.Update.Text))
{
Console.Write(update.Update.Text);
}
WriteNewTools(update.ExecutorId, update.Update.Contents, printedCalls);
break;
case WorkflowOutputEvent output:
string? summary = ReadSummary(output);
if (!string.IsNullOrWhiteSpace(summary) && summary != lastSummary)
{
lastSummary = summary;
Console.WriteLine();
Console.WriteLine();
Console.WriteLine($"[最终输出] {Trim(summary)}");
}
break;
}
}
Console.WriteLine();
Console.WriteLine("======== Handoff 本轮结束 ========");
}
private static void WriteNewTools(string executorId, IList<AIContent> contents, HashSet<string> printedCalls)
{
foreach (AIContent content in contents)
{
if (content is FunctionCallContent call)
{
string key = "call:" + (string.IsNullOrWhiteSpace(call.CallId) ? call.Name + FormatArguments(call.Arguments) : call.CallId);
if (!printedCalls.Add(key))
{
continue;
}
Console.WriteLine();
Console.WriteLine($"[工具调用] {Friendly(executorId)} → {call.Name}");
Console.WriteLine($" 参数: {FormatArguments(call.Arguments)}");
continue;
}
if (content is FunctionResultContent result)
{
string key = "result:" + result.CallId;
if (!printedCalls.Add(key))
{
continue;
}
Console.WriteLine();
Console.WriteLine($"[工具返回] {Friendly(executorId)} → {result.CallId}");
Console.WriteLine($" 结果: {Trim(FormatResult(result.Result))}");
}
}
}
private static string? ReadSummary(WorkflowOutputEvent output)
{
if (output.As<List<ChatMessage>>() is List<ChatMessage> chat)
{
return LastAssistantText(chat);
}
return output.As<string>();
}
private static string FormatArguments(IDictionary<string, object?>? arguments)
{
if (arguments is null || arguments.Count == 0)
{
return "{}";
}
try
{
return JsonSerializer.Serialize(arguments, JsonOptions);
}
catch
{
return string.Join(", ", arguments.Select(kv => $"{kv.Key}={kv.Value}"));
}
}
private static string FormatResult(object? result)
=> result is null ? "(空)" : result.ToString() ?? "(空)";
private static string LastAssistantText(List<ChatMessage> chat)
{
string text = "";
foreach (ChatMessage message in chat)
{
if (message.Role == ChatRole.Assistant && !string.IsNullOrWhiteSpace(message.Text))
{
text = message.Text;
}
}
return text;
}
private static string Friendly(string executorId)
{
int us = executorId.IndexOf('_');
if (us > 0 && executorId[(us + 1)..] == executorId[..us])
{
return executorId[..us];
}
return executorId;
}
private static string Trim(string text)
{
string oneLine = text.Replace("\r\n", " ").Replace('\n', ' ').Trim();
return oneLine.Length <= 500 ? oneLine : oneLine[..500] + "…";
}
}