omni_agent 0.1.8 → 0.1.9

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: ea0324a20bc12e064b994b4fcdf7af700cd0280ba7b0609dc75724717e9fc881
4
- data.tar.gz: dfda03e711340e59dfbb1cd803bc785a51bbe39d972992c6a26fb5b2d4d8761a
3
+ metadata.gz: 66c390e526b0bd6176f8755538304d0e0e99d9cc36627be1ee9b63119b53ce34
4
+ data.tar.gz: 4918452fbba6d30772abbf9d072d0c16cf363d5220a099183e9caa04d7481220
5
5
  SHA512:
6
- metadata.gz: 72739c339838d0f38c172455226ff79244ab7e739920ec9e7b89f16182d220a1526886bff1f85b7b943bc6426676d5cc4dbd727f903c612df401587fc627f3f0
7
- data.tar.gz: b90d265c701efaf90d5d5f5e5f9aac3b337095e998d413bf5e872e05d794a9793a44e0bd669425d30ee13eabd50ded2f0fdc2c19cadbc1ff976ec02d0e8d888f
6
+ metadata.gz: 522e4be90a757a9056e95cf45f5aafb1f18d7b6914e1ca667e32b3774ab70422f552e333ac2e340c4d1dab1c628350e04d494a9726b908467eb53723ae300dd6
7
+ data.tar.gz: c2c7939747237e554df0ac2569b84e36b164c1235f75fa876f04518cb644ad02579e18f4ee168c53ff55f32b75c8a58284ff921b217eda4b9148f67a8d5e4de7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.9](https://github.com/ACR1209/omni_agent/compare/omni_agent/v0.1.8...omni_agent/v0.1.9) (2026-07-12)
6
+
7
+
8
+ ### Features
9
+
10
+ * **agent:** support streaming responses via stream sink ([7cf1fba](https://github.com/ACR1209/omni_agent/commit/7cf1fba4fa378c5cee7f464354f760506a0ff513))
11
+ * **docs:** add enum type for temperature unit in input schema ([d992658](https://github.com/ACR1209/omni_agent/commit/d99265836307ce8f863b060eff73c7acdbc8d2ff))
12
+ * **providers:** add Ollama provider via OpenAI-compat endpoint ([4ae92ad](https://github.com/ACR1209/omni_agent/commit/4ae92ad0f549119b43c2e8a7381f4f7245aa2f68))
13
+ * **tool:** add optional constraint validations and custom validate procs ([db8371a](https://github.com/ACR1209/omni_agent/commit/db8371a189f710e812c3c3ce15f200df1ba94902))
14
+ * **tool:** add polymorphic reference datatype ([1686bbb](https://github.com/ACR1209/omni_agent/commit/1686bbbf41932622a5537c335da8c41c68a73f73))
15
+ * **tool:** infer and validate enum value types in schema builder ([ceb3e78](https://github.com/ACR1209/omni_agent/commit/ceb3e780498caa1158e9fc8af3eb6e9b95c33ddb))
16
+ * **tool:** validate required and enum arguments on invoke ([543c6e0](https://github.com/ACR1209/omni_agent/commit/543c6e08f9c394b84c73bc5957aff7058e0fab19))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **lint:** add space inside empty block braces ([9116b3f](https://github.com/ACR1209/omni_agent/commit/9116b3ff82e9fab9824347515b4c1cd40a40785e))
22
+ * **spec:** redact OpenAI API key from recorded VCR cassettes ([53cecd5](https://github.com/ACR1209/omni_agent/commit/53cecd540b6dd52ddc5bf3da0550836fbfcfab88))
23
+
5
24
  ## [0.1.8](https://github.com/ACR1209/omni_agent/compare/omni_agent/v0.1.7...omni_agent/v0.1.8) (2026-07-07)
6
25
 
7
26
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ tool schemas, and generation lifecycle callbacks.
11
11
  - Prompt composition from ERB files in `app/agents/<agent_name>/`
12
12
  - Agent callbacks (`before_generation`, `after_generation`)
13
13
  - Agent and tool tags to support filtering strategies
14
- - OpenAI provider integration out of the box
14
+ - OpenAI provider integration out of the box, plus an Ollama provider for local models
15
15
  - Rake tasks and Rails generators for scaffolding
16
16
 
17
17
  ## Installation
@@ -27,6 +27,8 @@ Add the provider you're using to the Gemfile as well:
27
27
  gem "openai"
28
28
  ```
29
29
 
30
+ The `ollama` provider also depends on the `openai` gem — it talks to Ollama's OpenAI-compatible endpoint, so no separate gem is needed.
31
+
30
32
  Then run:
31
33
 
32
34
  ```bash
@@ -53,6 +55,14 @@ bundle exec rails generate omni_agent:agent ResearchAgent --model gpt-4.1-mini -
53
55
  OPENAI_ACCESS_TOKEN=your_api_key_here
54
56
  ```
55
57
 
58
+ To use Ollama instead, set `config.default_provider = :ollama` (see below) and, if needed, override its endpoint/model:
59
+
60
+ ```dotenv
61
+ OLLAMA_HOST=http://localhost:11434 # defaults to this
62
+ OLLAMA_MODEL=llama3.1 # defaults to this; pick a tool-calling-capable model
63
+ OLLAMA_API_KEY=ollama # unused by Ollama, but required by the openai client
64
+ ```
65
+
56
66
  4. Implement your agent prompt and optional tools under:
57
67
 
58
68
  ```text
@@ -129,6 +139,23 @@ class SupervisorAgent < OmniAgent::Agent
129
139
  end
130
140
  ```
131
141
 
142
+ ## Streaming
143
+
144
+ Prefix any run entrypoint with `.stream` and pass a block to receive the response as it's generated instead of waiting for the full result:
145
+
146
+ ```ruby
147
+ ResearchAgent.with(user_id: 42).stream.run("What's new?") do |event|
148
+ case event.type
149
+ when :text then print event.text
150
+ when :tool_call then puts "\n[using #{event.tool_name}...]"
151
+ when :tool_result then puts "[#{event.error? ? "failed" : "done"}]"
152
+ when :done then puts "\n---"
153
+ end
154
+ end
155
+ ```
156
+
157
+ `.stream` must come before the call (`.stream.run(...)`, not `.run(...).stream`) — without it, or without a block, behavior is unchanged and the same `Response` is returned either way. Only the `openai`, `ollama`, and `mock` providers stream today. See [Streaming Responses](omniagent-docs/docs/agent/streaming.mdx) for the full event reference.
158
+
132
159
  ## Evals
133
160
 
134
161
  `OmniAgent::Eval` lets you test agent quality: deterministic assertions (tool calls, output matching) and pluggable LLM-as-judge scoring.
@@ -20,7 +20,7 @@ module OmniAgent
20
20
  @_omni_agent_wrapping_method = true
21
21
  alias_method alias_name, method_name
22
22
 
23
- define_method(method_name) do |input = nil, context: {}, **context_keywords|
23
+ define_method(method_name) do |input = nil, context: {}, **context_keywords, &block|
24
24
  if input.nil? && context == {} && context_keywords.empty?
25
25
  run_alias_entrypoint_logic(alias_name)
26
26
  else
@@ -29,7 +29,7 @@ module OmniAgent
29
29
 
30
30
  run_input = run_alias_entrypoint_logic(alias_name, fallback_input: input)
31
31
 
32
- run(run_input, context: merged_context, prompt_method: method_name)
32
+ run(run_input, context: merged_context, prompt_method: method_name, &block)
33
33
  end
34
34
  end
35
35
  ensure
@@ -78,8 +78,8 @@ module OmniAgent
78
78
  aliases = normalize_callbacks(:run_aliases, method_names)
79
79
 
80
80
  aliases.each do |method_name|
81
- define_method(method_name) do |input, context: {}|
82
- run(input, context: context, prompt_method: method_name)
81
+ define_method(method_name) do |input, context: {}, &block|
82
+ run(input, context: context, prompt_method: method_name, &block)
83
83
  end
84
84
  end
85
85
  end
@@ -212,7 +212,11 @@ module OmniAgent
212
212
  @default_context = context_override || {}
213
213
  end
214
214
 
215
- def run(input, context: {}, prompt_method: nil)
215
+ def stream
216
+ OmniAgent::Streaming::Proxy.new(self)
217
+ end
218
+
219
+ def run(input, context: {}, prompt_method: nil, &stream)
216
220
  context = @default_context.merge(context || {})
217
221
  bind_context_instance_variables(context)
218
222
 
@@ -237,13 +241,14 @@ module OmniAgent
237
241
  "Increase OmniAgent.configuration.max_tool_iterations if more tool calls are expected."
238
242
  end
239
243
 
240
- response = provider.chat(messages: messages, tools: filtered_tools, **@chat_options)
244
+ response = provider.chat(messages: messages, tools: filtered_tools, stream: stream, **@chat_options)
241
245
 
242
246
  if response.content && !response.tool_calls?
243
247
  messages << { role: "assistant", content: response.content }
244
248
  set_after_generation_state(response: response, messages: messages, initial_messages_count: initial_messages_count)
245
249
  run_after_generation_callbacks(input: input, context: context, messages: messages, response: response)
246
250
  sync_context_from_instance_variables(context)
251
+ stream&.call(OmniAgent::Streaming::Event.done(response))
247
252
  return response
248
253
  end
249
254
 
@@ -265,6 +270,8 @@ module OmniAgent
265
270
  tool_instance = tool_class.new
266
271
  tool_instance.context = context if tool_instance.respond_to?(:context=)
267
272
 
273
+ stream&.call(OmniAgent::Streaming::Event.tool_call(name: tool_name, arguments: tool_args, id: tool_id))
274
+
268
275
  begin
269
276
  result = tool_instance.invoke(tool_args)
270
277
 
@@ -274,13 +281,16 @@ module OmniAgent
274
281
  name: tool_name,
275
282
  content: result.to_s
276
283
  }
284
+ stream&.call(OmniAgent::Streaming::Event.tool_result(name: tool_name, id: tool_id, content: result.to_s))
277
285
  rescue => e
286
+ error_content = "Error executing tool: #{e.message}"
278
287
  messages << {
279
288
  role: "tool",
280
289
  tool_call_id: tool_id,
281
290
  name: tool_name,
282
- content: "Error executing tool: #{e.message}"
291
+ content: error_content
283
292
  }
293
+ stream&.call(OmniAgent::Streaming::Event.tool_result(name: tool_name, id: tool_id, content: error_content, error: true))
284
294
  end
285
295
 
286
296
  should_stop_generation ||= tool_class.respond_to?(:stops_generation?) && tool_class.stops_generation?
@@ -299,6 +309,7 @@ module OmniAgent
299
309
  set_after_generation_state(response: response, messages: messages, initial_messages_count: initial_messages_count)
300
310
  run_after_generation_callbacks(input: input, context: context, messages: messages, response: response)
301
311
  sync_context_from_instance_variables(context)
312
+ stream&.call(OmniAgent::Streaming::Event.done(response))
302
313
  return response
303
314
  end
304
315
  end
@@ -9,7 +9,7 @@ module OmniAgent
9
9
  @model = model || default_model
10
10
  end
11
11
 
12
- def chat(messages:, tools: [], **_options)
12
+ def chat(messages:, tools: [], stream: nil, **_options)
13
13
  raise NotImplementedError, "Providers must implement #chat"
14
14
  end
15
15
 
@@ -3,9 +3,16 @@ module OmniAgent
3
3
  class Mock < Base
4
4
  LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
5
5
 
6
- def chat(messages:, tools: [], **_options)
6
+ def chat(messages:, tools: [], stream: nil, **_options)
7
7
  validate_messages!(messages, allowed_roles: %i[system user assistant tool])
8
8
 
9
+ if stream
10
+ LOREM_IPSUM.split(" ").each_with_index do |word, index|
11
+ chunk = index.zero? ? word : " #{word}"
12
+ stream.call(OmniAgent::Streaming::Event.text(chunk))
13
+ end
14
+ end
15
+
9
16
  OmniAgent::Providers::Response.new(
10
17
  content: LOREM_IPSUM,
11
18
  raw_request: {
@@ -0,0 +1,30 @@
1
+ # lib/omni_agent/providers/ollama.rb
2
+ module OmniAgent
3
+ module Providers
4
+ class Ollama < OpenAI
5
+ protected
6
+
7
+ def provider_label
8
+ "Ollama"
9
+ end
10
+
11
+ def client
12
+ @client ||= ::OpenAI::Client.new(api_key: @api_key, base_url: base_url)
13
+ end
14
+
15
+ def default_api_key
16
+ ENV.fetch("OLLAMA_API_KEY", "ollama")
17
+ end
18
+
19
+ def default_model
20
+ ENV.fetch("OLLAMA_MODEL", "llama3.1")
21
+ end
22
+
23
+ private
24
+
25
+ def base_url
26
+ "#{ENV.fetch('OLLAMA_HOST', 'http://localhost:11434')}/v1"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -12,7 +12,7 @@ module OmniAgent
12
12
  "Please add `gem 'openai'` to your Gemfile."
13
13
  end
14
14
 
15
- def chat(messages:, tools: [], **options)
15
+ def chat(messages:, tools: [], stream: nil, **options)
16
16
  validate_messages!(messages, allowed_roles: %i[system user assistant tool])
17
17
 
18
18
  messages = messages.map do |msg|
@@ -44,15 +44,23 @@ module OmniAgent
44
44
  payload[:tools] = openai_tools if openai_tools.any?
45
45
  payload.merge!(options) if options.any?
46
46
 
47
- response = with_retries { client.chat.completions.create(**payload) }
47
+ response = if stream
48
+ stream_chat(payload: payload, stream: stream)
49
+ else
50
+ with_retries { client.chat.completions.create(**payload) }
51
+ end
48
52
 
49
53
  parse_response(response, payload)
50
54
  rescue => e
51
- raise OmniAgent::Error, "Error during OpenAI chat: #{e.message}"
55
+ raise OmniAgent::Error, "Error during #{provider_label} chat: #{e.message}"
52
56
  end
53
57
 
54
58
  protected
55
59
 
60
+ def provider_label
61
+ "OpenAI"
62
+ end
63
+
56
64
  def retryable_error?(error)
57
65
  defined?(::OpenAI::Errors) &&
58
66
  (error.is_a?(::OpenAI::Errors::RateLimitError) ||
@@ -74,6 +82,18 @@ module OmniAgent
74
82
 
75
83
  private
76
84
 
85
+ def stream_chat(payload:, stream:)
86
+ chat_stream = with_retries { client.chat.completions.stream(**payload) }
87
+
88
+ chat_stream.each do |event|
89
+ next unless event.is_a?(::OpenAI::Helpers::Streaming::ChatContentDeltaEvent)
90
+
91
+ stream.call(OmniAgent::Streaming::Event.text(event.delta))
92
+ end
93
+
94
+ chat_stream.get_final_completion
95
+ end
96
+
77
97
  def format_tool(tool_class)
78
98
  schema = tool_class.json_schema.dup
79
99
 
@@ -4,6 +4,7 @@ module OmniAgent
4
4
  def self.registry
5
5
  {
6
6
  openai: OmniAgent::Providers::OpenAI,
7
+ ollama: OmniAgent::Providers::Ollama,
7
8
  mock: OmniAgent::Providers::Mock,
8
9
  mock_judge: OmniAgent::Providers::MockJudge
9
10
  }
@@ -0,0 +1,54 @@
1
+ module OmniAgent
2
+ module Streaming
3
+ class Event
4
+ attr_reader :type, :text, :tool_name, :tool_arguments, :tool_id, :content, :error, :response
5
+
6
+ def self.text(delta)
7
+ new(type: :text, text: delta)
8
+ end
9
+
10
+ def self.tool_call(name:, arguments:, id:)
11
+ new(type: :tool_call, tool_name: name, tool_arguments: arguments, tool_id: id)
12
+ end
13
+
14
+ def self.tool_result(name:, id:, content:, error: false)
15
+ new(type: :tool_result, tool_name: name, tool_id: id, content: content, error: error)
16
+ end
17
+
18
+ def self.done(response)
19
+ new(type: :done, response: response)
20
+ end
21
+
22
+ def initialize(type:, text: nil, tool_name: nil, tool_arguments: nil, tool_id: nil, content: nil, error: false, response: nil)
23
+ @type = type
24
+ @text = text
25
+ @tool_name = tool_name
26
+ @tool_arguments = tool_arguments
27
+ @tool_id = tool_id
28
+ @content = content
29
+ @error = error
30
+ @response = response
31
+ end
32
+
33
+ def text?
34
+ type == :text
35
+ end
36
+
37
+ def tool_call?
38
+ type == :tool_call
39
+ end
40
+
41
+ def tool_result?
42
+ type == :tool_result
43
+ end
44
+
45
+ def done?
46
+ type == :done
47
+ end
48
+
49
+ def error?
50
+ @error == true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ module OmniAgent
2
+ module Streaming
3
+ class Proxy
4
+ def initialize(agent)
5
+ @agent = agent
6
+ end
7
+
8
+ def method_missing(name, *args, **kwargs, &block)
9
+ @agent.public_send(name, *args, **kwargs, &block)
10
+ end
11
+
12
+ def respond_to_missing?(name, include_private = false)
13
+ @agent.respond_to?(name, include_private) || super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,26 +1,46 @@
1
1
  module OmniAgent
2
2
  class Tool
3
3
  class SchemaBuilder
4
- attr_reader :properties, :required_fields
4
+ attr_reader :properties, :required_fields, :validators, :polymorphics
5
5
 
6
6
  def initialize
7
7
  @properties = {}
8
8
  @required_fields = []
9
+ @validators = {}
10
+ @polymorphics = {}
9
11
  end
10
12
 
11
- def string(name, description: nil, required: true)
12
- add_property(name, type: "string", description: description, required: required)
13
+ def string(name, description: nil, required: true, min_length: nil, max_length: nil, pattern: nil, format: nil, validate: nil)
14
+ constraints = {}
15
+ constraints[:minLength] = min_length if min_length
16
+ constraints[:maxLength] = max_length if max_length
17
+ constraints[:pattern] = pattern.is_a?(Regexp) ? pattern.source : pattern if pattern
18
+ constraints[:format] = format if format
19
+
20
+ add_property(name, type: "string", description: description, required: required, constraints: constraints, validate: validate)
21
+ end
22
+
23
+ def integer(name, description: nil, required: true, min: nil, max: nil, validate: nil)
24
+ constraints = {}
25
+ constraints[:minimum] = min if min
26
+ constraints[:maximum] = max if max
27
+
28
+ add_property(name, type: "integer", description: description, required: required, constraints: constraints, validate: validate)
13
29
  end
14
30
 
15
- def integer(name, description: nil, required: true)
16
- add_property(name, type: "integer", description: description, required: required)
31
+ def number(name, description: nil, required: true, min: nil, max: nil, validate: nil)
32
+ constraints = {}
33
+ constraints[:minimum] = min if min
34
+ constraints[:maximum] = max if max
35
+
36
+ add_property(name, type: "number", description: description, required: required, constraints: constraints, validate: validate)
17
37
  end
18
38
 
19
- def boolean(name, description: nil, required: true)
20
- add_property(name, type: "boolean", description: description, required: required)
39
+ def boolean(name, description: nil, required: true, validate: nil)
40
+ add_property(name, type: "boolean", description: description, required: required, validate: validate)
21
41
  end
22
42
 
23
- def array(name, items_type: nil, description: nil, required: true, &block)
43
+ def array(name, items_type: nil, description: nil, required: true, min_items: nil, max_items: nil, validate: nil, &block)
24
44
  property = { type: "array" }
25
45
  property[:description] = description if description
26
46
 
@@ -38,11 +58,15 @@ module OmniAgent
38
58
  property[:items] = { type: items_type || "string" }
39
59
  end
40
60
 
61
+ property[:minItems] = min_items if min_items
62
+ property[:maxItems] = max_items if max_items
63
+
41
64
  @properties[name] = property
42
65
  @required_fields << name.to_s if required
66
+ @validators[name] = validate if validate
43
67
  end
44
68
 
45
- def hash(name, description: nil, required: true, &block)
69
+ def hash(name, description: nil, required: true, validate: nil, &block)
46
70
  property = { type: "object" }
47
71
  property[:description] = description if description
48
72
 
@@ -59,16 +83,112 @@ module OmniAgent
59
83
 
60
84
  @properties[name] = property
61
85
  @required_fields << name.to_s if required
86
+ @validators[name] = validate if validate
87
+ end
88
+
89
+ def enum(name, values:, description: nil, required: true, validate: nil)
90
+ raise ArgumentError, "enum requires at least one value" if values.empty?
91
+
92
+ normalized_values = values.map { |v| v.is_a?(Symbol) ? v.to_s : v }
93
+ value_data_type = enum_data_type(normalized_values)
94
+
95
+ property = { type: value_data_type, enum: normalized_values }
96
+ property[:description] = description if description
97
+ @properties[name] = property
98
+ @required_fields << name.to_s if required
99
+ @validators[name] = validate if validate
100
+ end
101
+
102
+ def polymorphic(name, types: nil, description: nil, id_type: "string", required: true, resolve: false, &block)
103
+ if resolve && (types.nil? || types.empty?)
104
+ raise ArgumentError, "polymorphic resolve: true requires types:"
105
+ end
106
+
107
+ type_values = types
108
+ type_description = description ? "#{description} (type)" : nil
109
+ id_type_value = id_type
110
+ id_description = description ? "#{description} (id)" : nil
111
+
112
+ if block_given?
113
+ field_builder = PolymorphicFieldBuilder.new
114
+ field_builder.instance_eval(&block)
115
+
116
+ type_values = field_builder.type_values || type_values
117
+ type_description = field_builder.type_description || type_description
118
+ id_type_value = field_builder.id_type || id_type_value
119
+ id_description = field_builder.id_description || id_description
120
+ end
121
+
122
+ type_field = :"#{name}_type"
123
+ id_field = :"#{name}_id"
124
+ id_json_type = id_type_value.to_s == "integer" ? "integer" : "string"
125
+
126
+ if type_values && !type_values.empty?
127
+ enum(type_field, values: type_values, description: type_description, required: required)
128
+ else
129
+ string(type_field, description: type_description, required: required)
130
+ end
131
+
132
+ if id_json_type == "integer"
133
+ integer(id_field, description: id_description, required: required)
134
+ else
135
+ string(id_field, description: id_description, required: required)
136
+ end
137
+
138
+ @polymorphics[name] = {
139
+ type_field: type_field,
140
+ id_field: id_field,
141
+ types: type_values,
142
+ id_type: id_json_type,
143
+ resolve: resolve
144
+ }
62
145
  end
63
146
 
64
147
  private
65
148
 
66
- def add_property(name, type:, description:, required:)
149
+ def enum_data_type(values)
150
+ types = values.map { |v| json_type_for(v) }.uniq
151
+
152
+ if types.size > 1
153
+ raise ArgumentError, "enum values must all be the same type, got: #{types.join(', ')}"
154
+ end
155
+
156
+ types.first
157
+ end
158
+
159
+ def json_type_for(value)
160
+ case value
161
+ when Integer then "integer"
162
+ when Float then "number"
163
+ when String then "string"
164
+ when true, false then "boolean"
165
+ else
166
+ raise ArgumentError, "unsupported enum value type: #{value.class}"
167
+ end
168
+ end
169
+
170
+ def add_property(name, type:, description:, required:, constraints: {}, validate: nil)
67
171
  property = { type: type }
68
172
  property[:description] = description if description
173
+ property.merge!(constraints)
69
174
 
70
175
  @properties[name] = property
71
176
  @required_fields << name.to_s if required
177
+ @validators[name] = validate if validate
178
+ end
179
+
180
+ class PolymorphicFieldBuilder
181
+ attr_reader :type_values, :type_description, :id_type, :id_description
182
+
183
+ def type(values: nil, description: nil)
184
+ @type_values = values
185
+ @type_description = description
186
+ end
187
+
188
+ def id(type: "string", description: nil)
189
+ @id_type = type
190
+ @id_description = description
191
+ end
72
192
  end
73
193
  end
74
194
  end
@@ -28,6 +28,8 @@ module OmniAgent
28
28
 
29
29
  @properties = builder.properties
30
30
  @required = builder.required_fields
31
+ @validators = builder.validators
32
+ @polymorphics = builder.polymorphics
31
33
  end
32
34
  end
33
35
 
@@ -43,7 +45,15 @@ module OmniAgent
43
45
  def parse_arguments(arguments_hash)
44
46
  kwargs = arguments_hash.transform_keys(&:to_sym)
45
47
  valid_keys = (@properties || {}).keys.map(&:to_sym)
46
- kwargs.slice(*valid_keys)
48
+ filtered = kwargs.slice(*valid_keys)
49
+
50
+ validate_required!(filtered)
51
+ validate_enums!(filtered)
52
+ validate_constraints!(filtered)
53
+ run_custom_validators!(filtered)
54
+ resolve_polymorphics!(filtered)
55
+
56
+ filtered
47
57
  end
48
58
 
49
59
  def stops_generation(value = true)
@@ -56,6 +66,87 @@ module OmniAgent
56
66
 
57
67
  private
58
68
 
69
+ def validate_required!(kwargs)
70
+ missing = (@required || []).map(&:to_sym) - kwargs.keys
71
+ return if missing.empty?
72
+
73
+ raise ArgumentError, "missing required argument(s): #{missing.join(', ')}"
74
+ end
75
+
76
+ def validate_enums!(kwargs)
77
+ (@properties || {}).each do |name, property|
78
+ allowed_values = property[:enum]
79
+ next unless allowed_values
80
+
81
+ key = name.to_sym
82
+ next unless kwargs.key?(key)
83
+
84
+ unless allowed_values.include?(kwargs[key])
85
+ raise ArgumentError, "invalid value for #{name}: #{kwargs[key].inspect} (must be one of: #{allowed_values.join(', ')})"
86
+ end
87
+ end
88
+ end
89
+
90
+ def validate_constraints!(kwargs)
91
+ (@properties || {}).each do |name, property|
92
+ key = name.to_sym
93
+ next unless kwargs.key?(key)
94
+
95
+ check_constraints!(name, property, kwargs[key])
96
+ end
97
+ end
98
+
99
+ def check_constraints!(name, property, value)
100
+ if property[:minLength] && value.is_a?(String) && value.length < property[:minLength]
101
+ raise ArgumentError, "#{name} must be at least #{property[:minLength]} characters"
102
+ end
103
+ if property[:maxLength] && value.is_a?(String) && value.length > property[:maxLength]
104
+ raise ArgumentError, "#{name} must be at most #{property[:maxLength]} characters"
105
+ end
106
+ if property[:pattern] && value.is_a?(String) && !value.match?(Regexp.new(property[:pattern]))
107
+ raise ArgumentError, "#{name} does not match required pattern #{property[:pattern]}"
108
+ end
109
+ if property[:minimum] && value.is_a?(Numeric) && value < property[:minimum]
110
+ raise ArgumentError, "#{name} must be >= #{property[:minimum]}"
111
+ end
112
+ if property[:maximum] && value.is_a?(Numeric) && value > property[:maximum]
113
+ raise ArgumentError, "#{name} must be <= #{property[:maximum]}"
114
+ end
115
+ if property[:minItems] && value.is_a?(Array) && value.length < property[:minItems]
116
+ raise ArgumentError, "#{name} must have at least #{property[:minItems]} items"
117
+ end
118
+ if property[:maxItems] && value.is_a?(Array) && value.length > property[:maxItems]
119
+ raise ArgumentError, "#{name} must have at most #{property[:maxItems]} items"
120
+ end
121
+ end
122
+
123
+ def run_custom_validators!(kwargs)
124
+ (@validators || {}).each do |name, validator|
125
+ key = name.to_sym
126
+ next unless kwargs.key?(key)
127
+
128
+ result = validator.call(kwargs[key])
129
+ raise ArgumentError, "invalid value for #{name}" if result == false
130
+ end
131
+ end
132
+
133
+ def resolve_polymorphics!(kwargs)
134
+ (@polymorphics || {}).each do |name, group|
135
+ next unless group[:resolve]
136
+
137
+ type_field = group[:type_field]
138
+ id_field = group[:id_field]
139
+ next unless kwargs.key?(type_field) && kwargs.key?(id_field)
140
+
141
+ type_value = kwargs.delete(type_field)
142
+ id_value = kwargs.delete(id_field)
143
+
144
+ klass = Object.const_get(type_value.to_s)
145
+ record = klass.find(id_value)
146
+ kwargs[name.to_sym] = record
147
+ end
148
+ end
149
+
59
150
  def normalize_tags(tag_names)
60
151
  raise ArgumentError, "tags requires at least one tag" if tag_names.empty?
61
152
 
@@ -1,3 +1,3 @@
1
1
  module OmniAgent
2
- VERSION = "0.1.8"
2
+ VERSION = "0.1.9"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omni_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - ACR1209
@@ -85,9 +85,12 @@ files:
85
85
  - lib/omni_agent/providers/base.rb
86
86
  - lib/omni_agent/providers/mock.rb
87
87
  - lib/omni_agent/providers/mock_judge.rb
88
+ - lib/omni_agent/providers/ollama.rb
88
89
  - lib/omni_agent/providers/openai.rb
89
90
  - lib/omni_agent/providers/response.rb
90
91
  - lib/omni_agent/railtie.rb
92
+ - lib/omni_agent/streaming/event.rb
93
+ - lib/omni_agent/streaming/proxy.rb
91
94
  - lib/omni_agent/tasks/omni_agent_tasks.rake
92
95
  - lib/omni_agent/tool.rb
93
96
  - lib/omni_agent/tool/schema_builder.rb