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; /// /// 创建聊天 Agent 的工厂。根据 Endpoint 判断走 Azure OpenAI 还是 OpenAI 兼容接口(DeepSeek 等)。 /// 密钥优先级:appsettings Llm → 环境变量。浏览器永远拿不到 Key。 /// 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); } /// 把 ChatClient 包成 Microsoft.Agents.AI 的 AIAgent,并挂上 tools。Id 固定为 name,Handoff 工具才是 handoff_to_n1。 public AIAgent CreateAgent(string name, string instructions, IList? 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); } /// 合并配置文件和环境变量。插件进程里环境变量通常由宿主写入。 public static LlmOptions Load(IConfiguration config) { LlmOptions options = config.GetSection("Llm").Get() ?? 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); } /// OpenAI 兼容 API 要求 base URL 以 /v1 结尾,用户常只填到域名。 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 ""; } }