active_genie 0.30.0 → 0.30.3
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/VERSION +1 -1
- data/lib/active_genie/comparator/debate.rb +12 -7
- data/lib/active_genie/configs/llm_config.rb +14 -12
- data/lib/active_genie/configs/providers/anthropic_config.rb +0 -21
- data/lib/active_genie/configs/providers/deepseek_config.rb +0 -21
- data/lib/active_genie/configs/providers/google_config.rb +0 -21
- data/lib/active_genie/configs/providers/openai_config.rb +0 -21
- data/lib/active_genie/configs/providers/provider_base.rb +1 -38
- data/lib/active_genie/configs/providers_config.rb +7 -7
- data/lib/active_genie/errors/invalid_model_error.rb +42 -0
- data/lib/active_genie/errors/invalid_provider_error.rb +1 -1
- data/lib/active_genie/errors/provider_server_error.rb +23 -0
- data/lib/active_genie/extractor/explanation.rb +15 -6
- data/lib/active_genie/extractor/litote.rb +13 -6
- data/lib/active_genie/lister/feud.rb +15 -6
- data/lib/active_genie/lister/juries.rb +14 -6
- data/lib/active_genie/providers/anthropic_provider.rb +1 -1
- data/lib/active_genie/providers/base_provider.rb +13 -7
- data/lib/active_genie/providers/deepseek_provider.rb +1 -1
- data/lib/active_genie/providers/google_provider.rb +1 -1
- data/lib/active_genie/providers/openai_provider.rb +2 -2
- data/lib/active_genie/providers/unified_provider.rb +30 -5
- data/lib/active_genie/ranker/elo.rb +3 -2
- data/lib/active_genie/ranker/free_for_all.rb +3 -3
- data/lib/active_genie/ranker/scoring.rb +5 -3
- data/lib/active_genie/ranker/tournament.rb +7 -8
- data/lib/active_genie/scorer/jury_bench.rb +22 -13
- data/lib/active_genie/utils/fiber_by_batch.rb +21 -0
- data/lib/tasks/benchmark.rake +1 -3
- data/lib/tasks/templates/active_genie.rb +0 -3
- data/lib/tasks/test.rake +3 -0
- metadata +23 -9
- /data/lib/active_genie/scorer/{jury_bench.md → jury_bench.prompt.md} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0305b370de6af340e3ae6d3afa10b67f7be5f68ba20916c2fb3b8c934c40fc3e
|
4
|
+
data.tar.gz: ab3ac5ed4b7bd14c53ceeff02240953b7427260e94630803dffd8669d5a5e051
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84c7f399b16c21599e6302aeb8c4243cd833dc7ab554bbc610d400d982e7e9cb48cdc5b626cd74897dfa235a97f4a48115110275e8761bfe2be2a8964b040890
|
7
|
+
data.tar.gz: 705f57b32a5f59b2b8d88650e3b5eb7be657e577b571e74709a53c5b60aee5bf38740621d901fa72342a7d89e113982173d6ab1120ecc76c105b0589bd8910db
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.30.
|
1
|
+
0.30.3
|
@@ -31,7 +31,7 @@ module ActiveGenie
|
|
31
31
|
@player_a = player_a
|
32
32
|
@player_b = player_b
|
33
33
|
@criteria = criteria
|
34
|
-
@
|
34
|
+
@initial_config = config
|
35
35
|
end
|
36
36
|
|
37
37
|
# @return [ComparatorResponse] The evaluation result containing the winner and reasoning
|
@@ -43,11 +43,7 @@ module ActiveGenie
|
|
43
43
|
{ role: 'user', content: "criteria: #{@criteria}" }
|
44
44
|
]
|
45
45
|
|
46
|
-
response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
|
47
|
-
messages,
|
48
|
-
FUNCTION,
|
49
|
-
config: @config
|
50
|
-
)
|
46
|
+
response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(messages, FUNCTION, config:)
|
51
47
|
|
52
48
|
response_formatted(response)
|
53
49
|
end
|
@@ -72,7 +68,7 @@ module ActiveGenie
|
|
72
68
|
end
|
73
69
|
|
74
70
|
def log_comparator(comparator_response)
|
75
|
-
|
71
|
+
config.logger.call(
|
76
72
|
code: :comparator,
|
77
73
|
player_a: @player_a[0..30],
|
78
74
|
player_b: @player_b[0..30],
|
@@ -80,6 +76,15 @@ module ActiveGenie
|
|
80
76
|
**comparator_response.to_h
|
81
77
|
)
|
82
78
|
end
|
79
|
+
|
80
|
+
def config
|
81
|
+
@config ||= begin
|
82
|
+
c = ActiveGenie.configuration.merge(@initial_config)
|
83
|
+
c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
|
84
|
+
|
85
|
+
c
|
86
|
+
end
|
87
|
+
end
|
83
88
|
end
|
84
89
|
end
|
85
90
|
end
|
@@ -3,21 +3,12 @@
|
|
3
3
|
module ActiveGenie
|
4
4
|
module Config
|
5
5
|
class LlmConfig
|
6
|
-
attr_accessor :model, :temperature, :max_tokens, :max_retries, :retry_delay,
|
7
|
-
:model_tier, :read_timeout, :open_timeout, :provider
|
6
|
+
attr_accessor :model, :recommended_model, :temperature, :max_tokens, :max_retries, :retry_delay,
|
7
|
+
:model_tier, :read_timeout, :open_timeout, :provider, :max_fibers
|
8
8
|
attr_reader :provider_name
|
9
9
|
|
10
10
|
def initialize
|
11
|
-
|
12
|
-
@provider_name = nil
|
13
|
-
@provider = nil
|
14
|
-
@temperature = 0
|
15
|
-
@max_tokens = 4096
|
16
|
-
@max_retries = nil
|
17
|
-
@retry_delay = nil
|
18
|
-
@model_tier = 'lower_tier'
|
19
|
-
@read_timeout = nil
|
20
|
-
@open_timeout = nil
|
11
|
+
set_defaults
|
21
12
|
end
|
22
13
|
|
23
14
|
def provider_name=(provider_name)
|
@@ -33,6 +24,17 @@ module ActiveGenie
|
|
33
24
|
end
|
34
25
|
end
|
35
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def set_defaults
|
31
|
+
@model = @recommended_model = @provider_name = @provider = nil
|
32
|
+
@max_retries = @retry_delay = @read_timeout = @open_timeout = nil
|
33
|
+
@temperature = 0
|
34
|
+
@max_tokens = 4096
|
35
|
+
@model_tier = 'lower_tier'
|
36
|
+
@max_fibers = 10
|
37
|
+
end
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|
@@ -30,27 +30,6 @@ module ActiveGenie
|
|
30
30
|
def anthropic_version
|
31
31
|
@anthropic_version || '2023-06-01'
|
32
32
|
end
|
33
|
-
|
34
|
-
# Retrieves the model name designated for the lower tier (e.g., cost-effective, faster).
|
35
|
-
# Defaults to 'claude-3-haiku'.
|
36
|
-
# @return [String] The lower tier model name.
|
37
|
-
def lower_tier_model
|
38
|
-
@lower_tier_model || 'claude-3-5-haiku-20241022'
|
39
|
-
end
|
40
|
-
|
41
|
-
# Retrieves the model name designated for the middle tier (e.g., balanced performance).
|
42
|
-
# Defaults to 'claude-3-sonnet'.
|
43
|
-
# @return [String] The middle tier model name.
|
44
|
-
def middle_tier_model
|
45
|
-
@middle_tier_model || 'claude-3-7-sonnet-20250219'
|
46
|
-
end
|
47
|
-
|
48
|
-
# Retrieves the model name designated for the upper tier (e.g., most capable).
|
49
|
-
# Defaults to 'claude-3-opus'.
|
50
|
-
# @return [String] The upper tier model name.
|
51
|
-
def higher_tier_model
|
52
|
-
@higher_tier_model || 'claude-3-opus-20240229'
|
53
|
-
end
|
54
33
|
end
|
55
34
|
end
|
56
35
|
end
|
@@ -23,27 +23,6 @@ module ActiveGenie
|
|
23
23
|
def api_url
|
24
24
|
@api_url || 'https://api.deepseek.com/v1'
|
25
25
|
end
|
26
|
-
|
27
|
-
# Retrieves the model name designated for the lower tier (e.g., cost-effective, faster).
|
28
|
-
# Defaults to 'deepseek-chat'.
|
29
|
-
# @return [String] The lower tier model name.
|
30
|
-
def lower_tier_model
|
31
|
-
@lower_tier_model || 'deepseek-chat'
|
32
|
-
end
|
33
|
-
|
34
|
-
# Retrieves the model name designated for the middle tier (e.g., balanced performance).
|
35
|
-
# Defaults to 'deepseek-chat'.
|
36
|
-
# @return [String] The middle tier model name.
|
37
|
-
def middle_tier_model
|
38
|
-
@middle_tier_model || 'deepseek-chat'
|
39
|
-
end
|
40
|
-
|
41
|
-
# Retrieves the model name designated for the upper tier (e.g., most capable).
|
42
|
-
# Defaults to 'deepseek-reasoner'.
|
43
|
-
# @return [String] The upper tier model name.
|
44
|
-
def higher_tier_model
|
45
|
-
@higher_tier_model || 'deepseek-reasoner'
|
46
|
-
end
|
47
26
|
end
|
48
27
|
end
|
49
28
|
end
|
@@ -25,27 +25,6 @@ module ActiveGenie
|
|
25
25
|
# The base URL here should be just the domain part.
|
26
26
|
@api_url || 'https://generativelanguage.googleapis.com'
|
27
27
|
end
|
28
|
-
|
29
|
-
# Retrieves the model name designated for the lower tier (e.g., cost-effective, faster).
|
30
|
-
# Defaults to 'gemini-2.0-flash-lite'.
|
31
|
-
# @return [String] The lower tier model name.
|
32
|
-
def lower_tier_model
|
33
|
-
@lower_tier_model || 'gemini-2.0-flash-lite'
|
34
|
-
end
|
35
|
-
|
36
|
-
# Retrieves the model name designated for the middle tier (e.g., balanced performance).
|
37
|
-
# Defaults to 'gemini-2.0-flash'.
|
38
|
-
# @return [String] The middle tier model name.
|
39
|
-
def middle_tier_model
|
40
|
-
@middle_tier_model || 'gemini-2.0-flash'
|
41
|
-
end
|
42
|
-
|
43
|
-
# Retrieves the model name designated for the upper tier (e.g., most capable).
|
44
|
-
# Defaults to 'gemini-2.5-pro-experimental'.
|
45
|
-
# @return [String] The upper tier model name.
|
46
|
-
def higher_tier_model
|
47
|
-
@higher_tier_model || 'gemini-2.5-pro-experimental'
|
48
|
-
end
|
49
28
|
end
|
50
29
|
end
|
51
30
|
end
|
@@ -23,27 +23,6 @@ module ActiveGenie
|
|
23
23
|
def api_url
|
24
24
|
@api_url || 'https://api.openai.com/v1'
|
25
25
|
end
|
26
|
-
|
27
|
-
# Retrieves the model name designated for the lower tier (e.g., cost-effective, faster).
|
28
|
-
# Defaults to 'gpt-4o-mini'.
|
29
|
-
# @return [String] The lower tier model name.
|
30
|
-
def lower_tier_model
|
31
|
-
@lower_tier_model || 'gpt-4.1-mini'
|
32
|
-
end
|
33
|
-
|
34
|
-
# Retrieves the model name designated for the middle tier (e.g., balanced performance).
|
35
|
-
# Defaults to 'gpt-4o'.
|
36
|
-
# @return [String] The middle tier model name.
|
37
|
-
def middle_tier_model
|
38
|
-
@middle_tier_model || 'gpt-4.1'
|
39
|
-
end
|
40
|
-
|
41
|
-
# Retrieves the model name designated for the upper tier (e.g., most capable).
|
42
|
-
# Defaults to 'o1-preview'.
|
43
|
-
# @return [String] The upper tier model name.
|
44
|
-
def higher_tier_model
|
45
|
-
@higher_tier_model || 'o3-mini'
|
46
|
-
end
|
47
26
|
end
|
48
27
|
end
|
49
28
|
end
|
@@ -6,20 +6,7 @@ module ActiveGenie
|
|
6
6
|
class ProviderBase
|
7
7
|
NAME = :unknown
|
8
8
|
|
9
|
-
attr_writer :api_key, :organization, :api_url, :client
|
10
|
-
:lower_tier_model, :middle_tier_model, :higher_tier_model
|
11
|
-
|
12
|
-
# Maps a symbolic tier (:lower_tier, :middle_tier, :upper_tier) to a specific model name.
|
13
|
-
# Falls back to the lower_tier_model if the tier is nil or unrecognized.
|
14
|
-
# @param tier [Symbol, String, nil] The symbolic tier name.
|
15
|
-
# @return [String] The corresponding model name.
|
16
|
-
def tier_to_model(tier)
|
17
|
-
{
|
18
|
-
lower_tier: lower_tier_model,
|
19
|
-
middle_tier: middle_tier_model,
|
20
|
-
upper_tier: higher_tier_model
|
21
|
-
}[tier&.to_sym] || lower_tier_model
|
22
|
-
end
|
9
|
+
attr_writer :api_key, :organization, :api_url, :client
|
23
10
|
|
24
11
|
# Returns a hash representation of the configuration.
|
25
12
|
# @param config [Hash] Additional key-value pairs to merge into the hash.
|
@@ -29,9 +16,6 @@ module ActiveGenie
|
|
29
16
|
name: NAME,
|
30
17
|
api_key:,
|
31
18
|
api_url:,
|
32
|
-
lower_tier_model:,
|
33
|
-
middle_tier_model:,
|
34
|
-
higher_tier_model:,
|
35
19
|
**config
|
36
20
|
}
|
37
21
|
end
|
@@ -62,27 +46,6 @@ module ActiveGenie
|
|
62
46
|
def client
|
63
47
|
raise NotImplementedError, 'Subclasses must implement this method'
|
64
48
|
end
|
65
|
-
|
66
|
-
# Retrieves the model name designated for the lower tier (e.g., cost-effective, faster).
|
67
|
-
# Defaults to 'gpt-4o-mini'.
|
68
|
-
# @return [String] The lower tier model name.
|
69
|
-
def lower_tier_model
|
70
|
-
raise NotImplementedError, 'Subclasses must implement this method'
|
71
|
-
end
|
72
|
-
|
73
|
-
# Retrieves the model name designated for the middle tier (e.g., balanced performance).
|
74
|
-
# Defaults to 'gpt-4o'.
|
75
|
-
# @return [String] The middle tier model name.
|
76
|
-
def middle_tier_model
|
77
|
-
raise NotImplementedError, 'Subclasses must implement this method'
|
78
|
-
end
|
79
|
-
|
80
|
-
# Retrieves the model name designated for the upper tier (e.g., most capable).
|
81
|
-
# Defaults to 'o1-preview'.
|
82
|
-
# @return [String] The upper tier model name.
|
83
|
-
def higher_tier_model
|
84
|
-
raise NotImplementedError, 'Subclasses must implement this method'
|
85
|
-
end
|
86
49
|
end
|
87
50
|
end
|
88
51
|
end
|
@@ -11,7 +11,7 @@ module ActiveGenie
|
|
11
11
|
attr_reader :all
|
12
12
|
|
13
13
|
def default
|
14
|
-
@default || valid.keys.first
|
14
|
+
@default || ENV.fetch('PROVIDER_NAME', nil) || valid.keys.first
|
15
15
|
end
|
16
16
|
|
17
17
|
def default=(provider)
|
@@ -24,20 +24,20 @@ module ActiveGenie
|
|
24
24
|
@all.slice(*valid_provider_keys)
|
25
25
|
end
|
26
26
|
|
27
|
-
def add(
|
27
|
+
def add(provider_configs)
|
28
28
|
@all ||= {}
|
29
|
-
Array(
|
30
|
-
name =
|
29
|
+
Array(provider_configs).each do |provider_config|
|
30
|
+
name = provider_config::NAME
|
31
31
|
remove([name]) if @all.key?(name)
|
32
32
|
|
33
|
-
@all[name] =
|
33
|
+
@all[name] = provider_config.new
|
34
34
|
end
|
35
35
|
|
36
36
|
self
|
37
37
|
end
|
38
38
|
|
39
|
-
def remove(
|
40
|
-
Array(
|
39
|
+
def remove(provider_configs)
|
40
|
+
Array(provider_configs).each do |provider|
|
41
41
|
@all.delete(provider::NAME)
|
42
42
|
end
|
43
43
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGenie
|
4
|
+
class InvalidModelError < StandardError
|
5
|
+
TEXT = <<~TEXT
|
6
|
+
Invalid model: %<model>s
|
7
|
+
|
8
|
+
To configure ActiveGenie, you can either:
|
9
|
+
1. Set up global configuration:
|
10
|
+
```ruby
|
11
|
+
ActiveGenie.configure do |config|
|
12
|
+
config.providers.openai.api_key = 'your_api_key'
|
13
|
+
config.llm.model = 'gpt-5'
|
14
|
+
# ... other configuration options
|
15
|
+
end
|
16
|
+
```
|
17
|
+
|
18
|
+
2. Or pass configuration directly to the method call:
|
19
|
+
```ruby
|
20
|
+
ActiveGenie::Extraction.call(
|
21
|
+
arg1,
|
22
|
+
arg2,
|
23
|
+
config: {
|
24
|
+
providers: {
|
25
|
+
openai: {
|
26
|
+
api_key: 'your_api_key'
|
27
|
+
}
|
28
|
+
},
|
29
|
+
llm: {
|
30
|
+
model: 'gpt-5'
|
31
|
+
}
|
32
|
+
}
|
33
|
+
)
|
34
|
+
```
|
35
|
+
|
36
|
+
TEXT
|
37
|
+
|
38
|
+
def initialize(model)
|
39
|
+
super(format(TEXT, model:))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGenie
|
4
|
+
class ProviderServerError < StandardError
|
5
|
+
TEXT = <<~TEXT.freeze
|
6
|
+
Provider server error: #{code}
|
7
|
+
#{body}
|
8
|
+
|
9
|
+
Providers errors are common and can occur for various reasons, such as:
|
10
|
+
- Invalid API key
|
11
|
+
- Exceeded usage limits
|
12
|
+
- Temporary server issues
|
13
|
+
|
14
|
+
Be ready to handle these errors gracefully in your application. We recommend implementing retry logic and exponential backoff strategies.
|
15
|
+
Usually async workers layer is the ideal place to handle such errors.
|
16
|
+
TEXT
|
17
|
+
|
18
|
+
def initialize(response)
|
19
|
+
@response = response
|
20
|
+
super(format(TEXT, **response))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -31,7 +31,7 @@ module ActiveGenie
|
|
31
31
|
def initialize(text, data_to_extract, config: {})
|
32
32
|
@text = text
|
33
33
|
@data_to_extract = data_to_extract
|
34
|
-
@
|
34
|
+
@initial_config = config
|
35
35
|
end
|
36
36
|
|
37
37
|
def call
|
@@ -54,7 +54,7 @@ module ActiveGenie
|
|
54
54
|
private
|
55
55
|
|
56
56
|
def data_to_extract_with_explanation
|
57
|
-
return @data_to_extract unless
|
57
|
+
return @data_to_extract unless config.extractor.with_explanation
|
58
58
|
|
59
59
|
with_explanation = {}
|
60
60
|
|
@@ -84,10 +84,10 @@ module ActiveGenie
|
|
84
84
|
response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
|
85
85
|
messages,
|
86
86
|
function,
|
87
|
-
config:
|
87
|
+
config: config
|
88
88
|
)
|
89
89
|
|
90
|
-
|
90
|
+
config.logger.call(
|
91
91
|
{
|
92
92
|
code: :extractor,
|
93
93
|
text: @text[0..30],
|
@@ -100,7 +100,7 @@ module ActiveGenie
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def simplify_response(response)
|
103
|
-
return response if
|
103
|
+
return response if config.extractor.verbose
|
104
104
|
|
105
105
|
simplified_response = {}
|
106
106
|
|
@@ -115,12 +115,21 @@ module ActiveGenie
|
|
115
115
|
end
|
116
116
|
|
117
117
|
def min_accuracy
|
118
|
-
|
118
|
+
config.extractor.min_accuracy # default 70
|
119
119
|
end
|
120
120
|
|
121
121
|
def prompt
|
122
122
|
File.read(File.join(__dir__, 'explanation.prompt.md'))
|
123
123
|
end
|
124
|
+
|
125
|
+
def config
|
126
|
+
@config ||= begin
|
127
|
+
c = ActiveGenie.configuration.merge(@initial_config)
|
128
|
+
c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
|
129
|
+
|
130
|
+
c
|
131
|
+
end
|
132
|
+
end
|
124
133
|
end
|
125
134
|
end
|
126
135
|
end
|
@@ -31,15 +31,13 @@ module ActiveGenie
|
|
31
31
|
def initialize(text, data_to_extract, config: {})
|
32
32
|
@text = text
|
33
33
|
@data_to_extract = data_to_extract
|
34
|
-
@
|
34
|
+
@initial_config = config
|
35
35
|
end
|
36
36
|
|
37
37
|
def call
|
38
|
-
response =
|
38
|
+
response = Explanation.call(@text, extract_with_litote, config:)
|
39
39
|
|
40
|
-
if response[:message_litote]
|
41
|
-
response = Generalist.call(response[:litote_rephrased], @data_to_extract, config: @config)
|
42
|
-
end
|
40
|
+
response = Explanation.call(response[:litote_rephrased], @data_to_extract, config:) if response[:message_litote]
|
43
41
|
|
44
42
|
response
|
45
43
|
end
|
@@ -47,10 +45,19 @@ module ActiveGenie
|
|
47
45
|
private
|
48
46
|
|
49
47
|
def extract_with_litote
|
50
|
-
parameters = JSON.parse(File.read(File.join(__dir__, '
|
48
|
+
parameters = JSON.parse(File.read(File.join(__dir__, 'litote.json')), symbolize_names: true)
|
51
49
|
|
52
50
|
@data_to_extract.merge(parameters)
|
53
51
|
end
|
52
|
+
|
53
|
+
def config
|
54
|
+
@config ||= begin
|
55
|
+
c = ActiveGenie.configuration.merge(@initial_config)
|
56
|
+
c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
|
57
|
+
|
58
|
+
c
|
59
|
+
end
|
60
|
+
end
|
54
61
|
end
|
55
62
|
end
|
56
63
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '../
|
3
|
+
require_relative '../providers/unified_provider'
|
4
4
|
|
5
5
|
module ActiveGenie
|
6
6
|
module Lister
|
@@ -19,7 +19,7 @@ module ActiveGenie
|
|
19
19
|
# @return [Array of strings] List of items
|
20
20
|
def initialize(theme, config: {})
|
21
21
|
@theme = theme
|
22
|
-
@
|
22
|
+
@initial_config = config
|
23
23
|
end
|
24
24
|
|
25
25
|
# @return [Array of strings] The list of items
|
@@ -30,10 +30,10 @@ module ActiveGenie
|
|
30
30
|
{ role: 'user', content: "theme: #{@theme}" }
|
31
31
|
]
|
32
32
|
|
33
|
-
response = ::ActiveGenie::
|
33
|
+
response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
|
34
34
|
messages,
|
35
35
|
FUNCTION,
|
36
|
-
config:
|
36
|
+
config:
|
37
37
|
)
|
38
38
|
|
39
39
|
log_feud(response)
|
@@ -46,16 +46,25 @@ module ActiveGenie
|
|
46
46
|
private
|
47
47
|
|
48
48
|
def number_of_items
|
49
|
-
|
49
|
+
config.lister.number_of_items
|
50
50
|
end
|
51
51
|
|
52
52
|
def log_feud(response)
|
53
|
-
|
53
|
+
config.logger.call(
|
54
54
|
code: :feud,
|
55
55
|
theme: @theme[0..30],
|
56
56
|
items: response['items'].map { |item| item[0..30] }
|
57
57
|
)
|
58
58
|
end
|
59
|
+
|
60
|
+
def config
|
61
|
+
@config ||= begin
|
62
|
+
c = ActiveGenie.configuration.merge(@initial_config)
|
63
|
+
c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
|
64
|
+
|
65
|
+
c
|
66
|
+
end
|
67
|
+
end
|
59
68
|
end
|
60
69
|
end
|
61
70
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '../
|
3
|
+
require_relative '../providers/unified_provider'
|
4
4
|
|
5
5
|
module ActiveGenie
|
6
6
|
module Lister
|
@@ -14,8 +14,7 @@ module ActiveGenie
|
|
14
14
|
# @example Getting jury for technical content
|
15
15
|
# Juries.call("Technical documentation about API design",
|
16
16
|
# "Evaluate technical accuracy and clarity")
|
17
|
-
# # =>
|
18
|
-
# # jury3: "Developer Advocate", reasoning: "..." }
|
17
|
+
# # => [ "API Architect", "Technical Writer", "Developer Advocate" ]
|
19
18
|
#
|
20
19
|
class Juries
|
21
20
|
def self.call(...)
|
@@ -30,7 +29,7 @@ module ActiveGenie
|
|
30
29
|
def initialize(text, criteria, config: {})
|
31
30
|
@text = text
|
32
31
|
@criteria = criteria
|
33
|
-
@
|
32
|
+
@initial_config = config
|
34
33
|
end
|
35
34
|
|
36
35
|
def call
|
@@ -62,7 +61,7 @@ module ActiveGenie
|
|
62
61
|
result = client.function_calling(
|
63
62
|
messages,
|
64
63
|
function,
|
65
|
-
config:
|
64
|
+
config:
|
66
65
|
)
|
67
66
|
|
68
67
|
result['juries'] || []
|
@@ -71,12 +70,21 @@ module ActiveGenie
|
|
71
70
|
private
|
72
71
|
|
73
72
|
def client
|
74
|
-
::ActiveGenie::
|
73
|
+
::ActiveGenie::Providers::UnifiedProvider
|
75
74
|
end
|
76
75
|
|
77
76
|
def prompt
|
78
77
|
@prompt ||= File.read(File.join(__dir__, 'juries.prompt.md'))
|
79
78
|
end
|
79
|
+
|
80
|
+
def config
|
81
|
+
@config ||= begin
|
82
|
+
c = ActiveGenie.configuration.merge(@initial_config)
|
83
|
+
c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
|
84
|
+
|
85
|
+
c
|
86
|
+
end
|
87
|
+
end
|
80
88
|
end
|
81
89
|
end
|
82
90
|
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'net/http'
|
4
|
+
|
3
5
|
module ActiveGenie
|
4
6
|
module Providers
|
5
7
|
class BaseProvider
|
6
|
-
class
|
8
|
+
class ProviderUnknownError < StandardError; end
|
7
9
|
|
8
10
|
DEFAULT_HEADERS = {
|
9
11
|
'Content-Type': 'application/json',
|
@@ -81,7 +83,7 @@ module ActiveGenie
|
|
81
83
|
|
82
84
|
response = http_request(request, uri)
|
83
85
|
|
84
|
-
raise
|
86
|
+
raise ProviderUnknownError, "Unexpected response: #{response.code} - #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
85
87
|
|
86
88
|
parsed_response = parse_response(response)
|
87
89
|
|
@@ -143,7 +145,7 @@ module ActiveGenie
|
|
143
145
|
begin
|
144
146
|
JSON.parse(response.body)
|
145
147
|
rescue JSON::ParserError => e
|
146
|
-
raise
|
148
|
+
raise ProviderUnknownError, "Failed to parse JSON response: #{e.message}"
|
147
149
|
end
|
148
150
|
end
|
149
151
|
|
@@ -169,8 +171,12 @@ module ActiveGenie
|
|
169
171
|
retries = 0
|
170
172
|
|
171
173
|
begin
|
172
|
-
yield
|
173
|
-
|
174
|
+
response = yield
|
175
|
+
|
176
|
+
raise ActiveGenie::ProviderServerError, response if response&.code.to_i >= 500
|
177
|
+
|
178
|
+
response
|
179
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, ProviderServerError => e
|
174
180
|
raise if retries > max_retries
|
175
181
|
|
176
182
|
sleep_time = retry_delay * (2**retries)
|
@@ -179,8 +185,8 @@ module ActiveGenie
|
|
179
185
|
@config.logger.call(
|
180
186
|
code: :retry_attempt,
|
181
187
|
attempt: retries,
|
182
|
-
max_retries
|
183
|
-
|
188
|
+
max_retries:,
|
189
|
+
next_retry_in_seconds: sleep_time,
|
184
190
|
error: e.message
|
185
191
|
)
|
186
192
|
|
@@ -37,7 +37,7 @@ module ActiveGenie
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def request(payload)
|
40
|
-
response = post(url, payload, headers:
|
40
|
+
response = post(url, payload, headers:)
|
41
41
|
|
42
42
|
return nil if response.nil?
|
43
43
|
|
@@ -71,7 +71,7 @@ module ActiveGenie
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def model
|
74
|
-
@config.llm.model
|
74
|
+
@config.llm.model
|
75
75
|
end
|
76
76
|
|
77
77
|
def url
|
@@ -5,12 +5,13 @@ require_relative 'anthropic_provider'
|
|
5
5
|
require_relative 'google_provider'
|
6
6
|
require_relative 'deepseek_provider'
|
7
7
|
require_relative '../errors/invalid_provider_error'
|
8
|
+
require_relative '../errors/invalid_model_error'
|
8
9
|
|
9
10
|
module ActiveGenie
|
10
11
|
module Providers
|
11
12
|
class UnifiedProvider
|
12
13
|
class << self
|
13
|
-
|
14
|
+
PROVIDER_NAME_TO_PROVIDER = {
|
14
15
|
openai: OpenaiProvider,
|
15
16
|
anthropic: AnthropicProvider,
|
16
17
|
google: GoogleProvider,
|
@@ -18,10 +19,8 @@ module ActiveGenie
|
|
18
19
|
}.freeze
|
19
20
|
|
20
21
|
def function_calling(messages, function, config: {})
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
raise ActiveGenie::InvalidProviderError, provider_name if provider.nil?
|
22
|
+
provider = provider(config)
|
23
|
+
define_llm_model(config)
|
25
24
|
|
26
25
|
response = provider.new(config).function_calling(messages, function)
|
27
26
|
|
@@ -30,6 +29,32 @@ module ActiveGenie
|
|
30
29
|
|
31
30
|
private
|
32
31
|
|
32
|
+
def provider(config)
|
33
|
+
provider_name = config.llm.provider_name || config.providers.default
|
34
|
+
|
35
|
+
unless config.providers.valid.keys.include?(provider_name.to_sym)
|
36
|
+
raise ActiveGenie::InvalidProviderError,
|
37
|
+
provider_name
|
38
|
+
end
|
39
|
+
|
40
|
+
provider = PROVIDER_NAME_TO_PROVIDER[provider_name.to_sym]
|
41
|
+
|
42
|
+
raise ActiveGenie::InvalidProviderError, provider_name if provider.nil?
|
43
|
+
|
44
|
+
provider
|
45
|
+
end
|
46
|
+
|
47
|
+
def define_llm_model(config)
|
48
|
+
if config.llm.model.nil?
|
49
|
+
raise ActiveGenie::InvalidModelError, 'nil' unless config.llm.recommended_model
|
50
|
+
|
51
|
+
config.llm.model = config.llm.recommended_model
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
config.llm.model
|
56
|
+
end
|
57
|
+
|
33
58
|
def normalize_response(response)
|
34
59
|
response.each do |key, value|
|
35
60
|
response[key] = nil if ['null', 'none', 'undefined', '', 'unknown',
|
@@ -23,9 +23,9 @@ module ActiveGenie
|
|
23
23
|
@config.log.add_observer(observers: ->(log) { log_observer(log) })
|
24
24
|
@config.log.additional_context = { elo_id: }
|
25
25
|
|
26
|
-
matches
|
27
|
-
# TODO: debate can take a while, can be parallelized
|
26
|
+
ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
|
28
27
|
winner, loser = debate(player_a, player_b)
|
28
|
+
|
29
29
|
update_players_elo(winner, loser)
|
30
30
|
end
|
31
31
|
|
@@ -105,6 +105,7 @@ module ActiveGenie
|
|
105
105
|
players_in: players_in.map(&:id),
|
106
106
|
debates_count: matches.size,
|
107
107
|
total_tokens: @total_tokens,
|
108
|
+
players_in_round: players_in.map(&:id),
|
108
109
|
previous_highest_elo: @previous_highest_elo,
|
109
110
|
highest_elo:,
|
110
111
|
highest_elo_diff: highest_elo - @previous_highest_elo,
|
@@ -8,7 +8,7 @@ module ActiveGenie
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def initialize(players, criteria, config: nil)
|
11
|
-
@players = Players.new(players)
|
11
|
+
@players = Entities::Players.new(players)
|
12
12
|
@criteria = criteria
|
13
13
|
@config = config || ActiveGenie.configuration
|
14
14
|
@start_time = Time.now
|
@@ -19,7 +19,7 @@ module ActiveGenie
|
|
19
19
|
@config.log.add_observer(observers: ->(log) { log_observer(log) })
|
20
20
|
@config.log.additional_context = { free_for_all_id: }
|
21
21
|
|
22
|
-
matches
|
22
|
+
ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
|
23
23
|
winner, loser = debate(player_a, player_b)
|
24
24
|
|
25
25
|
update_players_score(winner, loser)
|
@@ -39,7 +39,7 @@ module ActiveGenie
|
|
39
39
|
def debate(player_a, player_b)
|
40
40
|
log_context = { player_a_id: player_a.id, player_b_id: player_b.id }
|
41
41
|
|
42
|
-
result = ActiveGenie::Comparator.
|
42
|
+
result = ActiveGenie::Comparator.by_debate(
|
43
43
|
player_a.content,
|
44
44
|
player_b.content,
|
45
45
|
@criteria,
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../utils/fiber_by_batch'
|
4
|
+
|
3
5
|
module ActiveGenie
|
4
6
|
module Ranker
|
5
7
|
class Scoring
|
@@ -8,7 +10,7 @@ module ActiveGenie
|
|
8
10
|
end
|
9
11
|
|
10
12
|
def initialize(players, criteria, juries: [], config: nil)
|
11
|
-
@players = Players.new(players)
|
13
|
+
@players = Entities::Players.new(players)
|
12
14
|
@criteria = criteria
|
13
15
|
@config = ActiveGenie.configuration.merge(config)
|
14
16
|
@juries = Array(juries).compact.uniq
|
@@ -17,7 +19,7 @@ module ActiveGenie
|
|
17
19
|
def call
|
18
20
|
@config.log.additional_context = { ranker_scoring_id: }
|
19
21
|
|
20
|
-
players_without_score
|
22
|
+
ActiveGenie::FiberByBatch.call(players_without_score, config: @config) do |player|
|
21
23
|
player.score = generate_score(player)
|
22
24
|
end
|
23
25
|
end
|
@@ -29,7 +31,7 @@ module ActiveGenie
|
|
29
31
|
end
|
30
32
|
|
31
33
|
def generate_score(player)
|
32
|
-
score, reasoning = ActiveGenie::Scorer.
|
34
|
+
score, reasoning = ActiveGenie::Scorer.by_jury_bench(
|
33
35
|
player.content,
|
34
36
|
@criteria,
|
35
37
|
@juries,
|
@@ -35,12 +35,11 @@ module ActiveGenie
|
|
35
35
|
new(...).call
|
36
36
|
end
|
37
37
|
|
38
|
-
def initialize(players, criteria,
|
39
|
-
@players = Players.new(players)
|
38
|
+
def initialize(players, criteria, juries: [], config: {})
|
39
|
+
@players = Entities::Players.new(players)
|
40
40
|
@criteria = criteria
|
41
|
-
@
|
41
|
+
@juries = Array(juries).compact.uniq
|
42
42
|
@config = ActiveGenie.configuration.merge(config)
|
43
|
-
@players = nil
|
44
43
|
end
|
45
44
|
|
46
45
|
def call
|
@@ -51,7 +50,7 @@ module ActiveGenie
|
|
51
50
|
|
52
51
|
while @players.elo_eligible?
|
53
52
|
elo_report = run_elo_round!
|
54
|
-
|
53
|
+
eliminate_lower_tier_players!
|
55
54
|
rebalance_players!(elo_report)
|
56
55
|
end
|
57
56
|
|
@@ -69,7 +68,7 @@ module ActiveGenie
|
|
69
68
|
Scoring.call(
|
70
69
|
@players,
|
71
70
|
@criteria,
|
72
|
-
|
71
|
+
juries: @juries,
|
73
72
|
config: @config
|
74
73
|
)
|
75
74
|
end
|
@@ -88,8 +87,8 @@ module ActiveGenie
|
|
88
87
|
)
|
89
88
|
end
|
90
89
|
|
91
|
-
def
|
92
|
-
@players.
|
90
|
+
def eliminate_lower_tier_players!
|
91
|
+
@players.calc_lower_tier.each { |player| player.eliminated = ELIMINATION_RELEGATION }
|
93
92
|
end
|
94
93
|
|
95
94
|
def rebalance_players!(elo_report)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '../
|
3
|
+
require_relative '../providers/unified_provider'
|
4
4
|
|
5
5
|
module ActiveGenie
|
6
6
|
module Scorer
|
@@ -34,7 +34,7 @@ module ActiveGenie
|
|
34
34
|
@text = text
|
35
35
|
@criteria = criteria
|
36
36
|
@param_juries = Array(juries).compact.uniq
|
37
|
-
@
|
37
|
+
@initial_config = config
|
38
38
|
end
|
39
39
|
|
40
40
|
def call
|
@@ -44,22 +44,22 @@ module ActiveGenie
|
|
44
44
|
{ role: 'user', content: "Text to score: #{@text}" }
|
45
45
|
]
|
46
46
|
|
47
|
-
result = ::ActiveGenie::
|
47
|
+
result = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
|
48
48
|
messages,
|
49
49
|
build_function,
|
50
|
-
config:
|
50
|
+
config:
|
51
51
|
)
|
52
52
|
|
53
53
|
result['final_score'] = 0 if result['final_score'].nil?
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
config.logger.call({
|
56
|
+
code: :Scorer,
|
57
|
+
text: @text[0..30],
|
58
|
+
criteria: @criteria[0..30],
|
59
|
+
juries: juries,
|
60
|
+
score: result['final_score'],
|
61
|
+
reasoning: result['final_reasoning']
|
62
|
+
})
|
63
63
|
|
64
64
|
result
|
65
65
|
end
|
@@ -113,9 +113,18 @@ module ActiveGenie
|
|
113
113
|
@juries ||= if @param_juries.any?
|
114
114
|
@param_juries
|
115
115
|
else
|
116
|
-
::ActiveGenie::Lister::Juries.call(@text, @criteria, config:
|
116
|
+
::ActiveGenie::Lister::Juries.call(@text, @criteria, config:)
|
117
117
|
end
|
118
118
|
end
|
119
|
+
|
120
|
+
def config
|
121
|
+
@config ||= begin
|
122
|
+
c = ActiveGenie.configuration.merge(@initial_config)
|
123
|
+
c.llm.recommended_model = 'deepseek-chat' unless c.llm.recommended_model
|
124
|
+
|
125
|
+
c
|
126
|
+
end
|
127
|
+
end
|
119
128
|
end
|
120
129
|
end
|
121
130
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'async'
|
4
|
+
|
5
|
+
module ActiveGenie
|
6
|
+
module FiberByBatch
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def call(items, config:, &block)
|
10
|
+
items.each_slice(config.llm.max_fibers).to_a.each do |batch|
|
11
|
+
Async do
|
12
|
+
tasks = batch.map do |item|
|
13
|
+
Async { block.call(item) }
|
14
|
+
end
|
15
|
+
|
16
|
+
tasks.each(&:wait)
|
17
|
+
end.wait
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/tasks/benchmark.rake
CHANGED
@@ -4,11 +4,9 @@ namespace :active_genie do
|
|
4
4
|
desc 'Run benchmarks, optionally for a specific module (e.g., rake active_genie:benchmark[data_extractor])'
|
5
5
|
task :benchmark, [:module_name] do |_, args|
|
6
6
|
Rake::TestTask.new(:run_benchmarks) do |t|
|
7
|
-
t.libs << 'benchmark'
|
8
|
-
|
9
7
|
if args[:module_name]
|
10
8
|
module_name = args[:module_name]
|
11
|
-
module_path = "benchmark
|
9
|
+
module_path = "test/benchmark/#{module_name}/"
|
12
10
|
t.test_files = FileList["#{module_path}**/*_test.rb"]
|
13
11
|
puts "Running benchmarks for module: #{module_name}"
|
14
12
|
else
|
@@ -5,9 +5,6 @@ ActiveGenie.configure do |config|
|
|
5
5
|
# config.providers.openai.api_key = ENV['OPENAI_API_KEY']
|
6
6
|
# config.providers.openai.organization = ENV['OPENAI_ORGANIZATION']
|
7
7
|
# config.providers.openai.api_url = 'https://api.openai.com/v1'
|
8
|
-
# config.providers.openai.lower_tier_model = 'gpt-4.1-mini'
|
9
|
-
# config.providers.openai.middle_tier_model = 'gpt-4.1'
|
10
|
-
# config.providers.openai.higher_tier_model = 'o3-mini'
|
11
8
|
# config.providers.openai.client = ActiveGenie::Providers::Openai::Client.new(config)
|
12
9
|
|
13
10
|
# example how add a new provider
|
data/lib/tasks/test.rake
CHANGED
metadata
CHANGED
@@ -1,17 +1,30 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_genie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.30.
|
4
|
+
version: 0.30.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Radamés Roriz
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: async
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.0'
|
13
26
|
description: |
|
14
|
-
ActiveGenie is a Ruby gem that helps developers build reliable, future-proof GenAI features without worrying about changing models, prompts, or providers. Like Lodash for GenAI, it offers simple, reusable modules for tasks like
|
27
|
+
ActiveGenie is a Ruby gem that helps developers build reliable, future-proof GenAI features without worrying about changing models, prompts, or providers. Like Lodash for GenAI, it offers simple, reusable modules for tasks like extractor, comparator, scorer, and ranker, so you can focus on your app’s logic, not the shifting AI landscape.
|
15
28
|
Behind the scenes, a custom benchmarking system keeps everything consistent across LLM vendors and versions, release after release.
|
16
29
|
email:
|
17
30
|
- radames@roriz.dev
|
@@ -45,7 +58,9 @@ files:
|
|
45
58
|
- lib/active_genie/configs/scorer_config.rb
|
46
59
|
- lib/active_genie/configuration.rb
|
47
60
|
- lib/active_genie/errors/invalid_log_output_error.rb
|
61
|
+
- lib/active_genie/errors/invalid_model_error.rb
|
48
62
|
- lib/active_genie/errors/invalid_provider_error.rb
|
63
|
+
- lib/active_genie/errors/provider_server_error.rb
|
49
64
|
- lib/active_genie/extractor.rb
|
50
65
|
- lib/active_genie/extractor/explanation.json
|
51
66
|
- lib/active_genie/extractor/explanation.prompt.md
|
@@ -73,8 +88,9 @@ files:
|
|
73
88
|
- lib/active_genie/ranker/scoring.rb
|
74
89
|
- lib/active_genie/ranker/tournament.rb
|
75
90
|
- lib/active_genie/scorer.rb
|
76
|
-
- lib/active_genie/scorer/jury_bench.md
|
91
|
+
- lib/active_genie/scorer/jury_bench.prompt.md
|
77
92
|
- lib/active_genie/scorer/jury_bench.rb
|
93
|
+
- lib/active_genie/utils/fiber_by_batch.rb
|
78
94
|
- lib/tasks/benchmark.rake
|
79
95
|
- lib/tasks/install.rake
|
80
96
|
- lib/tasks/templates/active_genie.rb
|
@@ -88,7 +104,6 @@ metadata:
|
|
88
104
|
changelog_uri: https://github.com/Roriz/active_genie/blob/master/CHANGELOG.md
|
89
105
|
bug_tracker_uri: https://github.com/Roriz/active_genie/issues
|
90
106
|
rubygems_mfa_required: 'true'
|
91
|
-
post_install_message:
|
92
107
|
rdoc_options: []
|
93
108
|
require_paths:
|
94
109
|
- lib
|
@@ -103,8 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
118
|
- !ruby/object:Gem::Version
|
104
119
|
version: '0'
|
105
120
|
requirements: []
|
106
|
-
rubygems_version: 3.
|
107
|
-
signing_key:
|
121
|
+
rubygems_version: 3.6.9
|
108
122
|
specification_version: 4
|
109
123
|
summary: 'The Lodash for GenAI: Real Value + Consistent + Model-Agnostic'
|
110
124
|
test_files: []
|
File without changes
|