easy_talk 3.3.0 → 3.3.2
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 +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +191 -31
- data/easy_talk.gemspec +42 -0
- data/examples/ruby_llm/Gemfile +12 -0
- data/examples/ruby_llm/structured_output.rb +47 -0
- data/examples/ruby_llm/tools_integration.rb +49 -0
- data/lib/easy_talk/builders/composition_builder.rb +3 -0
- data/lib/easy_talk/builders/null_builder.rb +5 -3
- data/lib/easy_talk/builders/object_builder.rb +5 -31
- data/lib/easy_talk/builders/union_builder.rb +3 -0
- data/lib/easy_talk/errors_helper.rb +4 -4
- data/lib/easy_talk/extensions/ruby_llm_compatibility.rb +58 -0
- data/lib/easy_talk/model.rb +33 -167
- data/lib/easy_talk/property.rb +2 -3
- data/lib/easy_talk/schema.rb +19 -129
- data/lib/easy_talk/schema_base.rb +181 -0
- data/lib/easy_talk/schema_definition.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +9 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +37 -37
- data/lib/easy_talk/validation_adapters/base.rb +7 -39
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +23 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b571b59c30a196b82285a7fa67c275002cb9f4bc6a07a0e0c70cc69d04d60b50
|
|
4
|
+
data.tar.gz: 599c489167248facc490dbc3add9c0fddb005b9aab654d919e0377a1ae2352af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f27233da948ebf61071d179f8b8b1031df4ffa262bd9050488aa09e82e462d8c8a52b25290b8af1965c7c23f39c73a43c8b95972ac3d388aea645c50d7754c9
|
|
7
|
+
data.tar.gz: 8cba0f544c9746674f15005232c714d385a3e407c3add6d4089eb2260a1a6889a0e7a0160283d42fdc388f91c26ac96c52d8fae8147458821cd346bde2c60651
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## [3.3.2] - 2026-03-09
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Schema Cache Corruption**: Fixed schema cache corruption when nested models have constraints (#163)
|
|
6
|
+
- **Mutable Default Values (Model)**: Fixed mutable default values (arrays, hashes) being shared across `EasyTalk::Model` instances (#165)
|
|
7
|
+
- **Nilable Properties Rejecting Nil**: Fixed nilable properties incorrectly rejecting `nil` in ActiveModel validations (#166)
|
|
8
|
+
- **Nested Model Blank False Positive**: Fixed nested model blank false positive in `apply_object_validations` (#167)
|
|
9
|
+
- **Nilable Boolean Validation**: Fixed `T.nilable(T::Boolean)` rejecting `nil` in ActiveModel validations (#168)
|
|
10
|
+
- **Schema False Value Discard**: Fixed `EasyTalk::Schema` silently discarding `false` values on initialization (#169)
|
|
11
|
+
- **Mutable Default Values (Schema)**: Fixed mutable default values shared across `EasyTalk::Schema` instances (#170)
|
|
12
|
+
- **Double-Call Stale Schema**: Fixed `define_schema` double-call leaving stale schema and validators (#171)
|
|
13
|
+
- **Incorrect Fallback String**: Fixed incorrect fallback string in type introspection for composition types (#175)
|
|
14
|
+
- **Optional Array Nil Rejection**: Fixed optional array properties incorrectly rejecting `nil` (#177)
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Option Validation for UnionBuilder and NullBuilder**: Added constraint option validation to `UnionBuilder` and `NullBuilder`, raising errors on unsupported options (#176)
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
|
|
22
|
+
- **SchemaBase Extraction**: Extracted `SchemaBase` module to eliminate duplication between `Model` and `Schema` (#172)
|
|
23
|
+
- **Type Introspection Consolidation**: Consolidated type introspection logic into the `TypeIntrospection` module, removing duplicate checks from adapters and helpers (#173, #174)
|
|
24
|
+
- **Test Suite Consolidation**: Consolidated standalone bug-fix specs into the regular test suite for better organization
|
|
25
|
+
|
|
26
|
+
## [3.3.1] - 2026-02-03
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **RubyLLM Compatibility Extension**: Seamless integration between EasyTalk models and RubyLLM's tool and structured output features (#122)
|
|
31
|
+
- New `RubyLLMCompatibility` module adds class method `to_json_schema` for `with_schema` support
|
|
32
|
+
- New `RubyLLMToolOverrides` module for classes inheriting from `RubyLLM::Tool`
|
|
33
|
+
- Overrides `description` and `params_schema` methods to use EasyTalk schema definitions
|
|
34
|
+
- Automatically included when using `EasyTalk::Model`
|
|
35
|
+
- Tools inherit from `RubyLLM::Tool` directly, gaining full access to features like `halt`, `call`, `provider_params`
|
|
36
|
+
- New example files: `examples/ruby_llm/tools_integration.rb`, `examples/ruby_llm/structured_output.rb`
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **Documentation**: Updated README with improved examples and documentation
|
|
41
|
+
|
|
1
42
|
## [3.3.0] - 2026-01-12
|
|
2
43
|
|
|
3
44
|
### 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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
data/easy_talk.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/easy_talk/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'easy_talk'
|
|
7
|
+
spec.version = EasyTalk::VERSION
|
|
8
|
+
spec.authors = ['Sergio Bayona']
|
|
9
|
+
spec.email = ['bayona.sergio@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'JSON Schema generation and validation for Ruby classes, ideal for LLM function calling.'
|
|
12
|
+
spec.description = 'Define schemas using a clean DSL and get both JSON Schema documents and runtime validations. ' \
|
|
13
|
+
'Perfect for API request/response validation, LLM function definitions (OpenAI, Anthropic), ' \
|
|
14
|
+
'and structured data modeling. Features Sorbet-style types, schema composition, ' \
|
|
15
|
+
'pluggable validation adapters, and multiple error output formats (JSON:API, RFC 7807).'
|
|
16
|
+
spec.homepage = 'https://github.com/sergiobayona/easy_talk'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
spec.required_ruby_version = '>= 3.2'
|
|
19
|
+
|
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
21
|
+
|
|
22
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
23
|
+
spec.metadata['changelog_uri'] = 'https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md'
|
|
24
|
+
|
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
|
26
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
27
|
+
spec.files = Dir.chdir(__dir__) do
|
|
28
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
29
|
+
(File.expand_path(f) == __FILE__) ||
|
|
30
|
+
f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
spec.require_paths = ['lib']
|
|
35
|
+
|
|
36
|
+
spec.add_dependency 'activemodel', ['>= 7.0', '< 9.0']
|
|
37
|
+
spec.add_dependency 'activesupport', ['>= 7.0', '< 9.0']
|
|
38
|
+
spec.add_dependency 'js_regex', '~> 3.0'
|
|
39
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
|
40
|
+
|
|
41
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
42
|
+
end
|
|
@@ -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}¤t=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}"
|
|
@@ -17,6 +17,8 @@ module EasyTalk
|
|
|
17
17
|
'OneOfBuilder' => 'oneOf'
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
|
+
VALID_OPTIONS = %i[title description optional as validate ref].freeze
|
|
21
|
+
|
|
20
22
|
sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
21
23
|
# Initializes a new instance of the CompositionBuilder class.
|
|
22
24
|
#
|
|
@@ -24,6 +26,7 @@ module EasyTalk
|
|
|
24
26
|
# @param type [Class] The type of the composition.
|
|
25
27
|
# @param constraints [Hash] The constraints for the composition.
|
|
26
28
|
def initialize(name, type, constraints)
|
|
29
|
+
EasyTalk.assert_valid_property_options(name, constraints, VALID_OPTIONS)
|
|
27
30
|
@composer_type = self.class.name.split('::').last
|
|
28
31
|
@name = name
|
|
29
32
|
@type = type
|
|
@@ -9,10 +9,12 @@ module EasyTalk
|
|
|
9
9
|
class NullBuilder < BaseBuilder
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
+
VALID_OPTIONS = {}.freeze
|
|
13
|
+
|
|
12
14
|
# Initializes a new instance of the NullBuilder class.
|
|
13
|
-
sig { params(name: Symbol,
|
|
14
|
-
def initialize(name,
|
|
15
|
-
super(name, { type: 'null' },
|
|
15
|
+
sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
16
|
+
def initialize(name, constraints = {})
|
|
17
|
+
super(name, { type: 'null' }, constraints, VALID_OPTIONS)
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
end
|
|
@@ -41,7 +41,7 @@ module EasyTalk
|
|
|
41
41
|
# Keep a reference to the original schema definition
|
|
42
42
|
@schema_definition = schema_definition
|
|
43
43
|
# Deep duplicate the raw schema hash so we can mutate it safely
|
|
44
|
-
@original_schema = deep_dup(schema_definition.schema)
|
|
44
|
+
@original_schema = EasyTalk.deep_dup(schema_definition.schema)
|
|
45
45
|
|
|
46
46
|
# We'll collect required property names in this Set
|
|
47
47
|
@required_properties = Set.new
|
|
@@ -71,24 +71,6 @@ module EasyTalk
|
|
|
71
71
|
|
|
72
72
|
private
|
|
73
73
|
|
|
74
|
-
##
|
|
75
|
-
# Deep duplicates a hash, including nested hashes.
|
|
76
|
-
# This prevents mutations from leaking back to the original schema.
|
|
77
|
-
#
|
|
78
|
-
def deep_dup(obj)
|
|
79
|
-
case obj
|
|
80
|
-
when Hash
|
|
81
|
-
obj.transform_values { |v| deep_dup(v) }
|
|
82
|
-
when Array
|
|
83
|
-
obj.map { |v| deep_dup(v) }
|
|
84
|
-
when Class, Module
|
|
85
|
-
# Don't duplicate Class or Module objects - they represent types
|
|
86
|
-
obj
|
|
87
|
-
else
|
|
88
|
-
obj.duplicable? ? obj.dup : obj
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
74
|
##
|
|
93
75
|
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
|
94
76
|
# into a single hash that we'll feed to BaseBuilder.
|
|
@@ -250,7 +232,7 @@ module EasyTalk
|
|
|
250
232
|
elsif prop_type.is_a?(EasyTalk::Types::Composer)
|
|
251
233
|
collect_ref_models(prop_type.items, constraints)
|
|
252
234
|
# Handle typed arrays with EasyTalk model items
|
|
253
|
-
elsif typed_array?(prop_type)
|
|
235
|
+
elsif TypeIntrospection.typed_array?(prop_type)
|
|
254
236
|
extract_inner_types(prop_type).each { |inner_type| collect_ref_models(inner_type, constraints) }
|
|
255
237
|
# Handle nilable types
|
|
256
238
|
elsif nilable_with_model?(prop_type)
|
|
@@ -272,12 +254,8 @@ module EasyTalk
|
|
|
272
254
|
EasyTalk.configuration.use_refs
|
|
273
255
|
end
|
|
274
256
|
|
|
275
|
-
def typed_array?(prop_type)
|
|
276
|
-
prop_type.is_a?(T::Types::TypedArray)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
257
|
def extract_inner_types(prop_type)
|
|
280
|
-
return [] unless typed_array?(prop_type)
|
|
258
|
+
return [] unless TypeIntrospection.typed_array?(prop_type)
|
|
281
259
|
|
|
282
260
|
if prop_type.type.is_a?(EasyTalk::Types::Composer)
|
|
283
261
|
prop_type.type.items
|
|
@@ -302,9 +280,7 @@ module EasyTalk
|
|
|
302
280
|
# Adds $defs entries for all collected ref models.
|
|
303
281
|
#
|
|
304
282
|
def add_ref_model_defs(schema_hash)
|
|
305
|
-
definitions = @ref_models.
|
|
306
|
-
acc[model.name] = model.schema
|
|
307
|
-
end
|
|
283
|
+
definitions = @ref_models.to_h { |model| [model.name, EasyTalk.deep_dup(model.schema)] }
|
|
308
284
|
|
|
309
285
|
existing_defs = schema_hash[:defs] || {}
|
|
310
286
|
schema_hash[:defs] = existing_defs.merge(definitions)
|
|
@@ -327,9 +303,7 @@ module EasyTalk
|
|
|
327
303
|
#
|
|
328
304
|
def add_defs_from_subschema(schema_hash, subschema)
|
|
329
305
|
# Build up a hash of class_name => schema for each sub-item
|
|
330
|
-
definitions = subschema.items.
|
|
331
|
-
acc[item.name] = item.schema
|
|
332
|
-
end
|
|
306
|
+
definitions = subschema.items.to_h { |item| [item.name, EasyTalk.deep_dup(item.schema)] }
|
|
333
307
|
# Merge or create :defs
|
|
334
308
|
existing_defs = schema_hash[:defs] || {}
|
|
335
309
|
schema_hash[:defs] = existing_defs.merge(definitions)
|
|
@@ -10,8 +10,11 @@ module EasyTalk
|
|
|
10
10
|
extend CollectionHelpers
|
|
11
11
|
extend T::Sig
|
|
12
12
|
|
|
13
|
+
VALID_OPTIONS = %i[title description optional as validate].freeze
|
|
14
|
+
|
|
13
15
|
sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
|
|
14
16
|
def initialize(name, type, constraints)
|
|
17
|
+
EasyTalk.assert_valid_property_options(name, constraints, VALID_OPTIONS)
|
|
15
18
|
@name = name
|
|
16
19
|
@type = type
|
|
17
20
|
@constraints = constraints
|
|
@@ -25,7 +25,7 @@ module EasyTalk
|
|
|
25
25
|
raise UnknownOptionError, message
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def self.
|
|
28
|
+
def self.extract_element_type(type_info)
|
|
29
29
|
# No change needed here
|
|
30
30
|
if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
|
|
31
31
|
type_info.type.raw_type
|
|
@@ -49,7 +49,7 @@ module EasyTalk
|
|
|
49
49
|
def self.validate_typed_array_values(property_name:, constraint_name:, type_info:, array_value:)
|
|
50
50
|
# Raise error if value is not an array but type expects one
|
|
51
51
|
unless array_value.is_a?(Array)
|
|
52
|
-
inner_type =
|
|
52
|
+
inner_type = extract_element_type(type_info)
|
|
53
53
|
expected_desc = TypeIntrospection.boolean_type?(inner_type) ? 'Boolean (true or false)' : inner_type.to_s
|
|
54
54
|
raise_constraint_error(
|
|
55
55
|
property_name: property_name,
|
|
@@ -59,7 +59,7 @@ module EasyTalk
|
|
|
59
59
|
)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
inner_type =
|
|
62
|
+
inner_type = extract_element_type(type_info)
|
|
63
63
|
array_value.each_with_index do |element, index|
|
|
64
64
|
validate_array_element(
|
|
65
65
|
property_name: property_name,
|
|
@@ -153,7 +153,7 @@ module EasyTalk
|
|
|
153
153
|
# Handle Sorbet type objects
|
|
154
154
|
elsif value_type.class.ancestors.include?(T::Types::Base)
|
|
155
155
|
# Extract the inner type
|
|
156
|
-
inner_type =
|
|
156
|
+
inner_type = extract_element_type(value_type)
|
|
157
157
|
|
|
158
158
|
if inner_type.is_a?(Array)
|
|
159
159
|
# For union types, check if the value matches any of the allowed types
|