using System.Net.Sockets; using System.Threading.Tasks; using OpenAI; using OpenAI.Responses; namespace OllamaStudy.UseExtensionsAI; /// /// Ollama兼容OpenAI接口,可以直接使用OpenAI的SDK调用 /// public class OpenAISdkTest { private ITestOutputHelper _output; private IOptionsMonitor _ollamaOptionsMonitor; private OpenAIClient _defaultOpenAIClient; private ChatClient _singtonChatClient; public OpenAISdkTest ( ITestOutputHelper outputHelper, OpenAIClient defaultOpenAIClient, IOptionsMonitor ollamaOptionsMonitor, //使用了FromKeyedServices特性,所以需要使用IKeyedServiceCollection注册服务 [FromKeyedServices("OpenAIChatClient")]ChatClient singtonChatClient ) { _output = outputHelper; _defaultOpenAIClient = defaultOpenAIClient; _ollamaOptionsMonitor = ollamaOptionsMonitor; _singtonChatClient = singtonChatClient; } #region 使用客户端库 /// /// 从OpenAIClient获取各种业务Client /// [Fact] public void GetClients_Test() { #pragma warning disable OPENAI001 Assert.NotNull(_defaultOpenAIClient); //音频客户端 var audioClient = _defaultOpenAIClient.GetAudioClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(audioClient); //聊天客户端 var chatClient = _defaultOpenAIClient.GetChatClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(chatClient); //嵌入客户端 var embeddingClient = _defaultOpenAIClient.GetEmbeddingClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(embeddingClient); //图像客户端 var imageClient = _defaultOpenAIClient.GetImageClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(imageClient); //微调客户端 var moderationClient = _defaultOpenAIClient.GetModerationClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(moderationClient); //文件客户端 var openAIFileClient = _defaultOpenAIClient.GetOpenAIFileClient(); Assert.NotNull(openAIFileClient); //模型客户端 var modelClient = _defaultOpenAIClient.GetOpenAIModelClient(); Assert.NotNull(modelClient); //助手客户端(仅评估) var assistantClient = _defaultOpenAIClient.GetAssistantClient(); Assert.NotNull(assistantClient); //批量客户端(仅评估) var batchClient = _defaultOpenAIClient.GetBatchClient(); Assert.NotNull(batchClient); //评估客户端(仅评估) var evaluationClient = _defaultOpenAIClient.GetEvaluationClient(); Assert.NotNull(evaluationClient); //微调客户端(仅评估) var FineTuningClient = _defaultOpenAIClient.GetFineTuningClient(); Assert.NotNull(FineTuningClient); //响应客户端(仅评估) var openAIResponseClient = _defaultOpenAIClient.GetOpenAIResponseClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(openAIResponseClient); //实时客户端(仅评估) #pragma warning disable OPENAI002 var realtimeClient = _defaultOpenAIClient.GetRealtimeClient(); Assert.NotNull(realtimeClient); #pragma warning restore OPENAI002 //向量存储客户端(仅评估) var vectorStoreClient = _defaultOpenAIClient.GetVectorStoreClient(); Assert.NotNull(vectorStoreClient); #pragma warning restore OPENAI001 } /// /// 自定义URL和API密钥 /// [Fact] public void Custom_OpenAIClient_Test() { var option = new OpenAIClientOptions() { OrganizationId = "TianyiJituan", ProjectId = "StudyProject", Endpoint = new Uri("http://localhost:11434/v1") }; //本地Ollama服务,不需要API密钥(随便填写) var openAIClient = new OpenAIClient(new ApiKeyCredential("nokey"), option); var chatClient = openAIClient.GetChatClient(_ollamaOptionsMonitor.CurrentValue.Model); Assert.NotNull(openAIClient); Assert.NotNull(chatClient); } /// /// 自定义URL和API密钥 /// [Fact] public void Custom_ChatClient_Test() { var option = new OpenAIClientOptions() { OrganizationId = "TianyiJituan", ProjectId = "StudyProject", UserAgentApplicationId = "StudyAgentApp", Endpoint = new Uri("http://localhost:11434/v1"), }; var chatClient = new ChatClient(_ollamaOptionsMonitor.CurrentValue.Model,new ApiKeyCredential("nokey"),option); Assert.NotNull(chatClient); } /// /// 使用异步API /// 每个客户端方法在同一客户端类中都有一个异步变体 /// [Fact] public async Task UseAsyncAPI_Test() { ChatClient chatClient = _defaultOpenAIClient.GetChatClient(_ollamaOptionsMonitor.CurrentValue.Model); ClientResult result = await chatClient.CompleteChatAsync("你好,请问河南的省会是什么?"); var responseText = result.Value.Content.First().Text; _output.WriteLine(responseText); Assert.NotNull(result); Assert.Contains("郑州",responseText); } #endregion #region 如何使用依赖注入 /// /// OpenAI 客户端是线程安全的。可以在DI中安全地注册为单例. /// 这最大限度地提高了资源效率和 HTTP 连接重用。 /// [Fact] public void Singleton_ChatClient_Test() { var result = _singtonChatClient.CompleteChat("你好"); var responseText = result.Value.Content.First().Text; _output.WriteLine(responseText); Assert.NotNull(result); } #endregion #region 如何将聊天完成与流式处理一起使用 /// /// 使用同步流式处理API,可以立即收到响应,而无需等待模型完成。 /// [Fact] public void Streamimg_ChatClient_Test() { CollectionResult result = _singtonChatClient.CompleteChatStreaming("你好"); var stringBuilder = new StringBuilder(500); foreach (StreamingChatCompletionUpdate completionUpdate in result) { if (completionUpdate.ContentUpdate.Count > 0) { stringBuilder.Append(completionUpdate.ContentUpdate[0].Text); } } _output.WriteLine(stringBuilder.ToString()); } /// /// 使用异步流式处理API /// [Fact] public async Task Singleton_Async_ChatClient_Test() { var result = _singtonChatClient.CompleteChatStreamingAsync("你好"); var stringBuilder = new StringBuilder(500); await foreach (StreamingChatCompletionUpdate completionUpdate in result) { if (completionUpdate.ContentUpdate.Count > 0) { stringBuilder.Append(completionUpdate.ContentUpdate[0].Text); } } _output.WriteLine(stringBuilder.ToString()); } #endregion #region 如何将聊天完成与工具和函数调用一起使用 /// /// 调用工具和函数 /// [Fact] public void Use_FunctionCalling_ChatClient_Test() { ChatTool getCurrentLocationTool = ChatTool.CreateFunctionTool ( functionName: nameof(GetCurrentLocation), functionDescription: "Get the user's current location" ); ChatTool getCurrentWeatherTool = ChatTool.CreateFunctionTool ( functionName: nameof(GetCurrentWeather), functionDescription: "Get the current weather in a given location", functionParameters: BinaryData.FromBytes(""" { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" }, "unit": { "type": "string", "enum": [ "celsius", "fahrenheit" ], "description": "The temperature unit to use. Infer this from the specified location." } }, "required": [ "location" ] } """u8.ToArray()) ); List messages = [new UserChatMessage("What's the weather like beijing today?"),]; ChatCompletionOptions options = new() { Tools = { getCurrentLocationTool, getCurrentWeatherTool }, }; bool requiresAction = false; do //实质上是手动调用函数 { requiresAction = false; ChatCompletion completion = _singtonChatClient.CompleteChat(messages, options); switch (completion.FinishReason) { case OpenAI.Chat.ChatFinishReason.Stop: { // Add the assistant message to the conversation history. messages.Add(new AssistantChatMessage(completion)); //输出 foreach (var message in messages) { _output.WriteLine(message.Content.First().Text); } break; } case OpenAI.Chat.ChatFinishReason.ToolCalls: { // First, add the assistant message with tool calls to the conversation history. messages.Add(new AssistantChatMessage(completion)); // Then, add a new tool message for each tool call that is resolved. foreach (ChatToolCall toolCall in completion.ToolCalls) { switch (toolCall.FunctionName) { case nameof(GetCurrentLocation): { string toolResult = GetCurrentLocation(); messages.Add(new ToolChatMessage(toolCall.Id, toolResult)); break; } case nameof(GetCurrentWeather): { // The arguments that the model wants to use to call the function are specified as a // stringified JSON object based on the schema defined in the tool definition. Note that // the model may hallucinate arguments too. Consequently, it is important to do the // appropriate parsing and validation before calling the function. using JsonDocument argumentsJson = JsonDocument.Parse(toolCall.FunctionArguments); bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location); bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit); if (!hasLocation) { throw new ArgumentNullException(nameof(location), "The location argument is required."); } string toolResult = hasUnit ? GetCurrentWeather(location.GetString() ?? "", unit.GetString() ?? "") : GetCurrentWeather(location.GetString() ?? ""); messages.Add(new ToolChatMessage(toolCall.Id, toolResult)); break; } default: { // Handle other unexpected calls. throw new NotImplementedException(); } } } requiresAction = true; break; } case OpenAI.Chat.ChatFinishReason.Length: throw new NotImplementedException("Incomplete model output due to MaxTokens parameter or token limit exceeded."); case OpenAI.Chat.ChatFinishReason.ContentFilter: throw new NotImplementedException("Omitted content due to a content filter flag."); case OpenAI.Chat.ChatFinishReason.FunctionCall: throw new NotImplementedException("Deprecated in favor of tool calls."); default: throw new NotImplementedException(completion.FinishReason.ToString()); } } while (requiresAction); } #endregion #region 如何将聊天完成与结构化输出一起使用 [Fact] public void StructuredOutputs_ChatClient_Test() { List messages =[new UserChatMessage("How can I solve 8x + 7 = -23?"),]; ChatCompletionOptions options = new() { ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( jsonSchemaFormatName: "math_reasoning", jsonSchema: BinaryData.FromBytes(""" { "type": "object", "properties": { "steps": { "type": "array", "items": { "type": "object", "properties": { "explanation": { "type": "string" }, "output": { "type": "string" } }, "required": ["explanation", "output"], "additionalProperties": false } }, "final_answer": { "type": "string" } }, "required": ["steps", "final_answer"], "additionalProperties": false } """u8.ToArray()), jsonSchemaIsStrict: true) }; ChatCompletion completion = _singtonChatClient.CompleteChat(messages, options); using JsonDocument structuredJson = JsonDocument.Parse(completion.Content[0].Text); _output.WriteLine($"Final answer: {structuredJson.RootElement.GetProperty("final_answer")}"); _output.WriteLine("Reasoning steps:"); foreach (JsonElement stepElement in structuredJson.RootElement.GetProperty("steps").EnumerateArray()) { _output.WriteLine($" - Explanation: {stepElement.GetProperty("explanation")}"); _output.WriteLine($" Output: {stepElement.GetProperty("output")}"); } } #endregion #region 如何将聊天完成与音频一起使用 /// /// 生成语音 /// //[Fact] [Fact(Skip ="因本地Ollama测试环境,不支持OpenAI音频接口,忽略测试")] //[Fact] public void GenerateSpeech_AudioClient_Test() { var aiClientOption = new OpenAIClientOptions() { Endpoint = new Uri("https://sg.uiuiapi.com/v1") }; AudioClient client = new("tts-1-1106", new ApiKeyCredential("sk-4azuOUkbzNGP22pQkND8ad1vZl7ladwBQyqGKlWWZyxYgX1L"), aiClientOption); string input = """ 对于那些照顾室内植物的人来说,过度浇水是一个常见的问题。 为了防止这种情况,让土壤在两次浇水之间变干至关重要。 与其按照固定的时间表浇水,不如考虑使用水分计来准确测量土壤的湿度。 如果土壤保持水分,明智的做法是再推迟几天浇水。 如有疑问,“节约用水,保持少即是多”的方法通常更安全。 """; BinaryData speech = client.GenerateSpeech(input, GeneratedSpeechVoice.Alloy); using FileStream stream = File.OpenWrite($"{Guid.NewGuid()}.mp3"); speech.ToStream().CopyTo(stream); } /// /// 语音转文本 /// [Fact(Skip ="因本地Ollama测试环境,不支持OpenAI音频接口,忽略测试")] //[Fact] public void AudioToText_AudioClient_Test() { var aiClientOption = new OpenAIClientOptions() { Endpoint = new Uri("https://sg.uiuiapi.com/v1") }; AudioClient client = new("whisper-1", new ApiKeyCredential("sk-4azuOUkbzNGP22pQkND8ad1vZl7ladwBQyqGKlWWZyxYgX1L"), aiClientOption); string audioFilePath = Path.Combine(Environment.CurrentDirectory, "Assets", "yuxia.mp3"); AudioTranscription transcription = client.TranscribeAudio(audioFilePath); _output.WriteLine($"{transcription.Text}"); } #endregion #region 如何将响应与流式处理和推理结合使用 [Fact(Skip ="因本地Ollama测试环境不支持,忽略测试")] public void Responses_With_Streaming_Reasoning_ChatClient_Test() { } #endregion [Fact] #region 如何将响应与文件搜索一起使用 public async Task Respones_With_FileSearch_Test() { #pragma warning disable OPENAI001 OpenAIResponseClient client = new( model: "gpt-4o-mini", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")); ResponseTool fileSearchTool = ResponseTool.CreateFileSearchTool(vectorStoreIds: ["sssssssss"]); OpenAIResponse response = await client.CreateResponseAsync ( userInputText: "According to available files, what's the secret number?", new ResponseCreationOptions() { Tools = { fileSearchTool } } ); foreach (ResponseItem outputItem in response.OutputItems) { if (outputItem is FileSearchCallResponseItem fileSearchCall) { Console.WriteLine($"[file_search] ({fileSearchCall.Status}): {fileSearchCall.Id}"); foreach (string query in fileSearchCall.Queries) { Console.WriteLine($" - {query}"); } } else if (outputItem is MessageResponseItem message) { Console.WriteLine($"[{message.Role}] {message.Content.FirstOrDefault()?.Text}"); } } #pragma warning restore OPENAI001 } #endregion #region 如何将响应与网络搜索结合使用 [Fact] public async Task WebSearch_ChatClient_Test() { #pragma warning disable OPENAI001 OpenAIResponseClient client = _defaultOpenAIClient.GetOpenAIResponseClient(ModelSelecter.ModelWithRawmodel); OpenAIResponse response = await client.CreateResponseAsync ( userInputText: "What's a happy news headline from today?", new ResponseCreationOptions() { Tools = { ResponseTool.CreateWebSearchTool() }, } ); foreach (ResponseItem item in response.OutputItems) { if (item is WebSearchCallResponseItem webSearchCall) { Console.WriteLine($"[Web search invoked]({webSearchCall.Status}) {webSearchCall.Id}"); } else if (item is MessageResponseItem message) { Console.WriteLine($"[{message.Role}] {message.Content?.FirstOrDefault()?.Text}"); } } #pragma warning restore OPENAI001 } #endregion #region 如何生成文本嵌入 #endregion #region 如何生成图像 #endregion #region 如何转录音频 #endregion #region 如何将助手与检索增强生成 (RAG) 结合使用。 #endregion #region 如何将助手与流媒体和视觉结合使用 #endregion #region 高级方案 #endregion #region 私有方法 private static string GetCurrentLocation() { // Call the location API here. return "San Francisco"; } private static string GetCurrentWeather(string location, string unit = "celsius") { // Call the weather API here. return $"31 {unit}"; } #endregion }