active_genie 0.0.24 → 0.0.25

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -50
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +5 -5
  5. data/lib/active_genie/battle/generalist.rb +132 -0
  6. data/lib/active_genie/battle.rb +6 -5
  7. data/lib/active_genie/clients/providers/anthropic_client.rb +77 -0
  8. data/lib/active_genie/clients/{base_client.rb → providers/base_client.rb} +74 -100
  9. data/lib/active_genie/clients/providers/deepseek_client.rb +91 -0
  10. data/lib/active_genie/clients/providers/google_client.rb +132 -0
  11. data/lib/active_genie/clients/providers/openai_client.rb +96 -0
  12. data/lib/active_genie/clients/unified_client.rb +42 -12
  13. data/lib/active_genie/concerns/loggable.rb +11 -23
  14. data/lib/active_genie/config/battle_config.rb +8 -0
  15. data/lib/active_genie/config/data_extractor_config.rb +23 -0
  16. data/lib/active_genie/config/llm_config.rb +36 -0
  17. data/lib/active_genie/config/log_config.rb +44 -0
  18. data/lib/active_genie/config/providers/anthropic_config.rb +57 -0
  19. data/lib/active_genie/config/providers/deepseek_config.rb +50 -0
  20. data/lib/active_genie/config/providers/google_config.rb +52 -0
  21. data/lib/active_genie/config/providers/openai_config.rb +50 -0
  22. data/lib/active_genie/config/providers/provider_base.rb +89 -0
  23. data/lib/active_genie/config/providers_config.rb +62 -0
  24. data/lib/active_genie/config/ranking_config.rb +21 -0
  25. data/lib/active_genie/config/scoring_config.rb +8 -0
  26. data/lib/active_genie/configuration.rb +51 -28
  27. data/lib/active_genie/data_extractor/README.md +13 -13
  28. data/lib/active_genie/data_extractor/from_informal.rb +54 -48
  29. data/lib/active_genie/data_extractor/generalist.md +12 -0
  30. data/lib/active_genie/data_extractor/generalist.rb +125 -0
  31. data/lib/active_genie/data_extractor.rb +7 -5
  32. data/lib/active_genie/errors/invalid_provider_error.rb +41 -0
  33. data/lib/active_genie/logger.rb +17 -66
  34. data/lib/active_genie/ranking/README.md +31 -1
  35. data/lib/active_genie/ranking/elo_round.rb +107 -104
  36. data/lib/active_genie/ranking/free_for_all.rb +78 -74
  37. data/lib/active_genie/ranking/player.rb +79 -71
  38. data/lib/active_genie/ranking/players_collection.rb +83 -71
  39. data/lib/active_genie/ranking/ranking.rb +71 -94
  40. data/lib/active_genie/ranking/ranking_scoring.rb +71 -50
  41. data/lib/active_genie/ranking.rb +2 -0
  42. data/lib/active_genie/scoring/README.md +4 -4
  43. data/lib/active_genie/scoring/generalist.rb +171 -0
  44. data/lib/active_genie/scoring/recommended_reviewers.rb +70 -71
  45. data/lib/active_genie/scoring.rb +8 -5
  46. data/lib/active_genie.rb +23 -1
  47. data/lib/tasks/benchmark.rake +10 -9
  48. data/lib/tasks/install.rake +3 -1
  49. data/lib/tasks/templates/active_genie.rb +11 -6
  50. metadata +31 -22
  51. data/lib/active_genie/battle/basic.rb +0 -129
  52. data/lib/active_genie/clients/anthropic_client.rb +0 -84
  53. data/lib/active_genie/clients/google_client.rb +0 -135
  54. data/lib/active_genie/clients/helpers/retry.rb +0 -29
  55. data/lib/active_genie/clients/openai_client.rb +0 -98
  56. data/lib/active_genie/configuration/log_config.rb +0 -14
  57. data/lib/active_genie/configuration/providers/anthropic_config.rb +0 -54
  58. data/lib/active_genie/configuration/providers/base_config.rb +0 -85
  59. data/lib/active_genie/configuration/providers/deepseek_config.rb +0 -54
  60. data/lib/active_genie/configuration/providers/google_config.rb +0 -56
  61. data/lib/active_genie/configuration/providers/internal_company_api_config.rb +0 -54
  62. data/lib/active_genie/configuration/providers/openai_config.rb +0 -54
  63. data/lib/active_genie/configuration/providers_config.rb +0 -40
  64. data/lib/active_genie/configuration/runtime_config.rb +0 -35
  65. data/lib/active_genie/data_extractor/basic.rb +0 -101
  66. data/lib/active_genie/scoring/basic.rb +0 -170
@@ -17,24 +17,24 @@ schema = {
17
17
  age: { type: 'integer', description: 'Age in years' }
18
18
  }
19
19
  result = ActiveGenie::DataExtractor.call(text, schema)
20
- # => {
21
- # name: "John Doe",
20
+ # => {
21
+ # name: "John Doe",
22
22
  # name_explanation: "Found directly in text",
23
- # age: 25,
24
- # age_explanation: "Explicitly stated as 25 years old"
23
+ # age: 25,
24
+ # age_explanation: "Explicitly stated as 25 years old"
25
25
  # }
26
26
 
27
27
  product = "Nike Air Max 90 - Size 42 - $199.99"
28
28
  schema = {
29
- brand: {
29
+ brand: {
30
30
  type: 'string',
31
31
  enum: ["Nike", "Adidas", "Puma"]
32
32
  },
33
- price: {
33
+ price: {
34
34
  type: 'number',
35
35
  minimum: 0
36
36
  },
37
- currency: {
37
+ currency: {
38
38
  type: 'string',
39
39
  enum: ["USD", "EUR"]
40
40
  },
@@ -46,8 +46,8 @@ schema = {
46
46
  }
47
47
 
48
48
  result = ActiveGenie::DataExtractor.call(product, schema)
49
- # => {
50
- # brand: "Nike",
49
+ # => {
50
+ # brand: "Nike",
51
51
  # brand_explanation: "Brand name found at start of text",
52
52
  # price: 199.99,
53
53
  # price_explanation: "Price found in USD format at end",
@@ -70,12 +70,12 @@ The `from_informal` method extends the basic extraction by analyzing rhetorical
70
70
 
71
71
  ```ruby
72
72
  text = "The weather isn't bad today"
73
- schema = {
74
- mood: { type: 'string', description: 'The mood of the message' }
73
+ schema = {
74
+ mood: { type: 'string', description: 'The mood of the message' }
75
75
  }
76
76
 
77
77
  result = ActiveGenie::DataExtractor.from_informal(text, schema)
78
- # => {
78
+ # => {
79
79
  # mood: "positive",
80
80
  # mood_explanation: "Speaker views weather favorably",
81
81
  # message_litote: true,
@@ -117,7 +117,7 @@ Extracts structured data from text based on a predefined schema.
117
117
  - Additional analysis fields when using `from_informal`
118
118
 
119
119
  ### `.from_informal(text, data_to_extract, config = {})`
120
- Extends basic extraction with rhetorical analysis, particularly for litotes.
120
+ Extends extraction with rhetorical analysis, particularly for litotes.
121
121
 
122
122
  #### Additional Return Fields
123
123
  | Name | Type | Description |
@@ -1,58 +1,64 @@
1
- module ActiveGenie::DataExtractor
2
- class FromInformal
3
- def self.call(...)
4
- new(...).call()
5
- end
1
+ # frozen_string_literal: true
6
2
 
7
- # Extracts data from informal text while also detecting litotes and their meanings.
8
- # This method extends the basic extraction by analyzing rhetorical devices.
9
- #
10
- # @param text [String] The informal text to analyze
11
- # @param data_to_extract [Hash] Schema defining the data structure to extract
12
- # @param config [Hash] Additional config for the extraction process
13
- #
14
- # @return [Hash] The extracted data including litote analysis. In addition to the
15
- # schema-defined fields, includes:
16
- # - message_litote: Whether the text contains a litote
17
- # - litote_rephrased: The positive rephrasing of any detected litote
18
- #
19
- # @example Analyze text with litote
20
- # text = "The weather isn't bad today"
21
- # schema = { mood: { type: 'string', description: 'The mood of the message' } }
22
- # DataExtractor.from_informal(text, schema)
23
- # # => { mood: "positive", mood_explanation: "Speaker views weather favorably",
24
- # # message_litote: true,
25
- # # litote_rephrased: "The weather is good today" }
26
- def initialize(text, data_to_extract, config: {})
27
- @text = text
28
- @data_to_extract = data_to_extract
29
- @config = ActiveGenie::Configuration.to_h(config)
30
- end
3
+ require_relative 'generalist'
31
4
 
32
- def call
33
- response = Basic.call(@text, data_to_extract_with_litote, config: @config)
5
+ module ActiveGenie
6
+ module DataExtractor
7
+ class FromInformal
8
+ def self.call(...)
9
+ new(...).call
10
+ end
34
11
 
35
- if response['message_litote']
36
- response = Basic.call(response['litote_rephrased'], @data_to_extract, config: @config)
12
+ # Extracts data from informal text while also detecting litotes and their meanings.
13
+ # This method extends the basic extraction by analyzing rhetorical devices.
14
+ #
15
+ # @param text [String] The informal text to analyze
16
+ # @param data_to_extract [Hash] Schema defining the data structure to extract
17
+ # @param config [Hash] Additional config for the extraction process
18
+ #
19
+ # @return [Hash] The extracted data including litote analysis. In addition to the
20
+ # schema-defined fields, includes:
21
+ # - message_litote: Whether the text contains a litote
22
+ # - litote_rephrased: The positive rephrasing of any detected litote
23
+ #
24
+ # @example Analyze text with litote
25
+ # text = "The weather isn't bad today"
26
+ # schema = { mood: { type: 'string', description: 'The mood of the message' } }
27
+ # DataExtractor.from_informal(text, schema)
28
+ # # => { mood: "positive", mood_explanation: "Speaker views weather favorably",
29
+ # # message_litote: true,
30
+ # # litote_rephrased: "The weather is good today" }
31
+ def initialize(text, data_to_extract, config: {})
32
+ @text = text
33
+ @data_to_extract = data_to_extract
34
+ @config = ActiveGenie.configuration.merge(config)
37
35
  end
38
36
 
39
- response
40
- end
37
+ def call
38
+ response = Generalist.call(@text, data_to_extract_with_litote, config: @config)
39
+
40
+ if response['message_litote']
41
+ response = Generalist.call(response['litote_rephrased'], @data_to_extract, config: @config)
42
+ end
41
43
 
42
- private
43
-
44
- def data_to_extract_with_litote
45
- {
46
- **@data_to_extract,
47
- message_litote: {
48
- type: 'boolean',
49
- description: 'Return true if the message is a litote. A litote is a figure of speech that uses understatement to emphasize a point by stating a negative to further affirm a positive, often incorporating double negatives for effect.'
50
- },
51
- litote_rephrased: {
52
- type: 'string',
53
- description: 'The true meaning of the litote. Rephrase the message to a positive and active statement.'
44
+ response
45
+ end
46
+
47
+ private
48
+
49
+ def data_to_extract_with_litote
50
+ {
51
+ **@data_to_extract,
52
+ message_litote: {
53
+ type: 'boolean',
54
+ description: 'Return true if the message is a litote. A litote is a figure of speech that uses understatement to emphasize a point by stating a negative to further affirm a positive, often incorporating double negatives for effect.'
55
+ },
56
+ litote_rephrased: {
57
+ type: 'string',
58
+ description: 'The true meaning of the litote. Rephrase the message to a positive and active statement.'
59
+ }
54
60
  }
55
- }
61
+ end
56
62
  end
57
63
  end
58
64
  end
@@ -0,0 +1,12 @@
1
+ Extract structured and typed data from user messages.
2
+ Identify relevant information within user messages and categorize it into predefined data fields with specific data types.
3
+
4
+ # Steps
5
+ 1. **Identify Data Types**: Determine the types of data to collect, such as names, dates, email addresses, phone numbers, etc.
6
+ 2. **Extract Information**: Use pattern recognition and language understanding to identify and extract the relevant pieces of data from the user message.
7
+ 3. **Categorize Data**: Assign the extracted data to the appropriate predefined fields.
8
+
9
+ # Notes
10
+ - Handle missing or partial information gracefully.
11
+ - Manage multiple occurrences of similar data points by prioritizing the first one unless specified otherwise.
12
+ - Be flexible to handle variations in data format and language clues.
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../clients/unified_client'
4
+
5
+ module ActiveGenie
6
+ module DataExtractor
7
+ class Generalist
8
+ def self.call(...)
9
+ new(...).call
10
+ end
11
+
12
+ # Extracts structured data from text based on a predefined schema.
13
+ #
14
+ # @param text [String] The input text to analyze and extract data from
15
+ # @param data_to_extract [Hash] Schema defining the data structure to extract.
16
+ # Each key in the hash represents a field to extract, and its value defines the expected type and constraints.
17
+ # @param config [Hash] Additional config for the extraction process
18
+ #
19
+ # @return [Hash] The extracted data matching the schema structure. Each field will include
20
+ # both the extracted value and an explanation of how it was derived.
21
+ #
22
+ # @example Extract a person's details
23
+ # schema = {
24
+ # name: { type: 'string', description: 'Full name of the person' },
25
+ # age: { type: 'integer', description: 'Age in years' }
26
+ # }
27
+ # text = "John Doe is 25 years old"
28
+ # DataExtractor.call(text, schema)
29
+ # # => { name: "John Doe", name_explanation: "Found directly in text",
30
+ # # age: 25, age_explanation: "Explicitly stated as 25 years old" }
31
+ def initialize(text, data_to_extract, config: {})
32
+ @text = text
33
+ @data_to_extract = data_to_extract
34
+ @config = ActiveGenie.configuration.merge(config)
35
+ end
36
+
37
+ def call
38
+ messages = [
39
+ { role: 'system', content: prompt },
40
+ { role: 'user', content: @text }
41
+ ]
42
+
43
+ properties = data_to_extract_with_explanation
44
+
45
+ function = {
46
+ name: 'data_extractor',
47
+ description: 'Extract structured and typed data from text',
48
+ parameters: {
49
+ type: 'object',
50
+ properties:,
51
+ required: properties.keys
52
+ }
53
+ }
54
+
55
+ response = function_calling(messages, function)
56
+
57
+ simplify_response(response)
58
+ end
59
+
60
+ private
61
+
62
+ def data_to_extract_with_explanation
63
+ return @data_to_extract unless @config.data_extractor.with_explanation
64
+
65
+ with_explanation = {}
66
+
67
+ @data_to_extract.each do |key, value|
68
+ with_explanation[key] = value
69
+ with_explanation["#{key}_explanation"] = {
70
+ type: 'string',
71
+ description: "The chain of thought that led to the conclusion about: #{key}. Can be blank if the user didn't provide any context"
72
+ }
73
+ with_explanation["#{key}_accuracy"] = {
74
+ type: 'integer',
75
+ description: 'The accuracy of the extracted data, what is the percentage of confidence? When 100 it means the data is explicitly stated in the text. When 0 it means is no way to discover the data from the text'
76
+ }
77
+ end
78
+
79
+ with_explanation
80
+ end
81
+
82
+ def function_calling(messages, function)
83
+ response = ::ActiveGenie::Clients::UnifiedClient.function_calling(
84
+ messages,
85
+ function,
86
+ config: @config
87
+ )
88
+
89
+ ActiveGenie::Logger.call(
90
+ {
91
+ code: :data_extractor,
92
+ text: @text[0..30],
93
+ data_to_extract: function[:parameters][:properties],
94
+ extracted_data: response
95
+ }
96
+ )
97
+
98
+ response
99
+ end
100
+
101
+ def simplify_response(response)
102
+ return response if @config.data_extractor.verbose
103
+
104
+ simplified_response = {}
105
+
106
+ @data_to_extract.each_key do |key|
107
+ next if !response.key?(key)
108
+ next if response.key?("#{key}_accuracy") && response["#{key}_accuracy"] < min_accuracy
109
+
110
+ simplified_response[key] = response[key]
111
+ end
112
+
113
+ simplified_response
114
+ end
115
+
116
+ def min_accuracy
117
+ @config.data_extractor.min_accuracy # default 70
118
+ end
119
+
120
+ def prompt
121
+ File.read(File.join(__dir__, 'generalist.md'))
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,4 +1,6 @@
1
- require_relative 'data_extractor/basic'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_extractor/generalist'
2
4
  require_relative 'data_extractor/from_informal'
3
5
 
4
6
  module ActiveGenie
@@ -6,12 +8,12 @@ module ActiveGenie
6
8
  module DataExtractor
7
9
  module_function
8
10
 
9
- def basic(...)
10
- Basic.call(...)
11
+ def call(...)
12
+ Generalist.call(...)
11
13
  end
12
14
 
13
- def call(...)
14
- Basic.call(...)
15
+ def generalist(...)
16
+ Generalist.call(...)
15
17
  end
16
18
 
17
19
  def from_informal(...)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveGenie
4
+ class InvalidProviderError < StandardError
5
+ TEXT = <<~TEXT
6
+ Invalid provider: %<provider>s
7
+
8
+ To configure ActiveGenie, you can either:
9
+ 1. Set up global configuration:
10
+ ```ruby
11
+ ActiveGenie.configure do |config|
12
+ config.provider = 'your_provider'
13
+ config.api_key = 'your_api_key'
14
+ # ... other configuration options
15
+ end
16
+ ```
17
+
18
+ 2. Or pass configuration directly to the method call:
19
+ ```ruby
20
+ ActiveGenie::DataExtraction.call(
21
+ arg1,
22
+ arg2,
23
+ config: {
24
+ provider: 'your_provider',
25
+ api_key: 'your_api_key'
26
+ }
27
+ )
28
+ ```
29
+
30
+ Available providers: %<available_providers>s
31
+ TEXT
32
+
33
+ def initialize(provider)
34
+ super(format(TEXT, provider:, available_providers:))
35
+ end
36
+
37
+ def available_providers
38
+ ActiveGenie.configuration.providers.all.keys.join(', ')
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'fileutils'
3
5
 
@@ -5,87 +7,36 @@ module ActiveGenie
5
7
  module Logger
6
8
  module_function
7
9
 
8
- def with_context(context, observer: nil)
9
- @context ||= {}
10
- @observers ||= []
11
- begin
12
- @context = @context.merge(context)
13
- @observers << observer if observer
14
- yield if block_given?
15
- ensure
16
- @context.delete_if { |key, _| context.key?(key) }
17
- @observers.delete(observer)
18
- end
19
- end
20
-
21
- def info(log)
22
- call(log, level: :info)
23
- end
24
-
25
- def error(log)
26
- call(log, level: :error)
27
- end
28
-
29
- def warn(log)
30
- call(log, level: :warn)
31
- end
32
-
33
- def debug(log)
34
- call(log, level: :debug)
35
- end
36
-
37
- def trace(log)
38
- call(log, level: :trace)
39
- end
40
-
41
- def call(data, level: :info)
10
+ def call(data)
42
11
  log = {
43
12
  **(@context || {}),
44
13
  **(data || {}),
45
14
  timestamp: Time.now,
46
- level: level.to_s.upcase,
47
15
  process_id: Process.pid
48
16
  }
49
17
 
50
- append_to_file(log)
51
- output(log, level)
52
- call_observers(log)
18
+ persist!(log)
19
+ $stdout.puts log
20
+ ActiveGenie.configuration.log.call_observers(log)
53
21
 
54
22
  log
55
23
  end
56
24
 
57
- # Log Levels
58
- #
59
- # LOG_LEVELS defines different levels of logging within the application.
60
- # Each level serves a specific purpose, balancing verbosity and relevance.
61
- #
62
- # - :info -> General log messages providing an overview of application behavior, ensuring readability without excessive detail.
63
- # - :warn -> Indicates unexpected behaviors that do not halt execution but require attention, such as retries, timeouts, or necessary conversions.
64
- # - :error -> Represents critical errors that prevent the application from functioning correctly.
65
- # - :debug -> Provides detailed logs for debugging, offering the necessary context for audits but with slightly less detail than trace logs.
66
- # - :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.
67
- LOG_LEVELS = { info: 0, error: 0, warn: 1, debug: 2, trace: 3 }.freeze
68
-
69
- attr_accessor :context
70
-
71
- def append_to_file(log)
72
- FileUtils.mkdir_p('logs')
73
- File.write('logs/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
74
- end
75
-
76
- def output(log, level)
77
- config_log_level = LOG_LEVELS[log.dig(:config, :log_level)] || LOG_LEVELS[:info]
78
- if config_log_level >= LOG_LEVELS[level]
79
- $stdout.puts log
80
- else
81
- $stdout.print '.'
25
+ def with_context(context)
26
+ @context ||= {}
27
+ begin
28
+ @context = @context.merge(context)
29
+ yield if block_given?
30
+ ensure
31
+ @context.delete_if { |key, _| context.key?(key) }
82
32
  end
83
33
  end
84
34
 
85
- def call_observers(log)
86
- return if @observers.nil? || @observers.size.zero?
35
+ attr_accessor :context
87
36
 
88
- @observers.each { |observer| observer.call(log) }
37
+ def persist!(log)
38
+ FileUtils.mkdir_p('log')
39
+ File.write('log/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
89
40
  end
90
41
  end
91
42
  end
@@ -40,4 +40,34 @@ The method processes the players through scoring, elimination, and ranking phase
40
40
  - Adjust initial criteria to ensure consistency
41
41
  - Adjust each player's content to ensure consistency
42
42
  - Support players with images or audio
43
- - Parallelize processing battles and scoring
43
+ - Parallelize processing battles and scoring
44
+
45
+ ## Ranking Configuration
46
+
47
+ | Config | Description | Default |
48
+ |--------|-------------|---------|
49
+ | `score_variation_threshold` | Threshold for eliminating players with inconsistent scores | `30` |
50
+
51
+ ## Ranking Callbacks
52
+ Callbacks are optional and can be used to watch any changes in players, battles, or scoring.
53
+
54
+ | Callback | Description |
55
+ |--------|-------------|
56
+ | `watch_players` | Callback to watch any changes in players |
57
+ | `watch_battles` | Callback to watch any changes in battles |
58
+ | `watch_scoring` | Callback to watch any changes in scoring |
59
+
60
+ Example of callback usage:
61
+
62
+ ```ruby
63
+ result = ActiveGenie::Ranking.call(
64
+ players,
65
+ criteria,
66
+ config: {
67
+ watch_players: ->(player) { puts player },
68
+ watch_battles: ->(battle) { puts battle },
69
+ watch_scoring: ->(scoring) { puts scoring }
70
+ }
71
+ )
72
+ ```
73
+