easy_talk 3.3.0 → 3.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f11b7be6e0fa32087d5be57be3d3e37d5bd5b96cc61ae8626b33d41a420e401
4
- data.tar.gz: 82ba98f220e8e75e3bb3e208a595a168f188c226a130e88f13991da9a386cfc6
3
+ metadata.gz: f1b72ef842cb6e49f4cbc0d1eab096e737f26c118e715170c35e791ceb28a0cd
4
+ data.tar.gz: 800ce6db8a8d0607589250fb76a0834bc1372de11617f2746abad2c00496bc77
5
5
  SHA512:
6
- metadata.gz: 90fb93ef486adb8151e883e6929437be2f52bdbe4a51bb45485006884a983b80ce9f10de1d14b313031be4b8126d6fe21c3bd54a998863543a1be1d752c124d9
7
- data.tar.gz: 2ff9102ba57ae6738cb79511fdd095b5f798a760c009b10d2b5577c30575f04d94dfb11e410036e566971d92e27e2c4bc2d17336e44c64f9d35b966bbe1b4960
6
+ metadata.gz: 1cb143e9d16027929bcaf119cdcc12254fd4324f2f902a493be7a55095cfce1f8d721bd44c5233fb5ef62cde19aab3017f6fdc78cb66c57ab670a304293b6fb0
7
+ data.tar.gz: 2968e2ab36843d8237759d9b7528cbe9a72388123b677b8c8117c27192c51d50f577df884a0a96d9cc7e308f2119e2485bcbf0b13bb5742675fb5cd1338ae31a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [3.3.1] - 2026-02-03
2
+
3
+ ### Added
4
+
5
+ - **RubyLLM Compatibility Extension**: Seamless integration between EasyTalk models and RubyLLM's tool and structured output features (#122)
6
+ - New `RubyLLMCompatibility` module adds class method `to_json_schema` for `with_schema` support
7
+ - New `RubyLLMToolOverrides` module for classes inheriting from `RubyLLM::Tool`
8
+ - Overrides `description` and `params_schema` methods to use EasyTalk schema definitions
9
+ - Automatically included when using `EasyTalk::Model`
10
+ - Tools inherit from `RubyLLM::Tool` directly, gaining full access to features like `halt`, `call`, `provider_params`
11
+ - New example files: `examples/ruby_llm/tools_integration.rb`, `examples/ruby_llm/structured_output.rb`
12
+
13
+ ### Changed
14
+
15
+ - **Documentation**: Updated README with improved examples and documentation
16
+
1
17
  ## [3.3.0] - 2026-01-12
2
18
 
3
19
  ### Added
data/README.md CHANGED
@@ -52,13 +52,43 @@ EasyTalk makes the schema definition the single source of truth, so you can:
52
52
  - **RFC 7807** problem details
53
53
  - **JSON:API** error objects
54
54
 
55
- - **LLM tool/function schemas without a second schema layer**
56
- Use the same contract to generate JSON Schema for function/tool calling.
55
+ - **LLM tool/function schemas without a second schema layer**
56
+ Use the same contract to generate JSON Schema for function/tool calling. See [RubyLLM Integration](#rubyllm-integration).
57
57
 
58
58
  EasyTalk is for teams who want their data contracts to be **correct, reusable, and boring** (the good kind of boring).
59
59
 
60
60
  ---
61
61
 
62
+ ## Table of Contents
63
+
64
+ - [Installation](#installation)
65
+ - [Quick start](#quick-start)
66
+ - [Property constraints](#property-constraints)
67
+ - [Core concepts](#core-concepts)
68
+ - [Required vs optional vs nullable](#required-vs-optional-vs-nullable-dont-get-tricked)
69
+ - [Nested models](#nested-models-and-automatic-instantiation)
70
+ - [Tuple arrays](#tuple-arrays-fixed-position-types)
71
+ - [Composition (AnyOf / OneOf / AllOf)](#composition-anyof--oneof--allof)
72
+ - [Validations](#validations)
73
+ - [Automatic validations](#automatic-validations-default)
74
+ - [Per-model validation control](#per-model-validation-control)
75
+ - [Per-property validation control](#per-property-validation-control)
76
+ - [Validation adapters](#validation-adapters)
77
+ - [Error formatting](#error-formatting)
78
+ - [Schema-only mode](#schema-only-mode)
79
+ - [RubyLLM Integration](#rubyllm-integration)
80
+ - [Configuration highlights](#configuration-highlights)
81
+ - [Advanced topics](#advanced-topics)
82
+ - [JSON Schema drafts, `$id`, and `$ref`](#json-schema-drafts-id-and-ref)
83
+ - [Additional properties with types](#additional-properties-with-types)
84
+ - [Object-level constraints](#object-level-constraints)
85
+ - [Custom type builders](#custom-type-builders)
86
+ - [Known limitations](#known-limitations)
87
+ - [Contributing](#contributing)
88
+ - [License](#license)
89
+
90
+ ---
91
+
62
92
  ## Installation
63
93
 
64
94
  ### Requirements
@@ -80,6 +110,14 @@ bundle install
80
110
 
81
111
  ## Quick start
82
112
 
113
+ <table>
114
+ <tr>
115
+ <th>EasyTalk Model</th>
116
+ <th>Generated JSON Schema</th>
117
+ </tr>
118
+ <tr>
119
+ <td>
120
+
83
121
  ```ruby
84
122
  require "easy_talk"
85
123
 
@@ -96,14 +134,10 @@ class User
96
134
  property :age, Integer, minimum: 18
97
135
  end
98
136
  end
99
-
100
- User.json_schema # => Ruby Hash (JSON Schema)
101
- user = User.new(name: "A") # invalid: min_length is 2
102
- user.valid? # => false
103
- user.errors # => ActiveModel::Errors
104
137
  ```
105
138
 
106
- **Generated JSON Schema:**
139
+ </td>
140
+ <td>
107
141
 
108
142
  ```json
109
143
  {
@@ -120,6 +154,17 @@ user.errors # => ActiveModel::Errors
120
154
  }
121
155
  ```
122
156
 
157
+ </td>
158
+ </tr>
159
+ </table>
160
+
161
+ ```ruby
162
+ User.json_schema # => Ruby Hash (JSON Schema)
163
+ user = User.new(name: "A") # invalid: min_length is 2
164
+ user.valid? # => false
165
+ user.errors # => ActiveModel::Errors
166
+ ```
167
+
123
168
  ---
124
169
 
125
170
  ## Property constraints
@@ -240,6 +285,14 @@ end
240
285
 
241
286
  Use `T::Tuple` for arrays where each position has a specific type (e.g., coordinates, CSV rows, database records):
242
287
 
288
+ <table>
289
+ <tr>
290
+ <th>EasyTalk Model</th>
291
+ <th>Generated JSON Schema</th>
292
+ </tr>
293
+ <tr>
294
+ <td>
295
+
243
296
  ```ruby
244
297
  class GeoLocation
245
298
  include EasyTalk::Model
@@ -257,7 +310,8 @@ location = GeoLocation.new(
257
310
  )
258
311
  ```
259
312
 
260
- **Generated JSON Schema:**
313
+ </td>
314
+ <td>
261
315
 
262
316
  ```json
263
317
  {
@@ -273,6 +327,10 @@ location = GeoLocation.new(
273
327
  }
274
328
  ```
275
329
 
330
+ </td>
331
+ </tr>
332
+ </table>
333
+
276
334
  **Mixed-type tuples:**
277
335
 
278
336
  ```ruby
@@ -469,6 +527,66 @@ Use this for documentation, OpenAPI generation, or when validation happens elsew
469
527
 
470
528
  ---
471
529
 
530
+ ## RubyLLM Integration
531
+
532
+ EasyTalk integrates seamlessly with [RubyLLM](https://github.com/crmne/ruby_llm) for structured outputs and tool definitions.
533
+
534
+ ### Structured Outputs
535
+
536
+ Use any EasyTalk model with RubyLLM's `with_schema` to get structured JSON responses:
537
+
538
+ ```ruby
539
+ class Recipe
540
+ include EasyTalk::Model
541
+
542
+ define_schema do
543
+ description "A cooking recipe"
544
+ property :name, String, description: "Name of the dish"
545
+ property :ingredients, T::Array[String], description: "List of ingredients"
546
+ property :prep_time_minutes, Integer, description: "Preparation time in minutes"
547
+ end
548
+ end
549
+
550
+ chat = RubyLLM.chat.with_schema(Recipe)
551
+ response = chat.ask("Give me a simple pasta recipe")
552
+
553
+ # RubyLLM returns parsed JSON - instantiate with EasyTalk model
554
+ recipe = Recipe.new(response.content)
555
+ recipe.name # => "Spaghetti Aglio e Olio"
556
+ recipe.ingredients # => ["spaghetti", "garlic", "olive oil", ...]
557
+ ```
558
+
559
+ ### Tools
560
+
561
+ Create LLM tools by inheriting from `RubyLLM::Tool` and including `EasyTalk::Model`:
562
+
563
+ ```ruby
564
+ class Weather < RubyLLM::Tool
565
+ include EasyTalk::Model
566
+
567
+ define_schema do
568
+ description 'Gets current weather for a location'
569
+ property :latitude, String, description: 'Latitude (e.g., 52.5200)'
570
+ property :longitude, String, description: 'Longitude (e.g., 13.4050)'
571
+ end
572
+
573
+ def execute(latitude:, longitude:)
574
+ # Fetch weather data from API...
575
+ { temperature: 22, conditions: "sunny" }
576
+ end
577
+ end
578
+
579
+ chat = RubyLLM.chat.with_tool(Weather)
580
+ response = chat.ask("What's the weather in Berlin?")
581
+ ```
582
+
583
+ This pattern gives you:
584
+ - Full access to `RubyLLM::Tool` features (`halt`, `call`, etc.)
585
+ - EasyTalk's schema DSL for parameter definitions
586
+ - Automatic JSON Schema generation for the LLM
587
+
588
+ ---
589
+
472
590
  ## Configuration highlights
473
591
 
474
592
  ```ruby
@@ -517,6 +635,14 @@ end
517
635
 
518
636
  Use external URIs in `$ref` for modular, reusable schemas:
519
637
 
638
+ <table>
639
+ <tr>
640
+ <th>EasyTalk Model</th>
641
+ <th>Generated JSON Schema</th>
642
+ </tr>
643
+ <tr>
644
+ <td>
645
+
520
646
  ```ruby
521
647
  EasyTalk.configure do |config|
522
648
  config.use_refs = true
@@ -544,20 +670,34 @@ class Customer
544
670
  end
545
671
 
546
672
  Customer.json_schema
547
- # =>
548
- # {
549
- # "properties": {
550
- # "address": { "$ref": "https://example.com/schemas/address" }
551
- # },
552
- # "$defs": {
553
- # "Address": {
554
- # "$id": "https://example.com/schemas/address",
555
- # "properties": { "street": {...}, "city": {...} }
556
- # }
557
- # }
558
- # }
559
673
  ```
560
674
 
675
+ </td>
676
+ <td>
677
+
678
+ ```json
679
+ {
680
+ "properties": {
681
+ "address": {
682
+ "$ref": "https://example.com/schemas/address"
683
+ }
684
+ },
685
+ "$defs": {
686
+ "Address": {
687
+ "$id": "https://example.com/schemas/address",
688
+ "properties": {
689
+ "street": { "type": "string" },
690
+ "city": { "type": "string" }
691
+ }
692
+ }
693
+ }
694
+ }
695
+ ```
696
+
697
+ </td>
698
+ </tr>
699
+ </table>
700
+
561
701
  **Explicit schema IDs:**
562
702
 
563
703
  ```ruby
@@ -608,6 +748,14 @@ config.as_json
608
748
 
609
749
  **With constraints:**
610
750
 
751
+ <table>
752
+ <tr>
753
+ <th>EasyTalk Model</th>
754
+ <th>Generated JSON Schema</th>
755
+ </tr>
756
+ <tr>
757
+ <td>
758
+
611
759
  ```ruby
612
760
  class StrictConfig
613
761
  include EasyTalk::Model
@@ -615,22 +763,34 @@ class StrictConfig
615
763
  define_schema do
616
764
  property :id, Integer
617
765
  # Integer values between 0 and 100 only
618
- additional_properties Integer, minimum: 0, maximum: 100
766
+ additional_properties Integer,
767
+ minimum: 0, maximum: 100
619
768
  end
620
769
  end
621
770
 
622
771
  StrictConfig.json_schema
623
- # =>
624
- # {
625
- # "properties": { "id": { "type": "integer" } },
626
- # "additionalProperties": {
627
- # "type": "integer",
628
- # "minimum": 0,
629
- # "maximum": 100
630
- # }
631
- # }
632
772
  ```
633
773
 
774
+ </td>
775
+ <td>
776
+
777
+ ```json
778
+ {
779
+ "properties": {
780
+ "id": { "type": "integer" }
781
+ },
782
+ "additionalProperties": {
783
+ "type": "integer",
784
+ "minimum": 0,
785
+ "maximum": 100
786
+ }
787
+ }
788
+ ```
789
+
790
+ </td>
791
+ </tr>
792
+ </table>
793
+
634
794
  **Nested models as additional properties:**
635
795
 
636
796
  ```ruby
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # EasyTalk from the parent directory
6
+ gem 'easy_talk', path: '../..'
7
+
8
+ # RubyLLM for LLM interactions
9
+ gem 'ruby_llm'
10
+
11
+ # HTTP client for API calls (used in tools example)
12
+ gem 'faraday'
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'ruby_llm'
5
+ require 'easy_talk'
6
+
7
+ # Example: Structured Outputs
8
+ # Demonstrates using EasyTalk models to generate structured JSON responses.
9
+
10
+ RubyLLM.configure do |config|
11
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
12
+ end
13
+
14
+ # 1. Define the Schema using EasyTalk
15
+ class Recipe
16
+ include EasyTalk::Model
17
+
18
+ define_schema do
19
+ description "A simple cooking recipe"
20
+ property :name, String, description: "Name of the dish"
21
+ property :ingredients, T::Array[String], description: "List of ingredients"
22
+ property :prep_time_minutes, Integer, description: "Preparation time in minutes"
23
+ property :steps, T::Array[String], description: "Step by step cooking instructions"
24
+ end
25
+ end
26
+
27
+ puts "--- Structured Output Example ---"
28
+
29
+ # 2. Use the EasyTalk model as the output schema
30
+ # RubyLLM uses the schema to force the LLM to reply with a matching JSON structure.
31
+ # Our compatibility layer ensures 'Recipe' responds to to_json_schema as RubyLLM expects.
32
+ chat = RubyLLM.chat.with_schema(Recipe)
33
+
34
+ puts "User: Give me a simple spaghetti carbonara recipe."
35
+ response = chat.ask "Give me a simple spaghetti carbonara recipe."
36
+
37
+ # 3. Access the structured data
38
+ # RubyLLM returns parsed JSON as a Hash, so we instantiate the model with it
39
+ recipe = Recipe.new(response.content)
40
+
41
+ puts "\nGenerated Recipe:"
42
+ puts "Name: #{recipe.name}"
43
+ puts "Time: #{recipe.prep_time_minutes} mins"
44
+ puts "Ingredients:"
45
+ recipe.ingredients.each { |ing| puts "- #{ing}" }
46
+ puts "Steps:"
47
+ recipe.steps.each_with_index { |step, i| puts "#{i + 1}. #{step}" }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'ruby_llm'
5
+ require 'easy_talk'
6
+ require 'faraday'
7
+
8
+ # Example: Tools Integration
9
+ # Demonstrates using EasyTalk models as Tools for RubyLLM.
10
+ #
11
+ # To create a tool, inherit from RubyLLM::Tool and include EasyTalk::Model.
12
+ # This gives you:
13
+ # - Full access to RubyLLM::Tool features like halt()
14
+ # - EasyTalk's schema DSL for defining parameters
15
+ # - Automatic integration with RubyLLM's with_tool method
16
+
17
+ RubyLLM.configure do |config|
18
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
19
+ end
20
+
21
+ class Weather < RubyLLM::Tool
22
+ include EasyTalk::Model
23
+
24
+ define_schema do
25
+ description 'Gets current weather for a location'
26
+ property :latitude, String, description: 'Latitude (e.g., 52.5200)'
27
+ property :longitude, String, description: 'Longitude (e.g., 13.4050)'
28
+ end
29
+
30
+ def execute(latitude:, longitude:)
31
+ puts "Executing Weather Tool for #{latitude}, #{longitude}"
32
+ url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
33
+ response = Faraday.get(url)
34
+ data = JSON.parse(response.body)
35
+ data.to_s
36
+ rescue StandardError => e
37
+ { error: e.message }
38
+ end
39
+ end
40
+
41
+ puts '--- Tools Integration Example ---'
42
+ puts
43
+
44
+ chat = RubyLLM.chat.with_tool(Weather)
45
+
46
+ puts 'User: What is the weather in Berlin (Lat: 52.52, Long: 13.405)?'
47
+ response = chat.ask 'What is the weather in Berlin (Lat: 52.52, Long: 13.405)?'
48
+
49
+ puts "Assistant: #{response.content}"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module Extensions
5
+ # Class methods for RubyLLM compatibility.
6
+ # These are added to the model class via `extend`.
7
+ module RubyLLMCompatibility
8
+ # Returns a Hash representing the schema in a format compatible with RubyLLM.
9
+ # RubyLLM expects an object that responds to #to_json_schema and returns
10
+ # a hash with :name, :description, and :schema keys.
11
+ #
12
+ # @return [Hash] The RubyLLM-compatible schema representation
13
+ def to_json_schema
14
+ {
15
+ name: name,
16
+ description: schema_definition.schema[:description] || "Schema for #{name}",
17
+ schema: json_schema
18
+ }
19
+ end
20
+ end
21
+
22
+ # Overrides for classes that inherit from RubyLLM::Tool.
23
+ # Only overrides schema-related methods, allowing all other RubyLLM::Tool
24
+ # functionality (halt, call, etc.) to work normally.
25
+ #
26
+ # Usage:
27
+ # class WeatherTool < RubyLLM::Tool
28
+ # include EasyTalk::Model
29
+ #
30
+ # define_schema do
31
+ # description 'Gets current weather'
32
+ # property :latitude, String
33
+ # property :longitude, String
34
+ # end
35
+ #
36
+ # def execute(latitude:, longitude:)
37
+ # # Can use halt() since we inherit from RubyLLM::Tool
38
+ # halt "Weather at #{latitude}, #{longitude}"
39
+ # end
40
+ # end
41
+ module RubyLLMToolOverrides
42
+ # Override to use EasyTalk's schema description.
43
+ #
44
+ # @return [String] The tool description from EasyTalk schema
45
+ def description
46
+ schema_def = self.class.schema_definition
47
+ schema_def.schema[:description] || "Tool: #{self.class.name}"
48
+ end
49
+
50
+ # Override to use EasyTalk's JSON schema for parameters.
51
+ #
52
+ # @return [Hash] The JSON schema for parameters
53
+ def params_schema
54
+ self.class.json_schema
55
+ end
56
+ end
57
+ end
58
+ end
@@ -12,6 +12,7 @@ require_relative 'builders/object_builder'
12
12
  require_relative 'schema_definition'
13
13
  require_relative 'validation_builder'
14
14
  require_relative 'error_formatter'
15
+ require_relative 'extensions/ruby_llm_compatibility'
15
16
 
16
17
  module EasyTalk
17
18
  # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
@@ -38,12 +39,18 @@ module EasyTalk
38
39
  module Model
39
40
  def self.included(base)
40
41
  base.extend(ClassMethods)
42
+ base.extend(EasyTalk::Extensions::RubyLLMCompatibility) # Add class-level methods
41
43
 
42
44
  base.include ActiveModel::API
43
45
  base.include ActiveModel::Validations
44
46
  base.extend ActiveModel::Callbacks
45
47
  base.include(InstanceMethods)
46
48
  base.include(ErrorFormatter::InstanceMethods)
49
+
50
+ # If inheriting from RubyLLM::Tool, override schema methods to use EasyTalk's schema
51
+ return unless defined?(RubyLLM::Tool) && base < RubyLLM::Tool
52
+
53
+ base.include(EasyTalk::Extensions::RubyLLMToolOverrides)
47
54
  end
48
55
 
49
56
  # Instance methods mixed into models that include EasyTalk::Model
@@ -150,6 +157,14 @@ module EasyTalk
150
157
  to_hash.merge(@additional_properties)
151
158
  end
152
159
 
160
+ # Returns a Hash representing the schema in a format compatible with RubyLLM.
161
+ # Delegates to the class method. Required for RubyLLM's with_schema method.
162
+ #
163
+ # @return [Hash] The RubyLLM-compatible schema representation
164
+ def to_json_schema
165
+ self.class.to_json_schema
166
+ end
167
+
153
168
  # Allow comparison with hashes
154
169
  def ==(other)
155
170
  case other
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EasyTalk
4
- VERSION = '3.3.0'
4
+ VERSION = '3.3.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy_talk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
@@ -106,6 +106,9 @@ files:
106
106
  - docs/primitive-schema-rfc.md
107
107
  - docs/property-types.markdown
108
108
  - docs/schema-definition.markdown
109
+ - examples/ruby_llm/Gemfile
110
+ - examples/ruby_llm/structured_output.rb
111
+ - examples/ruby_llm/tools_integration.rb
109
112
  - lib/easy_talk.rb
110
113
  - lib/easy_talk/builders/base_builder.rb
111
114
  - lib/easy_talk/builders/boolean_builder.rb
@@ -132,6 +135,7 @@ files:
132
135
  - lib/easy_talk/error_formatter/rfc7807.rb
133
136
  - lib/easy_talk/errors.rb
134
137
  - lib/easy_talk/errors_helper.rb
138
+ - lib/easy_talk/extensions/ruby_llm_compatibility.rb
135
139
  - lib/easy_talk/json_schema_equality.rb
136
140
  - lib/easy_talk/keywords.rb
137
141
  - lib/easy_talk/model.rb