Betalgo.Ranul.OpenAI

repository·master·Indexed 25 days ago

https://github.com/betalgo/openai

A community-driven .NET library for accessing OpenAI's API. It provides the OpenAIService for interacting with models like GPT-4o and includes Betalgo.OpenAI.Utilities for context-aware AI applications using EmbeddingTools for RAG (Retrieval Augmented Generation). The library supports manual instantiation and dependency injection in ASP.NET Core.

Tokens
5.3K
Snippets
13
Records
17
Agent score
84%

What's inside Betalgo.Ranul.OpenAI

  1. Document contracts using XML comments

    master

    When documenting contracts, convert OpenAPI description fields into C# XML <summary> tags. Follow these conversion rules:

    • Markdown Links: Convert [Link](url) to <see href="url">Link</see>.
    • Code Formatting: Convert backticks `code` to <c>code</c>.
    • External References: Include a <see href="..."> link to the official OpenAI API reference and a link to the source definition in the GitHub repository.
    /// <summary>
    ///     The format in which the generated images are returned. Must be one of <c>url</c> or <c>b64_json</c>.
    ///     <see href="https://platform.openai.com/docs/api-reference/images">Learn more</see>.
    ///     <see href="https://github.com/betalgo/openai/blob/master/Docs/openapi-split/components/schemas/createimagerequest.yml">
    ///         Source Definition
    ///     </see>
    /// </summary>
  2. Follow naming conventions for Betalgo.Ranul.OpenAI.Contracts

    master

    When creating or updating contracts, adhere to these naming rules to maintain consistency with the OpenAPI specification:

    • Class Names: Must match the YAML Schema name exactly using PascalCase.
    • Property Names (C#): Use PascalCase.
    • Property Names (JSON): Use strict snake_case to match the YAML property, utilizing [JsonPropertyName].
    • Files: Use one class per file, with the filename matching the Class name.
  3. Update OpenAPI Documentation using Blueflow Chopper

    master

    This project uses Blueflow Chopper to split the large OpenAPI specification into smaller files located in Docs/openapi-split. To update these files, you must have the .NET SDK installed.

    1. One-time Setup

    If you have not previously restored the local tools for this repository, run:

    dotnet tool restore

    2. Run the Update Command

    Execute the blueflow-chopper tool with the source URL and output directory. Use the --clean flag to ensure a fresh generation.

    Windows (PowerShell):

    dotnet blueflow-chopper --url "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml" --output "Docs/openapi-split" --clean

    Mac/Linux (Bash):

    dotnet blueflow-chopper --url "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml" --output "Docs/openapi-split" --clean
    dotnet blueflow-chopper --url "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml" --output "Docs/openapi-split" --clean
  4. Use Smart Enums for API Enums

    master

    Do not use standard C# enum. Instead, use the readonly struct "Smart Enum" pattern. This prevents deserialization crashes if the API introduces new string values in the future, as the struct preserves the underlying string value.

    Implementation Template:

    [JsonConverter(typeof(Converter))]
    public readonly struct MyEnum(string value) : IEquatable<MyEnum>
    {
        public static MyEnum Value1 { get; } = new("value1");
        public static MyEnum Value2 { get; } = new("value2");
    
        public string Value { get; } = value;
        
        // ... Boilerplate Equality & Operators ...
        public override string ToString() => Value;
        public bool Equals(MyEnum other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
        public override bool Equals(object? obj) => obj is MyEnum other && Equals(other);
        public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
        public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
        public static bool operator !=(MyEnum left, MyEnum right) => !(left == right);
        public static implicit operator string(MyEnum format) => format.Value;
        public static implicit operator MyEnum(string value) => new(value);
    
        public sealed class Converter : JsonConverter<MyEnum>
        {
            public override MyEnum Read(ref Utf8JsonReader reader, Type t, JsonSerializerOptions o) => new(reader.GetString()!);
            public override void Write(Utf8JsonWriter writer, MyEnum value, JsonSerializerOptions o) => writer.WriteStringValue(value.Value);
        }
    }
  5. Handle Union types (anyOf / oneOf)

    master

    When a field accepts multiple types, follow these patterns:

    Scenario A: String vs Enum (String Union)

    If a field accepts a specific set of strings OR any arbitrary string (e.g., models), use the Smart Enum pattern described in the documentation. It handles both predefined values and unknown strings.

    Scenario B: Different Types (e.g., String vs List)

    If a field accepts different JSON types (e.g., a property that can be a string or an array of strings), you must create a custom class with a custom JsonConverter. Do not use object or dynamic.

    [JsonConverter(typeof(StringOrListConverter))]
    public class StringOrList
    {
        public StringOrList(string? value) { AsString = value; }
        public StringOrList(List<string>? value) { AsList = value; }
    
        public string? AsString { get; }
        public List<string>? AsList { get; }
        
        // Implicit operators for ease of use
        public static implicit operator StringOrList(string value) => new(value);
    }
  6. Use EmbeddingTools for context-aware AI applications

    master

    The EmbeddingTools class allows you to transform raw text into numerical embeddings, which can be used to provide context to models like GPT-4. This is useful for building chatbots that can answer questions based on specific datasets (RAG - Retrieval Augmented Generation).

    Key workflow:

    1. Initialize: Create an IEmbeddingTools instance with an existing SDK instance, a dimension size, and a model (e.g., Models.TextEmbeddingAdaV2).
    2. Process Data: Use ReadFilesAndCreateEmbeddingDataAsCsv to read files from a directory and generate a CSV containing the embeddings.
    3. Load Data: Use LoadEmbeddedDataFromCsv to load previously generated embeddings into a data structure for fast retrieval.
    4. Generate Context: Use CreateContext to find the most relevant information from your embedded data based on a user's question.
    5. Query Model: Pass the generated context into a ChatCompletionCreateRequest as a system message to guide the model's response.
    // Instantiate EmbeddingTools for text embedding tasks.
    IEmbeddingTools embeddingTools = new EmbeddingTools(sdk, 500, Models.TextEmbeddingAdaV2);
    
    // Read files from the provided path and create an embedding data CSV file.
    var dataFrame = await embeddingTools.ReadFilesAndCreateEmbeddingDataAsCsv(Path.Combine("Data", "OpenAI"), "processed/scraped.csv"); 
    
    // Load the embedded data from the CSV file into a DataFrame-like data structure.
    var dataFrame2 = embeddingTools.LoadEmbeddedDataFromCsv("processed/scraped.csv");
    
    // ... inside a user interaction loop ...
    var question = "What is the context-aware question?";
    
    // Create a context for the question using the loaded embedded data.
    var context = embeddingTools.CreateContext(question, dataFrame);
    
    // Pass the context and the user's question to the Gpt_4 model via the sdk's ChatCompletion method.
    var completionResponse = await sdk.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest()
    {
        Model = Models.Gpt_4,
        Messages = new List<ChatMessage>()
        {
            ChatMessage.FromSystem($"Answer the question based on the context below, and if the question can't be answered based on the context, say \"I don't know\".\n\nContext: {context}"),
            ChatMessage.FromUser(question)
        }
    });
    
    Console.WriteLine(completionResponse.Successful ? completionResponse.Choices.First().Message.Content : completionResponse.Error?.Message);
  7. Install Betalgo.Ranul.OpenAI packages

    master

    The library has moved to a new PackageId and Namespace. Use Betalgo.Ranul.OpenAI instead of the legacy Betalgo.OpenAI.

    To install the core library:

    Install-Package Betalgo.Ranul.OpenAI

    To install the experimental utilities library:

    Install-Package Betalgo.OpenAI.Utilities
    Install-Package Betalgo.Ranul.OpenAI
  8. Document Breaking Changes in Changelogs

    master

    When migrating a domain, you must create a domain-specific changelog file (e.g., CHANGELOG_IMAGES.md) in the root of Betalgo.Ranul.OpenAI.Contracts. Use the following structure to help users identify breaking changes:

    • Breaking Changes: List renamed properties (Old Name -> New Name), type changes (e.g., string to Enum), and removed properties.
    • Changes: List new properties and namespace changes.
    ## [Image Domain]
    ### Breaking Changes
    - `ImageCreateRequest.N` -> `CreateImageRequest.N` (Class renamed)
    - `ImageCreateRequest.ImageSize` (enum) -> `CreateImageRequest.Size` (Smart Enum)
    - Removed `User` property (not in current spec).
    
    ### Changes
    - Added `Quality` property.
    - Moved to namespace `Betalgo.Ranul.OpenAI.Contracts.Requests.Image`.
  9. Migrate Object Models to Betalgo.Ranul.OpenAI.Contracts

    master

    When migrating from the legacy OpenAI.SDK to the new Betalgo.Ranul.OpenAI.Contracts library, follow this workflow to ensure a strict, schema-driven contract layer:

    1. Choose a Domain: Select one domain at a time (e.g., "Images", "Chat", "Assistants").
    2. Identify Legacy Models: Locate existing models in OpenAI.SDK/ObjectModels/ (check RequestModels/, ResponseModels/, or SharedModels/).
    3. Locate Source of Truth: Find the corresponding OpenAPI definition in Docs/openapi-split/. Crucial: The new contract MUST be based on the YAML Schema, not the old C# class.
    4. Create the New Contract: Create the file in Betalgo.Ranul.OpenAI.Contracts/ following the project's naming and structure rules.
    5. Handle Divergences: If a legacy model has helper methods/properties not in the YAML, move them to Extension methods or Logic classes. Contracts must remain pure DTOs.
    6. Review and Verify: Ensure file names match class names, required fields are in constructors, and enums are replaced with readonly struct Smart Enums.
    7. Update Managers and Services: Update service interfaces (e.g., IAudioService.cs) and implementations (e.g., OpenAIAudioService.cs) to use the new Contracts, then delete the legacy models.
    8. Update TestHelpers: Update mock implementations in OpenAI.Playground/TestHelpers or OpenAI.UtilitiesPlayground/TestHelpers to reflect breaking changes.
    9. Document Changes: Create a domain-specific changelog (e.g., CHANGELOG_IMAGES.md) documenting Breaking Changes (renames, type changes, removals) and Changes (new properties, namespace moves).
  10. Configure OpenAIService with Dependency Injection

    master

    For ASP.NET Core or other DI-based applications, you can register the service using AddOpenAIService().

    Configuration via secrets.json

    You can configure the service using the OpenAIServiceOptions section in your secrets.json or appsettings.json:

    "OpenAIServiceOptions": {
        "ApiKey": "Your api key goes here",
        "Organization": "Your Organization Id goes here (optional)",
        "UseBeta": "true/false (optional)"
    }

    Registration in Program.cs

    Register the service in your service collection:

    // Option 1: Use default configuration (e.g., from appsettings/secrets)
    serviceCollection.AddOpenAIService();
    
    // Option 2: Configure via code
    serviceCollection.AddOpenAIService(settings => { settings.ApiKey = Environment.GetEnvironmentVariable("MY_OPEN_AI_API_KEY"); });

    Retrieving the service

    Inject IOpenAIService into your classes or retrieve it from the provider:

    var openAiService = serviceProvider.GetRequiredService<IOpenAIService>();
    
    // Optional: Set a default model
    openAiService.SetDefaultModelId(Models.Gpt_4o);
    serviceCollection.AddOpenAIService();
  11. Structure Request and Response contract classes

    master

    Contracts must follow specific inheritance and constructor rules:

    Base Interfaces & Classes

    • Requests: Must implement IRequest.
    • Responses: Must inherit ResponseBase and implement IDefaultResult<T>.

    Mandatory Constructors

    Every contract class must implement at least two constructors:

    1. Empty Constructor: Required for serialization.
    2. Required Parameters Constructor: Accepts all properties marked as required in the YAML specification.

    Shared Properties

    If a property (e.g., model, size, quality) is shared across multiple contracts, define or use an IHas{PropertyName} interface (e.g., IHasImageSize).

    public class CreateImageRequest : IRequest
    {
        // 1. Empty Constructor
        public CreateImageRequest() { }
    
        // 2. Required Parameters Constructor
        public CreateImageRequest(string prompt)
        {
            Prompt = prompt;
        }
    
        [JsonPropertyName("prompt")]
        public string Prompt { get; set; } = null!;
    }