166 lines
5.3 KiB
C#
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] + "…";
|
||
|
|
}
|
||
|
|
}
|