active_genie 0.30.1 → 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 (59) 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 -31
  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 -25
  11. data/lib/active_genie/configs/log_config.rb +19 -16
  12. data/lib/active_genie/configs/providers/anthropic_config.rb +10 -16
  13. data/lib/active_genie/configs/providers/deepseek_config.rb +4 -19
  14. data/lib/active_genie/configs/providers/google_config.rb +4 -19
  15. data/lib/active_genie/configs/providers/openai_config.rb +4 -19
  16. data/lib/active_genie/configs/providers/provider_base.rb +21 -67
  17. data/lib/active_genie/configs/providers_config.rb +39 -20
  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/invalid_model_error.rb +42 -0
  24. data/lib/active_genie/errors/invalid_provider_error.rb +1 -1
  25. data/lib/active_genie/errors/provider_server_error.rb +26 -0
  26. data/lib/active_genie/errors/without_available_provider_error.rb +39 -0
  27. data/lib/active_genie/extractor/data.json +9 -0
  28. data/lib/active_genie/extractor/data.prompt.md +12 -0
  29. data/lib/active_genie/extractor/data.rb +71 -0
  30. data/lib/active_genie/extractor/explanation.rb +22 -41
  31. data/lib/active_genie/extractor/litote.rb +10 -9
  32. data/lib/active_genie/extractor.rb +5 -0
  33. data/lib/active_genie/lister/feud.json +5 -1
  34. data/lib/active_genie/lister/feud.rb +12 -17
  35. data/lib/active_genie/lister/juries.rb +18 -16
  36. data/lib/active_genie/logger.rb +16 -28
  37. data/lib/active_genie/providers/anthropic_provider.rb +12 -6
  38. data/lib/active_genie/providers/base_provider.rb +15 -18
  39. data/lib/active_genie/providers/deepseek_provider.rb +18 -10
  40. data/lib/active_genie/providers/google_provider.rb +11 -5
  41. data/lib/active_genie/providers/openai_provider.rb +8 -6
  42. data/lib/active_genie/providers/unified_provider.rb +58 -3
  43. data/lib/active_genie/ranker/elo.rb +41 -36
  44. data/lib/active_genie/ranker/free_for_all.rb +45 -28
  45. data/lib/active_genie/ranker/scoring.rb +20 -11
  46. data/lib/active_genie/ranker/tournament.rb +23 -35
  47. data/lib/active_genie/scorer/jury_bench.rb +18 -23
  48. data/lib/active_genie/utils/base_module.rb +34 -0
  49. data/lib/active_genie/utils/call_wrapper.rb +20 -0
  50. data/lib/active_genie/utils/deep_merge.rb +12 -0
  51. data/lib/active_genie/utils/fiber_by_batch.rb +2 -2
  52. data/lib/active_genie/utils/text_case.rb +18 -0
  53. data/lib/active_genie.rb +16 -18
  54. data/lib/tasks/benchmark.rake +1 -3
  55. data/lib/tasks/templates/active_genie.rb +0 -3
  56. data/lib/tasks/test.rake +62 -1
  57. metadata +25 -15
  58. data/lib/active_genie/configs/comparator_config.rb +0 -10
  59. data/lib/active_genie/configs/scorer_config.rb +0 -10
@@ -12,7 +12,7 @@ module ActiveGenie
12
12
  @higher_tier = players.calc_higher_tier
13
13
  @lower_tier = players.calc_lower_tier
14
14
  @criteria = criteria
15
- @config = ActiveGenie.configuration.merge(config)
15
+ @initial_config = config
16
16
  @tmp_highers = []
17
17
  @total_tokens = 0
18
18
  @previous_elo = @players.to_h { |player| [player.id, player.elo] }
@@ -20,16 +20,16 @@ module ActiveGenie
20
20
  end
21
21
 
22
22
  def call
23
- @config.log.add_observer(observers: ->(log) { log_observer(log) })
24
- @config.log.additional_context = { elo_id: }
23
+ config.log.add_observer(observers: ->(log) { log_observer(log) })
24
+ config.log.additional_context = { elo_id: }
25
25
 
26
- ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
26
+ ActiveGenie::FiberByBatch.call(matches, config:) do |player_a, player_b|
27
27
  winner, loser = debate(player_a, player_b)
28
28
 
29
29
  update_players_elo(winner, loser)
30
30
  end
31
31
 
32
- build_report
32
+ elo_result
33
33
  end
34
34
 
35
35
  DEBATE_PER_PLAYER = 3
@@ -38,39 +38,37 @@ module ActiveGenie
38
38
  private
39
39
 
40
40
  def matches
41
- match_keys = {}
42
-
43
- @higher_tier.each_with_object([]) do |attack_player, matches|
41
+ @matches ||= @lower_tier.each_with_object([]) do |lower_player, matches|
44
42
  DEBATE_PER_PLAYER.times do
45
43
  higher_player = next_higher_player
46
44
 
47
- next if match_keys["#{attack_player.id}_#{higher_player.id}"]
45
+ next if matches.include?([lower_player, higher_player])
48
46
 
49
- match_keys["#{attack_player.id}_#{higher_player.id}"] = true
50
- matches << [attack_player, higher_player]
47
+ matches << [lower_player, higher_player]
51
48
  end
52
49
  end
53
50
  end
54
51
 
55
52
  def next_higher_player
56
- @tmp_highers = @higher_tier.shuffle if @tmp_highers.empty?
53
+ @tmp_highers = @higher_tier.dup if @tmp_highers.empty?
57
54
 
58
- @tmp_highers.pop
55
+ @tmp_highers.count % 2 ? @tmp_highers.shift : @tmp_highers.pop
59
56
  end
60
57
 
61
58
  def debate(player_a, player_b)
59
+ debate_config = ActiveGenie::DeepMerge.call(
60
+ config.to_h,
61
+ { log: { additional_context: { player_a_id: player_a.id, player_b_id: player_b.id } } }
62
+ )
63
+
62
64
  result = ActiveGenie::Comparator.by_debate(
63
65
  player_a.content,
64
66
  player_b.content,
65
67
  @criteria,
66
- config: @config.merge(additional_context: { player_a_id: player_a.id, player_b_id: player_b.id })
68
+ config: ActiveGenie.new_configuration(debate_config)
67
69
  )
68
70
 
69
- winner, loser = case result['winner']
70
- when 'player_a' then [player_a, player_b]
71
- when 'player_b' then [player_b, player_a]
72
- when 'draw' then [nil, nil]
73
- end
71
+ winner, loser = result.data == player_a.content ? [player_a, player_b] : [player_b, player_a]
74
72
 
75
73
  [winner, loser]
76
74
  end
@@ -94,27 +92,30 @@ module ActiveGenie
94
92
  higher_tier_ids = @higher_tier.map(&:id).join(',')
95
93
  lower_tier_ids = @lower_tier.map(&:id).join(',')
96
94
 
97
- ranker_unique_key = [higher_tier_ids, lower_tier_ids, @criteria, @config.to_json].join('-')
95
+ ranker_unique_key = [higher_tier_ids, lower_tier_ids, @criteria].join('-')
98
96
  Digest::MD5.hexdigest(ranker_unique_key)
99
97
  end
100
98
  end
101
99
 
102
- def build_report
103
- report = {
104
- elo_id:,
105
- players_in: players_in.map(&:id),
106
- debates_count: matches.size,
107
- total_tokens: @total_tokens,
108
- players_in_round: players_in.map(&:id),
109
- previous_highest_elo: @previous_highest_elo,
110
- highest_elo:,
111
- highest_elo_diff: highest_elo - @previous_highest_elo,
112
- players_elo_diff:
113
- }
114
-
115
- @config.logger.call({ elo_id:, code: :elo_report, **report })
116
-
117
- report
100
+ def elo_result
101
+ result = ActiveGenie::Result.new(
102
+ data: @players.sorted.map(&:content),
103
+ metadata: {
104
+ elo_id:,
105
+ players: @players,
106
+ players_in_round: players_in.map(&:id),
107
+ debates_count: matches.size,
108
+ total_tokens: @total_tokens,
109
+ previous_highest_elo: @previous_highest_elo,
110
+ highest_elo:,
111
+ highest_elo_diff: highest_elo - @previous_highest_elo,
112
+ players_elo_diff:
113
+ }
114
+ )
115
+
116
+ ActiveGenie.logger.call({ elo_id:, code: :elo_report, **result.metadata }, config:)
117
+
118
+ result
118
119
  end
119
120
 
120
121
  def players_in
@@ -135,6 +136,10 @@ module ActiveGenie
135
136
  def log_observer(log)
136
137
  @total_tokens += log[:total_tokens] if log[:code] == :llm_usage
137
138
  end
139
+
140
+ def config
141
+ @config ||= ActiveGenie.new_configuration(@initial_config)
142
+ end
138
143
  end
139
144
  end
140
145
  end
@@ -10,22 +10,19 @@ module ActiveGenie
10
10
  def initialize(players, criteria, config: nil)
11
11
  @players = Entities::Players.new(players)
12
12
  @criteria = criteria
13
- @config = config || ActiveGenie.configuration
13
+ @initial_config = config
14
14
  @start_time = Time.now
15
15
  @total_tokens = 0
16
16
  end
17
17
 
18
18
  def call
19
- @config.log.add_observer(observers: ->(log) { log_observer(log) })
20
- @config.log.additional_context = { free_for_all_id: }
21
-
22
- ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
19
+ ActiveGenie::FiberByBatch.call(matches, config:) do |player_a, player_b|
23
20
  winner, loser = debate(player_a, player_b)
24
21
 
25
22
  update_players_score(winner, loser)
26
23
  end
27
24
 
28
- build_report
25
+ build_result
29
26
  end
30
27
 
31
28
  private
@@ -37,29 +34,32 @@ module ActiveGenie
37
34
  end
38
35
 
39
36
  def debate(player_a, player_b)
40
- log_context = { player_a_id: player_a.id, player_b_id: player_b.id }
37
+ debate_config = ActiveGenie::DeepMerge.call(
38
+ config.to_h,
39
+ { log: { additional_context: { player_a_id: player_a.id, player_b_id: player_b.id } } }
40
+ )
41
41
 
42
42
  result = ActiveGenie::Comparator.by_debate(
43
43
  player_a.content,
44
44
  player_b.content,
45
45
  @criteria,
46
- config: @config.merge(additional_context: log_context)
46
+ config: ActiveGenie.new_configuration(debate_config)
47
47
  )
48
48
 
49
- winner, loser = case result['winner']
50
- when 'player_a' then [player_a, player_b]
51
- when 'player_b' then [player_b, player_a]
52
- when 'draw' then [nil, nil]
49
+ winner, loser = case result.data
50
+ when player_a.to_s then [player_a, player_b]
51
+ else [player_b, player_a]
53
52
  end
54
53
 
55
- @config.logger.call(
54
+ ActiveGenie.logger.call(
56
55
  {
57
- **log_context,
56
+ player_a_id: player_a.id,
57
+ player_b_id: player_b.id,
58
58
  code: :free_for_all,
59
- winner_id: winner&.id,
60
- loser_id: loser&.id,
61
- reasoning: result['reasoning']
62
- }
59
+ winner: winner.to_s[0..20],
60
+ loser: loser.to_s[0..20],
61
+ reasoning: result.reasoning
62
+ }, config:
63
63
  )
64
64
 
65
65
  [winner, loser]
@@ -80,27 +80,44 @@ module ActiveGenie
80
80
  def free_for_all_id
81
81
  @free_for_all_id ||= begin
82
82
  eligible_ids = @players.eligible.map(&:id).join(',')
83
- ranking_unique_key = [eligible_ids, @criteria, @config.to_json].join('-')
83
+ ranking_unique_key = [eligible_ids, @criteria].join('-')
84
84
  Digest::MD5.hexdigest(ranking_unique_key)
85
85
  end
86
86
  end
87
87
 
88
- def build_report
89
- report = {
90
- free_for_all_id:,
91
- debates_count: matches.size,
92
- duration_seconds: Time.now - @start_time,
93
- total_tokens: @total_tokens
94
- }
88
+ def build_result
89
+ result = ActiveGenie::Result.new(
90
+ data: @players.sorted.map(&:content),
91
+ metadata: {
92
+ free_for_all_id:,
93
+ players: @players,
94
+ debates_count: matches.size,
95
+ duration_seconds: Time.now - @start_time,
96
+ total_tokens: @total_tokens
97
+ }
98
+ )
95
99
 
96
- @config.logger.call({ code: :free_for_all_report, **report })
100
+ ActiveGenie.logger.call({ code: :free_for_all_report, **result.metadata }, config:)
97
101
 
98
- report
102
+ result
99
103
  end
100
104
 
101
105
  def log_observer(log)
102
106
  @total_tokens += log[:total_tokens] if log[:code] == :llm_usage
103
107
  end
108
+
109
+ def config
110
+ @config ||= begin
111
+ c = ActiveGenie.new_configuration(
112
+ ActiveGenie::DeepMerge.call(
113
+ @initial_config.to_h,
114
+ { log: { context: { free_for_all_id: } } }
115
+ )
116
+ )
117
+ c.log.add_observer(observers: ->(log) { log_observer(log) })
118
+ c
119
+ end
120
+ end
104
121
  end
105
122
  end
106
123
  end
@@ -12,14 +12,12 @@ module ActiveGenie
12
12
  def initialize(players, criteria, juries: [], config: nil)
13
13
  @players = Entities::Players.new(players)
14
14
  @criteria = criteria
15
- @config = ActiveGenie.configuration.merge(config)
15
+ @initial_config = config
16
16
  @juries = Array(juries).compact.uniq
17
17
  end
18
18
 
19
19
  def call
20
- @config.log.additional_context = { ranker_scoring_id: }
21
-
22
- ActiveGenie::FiberByBatch.call(players_without_score, config: @config) do |player|
20
+ ActiveGenie::FiberByBatch.call(players_without_score, config:) do |player|
23
21
  player.score = generate_score(player)
24
22
  end
25
23
  end
@@ -31,14 +29,16 @@ module ActiveGenie
31
29
  end
32
30
 
33
31
  def generate_score(player)
34
- score, reasoning = ActiveGenie::Scorer.by_jury_bench(
32
+ result = ActiveGenie::Scorer.by_jury_bench(
35
33
  player.content,
36
34
  @criteria,
37
35
  @juries,
38
- config: @config
39
- ).values_at('final_score', 'final_reasoning')
36
+ config:
37
+ )
38
+ score = result.data.to_i
39
+ reasoning = result.reasoning
40
40
 
41
- @config.logger.call({ code: :new_score, player_id: player.id, score:, reasoning: })
41
+ ActiveGenie.logger.call({ code: :new_score, player_id: player.id, score:, reasoning: }, config:)
42
42
 
43
43
  score
44
44
  end
@@ -48,9 +48,9 @@ module ActiveGenie
48
48
  response = ActiveGenie::Lister.juries(
49
49
  [@players.sample.content, @players.sample.content].join("\n\n"),
50
50
  @criteria,
51
- config: @config
51
+ config:
52
52
  )
53
- @config.logger.call({ code: :new_juries, juries: response })
53
+ ActiveGenie.logger.call({ code: :new_juries, juries: response }, config:)
54
54
  response
55
55
  end
56
56
  end
@@ -58,11 +58,20 @@ module ActiveGenie
58
58
  def ranker_scoring_id
59
59
  @ranker_scoring_id ||= begin
60
60
  player_ids = players_without_score.map(&:id).join(',')
61
- ranker_unique_key = [player_ids, @criteria, @config.to_json].join('-')
61
+ ranker_unique_key = [player_ids, @criteria].join('-')
62
62
 
63
63
  "#{Digest::MD5.hexdigest(ranker_unique_key)}-scoring"
64
64
  end
65
65
  end
66
+
67
+ def config
68
+ @config ||= ActiveGenie.new_configuration(
69
+ DeepMerge.call(
70
+ @initial_config,
71
+ { log: { additional_context: { ranker_scoring_id: } } }
72
+ )
73
+ )
74
+ end
66
75
  end
67
76
  end
68
77
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entities/players'
3
+ require_relative '../entities/players'
4
4
  require_relative 'free_for_all'
5
5
  require_relative 'elo'
6
6
  require_relative 'scoring'
@@ -30,33 +30,30 @@ require_relative 'scoring'
30
30
  # @return [Hash] Final ranked player results
31
31
  module ActiveGenie
32
32
  module Ranker
33
- class Tournament
34
- def self.call(...)
35
- new(...).call
36
- end
37
-
33
+ class Tournament < ActiveGenie::BaseModule
38
34
  def initialize(players, criteria, juries: [], config: {})
39
35
  @players = Entities::Players.new(players)
40
36
  @criteria = criteria
41
37
  @juries = Array(juries).compact.uniq
42
- @config = ActiveGenie.configuration.merge(config)
38
+ super(config:)
43
39
  end
44
40
 
45
41
  def call
46
- @config.log.additional_context = { ranker_id: }
47
-
48
42
  set_initial_player_scores!
49
43
  eliminate_obvious_bad_players!
50
44
 
51
45
  while @players.elo_eligible?
52
- elo_report = run_elo_round!
46
+ elo_result = run_elo_round!
53
47
  eliminate_lower_tier_players!
54
- rebalance_players!(elo_report)
48
+ rebalance_players!(elo_result)
55
49
  end
56
50
 
57
51
  run_free_for_all!
58
52
 
59
- sorted_players
53
+ ActiveGenie::Result.new(
54
+ data: sorted_players.map(&:content),
55
+ metadata: @players.map(&:to_h)
56
+ )
60
57
  end
61
58
 
62
59
  ELIMINATION_VARIATION = 'variation_too_high'
@@ -65,12 +62,7 @@ module ActiveGenie
65
62
  private
66
63
 
67
64
  def set_initial_player_scores!
68
- Scoring.call(
69
- @players,
70
- @criteria,
71
- juries: @juries,
72
- config: @config
73
- )
65
+ Scoring.call(@players, @criteria, juries: @juries, config:)
74
66
  end
75
67
 
76
68
  def eliminate_obvious_bad_players!
@@ -80,50 +72,46 @@ module ActiveGenie
80
72
  end
81
73
 
82
74
  def run_elo_round!
83
- Elo.call(
84
- @players,
85
- @criteria,
86
- config: @config
87
- )
75
+ Elo.call(@players, @criteria, config:)
88
76
  end
89
77
 
90
78
  def eliminate_lower_tier_players!
91
79
  @players.calc_lower_tier.each { |player| player.eliminated = ELIMINATION_RELEGATION }
92
80
  end
93
81
 
94
- def rebalance_players!(elo_report)
95
- return if elo_report[:highest_elo_diff].negative?
82
+ def rebalance_players!(elo_result)
83
+ return if elo_result.metadata[:highest_elo_diff].negative?
96
84
 
97
85
  @players.eligible.each do |player|
98
- next if elo_report[:players_in_round].include?(player.id)
86
+ next if elo_result.metadata[:players_in_round].include?(player.id)
99
87
 
100
- player.elo += elo_report[:highest_elo_diff]
88
+ player.elo += elo_result.metadata[:highest_elo_diff]
101
89
  end
102
90
  end
103
91
 
104
92
  def run_free_for_all!
105
- FreeForAll.call(
106
- @players,
107
- @criteria,
108
- config: @config
109
- )
93
+ FreeForAll.call(@players, @criteria, config:)
110
94
  end
111
95
 
112
96
  def sorted_players
113
97
  players = @players.sorted
114
- @config.logger.call({ ranker_id:, code: :ranker_final, players: players.map(&:to_h) })
98
+ ActiveGenie.logger.call({ ranker_id:, code: :ranker_final, players: players.map(&:to_h) }, config:)
115
99
 
116
- players.map(&:to_h)
100
+ players
117
101
  end
118
102
 
119
103
  def ranker_id
120
104
  @ranker_id ||= begin
121
105
  player_ids = @players.map(&:id).join(',')
122
- ranker_unique_key = [player_ids, @criteria, @config.to_json].join('-')
106
+ ranker_unique_key = [player_ids, @criteria].join('-')
123
107
 
124
108
  Digest::MD5.hexdigest(ranker_unique_key)
125
109
  end
126
110
  end
111
+
112
+ def module_config
113
+ { log: { additional_context: { ranker_id: } } }
114
+ end
127
115
  end
128
116
  end
129
117
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../providers/unified_provider'
4
+ require_relative '../utils/text_case'
4
5
 
5
6
  module ActiveGenie
6
7
  module Scorer
@@ -17,7 +18,7 @@ module ActiveGenie
17
18
  # @example Usage with automatic jury recommendation
18
19
  # JuryBench.call("Sample text", "Evaluate technical accuracy")
19
20
  #
20
- class JuryBench
21
+ class JuryBench < ActiveGenie::BaseModule
21
22
  # @param text [String] The text content to be evaluated
22
23
  # @param criteria [String] The evaluation criteria or rubric to assess against
23
24
  # @param juries [Array<String>] Optional list of specific juries. If empty,
@@ -26,15 +27,11 @@ module ActiveGenie
26
27
  # @return [Hash] The evaluation result containing the scores and reasoning
27
28
  # @return [Number] :final_score The final score of the text based on the criteria and juries
28
29
  # @return [String] :final_reasoning Detailed explanation of why the final score was reached
29
- def self.call(...)
30
- new(...).call
31
- end
32
-
33
30
  def initialize(text, criteria, juries = [], config: {})
34
31
  @text = text
35
32
  @criteria = criteria
36
33
  @param_juries = Array(juries).compact.uniq
37
- @config = ActiveGenie.configuration.merge(config)
34
+ super(config:)
38
35
  end
39
36
 
40
37
  def call
@@ -44,24 +41,17 @@ module ActiveGenie
44
41
  { role: 'user', content: "Text to score: #{@text}" }
45
42
  ]
46
43
 
47
- result = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
44
+ provider_response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
48
45
  messages,
49
46
  build_function,
50
- config: @config
47
+ config:
51
48
  )
52
49
 
53
- result['final_score'] = 0 if result['final_score'].nil?
54
-
55
- @config.logger.call({
56
- code: :Scorer,
57
- text: @text[0..30],
58
- criteria: @criteria[0..30],
59
- juries: juries,
60
- score: result['final_score'],
61
- reasoning: result['final_reasoning']
62
- })
63
-
64
- result
50
+ ActiveGenie::Result.new(
51
+ data: provider_response['final_score'] || 0,
52
+ reasoning: provider_response['final_reasoning'],
53
+ metadata: provider_response
54
+ )
65
55
  end
66
56
 
67
57
  PROMPT = File.read(File.join(__dir__, 'jury_bench.prompt.md'))
@@ -84,11 +74,12 @@ module ActiveGenie
84
74
  @properties ||= begin
85
75
  tmp = {}
86
76
  juries.each do |jury|
87
- tmp["#{jury}_reasoning"] = {
77
+ jury_key = ActiveGenie::TextCase.underscore(jury)
78
+ tmp["#{jury_key}_reasoning"] = {
88
79
  type: 'string',
89
80
  description: "The reasoning of the Scorer process by #{jury}."
90
81
  }
91
- tmp["#{jury}_score"] = {
82
+ tmp["#{jury_key}_score"] = {
92
83
  type: 'number',
93
84
  description: "The score given by #{jury}.",
94
85
  min: 0,
@@ -113,9 +104,13 @@ module ActiveGenie
113
104
  @juries ||= if @param_juries.any?
114
105
  @param_juries
115
106
  else
116
- ::ActiveGenie::Lister::Juries.call(@text, @criteria, config: @config)
107
+ ::ActiveGenie::Lister::Juries.call(@text, @criteria, config:).data
117
108
  end
118
109
  end
110
+
111
+ def module_config
112
+ { llm: { recommended_model: 'deepseek-chat' } }
113
+ end
119
114
  end
120
115
  end
121
116
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'call_wrapper'
4
+
5
+ module ActiveGenie
6
+ class BaseModule
7
+ prepend CallWrapper
8
+
9
+ def self.call(...)
10
+ new(...).call
11
+ end
12
+
13
+ def initialize(config: {})
14
+ @initial_config = config || {}
15
+ end
16
+
17
+ def call
18
+ raise NotImplementedError, 'Subclasses must implement the `call` method'
19
+ end
20
+
21
+ def config
22
+ @config ||= ActiveGenie.new_configuration(
23
+ ActiveGenie::DeepMerge.call(
24
+ @initial_config.to_h,
25
+ module_config
26
+ )
27
+ )
28
+ end
29
+
30
+ def module_config
31
+ {}
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveGenie
4
+ module CallWrapper
5
+ def call(*, &)
6
+ response = super # Call the original method
7
+
8
+ if defined?(config)
9
+ ActiveGenie.logger.call(
10
+ {
11
+ code: self.class.name,
12
+ response: response.to_h
13
+ }, config:
14
+ )
15
+ end
16
+
17
+ response
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveGenie
4
+ module DeepMerge
5
+ module_function
6
+
7
+ def call(first_hash, second_hash)
8
+ merger = proc { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2 }
9
+ first_hash.to_h.merge(second_hash.to_h, &merger)
10
+ end
11
+ end
12
+ end
@@ -8,9 +8,9 @@ module ActiveGenie
8
8
 
9
9
  def call(items, config:, &block)
10
10
  items.each_slice(config.llm.max_fibers).to_a.each do |batch|
11
- Async do
11
+ Async do |task|
12
12
  tasks = batch.map do |item|
13
- Async { block.call(item) }
13
+ task.async { block.call(item) }
14
14
  end
15
15
 
16
16
  tasks.each(&:wait)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveGenie
4
+ module TextCase
5
+ module_function
6
+
7
+ def underscore(camel_cased_word)
8
+ return camel_cased_word.to_s.dup unless /[A-Z-]|::/.match?(camel_cased_word)
9
+
10
+ word = camel_cased_word.to_s.gsub('::', '/')
11
+ word.gsub!(/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/, '_')
12
+ word.tr!('-', '_')
13
+ word.tr!(' ', '_')
14
+ word.downcase!
15
+ word
16
+ end
17
+ end
18
+ end