feat: 新增方案 C 的 Handoff 路由宿主,并让抽城市支持自然语言。
把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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] + "…";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user