activeagent 0.5.0rc3 → 0.6.0rc1

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.
@@ -4,11 +4,339 @@ require_relative "open_ai_provider"
4
4
  module ActiveAgent
5
5
  module GenerationProvider
6
6
  class OpenRouterProvider < OpenAIProvider
7
+ # Vision-capable models on OpenRouter
8
+ VISION_MODELS = [
9
+ "openai/gpt-4-vision-preview",
10
+ "openai/gpt-4o",
11
+ "openai/gpt-4o-mini",
12
+ "anthropic/claude-3-5-sonnet",
13
+ "anthropic/claude-3-opus",
14
+ "anthropic/claude-3-sonnet",
15
+ "anthropic/claude-3-haiku",
16
+ "google/gemini-pro-1.5",
17
+ "google/gemini-pro-vision"
18
+ ].freeze
19
+
20
+ # Models that support structured output
21
+ STRUCTURED_OUTPUT_MODELS = [
22
+ "openai/gpt-4o",
23
+ "openai/gpt-4o-2024-08-06",
24
+ "openai/gpt-4o-mini",
25
+ "openai/gpt-4o-mini-2024-07-18",
26
+ "openai/gpt-4-turbo",
27
+ "openai/gpt-4-turbo-2024-04-09",
28
+ "openai/gpt-3.5-turbo-0125",
29
+ "openai/gpt-3.5-turbo-1106"
30
+ ].freeze
31
+
7
32
  def initialize(config)
8
33
  @config = config
9
- @access_token ||= config["api_key"] || config["access_token"] || ENV["OPENROUTER_API_KEY"] || ENV["OPENROUTER_ACCESS_TOKEN"]
34
+ @access_token = config["api_key"] || config["access_token"] ||
35
+ ENV["OPENROUTER_API_KEY"] || ENV["OPENROUTER_ACCESS_TOKEN"]
10
36
  @model_name = config["model"]
11
- @client = OpenAI::Client.new(uri_base: "https://openrouter.ai/api/v1", access_token: @access_token, log_errors: true)
37
+
38
+ # OpenRouter-specific configuration
39
+ @app_name = config["app_name"] || default_app_name
40
+ @site_url = config["site_url"] || default_site_url
41
+ @enable_fallbacks = config["enable_fallbacks"] != false
42
+ @fallback_models = config["fallback_models"] || []
43
+ @transforms = config["transforms"] || []
44
+ @provider_preferences = config["provider"] || {}
45
+ @track_costs = config["track_costs"] != false
46
+ @route = config["route"] || "fallback"
47
+
48
+ # Data collection preference (allow, deny, or specific provider list)
49
+ @data_collection = config["data_collection"] || @provider_preferences["data_collection"] || "allow"
50
+
51
+ # Initialize OpenAI client with OpenRouter base URL
52
+ @client = OpenAI::Client.new(
53
+ uri_base: "https://openrouter.ai/api/v1",
54
+ access_token: @access_token,
55
+ log_errors: true,
56
+ default_headers: openrouter_headers
57
+ )
58
+ end
59
+
60
+ def generate(prompt)
61
+ @prompt = prompt
62
+
63
+ with_error_handling do
64
+ parameters = build_openrouter_parameters
65
+ response = execute_with_fallback(parameters)
66
+ process_openrouter_response(response)
67
+ end
68
+ rescue => e
69
+ handle_openrouter_error(e)
70
+ end
71
+
72
+ # Helper methods for checking model capabilities
73
+ def supports_vision?(model = @model_name)
74
+ VISION_MODELS.include?(model)
75
+ end
76
+
77
+ def supports_structured_output?(model = @model_name)
78
+ STRUCTURED_OUTPUT_MODELS.include?(model)
79
+ end
80
+
81
+ protected
82
+
83
+ def build_provider_parameters
84
+ # Start with base OpenAI parameters
85
+ params = super
86
+
87
+ # Add OpenRouter-specific parameters
88
+ add_openrouter_params(params)
89
+ end
90
+
91
+ private
92
+
93
+ def default_app_name
94
+ if defined?(Rails) && Rails.application
95
+ Rails.application.class.name.split("::").first
96
+ else
97
+ "ActiveAgent"
98
+ end
99
+ end
100
+
101
+ def default_site_url
102
+ # First check ActiveAgent config
103
+ return config["default_url_options"]["host"] if config.dig("default_url_options", "host")
104
+
105
+ # Then check Rails routes default_url_options
106
+ if defined?(Rails) && Rails.application&.routes&.default_url_options&.any?
107
+ host = Rails.application.routes.default_url_options[:host]
108
+ port = Rails.application.routes.default_url_options[:port]
109
+ protocol = Rails.application.routes.default_url_options[:protocol] || "https"
110
+
111
+ if host
112
+ url = "#{protocol}://#{host}"
113
+ url += ":#{port}" if port && port != 80 && port != 443
114
+ return url
115
+ end
116
+ end
117
+
118
+ # Finally check ActionMailer config as fallback
119
+ if defined?(Rails) && Rails.application&.config&.action_mailer&.default_url_options
120
+ options = Rails.application.config.action_mailer.default_url_options
121
+ host = options[:host]
122
+ port = options[:port]
123
+ protocol = options[:protocol] || "https"
124
+
125
+ if host
126
+ url = "#{protocol}://#{host}"
127
+ url += ":#{port}" if port && port != 80 && port != 443
128
+ return url
129
+ end
130
+ end
131
+
132
+ nil
133
+ end
134
+
135
+ def openrouter_headers
136
+ headers = {}
137
+ headers["HTTP-Referer"] = @site_url if @site_url
138
+ headers["X-Title"] = @app_name if @app_name
139
+ headers
140
+ end
141
+
142
+ def build_openrouter_parameters
143
+ parameters = prompt_parameters
144
+
145
+ # Handle multiple models for fallback
146
+ if @fallback_models.present?
147
+ parameters[:models] = [ @model_name ] + @fallback_models
148
+ parameters[:route] = @route
149
+ end
150
+
151
+ # Add transforms if specified
152
+ parameters[:transforms] = @transforms if @transforms.present?
153
+
154
+ # Add provider preferences (always include if we have data_collection or other settings)
155
+ # Check both configured and runtime data_collection values
156
+ runtime_data_collection = prompt&.options&.key?(:data_collection)
157
+ if @provider_preferences.present? || @data_collection != "allow" || runtime_data_collection
158
+ parameters[:provider] = build_provider_preferences
159
+ end
160
+
161
+ parameters
162
+ end
163
+
164
+ def build_provider_preferences
165
+ prefs = {}
166
+ prefs[:order] = @provider_preferences["order"] if @provider_preferences["order"]
167
+ prefs[:require_parameters] = @provider_preferences["require_parameters"] if @provider_preferences.key?("require_parameters")
168
+ prefs[:allow_fallbacks] = @enable_fallbacks
169
+
170
+ # Data collection can be:
171
+ # - "allow" (default): Allow all providers to collect data
172
+ # - "deny": Deny all providers from collecting data
173
+ # - Array of provider names: Only allow these providers to collect data
174
+ # Check prompt options first (runtime override), then fall back to configured value
175
+ data_collection = prompt.options[:data_collection] if prompt&.options&.key?(:data_collection)
176
+ data_collection ||= @data_collection
177
+ prefs[:data_collection] = data_collection
178
+
179
+ prefs.compact
180
+ end
181
+
182
+ def add_openrouter_params(params)
183
+ # Add OpenRouter-specific routing parameters
184
+ if @enable_fallbacks && @fallback_models.present?
185
+ params[:models] = [ @model_name ] + @fallback_models
186
+ params[:route] = @route
187
+ end
188
+
189
+ # Add transforms
190
+ params[:transforms] = @transforms if @transforms.present?
191
+
192
+ # Add provider configuration (always include if we have data_collection or other settings)
193
+ # Check both configured and runtime data_collection values
194
+ runtime_data_collection = prompt&.options&.key?(:data_collection)
195
+ if @provider_preferences.present? || @data_collection != "allow" || runtime_data_collection
196
+ params[:provider] = build_provider_preferences
197
+ end
198
+
199
+ # Add plugins (e.g., for PDF processing)
200
+ if prompt.options[:plugins].present?
201
+ params[:plugins] = prompt.options[:plugins]
202
+ end
203
+
204
+ params
205
+ end
206
+
207
+ def execute_with_fallback(parameters)
208
+ parameters[:stream] = provider_stream if prompt.options[:stream] || config["stream"]
209
+
210
+ response = @client.chat(parameters: parameters)
211
+
212
+ # Log if fallback was used
213
+ if response.respond_to?(:headers) && response.headers["x-model"] != @model_name
214
+ Rails.logger.info "[OpenRouter] Fallback model used: #{response.headers['x-model']}" if defined?(Rails)
215
+ end
216
+
217
+ response
218
+ end
219
+
220
+ def process_openrouter_response(response)
221
+ # Process as normal OpenAI response first
222
+ if prompt.options[:stream]
223
+ return @response
224
+ end
225
+
226
+ # Extract standard response
227
+ message_json = response.dig("choices", 0, "message")
228
+ message_json["id"] = response.dig("id") if message_json && message_json["id"].blank?
229
+ message = handle_message(message_json) if message_json
230
+
231
+ update_context(prompt: prompt, message: message, response: response) if message
232
+
233
+ # Create response with OpenRouter metadata
234
+ @response = ActiveAgent::GenerationProvider::Response.new(
235
+ prompt: prompt,
236
+ message: message,
237
+ raw_response: response
238
+ )
239
+
240
+ # OpenRouter includes provider and model info directly in the response body
241
+ if response["provider"] || response["model"]
242
+ @response.metadata = {
243
+ provider: response["provider"],
244
+ model_used: response["model"],
245
+ fallback_used: response["model"] != @model_name
246
+ }.compact
247
+ end
248
+
249
+ # Track costs if enabled
250
+ track_usage(response) if @track_costs && response["usage"]
251
+
252
+ @response
253
+ end
254
+
255
+ def add_openrouter_metadata(response, headers)
256
+ return unless response.respond_to?(:metadata=)
257
+
258
+ response.metadata = {
259
+ provider: headers["x-provider"],
260
+ model_used: headers["x-model"],
261
+ trace_id: headers["x-trace-id"],
262
+ fallback_used: headers["x-model"] != @model_name,
263
+ ratelimit: {
264
+ requests_limit: headers["x-ratelimit-requests-limit"],
265
+ requests_remaining: headers["x-ratelimit-requests-remaining"],
266
+ requests_reset: headers["x-ratelimit-requests-reset"],
267
+ tokens_limit: headers["x-ratelimit-tokens-limit"],
268
+ tokens_remaining: headers["x-ratelimit-tokens-remaining"],
269
+ tokens_reset: headers["x-ratelimit-tokens-reset"]
270
+ }.compact
271
+ }.compact
272
+ end
273
+
274
+ def track_usage(response)
275
+ return nil unless @track_costs
276
+ return nil unless response["usage"]
277
+
278
+ usage = response["usage"]
279
+ model = response.dig("model") || @model_name
280
+
281
+ # Calculate costs (simplified - would need actual pricing data)
282
+ cost_info = {
283
+ model: model,
284
+ prompt_tokens: usage["prompt_tokens"],
285
+ completion_tokens: usage["completion_tokens"],
286
+ total_tokens: usage["total_tokens"]
287
+ }
288
+
289
+ # Log usage information
290
+ if defined?(Rails)
291
+ Rails.logger.info "[OpenRouter] Usage: #{cost_info.to_json}"
292
+
293
+ # Store in cache if available
294
+ if Rails.cache
295
+ cache_key = "openrouter:usage:#{Date.current}"
296
+ Rails.cache.increment("#{cache_key}:tokens", usage["total_tokens"])
297
+ Rails.cache.increment("#{cache_key}:requests")
298
+ end
299
+ end
300
+
301
+ cost_info
302
+ end
303
+
304
+ def handle_openrouter_error(error)
305
+ error_message = error.message || error.to_s
306
+
307
+ case error_message
308
+ when /rate limit/i
309
+ handle_rate_limit_error(error)
310
+ when /insufficient credits|payment required/i
311
+ handle_insufficient_credits(error)
312
+ when /no available provider/i
313
+ handle_no_provider_error(error)
314
+ when /timeout/i
315
+ handle_timeout_error(error)
316
+ else
317
+ # Fall back to parent error handling
318
+ super(error) if defined?(super)
319
+ end
320
+ end
321
+
322
+ def handle_rate_limit_error(error)
323
+ Rails.logger.error "[OpenRouter] Rate limit exceeded: #{error.message}" if defined?(Rails)
324
+ raise GenerationProviderError, "OpenRouter rate limit exceeded. Please retry later."
325
+ end
326
+
327
+ def handle_insufficient_credits(error)
328
+ Rails.logger.error "[OpenRouter] Insufficient credits: #{error.message}" if defined?(Rails)
329
+ raise GenerationProviderError, "OpenRouter account has insufficient credits."
330
+ end
331
+
332
+ def handle_no_provider_error(error)
333
+ Rails.logger.error "[OpenRouter] No available provider: #{error.message}" if defined?(Rails)
334
+ raise GenerationProviderError, "No available provider for the requested model."
335
+ end
336
+
337
+ def handle_timeout_error(error)
338
+ Rails.logger.error "[OpenRouter] Request timeout: #{error.message}" if defined?(Rails)
339
+ raise GenerationProviderError, "OpenRouter request timed out."
12
340
  end
13
341
  end
14
342
  end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module GenerationProvider
5
+ module ParameterBuilder
6
+ extend ActiveSupport::Concern
7
+
8
+ def prompt_parameters(overrides = {})
9
+ base_params = build_base_parameters
10
+ provider_params = build_provider_parameters
11
+
12
+ # Merge parameters with proper precedence:
13
+ # 1. Overrides (highest priority)
14
+ # 2. Prompt options
15
+ # 3. Provider-specific parameters
16
+ # 4. Base parameters (lowest priority)
17
+ base_params
18
+ .merge(provider_params)
19
+ .merge(extract_prompt_options)
20
+ .merge(overrides)
21
+ .compact
22
+ end
23
+
24
+ protected
25
+
26
+ def build_base_parameters
27
+ {
28
+ model: determine_model,
29
+ messages: provider_messages(@prompt.messages),
30
+ temperature: determine_temperature
31
+ }.tap do |params|
32
+ # Add optional parameters if present
33
+ params[:max_tokens] = determine_max_tokens if determine_max_tokens
34
+ params[:tools] = format_tools(@prompt.actions) if @prompt.actions.present?
35
+ end
36
+ end
37
+
38
+ def build_provider_parameters
39
+ # Override in provider for specific parameters
40
+ # For example, Anthropic needs 'system' parameter instead of system message
41
+ {}
42
+ end
43
+
44
+ def extract_prompt_options
45
+ # Extract relevant options from prompt
46
+ options = {}
47
+
48
+ # Common options that map directly
49
+ [ :stream, :top_p, :frequency_penalty, :presence_penalty, :seed, :stop, :user ].each do |key|
50
+ options[key] = @prompt.options[key] if @prompt.options.key?(key)
51
+ end
52
+
53
+ # Handle response format for structured output
54
+ if @prompt.output_schema.present?
55
+ options[:response_format] = build_response_format
56
+ end
57
+
58
+ options
59
+ end
60
+
61
+ def determine_model
62
+ @prompt.options[:model] || @model_name || @config["model"]
63
+ end
64
+
65
+ def determine_temperature
66
+ @prompt.options[:temperature] || @config["temperature"] || 0.7
67
+ end
68
+
69
+ def determine_max_tokens
70
+ @prompt.options[:max_tokens] || @config["max_tokens"]
71
+ end
72
+
73
+ def build_response_format
74
+ # Default OpenAI-style response format
75
+ # Override in provider for different formats
76
+ {
77
+ type: "json_schema",
78
+ json_schema: @prompt.output_schema
79
+ }
80
+ end
81
+
82
+ # Embedding-specific parameters
83
+ def embeddings_parameters(input: nil, model: nil, **options)
84
+ {
85
+ model: model || determine_embedding_model,
86
+ input: input || format_embedding_input,
87
+ dimensions: options[:dimensions] || @config["embedding_dimensions"],
88
+ encoding_format: options[:encoding_format] || "float"
89
+ }.compact
90
+ end
91
+
92
+ def determine_embedding_model
93
+ @prompt.options[:embedding_model] || @config["embedding_model"] || "text-embedding-3-small"
94
+ end
95
+
96
+ def format_embedding_input
97
+ # Handle single or batch embedding inputs
98
+ if @prompt.message
99
+ @prompt.message.content
100
+ elsif @prompt.messages
101
+ @prompt.messages.map(&:content)
102
+ else
103
+ nil
104
+ end
105
+ end
106
+
107
+ module ClassMethods
108
+ # Class-level configuration for default parameters
109
+ def default_parameters(params = {})
110
+ @default_parameters = params
111
+ end
112
+
113
+ def get_default_parameters
114
+ @default_parameters || {}
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -4,11 +4,13 @@ module ActiveAgent
4
4
  module GenerationProvider
5
5
  class Response
6
6
  attr_reader :message, :prompt, :raw_response
7
+ attr_accessor :metadata
7
8
 
8
- def initialize(prompt:, message: nil, raw_response: nil)
9
+ def initialize(prompt:, message: nil, raw_response: nil, metadata: nil)
9
10
  @prompt = prompt
10
11
  @message = message || prompt.message
11
12
  @raw_response = raw_response
13
+ @metadata = metadata || {}
12
14
  end
13
15
 
14
16
  # Extract usage statistics from the raw response
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module GenerationProvider
5
+ module StreamProcessing
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_accessor :stream_buffer, :stream_context
10
+ end
11
+
12
+ def provider_stream
13
+ agent_stream = prompt.options[:stream]
14
+ message = initialize_stream_message
15
+
16
+ @response = ActiveAgent::GenerationProvider::Response.new(prompt:, message:)
17
+
18
+ proc do |chunk|
19
+ process_stream_chunk(chunk, message, agent_stream)
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def initialize_stream_message
26
+ ActiveAgent::ActionPrompt::Message.new(content: "", role: :assistant)
27
+ end
28
+
29
+ def process_stream_chunk(chunk, message, agent_stream)
30
+ # Default implementation - must be overridden in provider
31
+ raise NotImplementedError, "Providers must implement process_stream_chunk"
32
+ end
33
+
34
+ def handle_stream_delta(delta, message, agent_stream)
35
+ # Common delta handling logic
36
+ new_content = extract_content_from_delta(delta)
37
+ if new_content && !new_content.blank?
38
+ message.content += new_content
39
+ agent_stream&.call(message, new_content, false, prompt.action_name)
40
+ end
41
+ end
42
+
43
+ def extract_content_from_delta(delta)
44
+ # Default extraction - override if needed
45
+ delta if delta.is_a?(String)
46
+ end
47
+
48
+ def handle_tool_stream_chunk(chunk, message, agent_stream)
49
+ # Handle tool calls in streaming
50
+ # Override in provider for specific implementation
51
+ end
52
+
53
+ def finalize_stream(message, agent_stream)
54
+ agent_stream&.call(message, nil, true, prompt.action_name)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module GenerationProvider
5
+ module ToolManagement
6
+ extend ActiveSupport::Concern
7
+
8
+ def format_tools(tools)
9
+ return nil if tools.blank?
10
+
11
+ tools.map do |tool|
12
+ format_single_tool(tool)
13
+ end
14
+ end
15
+
16
+ def handle_actions(tool_calls)
17
+ return [] if tool_calls.nil? || tool_calls.empty?
18
+
19
+ tool_calls.map do |tool_call|
20
+ parse_tool_call(tool_call)
21
+ end.compact
22
+ end
23
+
24
+ protected
25
+
26
+ def format_single_tool(tool)
27
+ # Default implementation for OpenAI-style tools
28
+ if tool["function"] || tool[:function]
29
+ # Tool already has the correct structure
30
+ tool
31
+ else
32
+ # Legacy format - wrap in function structure
33
+ wrap_tool_in_function(tool)
34
+ end
35
+ end
36
+
37
+ def wrap_tool_in_function(tool)
38
+ {
39
+ type: "function",
40
+ function: {
41
+ name: tool["name"] || tool[:name],
42
+ description: tool["description"] || tool[:description],
43
+ parameters: tool["parameters"] || tool[:parameters]
44
+ }
45
+ }
46
+ end
47
+
48
+ def parse_tool_call(tool_call)
49
+ # Skip if no function information
50
+ return nil if tool_call.nil?
51
+
52
+ # Extract tool information based on provider format
53
+ tool_id = extract_tool_id(tool_call)
54
+ tool_name = extract_tool_name(tool_call)
55
+ tool_params = extract_tool_params(tool_call)
56
+
57
+ # Skip if no name found
58
+ return nil if tool_name.blank?
59
+
60
+ ActiveAgent::ActionPrompt::Action.new(
61
+ id: tool_id,
62
+ name: tool_name,
63
+ params: tool_params
64
+ )
65
+ end
66
+
67
+ def extract_tool_id(tool_call)
68
+ tool_call["id"] || tool_call[:id]
69
+ end
70
+
71
+ def extract_tool_name(tool_call)
72
+ # Try different paths for tool name
73
+ tool_call.dig("function", "name") ||
74
+ tool_call.dig(:function, :name) ||
75
+ tool_call["name"] ||
76
+ tool_call[:name]
77
+ end
78
+
79
+ def extract_tool_params(tool_call)
80
+ # Try different paths for tool parameters/arguments
81
+ args = tool_call.dig("function", "arguments") ||
82
+ tool_call.dig(:function, :arguments) ||
83
+ tool_call["arguments"] ||
84
+ tool_call[:arguments] ||
85
+ tool_call["input"] ||
86
+ tool_call[:input]
87
+
88
+ return nil if args.blank?
89
+
90
+ # Parse JSON string if needed
91
+ if args.is_a?(String)
92
+ begin
93
+ JSON.parse(args, symbolize_names: true)
94
+ rescue JSON::ParserError
95
+ nil
96
+ end
97
+ else
98
+ args
99
+ end
100
+ end
101
+
102
+ # Provider-specific tool format methods
103
+ # Override these in specific providers
104
+
105
+ def format_tools_for_anthropic(tools)
106
+ # Anthropic-specific tool format
107
+ tools.map do |tool|
108
+ {
109
+ name: extract_tool_name_from_schema(tool),
110
+ description: extract_tool_description_from_schema(tool),
111
+ input_schema: extract_tool_parameters_from_schema(tool)
112
+ }
113
+ end
114
+ end
115
+
116
+ def format_tools_for_openai(tools)
117
+ # OpenAI-specific tool format (default)
118
+ format_tools(tools)
119
+ end
120
+
121
+ private
122
+
123
+ def extract_tool_name_from_schema(tool)
124
+ tool["name"] || tool[:name] ||
125
+ tool.dig("function", "name") || tool.dig(:function, "name") ||
126
+ tool.dig("function", :name) || tool.dig(:function, :name)
127
+ end
128
+
129
+ def extract_tool_description_from_schema(tool)
130
+ tool["description"] || tool[:description] ||
131
+ tool.dig("function", "description") || tool.dig(:function, "description") ||
132
+ tool.dig("function", :description) || tool.dig(:function, :description)
133
+ end
134
+
135
+ def extract_tool_parameters_from_schema(tool)
136
+ tool["parameters"] || tool[:parameters] ||
137
+ tool.dig("function", "parameters") || tool.dig(:function, "parameters") ||
138
+ tool.dig("function", :parameters) || tool.dig(:function, :parameters)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -14,7 +14,6 @@ module ActiveAgent
14
14
  module ClassMethods
15
15
  def configuration(name_or_provider, **options)
16
16
  config = ActiveAgent.config[name_or_provider.to_s] || ActiveAgent.config.dig(ENV["RAILS_ENV"], name_or_provider.to_s) || {}
17
-
18
17
  config = { "service" => "OpenAI" } if config.empty? && name_or_provider == :openai
19
18
  config.merge!(options)
20
19
  raise "Failed to load provider #{name_or_provider}: configuration not found for provider" if config["service"].nil?
@@ -26,6 +25,7 @@ module ActiveAgent
26
25
  def configure_provider(config)
27
26
  service_name = config["service"]
28
27
  require "active_agent/generation_provider/#{service_name.underscore}_provider"
28
+
29
29
  ActiveAgent::GenerationProvider.const_get("#{service_name.camelize}Provider").new(config)
30
30
  end
31
31