diff --git a/OllamaStudy.Core/OllamaStudy.Core.csproj b/OllamaStudy.Core/OllamaStudy.Core.csproj
index 722337e..c9de1bd 100644
--- a/OllamaStudy.Core/OllamaStudy.Core.csproj
+++ b/OllamaStudy.Core/OllamaStudy.Core.csproj
@@ -7,17 +7,17 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OllamaStudy.UseExtensionsAI/Assets/audio_french.wav b/OllamaStudy.UseExtensionsAI/Assets/audio_french.wav
new file mode 100644
index 0000000..847f346
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/audio_french.wav differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/audio_houseplant_care.mp3 b/OllamaStudy.UseExtensionsAI/Assets/audio_houseplant_care.mp3
new file mode 100644
index 0000000..8a2f614
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/audio_houseplant_care.mp3 differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/chengcheng.mp3 b/OllamaStudy.UseExtensionsAI/Assets/chengcheng.mp3
new file mode 100644
index 0000000..b97eede
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/chengcheng.mp3 differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/dongdong.mp3 b/OllamaStudy.UseExtensionsAI/Assets/dongdong.mp3
new file mode 100644
index 0000000..a4adfef
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/dongdong.mp3 differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/images_apple.png b/OllamaStudy.UseExtensionsAI/Assets/images_apple.png
new file mode 100644
index 0000000..4e04c26
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/images_apple.png differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/images_dog_and_cat.png b/OllamaStudy.UseExtensionsAI/Assets/images_dog_and_cat.png
new file mode 100644
index 0000000..063f3e4
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/images_dog_and_cat.png differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/images_flower_vase.png b/OllamaStudy.UseExtensionsAI/Assets/images_flower_vase.png
new file mode 100644
index 0000000..b2647dd
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/images_flower_vase.png differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/images_flower_vase_with_mask.png b/OllamaStudy.UseExtensionsAI/Assets/images_flower_vase_with_mask.png
new file mode 100644
index 0000000..cb19542
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/images_flower_vase_with_mask.png differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/images_orange.png b/OllamaStudy.UseExtensionsAI/Assets/images_orange.png
new file mode 100644
index 0000000..30e8e89
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/images_orange.png differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/realtime_whats_the_weather_pcm16_24khz_mono.wav b/OllamaStudy.UseExtensionsAI/Assets/realtime_whats_the_weather_pcm16_24khz_mono.wav
new file mode 100644
index 0000000..399cd5a
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/realtime_whats_the_weather_pcm16_24khz_mono.wav differ
diff --git a/OllamaStudy.UseExtensionsAI/Assets/yuxia.mp3 b/OllamaStudy.UseExtensionsAI/Assets/yuxia.mp3
new file mode 100644
index 0000000..5b2cf25
Binary files /dev/null and b/OllamaStudy.UseExtensionsAI/Assets/yuxia.mp3 differ
diff --git a/OllamaStudy.UseExtensionsAI/OllamaStudy.UseExtensionsAI.csproj b/OllamaStudy.UseExtensionsAI/OllamaStudy.UseExtensionsAI.csproj
index bc3235c..7c5be87 100644
--- a/OllamaStudy.UseExtensionsAI/OllamaStudy.UseExtensionsAI.csproj
+++ b/OllamaStudy.UseExtensionsAI/OllamaStudy.UseExtensionsAI.csproj
@@ -19,8 +19,9 @@
-
-
+
+
+
@@ -38,4 +39,40 @@
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
diff --git a/OllamaStudy.UseExtensionsAI/OpenAISdkTest.cs b/OllamaStudy.UseExtensionsAI/OpenAISdkTest.cs
index d46b248..83858f4 100644
--- a/OllamaStudy.UseExtensionsAI/OpenAISdkTest.cs
+++ b/OllamaStudy.UseExtensionsAI/OpenAISdkTest.cs
@@ -1,4 +1,10 @@
-namespace OllamaStudy.UseExtensionsAI;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+using OpenAI;
+using OpenAI.Responses;
+
+namespace OllamaStudy.UseExtensionsAI;
///
/// Ollama兼容OpenAI接口,可以直接使用OpenAI的SDK调用
@@ -8,12 +14,22 @@ public class OpenAISdkTest
private ITestOutputHelper _output;
private IOptionsMonitor _ollamaOptionsMonitor;
private OpenAIClient _defaultOpenAIClient;
+ private ChatClient _singtonChatClient;
+
+ public OpenAISdkTest
+ (
+ ITestOutputHelper outputHelper,
+ OpenAIClient defaultOpenAIClient,
+ IOptionsMonitor ollamaOptionsMonitor,
- public OpenAISdkTest(ITestOutputHelper outputHelper, OpenAIClient defaultOpenAIClient, IOptionsMonitor ollamaOptionsMonitor)
+ //使用了FromKeyedServices特性,所以需要使用IKeyedServiceCollection注册服务
+ [FromKeyedServices("OpenAIChatClient")]ChatClient singtonChatClient
+ )
{
_output = outputHelper;
_defaultOpenAIClient = defaultOpenAIClient;
_ollamaOptionsMonitor = ollamaOptionsMonitor;
+ _singtonChatClient = singtonChatClient;
}
#region 使用客户端库
@@ -87,31 +103,435 @@ public class OpenAISdkTest
#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 如何生成文本嵌入
@@ -131,4 +551,18 @@ public class OpenAISdkTest
#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
}
diff --git a/OllamaStudy.UseExtensionsAI/Startup.cs b/OllamaStudy.UseExtensionsAI/Startup.cs
index e632b80..1d84f63 100644
--- a/OllamaStudy.UseExtensionsAI/Startup.cs
+++ b/OllamaStudy.UseExtensionsAI/Startup.cs
@@ -91,6 +91,19 @@ namespace OllamaStudy.UseExtensionsAI
};
return new ChatClient(options.Model,new ApiKeyCredential("nokey"),openAIClientOptions);
+ })
+
+ //OpenAI 客户端是线程安全的,可安全的注册为单例
+ .AddKeyedSingleton("OpenAIChatClient",(provider,obj) =>
+ {
+ var options = provider.GetRequiredService>().CurrentValue;
+
+ var openAIClientOptions = new OpenAIClientOptions()
+ {
+ Endpoint = new Uri(new Uri(options.OllamaServerUrl), "v1")
+ };
+
+ return new ChatClient(options.Model, new ApiKeyCredential("nokey"), openAIClientOptions);
});
}
#endregion
diff --git a/OllamaStudy.UseOllamaSharp/OllamaStudy.UseOllamaSharp.csproj b/OllamaStudy.UseOllamaSharp/OllamaStudy.UseOllamaSharp.csproj
index 85d77b8..0267971 100644
--- a/OllamaStudy.UseOllamaSharp/OllamaStudy.UseOllamaSharp.csproj
+++ b/OllamaStudy.UseOllamaSharp/OllamaStudy.UseOllamaSharp.csproj
@@ -15,7 +15,7 @@
-
+