Skip to content

Commit

Permalink
Merge pull request #3 from andgordio/structured-output-ii
Browse files Browse the repository at this point in the history
README + refactoring
  • Loading branch information
andgordio authored Aug 30, 2024
2 parents b956ac4 + db14b2e commit 1495de3
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 85 deletions.
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This repository contains Swift community-maintained implementation over [OpenAI]
- [Completions Streaming](#completions-streaming)
- [Chats](#chats)
- [Chats Streaming](#chats-streaming)
- [Structured Output](#structured-output)
- [Images](#images)
- [Create Image](#create-image)
- [Create Image Edit](#create-image-edit)
Expand Down Expand Up @@ -381,9 +382,57 @@ Result will be (serialized as JSON here for readability):

```


Review [Chat Documentation](https://platform.openai.com/docs/guides/chat) for more info.

#### Structured Output

JSON is one of the most widely used formats in the world for applications to exchange data.

Structured Outputs is a feature that ensures the model will always generate responses that adhere to your supplied JSON Schema, so you don't need to worry about the model omitting a required key, or hallucinating an invalid enum value.

**Example**

```swift
struct MovieInfo: StructuredOutput {

let title: String
let director: String
let release: Date
let genres: [MovieGenre]
let cast: [String]

static let example: Self = {
.init(
title: "Earth",
director: "Alexander Dovzhenko",
release: Calendar.current.date(year: 1930, month: 4, day: 8)!,
genres: [.drama],
cast: [ "Stepan Shkurat", "Semyon Svashenko", "Yuliya Solntseva" ]
)
}()
}

enum MovieGenre: String, StructuredOutputEnum {
case action, drama, comedy, scifi

var caseNames: [String] { Self.allCases.map { $0.rawValue } }
}

let query = ChatQuery(
messages: [.system(.init(content: message))],
model: .gpt4_o_2024_08_06,
responseFormat: .jsonSchema(name: "movie-info", type: MovieInfo.self)
)
let result = try await openAI.chats(query: query)
```

- Use the `jsonSchema(name:type:)` response format when creating a `ChatQuery`
- Provide a schema name and a type that conforms to `ChatQuery.StructuredOutput` and generates an instance as an example
- Make sure all enum types within the provided type conform to `ChatQuery.StructuredOutputEnum` and generate an array of names for all cases


Review [Structured Output Documentation](https://platform.openai.com/docs/guides/structured-outputs) for more info.

### Images

Given a prompt and/or an input image, the model will generate a new image.
Expand Down
142 changes: 58 additions & 84 deletions Sources/OpenAI/Public/Models/ChatQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -657,12 +657,13 @@ public struct ChatQuery: Equatable, Codable, Streamable {
init(name: String, schema: StructuredOutput) {

func format(_ name: String) -> String {
let underscored = name.replacingOccurrences(of: " ", with: "_")
var formattedName = 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))
let range = NSRange(location: 0, length: formattedName.utf16.count)
formattedName = regex.stringByReplacingMatches(in: formattedName, options: [], range: range, withTemplate: "")
formattedName = formattedName.isEmpty ? "sample" : formattedName
formattedName = String(formattedName.prefix(64))
return formattedName
}

self.name = format(name)
Expand All @@ -677,25 +678,33 @@ public struct ChatQuery: Equatable, Codable, Streamable {
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)
try container.encode(try PropertyValue(from: schema), forKey: .schema)
}
}

private indirect enum PropertyValue: Codable {

case string(isOptional: Bool)
enum SimpleType: String, Codable {
case string, integer, number, boolean
}

enum ComplexType: String, Codable {
case object, array, date
}

enum SpecialType: String, Codable {
case null
}

case simple(SimpleType, isOptional: Bool)
case date(isOptional: Bool)
case integer(isOptional: Bool)
case number(isOptional: Bool)
case boolean(isOptional: Bool)
case `enum`(cases: [String], isOptional: Bool)
case object([String: PropertyValue], isOptional: Bool)
case array(PropertyValue, isOptional: Bool)

enum CodingKeys: String, CodingKey {
case type
case description
case value
case properties
case items
case additionalProperties
Expand All @@ -717,111 +726,66 @@ public struct ChatQuery: Equatable, Codable, Streamable {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case .string(let isOptional):
case .simple(let type, let isOptional):
if isOptional {
try container.encode(["string", "null"], forKey: .type)
try container.encode([type.rawValue, SpecialType.null.rawValue], forKey: .type)
} else {
try container.encode(String("string"), forKey: .type)
try container.encode(type.rawValue, forKey: .type)
}
case .date(let isOptional):
if isOptional {
try container.encode(["string", "null"], forKey: .type)
} else {
try container.encode(String("string"), forKey: .type)
}
try container.encode(String("Date in iso8601 format"), forKey: .description)
case .integer(let isOptional):
if isOptional {
try container.encode(["integer", "null"], forKey: .type)
try container.encode([SimpleType.string.rawValue, SpecialType.null.rawValue], forKey: .type)
} else {
try container.encode(String("integer"), forKey: .type)
}
case .number(let isOptional):
if isOptional {
try container.encode(["number", "null"], forKey: .type)
} else {
try container.encode(String("number"), forKey: .type)
}
case .boolean(let isOptional):
if isOptional {
try container.encode(["boolean", "null"], forKey: .type)
} else {
try container.encode(String("boolean"), forKey: .type)
try container.encode(SimpleType.string.rawValue, forKey: .type)
}
try container.encode("String that represents a date formatted in iso8601", forKey: .description)
case .enum(let cases, let isOptional):
if isOptional {
try container.encode(["string", "null"], forKey: .type)
try container.encode([SimpleType.string.rawValue, SpecialType.null.rawValue], forKey: .type)
} else {
try container.encode(String("string"), forKey: .type)
try container.encode(SimpleType.string.rawValue, forKey: .type)
}
try container.encode(cases, forKey: .enum)
case .object(let object, let isOptional):
if isOptional {
try container.encode(["object", "null"], forKey: .type)
try container.encode([ComplexType.object.rawValue, SpecialType.null.rawValue], forKey: .type)
} else {
try container.encode(String("object"), forKey: .type)
try container.encode(ComplexType.object.rawValue, 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, let isOptional):
if isOptional {
try container.encode(["array", "null"], forKey: .type)
try container.encode([ComplexType.array.rawValue, SpecialType.null.rawValue], forKey: .type)
} else {
try container.encode(String("array"), forKey: .type)
try container.encode(ComplexType.array.rawValue, 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(isOptional: false)
case .date:
let date = try container.decode(Date.self, forKey: .value)
self = .date(isOptional: false)
case .integer:
let integer = try container.decode(Int.self, forKey: .value)
self = .integer(isOptional: false)
case .number:
let double = try container.decode(Double.self, forKey: .value)
self = .number(isOptional: false)
case .boolean:
let bool = try container.decode(Bool.self, forKey: .value)
self = .boolean(isOptional: false)
case .object:
let object = try container.decode([String: PropertyValue].self, forKey: .value)
self = .object(object, isOptional: false)
case .array:
let array = try container.decode(PropertyValue.self, forKey: .value)
self = .array(array, isOptional: false)
}
}

static func generate<T: Any>(from value: T) throws -> PropertyValue {

init<T: Any>(from value: T) throws {
let mirror = Mirror(reflecting: value)
let isOptional = mirror.displayStyle == .optional

switch value {
case _ as String:
return .string(isOptional: isOptional)
self = .simple(.string, isOptional: isOptional)
return
case _ as Bool:
return .boolean(isOptional: isOptional)
case _ as Int, _ as Int8, _ as Int16, _ as Int32, _ as Int64,
_ as UInt, _ as UInt8, _ as UInt16, _ as UInt32, _ as UInt64:
return .integer(isOptional: isOptional)
self = .simple(.boolean, isOptional: isOptional)
return
case _ as Int, _ as Int8, _ as Int16, _ as Int32, _ as Int64, _ as UInt, _ as UInt8, _ as UInt16, _ as UInt32, _ as UInt64:
self = .simple(.integer, isOptional: isOptional)
return
case _ as Double, _ as Float, _ as CGFloat:
return .number(isOptional: isOptional)
self = .simple(.number, isOptional: isOptional)
return
case _ as Date:
return .date(isOptional: isOptional)
self = .date(isOptional: isOptional)
return
default:

var unwrappedMirror: Mirror!
Expand All @@ -841,20 +805,23 @@ public struct ChatQuery: Equatable, Codable, Streamable {
case .struct, .class:
var dict = [String: PropertyValue]()
for child in unwrappedMirror.children {
dict[child.label!] = try generate(from: child.value)
dict[child.label!] = try Self(from: child.value)
}
return .object(dict, isOptional: isOptional)
self = .object(dict, isOptional: isOptional)
return

case .collection:
if let child = unwrappedMirror.children.first {
return .array(try generate(from: child.value), isOptional: isOptional)
self = .array(try Self(from: child.value), isOptional: isOptional)
return
} else {
throw StructuredOutputError.typeUnsupported
}

case .enum:
if let structuredEnum = value as? any StructuredOutputEnum {
return .enum(cases: structuredEnum.caseNames, isOptional: isOptional)
self = .enum(cases: structuredEnum.caseNames, isOptional: isOptional)
return
} else {
throw StructuredOutputError.enumsConformance
}
Expand All @@ -866,6 +833,13 @@ public struct ChatQuery: Equatable, Codable, Streamable {
throw StructuredOutputError.typeUnsupported
}
}


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

public enum StructuredOutputError: LocalizedError {
Expand Down

0 comments on commit 1495de3

Please sign in to comment.