active_genie 0.0.10 → 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 +17 -54
- data/VERSION +1 -1
- data/lib/active_genie/battle/basic.rb +49 -58
- data/lib/active_genie/clients/openai_client.rb +60 -18
- data/lib/active_genie/clients/unified_client.rb +2 -2
- data/lib/active_genie/configuration/openai_config.rb +9 -9
- data/lib/active_genie/data_extractor/README.md +0 -1
- data/lib/active_genie/data_extractor/basic.rb +8 -18
- data/lib/active_genie/data_extractor/from_informal.rb +4 -15
- data/lib/active_genie/logger.rb +34 -7
- data/lib/active_genie/{league → ranking}/README.md +7 -7
- 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/{league → 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 +1 -1
- data/lib/active_genie/scoring/basic.rb +55 -30
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +18 -7
- data/lib/active_genie/scoring.rb +3 -3
- data/lib/active_genie.rb +1 -1
- metadata +66 -87
- data/lib/active_genie/league/elo_ranking.rb +0 -121
- data/lib/active_genie/league/free_for_all.rb +0 -62
- data/lib/active_genie/league/league.rb +0 -120
- data/lib/active_genie/league/player.rb +0 -59
- data/lib/active_genie/league.rb +0 -12
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,16 +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
|
-
-
|
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
|
14
15
|
|
15
16
|
## Installation
|
16
17
|
|
@@ -79,7 +80,7 @@ Features:
|
|
79
80
|
|
80
81
|
See the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal text processing, advanced schemas, and detailed interface documentation.
|
81
82
|
|
82
|
-
### Scoring
|
83
|
+
### Data Scoring
|
83
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.
|
84
85
|
|
85
86
|
```ruby
|
@@ -104,7 +105,7 @@ Features:
|
|
104
105
|
|
105
106
|
See the [Scoring README](lib/active_genie/scoring/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
|
106
107
|
|
107
|
-
### Battle
|
108
|
+
### Data Battle
|
108
109
|
AI-powered battle evaluation system that determines winners between two players based on specified criteria.
|
109
110
|
|
110
111
|
```ruby
|
@@ -132,8 +133,8 @@ Features:
|
|
132
133
|
|
133
134
|
See the [Battle README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
|
134
135
|
|
135
|
-
###
|
136
|
-
The
|
136
|
+
### Data Ranking
|
137
|
+
The Ranking module provides competitive ranking through multi-stage evaluation:
|
137
138
|
|
138
139
|
|
139
140
|
```ruby
|
@@ -142,7 +143,7 @@ require 'active_genie'
|
|
142
143
|
players = ['REST API', 'GraphQL API', 'SOAP API', 'gRPC API', 'Websocket API']
|
143
144
|
criteria = "Best one to be used into a high changing environment"
|
144
145
|
|
145
|
-
result = ActiveGenie::
|
146
|
+
result = ActiveGenie::Ranking.call(players, criteria)
|
146
147
|
# => {
|
147
148
|
# winner_player: "gRPC API",
|
148
149
|
# reasoning: "gRPC API is the best one to be used into a high changing environment",
|
@@ -153,51 +154,12 @@ result = ActiveGenie::League.call(players, criteria)
|
|
153
154
|
- **Automatic elimination** of inconsistent performers using statistical analysis
|
154
155
|
- **Dynamic ranking adjustments** based on simulated pairwise battles, from bottom to top
|
155
156
|
|
156
|
-
See the [
|
157
|
+
See the [Ranking README](lib/active_genie/ranking/README.md) for implementation details, configuration, and advanced ranking strategies.
|
157
158
|
|
158
|
-
### Summarizer (
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
require 'active_genie'
|
163
|
-
|
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"
|
167
|
-
```
|
168
|
-
|
169
|
-
### Language detector (WIP)
|
170
|
-
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.
|
171
|
-
|
172
|
-
```ruby
|
173
|
-
require 'active_genie'
|
174
|
-
|
175
|
-
text = "Example text to be detected"
|
176
|
-
language = ActiveGenie::LanguageDetector.call(text)
|
177
|
-
puts language # => "en"
|
178
|
-
```
|
179
|
-
|
180
|
-
### Translator (WIP)
|
181
|
-
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.
|
182
|
-
|
183
|
-
```ruby
|
184
|
-
require 'active_genie'
|
185
|
-
|
186
|
-
text = "Example text to be translated"
|
187
|
-
translated_text = ActiveGenie::Translator.call(text, from: 'en', to: 'pt')
|
188
|
-
puts translated_text # => "Exemplo de texto a ser traduzido"
|
189
|
-
```
|
190
|
-
|
191
|
-
### Sentiment analyzer (WIP)
|
192
|
-
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.
|
193
|
-
|
194
|
-
```ruby
|
195
|
-
require 'active_genie'
|
196
|
-
|
197
|
-
text = "Example text to be analyzed"
|
198
|
-
sentiment = ActiveGenie::SentimentAnalyzer.call(text)
|
199
|
-
puts sentiment # => "positive"
|
200
|
-
```
|
159
|
+
### Text Summarizer (Future)
|
160
|
+
### Language detector (Future)
|
161
|
+
### Translator (Future)
|
162
|
+
### Sentiment analyzer (Future)
|
201
163
|
|
202
164
|
## Configuration
|
203
165
|
|
@@ -218,6 +180,7 @@ puts sentiment # => "positive"
|
|
218
180
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
219
181
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
220
182
|
5. Open a Pull Request
|
183
|
+
|
221
184
|
## License
|
222
185
|
|
223
|
-
This project is licensed under the
|
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
|
@@ -14,8 +14,8 @@ 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
|
@@ -30,65 +30,55 @@ module ActiveGenie::Battle
|
|
30
30
|
@player_a = player_a
|
31
31
|
@player_b = player_b
|
32
32
|
@criteria = criteria
|
33
|
-
@config = config
|
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
|
-
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)
|
64
|
+
{ winner:, loser:, reasoning: response['impartial_judge_winner_reasoning'] }
|
65
65
|
end
|
66
66
|
|
67
67
|
PROMPT = <<~PROMPT
|
68
|
-
|
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.
|
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.
|
71
69
|
|
72
70
|
# Steps
|
73
|
-
1.
|
74
|
-
2.
|
75
|
-
3.
|
76
|
-
4.
|
77
|
-
5.
|
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.
|
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
|
-
- Output: winner: player_b
|
88
|
-
- 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.
|
89
79
|
|
90
80
|
# Notes
|
91
|
-
- Avoid
|
81
|
+
- Avoid resulting in a draw. Use reasoning or make fair assumptions if needed.
|
92
82
|
- Critically assess each player's adherence to the criteria.
|
93
83
|
- Clearly communicate the reasoning behind your decision.
|
94
84
|
PROMPT
|
@@ -99,32 +89,33 @@ module ActiveGenie::Battle
|
|
99
89
|
schema: {
|
100
90
|
type: "object",
|
101
91
|
properties: {
|
102
|
-
|
92
|
+
player_a_sell_himself: {
|
103
93
|
type: 'string',
|
104
|
-
description: '
|
105
|
-
|
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.',
|
106
99
|
},
|
107
|
-
|
100
|
+
player_a_arguments: {
|
108
101
|
type: 'string',
|
109
|
-
description: '
|
102
|
+
description: 'player_a arguments why he is the winner compared to player_b. Max of 100 words.',
|
110
103
|
},
|
111
|
-
|
104
|
+
player_b_counter: {
|
112
105
|
type: 'string',
|
113
|
-
description: '
|
114
|
-
}
|
106
|
+
description: 'player_b counter arguments why he is the winner compared to player_a. Max of 100 words.',
|
107
|
+
},
|
108
|
+
impartial_judge_winner_reasoning: {
|
109
|
+
type: 'string',
|
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
|
+
},
|
115
117
|
}
|
116
118
|
}
|
117
119
|
}
|
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
120
|
end
|
130
121
|
end
|
@@ -3,14 +3,17 @@ require 'net/http'
|
|
3
3
|
|
4
4
|
module ActiveGenie::Clients
|
5
5
|
class OpenaiClient
|
6
|
+
MAX_RETRIES = 3
|
7
|
+
|
8
|
+
class OpenaiError < StandardError; end
|
9
|
+
class RateLimitError < OpenaiError; end
|
10
|
+
|
6
11
|
def initialize(config)
|
7
12
|
@app_config = config
|
8
13
|
end
|
9
14
|
|
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?
|
15
|
+
def function_calling(messages, function, model_tier: nil, config: {})
|
16
|
+
model = config[:model] || @app_config.tier_to_model(model_tier)
|
14
17
|
|
15
18
|
payload = {
|
16
19
|
messages:,
|
@@ -37,27 +40,69 @@ module ActiveGenie::Clients
|
|
37
40
|
private
|
38
41
|
|
39
42
|
def request(payload, headers, config:)
|
43
|
+
retries = config[:max_retries] || MAX_RETRIES
|
40
44
|
start_time = Time.now
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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)
|
46
58
|
|
47
|
-
|
48
|
-
return nil if response.body.empty?
|
59
|
+
return nil if response.body.empty?
|
49
60
|
|
50
|
-
|
51
|
-
|
61
|
+
parsed_body = JSON.parse(response.body)
|
62
|
+
# log_response(start_time, parsed_body, config:)
|
52
63
|
|
53
|
-
|
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
|
54
99
|
end
|
55
100
|
|
56
101
|
DEFAULT_HEADERS = {
|
57
102
|
'Content-Type': 'application/json',
|
58
103
|
}
|
59
104
|
|
60
|
-
def log_response(start_time, response, config:)
|
105
|
+
def log_response(start_time, response, config: {})
|
61
106
|
ActiveGenie::Logger.trace(
|
62
107
|
{
|
63
108
|
**config.dig(:log),
|
@@ -70,8 +115,5 @@ module ActiveGenie::Clients
|
|
70
115
|
}
|
71
116
|
)
|
72
117
|
end
|
73
|
-
|
74
|
-
# TODO: add some more rich error handling
|
75
|
-
class OpenaiError < StandardError; end
|
76
118
|
end
|
77
119
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module ActiveGenie::Clients
|
2
2
|
class UnifiedClient
|
3
3
|
class << self
|
4
|
-
def function_calling(messages, function, config: {})
|
4
|
+
def function_calling(messages, function, model_tier: nil, config: {})
|
5
5
|
provider_name = config[:provider]&.downcase&.strip&.to_sym
|
6
6
|
provider = ActiveGenie.configuration.providers.all[provider_name] || ActiveGenie.configuration.providers.default
|
7
7
|
|
8
8
|
raise InvalidProviderError if provider.nil? || provider.client.nil?
|
9
9
|
|
10
|
-
provider.client.function_calling(messages, function, config:)
|
10
|
+
provider.client.function_calling(messages, function, model_tier:, config:)
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
@@ -13,6 +13,14 @@ module ActiveGenie::Configuration
|
|
13
13
|
@organization || ENV['OPENAI_ORGANIZATION']
|
14
14
|
end
|
15
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
|
+
|
16
24
|
def lower_tier_model
|
17
25
|
@lower_tier_model || 'gpt-4o-mini'
|
18
26
|
end
|
@@ -30,15 +38,7 @@ module ActiveGenie::Configuration
|
|
30
38
|
lower_tier: lower_tier_model,
|
31
39
|
middle_tier: middle_tier_model,
|
32
40
|
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)
|
41
|
+
}[tier&.to_sym] || lower_tier_model
|
42
42
|
end
|
43
43
|
|
44
44
|
def to_h(config = {})
|
@@ -65,7 +65,6 @@ The `from_informal` method extends the basic extraction by analyzing rhetorical
|
|
65
65
|
- Litotes ("not bad", "isn't terrible")
|
66
66
|
- Affirmative expressions ("sure", "no problem")
|
67
67
|
- Negative expressions ("nah", "not really")
|
68
|
-
- Hedging ("maybe", "I guess")
|
69
68
|
|
70
69
|
### Example
|
71
70
|
|
@@ -3,8 +3,8 @@ require_relative '../clients/unified_client'
|
|
3
3
|
|
4
4
|
module ActiveGenie::DataExtractor
|
5
5
|
class Basic
|
6
|
-
def self.call(
|
7
|
-
new(
|
6
|
+
def self.call(...)
|
7
|
+
new(...).call
|
8
8
|
end
|
9
9
|
|
10
10
|
# Extracts structured data from text based on a predefined schema.
|
@@ -46,7 +46,12 @@ module ActiveGenie::DataExtractor
|
|
46
46
|
}
|
47
47
|
}
|
48
48
|
|
49
|
-
::ActiveGenie::Clients::UnifiedClient.function_calling(
|
49
|
+
::ActiveGenie::Clients::UnifiedClient.function_calling(
|
50
|
+
messages,
|
51
|
+
function,
|
52
|
+
model_tier: 'lower_tier',
|
53
|
+
config: @config
|
54
|
+
)
|
50
55
|
end
|
51
56
|
|
52
57
|
private
|
@@ -59,10 +64,6 @@ module ActiveGenie::DataExtractor
|
|
59
64
|
1. **Identify Data Types**: Determine the types of data to collect, such as names, dates, email addresses, phone numbers, etc.
|
60
65
|
2. **Extract Information**: Use pattern recognition and language understanding to identify and extract the relevant pieces of data from the user message.
|
61
66
|
3. **Categorize Data**: Assign the extracted data to the appropriate predefined fields.
|
62
|
-
4. **Structure Data**: Format the extracted and categorized data in a structured format, such as JSON.
|
63
|
-
|
64
|
-
# Output Format
|
65
|
-
The output should be a JSON object containing fields with their corresponding extracted values. If a value is not found, the field should still be included with a null value.
|
66
67
|
|
67
68
|
# Notes
|
68
69
|
- Handle missing or partial information gracefully.
|
@@ -83,16 +84,5 @@ module ActiveGenie::DataExtractor
|
|
83
84
|
|
84
85
|
with_explaination
|
85
86
|
end
|
86
|
-
|
87
|
-
def config
|
88
|
-
{
|
89
|
-
all_providers: { model_tier: 'lower_tier' },
|
90
|
-
log: {
|
91
|
-
**(@config.dig(:log) || {}),
|
92
|
-
trace: self.class.name,
|
93
|
-
},
|
94
|
-
**@config
|
95
|
-
}
|
96
|
-
end
|
97
87
|
end
|
98
88
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module ActiveGenie::DataExtractor
|
2
2
|
class FromInformal
|
3
|
-
def self.call(
|
4
|
-
new(
|
3
|
+
def self.call(...)
|
4
|
+
new(...).call()
|
5
5
|
end
|
6
6
|
|
7
7
|
# Extracts data from informal text while also detecting litotes and their meanings.
|
@@ -30,10 +30,10 @@ module ActiveGenie::DataExtractor
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def call
|
33
|
-
response = Basic.call(@text, data_to_extract_with_litote, config:)
|
33
|
+
response = Basic.call(@text, data_to_extract_with_litote, config: @config)
|
34
34
|
|
35
35
|
if response['message_litote']
|
36
|
-
response = Basic.call(response['litote_rephrased'], @data_to_extract, config:)
|
36
|
+
response = Basic.call(response['litote_rephrased'], @data_to_extract, config: @config)
|
37
37
|
end
|
38
38
|
|
39
39
|
response
|
@@ -54,16 +54,5 @@ module ActiveGenie::DataExtractor
|
|
54
54
|
}
|
55
55
|
}
|
56
56
|
end
|
57
|
-
|
58
|
-
def config
|
59
|
-
{
|
60
|
-
all_providers: { model_tier: 'lower_tier' },
|
61
|
-
**@config,
|
62
|
-
log: {
|
63
|
-
**@config.dig(:log),
|
64
|
-
trace: self.class.name,
|
65
|
-
},
|
66
|
-
}
|
67
|
-
end
|
68
57
|
end
|
69
58
|
end
|
data/lib/active_genie/logger.rb
CHANGED
@@ -5,6 +5,16 @@ module ActiveGenie
|
|
5
5
|
module Logger
|
6
6
|
module_function
|
7
7
|
|
8
|
+
def with_context(context)
|
9
|
+
@context ||= {}
|
10
|
+
begin
|
11
|
+
@context = @context.merge(context)
|
12
|
+
yield if block_given?
|
13
|
+
ensure
|
14
|
+
@context.delete_if { |key, _| context.key?(key) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
8
18
|
def info(log)
|
9
19
|
save(log, level: :info)
|
10
20
|
end
|
@@ -25,21 +35,38 @@ module ActiveGenie
|
|
25
35
|
save(log, level: :trace)
|
26
36
|
end
|
27
37
|
|
28
|
-
|
29
|
-
|
30
|
-
def save(log, level: :info)
|
31
|
-
return if LOG_LEVELS[log.dig(:log, :log_level)] || -1 < LOG_LEVELS[level]
|
32
|
-
|
33
|
-
log[:trace] = log.dig(:trace)&.to_s&.gsub('ActiveGenie::', '')
|
38
|
+
def save(data, level: :info)
|
39
|
+
log = @context.merge(data || {})
|
34
40
|
log[:timestamp] = Time.now
|
35
41
|
log[:level] = level.to_s.upcase
|
36
42
|
log[:process_id] = Process.pid
|
43
|
+
config_log_level = LOG_LEVELS[log.dig(:config, :log_level)] || LOG_LEVELS[:info]
|
37
44
|
|
38
45
|
FileUtils.mkdir_p('logs')
|
39
46
|
File.write('logs/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
|
40
|
-
|
47
|
+
if config_log_level >= LOG_LEVELS[level]
|
48
|
+
$stdout.puts log
|
49
|
+
else
|
50
|
+
$stdout.print '.'
|
51
|
+
end
|
41
52
|
|
42
53
|
log
|
43
54
|
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Log Levels
|
59
|
+
#
|
60
|
+
# LOG_LEVELS defines different levels of logging within the application.
|
61
|
+
# Each level serves a specific purpose, balancing verbosity and relevance.
|
62
|
+
#
|
63
|
+
# - :info -> General log messages providing an overview of application behavior, ensuring readability without excessive detail.
|
64
|
+
# - :warn -> Indicates unexpected behaviors that do not halt execution but require attention, such as retries, timeouts, or necessary conversions.
|
65
|
+
# - :error -> Represents critical errors that prevent the application from functioning correctly.
|
66
|
+
# - :debug -> Provides detailed logs for debugging, offering the necessary context for audits but with slightly less detail than trace logs.
|
67
|
+
# - :trace -> Logs every external call with the highest level of detail, primarily for auditing or state-saving purposes. These logs do not provide context regarding triggers or reasons.
|
68
|
+
LOG_LEVELS = { info: 0, error: 0, warn: 1, debug: 2, trace: 3 }.freeze
|
69
|
+
|
70
|
+
attr_accessor :context
|
44
71
|
end
|
45
72
|
end
|