omni_agent 0.1.8 → 0.1.10

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.
@@ -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
@@ -11,6 +11,31 @@ module OmniAgent
11
11
  @metadata || {}
12
12
  end
13
13
 
14
+ def title(text = nil)
15
+ @title = text if text
16
+ @title
17
+ end
18
+
19
+ def annotations(read_only: nil, destructive: nil, idempotent: nil, open_world: nil)
20
+ @annotations = {
21
+ read_only: read_only,
22
+ destructive: destructive,
23
+ idempotent: idempotent,
24
+ open_world: open_world
25
+ }.compact
26
+ end
27
+
28
+ def mcp_annotations
29
+ hints = @annotations || {}
30
+
31
+ {
32
+ readOnlyHint: hints[:read_only],
33
+ destructiveHint: hints[:destructive],
34
+ idempotentHint: hints[:idempotent],
35
+ openWorldHint: hints[:open_world]
36
+ }.compact
37
+ end
38
+
14
39
  def tags(*tag_names)
15
40
  return @tags || [] if tag_names.empty?
16
41
 
@@ -28,6 +53,8 @@ module OmniAgent
28
53
 
29
54
  @properties = builder.properties
30
55
  @required = builder.required_fields
56
+ @validators = builder.validators
57
+ @polymorphics = builder.polymorphics
31
58
  end
32
59
  end
33
60
 
@@ -43,7 +70,15 @@ module OmniAgent
43
70
  def parse_arguments(arguments_hash)
44
71
  kwargs = arguments_hash.transform_keys(&:to_sym)
45
72
  valid_keys = (@properties || {}).keys.map(&:to_sym)
46
- kwargs.slice(*valid_keys)
73
+ filtered = kwargs.slice(*valid_keys)
74
+
75
+ validate_required!(filtered)
76
+ validate_enums!(filtered)
77
+ validate_constraints!(filtered)
78
+ run_custom_validators!(filtered)
79
+ resolve_polymorphics!(filtered)
80
+
81
+ filtered
47
82
  end
48
83
 
49
84
  def stops_generation(value = true)
@@ -56,6 +91,87 @@ module OmniAgent
56
91
 
57
92
  private
58
93
 
94
+ def validate_required!(kwargs)
95
+ missing = (@required || []).map(&:to_sym) - kwargs.keys
96
+ return if missing.empty?
97
+
98
+ raise ArgumentError, "missing required argument(s): #{missing.join(', ')}"
99
+ end
100
+
101
+ def validate_enums!(kwargs)
102
+ (@properties || {}).each do |name, property|
103
+ allowed_values = property[:enum]
104
+ next unless allowed_values
105
+
106
+ key = name.to_sym
107
+ next unless kwargs.key?(key)
108
+
109
+ unless allowed_values.include?(kwargs[key])
110
+ raise ArgumentError, "invalid value for #{name}: #{kwargs[key].inspect} (must be one of: #{allowed_values.join(', ')})"
111
+ end
112
+ end
113
+ end
114
+
115
+ def validate_constraints!(kwargs)
116
+ (@properties || {}).each do |name, property|
117
+ key = name.to_sym
118
+ next unless kwargs.key?(key)
119
+
120
+ check_constraints!(name, property, kwargs[key])
121
+ end
122
+ end
123
+
124
+ def check_constraints!(name, property, value)
125
+ if property[:minLength] && value.is_a?(String) && value.length < property[:minLength]
126
+ raise ArgumentError, "#{name} must be at least #{property[:minLength]} characters"
127
+ end
128
+ if property[:maxLength] && value.is_a?(String) && value.length > property[:maxLength]
129
+ raise ArgumentError, "#{name} must be at most #{property[:maxLength]} characters"
130
+ end
131
+ if property[:pattern] && value.is_a?(String) && !value.match?(Regexp.new(property[:pattern]))
132
+ raise ArgumentError, "#{name} does not match required pattern #{property[:pattern]}"
133
+ end
134
+ if property[:minimum] && value.is_a?(Numeric) && value < property[:minimum]
135
+ raise ArgumentError, "#{name} must be >= #{property[:minimum]}"
136
+ end
137
+ if property[:maximum] && value.is_a?(Numeric) && value > property[:maximum]
138
+ raise ArgumentError, "#{name} must be <= #{property[:maximum]}"
139
+ end
140
+ if property[:minItems] && value.is_a?(Array) && value.length < property[:minItems]
141
+ raise ArgumentError, "#{name} must have at least #{property[:minItems]} items"
142
+ end
143
+ if property[:maxItems] && value.is_a?(Array) && value.length > property[:maxItems]
144
+ raise ArgumentError, "#{name} must have at most #{property[:maxItems]} items"
145
+ end
146
+ end
147
+
148
+ def run_custom_validators!(kwargs)
149
+ (@validators || {}).each do |name, validator|
150
+ key = name.to_sym
151
+ next unless kwargs.key?(key)
152
+
153
+ result = validator.call(kwargs[key])
154
+ raise ArgumentError, "invalid value for #{name}" if result == false
155
+ end
156
+ end
157
+
158
+ def resolve_polymorphics!(kwargs)
159
+ (@polymorphics || {}).each do |name, group|
160
+ next unless group[:resolve]
161
+
162
+ type_field = group[:type_field]
163
+ id_field = group[:id_field]
164
+ next unless kwargs.key?(type_field) && kwargs.key?(id_field)
165
+
166
+ type_value = kwargs.delete(type_field)
167
+ id_value = kwargs.delete(id_field)
168
+
169
+ klass = Object.const_get(type_value.to_s)
170
+ record = klass.find(id_value)
171
+ kwargs[name.to_sym] = record
172
+ end
173
+ end
174
+
59
175
  def normalize_tags(tag_names)
60
176
  raise ArgumentError, "tags requires at least one tag" if tag_names.empty?
61
177
 
@@ -1,3 +1,3 @@
1
1
  module OmniAgent
2
- VERSION = "0.1.8"
2
+ VERSION = "0.1.10"
3
3
  end
data/lib/omni_agent.rb CHANGED
@@ -3,7 +3,7 @@ require "omni_agent/engine" if defined?(Rails)
3
3
  require "zeitwerk"
4
4
 
5
5
  loader = Zeitwerk::Loader.for_gem
6
- loader.inflector.inflect("openai" => "OpenAI", "cli" => "CLI")
6
+ loader.inflector.inflect("openai" => "OpenAI", "cli" => "CLI", "mcp" => "MCP")
7
7
  loader.ignore(File.expand_path("generators", __dir__))
8
8
  loader.setup
9
9
 
@@ -0,0 +1,9 @@
1
+ namespace :omni_agent do
2
+ desc 'Run an MCP server over stdio. Usage: rake "omni_agent:mcp[SupportServer]"'
3
+ task :mcp, [ :server ] => :environment do |_task, args|
4
+ server = args[:server].to_s.strip
5
+ abort 'Please provide a server class. Example: bin/rails "omni_agent:mcp[SupportServer]"' if server.empty?
6
+
7
+ OmniAgent::MCP::Stdio.start(server)
8
+ end
9
+ 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.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - ACR1209
@@ -62,6 +62,7 @@ files:
62
62
  - lib/generators/omni_agent/agent/agent_generator.rb
63
63
  - lib/generators/omni_agent/eval/eval_generator.rb
64
64
  - lib/generators/omni_agent/install/install_generator.rb
65
+ - lib/generators/omni_agent/mcp_server/mcp_server_generator.rb
65
66
  - lib/omni_agent.rb
66
67
  - lib/omni_agent/agent.rb
67
68
  - lib/omni_agent/configuration.rb
@@ -81,18 +82,31 @@ files:
81
82
  - lib/omni_agent/eval/run.rb
82
83
  - lib/omni_agent/eval/runner.rb
83
84
  - lib/omni_agent/eval/tool_call_assertion.rb
85
+ - lib/omni_agent/mcp.rb
86
+ - lib/omni_agent/mcp/authenticator.rb
87
+ - lib/omni_agent/mcp/handler.rb
88
+ - lib/omni_agent/mcp/rack_app.rb
89
+ - lib/omni_agent/mcp/request.rb
90
+ - lib/omni_agent/mcp/routing.rb
91
+ - lib/omni_agent/mcp/server.rb
92
+ - lib/omni_agent/mcp/stdio.rb
93
+ - lib/omni_agent/mcp/tool_adapter.rb
84
94
  - lib/omni_agent/providers.rb
85
95
  - lib/omni_agent/providers/base.rb
86
96
  - lib/omni_agent/providers/mock.rb
87
97
  - lib/omni_agent/providers/mock_judge.rb
98
+ - lib/omni_agent/providers/ollama.rb
88
99
  - lib/omni_agent/providers/openai.rb
89
100
  - lib/omni_agent/providers/response.rb
90
101
  - lib/omni_agent/railtie.rb
102
+ - lib/omni_agent/streaming/event.rb
103
+ - lib/omni_agent/streaming/proxy.rb
91
104
  - lib/omni_agent/tasks/omni_agent_tasks.rake
92
105
  - lib/omni_agent/tool.rb
93
106
  - lib/omni_agent/tool/schema_builder.rb
94
107
  - lib/omni_agent/version.rb
95
108
  - lib/tasks/omni_agent_eval_tasks.rake
109
+ - lib/tasks/omni_agent_mcp_tasks.rake
96
110
  homepage: https://github.com/ACR1209/omni_agent
97
111
  licenses:
98
112
  - MIT