active_genie 0.0.3 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +133 -47
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +39 -0
- data/lib/active_genie/battle/basic.rb +125 -0
- data/lib/active_genie/battle.rb +13 -0
- data/lib/active_genie/clients/openai.rb +3 -1
- data/lib/active_genie/configuration.rb +3 -2
- data/lib/active_genie/data_extractor.rb +4 -4
- data/lib/active_genie/leaderboard/elo_ranking.rb +88 -0
- data/lib/active_genie/leaderboard/leaderboard.rb +72 -0
- data/lib/active_genie/leaderboard/league.rb +48 -0
- data/lib/active_genie/leaderboard/player.rb +52 -0
- data/lib/active_genie/leaderboard/players_collection.rb +68 -0
- data/lib/active_genie/leaderboard.rb +11 -0
- data/lib/active_genie/scoring/basic.rb +7 -6
- data/lib/active_genie/scoring/recommended_reviews.rb +1 -1
- data/lib/active_genie/scoring.rb +4 -4
- data/lib/active_genie/utils/math.rb +15 -0
- data/lib/active_genie.rb +18 -7
- data/lib/tasks/install.rake +1 -1
- data/lib/tasks/templates/{active_ai.yml → active_genie.yml} +1 -1
- metadata +112 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 834787b505134623a3f6c4995c9dec8a0ce89a1a014600c4d676d8801bf541cd
|
4
|
+
data.tar.gz: 16ae75ffa3926ecd87052d72ca3f5434be80327b712bb2dba54e55c46fa5ff8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1772fbfa59891e7a3673b50a54bd90914007917854e78fb5f0458db681bf89bd1ec118600d23ab98d6255e1b063b5d582d3b73dc738e66aeadc7f4f0bee75af8
|
7
|
+
data.tar.gz: bb6a0ea9ef737f7a2ed3ec558dcea5c67bcc2f90a155828e1e25a5a7a3ebd9d0fe6c5422dd9f3d9ff6fb667b63822e3bc86627cdb3e40a3c72c4ef50cee32c68
|
data/README.md
CHANGED
@@ -1,86 +1,154 @@
|
|
1
|
-
# ActiveGenie
|
2
|
-
|
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
|
[](https://badge.fury.io/rb/active_genie)
|
5
|
+
[](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
|
+
- 💭 **Sentiment Analysis**: Advanced sentiment analysis with customizable rules
|
14
|
+
- 🔒 **Safe & Secure**: Built-in validation and sanitization
|
15
|
+
- 🛠️ **Configurable**: Supports multiple GenAI providers and models
|
6
16
|
|
7
17
|
## Installation
|
8
|
-
Add this line to your application's Gemfile:
|
9
18
|
|
10
|
-
1. Add
|
19
|
+
1. Add to your Gemfile:
|
11
20
|
```ruby
|
12
21
|
gem 'active_genie'
|
13
22
|
```
|
14
|
-
|
23
|
+
|
24
|
+
2. Install the gem:
|
15
25
|
```shell
|
16
26
|
bundle install
|
17
27
|
```
|
18
|
-
|
28
|
+
|
29
|
+
3. Generate the configuration:
|
19
30
|
```shell
|
31
|
+
echo "ActiveGenie.load_tasks" >> Rakefile
|
20
32
|
rails g active_genie:install
|
21
33
|
```
|
22
|
-
|
34
|
+
|
35
|
+
4. [Optional] Configure your credentials in `config/active_genie.yml`:
|
23
36
|
```yaml
|
24
37
|
GPT-4o-mini:
|
25
38
|
api_key: <%= ENV['OPENAI_API_KEY'] %>
|
26
39
|
provider: "openai"
|
40
|
+
|
41
|
+
claude-3-5-sonnet:
|
42
|
+
api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
|
43
|
+
provider: "anthropic"
|
27
44
|
```
|
28
45
|
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
require 'active_genie'
|
46
|
+
> The first key will be used as default in all modules, in this example `GPT-4o-mini`
|
32
47
|
|
33
|
-
|
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" }
|
38
|
-
```
|
48
|
+
## Quick Start
|
39
49
|
|
40
50
|
### Data Extractor
|
41
|
-
Extract structured data from text using
|
51
|
+
Extract structured data from text using AI-powered analysis, handling informal language and complex expressions.
|
42
52
|
|
43
53
|
```ruby
|
44
|
-
|
45
|
-
|
46
|
-
text = "iPhone 14 Pro Max"
|
54
|
+
text = "Nike Air Max 90 - Size 42 - $199.99"
|
47
55
|
schema = {
|
48
|
-
brand: {
|
49
|
-
|
56
|
+
brand: {
|
57
|
+
type: 'string',
|
58
|
+
enum: ["Nike", "Adidas", "Puma"]
|
59
|
+
},
|
60
|
+
price: {
|
61
|
+
type: 'number',
|
62
|
+
minimum: 0
|
63
|
+
},
|
64
|
+
size: {
|
65
|
+
type: 'integer',
|
66
|
+
minimum: 35,
|
67
|
+
maximum: 46
|
68
|
+
}
|
50
69
|
}
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
#
|
70
|
+
|
71
|
+
result = ActiveGenie::DataExtractor.call(text, schema)
|
72
|
+
# => {
|
73
|
+
# brand: "Nike",
|
74
|
+
# brand_explanation: "Brand name found at start of text",
|
75
|
+
# price: 199.99,
|
76
|
+
# price_explanation: "Price found in USD format at end",
|
77
|
+
# size: 42,
|
78
|
+
# size_explanation: "Size explicitly stated in the middle"
|
79
|
+
# }
|
57
80
|
```
|
58
81
|
|
59
|
-
|
60
|
-
-
|
61
|
-
-
|
82
|
+
Features:
|
83
|
+
- Structured data extraction with type validation
|
84
|
+
- Schema-based extraction with custom constraints
|
85
|
+
- Informal text analysis (litotes, hedging)
|
86
|
+
- Detailed explanations for extracted values
|
62
87
|
|
63
|
-
|
64
|
-
|
88
|
+
See the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal text processing, advanced schemas, and detailed interface documentation.
|
89
|
+
|
90
|
+
### Scoring
|
91
|
+
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
|
+
|
93
|
+
```ruby
|
94
|
+
text = "The code implements a binary search algorithm with O(log n) complexity"
|
95
|
+
criteria = "Evaluate technical accuracy and clarity"
|
96
|
+
|
97
|
+
result = ActiveGenie::Scoring::Basic.call(text, criteria)
|
98
|
+
# => {
|
99
|
+
# algorithm_expert_score: 95,
|
100
|
+
# algorithm_expert_reasoning: "Accurately describes binary search and its complexity",
|
101
|
+
# technical_writer_score: 90,
|
102
|
+
# technical_writer_reasoning: "Clear and concise explanation of the algorithm",
|
103
|
+
# final_score: 92.5
|
104
|
+
# }
|
105
|
+
```
|
106
|
+
|
107
|
+
Features:
|
108
|
+
- Multi-reviewer evaluation with automatic expert selection
|
109
|
+
- Detailed feedback with scoring reasoning
|
110
|
+
- Customizable reviewer weights
|
111
|
+
- Flexible evaluation criteria
|
112
|
+
|
113
|
+
See the [Scoring README](lib/active_genie/scoring/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
|
114
|
+
|
115
|
+
### Battle
|
116
|
+
AI-powered battle evaluation system that determines winners between two players based on specified criteria.
|
65
117
|
|
66
118
|
```ruby
|
67
119
|
require 'active_genie'
|
68
120
|
|
69
|
-
|
70
|
-
|
71
|
-
|
121
|
+
player_a = "Implementation uses dependency injection for better testability"
|
122
|
+
player_b = "Code has high test coverage but tightly coupled components"
|
123
|
+
criteria = "Evaluate code quality and maintainability"
|
124
|
+
|
125
|
+
result = ActiveGenie::Battle::Basic.call(player_a, player_b, criteria)
|
126
|
+
# => {
|
127
|
+
# winner_player: "Implementation uses dependency injection for better testability",
|
128
|
+
# reasoning: "Player A's implementation demonstrates better maintainability through dependency injection,
|
129
|
+
# which allows for easier testing and component replacement. While Player B has good test coverage,
|
130
|
+
# the tight coupling makes the code harder to maintain and modify.",
|
131
|
+
# what_could_be_changed_to_avoid_draw: "Focus on specific architectural patterns and design principles"
|
132
|
+
# }
|
72
133
|
```
|
73
134
|
|
74
|
-
|
75
|
-
|
135
|
+
Features:
|
136
|
+
- Multi-reviewer evaluation with automatic expert selection
|
137
|
+
- Detailed feedback with scoring reasoning
|
138
|
+
- Customizable reviewer weights
|
139
|
+
- Flexible evaluation criteria
|
140
|
+
|
141
|
+
See the [Battle README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
|
142
|
+
|
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.
|
76
145
|
|
77
146
|
```ruby
|
78
147
|
require 'active_genie'
|
79
148
|
|
80
|
-
text = "Example text to be
|
81
|
-
|
82
|
-
|
83
|
-
puts score # => { 'Grammar' => 0.8, 'Relevance' => 1.0, total: 0.9 }
|
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"
|
84
152
|
```
|
85
153
|
|
86
154
|
### Language detector (WIP)
|
@@ -128,9 +196,27 @@ ranked_items = ActiveGenie::EloRanking.call(items, criterias, rounds: 10)
|
|
128
196
|
puts ranked_items # => [{ name: "Circle", score: 1500 }, { name: "Square", score: 800 }, { name: "Triangle", score: 800 }]
|
129
197
|
```
|
130
198
|
|
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
|
134
199
|
|
200
|
+
## Configuration Options
|
201
|
+
|
202
|
+
| Option | Description | Default |
|
203
|
+
|--------|-------------|---------|
|
204
|
+
| `provider` | LLM provider (openai, anthropic, etc) | `nil` |
|
205
|
+
| `model` | Model to use | `nil` |
|
206
|
+
| `api_key` | Provider API key | `nil` |
|
207
|
+
| `timeout` | Request timeout in seconds | `5` |
|
208
|
+
| `max_retries` | Maximum retry attempts | `3` |
|
209
|
+
|
210
|
+
> **Note:** Each module can append its own set of configuration options, see the individual module documentation for details.
|
211
|
+
|
212
|
+
## Contributing
|
213
|
+
|
214
|
+
1. Fork the repository
|
215
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
216
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
217
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
218
|
+
5. Open a Pull Request
|
135
219
|
## License
|
136
|
-
|
220
|
+
|
221
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
222
|
+
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.8
|
@@ -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, options: {})
|
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
|
+
- `options` [Hash] - Additional configuration options 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,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../clients/router'
|
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, options: {})
|
18
|
+
new(player_a, player_b, criteria, options:).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 options [Hash] Additional configuration options 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, options: {})
|
30
|
+
@player_a = player_a
|
31
|
+
@player_b = player_b
|
32
|
+
@criteria = criteria
|
33
|
+
@options = options
|
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::Router.function_calling(messages, FUNCTION, options: @options)
|
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
|
+
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
|
71
|
+
end
|
72
|
+
|
73
|
+
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.
|
77
|
+
|
78
|
+
# 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.
|
84
|
+
|
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.
|
95
|
+
|
96
|
+
# Notes
|
97
|
+
- Avoid drawing if a clear winner can be discerned.
|
98
|
+
- Critically assess each player's adherence to the criteria.
|
99
|
+
- Clearly communicate the reasoning behind your decision.
|
100
|
+
PROMPT
|
101
|
+
|
102
|
+
FUNCTION = {
|
103
|
+
name: 'battle_evaluation',
|
104
|
+
description: 'Evaluate a battle between player_a and player_b using predefined criteria and identify the winner.',
|
105
|
+
schema: {
|
106
|
+
type: "object",
|
107
|
+
properties: {
|
108
|
+
winner: {
|
109
|
+
type: 'string',
|
110
|
+
description: 'The player who won the battle based on the criteria.',
|
111
|
+
enum: ['player_a', 'player_b', 'draw']
|
112
|
+
},
|
113
|
+
reasoning_of_winner: {
|
114
|
+
type: 'string',
|
115
|
+
description: 'The detailed reasoning about why the winner won based on the criteria.',
|
116
|
+
},
|
117
|
+
what_could_be_changed_to_avoid_draw: {
|
118
|
+
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
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
@@ -28,7 +28,9 @@ module ActiveGenie::Clients
|
|
28
28
|
|
29
29
|
response = request(payload, headers)
|
30
30
|
|
31
|
-
JSON.parse(response.dig('choices', 0, 'message', 'content'))
|
31
|
+
parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'content'))
|
32
|
+
|
33
|
+
parsed_response.dig('properties') || parsed_response
|
32
34
|
rescue JSON::ParserError
|
33
35
|
nil
|
34
36
|
end
|
@@ -5,7 +5,7 @@ module ActiveGenie
|
|
5
5
|
attr_accessor :path_to_config
|
6
6
|
|
7
7
|
def initialize
|
8
|
-
@path_to_config = File.join(
|
8
|
+
@path_to_config = File.join('config', 'active_genie.yml')
|
9
9
|
end
|
10
10
|
|
11
11
|
def values
|
@@ -23,7 +23,8 @@ module ActiveGenie
|
|
23
23
|
def load_values
|
24
24
|
return {} unless File.exist?(@path_to_config)
|
25
25
|
|
26
|
-
|
26
|
+
yaml_content = ERB.new(File.read(@path_to_config)).result
|
27
|
+
YAML.safe_load(yaml_content, aliases: true) || {}
|
27
28
|
rescue Psych::SyntaxError => e
|
28
29
|
warn "ActiveGenie.warning: Config file '#{@path_to_config}' is not a valid YAML file (#{e.message}), using default configuration"
|
29
30
|
{}
|
@@ -6,12 +6,12 @@ module ActiveGenie
|
|
6
6
|
module DataExtractor
|
7
7
|
module_function
|
8
8
|
|
9
|
-
def basic(
|
10
|
-
Basic.call(
|
9
|
+
def basic(...)
|
10
|
+
Basic.call(...)
|
11
11
|
end
|
12
12
|
|
13
|
-
def from_informal(
|
14
|
-
FromInformal.call(
|
13
|
+
def from_informal(...)
|
14
|
+
FromInformal.call(...)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_relative '../battle/basic'
|
2
|
+
require_relative '../utils/math'
|
3
|
+
|
4
|
+
module ActiveGenie::Leaderboard
|
5
|
+
class EloRanking
|
6
|
+
def self.call(players, criteria, options: {})
|
7
|
+
new(players, criteria, options:).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(players, criteria, options: {})
|
11
|
+
@players = players
|
12
|
+
@criteria = criteria
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
@players.each(&:generate_elo_by_score)
|
18
|
+
|
19
|
+
while @players.eligible_size > MINIMAL_PLAYERS_TO_BATTLE
|
20
|
+
round = create_round(@players.tier_relegation, @players.tier_defense)
|
21
|
+
|
22
|
+
round.each do |player_a, player_b|
|
23
|
+
winner, loser = battle(player_a, player_b) # This can take a while, can be parallelized
|
24
|
+
update_elo(winner, loser)
|
25
|
+
end
|
26
|
+
|
27
|
+
@players.tier_relegation.each { |player| player.eliminated = "relegation/#{@players.eligible_size}" }
|
28
|
+
end
|
29
|
+
|
30
|
+
@players
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
MATCHS_PER_PLAYER = 3
|
36
|
+
LOSE_PENALTY = 15
|
37
|
+
MINIMAL_PLAYERS_TO_BATTLE = 10
|
38
|
+
|
39
|
+
# Create a round of matches
|
40
|
+
# each round is exactly 1 regation player vs 3 defense players for all regation players
|
41
|
+
# each match is unique (player vs player)
|
42
|
+
# each defense player is battle exactly 3 times
|
43
|
+
def create_round(relegation_players, defense_players)
|
44
|
+
matches = []
|
45
|
+
|
46
|
+
relegation_players.each do |player_a|
|
47
|
+
player_enemies = []
|
48
|
+
MATCHS_PER_PLAYER.times do
|
49
|
+
defender = nil
|
50
|
+
while defender.nil? || player_enemies.include?(defender.id)
|
51
|
+
defender = defense_players.sample
|
52
|
+
end
|
53
|
+
|
54
|
+
matches << [player_a, defender].shuffle
|
55
|
+
player_enemies << defender.id
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
matches
|
60
|
+
end
|
61
|
+
|
62
|
+
def battle(player_a, player_b)
|
63
|
+
ActiveGenie::Battle.basic(
|
64
|
+
player_a,
|
65
|
+
player_b,
|
66
|
+
@criteria,
|
67
|
+
options: @options
|
68
|
+
).values_at('winner', 'loser')
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_elo(winner, loser)
|
72
|
+
return if winner.nil? || loser.nil?
|
73
|
+
|
74
|
+
new_winner_elo, new_loser_elo = ActiveGenie::Utils::Math.calculate_new_elo(winner.elo, loser.elo)
|
75
|
+
|
76
|
+
winner.elo = [new_winner_elo, max_defense_elo].min
|
77
|
+
loser.elo = [new_loser_elo - LOSE_PENALTY, min_relegation_elo].max
|
78
|
+
end
|
79
|
+
|
80
|
+
def max_defense_elo
|
81
|
+
@players.tier_defense.max_by(&:elo).elo
|
82
|
+
end
|
83
|
+
|
84
|
+
def min_relegation_elo
|
85
|
+
@players.tier_relegation.min_by(&:elo).elo
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative './players_collection'
|
2
|
+
require_relative './league'
|
3
|
+
require_relative './elo_ranking'
|
4
|
+
require_relative '../scoring/recommended_reviews'
|
5
|
+
|
6
|
+
module ActiveGenie::Leaderboard
|
7
|
+
class Leaderboard
|
8
|
+
def self.call(param_players, criteria, options: {})
|
9
|
+
new(param_players, criteria, options:).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(param_players, criteria, options: {})
|
13
|
+
@param_players = param_players
|
14
|
+
@criteria = criteria
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
set_initial_score_players
|
20
|
+
eliminate_obvious_bad_players
|
21
|
+
run_elo_ranking if players.eligible_size > 10
|
22
|
+
run_league
|
23
|
+
|
24
|
+
players.to_h
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
SCORE_VARIATION_THRESHOLD = 10
|
30
|
+
MATCHS_PER_PLAYER = 3
|
31
|
+
|
32
|
+
def set_initial_score_players
|
33
|
+
players.each do |player|
|
34
|
+
player.score = generate_score(player.content) # This can take a while, can be parallelized
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_score(content)
|
39
|
+
ActiveGenie::Scoring::Basic.call(content, @criteria, reviewers, options: @options)['final_score']
|
40
|
+
end
|
41
|
+
|
42
|
+
def eliminate_obvious_bad_players
|
43
|
+
while players.coefficient_of_variation >= SCORE_VARIATION_THRESHOLD
|
44
|
+
players.eligible.last.eliminated = 'too_low_score'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_elo_ranking
|
49
|
+
EloRanking.call(players, @criteria, options: @options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def run_league
|
53
|
+
League.call(players, @criteria, options: @options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def reviewers
|
57
|
+
[recommended_reviews['reviewer1'], recommended_reviews['reviewer2'], recommended_reviews['reviewer3']]
|
58
|
+
end
|
59
|
+
|
60
|
+
def recommended_reviews
|
61
|
+
@recommended_reviews ||= ActiveGenie::Scoring::RecommendedReviews.call(
|
62
|
+
players.sample,
|
63
|
+
@criteria,
|
64
|
+
options: @options
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def players
|
69
|
+
@players ||= PlayersCollection.new(@param_players)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../battle/basic'
|
2
|
+
|
3
|
+
module ActiveGenie::Leaderboard
|
4
|
+
class League
|
5
|
+
def self.call(players, criteria, options: {})
|
6
|
+
new(players, criteria, options:).call
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(players, criteria, options: {})
|
10
|
+
@players = players
|
11
|
+
@criteria = criteria
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
matches.each do |player_a, player_b|
|
17
|
+
winner, loser = battle(player_a, player_b)
|
18
|
+
|
19
|
+
if winner.nil? || loser.nil?
|
20
|
+
player_a.league[:draw] += 1
|
21
|
+
player_b.league[:draw] += 1
|
22
|
+
else
|
23
|
+
winner.league[:win] += 1
|
24
|
+
loser.league[:lose] += 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@players
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# TODO: reduce the number of matches based on transitivity.
|
34
|
+
# For example, if A is better than B, and B is better than C, then A should clearly be better than C
|
35
|
+
def matches
|
36
|
+
@players.eligible.combination(2).to_a
|
37
|
+
end
|
38
|
+
|
39
|
+
def battle(player_a, player_b)
|
40
|
+
ActiveGenie::Battle.basic(
|
41
|
+
player_a,
|
42
|
+
player_b,
|
43
|
+
@criteria,
|
44
|
+
options: @options
|
45
|
+
).values_at('winner', 'loser')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module ActiveGenie::Leaderboard
|
4
|
+
class Player
|
5
|
+
def initialize(params)
|
6
|
+
params = { content: params } if params.is_a?(String)
|
7
|
+
|
8
|
+
@id = params.dig(:id) || SecureRandom.uuid
|
9
|
+
@content = params.dig(:content) || params
|
10
|
+
@score = params.dig(:score) || nil
|
11
|
+
@elo = params.dig(:elo) || nil
|
12
|
+
@league = {
|
13
|
+
win: params.dig(:league, :win) || 0,
|
14
|
+
lose: params.dig(:league, :lose) || 0,
|
15
|
+
draw: params.dig(:league, :draw) || 0
|
16
|
+
}
|
17
|
+
@eliminated = params.dig(:eliminated) || nil
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :id, :content, :score, :elo, :league, :eliminated
|
21
|
+
|
22
|
+
def generate_elo_by_score
|
23
|
+
return if !@elo.nil? || @score.nil?
|
24
|
+
|
25
|
+
@elo = BASE_ELO + (@score - 50)
|
26
|
+
end
|
27
|
+
|
28
|
+
def score=(value)
|
29
|
+
@score = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def elo=(value)
|
33
|
+
@elo = value
|
34
|
+
end
|
35
|
+
|
36
|
+
def eliminated=(value)
|
37
|
+
@eliminated = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def league_score
|
41
|
+
@league[:win] * 3 + @league[:draw]
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h
|
45
|
+
{ id:, content:, score:, elo:, eliminated:, league: }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
BASE_ELO = 1000
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative '../utils/math'
|
2
|
+
require_relative './player'
|
3
|
+
|
4
|
+
module ActiveGenie::Leaderboard
|
5
|
+
class PlayersCollection
|
6
|
+
def initialize(param_players)
|
7
|
+
@players = build(param_players)
|
8
|
+
end
|
9
|
+
attr_reader :players
|
10
|
+
|
11
|
+
def coefficient_of_variation
|
12
|
+
score_list = eligible.map(&:score)
|
13
|
+
mean = score_list.sum.to_f / score_list.size
|
14
|
+
return nil if mean == 0 # To avoid division by zero
|
15
|
+
|
16
|
+
variance = score_list.map { |num| (num - mean) ** 2 }.sum / score_list.size
|
17
|
+
standard_deviation = Math.sqrt(variance)
|
18
|
+
|
19
|
+
(standard_deviation / mean) * 100
|
20
|
+
end
|
21
|
+
|
22
|
+
def tier_relegation
|
23
|
+
eligible[(tier_size*-1)..-1]
|
24
|
+
end
|
25
|
+
|
26
|
+
def tier_defense
|
27
|
+
eligible[(tier_size*-2)...(tier_size*-1)]
|
28
|
+
end
|
29
|
+
|
30
|
+
def eligible
|
31
|
+
sorted.reject(&:eliminated)
|
32
|
+
end
|
33
|
+
|
34
|
+
def eligible_size
|
35
|
+
@players.reject(&:eliminated).size
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_h
|
39
|
+
sorted.map(&:to_h)
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(...)
|
43
|
+
@players.send(...)
|
44
|
+
end
|
45
|
+
|
46
|
+
def sorted
|
47
|
+
@players.sort_by { |p| [-p.league_score, -(p.elo || 0), -p.score] }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def build(param_players)
|
53
|
+
param_players.map { |player| Player.new(player) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the number of players to battle in each round
|
57
|
+
# based on the eligible size, start fast and go slow until top 10
|
58
|
+
# Example:
|
59
|
+
# - 50 eligible, tier_size: 15
|
60
|
+
# - 35 eligible, tier_size: 11
|
61
|
+
# - 24 eligible, tier_size: 10
|
62
|
+
# - 14 eligible, tier_size: 4
|
63
|
+
# 4 rounds to reach top 10 with 50 players
|
64
|
+
def tier_size
|
65
|
+
[[(eligible_size / 3).ceil, 10].max, eligible_size - 10].min
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -17,12 +17,6 @@ module ActiveGenie::Scoring
|
|
17
17
|
# Basic.call("Sample text", "Evaluate technical accuracy")
|
18
18
|
#
|
19
19
|
class Basic
|
20
|
-
def self.call(text, criteria, reviewers = [], options: {})
|
21
|
-
new(text, criteria, reviewers, options:).call
|
22
|
-
end
|
23
|
-
|
24
|
-
# Initializes a new scoring evaluation instance
|
25
|
-
#
|
26
20
|
# @param text [String] The text content to be evaluated
|
27
21
|
# @param criteria [String] The evaluation criteria or rubric to assess against
|
28
22
|
# @param reviewers [Array<String>] Optional list of specific reviewers. If empty,
|
@@ -30,6 +24,13 @@ module ActiveGenie::Scoring
|
|
30
24
|
# @param options [Hash] Additional configuration options that modify the scoring behavior
|
31
25
|
# @option options [Boolean] :detailed_feedback Request more detailed feedback in the reasoning
|
32
26
|
# @option options [Hash] :reviewer_weights Custom weights for different reviewers
|
27
|
+
# @return [Hash] The evaluation result containing the scores and reasoning
|
28
|
+
# @return [Number] :final_score The final score of the text based on the criteria and reviewers
|
29
|
+
# @return [String] :final_reasoning Detailed explanation of why the final score was reached
|
30
|
+
def self.call(text, criteria, reviewers = [], options: {})
|
31
|
+
new(text, criteria, reviewers, options:).call
|
32
|
+
end
|
33
|
+
|
33
34
|
def initialize(text, criteria, reviewers = [], options: {})
|
34
35
|
@text = text
|
35
36
|
@criteria = criteria
|
data/lib/active_genie/scoring.rb
CHANGED
@@ -6,12 +6,12 @@ module ActiveGenie
|
|
6
6
|
module Scoring
|
7
7
|
module_function
|
8
8
|
|
9
|
-
def basic(
|
10
|
-
Basic.call(
|
9
|
+
def basic(...)
|
10
|
+
Basic.call(...)
|
11
11
|
end
|
12
12
|
|
13
|
-
def recommended_reviews(
|
14
|
-
RecommendedReviews.call(
|
13
|
+
def recommended_reviews(...)
|
14
|
+
RecommendedReviews.call(...)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveGenie::Utils
|
2
|
+
module Math
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def self.calculate_new_elo(winner, loser, k: 32)
|
6
|
+
expected_score_a = 1 / (1 + 10**((loser - winner) / 400))
|
7
|
+
expected_score_b = 1 - expected_score_a
|
8
|
+
|
9
|
+
new_elo_winner = winner + k * (1 - expected_score_a)
|
10
|
+
new_elo_loser = loser + k * (1 - expected_score_b)
|
11
|
+
|
12
|
+
[new_elo_winner, new_elo_loser]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/active_genie.rb
CHANGED
@@ -1,22 +1,33 @@
|
|
1
1
|
module ActiveGenie
|
2
|
+
autoload :Configuration, File.join(__dir__, 'active_genie/configuration')
|
3
|
+
|
4
|
+
# Modules
|
2
5
|
autoload :DataExtractor, File.join(__dir__, 'active_genie/data_extractor')
|
6
|
+
autoload :Battle, File.join(__dir__, 'active_genie/battle')
|
3
7
|
autoload :Scoring, File.join(__dir__, 'active_genie/scoring')
|
4
|
-
autoload :
|
8
|
+
autoload :Leaderboard, File.join(__dir__, 'active_genie/leaderboard')
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def configure
|
12
|
+
yield(config) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_tasks
|
16
|
+
return unless defined?(Rake)
|
17
|
+
|
18
|
+
Rake::Task.define_task(:environment)
|
19
|
+
Dir.glob(File.join(__dir__, 'tasks', '*.rake')).each { |r| load r }
|
20
|
+
end
|
5
21
|
|
6
|
-
class << self
|
7
22
|
def config
|
8
23
|
@config ||= Configuration.new
|
9
24
|
end
|
10
25
|
|
11
|
-
def configure
|
12
|
-
yield(config) if block_given?
|
13
|
-
end
|
14
|
-
|
15
26
|
def [](key)
|
16
27
|
config.values[key.to_s]
|
17
28
|
end
|
18
29
|
|
19
|
-
def config_by_model(model)
|
30
|
+
def config_by_model(model = nil)
|
20
31
|
config.values[model&.to_s&.downcase&.strip] || config.values.values.first || {}
|
21
32
|
end
|
22
33
|
end
|
data/lib/tasks/install.rake
CHANGED
@@ -3,7 +3,7 @@ require 'fileutils'
|
|
3
3
|
namespace :active_genie do
|
4
4
|
desc 'Install active_genie configuration file'
|
5
5
|
task :install do
|
6
|
-
source = File.join(
|
6
|
+
source = File.join(__dir__, 'templates', 'active_genie.yml')
|
7
7
|
target = File.join('config', 'active_genie.yml')
|
8
8
|
|
9
9
|
FileUtils.cp(source, target)
|
metadata
CHANGED
@@ -1,22 +1,113 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_genie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Radamés Roriz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
description: "# ActiveGenie \U0001F9DE♂️\n> Transform your Ruby application with
|
14
|
+
powerful, production-ready GenAI features\n\n[](https://badge.fury.io/rb/active_genie)\n[](https://github.com/roriz/active_genie/actions/workflows/ruby.yml)\n\nActiveGenie
|
15
|
+
is a Ruby gem that provides a polished, production-ready interface for working with
|
16
|
+
Generative AI (GenAI) models. Just like ActiveStorage simplifies file handling in
|
17
|
+
Rails, ActiveGenie makes it effortless to integrate GenAI capabilities into your
|
18
|
+
Ruby applications.\n\n## Features\n\n- \U0001F3AF **Data Extraction**: Extract structured
|
19
|
+
data from unstructured text with type validation\n- \U0001F4CA **Smart Scoring**:
|
20
|
+
Multi-reviewer evaluation system with automatic expert selection\n- \U0001F4AD **Sentiment
|
21
|
+
Analysis**: Advanced sentiment analysis with customizable rules\n- \U0001F512 **Safe
|
22
|
+
& Secure**: Built-in validation and sanitization\n- \U0001F6E0️ **Configurable**:
|
23
|
+
Supports multiple GenAI providers and models\n\n## Installation\n\n1. Add to your
|
24
|
+
Gemfile:\n```ruby\ngem 'active_genie'\n```\n\n2. Install the gem:\n```shell\nbundle
|
25
|
+
install\n```\n\n3. Generate the configuration:\n```shell\necho \"ActiveGenie.load_tasks\"
|
26
|
+
>> Rakefile\nrails g active_genie:install\n```\n\n4. [Optional] Configure your credentials
|
27
|
+
in `config/active_genie.yml`:\n```yaml\nGPT-4o-mini:\n api_key: <%= ENV['OPENAI_API_KEY']
|
28
|
+
%>\n provider: \"openai\"\n\nclaude-3-5-sonnet:\n api_key: <%= ENV['ANTHROPIC_API_KEY']
|
29
|
+
%>\n provider: \"anthropic\"\n```\n\n> The first key will be used as default in
|
30
|
+
all modules, in this example `GPT-4o-mini`\n\n## Quick Start\n\n### Data Extractor\nExtract
|
31
|
+
structured data from text using AI-powered analysis, handling informal language
|
32
|
+
and complex expressions.\n\n```ruby\ntext = \"Nike Air Max 90 - Size 42 - $199.99\"\nschema
|
33
|
+
= {\n brand: { \n type: 'string',\n enum: [\"Nike\", \"Adidas\", \"Puma\"]\n
|
34
|
+
\ },\n price: { \n type: 'number',\n minimum: 0\n },\n size: {\n type:
|
35
|
+
'integer',\n minimum: 35,\n maximum: 46\n }\n}\n\nresult = ActiveGenie::DataExtractor.call(text,
|
36
|
+
schema)\n# => { \n# brand: \"Nike\", \n# brand_explanation: \"Brand name
|
37
|
+
found at start of text\",\n# price: 199.99,\n# price_explanation: \"Price
|
38
|
+
found in USD format at end\",\n# size: 42,\n# size_explanation: \"Size
|
39
|
+
explicitly stated in the middle\"\n# }\n```\n\nFeatures:\n- Structured data extraction
|
40
|
+
with type validation\n- Schema-based extraction with custom constraints\n- Informal
|
41
|
+
text analysis (litotes, hedging)\n- Detailed explanations for extracted values\n\nSee
|
42
|
+
the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal
|
43
|
+
text processing, advanced schemas, and detailed interface documentation.\n\n###
|
44
|
+
Scoring\nText evaluation system that provides detailed scoring and feedback using
|
45
|
+
multiple expert reviewers. Get balanced scoring through AI-powered expert reviewers
|
46
|
+
that automatically adapt to your content.\n\n```ruby\ntext = \"The code implements
|
47
|
+
a binary search algorithm with O(log n) complexity\"\ncriteria = \"Evaluate technical
|
48
|
+
accuracy and clarity\"\n\nresult = ActiveGenie::Scoring::Basic.call(text, criteria)\n#
|
49
|
+
=> {\n# algorithm_expert_score: 95,\n# algorithm_expert_reasoning: \"Accurately
|
50
|
+
describes binary search and its complexity\",\n# technical_writer_score: 90,\n#
|
51
|
+
\ technical_writer_reasoning: \"Clear and concise explanation of the algorithm\",\n#
|
52
|
+
\ final_score: 92.5\n# }\n```\n\nFeatures:\n- Multi-reviewer evaluation with
|
53
|
+
automatic expert selection\n- Detailed feedback with scoring reasoning\n- Customizable
|
54
|
+
reviewer weights\n- Flexible evaluation criteria\n\nSee the [Scoring README](lib/active_genie/scoring/README.md)
|
55
|
+
for advanced usage, custom reviewers, and detailed interface documentation.\n\n###
|
56
|
+
Battle\nAI-powered battle evaluation system that determines winners between two
|
57
|
+
players based on specified criteria.\n\n```ruby\nrequire 'active_genie'\n\nplayer_a
|
58
|
+
= \"Implementation uses dependency injection for better testability\"\nplayer_b
|
59
|
+
= \"Code has high test coverage but tightly coupled components\"\ncriteria = \"Evaluate
|
60
|
+
code quality and maintainability\"\n\nresult = ActiveGenie::Battle::Basic.call(player_a,
|
61
|
+
player_b, criteria)\n# => {\n# winner_player: \"Implementation uses dependency
|
62
|
+
injection for better testability\",\n# reasoning: \"Player A's implementation
|
63
|
+
demonstrates better maintainability through dependency injection, \n# which
|
64
|
+
allows for easier testing and component replacement. While Player B has good test
|
65
|
+
coverage, \n# the tight coupling makes the code harder to maintain
|
66
|
+
and modify.\",\n# what_could_be_changed_to_avoid_draw: \"Focus on specific
|
67
|
+
architectural patterns and design principles\"\n# }\n```\n\nFeatures:\n- Multi-reviewer
|
68
|
+
evaluation with automatic expert selection\n- Detailed feedback with scoring reasoning\n-
|
69
|
+
Customizable reviewer weights\n- Flexible evaluation criteria\n\nSee the [Battle
|
70
|
+
README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers,
|
71
|
+
and detailed interface documentation.\n\n### Summarizer (WIP)\nThe summarizer is
|
72
|
+
a tool that can be used to summarize a given text. It uses a set of rules to summarize
|
73
|
+
the text out of the box. Uses the best practices of prompt engineering and engineering
|
74
|
+
to make the summarization as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
|
75
|
+
= \"Example text to be summarized. The fox jumps over the dog\"\nsummarized_text
|
76
|
+
= ActiveGenie::Summarizer.call(text)\nputs summarized_text # => \"The fox jumps
|
77
|
+
over the dog\"\n```\n\n### Language detector (WIP)\nThe language detector is a tool
|
78
|
+
that can be used to detect the language of a given text. It uses a set of rules
|
79
|
+
to detect the language of the text out of the box. Uses the best practices of prompt
|
80
|
+
engineering and engineering to make the language detection as accurate as possible.\n\n```ruby\nrequire
|
81
|
+
'active_genie'\n\ntext = \"Example text to be detected\"\nlanguage = ActiveGenie::LanguageDetector.call(text)\nputs
|
82
|
+
language # => \"en\"\n```\n\n### Translator (WIP)\nThe translator is a tool that
|
83
|
+
can be used to translate a given text. It uses a set of rules to translate the text
|
84
|
+
out of the box. Uses the best practices of prompt engineering and engineering to
|
85
|
+
make the translation as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
|
86
|
+
= \"Example text to be translated\"\ntranslated_text = ActiveGenie::Translator.call(text,
|
87
|
+
from: 'en', to: 'pt')\nputs translated_text # => \"Exemplo de texto a ser traduzido\"\n```\n\n###
|
88
|
+
Sentiment analyzer (WIP)\nThe sentiment analyzer is a tool that can be used to analyze
|
89
|
+
the sentiment of a given text. It uses a set of rules to analyze the sentiment of
|
90
|
+
the text out of the box. Uses the best practices of prompt engineering and engineering
|
91
|
+
to make the sentiment analysis as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
|
92
|
+
= \"Example text to be analyzed\"\nsentiment = ActiveGenie::SentimentAnalyzer.call(text)\nputs
|
93
|
+
sentiment # => \"positive\"\n```\n\n### Elo ranking (WIP)\nThe Elo ranking is a
|
94
|
+
tool that can be used to rank a set of items. It uses a set of rules to rank the
|
95
|
+
items out of the box. Uses the best practices of prompt engineering and engineering
|
96
|
+
to make the ranking as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\nitems
|
97
|
+
= ['Square', 'Circle', 'Triangle']\ncriterias = 'items that look rounded'\nranked_items
|
98
|
+
= ActiveGenie::EloRanking.call(items, criterias, rounds: 10)\nputs ranked_items
|
99
|
+
# => [{ name: \"Circle\", score: 1500 }, { name: \"Square\", score: 800 }, { name:
|
100
|
+
\"Triangle\", score: 800 }]\n```\n\n\n## Configuration Options\n\n| Option | Description
|
101
|
+
| Default |\n|--------|-------------|---------|\n| `provider` | LLM provider (openai,
|
102
|
+
anthropic, etc) | `nil` |\n| `model` | Model to use | `nil` |\n| `api_key` | Provider
|
103
|
+
API key | `nil` |\n| `timeout` | Request timeout in seconds | `5` |\n| `max_retries`
|
104
|
+
| Maximum retry attempts | `3` |\n\n> **Note:** Each module can append its own set
|
105
|
+
of configuration options, see the individual module documentation for details.\n\n##
|
106
|
+
Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout
|
107
|
+
-b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing
|
108
|
+
feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5.
|
109
|
+
Open a Pull Request\n## License\n\nThis project is licensed under the MIT License
|
110
|
+
- see the [LICENSE](LICENSE) file for details.\n\n"
|
20
111
|
email:
|
21
112
|
- radames@roriz.dev
|
22
113
|
executables: []
|
@@ -27,6 +118,9 @@ files:
|
|
27
118
|
- README.md
|
28
119
|
- VERSION
|
29
120
|
- lib/active_genie.rb
|
121
|
+
- lib/active_genie/battle.rb
|
122
|
+
- lib/active_genie/battle/README.md
|
123
|
+
- lib/active_genie/battle/basic.rb
|
30
124
|
- lib/active_genie/clients/openai.rb
|
31
125
|
- lib/active_genie/clients/router.rb
|
32
126
|
- lib/active_genie/configuration.rb
|
@@ -34,12 +128,19 @@ files:
|
|
34
128
|
- lib/active_genie/data_extractor/README.md
|
35
129
|
- lib/active_genie/data_extractor/basic.rb
|
36
130
|
- lib/active_genie/data_extractor/from_informal.rb
|
131
|
+
- lib/active_genie/leaderboard.rb
|
132
|
+
- lib/active_genie/leaderboard/elo_ranking.rb
|
133
|
+
- lib/active_genie/leaderboard/leaderboard.rb
|
134
|
+
- lib/active_genie/leaderboard/league.rb
|
135
|
+
- lib/active_genie/leaderboard/player.rb
|
136
|
+
- lib/active_genie/leaderboard/players_collection.rb
|
37
137
|
- lib/active_genie/scoring.rb
|
38
138
|
- lib/active_genie/scoring/README.md
|
39
139
|
- lib/active_genie/scoring/basic.rb
|
40
140
|
- lib/active_genie/scoring/recommended_reviews.rb
|
141
|
+
- lib/active_genie/utils/math.rb
|
41
142
|
- lib/tasks/install.rake
|
42
|
-
- lib/tasks/templates/
|
143
|
+
- lib/tasks/templates/active_genie.yml
|
43
144
|
homepage: https://github.com/Roriz/active_genie
|
44
145
|
licenses:
|
45
146
|
- Apache-2.0
|
@@ -67,6 +168,5 @@ requirements: []
|
|
67
168
|
rubygems_version: 3.5.3
|
68
169
|
signing_key:
|
69
170
|
specification_version: 4
|
70
|
-
summary:
|
71
|
-
summarization, scoring, and ranking.
|
171
|
+
summary: Transform your Ruby application with powerful, production-ready GenAI features
|
72
172
|
test_files: []
|