65.9K
CodeProject 正在变化。 阅读更多。
Home

支持 Assistants 流式 API 的 C# OpenAI 库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2023年11月11日

CPOL

3分钟阅读

viewsIcon

61041

C# OpenAI 库,支持助手、聊天补全、微调、图像生成等功能

引言

本周,OpenAI 推出了 GPT4-turbo、GPTs、GPT 商店等。他们还发布了一个名为 Assistant API 的新 API。它是新的,还没有 C# 库。所以,我开发了它。

新增!!!(2024-5-07)

我添加了矢量文件,在聊天补全中使用。请查看!

如何使用?

通过 Nuget 下载

HigLabo.OpenAI

HigLabo.OpenAI 就是那个。

你可以在这里看到示例代码

主要类是 OpenAIClient。你创建 OpenAIClient 类用于 OpenAI API。

var apiKey = "your api key of OpenAI";
var cl = new OpenAIClient(apiKey);

对于 Azure 端点。

var apiKey = "your api key of OpenAI";
var cl = new OpenAIClient(new AzureSettings
(apiKey, "https://tinybetter-work-for-our-future.openai.azure.com/", "MyDeploymentName"));

调用 ChatCompletion 端点。

var cl = new OpenAIClient("API KEY");
var p = new ChatCompletionsParameter();
p.Messages.Add(new ChatMessage(ChatMessageRole.User, $"How to enjoy coffee?"));
p.Model = "gpt-4";
var res = await cl.ChatCompletionsAsync(p);
foreach (var choice in res.Choices)
{
    Console.Write(choice.Message.Content);
}

Console.WriteLine();
Console.WriteLine();
Console.WriteLine("----------------------------------------");
Console.WriteLine("Total tokens: " + res.Usage.Total_Tokens);

使用服务器发送事件消耗 ChatCompletion 端点。

var cl = new OpenAIClient("API KEY");
var result = new ChatCompletionStreamResult();
await foreach (string text in cl.ChatCompletionsStreamAsync("How to enjoy coffee?", "gpt-4", result, CancellationToken.None))
{
    Console.Write(text);
}
Console.WriteLine();
Console.WriteLine("Finish reason: " + result.GetFinishReason());

带有函数调用的 ChatCompletion

var cl = new OpenAIClient("API KEY");
var p = new ChatCompletionsParameter();
//ChatGPT can correct Newyork,Sanflansisco to New york and San Flancisco.
p.Messages.Add(new ChatMessage(ChatMessageRole.User, 
 $"I want to know the whether of these locations. Newyork, Sanflansisco, Paris, Tokyo."));
p.Model = "gpt-4";

{
    var tool = new ToolObject("function");
    tool.Function = new FunctionObject();
    tool.Function.Name = "getWhether";
    tool.Function.Description = "This service can get whether of specified location.";
    tool.Function.Parameters = new
    {
        type = "object",
        properties = new
        {
            locationList = new
            {
                type = "array",
                description = "Location list that you want to know.",
                items = new
                {
                    type = "string",
                }
            }
        }
    };
    p.Tools = new List<ToolObject>();
    p.Tools.Add(tool);
}
{
    var tool = new ToolObject("function");
    tool.Function = new FunctionObject();
    tool.Function.Name = "getLatLong";
    tool.Function.Description = 
         "This service can get latitude and longitude of specified location.";
    tool.Function.Parameters = new
    {
        type = "object",
        properties = new
        {
            locationList = new
            {
                type = "array",
                description = "Location list that you want to know.",
                items = new
                {
                    type = "string",
                }
            }
        }
    };
    p.Tools = new List<ToolObject>();
    p.Tools.Add(tool);
}

var result = new ChatCompletionStreamResult();
await foreach (var text in cl.ChatCompletionsStreamAsync(p, result, CancellationToken.None))
{
    Console.Write(text);
}
Console.WriteLine();

foreach (var f in result.GetFunctionCallList())
{
    Console.WriteLine("Function name is " + f.Name);
    Console.WriteLine("Arguments is " + f.Arguments);
}

通过 ChatCompletion 端点使用视觉 API。

var cl = new OpenAIClient("API KEY");
var p = new ChatCompletionsParameter();

var message = new ChatImageMessage(ChatMessageRole.User);
message.AddTextContent("Please describe this image.");
message.AddImageFile(Path.Combine(Environment.CurrentDirectory, "Image", "Pond.jpg"));
p.Messages.Add(message);
p.Model = "gpt-4-vision-preview";
p.Max_Tokens = 300;

var result = new ChatCompletionStreamResult();
await foreach (var text in cl.ChatCompletionsStreamAsync(p, result, CancellationToken.None))
{
    Console.Write(text);
}

上传文件以进行微调或传递给助手。

var p = new FileUploadParameter();
p.File.SetFile("my_file.pdf", File.ReadAllBytes("D:\\Data\\my_file.pdf"));
p.Purpose = "assistants";
var res = await client.FileUploadAsync(p);
Console.WriteLine(res);

图像生成。

var cl = new OpenAIClient("API KEY");
var res = await cl.ImagesGenerationsAsync("Blue sky and green field.");
foreach (var item in res.Data)
{
    Console.WriteLine(item.Url);
}

通过 API 创建助手。

var cl = new OpenAIClient("API KEY");
var p = new AssistantCreateParameter();
p.Name = "Legal tutor";
p.Instructions = "You are a personal legal tutor. 
                  Write and run code to legal questions based on passed files.";
p.Model = "gpt-4-1106-preview";

p.Tools = new List<ToolObject>();
p.Tools.Add(new ToolObject("code_interpreter"));
p.Tools.Add(new ToolObject("retrieval"));

var res = await cl.AssistantCreateAsync(p);
Console.WriteLine(res);

将文件添加到助手。

var cl = new OpenAIClient("API KEY");
var res = await cl.FilesAsync();
foreach (var item in res.Data)
{
    if (item.Purpose == "assistants")
    {
        var res1 = await cl.AssistantFileCreateAsync(id, item.Id);
    }
}

调用助手流式 API。

var assistantId = "your assistant Id";
var threadId = "your thread Id";
if (threadId.Length == 0)
{
    var res = await cl.ThreadCreateAsync();
    threadId = res.Id;
}
{
    var p = new MessageCreateParameter();
    p.Thread_Id = threadId;
    p.Role = "user";
    p.Content = "Hello! I want to know how to use OpenAI assistant API 
                 to get stream response.";
    var res = await cl.MessageCreateAsync(p);
}
{
    var p = new RunCreateParameter();
    p.Assistant_Id = assistantId;
    p.Thread_Id = threadId;
    p.Stream = true;
    var result = new AssistantMessageStreamResult();
    await foreach (string text in cl.RunCreateStreamAsync
                  (p, result, CancellationToken.None))
    {
        Console.Write(text);
    }
    Console.WriteLine();
    // You can get each server sent event value by these property.
    Console.WriteLine(JsonConvert.SerializeObject(result.Thread));
    Console.WriteLine(JsonConvert.SerializeObject(result.Run));
    Console.WriteLine(JsonConvert.SerializeObject(result.RunStep));
    Console.WriteLine(JsonConvert.SerializeObject(result.Message));
}

函数调用并将工具输出提交给助手 API。

var cl = new OpenAIClient("API KEY");

var p0 = new MessageCreateParameter();
p0.Thread_Id = threadId;
p0.Role = "user";
p0.Content = $"I want to know the whether of Tokyo.";
var res = await cl.MessageCreateAsync(p0);

var p = new RunCreateParameter();
p.Assistant_Id = assistantId;
p.Thread_Id = threadId;
p.Tools = new List<ToolObject>();
p.Tools.Add(CreateGetWheatherTool());

var result = new AssistantMessageStreamResult();
await foreach (string text in cl.RunCreateStreamAsync(p, result, CancellationToken.None))
{
    Console.Write(text);
}
Console.WriteLine();

Console.WriteLine(JsonConvert.SerializeObject(result.Run));

if (result.Run != null)
{
    if (result.Run.Status == "requires_action" &&
        result.Run.Required_Action != null)
    {
        var p1 = new SubmitToolOutputsParameter();
        p1.Thread_Id = threadId;
        p1.Run_Id = result.Run.Id;
        p1.Tool_Outputs = new();
        foreach (var toolCall in result.Run.Required_Action.GetToolCallList())
        {
            Console.WriteLine(toolCall.ToString());

            //Pass output from calling your function. 
            var output = new ToolOutput();
            output.Tool_Call_Id = toolCall.Id;
            var o = new
            {
                Wheather = "Cloud",
                Temperature = "20℃",
                Forecast = "Rain after 3 hours",
            };
            output.Output = $"{toolCall.Function.Arguments} is {JsonConvert.SerializeObject(o)}";
            p1.Tool_Outputs.Add(output);
        }
        await foreach (var text in cl.SubmitToolOutputsStreamAsync(p1))
        {
            Console.Write(text);
        }
    }
}

类架构

主要的类有

  • OpenAIClient
  • XXXParameter
  • XXXAsync
  • XXXResponse
  • RestApiResponse
  • QueryParameter

OpenAIClient

此类管理 API 密钥并调用端点。

你可以在智能感知中将所有端点视为方法。

我为所有具有必需参数的端点提供了方法。这些是调用端点的最简单方法。

var res = await cl.AudioTranscriptionsAsync("GoodMorningItFineDayToday.mp3"
    , new MemoryStream(File.ReadAllBytes("D:\\Data\\Dev\\GoodMorningItFineDayToday.mp3"))
    , "whisper-1");

OpenAI 提供三种类型的端点。这些内容类型是 jsonform-dataserver-sent-event。示例就在那里。

Json 端点

Form-data 端点

流式端点

因此,我提供了 SendJsonAsyncSendFormDataAsyncGetStreamAsync 方法来调用这些端点。这些类管理 HTTP 标头和 content-type 并正确处理响应。

你可以通过传递参数对象来调用端点。

var p = new AudioTranscriptionsParameter();
p.File.SetFile("GoodMorningItFineDayToday.mp3", 
new MemoryStream(File.ReadAllBytes("D:\\Data\\Dev\\GoodMorningItFineDayToday.mp3")));
p.Model = "whisper-1";
var res = await cl.SendFormDataAsync<AudioTranscriptionsParameter, 
          AudioTranscriptionsResponse>(p, CancellationToken.None);

但是这种方法需要参数和响应的类型,所以我提供了易于使用的方法。

var p = new AudioTranscriptionsParameter();
p.File.SetFile("GoodMorningItFineDayToday.mp3", 
new MemoryStream(File.ReadAllBytes("D:\\Data\\Dev\\GoodMorningItFineDayToday.mp3")));
p.Model = "whisper-1";
var res = await cl.AudioTranscriptionsAsync(p);

XXXParameter

我提供了代表端点所有值的参数类。

例如,这是创建助手端点。

这是 AssistantCreateParameter 类。

/// <summary>
/// Create an assistant with a model and instructions.
/// <seealso href="https://api.openai.com/v1/assistants">
/// https://api.openai.com/v1/assistants</seealso>
/// </summary>
public partial class AssistantCreateParameter : RestApiParameter, IRestApiParameter
{
    string IRestApiParameter.HttpMethod { get; } = "POST";
    /// <summary>
    /// ID of the model to use. You can use the List models API to see all of 
    /// your available models, or see our Model overview for descriptions of them.
    /// </summary>
    public string Model { get; set; } = "";
    /// <summary>
    /// The name of the assistant. The maximum length is 256 characters.
    /// </summary>
    public string? Name { get; set; }
    /// <summary>
    /// The description of the assistant. The maximum length is 512 characters.
    /// </summary>
    public string? Description { get; set; }
    /// <summary>
    /// The system instructions that the assistant uses. 
    /// The maximum length is 32768 characters.
    /// </summary>
    public string? Instructions { get; set; }
    /// <summary>
    /// A list of tool enabled on the assistant. 
    /// There can be a maximum of 128 tools per assistant. 
    /// Tools can be of types code_interpreter, retrieval, or function.
    /// </summary>
    public List<ToolObject>? Tools { get; set; }
    /// <summary>
    /// A list of file IDs attached to this assistant. 
    /// There can be a maximum of 20 files attached to the assistant. 
    /// Files are ordered by their creation date in ascending order.
    /// </summary>
    public List<string>? File_Ids { get; set; }
    /// <summary>
    /// Set of 16 key-value pairs that can be attached to an object. 
    /// This can be useful for storing additional information about the object 
    /// in a structured format. Keys can be a maximum of 64 characters long 
    /// and values can be a maximum of 512 characters long.
    /// </summary>
    public object? Metadata { get; set; }

    string IRestApiParameter.GetApiPath()
    {
        return $"/assistants";
    }
    public override object GetRequestBody()
    {
        return new {
            model = this.Model,
            name = this.Name,
            description = this.Description,
            instructions = this.Instructions,
            tools = this.Tools,
            file_ids = this.File_Ids,
            metadata = this.Metadata,
        };
    }
}

这些参数类是从 API 文档生成的。你可以在 Github 上看到实际的生成器代码。

XXXAsync

这些方法是生成的。我生成了四种方法,你可以轻松调用 API 端点。

AssistantCreate 端点的一个例子。

public async ValueTask<AssistantCreateResponse> AssistantCreateAsync(string model)
public async ValueTask<AssistantCreateResponse> 
    AssistantCreateAsync(string model, CancellationToken cancellationToken)
public async ValueTask<AssistantCreateResponse> 
    AssistantCreateAsync(AssistantCreateParameter parameter)
public async ValueTask<AssistantCreateResponse> 
    AssistantCreateAsync(AssistantCreateParameter parameter, 
    CancellationToken cancellationToken)

基本上,有两种类型的方法。

一种是仅传递所需参数值。

AssistantCreate 端点需要模型。所以,我生成了

public async ValueTask<AssistantCreateResponse> AssistantCreateAsync(string model)

此方法易于使用,只需使用所需参数即可调用端点。

另一种是你可以使用所有参数值调用 API 端点。

你可以像这样创建参数

var p = new AssistantCreateParameter();
p.Name = "Legal tutor";
p.Instructions = "You are a personal legal tutor. 
                  Write and run code to legal questions based on passed files.";
p.Model = "gpt-4-1106-preview";

p.Tools = new List<ToolObject>();
p.Tools.Add(new ToolObject("code_interpreter"));
p.Tools.Add(new ToolObject("retrieval"));

并将其传递给方法。

var res = await cl.AssistantCreateAsync(p);

XXXResponse

响应类表示 API 端点的实际响应数据。

例如,检索助手端点返回助手对象。

我提供了 AssistantObjectResponse。(我创建了这个类,而不是代码生成。)

public class AssistantObjectResponse: RestApiResponse
{
    public string Id { get; set; } = "";
    public int Created_At { get; set; }
    public DateTimeOffset CreateTime
    {
        get
        {
            return new DateTimeOffset
               (DateTime.UnixEpoch.AddSeconds(this.Created_At), TimeSpan.Zero);
        }
    }
    public string Name { get; set; } = "";
    public string Description { get; set; } = "";
    public string Model { get; set; } = "";
    public string Instructions { get; set; } = "";
    public List<ToolObject> Tools { get; set; } = new();
    public List<string>? File_Ids { get; set; }
    public object? MetaData { get; set; }

    public override string ToString()
    {
        return $"{this.Id}\r\n{this.Name}\r\n{this.Instructions}";
    }
}

你可以获取 API 端点的响应值。

RestApiResponse

有时,你想获取响应的元数据。你可以获取响应文本、创建此响应的请求对象等。

这是 RestApiResponse 类。

public abstract class RestApiResponse : IRestApiResponse
{
    object? IRestApiResponse.Parameter
    {
        get { return _Parameter; }
    }
    HttpRequestMessage IRestApiResponse.Request
    {
        get { return _Request!; }
    }
    string IRestApiResponse.RequestBodyText
    {
        get { return _RequestBodyText; }
    }
    HttpStatusCode IRestApiResponse.StatusCode
    {
        get { return _StatusCode; }
    }
    Dictionary<String, String> IRestApiResponse.Headers
    {
        get { return _Headers; }
    }
    string IRestApiResponse.ResponseBodyText
    {
        get { return _ResponseBodyText; }
    }
}

这些是隐藏的属性。你可以通过转换为 RestApiResponse 类来访问它

var p = new AssistantCreateParameter();
p.Name = "Legal tutor";
p.Instructions = "You are a personal legal tutor. 
                  Write and run code to legal questions based on passed files.";
p.Model = "gpt-4-1106-preview";

var res = await cl.AssistantCreateAsync(p);
var iRes = res as RestApiResponse;
var responseText = iRes.ResponseBodyText;
Dictionary<string, string> responseHeaders = iRes.Headers;
var parameter = iRes.Parameter as AssistantCreateParameter;

你可以通过 RestApiResponse 将响应文本记录到你自己的日志数据库中。

QueryParameter

一些 API 端点提供过滤、分页功能。你可以通过 QueryParameter 类指定条件。

你可以像这样指定顺序

var p = new MessagesParameter();
p.Thread_Id = "thread_xxxxxxxxxxxx";
p.QueryParameter.Order = "asc";

结论

我对 OpenAI GPTs、GPT 商店等感到非常兴奋。如果你也对 OpenAI 感兴趣,我的库可能会对你的工作有所帮助。 很高兴使用!

历史

  • 2023年11月6日:OpenAI 发布了助手 API
  • 2023年11月11日:首次发布 HigLabo.OpenAI
  • 2023年12月2日:添加发送消息示例
  • 2024年1月8日:添加视觉 API 功能
  • 2024年2月12日:更新文件上传端点
  • 2024年3月17日:添加助手流式 API 的功能
© . All rights reserved.