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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d0424a39ba21d821cb2419730387e1b026c35b5e2e5dff9f6d615f3ec54e6a3
4
- data.tar.gz: 17b460ccd1a689d0f8709af2b84f3cde65aa0075b76104ee1b4bb8b3b0ffc182
3
+ metadata.gz: fbc4033f3c0880973cf732d704921bb61c3cb861c438b732aa6ebe0cc88b2de4
4
+ data.tar.gz: 11794293170ec43a1d3b3d09e5ec488351a22428a5370ae0e42266a6f8098646
5
5
  SHA512:
6
- metadata.gz: ad98b2d5d063d0d1c4009e1a9f92d6d326ed948cbdee71317c94b6a4a0ee57042609c1f405ca7ce00d4beb321bc1e98ad722646349076929bcdc0a28da8b6b8b
7
- data.tar.gz: d2cc39b77757619c5235041f12d5182778b4237444f3c2246982ebcf54c0542af0da194783cea57fbe4fbde0985ef635802c648796b4f5c4d7a5d4f42c6519c7
6
+ metadata.gz: f6b13b3f36a8e516e5126d4cea0de52d046e498834cd10b90b1905460e9bba5ff6e7c4cebd58e6ea2c073176566601c8b806015baa01f11f28792942eec6ca1d
7
+ data.tar.gz: fc79aed12aaba0ca335d3a7ef8405f4a7c2809de9d5e41ca14fa7bd843d9fb69db8e124b6a2e96321432575a58b271c724a52fd3c1a1165c5ecfe346bedd4b5f
data/README.md CHANGED
@@ -1,16 +1,17 @@
1
1
  # ActiveGenie 🧞‍♂️
2
- > Transform your Ruby application with powerful, production-ready GenAI features
2
+ > The lodash for GenAI, stop reinventing the wheel
3
3
 
4
4
  [![Gem Version](https://badge.fury.io/rb/active_genie.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/active_genie)
5
5
  [![Ruby](https://github.com/roriz/active_genie/actions/workflows/ruby.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/ruby.yml)
6
6
 
7
- ActiveGenie is a Ruby gem that provides a polished, production-ready interface for working with Generative AI (GenAI) models. Just like ActiveStorage simplifies file handling in Rails, ActiveGenie makes it effortless to integrate GenAI capabilities into your Ruby applications.
7
+ ActiveGenie is a Ruby gem that provides a polished, production-ready interface for working with Generative AI (GenAI) models. Just like Lodash or ActiveStorage, ActiveGenie simplifies GenAI integration in your Ruby applications.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - 🎯 **Data Extraction**: Extract structured data from unstructured text with type validation
12
- - 📊 **Smart Scoring**: Multi-reviewer evaluation system with automatic expert selection
13
- - 💭 **Leaderboard**: Consistent rank items based on custom criteria, using multiple tecniques of ranking
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
- ### League
136
- The League module provides competitive ranking through multi-stage evaluation:
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::League.call(players, criteria)
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 [League README](lib/active_genie/league/README.md) for implementation details, configuration, and advanced ranking strategies.
157
+ See the [Ranking README](lib/active_genie/ranking/README.md) for implementation details, configuration, and advanced ranking strategies.
157
158
 
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.
160
-
161
- ```ruby
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 MIT License - see the [LICENSE](LICENSE) file for details.
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.10
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(player_a, player_b, criteria, config: {})
18
- new(player_a, player_b, criteria, config:).call
17
+ def self.call(...)
18
+ new(...).call
19
19
  end
20
20
 
21
21
  # @param player_a [String] The content or submission from the first player
@@ -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: #{player_content(@player_a)}" },
42
- { role: 'user', content: "player_b: #{player_content(@player_b)}" },
40
+ { role: 'user', content: "player_a: #{@player_a}" },
41
+ { role: 'user', content: "player_b: #{@player_b}" },
43
42
  ]
44
43
 
45
- @response = ::ActiveGenie::Clients::UnifiedClient.function_calling(messages, FUNCTION, config:)
44
+ response = ::ActiveGenie::Clients::UnifiedClient.function_calling(
45
+ messages,
46
+ FUNCTION,
47
+ model_tier: 'lower_tier',
48
+ config: @config
49
+ )
46
50
 
47
- response_formatted
51
+ response_formatted(response)
48
52
  end
49
53
 
50
54
  private
51
55
 
52
- def player_content(player)
53
- return player.dig('content') if player.is_a?(Hash)
56
+ def response_formatted(response)
57
+ winner = response['impartial_judge_winner']
58
+ loser = case response['impartial_judge_winner']
59
+ when 'player_a' then 'player_b'
60
+ when 'player_b' then 'player_a'
61
+ else 'draw'
62
+ end
54
63
 
55
- player
56
- end
57
-
58
- def response_formatted
59
- 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
- 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.
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. **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.
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
- - **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.
77
+ # Output Format
78
+ - The impartial judge chooses this player as the winner.
89
79
 
90
80
  # Notes
91
- - Avoid drawing if a clear winner can be discerned.
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
- winner: {
92
+ player_a_sell_himself: {
103
93
  type: 'string',
104
- description: 'The player who won the battle based on the criteria.',
105
- enum: ['player_a', 'player_b', 'draw']
94
+ description: 'player_a sell himself, highlighting his strengths and how he meets the criteria. Max of 100 words.',
95
+ },
96
+ player_b_sell_himself: {
97
+ type: 'string',
98
+ description: 'player_b sell himself, highlighting his strengths and how he meets the criteria. Max of 100 words.',
106
99
  },
107
- reasoning_of_winner: {
100
+ player_a_arguments: {
108
101
  type: 'string',
109
- description: 'The detailed reasoning about why the winner won based on the criteria.',
102
+ description: 'player_a arguments why he is the winner compared to player_b. Max of 100 words.',
110
103
  },
111
- what_could_be_changed_to_avoid_draw: {
104
+ player_b_counter: {
112
105
  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
- }
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
- response = Net::HTTP.post(
42
- URI("#{@app_config.api_url}/chat/completions"),
43
- payload.to_json,
44
- headers
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
- raise OpenaiError, response.body unless response.is_a?(Net::HTTPSuccess)
48
- return nil if response.body.empty?
59
+ return nil if response.body.empty?
49
60
 
50
- parsed_body = JSON.parse(response.body)
51
- log_response(start_time, parsed_body, config:)
61
+ parsed_body = JSON.parse(response.body)
62
+ # log_response(start_time, parsed_body, config:)
52
63
 
53
- parsed_body
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(text, data_to_extract, config: {})
7
- new(text, data_to_extract, config:).call
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(messages, function, config:)
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(text, data_to_extract, config: {})
4
- new(text, data_to_extract, config:).call()
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
@@ -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
- LOG_LEVELS = { info: 0, error: 1, warn: 2, debug: 3, trace: 4 }.freeze
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
- puts log
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