easy_talk_two 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +36 -0
  3. data/CHANGELOG.md +127 -0
  4. data/CONSTRAINTS.md +70 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +1060 -0
  7. data/Rakefile +11 -0
  8. data/docs/.gitignore +5 -0
  9. data/docs/404.html +25 -0
  10. data/docs/Gemfile +38 -0
  11. data/docs/_config.yml +53 -0
  12. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +29 -0
  13. data/docs/about.markdown +18 -0
  14. data/docs/index.markdown +7 -0
  15. data/lib/easy_talk/active_record_schema_builder.rb +299 -0
  16. data/lib/easy_talk/builders/base_builder.rb +65 -0
  17. data/lib/easy_talk/builders/boolean_builder.rb +23 -0
  18. data/lib/easy_talk/builders/collection_helpers.rb +12 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +77 -0
  20. data/lib/easy_talk/builders/integer_builder.rb +28 -0
  21. data/lib/easy_talk/builders/null_builder.rb +16 -0
  22. data/lib/easy_talk/builders/number_builder.rb +27 -0
  23. data/lib/easy_talk/builders/object_builder.rb +180 -0
  24. data/lib/easy_talk/builders/string_builder.rb +27 -0
  25. data/lib/easy_talk/builders/temporal_builder.rb +49 -0
  26. data/lib/easy_talk/builders/typed_array_builder.rb +56 -0
  27. data/lib/easy_talk/builders/union_builder.rb +37 -0
  28. data/lib/easy_talk/configuration.rb +29 -0
  29. data/lib/easy_talk/errors.rb +8 -0
  30. data/lib/easy_talk/errors_helper.rb +147 -0
  31. data/lib/easy_talk/keywords.rb +37 -0
  32. data/lib/easy_talk/model.rb +197 -0
  33. data/lib/easy_talk/property.rb +130 -0
  34. data/lib/easy_talk/schema_definition.rb +78 -0
  35. data/lib/easy_talk/sorbet_extension.rb +15 -0
  36. data/lib/easy_talk/tools/function_builder.rb +40 -0
  37. data/lib/easy_talk/types/base_composer.rb +23 -0
  38. data/lib/easy_talk/types/composer.rb +88 -0
  39. data/lib/easy_talk/version.rb +5 -0
  40. data/lib/easy_talk.rb +29 -0
  41. metadata +265 -0
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+ require 'active_support/time'
7
+ require 'active_support/concern'
8
+ require 'active_support/json'
9
+ require 'active_model'
10
+ require_relative 'builders/object_builder'
11
+ require_relative 'schema_definition'
12
+ require_relative 'active_record_schema_builder'
13
+
14
+ module EasyTalk
15
+ # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
16
+ #
17
+ # It includes methods for defining the schema, retrieving the schema definition,
18
+ # and generating the JSON schema for the model.
19
+ #
20
+ # Example usage:
21
+ #
22
+ # class Person
23
+ # include EasyTalk::Model
24
+ #
25
+ # define_schema do
26
+ # property :name, String, description: 'The person\'s name'
27
+ # property :age, Integer, description: 'The person\'s age'
28
+ # end
29
+ # end
30
+ #
31
+ # Person.json_schema #=> returns the JSON schema for Person
32
+ # jim = Person.new(name: 'Jim', age: 30)
33
+ # jim.valid? #=> returns true
34
+ #
35
+ # @see SchemaDefinition
36
+ module Model
37
+ def self.included(base)
38
+ base.include ActiveModel::API # Include ActiveModel::API in the class including EasyTalk::Model
39
+ base.include ActiveModel::Validations
40
+ base.extend ActiveModel::Callbacks
41
+ base.extend(ClassMethods)
42
+ base.include(InstanceMethods)
43
+
44
+ # Apply ActiveRecord-specific functionality if appropriate
45
+ return unless defined?(ActiveRecord) && base.ancestors.include?(ActiveRecord::Base)
46
+
47
+ base.extend(ActiveRecordClassMethods)
48
+ end
49
+
50
+ module InstanceMethods
51
+ def initialize(attributes = {})
52
+ @additional_properties = {}
53
+ super
54
+ end
55
+
56
+ def method_missing(method_name, *args)
57
+ method_string = method_name.to_s
58
+ if method_string.end_with?('=')
59
+ property_name = method_string.chomp('=')
60
+ if self.class.additional_properties_allowed?
61
+ @additional_properties[property_name] = args.first
62
+ else
63
+ super
64
+ end
65
+ elsif self.class.additional_properties_allowed? && @additional_properties.key?(method_string)
66
+ @additional_properties[method_string]
67
+ else
68
+ super
69
+ end
70
+ end
71
+
72
+ def respond_to_missing?(method_name, include_private = false)
73
+ method_string = method_name.to_s
74
+ method_string.end_with?('=') ? method_string.chomp('=') : method_string
75
+ self.class.additional_properties_allowed? || super
76
+ end
77
+
78
+ # Add to_hash method to convert defined properties to hash
79
+ def to_hash
80
+ return {} unless self.class.properties
81
+
82
+ self.class.properties.each_with_object({}) do |prop, hash|
83
+ hash[prop.to_s] = send(prop)
84
+ end
85
+ end
86
+
87
+ # Override as_json to include both defined and additional properties
88
+ def as_json(_options = {})
89
+ to_hash.merge(@additional_properties)
90
+ end
91
+ end
92
+
93
+ # Module containing class-level methods for defining and accessing the schema of a model.
94
+ module ClassMethods
95
+ # Returns the schema for the model.
96
+ #
97
+ # @return [Schema] The schema for the model.
98
+ def schema
99
+ @schema ||= if defined?(@schema_definition) && @schema_definition
100
+ # Schema defined explicitly via define_schema
101
+ build_schema(@schema_definition)
102
+ elsif respond_to?(:active_record_schema_definition)
103
+ # ActiveRecord model without explicit schema definition
104
+ build_schema(active_record_schema_definition)
105
+ else
106
+ # Default case - empty schema
107
+ {}
108
+ end
109
+ end
110
+
111
+ # Returns the reference template for the model.
112
+ #
113
+ # @return [String] The reference template for the model.
114
+ def ref_template
115
+ "#/$defs/#{name}"
116
+ end
117
+
118
+ def properties
119
+ @properties ||= begin
120
+ return unless schema[:properties].present?
121
+
122
+ schema[:properties].keys.map(&:to_sym)
123
+ end
124
+ end
125
+
126
+ # Returns the JSON schema for the model.
127
+ #
128
+ # @return [Hash] The JSON schema for the model.
129
+ def json_schema
130
+ @json_schema ||= schema.as_json
131
+ end
132
+
133
+ # Define the schema for the model using the provided block.
134
+ #
135
+ # @yield The block to define the schema.
136
+ # @raise [ArgumentError] If the class does not have a name.
137
+ def define_schema(&block)
138
+ raise ArgumentError, 'The class must have a name' unless name.present?
139
+
140
+ @schema_definition = SchemaDefinition.new(name)
141
+ @schema_definition.instance_eval(&block)
142
+ attr_accessor(*properties)
143
+
144
+ @schema_definition
145
+ end
146
+
147
+ # Returns the unvalidated schema definition for the model.
148
+ #
149
+ # @return [SchemaDefinition] The unvalidated schema definition for the model.
150
+ def schema_definition
151
+ @schema_definition ||= {}
152
+ end
153
+
154
+ def additional_properties_allowed?
155
+ @schema_definition&.schema&.fetch(:additional_properties, false)
156
+ end
157
+
158
+ # Builds the schema using the provided schema definition.
159
+ # This is the convergence point for all schema generation.
160
+ #
161
+ # @param schema_definition [SchemaDefinition] The schema definition.
162
+ # @return [Schema] The validated schema.
163
+ def build_schema(schema_definition)
164
+ Builders::ObjectBuilder.new(schema_definition).build
165
+ end
166
+ end
167
+
168
+ # Module containing ActiveRecord-specific methods for schema generation
169
+ module ActiveRecordClassMethods
170
+ # Gets a SchemaDefinition that's built from the ActiveRecord database schema
171
+ #
172
+ # @return [SchemaDefinition] A schema definition built from the database
173
+ def active_record_schema_definition
174
+ @active_record_schema_definition ||= ActiveRecordSchemaBuilder.new(self).build_schema_definition
175
+ end
176
+
177
+ # Store enhancements to be applied to the schema
178
+ #
179
+ # @return [Hash] The schema enhancements
180
+ def schema_enhancements
181
+ @schema_enhancements ||= {}
182
+ end
183
+
184
+ # Enhance the generated schema with additional information
185
+ #
186
+ # @param enhancements [Hash] The schema enhancements
187
+ # @return [void]
188
+ def enhance_schema(enhancements)
189
+ @schema_enhancements = enhancements
190
+ # Clear cached values to force regeneration
191
+ @active_record_schema_definition = nil
192
+ @schema = nil
193
+ @json_schema = nil
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'builders/integer_builder'
5
+ require_relative 'builders/number_builder'
6
+ require_relative 'builders/boolean_builder'
7
+ require_relative 'builders/null_builder'
8
+ require_relative 'builders/string_builder'
9
+ require_relative 'builders/temporal_builder'
10
+ require_relative 'builders/composition_builder'
11
+ require_relative 'builders/typed_array_builder'
12
+ require_relative 'builders/union_builder'
13
+
14
+ # frozen_string_literal: true
15
+
16
+ # EasyTalk module provides classes for building JSON schema properties.
17
+ #
18
+ # This module contains the `Property` class, which is used to build a JSON schema property.
19
+ # It also defines a constant `TYPE_TO_BUILDER` which maps property types to their respective builders.
20
+ #
21
+ # Example usage:
22
+ # property = EasyTalk::Property.new(:name, 'String', minLength: 3, maxLength: 50)
23
+ # property.build
24
+ #
25
+ # @see EasyTalk::Property
26
+ module EasyTalk
27
+ # Property class for building a JSON schema property.
28
+ class Property
29
+ extend T::Sig
30
+ attr_reader :name, :type, :constraints
31
+
32
+ TYPE_TO_BUILDER = {
33
+ 'String' => Builders::StringBuilder,
34
+ 'Integer' => Builders::IntegerBuilder,
35
+ 'Float' => Builders::NumberBuilder,
36
+ 'BigDecimal' => Builders::NumberBuilder,
37
+ 'T::Boolean' => Builders::BooleanBuilder,
38
+ 'TrueClass' => Builders::BooleanBuilder,
39
+ 'NilClass' => Builders::NullBuilder,
40
+ 'Date' => Builders::TemporalBuilder::DateBuilder,
41
+ 'DateTime' => Builders::TemporalBuilder::DatetimeBuilder,
42
+ 'Time' => Builders::TemporalBuilder::TimeBuilder,
43
+ 'anyOf' => Builders::CompositionBuilder::AnyOfBuilder,
44
+ 'allOf' => Builders::CompositionBuilder::AllOfBuilder,
45
+ 'oneOf' => Builders::CompositionBuilder::OneOfBuilder,
46
+ 'T::Types::TypedArray' => Builders::TypedArrayBuilder,
47
+ 'T::Types::Union' => Builders::UnionBuilder
48
+ }.freeze
49
+
50
+ # Initializes a new instance of the Property class.
51
+ # @param name [Symbol] The name of the property.
52
+ # @param type [Object] The type of the property.
53
+ # @param constraints [Hash] The property constraints.
54
+ # @raise [ArgumentError] If the property type is missing.
55
+ sig do
56
+ params(name: Symbol, type: T.any(String, Object),
57
+ constraints: T::Hash[Symbol, T.untyped]).void
58
+ end
59
+ def initialize(name, type = nil, constraints = {})
60
+ @name = name
61
+ @type = type
62
+ @constraints = constraints
63
+ raise ArgumentError, 'property type is missing' if type.blank?
64
+ end
65
+
66
+ # Builds the property based on the specified type, constraints, and builder.
67
+ #
68
+ # If the type responds to the `schema` method, it returns the schema of the type.
69
+ # Otherwise, it returns 'object'.
70
+ #
71
+ # If a builder is specified, it uses the builder to build the property.
72
+ # The arguments passed to the builder depend on whether the builder is a collection type or not.
73
+ #
74
+ # @return [Object] The built property.
75
+ def build
76
+ if nilable_type?
77
+ build_nilable_schema
78
+ elsif builder
79
+ args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
80
+ builder.new(*args).build
81
+ elsif type.respond_to?(:schema)
82
+ # merge the top-level constraints from *this* property
83
+ # e.g. :title, :description, :default, etc
84
+ type.schema.merge!(constraints)
85
+ else
86
+ 'object'
87
+ end
88
+ end
89
+
90
+ # Converts the object to a JSON representation.
91
+ #
92
+ # @param _args [Array] Optional arguments
93
+ # @return [Hash] The JSON representation of the object
94
+ def as_json(*_args)
95
+ build.as_json
96
+ end
97
+
98
+ # Returns the builder associated with the property type.
99
+ #
100
+ # The builder is responsible for constructing the property based on its type.
101
+ # It looks up the builder based on the type's class name or name.
102
+ #
103
+ # @return [Builder] The builder associated with the property type.
104
+ def builder
105
+ @builder ||= TYPE_TO_BUILDER[type.class.name.to_s] || TYPE_TO_BUILDER[type.name.to_s]
106
+ end
107
+
108
+ private
109
+
110
+ def nilable_type?
111
+ return unless type.respond_to?(:types)
112
+ return unless type.types.all? { |t| t.respond_to?(:raw_type) }
113
+
114
+ type.types.any? { |t| t.raw_type == NilClass }
115
+ end
116
+
117
+ def build_nilable_schema
118
+ # Extract the non-nil type from the Union
119
+ actual_type = type.types.find { |t| t != NilClass }
120
+
121
+ # Create a property with the actual type
122
+ non_nil_schema = Property.new(name, actual_type, constraints).build
123
+
124
+ # Merge the types into an array
125
+ non_nil_schema.merge(
126
+ type: [non_nil_schema[:type], 'null']
127
+ )
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'keywords'
4
+ require_relative 'types/composer'
5
+
6
+ module EasyTalk
7
+ #
8
+ #= EasyTalk \SchemaDefinition
9
+ # SchemaDefinition provides the methods for defining a schema within the define_schema block.
10
+ # The @schema is a hash that contains the unvalidated schema definition for the model.
11
+ # A SchemaDefinition instanace is the passed to the Builder.build_schema method to validate and compile the schema.
12
+ class SchemaDefinition
13
+ extend T::Sig
14
+ extend T::AnyOf
15
+ extend T::OneOf
16
+ extend T::AllOf
17
+
18
+ attr_reader :name, :schema
19
+
20
+ def initialize(name, schema = {})
21
+ @schema = schema
22
+ @schema[:additional_properties] = false unless schema.key?(:additional_properties)
23
+ @name = name
24
+ end
25
+
26
+ EasyTalk::KEYWORDS.each do |keyword|
27
+ define_method(keyword) do |*values|
28
+ @schema[keyword] = values.size > 1 ? values : values.first
29
+ end
30
+ end
31
+
32
+ def compose(*subschemas)
33
+ @schema[:subschemas] ||= []
34
+ @schema[:subschemas] += subschemas
35
+ end
36
+
37
+ sig do
38
+ params(name: T.any(Symbol, String), type: T.untyped, constraints: T.untyped, blk: T.nilable(T.proc.void)).void
39
+ end
40
+ def property(name, type, constraints = {}, &blk)
41
+ validate_property_name(name)
42
+ @schema[:properties] ||= {}
43
+
44
+ if block_given?
45
+ raise ArgumentError, 'Block-style sub-schemas are no longer supported. Use class references as types instead.'
46
+ end
47
+
48
+ @schema[:properties][name] = { type:, constraints: }
49
+ end
50
+
51
+ def validate_property_name(name)
52
+ return if name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)
53
+
54
+ raise InvalidPropertyNameError,
55
+ "Invalid property name '#{name}'. Must start with letter/underscore and contain only letters, numbers, underscores"
56
+ end
57
+
58
+ def optional?
59
+ @schema[:optional]
60
+ end
61
+
62
+ # Helper method for nullable and optional properties
63
+ def nullable_optional_property(name, type, constraints = {}, &blk)
64
+ # Ensure type is nilable
65
+ nilable_type = if type.respond_to?(:nilable?) && type.nilable?
66
+ type
67
+ else
68
+ T.nilable(type)
69
+ end
70
+
71
+ # Ensure constraints include optional: true
72
+ constraints = constraints.merge(optional: true)
73
+
74
+ # Call standard property method
75
+ property(name, nilable_type, constraints)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module provides additional functionality for working with Sorbet types.
4
+ module SorbetExtension
5
+ # Checks if the types in the collection include the NilClass type.
6
+ #
7
+ # @return [Boolean] true if the types include NilClass, false otherwise.
8
+ def nilable?
9
+ types.any? do |type|
10
+ type.respond_to?(:raw_type) && type.raw_type == NilClass
11
+ end
12
+ end
13
+ end
14
+
15
+ T::Types::Union.include SorbetExtension
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module Tools
5
+ # FunctionBuilder is a module that builds a hash with the function type and function details.
6
+ # The return value is typically passed as argument to LLM function calling APIs.
7
+ module FunctionBuilder
8
+ class << self
9
+ # Creates a new function object based on the given model.
10
+ #
11
+ # @param [Model] model The EasyTalk model containing the function details.
12
+ # @return [Hash] The function object.
13
+ def new(model)
14
+ {
15
+ type: 'function',
16
+ function: {
17
+ name: generate_function_name(model),
18
+ description: generate_function_description(model),
19
+ parameters: model.json_schema
20
+ }
21
+ }
22
+ end
23
+
24
+ def generate_function_name(model)
25
+ model.schema.fetch(:title, model.name)
26
+ end
27
+
28
+ def generate_function_description(model)
29
+ if model.respond_to?(:instructions)
30
+ raise Instructor::Error, 'The instructions must be a string' unless model.instructions.is_a?(String)
31
+
32
+ model.instructions
33
+ else
34
+ "Correctly extracted `#{model.name}` with all the required parameters and correct types."
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module Types
5
+ # no-doc
6
+ class BaseComposer
7
+ extend T::Sig
8
+ extend T::Generic
9
+
10
+ Elem = type_member
11
+
12
+ sig { returns(T::Array[Elem]) }
13
+ attr_reader :items
14
+
15
+ # Initializes a new instance of the BaseComposer class.
16
+ #
17
+ # @param args [Array] the items to be assigned to the instance variable @items
18
+ def initialize(*args)
19
+ @items = args
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_composer'
4
+
5
+ module EasyTalk
6
+ module Types
7
+ # Base class for composition types
8
+ class Composer < BaseComposer
9
+ # Returns the name of the composition type.
10
+ def self.name
11
+ raise NotImplementedError, "#{self.class.name} must implement the name method"
12
+ end
13
+
14
+ # Returns the name of the composition type.
15
+ def name
16
+ self.class.name
17
+ end
18
+
19
+ # Represents a composition type that allows all of the specified types.
20
+ class AllOf < Composer
21
+ def self.name
22
+ :allOf
23
+ end
24
+
25
+ def name
26
+ :allOf
27
+ end
28
+ end
29
+
30
+ # Represents a composition type that allows any of the specified types.
31
+ class AnyOf < Composer
32
+ def self.name
33
+ :anyOf
34
+ end
35
+
36
+ def name
37
+ :anyOf
38
+ end
39
+ end
40
+
41
+ # Represents a composition type that allows one of the specified types.
42
+ class OneOf < Composer
43
+ def self.name
44
+ :oneOf
45
+ end
46
+
47
+ def name
48
+ :oneOf
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # Shorthand module for accessing the AllOf composer
56
+ module T
57
+ module AllOf
58
+ # Creates a new instance of `EasyTalk::Types::Composer::AllOf` with the given arguments.
59
+ #
60
+ # @param args [Array] the list of arguments to be passed to the constructor
61
+ # @return [EasyTalk::Types::Composer::AllOf] a new instance
62
+ def self.[](*args)
63
+ EasyTalk::Types::Composer::AllOf.new(*args)
64
+ end
65
+ end
66
+
67
+ # Shorthand module for accessing the AnyOf composer
68
+ module AnyOf
69
+ # Creates a new instance of `EasyTalk::Types::Composer::AnyOf` with the given arguments.
70
+ #
71
+ # @param args [Array] the list of arguments to be passed to the constructor
72
+ # @return [EasyTalk::Types::Composer::AnyOf] a new instance
73
+ def self.[](*args)
74
+ EasyTalk::Types::Composer::AnyOf.new(*args)
75
+ end
76
+ end
77
+
78
+ # Shorthand module for accessing the OneOf composer
79
+ module OneOf
80
+ # Creates a new instance of `EasyTalk::Types::Composer::OneOf` with the given arguments.
81
+ #
82
+ # @param args [Array] the list of arguments to be passed to the constructor
83
+ # @return [EasyTalk::Types::Composer::OneOf] a new instance
84
+ def self.[](*args)
85
+ EasyTalk::Types::Composer::OneOf.new(*args)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ VERSION = '1.1.0'
5
+ end
data/lib/easy_talk.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The EasyTalk module is the main namespace for the gem.
4
+ module EasyTalk
5
+ require 'sorbet-runtime'
6
+ require 'easy_talk/sorbet_extension'
7
+ require 'easy_talk/errors'
8
+ require 'easy_talk/errors_helper'
9
+ require 'easy_talk/configuration'
10
+ require 'easy_talk/types/composer'
11
+ require 'easy_talk/model'
12
+ require 'easy_talk/property'
13
+ require 'easy_talk/schema_definition'
14
+ require 'easy_talk/tools/function_builder'
15
+ require 'easy_talk/version'
16
+
17
+ def self.assert_valid_property_options(property_name, options, *valid_keys)
18
+ valid_keys.flatten!
19
+ options.each_key do |k|
20
+ next if valid_keys.include?(k)
21
+
22
+ ErrorHelper.raise_unknown_option_error(property_name: property_name, option: options, valid_options: valid_keys)
23
+ end
24
+ end
25
+
26
+ def self.configure_nilable_behavior(nilable_is_optional = false)
27
+ configuration.nilable_is_optional = nilable_is_optional
28
+ end
29
+ end