active_genie 0.0.8 → 0.0.12

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -72
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +2 -2
  5. data/lib/active_genie/battle/basic.rb +52 -56
  6. data/lib/active_genie/battle.rb +1 -1
  7. data/lib/active_genie/clients/openai_client.rb +119 -0
  8. data/lib/active_genie/clients/unified_client.rb +19 -0
  9. data/lib/active_genie/configuration/log_config.rb +14 -0
  10. data/lib/active_genie/configuration/openai_config.rb +56 -0
  11. data/lib/active_genie/configuration/providers_config.rb +37 -0
  12. data/lib/active_genie/configuration.rb +18 -23
  13. data/lib/active_genie/data_extractor/README.md +4 -5
  14. data/lib/active_genie/data_extractor/basic.rb +13 -13
  15. data/lib/active_genie/data_extractor/from_informal.rb +7 -7
  16. data/lib/active_genie/data_extractor.rb +1 -1
  17. data/lib/active_genie/logger.rb +72 -0
  18. data/lib/active_genie/ranking/README.md +43 -0
  19. data/lib/active_genie/ranking/elo_round.rb +113 -0
  20. data/lib/active_genie/ranking/free_for_all.rb +76 -0
  21. data/lib/active_genie/ranking/player.rb +97 -0
  22. data/lib/active_genie/{leaderboard → ranking}/players_collection.rb +18 -11
  23. data/lib/active_genie/ranking/ranking.rb +98 -0
  24. data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
  25. data/lib/active_genie/ranking.rb +12 -0
  26. data/lib/active_genie/scoring/README.md +4 -8
  27. data/lib/active_genie/scoring/basic.rb +58 -24
  28. data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +21 -12
  29. data/lib/active_genie/scoring.rb +4 -4
  30. data/lib/active_genie.rb +10 -18
  31. data/lib/tasks/install.rake +3 -3
  32. data/lib/tasks/templates/active_genie.rb +17 -0
  33. metadata +74 -90
  34. data/lib/active_genie/clients/openai.rb +0 -61
  35. data/lib/active_genie/clients/router.rb +0 -41
  36. data/lib/active_genie/leaderboard/elo_ranking.rb +0 -88
  37. data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
  38. data/lib/active_genie/leaderboard/league.rb +0 -48
  39. data/lib/active_genie/leaderboard/player.rb +0 -52
  40. data/lib/active_genie/leaderboard.rb +0 -11
  41. data/lib/active_genie/utils/math.rb +0 -15
  42. data/lib/tasks/templates/active_genie.yml +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 834787b505134623a3f6c4995c9dec8a0ce89a1a014600c4d676d8801bf541cd
4
- data.tar.gz: 16ae75ffa3926ecd87052d72ca3f5434be80327b712bb2dba54e55c46fa5ff8d
3
+ metadata.gz: fbc4033f3c0880973cf732d704921bb61c3cb861c438b732aa6ebe0cc88b2de4
4
+ data.tar.gz: 11794293170ec43a1d3b3d09e5ec488351a22428a5370ae0e42266a6f8098646
5
5
  SHA512:
6
- metadata.gz: 1772fbfa59891e7a3673b50a54bd90914007917854e78fb5f0458db681bf89bd1ec118600d23ab98d6255e1b063b5d582d3b73dc738e66aeadc7f4f0bee75af8
7
- data.tar.gz: bb6a0ea9ef737f7a2ed3ec558dcea5c67bcc2f90a155828e1e25a5a7a3ebd9d0fe6c5422dd9f3d9ff6fb667b63822e3bc86627cdb3e40a3c72c4ef50cee32c68
6
+ metadata.gz: f6b13b3f36a8e516e5126d4cea0de52d046e498834cd10b90b1905460e9bba5ff6e7c4cebd58e6ea2c073176566601c8b806015baa01f11f28792942eec6ca1d
7
+ data.tar.gz: fc79aed12aaba0ca335d3a7ef8405f4a7c2809de9d5e41ca14fa7bd843d9fb69db8e124b6a2e96321432575a58b271c724a52fd3c1a1165c5ecfe346bedd4b5f
data/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # ActiveGenie 🧞‍♂️
2
- > Transform your Ruby application with powerful, production-ready GenAI features
2
+ > The lodash for GenAI, stop reinventing the wheel
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/ruby.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/ruby.yml)
6
6
 
7
- ActiveGenie is a Ruby gem that provides a polished, production-ready interface for working with Generative AI (GenAI) models. Just like ActiveStorage simplifies file handling in Rails, ActiveGenie makes it effortless to integrate GenAI capabilities into your Ruby applications.
7
+ ActiveGenie is a Ruby gem that provides a polished, production-ready interface for working with Generative AI (GenAI) models. Just like Lodash or ActiveStorage, ActiveGenie simplifies GenAI integration in your Ruby applications.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - 🎯 **Data Extraction**: Extract structured data from unstructured text with type validation
12
- - 📊 **Smart Scoring**: Multi-reviewer evaluation system with automatic expert selection
13
- - 💭 **Sentiment Analysis**: Advanced sentiment analysis with customizable rules
14
- - 🔒 **Safe & Secure**: Built-in validation and sanitization
15
- - 🛠️ **Configurable**: Supports multiple GenAI providers and models
12
+ - 📊 **Data Scoring**: Multi-reviewer evaluation system
13
+ - ⚔️ **Data Battle**: Battle between two data like a political debate
14
+ - 💭 **Data Ranking**: Consistent rank data using scoring + elo ranking + battles
16
15
 
17
16
  ## Installation
18
17
 
@@ -32,19 +31,13 @@ echo "ActiveGenie.load_tasks" >> Rakefile
32
31
  rails g active_genie:install
33
32
  ```
34
33
 
35
- 4. [Optional] Configure your credentials in `config/active_genie.yml`:
36
- ```yaml
37
- GPT-4o-mini:
38
- api_key: <%= ENV['OPENAI_API_KEY'] %>
39
- provider: "openai"
40
-
41
- claude-3-5-sonnet:
42
- api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
43
- provider: "anthropic"
34
+ 4. Configure your credentials in `config/initializers/active_genie.rb`:
35
+ ```ruby
36
+ ActiveGenie.configure do |config|
37
+ config.openai.api_key = ENV['OPENAI_API_KEY']
38
+ end
44
39
  ```
45
40
 
46
- > The first key will be used as default in all modules, in this example `GPT-4o-mini`
47
-
48
41
  ## Quick Start
49
42
 
50
43
  ### Data Extractor
@@ -87,14 +80,14 @@ Features:
87
80
 
88
81
  See the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal text processing, advanced schemas, and detailed interface documentation.
89
82
 
90
- ### Scoring
83
+ ### Data Scoring
91
84
  Text evaluation system that provides detailed scoring and feedback using multiple expert reviewers. Get balanced scoring through AI-powered expert reviewers that automatically adapt to your content.
92
85
 
93
86
  ```ruby
94
87
  text = "The code implements a binary search algorithm with O(log n) complexity"
95
88
  criteria = "Evaluate technical accuracy and clarity"
96
89
 
97
- result = ActiveGenie::Scoring::Basic.call(text, criteria)
90
+ result = ActiveGenie::Scoring.basic(text, criteria)
98
91
  # => {
99
92
  # algorithm_expert_score: 95,
100
93
  # algorithm_expert_reasoning: "Accurately describes binary search and its complexity",
@@ -112,7 +105,7 @@ Features:
112
105
 
113
106
  See the [Scoring README](lib/active_genie/scoring/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
114
107
 
115
- ### Battle
108
+ ### Data Battle
116
109
  AI-powered battle evaluation system that determines winners between two players based on specified criteria.
117
110
 
118
111
  ```ruby
@@ -122,7 +115,7 @@ player_a = "Implementation uses dependency injection for better testability"
122
115
  player_b = "Code has high test coverage but tightly coupled components"
123
116
  criteria = "Evaluate code quality and maintainability"
124
117
 
125
- result = ActiveGenie::Battle::Basic.call(player_a, player_b, criteria)
118
+ result = ActiveGenie::Battle.call(player_a, player_b, criteria)
126
119
  # => {
127
120
  # winner_player: "Implementation uses dependency injection for better testability",
128
121
  # reasoning: "Player A's implementation demonstrates better maintainability through dependency injection,
@@ -140,66 +133,37 @@ Features:
140
133
 
141
134
  See the [Battle README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
142
135
 
143
- ### Summarizer (WIP)
144
- The summarizer is a tool that can be used to summarize a given text. It uses a set of rules to summarize the text out of the box. Uses the best practices of prompt engineering and engineering to make the summarization as accurate as possible.
145
-
146
- ```ruby
147
- require 'active_genie'
148
-
149
- text = "Example text to be summarized. The fox jumps over the dog"
150
- summarized_text = ActiveGenie::Summarizer.call(text)
151
- puts summarized_text # => "The fox jumps over the dog"
152
- ```
136
+ ### Data Ranking
137
+ The Ranking module provides competitive ranking through multi-stage evaluation:
153
138
 
154
- ### Language detector (WIP)
155
- The language detector is a tool that can be used to detect the language of a given text. It uses a set of rules to detect the language of the text out of the box. Uses the best practices of prompt engineering and engineering to make the language detection as accurate as possible.
156
139
 
157
140
  ```ruby
158
141
  require 'active_genie'
159
142
 
160
- text = "Example text to be detected"
161
- language = ActiveGenie::LanguageDetector.call(text)
162
- puts language # => "en"
163
- ```
164
-
165
- ### Translator (WIP)
166
- The translator is a tool that can be used to translate a given text. It uses a set of rules to translate the text out of the box. Uses the best practices of prompt engineering and engineering to make the translation as accurate as possible.
143
+ players = ['REST API', 'GraphQL API', 'SOAP API', 'gRPC API', 'Websocket API']
144
+ criteria = "Best one to be used into a high changing environment"
167
145
 
168
- ```ruby
169
- require 'active_genie'
170
-
171
- text = "Example text to be translated"
172
- translated_text = ActiveGenie::Translator.call(text, from: 'en', to: 'pt')
173
- puts translated_text # => "Exemplo de texto a ser traduzido"
174
- ```
175
-
176
- ### Sentiment analyzer (WIP)
177
- The sentiment analyzer is a tool that can be used to analyze the sentiment of a given text. It uses a set of rules to analyze the sentiment of the text out of the box. Uses the best practices of prompt engineering and engineering to make the sentiment analysis as accurate as possible.
178
-
179
- ```ruby
180
- require 'active_genie'
181
-
182
- text = "Example text to be analyzed"
183
- sentiment = ActiveGenie::SentimentAnalyzer.call(text)
184
- puts sentiment # => "positive"
146
+ result = ActiveGenie::Ranking.call(players, criteria)
147
+ # => {
148
+ # winner_player: "gRPC API",
149
+ # reasoning: "gRPC API is the best one to be used into a high changing environment",
150
+ # }
185
151
  ```
186
152
 
187
- ### Elo ranking (WIP)
188
- The Elo ranking is a tool that can be used to rank a set of items. It uses a set of rules to rank the items out of the box. Uses the best practices of prompt engineering and engineering to make the ranking as accurate as possible.
153
+ - **Multi-phase ranking system** combining expert scoring and ELO algorithms
154
+ - **Automatic elimination** of inconsistent performers using statistical analysis
155
+ - **Dynamic ranking adjustments** based on simulated pairwise battles, from bottom to top
189
156
 
190
- ```ruby
191
- require 'active_genie'
192
-
193
- items = ['Square', 'Circle', 'Triangle']
194
- criterias = 'items that look rounded'
195
- ranked_items = ActiveGenie::EloRanking.call(items, criterias, rounds: 10)
196
- puts ranked_items # => [{ name: "Circle", score: 1500 }, { name: "Square", score: 800 }, { name: "Triangle", score: 800 }]
197
- ```
157
+ See the [Ranking README](lib/active_genie/ranking/README.md) for implementation details, configuration, and advanced ranking strategies.
198
158
 
159
+ ### Text Summarizer (Future)
160
+ ### Language detector (Future)
161
+ ### Translator (Future)
162
+ ### Sentiment analyzer (Future)
199
163
 
200
- ## Configuration Options
164
+ ## Configuration
201
165
 
202
- | Option | Description | Default |
166
+ | Config | Description | Default |
203
167
  |--------|-------------|---------|
204
168
  | `provider` | LLM provider (openai, anthropic, etc) | `nil` |
205
169
  | `model` | Model to use | `nil` |
@@ -207,7 +171,7 @@ puts ranked_items # => [{ name: "Circle", score: 1500 }, { name: "Square", score
207
171
  | `timeout` | Request timeout in seconds | `5` |
208
172
  | `max_retries` | Maximum retry attempts | `3` |
209
173
 
210
- > **Note:** Each module can append its own set of configuration options, see the individual module documentation for details.
174
+ > **Note:** Each module can append its own set of configuration, see the individual module documentation for details.
211
175
 
212
176
  ## Contributing
213
177
 
@@ -216,7 +180,7 @@ puts ranked_items # => [{ name: "Circle", score: 1500 }, { name: "Square", score
216
180
  3. Commit your changes (`git commit -m 'Add amazing feature'`)
217
181
  4. Push to the branch (`git push origin feature/amazing-feature`)
218
182
  5. Open a Pull Request
219
- ## License
220
183
 
221
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
184
+ ## License
222
185
 
186
+ This project is licensed under the Apache License 2.0 License - see the [LICENSE](LICENSE) file for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.8
1
+ 0.0.12
@@ -27,11 +27,11 @@ result = ActiveGenie::Battle::Basic.call(player_a, player_b, criteria)
27
27
  ```
28
28
 
29
29
  ## Interface
30
- ### Basic.call(player_a, player_b, criteria, options: {})
30
+ ### Basic.call(player_a, player_b, criteria, config: {})
31
31
  - `player_a` [String, Hash] - The content or submission from the first player
32
32
  - `player_b` [String, Hash] - The content or submission from the second player
33
33
  - `criteria` [String] - The evaluation criteria or rules to assess against
34
- - `options` [Hash] - Additional configuration options that modify the battle evaluation behavior
34
+ - `config` [Hash] - Additional configuration config that modify the battle evaluation behavior
35
35
 
36
36
  Returns a Hash containing:
37
37
  - `winner_player` [String, Hash] - The winning player's content (either player_a or player_b)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../clients/router'
3
+ require_relative '../clients/unified_client'
4
4
 
5
5
  module ActiveGenie::Battle
6
6
  # The Basic class provides a foundation for evaluating battles between two players
@@ -14,87 +14,71 @@ module ActiveGenie::Battle
14
14
  # Basic.call("Player A content", "Player B content", "Evaluate keyword usage and pattern matching")
15
15
  #
16
16
  class Basic
17
- def self.call(player_a, player_b, criteria, options: {})
18
- new(player_a, player_b, criteria, options:).call
17
+ def self.call(...)
18
+ new(...).call
19
19
  end
20
20
 
21
21
  # @param player_a [String] The content or submission from the first player
22
22
  # @param player_b [String] The content or submission from the second player
23
23
  # @param criteria [String] The evaluation criteria or rules to assess against
24
- # @param options [Hash] Additional configuration options that modify the battle evaluation behavior
24
+ # @param config [Hash] Additional configuration config that modify the battle evaluation behavior
25
25
  # @return [Hash] The evaluation result containing the winner and reasoning
26
26
  # @return [String] :winner The @param player_a or player_b
27
27
  # @return [String] :reasoning Detailed explanation of why the winner was chosen
28
28
  # @return [String] :what_could_be_changed_to_avoid_draw A suggestion on how to avoid a draw
29
- def initialize(player_a, player_b, criteria, options: {})
29
+ def initialize(player_a, player_b, criteria, config: {})
30
30
  @player_a = player_a
31
31
  @player_b = player_b
32
32
  @criteria = criteria
33
- @options = options
34
- @response = nil
33
+ @config = ActiveGenie::Configuration.to_h(config)
35
34
  end
36
35
 
37
36
  def call
38
37
  messages = [
39
38
  { role: 'system', content: PROMPT },
40
39
  { role: 'user', content: "criteria: #{@criteria}" },
41
- { role: 'user', content: "player_a: #{player_content(@player_a)}" },
42
- { role: 'user', content: "player_b: #{player_content(@player_b)}" },
40
+ { role: 'user', content: "player_a: #{@player_a}" },
41
+ { role: 'user', content: "player_b: #{@player_b}" },
43
42
  ]
44
43
 
45
- @response = ::ActiveGenie::Clients::Router.function_calling(messages, FUNCTION, options: @options)
44
+ response = ::ActiveGenie::Clients::UnifiedClient.function_calling(
45
+ messages,
46
+ FUNCTION,
47
+ model_tier: 'lower_tier',
48
+ config: @config
49
+ )
46
50
 
47
- response_formatted
51
+ response_formatted(response)
48
52
  end
49
53
 
50
54
  private
51
55
 
52
- def player_content(player)
53
- return player.dig('content') if player.is_a?(Hash)
56
+ def response_formatted(response)
57
+ winner = response['impartial_judge_winner']
58
+ loser = case response['impartial_judge_winner']
59
+ when 'player_a' then 'player_b'
60
+ when 'player_b' then 'player_a'
61
+ else 'draw'
62
+ end
54
63
 
55
- player
56
- end
57
-
58
- def response_formatted
59
- if @response['winner'] == 'player_a'
60
- @response['winner'] = @player_a
61
- @response['loser'] = @player_b
62
- elsif @response['winner'] == 'player_b'
63
- @response['winner'] = @player_b
64
- @response['loser'] = @player_a
65
- else
66
- @response['winner'] = nil
67
- @response['loser'] = nil
68
- end
69
-
70
- @response
64
+ { winner:, loser:, reasoning: response['impartial_judge_winner_reasoning'] }
71
65
  end
72
66
 
73
67
  PROMPT = <<~PROMPT
74
- Evaluate a battle between player_a and player_b using predefined criteria and identify the winner.
75
-
76
- Consider rules, keywords, and patterns as the criteria for evaluation. Analyze the content from both players objectively, focusing on who meets the criteria most effectively. Explain your decision clearly, with specific reasoning on how the chosen player fulfilled the criteria better than the other. Avoid selecting a draw unless absolutely necessary.
68
+ Based on two players, player_a and player_b, they will battle against each other based on criteria. Criteria are vital as they provide a clear metric to compare the players. Follow these criteria strictly.
77
69
 
78
70
  # Steps
79
- 1. **Review Predefined Criteria**: Understand the specific rules, keywords, and patterns that serve as the basis for evaluation.
80
- 2. **Analyze Content**: Examine the contributions of both player_a and player_b. Look for how each player meets or fails to meet the criteria.
81
- 3. **Comparison**: Compare both players against each criterion to determine who aligns better with the standards set.
82
- 4. **Decision-Making**: Based on the analysis, determine the player who meets the most or all criteria effectively.
83
- 5. **Provide Justification**: Offer a clear and concise reason for your choice detailing how the winner outperformed the other.
71
+ 1. Player_a sells himself, highlighting his strengths and how he meets the criteria. Max of 100 words.
72
+ 2. Player_b sells himself, highlighting his strengths and how he meets the criteria. Max of 100 words.
73
+ 3. Player_a argues why he is the winner compared to player_b. Max of 100 words.
74
+ 4. Player_b counter-argues why he is the winner compared to player_a. Max of 100 words.
75
+ 5. The impartial judge chooses which player as the winner.
84
76
 
85
- # Examples
86
- - **Example 1**:
87
- - Input: Player A uses keyword X, follows rule Y, Player B uses keyword Z, breaks rule Y.
88
- - Output: winner: player_a
89
- - Justification: Player A successfully used keyword X and followed rule Y, whereas Player B broke rule Y.
90
-
91
- - **Example 2**:
92
- - Input: Player A matches pattern P, Player B matches pattern P, uses keyword Q.
93
- - Output: winner: player_b
94
- - Justification: Both matched pattern P, but Player B also used keyword Q, meeting more criteria.
77
+ # Output Format
78
+ - The impartial judge chooses this player as the winner.
95
79
 
96
80
  # Notes
97
- - Avoid drawing if a clear winner can be discerned.
81
+ - Avoid resulting in a draw. Use reasoning or make fair assumptions if needed.
98
82
  - Critically assess each player's adherence to the criteria.
99
83
  - Clearly communicate the reasoning behind your decision.
100
84
  PROMPT
@@ -105,19 +89,31 @@ module ActiveGenie::Battle
105
89
  schema: {
106
90
  type: "object",
107
91
  properties: {
108
- winner: {
92
+ player_a_sell_himself: {
109
93
  type: 'string',
110
- description: 'The player who won the battle based on the criteria.',
111
- enum: ['player_a', 'player_b', 'draw']
94
+ description: 'player_a sell himself, highlighting his strengths and how he meets the criteria. Max of 100 words.',
95
+ },
96
+ player_b_sell_himself: {
97
+ type: 'string',
98
+ description: 'player_b sell himself, highlighting his strengths and how he meets the criteria. Max of 100 words.',
99
+ },
100
+ player_a_arguments: {
101
+ type: 'string',
102
+ description: 'player_a arguments why he is the winner compared to player_b. Max of 100 words.',
112
103
  },
113
- reasoning_of_winner: {
104
+ player_b_counter: {
114
105
  type: 'string',
115
- description: 'The detailed reasoning about why the winner won based on the criteria.',
106
+ description: 'player_b counter arguments why he is the winner compared to player_a. Max of 100 words.',
116
107
  },
117
- what_could_be_changed_to_avoid_draw: {
108
+ impartial_judge_winner_reasoning: {
118
109
  type: 'string',
119
- description: 'Suggestions on how to avoid a draw based on the criteria. Be as objective and short as possible. Can be empty.',
120
- }
110
+ description: 'The detailed reasoning about why the impartial judge chose the winner. Max of 100 words.',
111
+ },
112
+ impartial_judge_winner: {
113
+ type: 'string',
114
+ description: 'The impartial judge chose this player as the winner.',
115
+ enum: ['player_a', 'player_b', 'draw']
116
+ },
121
117
  }
122
118
  }
123
119
  }
@@ -2,7 +2,7 @@
2
2
  require_relative 'battle/basic'
3
3
 
4
4
  module ActiveGenie
5
- # Battle module
5
+ # See the [Battle README](lib/active_genie/battle/README.md) for more information.
6
6
  module Battle
7
7
  module_function
8
8
 
@@ -0,0 +1,119 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ module ActiveGenie::Clients
5
+ class OpenaiClient
6
+ MAX_RETRIES = 3
7
+
8
+ class OpenaiError < StandardError; end
9
+ class RateLimitError < OpenaiError; end
10
+
11
+ def initialize(config)
12
+ @app_config = config
13
+ end
14
+
15
+ def function_calling(messages, function, model_tier: nil, config: {})
16
+ model = config[:model] || @app_config.tier_to_model(model_tier)
17
+
18
+ payload = {
19
+ messages:,
20
+ response_format: {
21
+ type: 'json_schema',
22
+ json_schema: function
23
+ },
24
+ model:,
25
+ }
26
+
27
+ api_key = config[:api_key] || @app_config.api_key
28
+ headers = DEFAULT_HEADERS.merge(
29
+ 'Authorization': "Bearer #{api_key}"
30
+ ).compact
31
+
32
+ response = request(payload, headers, config:)
33
+
34
+ parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'content'))
35
+ parsed_response.dig('properties') || parsed_response
36
+ rescue JSON::ParserError
37
+ nil
38
+ end
39
+
40
+ private
41
+
42
+ def request(payload, headers, config:)
43
+ retries = config[:max_retries] || MAX_RETRIES
44
+ start_time = Time.now
45
+
46
+ begin
47
+ response = Net::HTTP.post(
48
+ URI("#{@app_config.api_url}/chat/completions"),
49
+ payload.to_json,
50
+ headers
51
+ )
52
+
53
+ if response.is_a?(Net::HTTPTooManyRequests)
54
+ raise RateLimitError, "OpenAI API rate limit exceeded: #{response.body}"
55
+ end
56
+
57
+ raise OpenaiError, response.body unless response.is_a?(Net::HTTPSuccess)
58
+
59
+ return nil if response.body.empty?
60
+
61
+ parsed_body = JSON.parse(response.body)
62
+ # log_response(start_time, parsed_body, config:)
63
+
64
+ parsed_body
65
+ rescue OpenaiError, Net::HTTPError, JSON::ParserError, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout => e
66
+ if retries > 0
67
+ retries -= 1
68
+ backoff_time = calculate_backoff(MAX_RETRIES - retries)
69
+ ActiveGenie::Logger.trace(
70
+ {
71
+ category: :llm,
72
+ trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
73
+ message: "Retrying request after error: #{e.message}. Attempts remaining: #{retries}",
74
+ backoff_time: backoff_time
75
+ }
76
+ )
77
+ sleep(backoff_time)
78
+ retry
79
+ else
80
+ ActiveGenie::Logger.trace(
81
+ {
82
+ category: :llm,
83
+ trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
84
+ message: "Max retries reached. Failing with error: #{e.message}"
85
+ }
86
+ )
87
+ raise
88
+ end
89
+ end
90
+ end
91
+
92
+ BASE_DELAY = 0.5
93
+ def calculate_backoff(retry_count)
94
+ # Exponential backoff with jitter: 2^retry_count + random jitter
95
+ # Base delay is 0.5 seconds, doubles each retry, plus up to 0.5 seconds of random jitter
96
+ # Simplified example: 0.5, 1, 2, 4, 8, 12, 16, 20, 24, 28, 30 seconds
97
+ jitter = rand * BASE_DELAY
98
+ [BASE_DELAY * (2 ** retry_count) + jitter, 30].min # Cap at 30 seconds
99
+ end
100
+
101
+ DEFAULT_HEADERS = {
102
+ 'Content-Type': 'application/json',
103
+ }
104
+
105
+ def log_response(start_time, response, config: {})
106
+ ActiveGenie::Logger.trace(
107
+ {
108
+ **config.dig(:log),
109
+ category: :llm,
110
+ trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
111
+ total_tokens: response.dig('usage', 'total_tokens'),
112
+ model: response.dig('model'),
113
+ request_duration: Time.now - start_time,
114
+ openai: response
115
+ }
116
+ )
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveGenie::Clients
2
+ class UnifiedClient
3
+ class << self
4
+ def function_calling(messages, function, model_tier: nil, config: {})
5
+ provider_name = config[:provider]&.downcase&.strip&.to_sym
6
+ provider = ActiveGenie.configuration.providers.all[provider_name] || ActiveGenie.configuration.providers.default
7
+
8
+ raise InvalidProviderError if provider.nil? || provider.client.nil?
9
+
10
+ provider.client.function_calling(messages, function, model_tier:, config:)
11
+ end
12
+
13
+ private
14
+
15
+ # TODO: improve error message
16
+ class InvalidProviderError < StandardError; end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+
2
+ module ActiveGenie::Configuration
3
+ class LogConfig
4
+ attr_writer :log_level
5
+
6
+ def log_level
7
+ @log_level ||= :info
8
+ end
9
+
10
+ def to_h(config = {})
11
+ { log_level:, **config }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,56 @@
1
+ require_relative '../clients/openai_client'
2
+
3
+ module ActiveGenie::Configuration
4
+ class OpenaiConfig
5
+ attr_writer :api_key, :organization, :api_url, :client,
6
+ :lower_tier_model, :middle_tier_model, :upper_tier_model
7
+
8
+ def api_key
9
+ @api_key || ENV['OPENAI_API_KEY']
10
+ end
11
+
12
+ def organization
13
+ @organization || ENV['OPENAI_ORGANIZATION']
14
+ end
15
+
16
+ def api_url
17
+ @api_url || 'https://api.openai.com/v1'
18
+ end
19
+
20
+ def client
21
+ @client ||= ::ActiveGenie::Clients::OpenaiClient.new(self)
22
+ end
23
+
24
+ def lower_tier_model
25
+ @lower_tier_model || 'gpt-4o-mini'
26
+ end
27
+
28
+ def middle_tier_model
29
+ @middle_tier_model || 'gpt-4o'
30
+ end
31
+
32
+ def upper_tier_model
33
+ @upper_tier_model || 'o1-preview'
34
+ end
35
+
36
+ def tier_to_model(tier)
37
+ {
38
+ lower_tier: lower_tier_model,
39
+ middle_tier: middle_tier_model,
40
+ upper_tier: upper_tier_model
41
+ }[tier&.to_sym] || lower_tier_model
42
+ end
43
+
44
+ def to_h(config = {})
45
+ {
46
+ api_key:,
47
+ organization:,
48
+ api_url:,
49
+ lower_tier_model:,
50
+ middle_tier_model:,
51
+ upper_tier_model:,
52
+ **config
53
+ }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
1
+ module ActiveGenie::Configuration
2
+ class ProvidersConfig
3
+ def initialize
4
+ @all = {}
5
+ @default = nil
6
+ end
7
+
8
+ def register(name, provider_class)
9
+ @all ||= {}
10
+ @all[name] = provider_class.new
11
+ define_singleton_method(name) do
12
+ instance_variable_get("@#{name}") || instance_variable_set("@#{name}", @all[name])
13
+ end
14
+
15
+ self
16
+ end
17
+
18
+ def default
19
+ @default || @all.values.first
20
+ end
21
+
22
+ def all
23
+ @all
24
+ end
25
+
26
+ def to_h(config = {})
27
+ hash_all = {}
28
+ @all.each do |name, provider|
29
+ hash_all[name] = provider.to_h(config.dig(name) || {})
30
+ end
31
+ hash_all
32
+ end
33
+
34
+ private
35
+ attr_writer :default
36
+ end
37
+ end