Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Sources/AnyLanguageModel/DynamicGenerationSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ public struct DynamicGenerationSchema: Sendable {
case number
case integer
case decimal
case null
}

internal let body: Body
internal private(set) var representsNilExplicitly = false
internal var name: String? {
switch body {
case .object(let name, _, _), .anyOf(let name, _, _), .stringEnum(let name, _, _):
Expand All @@ -48,6 +50,26 @@ public struct DynamicGenerationSchema: Sendable {
self.body = .object(name: name, description: description, properties: properties)
}

/// Creates an object schema.
///
/// - Parameters:
/// - name: A name this dynamic schema can be referenced by.
/// - description: A natural language description of this schema.
/// - explicitNil: Whether generated content has a `null` value
/// for each optional property that it would otherwise leave out.
/// Like Foundation Models,
/// the encoded form of a schema built from this one doesn't include this setting.
/// - properties: The properties associated with this schema.
public init(
name: String,
description: String? = nil,
representNilExplicitlyInGeneratedContent explicitNil: Bool,
properties: [DynamicGenerationSchema.Property]
) {
self.body = .object(name: name, description: description, properties: properties)
self.representsNilExplicitly = explicitNil
}

/// Creates an any-of schema.
///
/// - Parameters:
Expand Down Expand Up @@ -115,6 +137,35 @@ public struct DynamicGenerationSchema: Sendable {
}
}

/// A schema that represents a null value.
///
/// Use a null schema to express a value that can't be absent but can be empty.
/// For example, combine it with another schema in an any-of schema:
///
/// ```swift
/// let person = DynamicGenerationSchema(
/// name: "Person",
/// properties: [
/// DynamicGenerationSchema.Property(
/// name: "fullName",
/// schema: DynamicGenerationSchema(type: String.self)
/// )
/// ]
/// )
/// let nullablePerson = DynamicGenerationSchema(
/// name: "NullablePerson",
/// anyOf: [person, .null]
/// )
/// let schema = try GenerationSchema(root: nullablePerson, dependencies: [])
/// ```
public static var null: DynamicGenerationSchema {
DynamicGenerationSchema(body: .scalar(.null))
}

private init(body: Body) {
self.body = body
}

/// Creates an refrence schema.
///
/// - Parameters:
Expand Down
11 changes: 11 additions & 0 deletions Sources/AnyLanguageModel/Generable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public typealias GeneratedContentConversionError = GeneratedContentError
public macro Generable(description: String? = nil) =
#externalMacro(module: "AnyLanguageModelMacros", type: "GenerableMacro")

/// Conforms a type to ``Generable`` protocol,
/// choosing how generated content represents `nil` optional properties.
///
/// By default, generated content leaves out an optional property whose value is `nil`.
/// Pass `true` for `representNilExplicitlyInGeneratedContent`
/// to give such properties a `null` value instead.
@attached(extension, conformances: Generable, names: named(init(_:)), named(generatedContent))
@attached(member, names: arbitrary)
public macro Generable(description: String? = nil, representNilExplicitlyInGeneratedContent: Bool) =
#externalMacro(module: "AnyLanguageModelMacros", type: "GenerableMacro")

/// Allows for influencing the allowed values of properties of a generable type.
@attached(peer)
public macro Guide(description: String) =
Expand Down
161 changes: 158 additions & 3 deletions Sources/AnyLanguageModel/GenerationSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
case string(StringNode)
case number(NumberNode)
case boolean
case null
case anyOf([Node])
case ref(String)

// MARK: - Equatable

static func == (lhs: GenerationSchema.Node, rhs: GenerationSchema.Node) -> Bool {
switch (lhs, rhs) {
case (.boolean, .boolean):
case (.boolean, .boolean), (.null, .null):
return true
case (.ref(let lhsName), .ref(let rhsName)):
return lhsName == rhsName
Expand All @@ -43,6 +44,8 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
case (.object(let lhsObject), .object(let rhsObject)):
return lhsObject.description == rhsObject.description
&& lhsObject.required == rhsObject.required
&& lhsObject.representsNilExplicitly == rhsObject.representsNilExplicitly
&& (!lhsObject.representsNilExplicitly || lhsObject.propertyOrder == rhsObject.propertyOrder)
&& lhsObject.properties.keys == rhsObject.properties.keys
Comment thread
Copilot marked this conversation as resolved.
&& lhsObject.properties.allSatisfy { key, lhsNode in
guard let rhsNode = rhsObject.properties[key] else { return false }
Expand Down Expand Up @@ -131,6 +134,9 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
case .boolean:
try container.encode("boolean", forKey: .type)

case .null:
try container.encode("null", forKey: .type)

case .anyOf(let nodes):
try container.encode(nodes, forKey: .anyOf)

Expand Down Expand Up @@ -209,6 +215,9 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
case "boolean":
self = .boolean

case "null":
self = .null

default:
throw DecodingError.dataCorruptedError(
forKey: .type,
Expand All @@ -223,6 +232,15 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
var description: String?
var properties: [String: Node]
var required: Set<String>
/// Whether generated content has a `null` value for each optional property
/// that it would otherwise leave out.
var representsNilExplicitly = false
/// The property names in declaration order, when known.
var propertyOrder: [String] = []

private enum CodingKeys: String, CodingKey {
case description, properties, required
}
Comment thread
Copilot marked this conversation as resolved.
}

struct ArrayNode: Sendable, Codable {
Expand Down Expand Up @@ -281,6 +299,8 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
return num.integerOnly ? "integer" : "number"
case .boolean:
return "boolean"
case .null:
return "null"
case .anyOf(let nodes):
return "anyOf(\(nodes.count) choices)"
case .ref(let name):
Expand All @@ -298,6 +318,34 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
type: any Generable.Type,
description: String? = nil,
properties: [GenerationSchema.Property]
) {
self.init(type: type, description: description, explicitNil: false, properties: properties)
}

/// Creates a schema by providing an array of properties.
///
/// - Parameters:
/// - type: The type this schema represents.
/// - description: A natural language description of this schema.
/// - explicitNil: Whether generated content has a `null` value
/// for each optional property that it would otherwise leave out.
/// Like Foundation Models,
/// the schema's encoded form doesn't include this setting.
/// - properties: An array of properties.
public init(
type: any Generable.Type,
description: String? = nil,
representNilExplicitlyInGeneratedContent explicitNil: Bool,
properties: [GenerationSchema.Property]
) {
self.init(type: type, description: description, explicitNil: explicitNil, properties: properties)
}

private init(
type: any Generable.Type,
description: String?,
explicitNil: Bool,
properties: [GenerationSchema.Property]
) {
let typeName = String(reflecting: type)
var props: [String: Node] = [:]
Expand All @@ -317,7 +365,13 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
}
}

let objectNode = ObjectNode(description: description, properties: props, required: required)
let objectNode = ObjectNode(
description: description,
properties: props,
required: required,
representsNilExplicitly: explicitNil,
propertyOrder: properties.map(\.name)
)
allDefs[typeName] = .object(objectNode)

self.root = .ref(typeName)
Expand Down Expand Up @@ -481,7 +535,15 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
required.insert(prop.name)
}
}
let node = Node.object(ObjectNode(description: desc, properties: props, required: required))
let node = Node.object(
ObjectNode(
description: desc,
properties: props,
required: required,
representsNilExplicitly: dynamic.representsNilExplicitly,
propertyOrder: properties.map(\.name)
)
)
if let name = name {
defs[name] = node
return .ref(name)
Expand Down Expand Up @@ -521,6 +583,8 @@ public struct GenerationSchema: Equatable, Codable, CustomDebugStringConvertible
switch scalar {
case .bool:
return .boolean
case .null:
return .null
case .string:
return .string(StringNode(description: dynamicProp?.description, pattern: nil, enumChoices: nil))
case .number:
Expand Down Expand Up @@ -883,3 +947,94 @@ extension GenerationSchema {
return "Respond with valid JSON matching this schema:\n\(schemaJSON)"
}
}

// MARK: - Explicit nil

extension GenerationSchema {
/// Returns generated content with a `null` value for each optional property
/// that the content leaves out,
/// in objects whose schema represents `nil` explicitly.
///
/// Content for a schema without such objects is returned unchanged.
func representingNilExplicitly(in content: GeneratedContent) -> GeneratedContent {
let representsNilExplicitly = ([root] + Array(defs.values)).contains { node in
if case .object(let object) = node { return object.representsNilExplicitly }
return false
}
guard representsNilExplicitly else { return content }
return representingNilExplicitly(in: content, node: root, depth: 0)
}

private func representingNilExplicitly(
in content: GeneratedContent,
node: Node,
depth: Int
) -> GeneratedContent {
guard depth < 64 else { return content }
switch node {
case .ref(let name):
guard let resolved = defs[name] else { return content }
return representingNilExplicitly(in: content, node: resolved, depth: depth + 1)
case .object(let object):
guard case .structure(var properties, var orderedKeys) = content.kind else { return content }
for (key, value) in properties {
if let child = object.properties[key] {
properties[key] = representingNilExplicitly(in: value, node: child, depth: depth + 1)
}
}
if object.representsNilExplicitly {
let declaredKeys = object.propertyOrder.isEmpty ? object.properties.keys.sorted() : object.propertyOrder
for key in declaredKeys
where properties[key] == nil && !object.required.contains(key) {
properties[key] = GeneratedContent(kind: .null)
orderedKeys.append(key)
}
// Put declared properties in declaration order, followed by any others.
if !object.propertyOrder.isEmpty {
let declared = Set(object.propertyOrder)
orderedKeys =
object.propertyOrder.filter { properties[$0] != nil }
+ orderedKeys.filter { !declared.contains($0) }
}
}
return GeneratedContent(kind: .structure(properties: properties, orderedKeys: orderedKeys), id: content.id)
case .array(let array):
guard case .array(let elements) = content.kind else { return content }
let items = elements.map { representingNilExplicitly(in: $0, node: array.items, depth: depth + 1) }
return GeneratedContent(kind: .array(items), id: content.id)
case .anyOf(let variants):
guard let variant = variant(matching: content, among: variants, depth: depth) else { return content }
return representingNilExplicitly(in: content, node: variant, depth: depth + 1)
case .string, .number, .boolean, .null:
return content
}
}

/// Returns the first variant whose shape matches the content:
/// an object that declares every property in a structure
/// and whose required properties the structure has,
/// an array for an array,
/// or a nested union with a matching variant.
private func variant(matching content: GeneratedContent, among variants: [Node], depth: Int) -> Node? {
variants.first { variant in
switch (resolving(variant, depth: depth), content.kind) {
case (.object(let object)?, .structure(let properties, _)):
return properties.keys.allSatisfy { object.properties[$0] != nil }
&& object.required.allSatisfy { properties[$0] != nil }
Comment on lines +1021 to +1023
case (.array?, .array):
return true
case (.anyOf(let nested)?, _):
guard depth < 64 else { return false }
return self.variant(matching: content, among: nested, depth: depth + 1) != nil
default:
return false
}
}
}

private func resolving(_ node: Node, depth: Int) -> Node? {
guard depth < 64 else { return nil }
guard case .ref(let name) = node else { return node }
return defs[name].flatMap { resolving($0, depth: depth + 1) }
}
}
Loading
Loading