active_genie 0.30.3 → 0.30.8

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/VERSION +1 -1
  4. data/lib/active_genie/comparator/debate.rb +13 -36
  5. data/lib/active_genie/comparator/fight.rb +3 -3
  6. data/lib/active_genie/comparator.rb +0 -2
  7. data/lib/active_genie/configs/base_config.rb +25 -0
  8. data/lib/active_genie/configs/extractor_config.rb +6 -14
  9. data/lib/active_genie/configs/lister_config.rb +6 -10
  10. data/lib/active_genie/configs/llm_config.rb +12 -26
  11. data/lib/active_genie/configs/log_config.rb +19 -16
  12. data/lib/active_genie/configs/providers/anthropic_config.rb +16 -1
  13. data/lib/active_genie/configs/providers/deepseek_config.rb +8 -2
  14. data/lib/active_genie/configs/providers/google_config.rb +8 -2
  15. data/lib/active_genie/configs/providers/openai_config.rb +8 -2
  16. data/lib/active_genie/configs/providers/provider_base.rb +22 -31
  17. data/lib/active_genie/configs/providers_config.rb +35 -16
  18. data/lib/active_genie/configs/ranker_config.rb +6 -12
  19. data/lib/active_genie/configuration.rb +23 -60
  20. data/lib/active_genie/{ranker/entities → entities}/player.rb +4 -0
  21. data/lib/active_genie/{ranker/entities → entities}/players.rb +1 -1
  22. data/lib/active_genie/entities/result.rb +29 -0
  23. data/lib/active_genie/errors/provider_server_error.rb +8 -5
  24. data/lib/active_genie/errors/without_available_provider_error.rb +39 -0
  25. data/lib/active_genie/extractor/data.json +9 -0
  26. data/lib/active_genie/extractor/data.prompt.md +12 -0
  27. data/lib/active_genie/extractor/data.rb +71 -0
  28. data/lib/active_genie/extractor/explanation.rb +19 -47
  29. data/lib/active_genie/extractor/litote.rb +8 -14
  30. data/lib/active_genie/extractor.rb +5 -0
  31. data/lib/active_genie/lister/feud.json +5 -1
  32. data/lib/active_genie/lister/feud.rb +10 -24
  33. data/lib/active_genie/lister/juries.rb +14 -20
  34. data/lib/active_genie/logger.rb +16 -28
  35. data/lib/active_genie/providers/anthropic_provider.rb +11 -5
  36. data/lib/active_genie/providers/base_provider.rb +15 -17
  37. data/lib/active_genie/providers/deepseek_provider.rb +17 -9
  38. data/lib/active_genie/providers/google_provider.rb +10 -4
  39. data/lib/active_genie/providers/openai_provider.rb +6 -4
  40. data/lib/active_genie/providers/unified_provider.rb +47 -17
  41. data/lib/active_genie/ranker/elo.rb +41 -36
  42. data/lib/active_genie/ranker/free_for_all.rb +45 -28
  43. data/lib/active_genie/ranker/scoring.rb +20 -11
  44. data/lib/active_genie/ranker/tournament.rb +23 -35
  45. data/lib/active_genie/scorer/jury_bench.rb +15 -29
  46. data/lib/active_genie/utils/base_module.rb +34 -0
  47. data/lib/active_genie/utils/call_wrapper.rb +20 -0
  48. data/lib/active_genie/utils/deep_merge.rb +12 -0
  49. data/lib/active_genie/utils/fiber_by_batch.rb +2 -2
  50. data/lib/active_genie/utils/text_case.rb +18 -0
  51. data/lib/active_genie.rb +16 -18
  52. data/lib/tasks/test.rake +61 -3
  53. metadata +19 -8
  54. data/lib/active_genie/configs/comparator_config.rb +0 -10
  55. data/lib/active_genie/configs/scorer_config.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0305b370de6af340e3ae6d3afa10b67f7be5f68ba20916c2fb3b8c934c40fc3e
4
- data.tar.gz: ab3ac5ed4b7bd14c53ceeff02240953b7427260e94630803dffd8669d5a5e051
3
+ metadata.gz: 02e46bc41ee34451323202588e8018cca0f7497529f9c7f6ce64c0e00e009f7e
4
+ data.tar.gz: 4a78b8402fee546b0fd6fef3352cd85d476399167edf051798ec71709c46bb7b
5
5
  SHA512:
6
- metadata.gz: 84c7f399b16c21599e6302aeb8c4243cd833dc7ab554bbc610d400d982e7e9cb48cdc5b626cd74897dfa235a97f4a48115110275e8761bfe2be2a8964b040890
7
- data.tar.gz: 705f57b32a5f59b2b8d88650e3b5eb7be657e577b571e74709a53c5b60aee5bf38740621d901fa72342a7d89e113982173d6ab1120ecc76c105b0589bd8910db
6
+ metadata.gz: 2e606f1827f1b88e67dea196d56173a5dd4c5ff51861a77942faebef38fdd6f252e3077c9ca2bc8585769ab4f873344dea0abebb88d19af1f40493aac9e7374d
7
+ data.tar.gz: 4beac5e264438b7c9eb692669ff16940e140251cc8e87b35e61fbef8eb3fdf604acc2966ed021705bca63a9ff90cd63312abe25143b68364a67d92becd113d58
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # ActiveGenie 🧞‍♂️
2
- > The Lodash for GenAI: Real Value + Consistent + Model-Agnostic
2
+ > The Lodash for GenAI: Consistent + Model-Agnostic
3
3
 
4
4
  [![Gem Version](https://badge.fury.io/rb/active_genie.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/active_genie)
5
5
  [![Ruby](https://github.com/roriz/active_genie/actions/workflows/benchmark.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/benchmark.yml)
6
6
 
7
- ActiveGenie is a developer-first library for GenAI workflows, designed to help you extract, compare, score, and rank with consistency and model-agnostic. Think of it as the Lodash for GenAI: built for real value, consistent results, and freedom from vendor lock-in. It solves the biggest pain in GenAI today: getting predictable, trustworthy answers across use cases, models, and providers.
8
-
9
- Behind the scenes, a custom benchmarking system keeps everything consistent across LLM vendors and versions, release after release.
7
+ **ActiveGenie** is an **enabler for creating reliable GenAI features**, offering powerful, **model-agnostic tools** across any provider. It allows you to settle subjective comparisons with a `ActibeGenie::Comparator` module that stages a political debate, get accurate scores from an **AI jury** using `ActiveGenie::Scorer`, and **rank large datasets** using `ActiveGenie::Ranker`'s tournament-style system.
8
+ This reliability is built on three core pillars:
9
+ * **Custom Benchmarking:** Testing for consistency with every new version and model update.
10
+ * **Reasoning Prompting:** Utilizing human reasoning techniques (like debate and jury review) to control a model's reasoning.
11
+ * **Overfitting Prompts:** Highly specialized, and potentially model-specific, prompt for each module's purpose.
10
12
 
11
13
  For full documentation, visit [activegenie.ai](https://activegenie.ai).
12
14
 
@@ -57,7 +59,7 @@ schema = {
57
59
  }
58
60
  }
59
61
 
60
- result = ActiveGenie::DataExtractor.call(
62
+ result = ActiveGenie::Extractor.call(
61
63
  text,
62
64
  schema,
63
65
  config: { provider_name: :openai, model: 'gpt-4.1-mini' } # optional
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.30.3
1
+ 0.30.8
@@ -14,11 +14,7 @@ module ActiveGenie
14
14
  # @example Debate usage with two players and criteria
15
15
  # Debate.call("Player A content", "Player B content", "Evaluate keyword usage and pattern matching")
16
16
  #
17
- class Debate
18
- def self.call(...)
19
- new(...).call
20
- end
21
-
17
+ class Debate < ActiveGenie::BaseModule
22
18
  # @param player_a [String] The content or submission from the first player
23
19
  # @param player_b [String] The content or submission from the second player
24
20
  # @param criteria [String] The evaluation criteria or rules to assess against
@@ -31,7 +27,7 @@ module ActiveGenie
31
27
  @player_a = player_a
32
28
  @player_b = player_b
33
29
  @criteria = criteria
34
- @initial_config = config
30
+ super(config:)
35
31
  end
36
32
 
37
33
  # @return [ComparatorResponse] The evaluation result containing the winner and reasoning
@@ -43,9 +39,9 @@ module ActiveGenie
43
39
  { role: 'user', content: "criteria: #{@criteria}" }
44
40
  ]
45
41
 
46
- response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(messages, FUNCTION, config:)
42
+ provider_response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(messages, FUNCTION, config:)
47
43
 
48
- response_formatted(response)
44
+ response_formatted(provider_response)
49
45
  end
50
46
 
51
47
  PROMPT = File.read(File.join(__dir__, 'debate.prompt.md'))
@@ -53,37 +49,18 @@ module ActiveGenie
53
49
 
54
50
  private
55
51
 
56
- def response_formatted(response)
57
- winner, loser = case response['impartial_judge_winner']
58
- when 'player_a' then [@player_a, @player_b]
59
- when 'player_b' then [@player_b, @player_a]
60
- end
61
- reasoning = response['impartial_judge_winner_reasoning']
62
-
63
- comparator_response = ActiveGenie::Comparator::ComparatorResponse.new(winner:, loser:, reasoning:,
64
- raw: response)
65
- log_comparator(comparator_response)
52
+ def response_formatted(provider_response)
53
+ winner, = case provider_response['impartial_judge_winner']
54
+ when 'player_a' then [@player_a, @player_b]
55
+ when 'player_b' then [@player_b, @player_a]
56
+ end
57
+ reasoning = provider_response['impartial_judge_winner_reasoning']
66
58
 
67
- comparator_response
59
+ ActiveGenie::Result.new(data: winner, reasoning:, metadata: provider_response)
68
60
  end
69
61
 
70
- def log_comparator(comparator_response)
71
- config.logger.call(
72
- code: :comparator,
73
- player_a: @player_a[0..30],
74
- player_b: @player_b[0..30],
75
- criteria: @criteria[0..30],
76
- **comparator_response.to_h
77
- )
78
- end
79
-
80
- def config
81
- @config ||= begin
82
- c = ActiveGenie.configuration.merge(@initial_config)
83
- c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
84
-
85
- c
86
- end
62
+ def module_config
63
+ { llm: { recommended_model: 'claude-haiku-4-5' } }
87
64
  end
88
65
  end
89
66
  end
@@ -21,13 +21,13 @@ module ActiveGenie
21
21
  { role: 'user', content: "criteria: #{@criteria}" }
22
22
  ]
23
23
 
24
- response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
24
+ provider_response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
25
25
  messages,
26
26
  FUNCTION,
27
- config: @config
27
+ config:
28
28
  )
29
29
 
30
- response_formatted(response)
30
+ response_formatted(provider_response)
31
31
  end
32
32
 
33
33
  PROMPT = File.read(File.join(__dir__, 'fight.prompt.md'))
@@ -7,8 +7,6 @@ module ActiveGenie
7
7
  module Comparator
8
8
  module_function
9
9
 
10
- ComparatorResponse = Struct.new(:winner, :loser, :reasoning, :raw, keyword_init: true)
11
-
12
10
  def call(...)
13
11
  Debate.call(...)
14
12
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveGenie
4
+ module Config
5
+ class BaseConfig
6
+ def initialize(**args)
7
+ attributes.each do |var|
8
+ send("#{var}=", args[var] || args[var.to_s])
9
+ end
10
+ end
11
+
12
+ def attributes
13
+ public_methods(false).grep(/=$/).map { |m| m.to_s.delete('=').to_sym }
14
+ end
15
+
16
+ def to_h
17
+ h = {}
18
+ attributes.each do |var|
19
+ h[var.to_sym] = send(var)
20
+ end
21
+ h
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,22 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_config'
4
+
3
5
  module ActiveGenie
4
6
  module Config
5
- class ExtractorConfig
6
- attr_accessor :with_explanation, :min_accuracy, :verbose
7
-
8
- def initialize
9
- @with_explanation = true
10
- @min_accuracy = 70
11
- @verbose = false
12
- end
7
+ class ExtractorConfig < BaseConfig
8
+ attr_writer :min_accuracy
13
9
 
14
- def merge(config_params = {})
15
- dup.tap do |config|
16
- config.with_explanation = config_params[:with_explanation] if config_params.key?(:with_explanation)
17
- config.min_accuracy = config_params[:min_accuracy] if config_params.key?(:min_accuracy)
18
- config.verbose = config_params[:verbose] if config_params.key?(:verbose)
19
- end
10
+ def min_accuracy
11
+ @min_accuracy ||= 70
20
12
  end
21
13
  end
22
14
  end
@@ -1,18 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_config'
4
+
3
5
  module ActiveGenie
4
6
  module Config
5
- class ListerConfig
6
- attr_accessor :number_of_items
7
-
8
- def initialize
9
- @number_of_items = 5
10
- end
7
+ class ListerConfig < BaseConfig
8
+ attr_writer :number_of_items
11
9
 
12
- def merge(config_params = {})
13
- dup.tap do |config|
14
- config.number_of_items = config_params[:number_of_items] if config_params.key?(:number_of_items)
15
- end
10
+ def number_of_items
11
+ @number_of_items ||= 5
16
12
  end
17
13
  end
18
14
  end
@@ -1,39 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_config'
4
+
3
5
  module ActiveGenie
4
6
  module Config
5
- class LlmConfig
6
- attr_accessor :model, :recommended_model, :temperature, :max_tokens, :max_retries, :retry_delay,
7
- :model_tier, :read_timeout, :open_timeout, :provider, :max_fibers
7
+ class LlmConfig < BaseConfig
8
+ attr_accessor :model, :recommended_model, :max_retries, :retry_delay,
9
+ :read_timeout, :open_timeout
10
+ attr_writer :temperature, :max_fibers, :max_tokens
8
11
  attr_reader :provider_name
9
12
 
10
- def initialize
11
- set_defaults
13
+ def temperature
14
+ @temperature ||= 0
12
15
  end
13
16
 
14
- def provider_name=(provider_name)
15
- return if provider_name.nil? || provider_name.empty?
16
-
17
- @provider_name = provider_name.to_s.downcase.strip.to_sym
17
+ def max_fibers
18
+ @max_fibers ||= 10
18
19
  end
19
20
 
20
- def merge(config_params = {})
21
- dup.tap do |config|
22
- config_params.each do |key, value|
23
- config.send("#{key}=", value) if config.respond_to?("#{key}=")
24
- end
25
- end
26
- end
27
-
28
- private
29
-
30
- def set_defaults
31
- @model = @recommended_model = @provider_name = @provider = nil
32
- @max_retries = @retry_delay = @read_timeout = @open_timeout = nil
33
- @temperature = 0
34
- @max_tokens = 4096
35
- @model_tier = 'lower_tier'
36
- @max_fibers = 10
21
+ def max_tokens
22
+ @max_tokens ||= 4096
37
23
  end
38
24
  end
39
25
  end
@@ -1,28 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_config'
4
+
3
5
  module ActiveGenie
4
6
  module Config
5
- class LogConfig
7
+ class LogConfig < BaseConfig
8
+ attr_reader :output
6
9
  attr_writer :file_path, :fine_tune_file_path
7
- attr_reader :output, :observers
8
10
 
9
11
  def file_path
10
- @file_path || 'log/active_genie.log'
12
+ @file_path ||= 'log/active_genie.log'
11
13
  end
12
14
 
13
15
  def fine_tune_file_path
14
- @fine_tune_file_path || 'log/active_genie_fine_tune.log'
16
+ @fine_tune_file_path ||= 'log/active_genie_fine_tune.log'
17
+ end
18
+
19
+ def observers=(observers)
20
+ Array(observers).each do |observer|
21
+ add_observer(observers: [observer[:observer]], scope: observer[:scope] || {})
22
+ end
23
+ end
24
+
25
+ def observers
26
+ @observers ||= []
15
27
  end
16
28
 
17
29
  def additional_context
18
- @additional_context || {}
30
+ @additional_context ||= {}
19
31
  end
20
32
 
21
33
  def additional_context=(context)
22
- @additional_context = additional_context.merge(context).compact
34
+ @additional_context = additional_context.merge(context || {}).compact
23
35
  end
24
36
 
25
37
  def output=(output)
38
+ return if output.nil?
26
39
  raise InvalidLogOutputError, output unless output.respond_to?(:call)
27
40
 
28
41
  @output = output
@@ -50,16 +63,6 @@ module ActiveGenie
50
63
  def clear_observers
51
64
  @observers = []
52
65
  end
53
-
54
- def merge(config_params = {})
55
- dup.tap do |config|
56
- config_params.compact.each do |key, value|
57
- config.send("#{key}=", value) if config.respond_to?("#{key}=")
58
- end
59
-
60
- config.add_observer(config_params[:observers]) if config_params[:observers]
61
- end
62
- end
63
66
  end
64
67
  end
65
68
  end
@@ -8,7 +8,10 @@ module ActiveGenie
8
8
  # Configuration class for the Anthropic API client.
9
9
  # Manages API keys, URLs, model selections, and client instantiation.
10
10
  class AnthropicConfig < ProviderBase
11
- NAME = :anthropic
11
+ def initialize(anthropic_version: nil, **args)
12
+ @anthropic_version = anthropic_version
13
+ super(**args)
14
+ end
12
15
 
13
16
  # Retrieves the API key.
14
17
  # Falls back to the ANTHROPIC_API_KEY environment variable if not set.
@@ -30,6 +33,18 @@ module ActiveGenie
30
33
  def anthropic_version
31
34
  @anthropic_version || '2023-06-01'
32
35
  end
36
+
37
+ def default_model
38
+ @default_model || 'claude-3-5-haiku-20241022'
39
+ end
40
+
41
+ def valid_model?(model)
42
+ model.include?('claude')
43
+ end
44
+
45
+ def to_h
46
+ super.merge({ anthropic_version: })
47
+ end
33
48
  end
34
49
  end
35
50
  end
@@ -8,8 +8,6 @@ module ActiveGenie
8
8
  # Configuration class for the DeepSeek API client.
9
9
  # Manages API keys, organization IDs, URLs, model selections, and client instantiation.
10
10
  class DeepseekConfig < ProviderBase
11
- NAME = :deepseek
12
-
13
11
  # Retrieves the API key.
14
12
  # Falls back to the DEEPSEEK_API_KEY environment variable if not set.
15
13
  # @return [String, nil] The API key.
@@ -23,6 +21,14 @@ module ActiveGenie
23
21
  def api_url
24
22
  @api_url || 'https://api.deepseek.com/v1'
25
23
  end
24
+
25
+ def default_model
26
+ @default_model || 'deepseek-chat'
27
+ end
28
+
29
+ def valid_model?(model)
30
+ model.include?('deepseek')
31
+ end
26
32
  end
27
33
  end
28
34
  end
@@ -8,8 +8,6 @@ module ActiveGenie
8
8
  # Configuration class for the Google Generative Language API client.
9
9
  # Manages API keys, URLs, model selections, and client instantiation.
10
10
  class GoogleConfig < ProviderBase
11
- NAME = :google
12
-
13
11
  # Retrieves the API key.
14
12
  # Falls back to the GENERATIVE_LANGUAGE_GOOGLE_API_KEY environment variable if not set.
15
13
  # @return [String, nil] The API key.
@@ -25,6 +23,14 @@ module ActiveGenie
25
23
  # The base URL here should be just the domain part.
26
24
  @api_url || 'https://generativelanguage.googleapis.com'
27
25
  end
26
+
27
+ def default_model
28
+ @default_model || 'gemini-2.5-flash'
29
+ end
30
+
31
+ def valid_model?(model)
32
+ model.include?('gemini')
33
+ end
28
34
  end
29
35
  end
30
36
  end
@@ -8,8 +8,6 @@ module ActiveGenie
8
8
  # Configuration class for the OpenAI API client.
9
9
  # Manages API keys, organization IDs, URLs, model selections, and client instantiation.
10
10
  class OpenaiConfig < ProviderBase
11
- NAME = :openai
12
-
13
11
  # Retrieves the API key.
14
12
  # Falls back to the OPENAI_API_KEY environment variable if not set.
15
13
  # @return [String, nil] The API key.
@@ -23,6 +21,14 @@ module ActiveGenie
23
21
  def api_url
24
22
  @api_url || 'https://api.openai.com/v1'
25
23
  end
24
+
25
+ def default_model
26
+ @default_model || 'gpt-5-mini'
27
+ end
28
+
29
+ def valid_model?(model)
30
+ model.include?('gpt')
31
+ end
26
32
  end
27
33
  end
28
34
  end
@@ -4,47 +4,38 @@ module ActiveGenie
4
4
  module Config
5
5
  module Providers
6
6
  class ProviderBase
7
- NAME = :unknown
8
-
9
- attr_writer :api_key, :organization, :api_url, :client
10
-
11
- # Returns a hash representation of the configuration.
12
- # @param config [Hash] Additional key-value pairs to merge into the hash.
13
- # @return [Hash] The configuration settings as a hash.
14
- def to_h(config = {})
15
- {
16
- name: NAME,
17
- api_key:,
18
- api_url:,
19
- **config
20
- }
7
+ def initialize(api_key: nil, organization: nil, api_url: nil, default_model: nil)
8
+ @api_key = api_key
9
+ @organization = organization
10
+ @api_url = api_url
11
+ @default_model = default_model
21
12
  end
22
13
 
14
+ attr_writer :api_key, :organization, :api_url, :default_model
15
+
23
16
  # Validates the configuration.
24
17
  # @return [Boolean] True if the configuration is valid, false otherwise.
25
18
  def valid?
26
19
  api_key && api_url
27
20
  end
28
21
 
29
- # Retrieves the API key.
30
- # Falls back to the OPENAI_API_KEY environment variable if not set.
31
- # @return [String, nil] The API key.
32
- def api_key
33
- raise NotImplementedError, 'Subclasses must implement this method'
22
+ # Checks if the given model is valid for this provider. Example provider.valid_model?('gpt-4') => true
23
+ # @param model [String, nil] The model name to validate.
24
+ # @return [Boolean] True if the model is valid, false otherwise.
25
+ def valid_model?(_model)
26
+ false
34
27
  end
35
28
 
36
- # Retrieves the base API URL for OpenAI API.
37
- # Defaults to 'https://api.openai.com/v1'.
38
- # @return [String] The API base URL.
39
- def api_url
40
- raise NotImplementedError, 'Subclasses must implement this method'
41
- end
42
-
43
- # Lazily initializes and returns an instance of the OpenaiClient.
44
- # Passes itself (the config object) to the client's constructor.
45
- # @return [ActiveGenie::Clients::OpenaiClient] The client instance.
46
- def client
47
- raise NotImplementedError, 'Subclasses must implement this method'
29
+ # Returns a hash representation of the configuration.
30
+ # @param config [Hash] Additional key-value pairs to merge into the hash.
31
+ # @return [Hash] The configuration settings as a hash.
32
+ def to_h
33
+ {
34
+ api_key: @api_key,
35
+ api_url: @api_url,
36
+ organization: @organization,
37
+ default_model: @default_model
38
+ }
48
39
  end
49
40
  end
50
41
  end
@@ -1,22 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_config'
4
+ require_relative 'providers/openai_config'
5
+ require_relative 'providers/google_config'
6
+ require_relative 'providers/anthropic_config'
7
+ require_relative 'providers/deepseek_config'
8
+
3
9
  module ActiveGenie
4
10
  module Config
5
- class ProvidersConfig
6
- def initialize
7
- @all = {}
8
- @default = nil
11
+ class ProvidersConfig < BaseConfig
12
+ def initialize(**args)
13
+ @all = {
14
+ openai: Providers::OpenaiConfig.new(**args.fetch(:openai, {})),
15
+ google: Providers::GoogleConfig.new(**args.fetch(:google, {})),
16
+ anthropic: Providers::AnthropicConfig.new(**args.fetch(:anthropic, {})),
17
+ deepseek: Providers::DeepseekConfig.new(**args.fetch(:deepseek, {}))
18
+ }
19
+ super
9
20
  end
10
21
 
11
- attr_reader :all
12
-
13
22
  def default
14
- @default || ENV.fetch('PROVIDER_NAME', nil) || valid.keys.first
23
+ @default ||= ENV.fetch('PROVIDER_NAME', nil)&.to_s&.downcase&.strip
15
24
  end
16
25
 
17
26
  def default=(provider)
18
- normalized_provider = provider.to_s.downcase.strip
19
- @default = normalized_provider.size.positive? ? normalized_provider : valid.keys.first
27
+ @default = provider&.to_s&.downcase&.strip
28
+ end
29
+
30
+ def all
31
+ @all ||= {}
20
32
  end
21
33
 
22
34
  def valid
@@ -24,10 +36,9 @@ module ActiveGenie
24
36
  @all.slice(*valid_provider_keys)
25
37
  end
26
38
 
27
- def add(provider_configs)
39
+ def add(name, provider_configs)
28
40
  @all ||= {}
29
41
  Array(provider_configs).each do |provider_config|
30
- name = provider_config::NAME
31
42
  remove([name]) if @all.key?(name)
32
43
 
33
44
  @all[name] = provider_config.new
@@ -44,11 +55,10 @@ module ActiveGenie
44
55
  self
45
56
  end
46
57
 
47
- def merge(config_params = {})
48
- dup.tap do |config|
49
- config.add(config_params[:providers]) if config_params[:providers]
50
- config.default = config_params[:default] if config_params[:default]
51
- end
58
+ def provider_name_by_model(model)
59
+ return nil if model.nil?
60
+
61
+ valid.find { |_, config| config.valid_model?(model) }&.first
52
62
  end
53
63
 
54
64
  def method_missing(method_name, *args, &)
@@ -58,6 +68,15 @@ module ActiveGenie
58
68
  def respond_to_missing?(method_name, include_private = false)
59
69
  @all.key?(method_name) || super
60
70
  end
71
+
72
+ def to_h
73
+ h = {}
74
+ @all.each do |key, config|
75
+ h[key] = config.to_h
76
+ end
77
+
78
+ super.merge(h)
79
+ end
61
80
  end
62
81
  end
63
82
  end
@@ -1,20 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_config'
4
+
3
5
  module ActiveGenie
4
6
  module Config
5
- class RankerConfig
6
- attr_accessor :score_variation_threshold
7
-
8
- def initialize
9
- @score_variation_threshold = 30
10
- end
7
+ class RankerConfig < BaseConfig
8
+ attr_writer :score_variation_threshold
11
9
 
12
- def merge(config_params = {})
13
- dup.tap do |config|
14
- if config_params[:score_variation_threshold]
15
- config.score_variation_threshold = config_params[:score_variation_threshold]
16
- end
17
- end
10
+ def score_variation_threshold
11
+ @score_variation_threshold ||= 30
18
12
  end
19
13
  end
20
14
  end