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
}