更新与类库更新

main
bicijinlian 1 week ago
parent 9215ae3f1f
commit 45636e0f32

@ -7,17 +7,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.8" />
</ItemGroup>
<ItemGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

@ -19,8 +19,9 @@
<PackageReference Include="Microsoft.Extensions.AI" Version="9.7.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.1-preview.1.25365.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="OllamaSharp" Version="5.3.3" />
<PackageReference Include="OllamaSharp.ModelContextProtocol" Version="5.3.3" />
<PackageReference Include="OllamaSharp" Version="5.3.4" />
<PackageReference Include="OllamaSharp.ModelContextProtocol" Version="5.3.4" />
<PackageReference Include="OpenAI" Version="2.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="Xunit.DependencyInjection" Version="9.9.1" />
<PackageReference Include="Xunit.DependencyInjection.Logging" Version="9.0.0" />
@ -38,4 +39,40 @@
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\audio_french.wav">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\audio_houseplant_care.mp3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\chengcheng.mp3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\dongdong.mp3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\images_apple.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\images_dog_and_cat.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\images_flower_vase.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\images_flower_vase_with_mask.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\images_orange.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\realtime_whats_the_weather_pcm16_24khz_mono.wav">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\yuxia.mp3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

@ -1,4 +1,10 @@
namespace OllamaStudy.UseExtensionsAI;
using System.Net.Sockets;
using System.Threading.Tasks;
using OpenAI;
using OpenAI.Responses;
namespace OllamaStudy.UseExtensionsAI;
/// <summary>
/// Ollama兼容OpenAI接口可以直接使用OpenAI的SDK调用
@ -8,12 +14,22 @@ public class OpenAISdkTest
private ITestOutputHelper _output;
private IOptionsMonitor<OllamaServerOption> _ollamaOptionsMonitor;
private OpenAIClient _defaultOpenAIClient;
private ChatClient _singtonChatClient;
public OpenAISdkTest
(
ITestOutputHelper outputHelper,
OpenAIClient defaultOpenAIClient,
IOptionsMonitor<OllamaServerOption> ollamaOptionsMonitor,
public OpenAISdkTest(ITestOutputHelper outputHelper, OpenAIClient defaultOpenAIClient, IOptionsMonitor<OllamaServerOption> 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
}
/// <summary>
/// 自定义URL和API密钥
/// </summary>
[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);
}
/// <summary>
/// 自定义URL和API密钥
/// </summary>
[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);
}
/// <summary>
/// 使用异步API
/// 每个客户端方法在同一客户端类中都有一个异步变体
/// </summary>
[Fact]
public async Task UseAsyncAPI_Test()
{
ChatClient chatClient = _defaultOpenAIClient.GetChatClient(_ollamaOptionsMonitor.CurrentValue.Model);
ClientResult<ChatCompletion> result = await chatClient.CompleteChatAsync("你好,请问河南的省会是什么?");
var responseText = result.Value.Content.First().Text;
_output.WriteLine(responseText);
Assert.NotNull(result);
Assert.Contains("郑州",responseText);
}
#endregion
#region 如何使用依赖注入
/// <summary>
/// OpenAI 客户端是线程安全的。可以在DI中安全地注册为单例.
/// 这最大限度地提高了资源效率和 HTTP 连接重用。
/// </summary>
[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 如何将聊天完成与流式处理一起使用
/// <summary>
/// 使用同步流式处理API可以立即收到响应而无需等待模型完成。
/// </summary>
[Fact]
public void Streamimg_ChatClient_Test()
{
CollectionResult<StreamingChatCompletionUpdate> 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());
}
/// <summary>
/// 使用异步流式处理API
/// </summary>
[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 如何将聊天完成与工具和函数调用一起使用
/// <summary>
/// 调用工具和函数
/// </summary>
[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<OpenAI.Chat.ChatMessage> 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<OpenAI.Chat.ChatMessage> 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 如何将聊天完成与音频一起使用
/// <summary>
/// 生成语音
/// </summary>
//[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);
}
/// <summary>
/// 语音转文本
/// </summary>
[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
}

@ -91,6 +91,19 @@ namespace OllamaStudy.UseExtensionsAI
};
return new ChatClient(options.Model,new ApiKeyCredential("nokey"),openAIClientOptions);
})
//OpenAI 客户端是线程安全的,可安全的注册为单例
.AddKeyedSingleton<OpenAI.Chat.ChatClient>("OpenAIChatClient",(provider,obj) =>
{
var options = provider.GetRequiredService<IOptionsMonitor<OllamaServerOption>>().CurrentValue;
var openAIClientOptions = new OpenAIClientOptions()
{
Endpoint = new Uri(new Uri(options.OllamaServerUrl), "v1")
};
return new ChatClient(options.Model, new ApiKeyCredential("nokey"), openAIClientOptions);
});
}
#endregion

@ -15,7 +15,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="OllamaSharp" Version="5.3.3" />
<PackageReference Include="OllamaSharp" Version="5.3.4" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="Xunit.DependencyInjection" Version="9.9.1" />
<PackageReference Include="Xunit.DependencyInjection.Logging" Version="9.0.0" />

Loading…
Cancel
Save