active_genie 0.25.1 → 0.26.0
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 +5 -5
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +7 -7
- data/lib/active_genie/battle/generalist.json +36 -0
- data/lib/active_genie/battle/generalist.md +16 -0
- data/lib/active_genie/battle/generalist.rb +16 -69
- data/lib/active_genie/clients/providers/anthropic_client.rb +61 -40
- data/lib/active_genie/clients/providers/base_client.rb +44 -57
- data/lib/active_genie/clients/providers/deepseek_client.rb +57 -52
- data/lib/active_genie/clients/providers/google_client.rb +58 -60
- data/lib/active_genie/clients/providers/openai_client.rb +52 -55
- data/lib/active_genie/clients/unified_client.rb +4 -4
- data/lib/active_genie/config/battle_config.rb +2 -0
- data/lib/active_genie/config/llm_config.rb +3 -1
- data/lib/active_genie/config/log_config.rb +38 -14
- data/lib/active_genie/config/providers/anthropic_config.rb +2 -2
- data/lib/active_genie/config/providers/deepseek_config.rb +2 -2
- data/lib/active_genie/config/providers/google_config.rb +2 -2
- data/lib/active_genie/config/providers/openai_config.rb +2 -2
- data/lib/active_genie/config/providers_config.rb +4 -4
- data/lib/active_genie/config/scoring_config.rb +2 -0
- data/lib/active_genie/configuration.rb +14 -8
- data/lib/active_genie/data_extractor/from_informal.json +11 -0
- data/lib/active_genie/data_extractor/from_informal.rb +5 -13
- data/lib/active_genie/data_extractor/generalist.json +9 -0
- data/lib/active_genie/data_extractor/generalist.rb +12 -11
- data/lib/active_genie/errors/invalid_log_output_error.rb +19 -0
- data/lib/active_genie/logger.rb +13 -5
- data/lib/active_genie/{concerns → ranking/concerns}/loggable.rb +2 -5
- data/lib/active_genie/ranking/elo_round.rb +30 -28
- data/lib/active_genie/ranking/free_for_all.rb +30 -22
- data/lib/active_genie/ranking/player.rb +53 -19
- data/lib/active_genie/ranking/players_collection.rb +17 -13
- data/lib/active_genie/ranking/ranking.rb +21 -20
- data/lib/active_genie/ranking/ranking_scoring.rb +2 -20
- data/lib/active_genie/scoring/generalist.json +9 -0
- data/lib/active_genie/scoring/generalist.md +46 -0
- data/lib/active_genie/scoring/generalist.rb +13 -65
- data/lib/active_genie/scoring/recommended_reviewers.rb +2 -2
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 848de45258263935accfd02ce16d14eb96c6b5a73f42979fe0a74e5b9b746fb4
|
4
|
+
data.tar.gz: cb1f9eb843559a7040dc20b1442e120325d734b7610a75b57bfde91cba63dc4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12f38e95348fae6beab3cc7a74cfdefc132ef9e8308303f2df04a40703d6f86a9fe6f859c958dc8a1d9fca52d41ceb1f380324c5b34d5d13e0bde4a702362830
|
7
|
+
data.tar.gz: c512e6ae0fb0b47403f9c2fc3be5c55a458320877fe2aae93780af1fab4e95b2fa1553ff60a62ec30653933f77a5c8892e96227ee637e8ab03a30cf0c9ad177d
|
data/README.md
CHANGED
@@ -118,13 +118,13 @@ AI-powered battle evaluation system that determines winners between two players
|
|
118
118
|
```ruby
|
119
119
|
require 'active_genie'
|
120
120
|
|
121
|
-
|
122
|
-
|
121
|
+
player_a = "Implementation uses dependency injection for better testability"
|
122
|
+
player_b = "Code has high test coverage but tightly coupled components"
|
123
123
|
criteria = "Evaluate code quality and maintainability"
|
124
124
|
|
125
125
|
result = ActiveGenie::Battle.call(
|
126
|
-
|
127
|
-
|
126
|
+
player_a,
|
127
|
+
player_b,
|
128
128
|
criteria,
|
129
129
|
config: { provider: :google, model: 'gemini-2.0-flash-lite' } # optional
|
130
130
|
)
|
@@ -238,7 +238,7 @@ ActiveGenie.configure do |config|
|
|
238
238
|
config.llm.client = InternalCompanyApi
|
239
239
|
end
|
240
240
|
# or
|
241
|
-
ActiveGenie::Battle.call('
|
241
|
+
ActiveGenie::Battle.call('player_a', 'player_b', 'criteria', { client: InternalCompanyApi })
|
242
242
|
```
|
243
243
|
|
244
244
|
## Observability
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.26.0
|
@@ -12,11 +12,11 @@ AI-powered battle evaluation system that determines winners between two players
|
|
12
12
|
Evaluate a battle between two players with simple text content:
|
13
13
|
|
14
14
|
```ruby
|
15
|
-
|
16
|
-
|
15
|
+
player_a = "Implementation uses dependency injection for better testability"
|
16
|
+
player_b = "Code has high test coverage but tightly coupled components"
|
17
17
|
criteria = "Evaluate code quality and maintainability"
|
18
18
|
|
19
|
-
result = ActiveGenie::Battle.call(
|
19
|
+
result = ActiveGenie::Battle.call(player_a, player_b, criteria)
|
20
20
|
# => {
|
21
21
|
# winner_player: "Implementation uses dependency injection for better testability",
|
22
22
|
# reasoning: "Player A's implementation demonstrates better maintainability through dependency injection,
|
@@ -27,13 +27,13 @@ result = ActiveGenie::Battle.call(player_1, player_2, criteria)
|
|
27
27
|
```
|
28
28
|
|
29
29
|
## Interface
|
30
|
-
### .call(
|
31
|
-
- `
|
32
|
-
- `
|
30
|
+
### .call(player_a, player_b, criteria, config: {})
|
31
|
+
- `player_a` [String, Hash] - The content or submission from the first player
|
32
|
+
- `player_b` [String, Hash] - The content or submission from the second player
|
33
33
|
- `criteria` [String] - The evaluation criteria or rules to assess against
|
34
34
|
- `config` [Hash] - Additional configuration config that modify the battle evaluation behavior
|
35
35
|
|
36
36
|
Returns a Hash containing:
|
37
|
-
- `winner_player` [String, Hash] - The winning player's content (either
|
37
|
+
- `winner_player` [String, Hash] - The winning player's content (either player_a or player_b)
|
38
38
|
- `reasoning` [String] - Detailed explanation of why the winner was chosen
|
39
39
|
- `what_could_be_changed_to_avoid_draw` [String] - A suggestion on how to avoid a draw
|
@@ -0,0 +1,36 @@
|
|
1
|
+
{
|
2
|
+
"name": "battle_evaluation",
|
3
|
+
"description": "Evaluate a battle between player_a and player_b using predefined criteria and identify the winner.",
|
4
|
+
"parameters": {
|
5
|
+
"type": "object",
|
6
|
+
"properties": {
|
7
|
+
"player_a_sell_himself": {
|
8
|
+
"type": "string",
|
9
|
+
"description": "player_a presents their strengths and how they meet the criteria. Max of 100 words."
|
10
|
+
},
|
11
|
+
"player_b_sell_himself": {
|
12
|
+
"type": "string",
|
13
|
+
"description": "player_b presents their strengths and how they meet the criteria. Max of 100 words."
|
14
|
+
},
|
15
|
+
"player_a_arguments": {
|
16
|
+
"type": "string",
|
17
|
+
"description": "player_a arguments for why they should be the winner compared to player_b. Max of 100 words."
|
18
|
+
},
|
19
|
+
"player_b_counter": {
|
20
|
+
"type": "string",
|
21
|
+
"description": "player_b counter arguments for why they should be the winner compared to player_a. Max of 100 words."
|
22
|
+
},
|
23
|
+
"impartial_judge_winner_reasoning": {
|
24
|
+
"type": "string",
|
25
|
+
"description": "The detailed reasoning about why the impartial judge chose the winner. Max of 100 words."
|
26
|
+
},
|
27
|
+
"impartial_judge_winner": {
|
28
|
+
"type": "string",
|
29
|
+
"description": "Who is the winner based on the impartial judge reasoning?",
|
30
|
+
"enum": ["player_a", "player_b"]
|
31
|
+
}
|
32
|
+
},
|
33
|
+
"required": ["player_a_sell_himself", "player_b_sell_himself", "player_a_arguments", "player_b_counter",
|
34
|
+
"impartial_judge_winner_reasoning", "impartial_judge_winner"]
|
35
|
+
}
|
36
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
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.
|
2
|
+
|
3
|
+
# Steps
|
4
|
+
1. player_a presents their strengths and how they meet the criteria. Max of 100 words.
|
5
|
+
2. player_b presents their strengths and how they meet the criteria. Max of 100 words.
|
6
|
+
3. player_a argues why they should be the winner compared to player_b. Max of 100 words.
|
7
|
+
4. player_b counter-argues why they should be the winner compared to player_a. Max of 100 words.
|
8
|
+
5. The impartial judge chooses the winner.
|
9
|
+
|
10
|
+
# Output Format
|
11
|
+
- The impartial judge chooses this player as the winner.
|
12
|
+
|
13
|
+
# Notes
|
14
|
+
- Avoid resulting in a draw. Use reasoning or make fair assumptions if needed.
|
15
|
+
- Critically assess each player's adherence to the criteria.
|
16
|
+
- Clearly communicate the reasoning behind your decision.
|
@@ -19,17 +19,17 @@ module ActiveGenie
|
|
19
19
|
new(...).call
|
20
20
|
end
|
21
21
|
|
22
|
-
# @param
|
23
|
-
# @param
|
22
|
+
# @param player_a [String] The content or submission from the first player
|
23
|
+
# @param player_b [String] The content or submission from the second player
|
24
24
|
# @param criteria [String] The evaluation criteria or rules to assess against
|
25
25
|
# @param config [Hash] Additional configuration options that modify the battle evaluation behavior
|
26
26
|
# @return [Hash] The evaluation result containing the winner and reasoning
|
27
|
-
# @return [String] :winner The winner, either
|
27
|
+
# @return [String] :winner The winner, either player_a or player_b
|
28
28
|
# @return [String] :reasoning Detailed explanation of why the winner was chosen
|
29
29
|
# @return [String] :what_could_be_changed_to_avoid_draw A suggestion on how to avoid a draw
|
30
|
-
def initialize(
|
31
|
-
@
|
32
|
-
@
|
30
|
+
def initialize(player_a, player_b, criteria, config: {})
|
31
|
+
@player_a = player_a
|
32
|
+
@player_b = player_b
|
33
33
|
@criteria = criteria
|
34
34
|
@config = ActiveGenie.configuration.merge(config)
|
35
35
|
end
|
@@ -38,8 +38,8 @@ module ActiveGenie
|
|
38
38
|
messages = [
|
39
39
|
{ role: 'system', content: PROMPT },
|
40
40
|
{ role: 'user', content: "criteria: #{@criteria}" },
|
41
|
-
{ role: 'user', content: "
|
42
|
-
{ role: 'user', content: "
|
41
|
+
{ role: 'user', content: "player_a: #{@player_a}" },
|
42
|
+
{ role: 'user', content: "player_b: #{@player_b}" }
|
43
43
|
]
|
44
44
|
|
45
45
|
response = ::ActiveGenie::Clients::UnifiedClient.function_calling(
|
@@ -50,8 +50,8 @@ module ActiveGenie
|
|
50
50
|
|
51
51
|
ActiveGenie::Logger.call({
|
52
52
|
code: :battle,
|
53
|
-
|
54
|
-
|
53
|
+
player_a: @player_a[0..30],
|
54
|
+
player_b: @player_b[0..30],
|
55
55
|
criteria: @criteria[0..30],
|
56
56
|
winner: response['impartial_judge_winner'],
|
57
57
|
reasoning: response['impartial_judge_winner_reasoning']
|
@@ -60,73 +60,20 @@ module ActiveGenie
|
|
60
60
|
response_formatted(response)
|
61
61
|
end
|
62
62
|
|
63
|
+
PROMPT = File.read(File.join(__dir__, 'generalist.md'))
|
64
|
+
FUNCTION = JSON.parse(File.read(File.join(__dir__, 'generalist.json')), symbolize_names: true)
|
65
|
+
|
63
66
|
private
|
64
67
|
|
65
68
|
def response_formatted(response)
|
66
69
|
winner = response['impartial_judge_winner']
|
67
|
-
loser = case
|
68
|
-
when '
|
69
|
-
when '
|
70
|
+
loser = case winner
|
71
|
+
when 'player_a' then 'player_b'
|
72
|
+
when 'player_b' then 'player_a'
|
70
73
|
end
|
71
74
|
|
72
75
|
{ 'winner' => winner, 'loser' => loser, 'reasoning' => response['impartial_judge_winner_reasoning'] }
|
73
76
|
end
|
74
|
-
|
75
|
-
PROMPT = <<~PROMPT
|
76
|
-
Based on two players, player_1 and player_2, 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.
|
77
|
-
|
78
|
-
# Steps
|
79
|
-
1. player_1 presents their strengths and how they meet the criteria. Max of 100 words.
|
80
|
-
2. player_2 presents their strengths and how they meet the criteria. Max of 100 words.
|
81
|
-
3. player_1 argues why they should be the winner compared to player_2. Max of 100 words.
|
82
|
-
4. player_2 counter-argues why they should be the winner compared to player_1. Max of 100 words.
|
83
|
-
5. The impartial judge chooses the winner.
|
84
|
-
|
85
|
-
# Output Format
|
86
|
-
- The impartial judge chooses this player as the winner.
|
87
|
-
|
88
|
-
# Notes
|
89
|
-
- Avoid resulting in a draw. Use reasoning or make fair assumptions if needed.
|
90
|
-
- Critically assess each player's adherence to the criteria.
|
91
|
-
- Clearly communicate the reasoning behind your decision.
|
92
|
-
PROMPT
|
93
|
-
|
94
|
-
FUNCTION = {
|
95
|
-
name: 'battle_evaluation',
|
96
|
-
description: 'Evaluate a battle between player_1 and player_2 using predefined criteria and identify the winner.',
|
97
|
-
parameters: {
|
98
|
-
type: 'object',
|
99
|
-
properties: {
|
100
|
-
player_1_sell_himself: {
|
101
|
-
type: 'string',
|
102
|
-
description: 'player_1 presents their strengths and how they meet the criteria. Max of 100 words.'
|
103
|
-
},
|
104
|
-
player_2_sell_himself: {
|
105
|
-
type: 'string',
|
106
|
-
description: 'player_2 presents their strengths and how they meet the criteria. Max of 100 words.'
|
107
|
-
},
|
108
|
-
player_1_arguments: {
|
109
|
-
type: 'string',
|
110
|
-
description: 'player_1 arguments for why they should be the winner compared to player_2. Max of 100 words.'
|
111
|
-
},
|
112
|
-
player_2_counter: {
|
113
|
-
type: 'string',
|
114
|
-
description: 'player_2 counter arguments for why they should be the winner compared to player_1. Max of 100 words.'
|
115
|
-
},
|
116
|
-
impartial_judge_winner_reasoning: {
|
117
|
-
type: 'string',
|
118
|
-
description: 'The detailed reasoning about why the impartial judge chose the winner. Max of 100 words.'
|
119
|
-
},
|
120
|
-
impartial_judge_winner: {
|
121
|
-
type: 'string',
|
122
|
-
description: 'Who is the winner based on the impartial judge reasoning?',
|
123
|
-
enum: %w[player_1 player_2]
|
124
|
-
}
|
125
|
-
},
|
126
|
-
required: %w[player_1_sell_himself player_2_sell_himself player_1_arguments player_2_counter
|
127
|
-
impartial_judge_winner_reasoning impartial_judge_winner]
|
128
|
-
}
|
129
|
-
}.freeze
|
130
77
|
end
|
131
78
|
end
|
132
79
|
end
|
@@ -3,17 +3,12 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'net/http'
|
5
5
|
require 'uri'
|
6
|
-
require_relative '
|
6
|
+
require_relative 'base_client'
|
7
7
|
|
8
8
|
module ActiveGenie
|
9
9
|
module Clients
|
10
10
|
# Client for interacting with the Anthropic (Claude) API with json response
|
11
11
|
class AnthropicClient < BaseClient
|
12
|
-
class AnthropicError < ClientError; end
|
13
|
-
class RateLimitError < AnthropicError; end
|
14
|
-
|
15
|
-
ANTHROPIC_ENDPOINT = '/v1/messages'
|
16
|
-
|
17
12
|
# Requests structured JSON output from the Anthropic Claude model based on a schema.
|
18
13
|
#
|
19
14
|
# @param messages [Array<Hash>] A list of messages representing the conversation history.
|
@@ -22,56 +17,82 @@ module ActiveGenie
|
|
22
17
|
# @param function [Hash] A JSON schema definition describing the desired output format.
|
23
18
|
# @return [Hash, nil] The parsed JSON object matching the schema, or nil if parsing fails or content is empty.
|
24
19
|
def function_calling(messages, function)
|
25
|
-
|
26
|
-
|
27
|
-
system_message = messages.find { |m| m[:role] == 'system' }&.dig(:content) || ''
|
28
|
-
user_messages = messages.select { |m| %w[user assistant].include?(m[:role]) }
|
29
|
-
.map { |m| { role: m[:role], content: m[:content] } }
|
30
|
-
|
31
|
-
anthropic_function = function.dup
|
32
|
-
anthropic_function[:input_schema] = function[:parameters]
|
33
|
-
anthropic_function.delete(:parameters)
|
20
|
+
system_message, user_messages = split_messages(messages)
|
21
|
+
tool = function_to_tool(function)
|
34
22
|
|
35
23
|
payload = {
|
36
24
|
model:,
|
37
25
|
system: system_message,
|
38
26
|
messages: user_messages,
|
39
|
-
tools: [
|
40
|
-
tool_choice: { name:
|
27
|
+
tools: [tool],
|
28
|
+
tool_choice: { name: tool[:name], type: 'tool' },
|
41
29
|
max_tokens: @config.llm.max_tokens,
|
42
30
|
temperature: @config.llm.temperature || 0
|
43
31
|
}
|
44
32
|
|
45
|
-
|
46
|
-
|
47
|
-
'anthropic-version': @config.providers.anthropic.anthropic_version
|
48
|
-
}.compact
|
49
|
-
|
50
|
-
retry_with_backoff do
|
51
|
-
start_time = Time.now
|
52
|
-
url = "#{@config.providers.anthropic.api_url}#{ANTHROPIC_ENDPOINT}"
|
33
|
+
request(payload).dig('content', 0, 'input')
|
34
|
+
end
|
53
35
|
|
54
|
-
|
36
|
+
ANTHROPIC_ENDPOINT = '/v1/messages'
|
55
37
|
|
56
|
-
|
38
|
+
private
|
57
39
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
total_tokens: response.dig('usage',
|
63
|
-
'input_tokens') + response.dig('usage',
|
64
|
-
'output_tokens'),
|
65
|
-
model: payload[:model],
|
66
|
-
duration: Time.now - start_time,
|
67
|
-
usage: response['usage']
|
68
|
-
})
|
40
|
+
def split_messages(messages)
|
41
|
+
system_message = messages.find { |m| m[:role] == 'system' }&.dig(:content) || ''
|
42
|
+
user_messages = messages.select { |m| %w[user assistant].include?(m[:role]) }
|
43
|
+
.map { |m| m.slice(:role, :content) }
|
69
44
|
|
70
|
-
|
45
|
+
[system_message, user_messages]
|
46
|
+
end
|
71
47
|
|
72
|
-
|
48
|
+
def function_to_tool(function)
|
49
|
+
function.dup.tap do |f|
|
50
|
+
f[:input_schema] = function[:parameters]
|
51
|
+
f.delete(:parameters)
|
73
52
|
end
|
74
53
|
end
|
54
|
+
|
55
|
+
def request(payload)
|
56
|
+
response = post(url, payload, headers:)
|
57
|
+
|
58
|
+
ActiveGenie::Logger.call(
|
59
|
+
{
|
60
|
+
code: :llm_usage,
|
61
|
+
input_tokens: response.dig('usage', 'input_tokens'),
|
62
|
+
output_tokens: response.dig('usage', 'output_tokens'),
|
63
|
+
total_tokens: response.dig('usage',
|
64
|
+
'input_tokens') + response.dig('usage',
|
65
|
+
'output_tokens'),
|
66
|
+
model: payload[:model],
|
67
|
+
usage: response['usage']
|
68
|
+
}
|
69
|
+
)
|
70
|
+
ActiveGenie::Logger.call(
|
71
|
+
{
|
72
|
+
code: :function_calling,
|
73
|
+
fine_tune: true,
|
74
|
+
payload:,
|
75
|
+
parsed_response: response.dig('content', 0, 'input')
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
response
|
80
|
+
end
|
81
|
+
|
82
|
+
def url
|
83
|
+
"#{@config.providers.anthropic.api_url}#{ANTHROPIC_ENDPOINT}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def model
|
87
|
+
@config.llm.model || @config.providers.anthropic.tier_to_model(@config.llm.model_tier)
|
88
|
+
end
|
89
|
+
|
90
|
+
def headers
|
91
|
+
{
|
92
|
+
'x-api-key': @config.providers.anthropic.api_key,
|
93
|
+
'anthropic-version': @config.providers.anthropic.anthropic_version
|
94
|
+
}.compact
|
95
|
+
end
|
75
96
|
end
|
76
97
|
end
|
77
98
|
end
|
@@ -7,7 +7,7 @@ module ActiveGenie
|
|
7
7
|
|
8
8
|
DEFAULT_HEADERS = {
|
9
9
|
'Content-Type': 'application/json',
|
10
|
-
|
10
|
+
Accept: 'application/json',
|
11
11
|
'User-Agent': "ActiveGenie/#{ActiveGenie::VERSION}"
|
12
12
|
}.freeze
|
13
13
|
|
@@ -29,7 +29,8 @@ module ActiveGenie
|
|
29
29
|
def get(endpoint, params: {}, headers: {})
|
30
30
|
uri = build_uri(endpoint, params)
|
31
31
|
request = Net::HTTP::Get.new(uri)
|
32
|
-
|
32
|
+
apply_headers(request, headers)
|
33
|
+
execute_request(uri, request)
|
33
34
|
end
|
34
35
|
|
35
36
|
# Make a POST request to the specified endpoint
|
@@ -42,7 +43,8 @@ module ActiveGenie
|
|
42
43
|
uri = build_uri(endpoint, params)
|
43
44
|
request = Net::HTTP::Post.new(uri)
|
44
45
|
request.body = payload.to_json
|
45
|
-
|
46
|
+
apply_headers(request, headers)
|
47
|
+
execute_request(uri, request)
|
46
48
|
end
|
47
49
|
|
48
50
|
# Make a PUT request to the specified endpoint
|
@@ -55,7 +57,8 @@ module ActiveGenie
|
|
55
57
|
uri = build_uri(endpoint)
|
56
58
|
request = Net::HTTP::Put.new(uri)
|
57
59
|
request.body = payload.to_json
|
58
|
-
|
60
|
+
apply_headers(request, headers)
|
61
|
+
execute_request(uri, request)
|
59
62
|
end
|
60
63
|
|
61
64
|
# Make a DELETE request to the specified endpoint
|
@@ -67,58 +70,40 @@ module ActiveGenie
|
|
67
70
|
def delete(endpoint, headers: {}, params: {})
|
68
71
|
uri = build_uri(endpoint, params)
|
69
72
|
request = Net::HTTP::Delete.new(uri)
|
70
|
-
|
73
|
+
apply_headers(request, headers)
|
74
|
+
execute_request(uri, request)
|
71
75
|
end
|
72
76
|
|
73
77
|
protected
|
74
78
|
|
75
|
-
|
76
|
-
#
|
77
|
-
# @param uri [URI] The URI for the request
|
78
|
-
# @param request [Net::HTTP::Request] The request object
|
79
|
-
# @param headers [Hash] Additional headers to include
|
80
|
-
# @return [Hash, nil] The parsed JSON response or nil if empty
|
81
|
-
def execute_request(uri, request, headers)
|
79
|
+
def execute_request(uri, request)
|
82
80
|
start_time = Time.now
|
83
81
|
|
84
|
-
|
85
|
-
apply_headers(request, headers)
|
82
|
+
response = http_request(request, uri)
|
86
83
|
|
87
|
-
|
84
|
+
raise ClientError, "Unexpected response: #{response.code} - #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
88
85
|
|
89
|
-
|
86
|
+
parsed_response = parse_response(response)
|
90
87
|
|
91
|
-
|
92
|
-
when Net::HTTPSuccess
|
93
|
-
parsed_response = parse_response(response)
|
88
|
+
log_request_details(uri:, request:, response:, start_time:, parsed_response:)
|
94
89
|
|
95
|
-
|
96
|
-
uri: uri,
|
97
|
-
method: request.method,
|
98
|
-
status: response.code,
|
99
|
-
duration: Time.now - start_time,
|
100
|
-
response: parsed_response
|
101
|
-
)
|
102
|
-
|
103
|
-
parsed_response
|
104
|
-
when Net::HTTPTooManyRequests, Net::HTTPClientError, Net::HTTPServerError
|
105
|
-
raise ClientError, "HTTP Error: #{response.code} - #{response.body}"
|
106
|
-
else
|
107
|
-
raise ClientError, "Unexpected response: #{response.code} - #{response.body}"
|
108
|
-
end
|
90
|
+
parsed_response
|
109
91
|
end
|
110
92
|
|
111
93
|
# Create and configure an HTTP client
|
112
94
|
#
|
113
95
|
# @param uri [URI] The URI for the request
|
114
96
|
# @return [Net::HTTP] Configured HTTP client
|
115
|
-
def
|
97
|
+
def http_request(request, uri)
|
116
98
|
http = Net::HTTP.new(uri.host, uri.port)
|
117
99
|
http.use_ssl = (uri.scheme == 'https')
|
118
100
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
119
101
|
http.read_timeout = @config.llm.read_timeout || DEFAULT_TIMEOUT
|
120
102
|
http.open_timeout = @config.llm.open_timeout || DEFAULT_OPEN_TIMEOUT
|
121
|
-
|
103
|
+
|
104
|
+
retry_with_backoff do
|
105
|
+
http.request(request)
|
106
|
+
end
|
122
107
|
end
|
123
108
|
|
124
109
|
# Apply headers to the request
|
@@ -165,51 +150,53 @@ module ActiveGenie
|
|
165
150
|
# Log request details if logging is enabled
|
166
151
|
#
|
167
152
|
# @param details [Hash] Request and response details
|
168
|
-
def log_request_details(
|
153
|
+
def log_request_details(uri:, request:, response:, start_time:, parsed_response:)
|
169
154
|
ActiveGenie::Logger.call(
|
170
155
|
{
|
171
156
|
code: :http_request,
|
172
|
-
uri:
|
173
|
-
method:
|
174
|
-
status:
|
175
|
-
duration:
|
176
|
-
response_size:
|
157
|
+
uri: uri.to_s,
|
158
|
+
method: request.method,
|
159
|
+
status: response.code,
|
160
|
+
duration: Time.now - start_time,
|
161
|
+
response_size: parsed_response.to_s.bytesize
|
177
162
|
}
|
178
163
|
)
|
179
164
|
end
|
180
165
|
|
181
|
-
#
|
182
|
-
#
|
183
|
-
# @yield The block to retry
|
184
|
-
# @return [Object] The result of the block
|
166
|
+
# FIXME: split the retry_with_backoff method into a separate class
|
167
|
+
# rubocop:disable Metrics/MethodLength
|
185
168
|
def retry_with_backoff
|
186
|
-
max_retries = @config.llm.max_retries || DEFAULT_MAX_RETRIES
|
187
|
-
retry_delay = @config.llm.retry_delay || DEFAULT_RETRY_DELAY
|
188
|
-
|
189
169
|
retries = 0
|
190
170
|
|
191
171
|
begin
|
192
172
|
yield
|
193
|
-
rescue Net::
|
194
|
-
raise
|
173
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, ClientError => e
|
174
|
+
raise if retries > max_retries
|
195
175
|
|
196
176
|
sleep_time = retry_delay * (2**retries)
|
197
177
|
retries += 1
|
198
178
|
|
199
179
|
ActiveGenie::Logger.call(
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
error: e.message
|
206
|
-
}
|
180
|
+
code: :retry_attempt,
|
181
|
+
attempt: retries,
|
182
|
+
max_retries: max_retries,
|
183
|
+
delay: sleep_time,
|
184
|
+
error: e.message
|
207
185
|
)
|
208
186
|
|
209
187
|
sleep(sleep_time)
|
210
188
|
retry
|
211
189
|
end
|
212
190
|
end
|
191
|
+
# rubocop:enable Metrics/MethodLength
|
192
|
+
|
193
|
+
def max_retries
|
194
|
+
@config.llm.max_retries || DEFAULT_MAX_RETRIES
|
195
|
+
end
|
196
|
+
|
197
|
+
def retry_delay
|
198
|
+
@config.llm.retry_delay || DEFAULT_RETRY_DELAY
|
199
|
+
end
|
213
200
|
end
|
214
201
|
end
|
215
202
|
end
|