active_genie 0.0.3 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +146 -59
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +39 -0
  5. data/lib/active_genie/battle/basic.rb +130 -0
  6. data/lib/active_genie/battle.rb +13 -0
  7. data/lib/active_genie/clients/openai_client.rb +77 -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 -22
  13. data/lib/active_genie/data_extractor/README.md +4 -4
  14. data/lib/active_genie/data_extractor/basic.rb +19 -9
  15. data/lib/active_genie/data_extractor/from_informal.rb +18 -7
  16. data/lib/active_genie/data_extractor.rb +5 -5
  17. data/lib/active_genie/league/README.md +43 -0
  18. data/lib/active_genie/league/elo_ranking.rb +121 -0
  19. data/lib/active_genie/league/free_for_all.rb +62 -0
  20. data/lib/active_genie/league/league.rb +120 -0
  21. data/lib/active_genie/league/player.rb +59 -0
  22. data/lib/active_genie/league/players_collection.rb +68 -0
  23. data/lib/active_genie/league.rb +12 -0
  24. data/lib/active_genie/logger.rb +45 -0
  25. data/lib/active_genie/scoring/README.md +4 -8
  26. data/lib/active_genie/scoring/basic.rb +24 -14
  27. data/lib/active_genie/scoring/recommended_reviews.rb +7 -9
  28. data/lib/active_genie/scoring.rb +5 -5
  29. data/lib/active_genie.rb +14 -11
  30. data/lib/tasks/install.rake +3 -3
  31. data/lib/tasks/templates/active_genie.rb +17 -0
  32. metadata +119 -14
  33. data/lib/active_genie/clients/openai.rb +0 -59
  34. data/lib/active_genie/clients/router.rb +0 -41
  35. data/lib/tasks/templates/active_ai.yml +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7f834cd4da9f695c2a3f69b1d7d5516831afaa2d6830f89527b46e7e19bc6f9
4
- data.tar.gz: 8e14a0011c2551b975105d2120506461fb5f0e9e33206a6960a9de544148016b
3
+ metadata.gz: 9d0424a39ba21d821cb2419730387e1b026c35b5e2e5dff9f6d615f3ec54e6a3
4
+ data.tar.gz: 17b460ccd1a689d0f8709af2b84f3cde65aa0075b76104ee1b4bb8b3b0ffc182
5
5
  SHA512:
6
- metadata.gz: ed3662a287028dacf2f60bda458d0de38ad16d4666421e3b836780d9f2dbb469be935e0b6c3d1fff851f82282c4c4564bebc725354f2f213d0cf21e30ce5ce93
7
- data.tar.gz: 50b0d666a061361322ec2bfb38ef0e49110d6b31be3c3b55e3873f6ca5389003d739f209c7a9e1b695e3901de703effe02344863fed80495c104e886b1d1fa3f
6
+ metadata.gz: ad98b2d5d063d0d1c4009e1a9f92d6d326ed948cbdee71317c94b6a4a0ee57042609c1f405ca7ce00d4beb321bc1e98ad722646349076929bcdc0a28da8b6b8b
7
+ data.tar.gz: d2cc39b77757619c5235041f12d5182778b4237444f3c2246982ebcf54c0542af0da194783cea57fbe4fbde0985ef635802c648796b4f5c4d7a5d4f42c6519c7
data/README.md CHANGED
@@ -1,86 +1,169 @@
1
- # ActiveGenie
2
- Ruby gem for out of the box AI features. LLMs are just a raw tools that need to be polished and refined to be useful. This gem is a collection of tools that can be used to make LLMs more useful and easier to use.
3
- Think this gem is like ActiveStorage but for LLMs.
1
+ # ActiveGenie 🧞‍♂️
2
+ > Transform your Ruby application with powerful, production-ready GenAI features
4
3
 
5
4
  [![Gem Version](https://badge.fury.io/rb/active_genie.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/active_genie)
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
+
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.
8
+
9
+ ## Features
10
+
11
+ - 🎯 **Data Extraction**: Extract structured data from unstructured text with type validation
12
+ - 📊 **Smart Scoring**: Multi-reviewer evaluation system with automatic expert selection
13
+ - 💭 **Leaderboard**: Consistent rank items based on custom criteria, using multiple tecniques of ranking
6
14
 
7
15
  ## Installation
8
- Add this line to your application's Gemfile:
9
16
 
10
- 1. Add the gem to your Gemfile
17
+ 1. Add to your Gemfile:
11
18
  ```ruby
12
19
  gem 'active_genie'
13
20
  ```
14
- 2. install the gem
21
+
22
+ 2. Install the gem:
15
23
  ```shell
16
24
  bundle install
17
25
  ```
18
- 3. call the initializer to create default configurations
26
+
27
+ 3. Generate the configuration:
19
28
  ```shell
29
+ echo "ActiveGenie.load_tasks" >> Rakefile
20
30
  rails g active_genie:install
21
31
  ```
22
- 4. Add the right credentials to the `config/active_genie.yml` file
23
- ```yaml
24
- GPT-4o-mini:
25
- api_key: <%= ENV['OPENAI_API_KEY'] %>
26
- provider: "openai"
32
+
33
+ 4. Configure your credentials in `config/initializers/active_genie.rb`:
34
+ ```ruby
35
+ ActiveGenie.configure do |config|
36
+ config.openai.api_key = ENV['OPENAI_API_KEY']
37
+ end
27
38
  ```
28
39
 
29
- ## Quick Example
40
+ ## Quick Start
41
+
42
+ ### Data Extractor
43
+ Extract structured data from text using AI-powered analysis, handling informal language and complex expressions.
44
+
30
45
  ```ruby
31
- require 'active_genie'
46
+ text = "Nike Air Max 90 - Size 42 - $199.99"
47
+ schema = {
48
+ brand: {
49
+ type: 'string',
50
+ enum: ["Nike", "Adidas", "Puma"]
51
+ },
52
+ price: {
53
+ type: 'number',
54
+ minimum: 0
55
+ },
56
+ size: {
57
+ type: 'integer',
58
+ minimum: 35,
59
+ maximum: 46
60
+ }
61
+ }
32
62
 
33
- puts ActiveGenie::DataExtractor.call(
34
- "Hello, my name is Radamés Roriz",
35
- { full_name: { type: 'string' } },
36
- options: { model: 'gpt-4o-mini', api_key: 'your-api-key', provider: 'openai' } # Optional if has config/active_genie.yml
37
- ) # => { full_name: "Radamés Roriz" }
63
+ result = ActiveGenie::DataExtractor.call(text, schema)
64
+ # => {
65
+ # brand: "Nike",
66
+ # brand_explanation: "Brand name found at start of text",
67
+ # price: 199.99,
68
+ # price_explanation: "Price found in USD format at end",
69
+ # size: 42,
70
+ # size_explanation: "Size explicitly stated in the middle"
71
+ # }
38
72
  ```
39
73
 
40
- ### Data Extractor
41
- Extract structured data from text using LLM-powered analysis, handling informal language and complex expressions.
74
+ Features:
75
+ - Structured data extraction with type validation
76
+ - Schema-based extraction with custom constraints
77
+ - Informal text analysis (litotes, hedging)
78
+ - Detailed explanations for extracted values
79
+
80
+ See the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal text processing, advanced schemas, and detailed interface documentation.
81
+
82
+ ### Scoring
83
+ 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.
84
+
85
+ ```ruby
86
+ text = "The code implements a binary search algorithm with O(log n) complexity"
87
+ criteria = "Evaluate technical accuracy and clarity"
88
+
89
+ result = ActiveGenie::Scoring.basic(text, criteria)
90
+ # => {
91
+ # algorithm_expert_score: 95,
92
+ # algorithm_expert_reasoning: "Accurately describes binary search and its complexity",
93
+ # technical_writer_score: 90,
94
+ # technical_writer_reasoning: "Clear and concise explanation of the algorithm",
95
+ # final_score: 92.5
96
+ # }
97
+ ```
98
+
99
+ Features:
100
+ - Multi-reviewer evaluation with automatic expert selection
101
+ - Detailed feedback with scoring reasoning
102
+ - Customizable reviewer weights
103
+ - Flexible evaluation criteria
104
+
105
+ See the [Scoring README](lib/active_genie/scoring/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
106
+
107
+ ### Battle
108
+ AI-powered battle evaluation system that determines winners between two players based on specified criteria.
42
109
 
43
110
  ```ruby
44
111
  require 'active_genie'
45
112
 
46
- text = "iPhone 14 Pro Max"
47
- schema = {
48
- brand: { type: 'string' },
49
- model: { type: 'string' }
50
- }
51
- result = ActiveGenie::DataExtractor.call(
52
- text,
53
- schema,
54
- options: { model: 'GPT-4o-mini', api_key: 'your-api-key', provider: 'openai' } # Optional if
55
- )
56
- # => { brand: "iPhone", model: "14 Pro Max" }
113
+ player_a = "Implementation uses dependency injection for better testability"
114
+ player_b = "Code has high test coverage but tightly coupled components"
115
+ criteria = "Evaluate code quality and maintainability"
116
+
117
+ result = ActiveGenie::Battle.call(player_a, player_b, criteria)
118
+ # => {
119
+ # winner_player: "Implementation uses dependency injection for better testability",
120
+ # reasoning: "Player A's implementation demonstrates better maintainability through dependency injection,
121
+ # which allows for easier testing and component replacement. While Player B has good test coverage,
122
+ # the tight coupling makes the code harder to maintain and modify.",
123
+ # what_could_be_changed_to_avoid_draw: "Focus on specific architectural patterns and design principles"
124
+ # }
57
125
  ```
58
126
 
59
- - More examples in the [Data Extractor README](lib/data_extractor/README.md)
60
- - Extract from ambiguous [from_informal](lib/data_extractor/README.md#extract-from-informal-text)
61
- - Interface details in the [Interface](lib/data_extractor/README.md#interface)
127
+ Features:
128
+ - Multi-reviewer evaluation with automatic expert selection
129
+ - Detailed feedback with scoring reasoning
130
+ - Customizable reviewer weights
131
+ - Flexible evaluation criteria
132
+
133
+ See the [Battle README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
134
+
135
+ ### League
136
+ The League module provides competitive ranking through multi-stage evaluation:
62
137
 
63
- ### Summarizer (WIP)
64
- 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.
65
138
 
66
139
  ```ruby
67
140
  require 'active_genie'
68
141
 
69
- text = "Example text to be summarized. The fox jumps over the dog"
70
- summarized_text = ActiveGenie::Summarizer.call(text)
71
- puts summarized_text # => "The fox jumps over the dog"
142
+ players = ['REST API', 'GraphQL API', 'SOAP API', 'gRPC API', 'Websocket API']
143
+ criteria = "Best one to be used into a high changing environment"
144
+
145
+ result = ActiveGenie::League.call(players, criteria)
146
+ # => {
147
+ # winner_player: "gRPC API",
148
+ # reasoning: "gRPC API is the best one to be used into a high changing environment",
149
+ # }
72
150
  ```
73
151
 
74
- ### Scorer (WIP)
75
- The scorer is a tool that can be used to score a given text. It uses a set of rules to score the text out of the box. Uses the best practices of prompt engineering and engineering to make the scoring as accurate as possible.
152
+ - **Multi-phase ranking system** combining expert scoring and ELO algorithms
153
+ - **Automatic elimination** of inconsistent performers using statistical analysis
154
+ - **Dynamic ranking adjustments** based on simulated pairwise battles, from bottom to top
155
+
156
+ See the [League README](lib/active_genie/league/README.md) for implementation details, configuration, and advanced ranking strategies.
157
+
158
+ ### Summarizer (WIP)
159
+ 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.
76
160
 
77
161
  ```ruby
78
162
  require 'active_genie'
79
163
 
80
- text = "Example text to be scored. The fox jumps over the dog"
81
- criterias = 'Grammar, Relevance'
82
- score = ActiveGenie::Scorer.call(text, criterias)
83
- puts score # => { 'Grammar' => 0.8, 'Relevance' => 1.0, total: 0.9 }
164
+ text = "Example text to be summarized. The fox jumps over the dog"
165
+ summarized_text = ActiveGenie::Summarizer.call(text)
166
+ puts summarized_text # => "The fox jumps over the dog"
84
167
  ```
85
168
 
86
169
  ### Language detector (WIP)
@@ -116,21 +199,25 @@ sentiment = ActiveGenie::SentimentAnalyzer.call(text)
116
199
  puts sentiment # => "positive"
117
200
  ```
118
201
 
119
- ### Elo ranking (WIP)
120
- 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.
202
+ ## Configuration
121
203
 
122
- ```ruby
123
- require 'active_genie'
204
+ | Config | Description | Default |
205
+ |--------|-------------|---------|
206
+ | `provider` | LLM provider (openai, anthropic, etc) | `nil` |
207
+ | `model` | Model to use | `nil` |
208
+ | `api_key` | Provider API key | `nil` |
209
+ | `timeout` | Request timeout in seconds | `5` |
210
+ | `max_retries` | Maximum retry attempts | `3` |
124
211
 
125
- items = ['Square', 'Circle', 'Triangle']
126
- criterias = 'items that look rounded'
127
- ranked_items = ActiveGenie::EloRanking.call(items, criterias, rounds: 10)
128
- puts ranked_items # => [{ name: "Circle", score: 1500 }, { name: "Square", score: 800 }, { name: "Triangle", score: 800 }]
129
- ```
212
+ > **Note:** Each module can append its own set of configuration, see the individual module documentation for details.
130
213
 
131
- ## Good practices
132
- - LLMs can take a long time to respond, so avoid putting them in the main thread
133
- - Do not use the LLMs to extract sensitive or personal data
214
+ ## Contributing
134
215
 
216
+ 1. Fork the repository
217
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
218
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
219
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
220
+ 5. Open a Pull Request
135
221
  ## License
136
- See the [LICENSE](LICENSE)
222
+
223
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.10
@@ -0,0 +1,39 @@
1
+ # Battle
2
+ AI-powered battle evaluation system that determines winners between two players based on specified criteria.
3
+
4
+ ## Features
5
+ - Content comparison - Evaluate submissions from two players against defined criteria
6
+ - Objective analysis - AI-powered assessment of how well each player meets requirements
7
+ - Detailed reasoning - Comprehensive explanation of why a winner was chosen
8
+ - Draw avoidance - Suggestions on how to modify content to avoid draws
9
+ - Flexible input - Support for both string content and structured data with content field
10
+
11
+ ## Basic Usage
12
+ Evaluate a battle between two players with simple text content:
13
+
14
+ ```ruby
15
+ player_a = "Implementation uses dependency injection for better testability"
16
+ player_b = "Code has high test coverage but tightly coupled components"
17
+ criteria = "Evaluate code quality and maintainability"
18
+
19
+ result = ActiveGenie::Battle::Basic.call(player_a, player_b, criteria)
20
+ # => {
21
+ # winner_player: "Implementation uses dependency injection for better testability",
22
+ # reasoning: "Player A's implementation demonstrates better maintainability through dependency injection,
23
+ # which allows for easier testing and component replacement. While Player B has good test coverage,
24
+ # the tight coupling makes the code harder to maintain and modify.",
25
+ # what_could_be_changed_to_avoid_draw: "Focus on specific architectural patterns and design principles"
26
+ # }
27
+ ```
28
+
29
+ ## Interface
30
+ ### Basic.call(player_a, player_b, criteria, config: {})
31
+ - `player_a` [String, Hash] - The content or submission from the first player
32
+ - `player_b` [String, Hash] - The content or submission from the second player
33
+ - `criteria` [String] - The evaluation criteria or rules to assess against
34
+ - `config` [Hash] - Additional configuration config that modify the battle evaluation behavior
35
+
36
+ Returns a Hash containing:
37
+ - `winner_player` [String, Hash] - The winning player's content (either player_a or player_b)
38
+ - `reasoning` [String] - Detailed explanation of why the winner was chosen
39
+ - `what_could_be_changed_to_avoid_draw` [String] - A suggestion on how to avoid a draw
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../clients/unified_client'
4
+
5
+ module ActiveGenie::Battle
6
+ # The Basic class provides a foundation for evaluating battles between two players
7
+ # using AI-powered evaluation. It determines a winner based on specified criteria,
8
+ # analyzing how well each player meets the requirements.
9
+ #
10
+ # The battle evaluation process compares two players' content against given criteria
11
+ # and returns detailed feedback including the winner and reasoning for the decision.
12
+ #
13
+ # @example Basic usage with two players and criteria
14
+ # Basic.call("Player A content", "Player B content", "Evaluate keyword usage and pattern matching")
15
+ #
16
+ class Basic
17
+ def self.call(player_a, player_b, criteria, config: {})
18
+ new(player_a, player_b, criteria, config:).call
19
+ end
20
+
21
+ # @param player_a [String] The content or submission from the first player
22
+ # @param player_b [String] The content or submission from the second player
23
+ # @param criteria [String] The evaluation criteria or rules to assess against
24
+ # @param config [Hash] Additional configuration config that modify the battle evaluation behavior
25
+ # @return [Hash] The evaluation result containing the winner and reasoning
26
+ # @return [String] :winner The @param player_a or player_b
27
+ # @return [String] :reasoning Detailed explanation of why the winner was chosen
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, config: {})
30
+ @player_a = player_a
31
+ @player_b = player_b
32
+ @criteria = criteria
33
+ @config = config
34
+ @response = nil
35
+ end
36
+
37
+ def call
38
+ messages = [
39
+ { role: 'system', content: PROMPT },
40
+ { 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)}" },
43
+ ]
44
+
45
+ @response = ::ActiveGenie::Clients::UnifiedClient.function_calling(messages, FUNCTION, config:)
46
+
47
+ response_formatted
48
+ end
49
+
50
+ private
51
+
52
+ def player_content(player)
53
+ return player.dig('content') if player.is_a?(Hash)
54
+
55
+ player
56
+ end
57
+
58
+ def response_formatted
59
+ winner = case @response['winner']
60
+ when 'player_a' then @player_a
61
+ when 'player_b' then @player_b
62
+ end
63
+
64
+ @response.merge!('winner' => winner, 'loser' => winner ? (winner == @player_a ? @player_b : @player_a) : nil)
65
+ end
66
+
67
+ PROMPT = <<~PROMPT
68
+ Evaluate a battle between player_a and player_b using predefined criteria and identify the winner.
69
+
70
+ 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.
71
+
72
+ # Steps
73
+ 1. **Review Predefined Criteria**: Understand the specific rules, keywords, and patterns that serve as the basis for evaluation.
74
+ 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.
75
+ 3. **Comparison**: Compare both players against each criterion to determine who aligns better with the standards set.
76
+ 4. **Decision-Making**: Based on the analysis, determine the player who meets the most or all criteria effectively.
77
+ 5. **Provide Justification**: Offer a clear and concise reason for your choice detailing how the winner outperformed the other.
78
+
79
+ # Examples
80
+ - **Example 1**:
81
+ - Input: Player A uses keyword X, follows rule Y, Player B uses keyword Z, breaks rule Y.
82
+ - Output: winner: player_a
83
+ - Justification: Player A successfully used keyword X and followed rule Y, whereas Player B broke rule Y.
84
+
85
+ - **Example 2**:
86
+ - Input: Player A matches pattern P, Player B matches pattern P, uses keyword Q.
87
+ - Output: winner: player_b
88
+ - Justification: Both matched pattern P, but Player B also used keyword Q, meeting more criteria.
89
+
90
+ # Notes
91
+ - Avoid drawing if a clear winner can be discerned.
92
+ - Critically assess each player's adherence to the criteria.
93
+ - Clearly communicate the reasoning behind your decision.
94
+ PROMPT
95
+
96
+ FUNCTION = {
97
+ name: 'battle_evaluation',
98
+ description: 'Evaluate a battle between player_a and player_b using predefined criteria and identify the winner.',
99
+ schema: {
100
+ type: "object",
101
+ properties: {
102
+ winner: {
103
+ type: 'string',
104
+ description: 'The player who won the battle based on the criteria.',
105
+ enum: ['player_a', 'player_b', 'draw']
106
+ },
107
+ reasoning_of_winner: {
108
+ type: 'string',
109
+ description: 'The detailed reasoning about why the winner won based on the criteria.',
110
+ },
111
+ what_could_be_changed_to_avoid_draw: {
112
+ type: 'string',
113
+ description: 'Suggestions on how to avoid a draw based on the criteria. Be as objective and short as possible. Can be empty.',
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ def config
120
+ {
121
+ all_providers: { model_tier: 'lower_tier' },
122
+ log: {
123
+ **(@config.dig(:log) || {}),
124
+ trace: self.class.name,
125
+ },
126
+ **@config
127
+ }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,13 @@
1
+
2
+ require_relative 'battle/basic'
3
+
4
+ module ActiveGenie
5
+ # See the [Battle README](lib/active_genie/battle/README.md) for more information.
6
+ module Battle
7
+ module_function
8
+
9
+ def basic(...)
10
+ Basic.call(...)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,77 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ module ActiveGenie::Clients
5
+ class OpenaiClient
6
+ def initialize(config)
7
+ @app_config = config
8
+ end
9
+
10
+ def function_calling(messages, function, config: {})
11
+ model = config[:model]
12
+ model = @app_config.tier_to_model(config.dig(:all_providers, :model_tier)) if model.nil? && config.dig(:all_providers, :model_tier)
13
+ model = @app_config.lower_tier_model if model.nil?
14
+
15
+ payload = {
16
+ messages:,
17
+ response_format: {
18
+ type: 'json_schema',
19
+ json_schema: function
20
+ },
21
+ model:,
22
+ }
23
+
24
+ api_key = config[:api_key] || @app_config.api_key
25
+ headers = DEFAULT_HEADERS.merge(
26
+ 'Authorization': "Bearer #{api_key}"
27
+ ).compact
28
+
29
+ response = request(payload, headers, config:)
30
+
31
+ parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'content'))
32
+ parsed_response.dig('properties') || parsed_response
33
+ rescue JSON::ParserError
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def request(payload, headers, config:)
40
+ start_time = Time.now
41
+ response = Net::HTTP.post(
42
+ URI("#{@app_config.api_url}/chat/completions"),
43
+ payload.to_json,
44
+ headers
45
+ )
46
+
47
+ raise OpenaiError, response.body unless response.is_a?(Net::HTTPSuccess)
48
+ return nil if response.body.empty?
49
+
50
+ parsed_body = JSON.parse(response.body)
51
+ log_response(start_time, parsed_body, config:)
52
+
53
+ parsed_body
54
+ end
55
+
56
+ DEFAULT_HEADERS = {
57
+ 'Content-Type': 'application/json',
58
+ }
59
+
60
+ def log_response(start_time, response, config:)
61
+ ActiveGenie::Logger.trace(
62
+ {
63
+ **config.dig(:log),
64
+ category: :llm,
65
+ trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
66
+ total_tokens: response.dig('usage', 'total_tokens'),
67
+ model: response.dig('model'),
68
+ request_duration: Time.now - start_time,
69
+ openai: response
70
+ }
71
+ )
72
+ end
73
+
74
+ # TODO: add some more rich error handling
75
+ class OpenaiError < StandardError; end
76
+ end
77
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveGenie::Clients
2
+ class UnifiedClient
3
+ class << self
4
+ def function_calling(messages, function, 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, 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 lower_tier_model
17
+ @lower_tier_model || 'gpt-4o-mini'
18
+ end
19
+
20
+ def middle_tier_model
21
+ @middle_tier_model || 'gpt-4o'
22
+ end
23
+
24
+ def upper_tier_model
25
+ @upper_tier_model || 'o1-preview'
26
+ end
27
+
28
+ def tier_to_model(tier)
29
+ {
30
+ lower_tier: lower_tier_model,
31
+ middle_tier: middle_tier_model,
32
+ upper_tier: upper_tier_model
33
+ }[tier&.to_sym]
34
+ end
35
+
36
+ def api_url
37
+ @api_url || 'https://api.openai.com/v1'
38
+ end
39
+
40
+ def client
41
+ @client ||= ::ActiveGenie::Clients::OpenaiClient.new(self)
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