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

148 lines
5.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ClientModel;
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI;
using OpenAI.Chat;
namespace MAF1.Utils;
/// <summary>
/// 创建聊天 Agent 的工厂。根据 Endpoint 判断走 Azure OpenAI 还是 OpenAI 兼容接口(DeepSeek 等)。
/// 密钥优先级:appsettings Llm → 环境变量。浏览器永远拿不到 Key。
/// </summary>
public sealed class AgentFactory
{
private readonly ChatClient _chatClient;
private readonly bool _disableDeepSeekThinking;
public AgentFactory(LlmOptions options)
{
if (string.IsNullOrWhiteSpace(options.ApiKey))
{
throw new InvalidOperationException(
"""
还没有配置模型。请在 appsettings.json Llm 节点,或环境变量中设置:
OPENAI_API_KEY / Llm:ApiKey
OPENAI_ENDPOINT / Llm:Endpoint
OPENAI_CHAT_MODEL / Llm:Model
""");
}
string model = string.IsNullOrWhiteSpace(options.Model)
? DefaultModelFor(options.Endpoint)
: options.Model;
_disableDeepSeekThinking = LooksLikeDeepSeek(options.Endpoint);
if (LooksLikeAzureOpenAI(options.Endpoint))
{
Console.Error.WriteLine($"使用 Azure OpenAI: {options.Endpoint} 部署: {model}");
_chatClient = new AzureOpenAIClient(new Uri(options.Endpoint), new ApiKeyCredential(options.ApiKey))
.GetChatClient(model);
return;
}
OpenAIClientOptions clientOptions = new();
if (!string.IsNullOrWhiteSpace(options.Endpoint))
{
clientOptions.Endpoint = ToOpenAICompatibleEndpoint(options.Endpoint);
}
Console.Error.WriteLine($"使用 OpenAI 兼容接口: {clientOptions.Endpoint?.ToString() ?? "https://api.openai.com/v1"} 模型: {model}");
if (_disableDeepSeekThinking)
{
Console.Error.WriteLine("已关闭 DeepSeek thinking,避免 Handoff 多轮丢掉 reasoning_content 导致 HTTP 400。");
}
_chatClient = new OpenAIClient(new ApiKeyCredential(options.ApiKey), clientOptions)
.GetChatClient(model);
}
/// <summary>把 ChatClient 包成 Microsoft.Agents.AI 的 AIAgent,并挂上 tools。Id 固定为 nameHandoff 工具才是 handoff_to_n1。</summary>
public AIAgent CreateAgent(string name, string instructions, IList<AITool>? tools = null, string? description = null)
{
ChatOptions chatOptions = new()
{
Instructions = instructions,
Tools = tools,
};
ChatClientAgentOptions options = new()
{
Id = name,
Name = name,
Description = description,
ChatOptions = chatOptions,
};
return _chatClient.AsAIAgent(options, clientFactory: inner =>
_disableDeepSeekThinking
? new ConfigureOptionsChatClient(inner, DeepSeekThinking.Disable)
: inner);
}
/// <summary>合并配置文件和环境变量。插件进程里环境变量通常由宿主写入。</summary>
public static LlmOptions Load(IConfiguration config)
{
LlmOptions options = config.GetSection("Llm").Get<LlmOptions>() ?? new LlmOptions();
options.ApiKey = FirstNonEmpty(options.ApiKey, Env("OPENAI_API_KEY"), Env("AZURE_OPENAI_API_KEY"));
options.Endpoint = FirstNonEmpty(options.Endpoint, Env("OPENAI_ENDPOINT"), Env("OPENAI_BASE_URL"), Env("AZURE_OPENAI_ENDPOINT"));
options.Model = FirstNonEmpty(options.Model, Env("OPENAI_CHAT_MODEL"), Env("AZURE_OPENAI_DEPLOYMENT_NAME"));
return options;
}
private static string DefaultModelFor(string? endpoint)
{
if (!string.IsNullOrWhiteSpace(endpoint) && endpoint.Contains("deepseek", StringComparison.OrdinalIgnoreCase))
{
return "deepseek-chat";
}
return "gpt-4o-mini";
}
private static bool LooksLikeDeepSeek(string? endpoint)
=> !string.IsNullOrWhiteSpace(endpoint)
&& endpoint.Contains("deepseek", StringComparison.OrdinalIgnoreCase);
private static bool LooksLikeAzureOpenAI(string? endpoint)
{
if (string.IsNullOrWhiteSpace(endpoint) || !Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri))
{
return false;
}
string host = uri.Host;
return host.Contains("openai.azure.com", StringComparison.OrdinalIgnoreCase)
|| host.Contains("cognitiveservices.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)
{
string trimmed = endpoint.TrimEnd('/');
if (!trimmed.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
trimmed += "/v1";
}
return new Uri(trimmed);
}
private static string? Env(string name) => Environment.GetEnvironmentVariable(name);
private static string FirstNonEmpty(params string?[] values)
{
foreach (string? value in values)
{
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return "";
}
}