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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +28 -1
- data/lib/omni_agent/agent.rb +18 -7
- data/lib/omni_agent/providers/base.rb +1 -1
- data/lib/omni_agent/providers/mock.rb +8 -1
- data/lib/omni_agent/providers/ollama.rb +30 -0
- data/lib/omni_agent/providers/openai.rb +23 -3
- data/lib/omni_agent/providers.rb +1 -0
- data/lib/omni_agent/streaming/event.rb +54 -0
- data/lib/omni_agent/streaming/proxy.rb +17 -0
- data/lib/omni_agent/tool/schema_builder.rb +130 -10
- data/lib/omni_agent/tool.rb +92 -1
- data/lib/omni_agent/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66c390e526b0bd6176f8755538304d0e0e99d9cc36627be1ee9b63119b53ce34
|
|
4
|
+
data.tar.gz: 4918452fbba6d30772abbf9d072d0c16cf363d5220a099183e9caa04d7481220
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/omni_agent/agent.rb
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
@@ -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 =
|
|
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
|
|
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
|
|
data/lib/omni_agent/providers.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
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
|
data/lib/omni_agent/tool.rb
CHANGED
|
@@ -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
|
|
data/lib/omni_agent/version.rb
CHANGED
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.
|
|
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
|