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.
- checksums.yaml +4 -4
- data/README.md +36 -72
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +2 -2
- data/lib/active_genie/battle/basic.rb +52 -56
- data/lib/active_genie/battle.rb +1 -1
- data/lib/active_genie/clients/openai_client.rb +119 -0
- data/lib/active_genie/clients/unified_client.rb +19 -0
- data/lib/active_genie/configuration/log_config.rb +14 -0
- data/lib/active_genie/configuration/openai_config.rb +56 -0
- data/lib/active_genie/configuration/providers_config.rb +37 -0
- data/lib/active_genie/configuration.rb +18 -23
- data/lib/active_genie/data_extractor/README.md +4 -5
- data/lib/active_genie/data_extractor/basic.rb +13 -13
- data/lib/active_genie/data_extractor/from_informal.rb +7 -7
- data/lib/active_genie/data_extractor.rb +1 -1
- data/lib/active_genie/logger.rb +72 -0
- data/lib/active_genie/ranking/README.md +43 -0
- data/lib/active_genie/ranking/elo_round.rb +113 -0
- data/lib/active_genie/ranking/free_for_all.rb +76 -0
- data/lib/active_genie/ranking/player.rb +97 -0
- data/lib/active_genie/{leaderboard → ranking}/players_collection.rb +18 -11
- data/lib/active_genie/ranking/ranking.rb +98 -0
- data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
- data/lib/active_genie/ranking.rb +12 -0
- data/lib/active_genie/scoring/README.md +4 -8
- data/lib/active_genie/scoring/basic.rb +58 -24
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +21 -12
- data/lib/active_genie/scoring.rb +4 -4
- data/lib/active_genie.rb +10 -18
- data/lib/tasks/install.rake +3 -3
- data/lib/tasks/templates/active_genie.rb +17 -0
- metadata +74 -90
- data/lib/active_genie/clients/openai.rb +0 -61
- data/lib/active_genie/clients/router.rb +0 -41
- data/lib/active_genie/leaderboard/elo_ranking.rb +0 -88
- data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
- data/lib/active_genie/leaderboard/league.rb +0 -48
- data/lib/active_genie/leaderboard/player.rb +0 -52
- data/lib/active_genie/leaderboard.rb +0 -11
- data/lib/active_genie/utils/math.rb +0 -15
- data/lib/tasks/templates/active_genie.yml +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fbc4033f3c0880973cf732d704921bb61c3cb861c438b732aa6ebe0cc88b2de4
|
4
|
+
data.tar.gz: 11794293170ec43a1d3b3d09e5ec488351a22428a5370ae0e42266a6f8098646
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6b13b3f36a8e516e5126d4cea0de52d046e498834cd10b90b1905460e9bba5ff6e7c4cebd58e6ea2c073176566601c8b806015baa01f11f28792942eec6ca1d
|
7
|
+
data.tar.gz: fc79aed12aaba0ca335d3a7ef8405f4a7c2809de9d5e41ca14fa7bd843d9fb69db8e124b6a2e96321432575a58b271c724a52fd3c1a1165c5ecfe346bedd4b5f
|
data/README.md
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
# ActiveGenie 🧞♂️
|
2
|
-
>
|
2
|
+
> The lodash for GenAI, stop reinventing the wheel
|
3
3
|
|
4
4
|
[](https://badge.fury.io/rb/active_genie)
|
5
5
|
[](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
|
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
|
-
- 📊 **
|
13
|
-
-
|
14
|
-
-
|
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.
|
36
|
-
```
|
37
|
-
|
38
|
-
api_key
|
39
|
-
|
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
|
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
|
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
|
-
###
|
144
|
-
The
|
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
|
-
|
161
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
188
|
-
|
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
|
-
|
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
|
164
|
+
## Configuration
|
201
165
|
|
202
|
-
|
|
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
|
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
|
-
|
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.
|
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,
|
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
|
-
- `
|
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/
|
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(
|
18
|
-
new(
|
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
|
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,
|
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
|
-
@
|
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: #{
|
42
|
-
{ role: 'user', content: "player_b: #{
|
40
|
+
{ role: 'user', content: "player_a: #{@player_a}" },
|
41
|
+
{ role: 'user', content: "player_b: #{@player_b}" },
|
43
42
|
]
|
44
43
|
|
45
|
-
|
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
|
53
|
-
|
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
|
-
|
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
|
-
|
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.
|
80
|
-
2.
|
81
|
-
3.
|
82
|
-
4.
|
83
|
-
5.
|
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
|
-
#
|
86
|
-
-
|
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
|
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
|
-
|
92
|
+
player_a_sell_himself: {
|
109
93
|
type: 'string',
|
110
|
-
description: '
|
111
|
-
|
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
|
-
|
104
|
+
player_b_counter: {
|
114
105
|
type: 'string',
|
115
|
-
description: '
|
106
|
+
description: 'player_b counter arguments why he is the winner compared to player_a. Max of 100 words.',
|
116
107
|
},
|
117
|
-
|
108
|
+
impartial_judge_winner_reasoning: {
|
118
109
|
type: 'string',
|
119
|
-
description: '
|
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
|
}
|
data/lib/active_genie/battle.rb
CHANGED
@@ -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,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
|