Skip to content

Commit

Permalink
Merge pull request #12 from Marco-Zechner/11-refactor-and-add-rest-of…
Browse files Browse the repository at this point in the history
…-api-model

11 refactor and add rest of api model

# Added the last missing API types
logit_bias
logprobs
top_logprobs

Added Custom Exception for GPTs Error response

Added Examples to Test Project
Split GPTModel into 3 partial classes
Added Payload and Response Handler where the json Content can be changed/printed/etc. by the user if needed.
Split some Models from one file into their own file each.

# Added LogProbs support in the GPT ChatResponse
Added example usage for LogProbs in ConversationWithCustomHandlers
Removed Deprecated Role "Functions"
  • Loading branch information
Marco-Zechner authored Jun 18, 2024
2 parents 994b5d5 + a5fcdab commit a2d077b
Show file tree
Hide file tree
Showing 19 changed files with 399 additions and 173 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using OpenAI.ChatGPT.Net.DataModels;
using OpenAI.ChatGPT.Net.Exeptions;

namespace OpenAI.ChatGPT.Net.IntegrationTests
{
public class ConversationWithCustomHandlers
{
public static async Task Run()
{
static string PrintPayloadHandler(string payload)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Payload:");
Console.WriteLine(payload);
Console.ResetColor();
return payload;
}

static string PrintResponseHandler(string response)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Response:");
Console.WriteLine(response);
Console.ResetColor();
return response;
}

GPTModel model = new("f", APIKey.KEY)
{
PayloadHandler = PrintPayloadHandler,
ResponseHandler = PrintResponseHandler,
Logprobs = true,
TopLogprobs = 2
};

/*======================================================================\\
|| The following code is the same as the SimpleConversationTest.cs file ||
|| But as an example the try-catch was moved inside and the catch won't ||
|| stop the conversation. ||
\\======================================================================*/

Console.Write($"{ChatRole.User}: ");
ChatMessage initialMessage = new(ChatRole.User, Console.ReadLine());
List<ChatMessage> messageHistory = [initialMessage];

while (true)
{
ChatResponse response = await model.Complete(messageHistory);
ChatMessage message;
try {
message = (ChatMessage)response; // This will throw an exception if the response is an error

Console.WriteLine($"{message.Role}: {message.Content}");
messageHistory.Add(message);
} catch (GPTAPIResponseException ex) {
Console.WriteLine(ex.Message);
// You don't need to stop here, you can try to recover from the error.
model.Model = "gpt-4o"; // Change to a different model since we know this is the error
Console.WriteLine("Switching to model \"gpt-4o\"");

// continue;
// be carful with skipping user input, this could lead to infinit recalling of the model.
// a better approach is to let the user reenter his message.
messageHistory.RemoveAt(messageHistory.Count - 1);
}

Console.Write($"{ChatRole.User}: ");
ChatMessage nextMessage = new(ChatRole.User, Console.ReadLine());
messageHistory.Add(nextMessage);
}
}
}
}
8 changes: 6 additions & 2 deletions OpenAI.ChatGPT.Net.IntegrationTests/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using OpenAI.ChatGPT.Net.IntegrationTests;

await SingleCompletionTest.Run();
//await SingleCompletionTest.TotalMin();

//SimpleConversationTest.Run();
//await SingleCompletionTest.Run();

//await SimpleConversationTest.Run();

await ConversationWithCustomHandlers.Run();

//AddTools.Run();
45 changes: 24 additions & 21 deletions OpenAI.ChatGPT.Net.IntegrationTests/SimpleConversationTest.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
using OpenAI.ChatGPT.Net;
using OpenAI.ChatGPT.Net.DataModels;
using OpenAI.ChatGPT.Net.Exeptions;

namespace OpenAI.ChatGPT.Net.IntegrationTests
{
internal class SimpleConversationTest
{
public static async void Run()
public static async Task Run()
{
//GPTModel model = new GPTModel("gpt-4o", "key");
GPTModel model = new("gpt-4o", APIKey.KEY);

//GPTMessage initialMessage = new GPTMessage(GPTRole.User, Console.ReadLine());
//List<GPTMessage> messageHistory = [initialMessage];
Console.Write($"{ChatRole.User}: ");
ChatMessage initialMessage = new(ChatRole.User, Console.ReadLine());
List<ChatMessage> messageHistory = [initialMessage];

//do
//{
// GPTResponse response = await model.Complete(messageHistory);
try
{
while (true)
{
ChatResponse response = await model.Complete(messageHistory);

// if (response is GPTError error)
// {
// Console.WriteLine($"Error: {error.Message}");
// break; // or try to generate it again.
// }
ChatMessage message = (ChatMessage)response;
Console.WriteLine(message);
messageHistory.Add(message);

// GPTMessage message = (GPTMessage)response; // or: GPTMessage messag = response as GPTMessage
// Console.WriteLine(message.Role + ": " + message.Message);
// messageHistory.Add(message);

// GPTMessage nextMessage = new GPTMessage(GPTRole.User, Console.ReadLine());
// messageHistory.Add(nextMessage);
//}
//while (true);
Console.Write($"{ChatRole.User}: ");
ChatMessage nextMessage = new(ChatRole.User, Console.ReadLine());
messageHistory.Add(nextMessage);
}
}
catch (GPTAPIResponseException ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
17 changes: 13 additions & 4 deletions OpenAI.ChatGPT.Net.IntegrationTests/SingleCompletionTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using OpenAI.ChatGPT.Net;
using OpenAI.ChatGPT.Net.DataModels;
using OpenAI.ChatGPT.Net.DataModels;

namespace OpenAI.ChatGPT.Net.IntegrationTests
{
Expand All @@ -22,8 +21,18 @@ public static async Task Run()
return;
}

var message = (ChatMessage)response;
Console.WriteLine(message.Role + ": " + message.Content);
Console.WriteLine((ChatMessage)response);
}

public static async Task TotalMin()
{
string? input = Console.ReadLine();

GPTModel model = new("gpt-4o", APIKey.KEY);
ChatMessage initialMessage = new(ChatRole.User, input);
ChatResponse response = await model.Complete(initialMessage);

Console.WriteLine((ChatMessage)response);
}
}
}
3 changes: 2 additions & 1 deletion OpenAI.ChatGPT.Net/DataModels/ChatChoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace OpenAI.ChatGPT.Net.DataModels
public record ChatChoice(
[property: JsonProperty("message")] ChatMessage Message,
[property: JsonProperty("finish_reason")] string FinishReason,
[property: JsonProperty("index")] long Index
[property: JsonProperty("index")] long Index,
[property: JsonProperty("logprobs")] LogProbs LogProbs
);
}
34 changes: 5 additions & 29 deletions OpenAI.ChatGPT.Net/DataModels/ChatGPTRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public record ChatGPTRequest(
[property: JsonProperty("messages")] List<ChatMessage> Messages,
[property: JsonProperty("model")] string Model,
[property: JsonProperty("frequency_penalty")] double? FrequencyPenalty = null,
[property: JsonProperty("logit_bias")] Dictionary<int, int>? LogitBias = null,
[property: JsonProperty("logprobs")] bool? Logprobs = null,
[property: JsonProperty("top_logprobs")] int? TopLogprobs = null,
[property: JsonProperty("max_tokens")] long? MaxTokens = null,
[property: JsonProperty("n")] long? N = null,
[property: JsonProperty("presence_penalty")] double? PresencePenalty = null,
Expand All @@ -24,6 +27,8 @@ public record ChatGPTRequest(
[property: JsonProperty("tool_choice")] ToolChoice? ToolChoice = null,
[property: JsonProperty("parallel_tool_calls")] bool? ParallelToolCalls = null,
[property: JsonProperty("user")] string? User = null
// service_tier. only mentioned here: // scale_tier? Can't find info about it: https://platform.openai.com/docs/api-reference/chat/object
// Can't find more info
)
{
public override int GetHashCode()
Expand All @@ -34,33 +39,4 @@ public override int GetHashCode()
return HashCode.Combine(one, two);
}
}

public record StreamOptions(
[property: JsonProperty("include_usage")] bool IncludeUsage
);

public record ToolChoice(
[property: JsonProperty("tool_choice")] string? Choice,
[property: JsonProperty("function", DefaultValueHandling = DefaultValueHandling.Ignore)] SimpleTool? Tool
)
{
public ToolChoice() : this(null, null) { }

public ToolChoice(string choice) : this(choice, null)
{
}

public ToolChoice(SimpleTool tool) : this(null, tool)
{
}
}

public record SimpleTool(
[property: JsonProperty("type")] string Type,
[property: JsonProperty("function")] SimpleToolFunction Function
);

public record SimpleToolFunction(
[property: JsonProperty("name")] string Name
);
}
11 changes: 11 additions & 0 deletions OpenAI.ChatGPT.Net/DataModels/ChatMessage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using OpenAI.ChatGPT.Net.Exeptions;

namespace OpenAI.ChatGPT.Net.DataModels
{
Expand All @@ -10,12 +11,22 @@ public record ChatMessage(
{
public static explicit operator ChatMessage(ChatResponse response)
{
if (response.Error != null)
{
throw new GPTAPIResponseException(response.Error.Message);
}

if (response == null || response.Choices == null || response.Choices.Count == 0 || response.Choices[0].Message == null)
{
throw new ArgumentException("Invalid GPTResponse object: Response, choices, or message is null or empty.");
}

return new ChatMessage(ChatRole.Assistant, response.Choices[0].Message.Content);
}

public override string ToString()
{
return $"{Role}: {Content}";
}
}
}
16 changes: 15 additions & 1 deletion OpenAI.ChatGPT.Net/DataModels/ChatResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@ namespace OpenAI.ChatGPT.Net.DataModels
{
public record ChatResponse(
[property: JsonProperty("id")] string Id,
[property: JsonProperty("object")] string Object,
[property: JsonProperty("choices")] List<ChatChoice> Choices,
[property: JsonProperty("created")] long Created,
[property: JsonProperty("model")] string Model,
// scale_tier? Can't find info about it: https://platform.openai.com/docs/api-reference/chat/object
[property: JsonProperty("system_fingerprint")] string SystemFingerprint,
[property: JsonProperty("object")] string Object, // always = chat.completion
[property: JsonProperty("usage")] TokenUsage Usage,
[property: JsonProperty("error")] ChatError? Error
);

// technically I can also just use ChatResponse
public record ChatResponseChunk(
[property: JsonProperty("id")] string Id,
[property: JsonProperty("choices")] List<ChatChoice> Choices,
[property: JsonProperty("created")] long Created,
[property: JsonProperty("model")] string Model,
[property: JsonProperty("system_fingerprint")] string SystemFingerprint,
[property: JsonProperty("object")] string Object, // always = chat.completion.chunk
[property: JsonProperty("usage")] TokenUsage Usage,
[property: JsonProperty("error")] ChatError? Error
);
}
3 changes: 1 addition & 2 deletions OpenAI.ChatGPT.Net/DataModels/ChatRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public enum ChatRole
System,
User,
Assistant,
Tool,
Function
Tool
}
}
82 changes: 15 additions & 67 deletions OpenAI.ChatGPT.Net/DataModels/GPTModel.cs
Original file line number Diff line number Diff line change
@@ -1,79 +1,27 @@
using Newtonsoft.Json;
using System.Text;

namespace OpenAI.ChatGPT.Net.DataModels
namespace OpenAI.ChatGPT.Net.DataModels
{
// https://platform.openai.com/docs/api-reference/chat/create
public class GPTModel(string model, string apiKey)
public partial class GPTModel(string model, string apiKey)
{
public string BaseUrl { get; set; } = "https://api.openai.com/v1/chat/completions";
private readonly string _apiKey = !string.IsNullOrWhiteSpace(apiKey) ? apiKey : throw new ArgumentException("API key cannot be null or empty", nameof(apiKey));

public string Model { get; } = !string.IsNullOrWhiteSpace(model) ? model : throw new ArgumentException("Model cannot be null or empty", nameof(model));
public double FrequencyPenalty { get; set; } = 0.0;
public long MaxTokens { get; set; } = 256;
public long N { get; set; } = 1;
public double PresencePenalty { get; set; } = 0.0;
public string Model { get; set; } = !string.IsNullOrWhiteSpace(model) ? model : throw new ArgumentException("Model cannot be null or empty", nameof(model));
public double? FrequencyPenalty { get; set; } = 0.0;
public Dictionary<int, int>? LogitBias { get; set; } = null;
public bool? Logprobs { get; set; } = null;
public int? TopLogprobs { get; set; } = null;
public long? MaxTokens { get; set; } = 256;
public long? N { get; set; } = 1;
public double? PresencePenalty { get; set; } = 0.0;
public string[]? Stop { get; set; } = null;
public bool Stream { get; set; } = false;
public bool? Stream { get; set; } = false;
public StreamOptions? StreamOptions { get; set; } = null;
public double Temperature { get; set; } = 0.7;
public double TopP { get; set; } = 0.9;
public double? Temperature { get; set; } = 0.7;
public double? TopP { get; set; } = 0.9;
private readonly List<Tool>? tools = null;
private readonly ToolChoice? toolChoice = null;
public bool ParallelToolCalls { get; set; } = true;
public string? User = null;


public async Task<ChatResponse> Complete(ChatMessage initialMessage) => await Complete([initialMessage]);
public async Task<ChatResponse> Complete(List<ChatMessage> chatHistory)
{
var jsonPayload = GenerateJsonPayload(chatHistory);
var jsonResponse = await SendAndReceiveJsonAsync(jsonPayload);
return GenerateResponse(jsonResponse);
}

private static ChatResponse GenerateResponse(string jsonResponse)
{
return JsonConvert.DeserializeObject<ChatResponse>(jsonResponse) ?? throw new JsonSerializationException("Deserialization failed.");
}

private string GenerateJsonPayload(List<ChatMessage> messages)
{
ChatGPTRequest requestBody = new(
Messages: messages,
Model: Model,
FrequencyPenalty: FrequencyPenalty,
MaxTokens: MaxTokens,
N: N,
PresencePenalty: PresencePenalty,
Stop: Stop,
Stream: Stream,
StreamOptions: StreamOptions,
Temperature: Temperature,
TopP: TopP,
Tools: tools,
ToolChoice: toolChoice,
ParallelToolCalls: ParallelToolCalls,
User: User
);

return JsonConvert.SerializeObject(requestBody, Formatting.Indented);
}

private async Task<string> SendAndReceiveJsonAsync(string jsonPayload)
{
Console.WriteLine(jsonPayload);
using var httpClient = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, BaseUrl)
{
Headers = { { "Authorization", $"Bearer {_apiKey}" } },
Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json")
};
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync();
}
public bool? ParallelToolCalls { get; set; } = true;
public string? User { get; set; } = null;
}
}
Loading

0 comments on commit a2d077b

Please sign in to comment.