|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module OpenAI |
| 4 | + module Helpers |
| 5 | + module StructuredOutput |
| 6 | + # Represents a response from OpenAI's API where the model's output has been structured according to a schema predefined by the user. |
| 7 | + # |
| 8 | + # This class is specifically used when making requests with the `response_format` parameter set to use structured output (e.g., JSON). |
| 9 | + # |
| 10 | + # See {examples/structured_outputs_chat_completions.rb} for a complete example of use |
| 11 | + class BaseModel < OpenAI::Internal::Type::BaseModel |
| 12 | + extend OpenAI::Helpers::StructuredOutput::JsonSchemaConverter |
| 13 | + |
| 14 | + class << self |
| 15 | + # @return [Hash{Symbol=>Object}] |
| 16 | + def to_json_schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema(self) |
| 17 | + |
| 18 | + # @api private |
| 19 | + # |
| 20 | + # @param state [Hash{Symbol=>Object}] |
| 21 | + # |
| 22 | + # @option state [Hash{Object=>String}] :defs |
| 23 | + # |
| 24 | + # @option state [Array<String>] :path |
| 25 | + # |
| 26 | + # @return [Hash{Symbol=>Object}] |
| 27 | + def to_json_schema_inner(state:) |
| 28 | + OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.cache_def!(state, type: self) do |
| 29 | + path = state.fetch(:path) |
| 30 | + properties = fields.to_h do |name, field| |
| 31 | + type, nilable = field.fetch_values(:type, :nilable) |
| 32 | + new_state = {**state, path: [*path, ".#{name}"]} |
| 33 | + |
| 34 | + schema = |
| 35 | + case type |
| 36 | + in {"$ref": String} |
| 37 | + type |
| 38 | + in OpenAI::Helpers::StructuredOutput::JsonSchemaConverter |
| 39 | + type.to_json_schema_inner(state: new_state).update(field.slice(:description)) |
| 40 | + else |
| 41 | + OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner( |
| 42 | + type, |
| 43 | + state: new_state |
| 44 | + ) |
| 45 | + end |
| 46 | + schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(schema) if nilable |
| 47 | + [name, schema] |
| 48 | + end |
| 49 | + |
| 50 | + { |
| 51 | + type: "object", |
| 52 | + properties: properties, |
| 53 | + required: properties.keys.map(&:to_s), |
| 54 | + additionalProperties: false |
| 55 | + } |
| 56 | + end |
| 57 | + end |
| 58 | + end |
| 59 | + |
| 60 | + class << self |
| 61 | + def required(name_sym, type_info, spec = {}) |
| 62 | + super |
| 63 | + |
| 64 | + doc = [type_info, spec].grep(Hash).filter_map { _1[:doc] }.first |
| 65 | + known_fields.fetch(name_sym).update(description: doc) unless doc.nil? |
| 66 | + end |
| 67 | + |
| 68 | + def optional(...) |
| 69 | + # rubocop:disable Layout/LineLength |
| 70 | + message = "`optional` is not supported for structured output APIs, use `#required` with `nil?: true` instead" |
| 71 | + # rubocop:enable Layout/LineLength |
| 72 | + raise RuntimeError.new(message) |
| 73 | + end |
| 74 | + end |
| 75 | + end |
| 76 | + end |
| 77 | + end |
| 78 | +end |
0 commit comments