action_ai 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/README.rdoc +149 -19
- data/lib/action_ai/agent.rb +54 -58
- data/lib/action_ai/model_schema/active_model.rb +123 -0
- data/lib/action_ai/model_schema/active_record.rb +151 -0
- data/lib/action_ai/model_schema.rb +195 -0
- data/lib/action_ai/railtie.rb +8 -1
- data/lib/action_ai/refinements/numeric.rb +32 -0
- data/lib/action_ai/refinements/regexp.rb +13 -0
- data/lib/action_ai/schema.rb +20 -0
- data/lib/action_ai/structured_output.rb +85 -0
- data/lib/action_ai/version.rb +1 -1
- data/lib/action_ai.rb +5 -0
- data/lib/rails/generators/ai/templates/agent.rb.tt +4 -0
- data/lib/ruby_llm/protocols/echo.rb +26 -0
- data/lib/ruby_llm/providers/test.rb +5 -21
- data/lib/ruby_llm/tester.rb +7 -3
- metadata +27 -4
- data/lib/ruby_llm/providers/test/echo.rb +0 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fa6d979d2013779ecfe3932a2c1bb04158dd99072ef5eb4b802f2ece910b34d
|
|
4
|
+
data.tar.gz: 46cb4d1ad9cca9f63c7af8d575abe3ad23fef1359d2f5d445617662e071b1ef6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0de59f1a428c9be0cd4e1926a8f1d59f1cca46c8fbdc77248fe34ab21c755412c39db3bfabcaa86855be878e707cfdb61e2d74a983c7ae2625de50eed3b96184
|
|
7
|
+
data.tar.gz: 4e77dc58ce51dc7a8d9027cabdab9465b82b3e9abccce7bbfb3591f8fd12cd36dbd4f28953118ec41ec3bc9f64b0569045d3e191c362ba8dbdcb9decaa9312e6
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
## [0.4.0] - 2026-10-20
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Moved agent-level configuration to RubyLLM agents.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
#### Configuration
|
|
15
|
+
|
|
16
|
+
- RubyLLM agent configuration DSL support on `ActionAI::Agent`: class methods `model`, `instructions`, `tools`, `tool_options`, `server_tools`, `thinking`, `citations`, `caching`, `compaction`, `provider_options`, `headers`, `schema`, `fallbacks`, `context`, `chat_model`, `inputs`, `temperature`, `max_output_tokens`, and `end_user` configure a private, internally managed `RubyLLM::Agent` subclass.
|
|
17
|
+
`chat` is built via that agent’s `.chat` instead of plain `RubyLLM.chat`.
|
|
18
|
+
- Parameters set through `ActionAI::Parameterized` are forwarded as the agent’s runtime `inputs`.
|
|
19
|
+
|
|
20
|
+
#### Schemas
|
|
21
|
+
|
|
22
|
+
- Consider Active Record _stored attributes_ for schema inference.
|
|
23
|
+
The type is `string` because `ActiveRecord::Store` doesn’t provide type casting. The same is true for the attributes a model will get from a user-filled form, so the behavior is consistent. One should override attribute setters for type casting if needed.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
#### Configuration
|
|
28
|
+
|
|
29
|
+
- `default`/`default_options=` class method and `default_params` class attribute.
|
|
30
|
+
Use the RubyLLM agent DSL (`model`, `instructions`, and so on) instead.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## [0.3.0] - 2026-10-13
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Moved to RubyLLM 2.
|
|
38
|
+
- Dropped `echo` test model in favor of `echo` protocol.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
#### Structured output
|
|
43
|
+
|
|
44
|
+
- `.schema` class method on any `ActiveModel::API` model, returning a cached `Schematist::Schema` instance derived from the model’s attribute types and constraints.
|
|
45
|
+
- Include Active Record associations as nested schemas via objects/arrays.
|
|
46
|
+
Consider associations registered for _nested attributes_ only.
|
|
47
|
+
- Prefer custom schemas over inferred ones.
|
|
48
|
+
Define them as `<ModelName>Schema < ActionAI::Schema` classes under `app/schemas`.
|
|
49
|
+
- `#returns(ModelClass)` instance method in agent actions: configures structured output via `chat.with_schema` and decorates the resulting `RubyLLM::Message` with `#object` returning a model instance.
|
|
50
|
+
- Use Strong Parameters to decorate returned JSON for security reasons.
|
|
51
|
+
AI model’s output shouldn’t be considered a trusted input.
|
|
52
|
+
- `#returns([ModelClass])` array syntax: configures an array schema and `#object` returns an array of model instances.
|
|
53
|
+
|
|
54
|
+
|
|
5
55
|
## [0.2.0] - 2026-06-30
|
|
6
56
|
|
|
7
57
|
### Added
|
|
@@ -13,6 +63,7 @@ All notable changes to this project will be documented in this file.
|
|
|
13
63
|
|
|
14
64
|
- `echo` test model to support multiple chat interactions.
|
|
15
65
|
|
|
66
|
+
|
|
16
67
|
## [0.1.0] - 2026-05-05
|
|
17
68
|
|
|
18
69
|
Refactored from Action Mailer.
|
data/README.rdoc
CHANGED
|
@@ -22,7 +22,7 @@ automatically at the end of the action unless you have already called it.
|
|
|
22
22
|
This can be as simple as:
|
|
23
23
|
|
|
24
24
|
class Generator < ApplicationAI
|
|
25
|
-
|
|
25
|
+
model "gpt-6.1-sol"
|
|
26
26
|
|
|
27
27
|
def code(task, language)
|
|
28
28
|
@task = task
|
|
@@ -88,33 +88,163 @@ You can also chain multiple agent actions to compose a small workflow before rea
|
|
|
88
88
|
WorkflowAgent.collect("release notes").refine(style: "bullet list").content
|
|
89
89
|
# => collected release notes refined as bullet list
|
|
90
90
|
|
|
91
|
-
==
|
|
91
|
+
== Configuration
|
|
92
|
+
|
|
93
|
+
Your agent class uses the full +RubyLLM::Agent+ configuration DSL
|
|
94
|
+
directly. You never need to define or reference a +RubyLLM::Agent+
|
|
95
|
+
subclass yourself; just call +model+, +instructions+, +tools+, +schema+, and
|
|
96
|
+
the other +RubyLLM::Agent+ class macros right on your Action AI agent:
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
supported by +RubyLLM::Chat+, such as +:provider+ and +:model+.
|
|
98
|
-
Finally, it is also possible to pass in a
|
|
99
|
-
Proc that will get evaluated when it is needed.
|
|
98
|
+
class Generator < ApplicationAI
|
|
99
|
+
model "gpt-6.1-sol"
|
|
100
|
+
instructions "You are a concise support assistant."
|
|
101
|
+
tools SearchDocs, LookupAccount
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
def code(task, language)
|
|
104
|
+
@task = task
|
|
105
|
+
@language = language
|
|
106
|
+
end
|
|
107
|
+
end
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
If you declare +inputs+, use ActionAI::Parameterized to supply them at call
|
|
110
|
+
time; the parameters given to +with+ are forwarded as the agent's runtime
|
|
111
|
+
inputs:
|
|
105
112
|
|
|
106
113
|
class Generator < ApplicationAI
|
|
107
|
-
|
|
114
|
+
inputs :workspace
|
|
115
|
+
|
|
116
|
+
instructions { "You are helping #{workspace.name}" }
|
|
108
117
|
end
|
|
109
118
|
|
|
110
|
-
|
|
119
|
+
Generator.with(workspace:).code("Parse CSV", :ruby)
|
|
120
|
+
|
|
121
|
+
Subclasses inherit their parent's RubyLLM agent configuration and can extend
|
|
122
|
+
or override it, the same way +RubyLLM::Agent+ subclasses do:
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
class ApplicationAI < ActionAI::Agent
|
|
125
|
+
model "gpt-6-luna", provider: :openai
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class Generator < ApplicationAI
|
|
129
|
+
model { Current.user.preferred_model }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
The Agent class has a few settings unrelated to the RubyLLM agent DSL that are specified on the class level.
|
|
133
|
+
Here's an example:
|
|
134
|
+
|
|
135
|
+
ActionAI::Agent.execute_later_queue_name = :ai_agents
|
|
136
|
+
|
|
137
|
+
* +logger+ — the logger is used for generating information on prompt execution if available.
|
|
138
|
+
Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
|
|
139
|
+
|
|
140
|
+
* +execution_job+ — The job class used with +later+. Agents can set this to use a
|
|
141
|
+
custom execution job. Defaults to +ActionAI::ExecutionJob+.
|
|
142
|
+
|
|
143
|
+
* +execute_later_queue_name+ — The queue name used by +later+ with the default
|
|
144
|
+
+execution_job+. Agents can set this to use a custom queue name.
|
|
145
|
+
|
|
146
|
+
== Structured output (data schemas)
|
|
147
|
+
|
|
148
|
+
Action AI can instruct the model to return structured JSON that maps directly
|
|
149
|
+
to an ActiveModel model. Two features cooperate to make this work:
|
|
150
|
+
|
|
151
|
+
=== +.schema+ on ActiveModel models
|
|
152
|
+
|
|
153
|
+
Any model that includes +ActiveModel::API+ automatically gains a +.schema+
|
|
154
|
+
class method that returns a +Schematist::Schema+ instance derived from the model's
|
|
155
|
+
attribute types.
|
|
156
|
+
|
|
157
|
+
class Person
|
|
158
|
+
include ActiveModel::Model
|
|
159
|
+
include ActiveModel::Attributes
|
|
160
|
+
|
|
161
|
+
attribute :name, :string
|
|
162
|
+
attribute :age, :integer
|
|
163
|
+
attribute :score, :float
|
|
164
|
+
end
|
|
113
165
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
166
|
+
Person.schema # => a Schematist::Schema instance
|
|
167
|
+
Person.schema.new.to_json_schema # => {name: "Person", schema: {...}}
|
|
168
|
+
|
|
169
|
+
Supported type mappings:
|
|
170
|
+
|
|
171
|
+
* +:string+, +:immutable_string+, +:text+ → JSON +string+
|
|
172
|
+
* +:integer+, +:big_integer+ → JSON +integer+
|
|
173
|
+
* +:float+, +:decimal+, +:big_decimal+ → JSON +number+
|
|
174
|
+
* +:boolean+ → JSON +boolean+
|
|
175
|
+
* +:date+, +:datetime+, +:time+ → JSON +string+ with +format+ set to
|
|
176
|
+
+"date"+, +"date-time"+, or +"time"+ respectively
|
|
177
|
+
* +:binary+ → JSON +string+ with +contentEncoding+ set to +"base64"+
|
|
178
|
+
|
|
179
|
+
Other types are not included in the schema.
|
|
180
|
+
|
|
181
|
+
Active Record associations are included as nested schemas via +object+/+array+.
|
|
182
|
+
To prevent unnescessary nesting or circular dependencies, the schema will include
|
|
183
|
+
associations registered for _nested attributes_ (via +accepts_nested_attributes_for+) only.
|
|
184
|
+
|
|
185
|
+
=== +returns+ in agent actions
|
|
186
|
+
|
|
187
|
+
Call +returns ModelClass+ inside an action method to declare the expected
|
|
188
|
+
return type. Action AI will:
|
|
189
|
+
|
|
190
|
+
1. Configure the chat to request output matching the model's JSON schema
|
|
191
|
+
(+chat.with_schema ModelClass.schema+).
|
|
192
|
+
2. Decorate the resulting +RubyLLM::Message+ with Strong Parameters.
|
|
193
|
+
3. Construct a model instance accessible via +#object+.
|
|
194
|
+
|
|
195
|
+
class Extractor < ApplicationAI
|
|
196
|
+
def person(text)
|
|
197
|
+
@text = text
|
|
198
|
+
|
|
199
|
+
returns Person do |params|
|
|
200
|
+
params.permit(:name, :age)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
result = Extractor.person("Alice is 30 years old")
|
|
206
|
+
result.content # => '{"name":"Alice","age":30}'
|
|
207
|
+
result.parsed # => {"name" => "Alice", "age" => 30}
|
|
208
|
+
result.object # => #<Person name="Alice" age=30>
|
|
209
|
+
|
|
210
|
+
Pass the class wrapped in an array to declare an array return type:
|
|
211
|
+
|
|
212
|
+
class Extractor < ApplicationAI
|
|
213
|
+
def people(text)
|
|
214
|
+
@text = text
|
|
215
|
+
|
|
216
|
+
returns [Person] do |params|
|
|
217
|
+
params.permit(:name, :age)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
result = Extractor.people("Alice is 30 and Bob is 25")
|
|
223
|
+
result.content # => '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'
|
|
224
|
+
result.parsed # => [{"name" => "Alice", "age" => 30}, {"name" => "Bob", "age" => 25}]
|
|
225
|
+
result.object # => [#<Person name="Alice" age=30>, #<Person name="Bob" age=25>]
|
|
226
|
+
|
|
227
|
+
The schema is built from the model's declared attributes and cached.
|
|
228
|
+
|
|
229
|
+
=== Custom schemas
|
|
230
|
+
|
|
231
|
+
If an inferred schema is not sufficient, define a +Schematist::Schema+
|
|
232
|
+
subclass under +app/schemas+ with the model's full class name followed by
|
|
233
|
+
+Schema+. Custom schemas are looked up first; models without a matching class
|
|
234
|
+
continue to use the inferred schema.
|
|
235
|
+
|
|
236
|
+
# app/schemas/person_schema.rb
|
|
237
|
+
class PersonSchema < ActionAI::Schema
|
|
238
|
+
string :name
|
|
239
|
+
integer :age, minimum: 0
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
For namespaced models, mirror the namespace in +app/schemas+:
|
|
243
|
+
|
|
244
|
+
# app/schemas/admin/person_schema.rb
|
|
245
|
+
class Admin::PersonSchema < ActionAI::Schema
|
|
246
|
+
string :name
|
|
247
|
+
end
|
|
118
248
|
|
|
119
249
|
|
|
120
250
|
== Download and installation
|
data/lib/action_ai/agent.rb
CHANGED
|
@@ -4,6 +4,9 @@ require "ruby_llm"
|
|
|
4
4
|
require "active_support/core_ext/string/inflections"
|
|
5
5
|
require "active_support/core_ext/hash/except"
|
|
6
6
|
require "active_support/core_ext/module/anonymous"
|
|
7
|
+
require "active_support/core_ext/module/delegation"
|
|
8
|
+
require "active_support/core_ext/object/try"
|
|
9
|
+
require "active_support/core_ext/object/with_options"
|
|
7
10
|
require "memery"
|
|
8
11
|
|
|
9
12
|
require "action_ai/log_subscriber"
|
|
@@ -26,12 +29,11 @@ module ActionAI
|
|
|
26
29
|
# the prompt views, options on the AI model used such as the <tt>:model</tt> ID, and attachments.
|
|
27
30
|
#
|
|
28
31
|
# class ApplicationAI < ActionAI::Agent
|
|
29
|
-
#
|
|
30
|
-
# provider: :openai
|
|
32
|
+
# model "gpt-6-luna", provider: :openai
|
|
31
33
|
# end
|
|
32
34
|
#
|
|
33
35
|
# class Generator < ApplicationAI
|
|
34
|
-
#
|
|
36
|
+
# model "gpt-6.1-sol"
|
|
35
37
|
#
|
|
36
38
|
# def code(task, language)
|
|
37
39
|
# @task = task
|
|
@@ -155,33 +157,40 @@ module ActionAI
|
|
|
155
157
|
# end
|
|
156
158
|
# end
|
|
157
159
|
#
|
|
158
|
-
# ==
|
|
160
|
+
# == Configuration
|
|
159
161
|
#
|
|
160
|
-
# Action AI
|
|
161
|
-
#
|
|
162
|
+
# Action AI agents configure their model, instructions, tools, and other
|
|
163
|
+
# chat options using the +RubyLLM::Agent+ class DSL directly. You never
|
|
164
|
+
# need to define or reference a +RubyLLM::Agent+ subclass yourself; just
|
|
165
|
+
# call +model+, +instructions+, +tools+, +schema+, and the other
|
|
166
|
+
# +RubyLLM::Agent+ class macros right on your +ActionAI::Agent+ subclass:
|
|
162
167
|
#
|
|
163
168
|
# class Generator < ApplicationAI
|
|
164
|
-
#
|
|
169
|
+
# model "gpt-6.1-sol"
|
|
170
|
+
# instructions "You are a concise support assistant."
|
|
171
|
+
# tools SearchDocs, LookupAccount
|
|
165
172
|
# end
|
|
166
173
|
#
|
|
167
|
-
#
|
|
174
|
+
# Under the hood, Action AI manages a private +RubyLLM::Agent+ subclass for you
|
|
175
|
+
# and builds +chat+ from it, so instructions, tools, schema, fallbacks, caching,
|
|
176
|
+
# and every other declaration are honored exactly as documented on
|
|
177
|
+
# <tt>RubyLLM::Agent</tt>. Any params set through ActionAI::Parameterized
|
|
178
|
+
# (<tt>Generator.with(...)</tt>) are forwarded as its runtime +inputs+.
|
|
168
179
|
#
|
|
169
|
-
#
|
|
170
|
-
# so
|
|
180
|
+
# Configuration is inherited the same way +RubyLLM::Agent+ subclasses inherit
|
|
181
|
+
# it, so a shared +ApplicationAI+ base class can declare a default +model+ and
|
|
182
|
+
# +instructions+ that every agent subclass then extends or overrides:
|
|
171
183
|
#
|
|
172
|
-
# class
|
|
173
|
-
#
|
|
174
|
-
# api_key: proc { Current.user.ai_api_key }
|
|
184
|
+
# class ApplicationAI < ActionAI::Agent
|
|
185
|
+
# model "gpt-6-luna", provider: :openai
|
|
175
186
|
# end
|
|
176
187
|
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
# It is also possible to set these default options that will be used in all agents through
|
|
182
|
-
# the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
|
|
188
|
+
# class Generator < ApplicationAI
|
|
189
|
+
# model { Current.user.preferred_model }
|
|
190
|
+
# end
|
|
183
191
|
#
|
|
184
|
-
#
|
|
192
|
+
# ::rescue_from continues to handle agent-action level exceptions; use it as
|
|
193
|
+
# usual, there's no separate chat-level rescue mechanism to reach for.
|
|
185
194
|
#
|
|
186
195
|
# == \Callbacks
|
|
187
196
|
#
|
|
@@ -265,9 +274,6 @@ module ActionAI
|
|
|
265
274
|
# These options are specified on the class level, like
|
|
266
275
|
# <tt>ActionAI::Agent.raise_execution_errors = true</tt>
|
|
267
276
|
#
|
|
268
|
-
# * <tt>default_options</tt> - You can pass this in at a class level as well as within the class itself as
|
|
269
|
-
# per the above section.
|
|
270
|
-
#
|
|
271
277
|
# * <tt>logger</tt> - the logger is used for generating information on prompt execution if available.
|
|
272
278
|
# Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
|
|
273
279
|
#
|
|
@@ -279,6 +285,7 @@ module ActionAI
|
|
|
279
285
|
class Agent < AbstractController::Base
|
|
280
286
|
include Callbacks
|
|
281
287
|
include ImplicitInteraction
|
|
288
|
+
prepend StructuredOutput
|
|
282
289
|
include QueuedExecution
|
|
283
290
|
include Rescuable
|
|
284
291
|
include Parameterized
|
|
@@ -303,11 +310,9 @@ module ActionAI
|
|
|
303
310
|
|
|
304
311
|
helper ActionAI::PromptHelper
|
|
305
312
|
|
|
306
|
-
class_attribute :default_params, default: {
|
|
307
|
-
# none
|
|
308
|
-
}.freeze
|
|
309
|
-
|
|
310
313
|
class << self
|
|
314
|
+
include Memery
|
|
315
|
+
|
|
311
316
|
# Returns the name of the current agent. This method is also being used as a path for a view lookup.
|
|
312
317
|
# If this is an anonymous agent, this method will return +anonymous+ instead.
|
|
313
318
|
def agent_name
|
|
@@ -317,18 +322,18 @@ module ActionAI
|
|
|
317
322
|
attr_writer :agent_name
|
|
318
323
|
alias :controller_path :agent_name
|
|
319
324
|
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
def
|
|
324
|
-
|
|
325
|
-
default_params
|
|
325
|
+
# The private, per-agent-class +RubyLLM::Agent+ subclass,
|
|
326
|
+
# lazily created and configured via its own class macros
|
|
327
|
+
# (+model+, +instructions+, +tools+, +schema+, and so on).
|
|
328
|
+
memoize def agent_class
|
|
329
|
+
Class.new superclass.try(:agent_class) || RubyLLM::Agent
|
|
326
330
|
end
|
|
327
|
-
alias :default_options= :default
|
|
328
331
|
|
|
329
332
|
private
|
|
330
333
|
def method_missing(method_name, ...)
|
|
331
|
-
if
|
|
334
|
+
if agent_class.respond_to? method_name # configuration
|
|
335
|
+
agent_class.public_send(method_name, ...)
|
|
336
|
+
elsif action_methods.include? method_name.name # actions
|
|
332
337
|
Interaction.new(self, method_name, ...)
|
|
333
338
|
else
|
|
334
339
|
super
|
|
@@ -336,13 +341,22 @@ module ActionAI
|
|
|
336
341
|
end
|
|
337
342
|
|
|
338
343
|
def respond_to_missing?(method, include_all = false)
|
|
339
|
-
|
|
344
|
+
agent_class.respond_to? method or
|
|
345
|
+
action_methods.include? method.name or
|
|
346
|
+
super
|
|
340
347
|
end
|
|
341
348
|
end
|
|
342
349
|
|
|
343
350
|
attr_internal :message
|
|
344
351
|
|
|
345
|
-
memoize def
|
|
352
|
+
memoize def agent = agent_class.new(**params
|
|
353
|
+
.slice(*agent_class.inputs)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
with_options private: true do
|
|
357
|
+
delegate :agent_class, to: :class
|
|
358
|
+
delegate :chat, to: :agent
|
|
359
|
+
end
|
|
346
360
|
|
|
347
361
|
def process(method_name, *args) # :nodoc:
|
|
348
362
|
payload = {
|
|
@@ -378,10 +392,10 @@ module ActionAI
|
|
|
378
392
|
#
|
|
379
393
|
# * +:with+ - Array of file paths or URLs to attach to the prompt.
|
|
380
394
|
#
|
|
381
|
-
# You can set default model options using the ::
|
|
395
|
+
# You can set default model options using the ::model class method:
|
|
382
396
|
#
|
|
383
397
|
# class Generator < ActionAI::Agent
|
|
384
|
-
#
|
|
398
|
+
# model "gpt-6.1-sol", provider: :openai
|
|
385
399
|
# end
|
|
386
400
|
#
|
|
387
401
|
# It will find a template in the view paths using by default the agent name and the
|
|
@@ -391,7 +405,7 @@ module ActionAI
|
|
|
391
405
|
# For example:
|
|
392
406
|
#
|
|
393
407
|
# class Generator < ActionAI::Agent
|
|
394
|
-
#
|
|
408
|
+
# model "gpt-6.1-sol"
|
|
395
409
|
#
|
|
396
410
|
# def code(task, language)
|
|
397
411
|
# @task = task
|
|
@@ -422,24 +436,6 @@ module ActionAI
|
|
|
422
436
|
false
|
|
423
437
|
end
|
|
424
438
|
|
|
425
|
-
def apply_defaults(config = {})
|
|
426
|
-
default_values = self.class.default.except(*config.keys).transform_values do |value|
|
|
427
|
-
compute_default(value)
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
config.reverse_merge(default_values)
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
def compute_default(value)
|
|
434
|
-
return value unless value.is_a?(Proc)
|
|
435
|
-
|
|
436
|
-
if value.arity == 1
|
|
437
|
-
instance_exec(self, &value)
|
|
438
|
-
else
|
|
439
|
-
instance_exec(&value)
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
439
|
def use_attachments
|
|
444
440
|
attachments.presence
|
|
445
441
|
.tap { @attachments = nil } # reset
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_ai/refinements/regexp"
|
|
4
|
+
|
|
5
|
+
module ActionAI
|
|
6
|
+
module ModelSchema
|
|
7
|
+
# Extends schema inference with Active Model–specific behavior.
|
|
8
|
+
#
|
|
9
|
+
# Overrides the base inference methods to extract constraint metadata
|
|
10
|
+
# from Active Model validation declarations (presence, inclusion,
|
|
11
|
+
# numericality, format, length, comparison, and acceptance).
|
|
12
|
+
module Inference::ActiveModel
|
|
13
|
+
using Refinements::Regexp
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Determines whether an attribute is required by checking for
|
|
18
|
+
# a presence validator, in addition to any other sources.
|
|
19
|
+
def attribute_required? name
|
|
20
|
+
super { [
|
|
21
|
+
attribute_validators(name, :presence)
|
|
22
|
+
.any?,
|
|
23
|
+
|
|
24
|
+
*(yield if block_given?)
|
|
25
|
+
] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Infers enum values from inclusion and numericality validators
|
|
29
|
+
# that declare an `:in` option with multiple values.
|
|
30
|
+
def attribute_enum name
|
|
31
|
+
super { [
|
|
32
|
+
*[
|
|
33
|
+
*attribute_validators(name, :numericality),
|
|
34
|
+
*attribute_validators(name, :inclusion),
|
|
35
|
+
].filter_map { it.options[:in] },
|
|
36
|
+
|
|
37
|
+
*(yield if block_given?)
|
|
38
|
+
] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Infers a constant value from validators that restrict the
|
|
42
|
+
# attribute to a single value: comparison/equality, single-item
|
|
43
|
+
# inclusion, or acceptance validators without `allow_nil`.
|
|
44
|
+
def attribute_const name
|
|
45
|
+
super { [
|
|
46
|
+
*[
|
|
47
|
+
*attribute_validators(name, :comparison),
|
|
48
|
+
*attribute_validators(name, :numericality),
|
|
49
|
+
].filter_map { it.options[:equal_to] },
|
|
50
|
+
|
|
51
|
+
*[
|
|
52
|
+
*attribute_validators(name, :inclusion),
|
|
53
|
+
*attribute_validators(name, :numericality),
|
|
54
|
+
].filter_map { it.options[:in] }
|
|
55
|
+
.select(&:one?)
|
|
56
|
+
.map(&:first),
|
|
57
|
+
|
|
58
|
+
*attribute_validators(name, :acceptance)
|
|
59
|
+
.reject { it.options[:allow_nil] }
|
|
60
|
+
.presence
|
|
61
|
+
&.any?,
|
|
62
|
+
|
|
63
|
+
*(yield if block_given?)
|
|
64
|
+
] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Infers a regex pattern from format validators, combining
|
|
68
|
+
# multiple patterns into a single expression.
|
|
69
|
+
# Validators with `:without` are negated.
|
|
70
|
+
def attribute_format name
|
|
71
|
+
super { [
|
|
72
|
+
*attribute_validators(name, :format)
|
|
73
|
+
.map(&:options)
|
|
74
|
+
.filter_map { it[:with] or ~it[:without] rescue nil },
|
|
75
|
+
|
|
76
|
+
*(yield if block_given?)
|
|
77
|
+
] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Infers minimum and maximum string length from length validators.
|
|
81
|
+
def attribute_length name
|
|
82
|
+
super { [
|
|
83
|
+
*attribute_validators(name, :length)
|
|
84
|
+
.map(&:options)
|
|
85
|
+
.map { {
|
|
86
|
+
minimum: it.values_at(:is, :minimum).find(&:itself),
|
|
87
|
+
maximum: it.values_at(:is, :maximum).find(&:itself),
|
|
88
|
+
} },
|
|
89
|
+
|
|
90
|
+
*(yield if block_given?)
|
|
91
|
+
] }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Infers numeric boundaries (minimum, maximum, greater_than,
|
|
95
|
+
# less_than, multiple_of) from comparison and numericality validators.
|
|
96
|
+
def attribute_boundaries name
|
|
97
|
+
super { [
|
|
98
|
+
*[
|
|
99
|
+
*attribute_validators(name, :comparison),
|
|
100
|
+
*attribute_validators(name, :numericality),
|
|
101
|
+
].map(&:options)
|
|
102
|
+
.map { {
|
|
103
|
+
minimum: it[:greater_than_or_equal_to],
|
|
104
|
+
greater_than: it[:greater_than],
|
|
105
|
+
less_than: it[:less_than],
|
|
106
|
+
maximum: it[:less_than_or_equal_to],
|
|
107
|
+
|
|
108
|
+
multiple_of: it[:multiple_of],
|
|
109
|
+
} },
|
|
110
|
+
|
|
111
|
+
*(yield if block_given?)
|
|
112
|
+
] }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns the validators for an attribute, optionally filtered
|
|
116
|
+
# by validator type (e.g., `:presence`, `:inclusion`, `:format`).
|
|
117
|
+
def attribute_validators name, type = nil
|
|
118
|
+
validators_on(name)
|
|
119
|
+
.grep(ActiveModel::Validations.const_get "#{type.to_s.camelize}Validator")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|