You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

569 lines
21 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using System.Net.Sockets;
using System.Threading.Tasks;
using OpenAI;
using OpenAI.Responses;
namespace OllamaStudy.UseExtensionsAI;
/// <summary>
/// Ollama兼容OpenAI接口可以直接使用OpenAI的SDK调用
/// </summary>
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,
//使用了FromKeyedServices特性所以需要使用IKeyedServiceCollection注册服务
[FromKeyedServices("OpenAIChatClient")]ChatClient singtonChatClient
)
{
_output = outputHelper;
_defaultOpenAIClient = defaultOpenAIClient;
_ollamaOptionsMonitor = ollamaOptionsMonitor;
_singtonChatClient = singtonChatClient;
}
#region 使用客户端库
/// <summary>
/// 从OpenAIClient获取各种业务Client
/// </summary>
[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
}
/// <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 如何生成文本嵌入
#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
}