Skip to content

Commit

Permalink
Merge pull request #1 from andgordio/encode-to-dictionary
Browse files Browse the repository at this point in the history
Encode to dictionary
  • Loading branch information
andgordio committed Aug 28, 2024
2 parents 843e087 + 54c7a2e commit d8124ac
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 7 deletions.
215 changes: 208 additions & 7 deletions Sources/OpenAI/Public/Models/ChatQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -599,14 +599,215 @@ public struct ChatQuery: Equatable, Codable, Streamable {
}
}

// See more https://platform.openai.com/docs/guides/text-generation/json-mode
public enum ResponseFormat: String, Codable, Equatable {
case jsonObject = "json_object"
// See more https://platform.openai.com/docs/guides/structured-outputs/introduction
public enum ResponseFormat: Codable, Equatable {

case text

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(["type": self.rawValue])
case jsonObject
case jsonSchema(name: String, type: StructuredOutput.Type)

enum CodingKeys: String, CodingKey {
case type
case jsonSchema = "json_schema"
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text:
try container.encode("text", forKey: .type)
case .jsonObject:
try container.encode("json_object", forKey: .type)
case .jsonSchema(let name, let type):
try container.encode("json_schema", forKey: .type)
let schema = JSONSchema(name: name, schema: type.example)
try container.encode(schema, forKey: .jsonSchema)
}
}

public static func == (lhs: ResponseFormat, rhs: ResponseFormat) -> Bool {
switch (lhs, rhs) {
case (.text, .text): return true
case (.jsonObject, .jsonObject): return true
case (.jsonSchema(let lhsName, let lhsType), .jsonSchema(let rhsName, let rhsType)):
return lhsName == rhsName && lhsType == rhsType
default:
return false
}
}

/// A formal initializer reqluired for the inherited Decodable conformance.
/// This type is never returned from the server and is never decoded into.
public init(from decoder: any Decoder) throws {
self = .text
}
}

private struct JSONSchema: Encodable {

let name: String
let schema: StructuredOutput

enum CodingKeys: String, CodingKey {
case name
case schema
case strict
}

init(name: String, schema: StructuredOutput) {

func format(_ name: String) -> String {
let underscored = name.replacingOccurrences(of: " ", with: "_")
let regex = try! NSRegularExpression(pattern: "[^a-zA-Z0-9_-]", options: [])
let range = NSRange(location: 0, length: underscored.utf16.count)
let sanitized = regex.stringByReplacingMatches(in: underscored, options: [], range: range, withTemplate: "")
let nonempty = sanitized.isEmpty ? "sample" : sanitized
return String(nonempty.prefix(64))
}

self.name = format(name)
self.schema = schema

if self.name != name {
print("The name was changed to \(self.name) to satisfy the API requirements. See more: https://platform.openai.com/docs/api-reference/chat/create")
}
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(true, forKey: .strict)
try container.encode(try PropertyValue.generate(from: schema), forKey: .schema)
}
}

private indirect enum PropertyValue: Codable {

case string(String)
case integer(Int)
case number(Double)
case boolean(Bool)
case object([String: PropertyValue])
case array(PropertyValue)

enum CodingKeys: String, CodingKey {
case type
case value
case properties
case items
case additionalProperties
case required
}

enum ValueType: String, Codable {
case string
case integer
case number
case boolean
case object
case array
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case .string:
try container.encode(String("string"), forKey: .type)
case .integer:
try container.encode(String("integer"), forKey: .type)
case .number:
try container.encode(String("number"), forKey: .type)
case .boolean:
try container.encode(String("boolean"), forKey: .type)
case .object(let object):
try container.encode(String("object"), forKey: .type)
try container.encode(false, forKey: .additionalProperties)
try container.encode(object, forKey: .properties)
let fields = try object.map { key, value in key }
try container.encode(fields, forKey: .required)
case .array(let items):
try container.encode(String("array"), forKey: .type)
try container.encode(items, forKey: .items)

}
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ValueType.self, forKey: .type)

switch type {
case .string:
let string = try container.decode(String.self, forKey: .value)
self = .string(string)
case .integer:
let integer = try container.decode(Int.self, forKey: .value)
self = .integer(integer)
case .number:
let double = try container.decode(Double.self, forKey: .value)
self = .number(double)
case .boolean:
let bool = try container.decode(Bool.self, forKey: .value)
self = .boolean(bool)
case .object:
let object = try container.decode([String: PropertyValue].self, forKey: .value)
self = .object(object)
case .array:
let array = try container.decode(PropertyValue.self, forKey: .value)
self = .array(array)
}
}

static func generate<T: Any>(from value: T) throws -> PropertyValue {
switch value {
case _ as String:
return .string("string")
case _ as Bool:
return .boolean(true)
case _ as Int, _ as Int8, _ as Int16, _ as Int32, _ as Int64,
_ as UInt, _ as UInt8, _ as UInt16, _ as UInt32, _ as UInt64:
return .integer(0)
case _ as Double, _ as Float, _ as CGFloat:
return .integer(0)
default:
let mirror = Mirror(reflecting: value)
if let displayStyle = mirror.displayStyle {
switch displayStyle {
case .struct, .class:
var dict = [String: PropertyValue]()
for child in mirror.children {
dict[child.label!] = try generate(from: child.value)
}
return .object(dict)
case .collection:
if let child = mirror.children.first {
return .array(try generate(from: child.value))
} else {
throw StructuredOutputError.typeUnsupported
}
case .enum:
throw StructuredOutputError.enumsUnsupported
default:
throw StructuredOutputError.typeUnsupported
}
}
throw StructuredOutputError.typeUnsupported
}
}
}

public enum StructuredOutputError: LocalizedError {
case enumsUnsupported
case typeUnsupported

public var errorDescription: String? {
switch self {
case .enumsUnsupported:
return "Enums are not supported at the moment. Consider using one of the basics types and specifying the accepted values in the prompt."
case .typeUnsupported:
return "Unsupported type. Supported types: String, Bool, Int, Double, Array, and Codable struct/class instances."
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/OpenAI/Public/Models/Models/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public extension Model {
// Chat Completion
// GPT-4

/// `gpt-4o-2024-08-06`, latest snapshot that supports Structured Outputs
static let gpt4_o_2024_08_06 = "gpt-4o-2024-08-06"

/// `gpt-4o`, currently the most advanced, multimodal flagship model that's cheaper and faster than GPT-4 Turbo.
static let gpt4_o = "gpt-4o"

Expand Down
12 changes: 12 additions & 0 deletions Sources/OpenAI/Public/Models/StructuredOutput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// StructuredOutput.swift
//
//
// Created by Andriy Gordiyenko on 8/28/24.
//

import Foundation

public protocol StructuredOutput: Codable {
static var example: Self { get }
}

0 comments on commit d8124ac

Please sign in to comment.