active_genie 0.0.12 → 0.0.19

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -22
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +7 -7
  5. data/lib/active_genie/battle/basic.rb +48 -32
  6. data/lib/active_genie/battle.rb +4 -0
  7. data/lib/active_genie/clients/anthropic_client.rb +84 -0
  8. data/lib/active_genie/clients/base_client.rb +241 -0
  9. data/lib/active_genie/clients/google_client.rb +135 -0
  10. data/lib/active_genie/clients/helpers/retry.rb +29 -0
  11. data/lib/active_genie/clients/openai_client.rb +70 -91
  12. data/lib/active_genie/clients/unified_client.rb +4 -4
  13. data/lib/active_genie/concerns/loggable.rb +44 -0
  14. data/lib/active_genie/configuration/log_config.rb +1 -1
  15. data/lib/active_genie/configuration/providers/anthropic_config.rb +54 -0
  16. data/lib/active_genie/configuration/providers/base_config.rb +85 -0
  17. data/lib/active_genie/configuration/providers/deepseek_config.rb +54 -0
  18. data/lib/active_genie/configuration/providers/google_config.rb +56 -0
  19. data/lib/active_genie/configuration/providers/openai_config.rb +54 -0
  20. data/lib/active_genie/configuration/providers_config.rb +7 -4
  21. data/lib/active_genie/configuration/runtime_config.rb +35 -0
  22. data/lib/active_genie/configuration.rb +18 -4
  23. data/lib/active_genie/data_extractor/basic.rb +16 -3
  24. data/lib/active_genie/data_extractor.rb +4 -0
  25. data/lib/active_genie/logger.rb +40 -21
  26. data/lib/active_genie/ranking/elo_round.rb +71 -50
  27. data/lib/active_genie/ranking/free_for_all.rb +31 -14
  28. data/lib/active_genie/ranking/player.rb +11 -16
  29. data/lib/active_genie/ranking/players_collection.rb +4 -4
  30. data/lib/active_genie/ranking/ranking.rb +74 -19
  31. data/lib/active_genie/ranking/ranking_scoring.rb +3 -3
  32. data/lib/active_genie/scoring/basic.rb +44 -25
  33. data/lib/active_genie/scoring/recommended_reviewers.rb +1 -1
  34. data/lib/active_genie/scoring.rb +3 -0
  35. data/lib/tasks/benchmark.rake +27 -0
  36. metadata +92 -70
  37. data/lib/active_genie/configuration/openai_config.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbc4033f3c0880973cf732d704921bb61c3cb861c438b732aa6ebe0cc88b2de4
4
- data.tar.gz: 11794293170ec43a1d3b3d09e5ec488351a22428a5370ae0e42266a6f8098646
3
+ metadata.gz: 12c5c526a10f93e649ca39c4789a21065c0f2d329bd57248260b1fd997507296
4
+ data.tar.gz: 6fc9074a5282f1b9759c41dd98aaa31a179bc495964839e8bfc42e891b15e7d2
5
5
  SHA512:
6
- metadata.gz: f6b13b3f36a8e516e5126d4cea0de52d046e498834cd10b90b1905460e9bba5ff6e7c4cebd58e6ea2c073176566601c8b806015baa01f11f28792942eec6ca1d
7
- data.tar.gz: fc79aed12aaba0ca335d3a7ef8405f4a7c2809de9d5e41ca14fa7bd843d9fb69db8e124b6a2e96321432575a58b271c724a52fd3c1a1165c5ecfe346bedd4b5f
6
+ metadata.gz: 76672044e7a1a88779100b9b0f32d75547ac1105031f5ca40d6bbce71a34edecadb63e0dda99c6ca23ee03647bf5dd09a3b3bc98a38faad3a07fdbc245f87dc4
7
+ data.tar.gz: 2ac5ed0ae70edbf7a736e7b7bc3408a16c76628c7efecb277291641bb8b04e8ea633a36c16dc0e86851bbf3c40d82c3f4456af0e7e3ea1beea057bb60660ad6a
data/README.md CHANGED
@@ -2,16 +2,10 @@
2
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
- [![Ruby](https://github.com/roriz/active_genie/actions/workflows/ruby.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/ruby.yml)
5
+ [![Ruby](https://github.com/roriz/active_genie/actions/workflows/benchmark.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/benchmark.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 Lodash or ActiveStorage, ActiveGenie simplifies GenAI integration in your Ruby applications.
8
-
9
- ## Features
10
-
11
- - 🎯 **Data Extraction**: Extract structured data from unstructured text with type validation
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
7
+ ActiveGenie is a Ruby gem that provides valuable solutions powered by Generative AI (GenAI) models. Just like Lodash or ActiveStorage, ActiveGenie brings a set of Modules reach real value fast and reliable.
8
+ ActiveGenie is backed by a custom benchmarking system that ensures consistent quality and performance across different models and providers in every release.
15
9
 
16
10
  ## Installation
17
11
 
@@ -41,6 +35,7 @@ end
41
35
  ## Quick Start
42
36
 
43
37
  ### Data Extractor
38
+
44
39
  Extract structured data from text using AI-powered analysis, handling informal language and complex expressions.
45
40
 
46
41
  ```ruby
@@ -55,13 +50,17 @@ schema = {
55
50
  minimum: 0
56
51
  },
57
52
  size: {
58
- type: 'integer',
53
+ type: 'number',
59
54
  minimum: 35,
60
55
  maximum: 46
61
56
  }
62
57
  }
63
58
 
64
- result = ActiveGenie::DataExtractor.call(text, schema)
59
+ result = ActiveGenie::DataExtractor.call(
60
+ text,
61
+ schema,
62
+ config: { provider: :openai, model: 'gpt-4o-mini' } # optional
63
+ )
65
64
  # => {
66
65
  # brand: "Nike",
67
66
  # brand_explanation: "Brand name found at start of text",
@@ -72,6 +71,8 @@ result = ActiveGenie::DataExtractor.call(text, schema)
72
71
  # }
73
72
  ```
74
73
 
74
+ *Recommended model*: `gpt-4o-mini`
75
+
75
76
  Features:
76
77
  - Structured data extraction with type validation
77
78
  - Schema-based extraction with custom constraints
@@ -80,14 +81,18 @@ Features:
80
81
 
81
82
  See the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal text processing, advanced schemas, and detailed interface documentation.
82
83
 
83
- ### Data Scoring
84
+ ### Scoring
84
85
  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.
85
86
 
86
87
  ```ruby
87
88
  text = "The code implements a binary search algorithm with O(log n) complexity"
88
89
  criteria = "Evaluate technical accuracy and clarity"
89
90
 
90
- result = ActiveGenie::Scoring.basic(text, criteria)
91
+ result = ActiveGenie::Scoring.basic(
92
+ text,
93
+ criteria,
94
+ config: { provider: :anthropic, model: 'claude-3-5-haiku-20241022' } # optional
95
+ )
91
96
  # => {
92
97
  # algorithm_expert_score: 95,
93
98
  # algorithm_expert_reasoning: "Accurately describes binary search and its complexity",
@@ -97,6 +102,8 @@ result = ActiveGenie::Scoring.basic(text, criteria)
97
102
  # }
98
103
  ```
99
104
 
105
+ *Recommended model*: `claude-3-5-haiku-20241022`
106
+
100
107
  Features:
101
108
  - Multi-reviewer evaluation with automatic expert selection
102
109
  - Detailed feedback with scoring reasoning
@@ -105,26 +112,33 @@ Features:
105
112
 
106
113
  See the [Scoring README](lib/active_genie/scoring/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
107
114
 
108
- ### Data Battle
115
+ ### Battle
109
116
  AI-powered battle evaluation system that determines winners between two players based on specified criteria.
110
117
 
111
118
  ```ruby
112
119
  require 'active_genie'
113
120
 
114
- player_a = "Implementation uses dependency injection for better testability"
115
- player_b = "Code has high test coverage but tightly coupled components"
121
+ player_1 = "Implementation uses dependency injection for better testability"
122
+ player_2 = "Code has high test coverage but tightly coupled components"
116
123
  criteria = "Evaluate code quality and maintainability"
117
124
 
118
- result = ActiveGenie::Battle.call(player_a, player_b, criteria)
125
+ result = ActiveGenie::Battle.call(
126
+ player_1,
127
+ player_2,
128
+ criteria,
129
+ config: { provider: :google, model: 'gemini-2.0-flash-lite' } # optional
130
+ )
119
131
  # => {
120
132
  # winner_player: "Implementation uses dependency injection for better testability",
121
- # reasoning: "Player A's implementation demonstrates better maintainability through dependency injection,
122
- # which allows for easier testing and component replacement. While Player B has good test coverage,
133
+ # reasoning: "Player 1 implementation demonstrates better maintainability through dependency injection,
134
+ # which allows for easier testing and component replacement. While Player 2 has good test coverage,
123
135
  # the tight coupling makes the code harder to maintain and modify.",
124
136
  # what_could_be_changed_to_avoid_draw: "Focus on specific architectural patterns and design principles"
125
137
  # }
126
138
  ```
127
139
 
140
+ *Recommended model*: `gemini-2.0-flash-lite`
141
+
128
142
  Features:
129
143
  - Multi-reviewer evaluation with automatic expert selection
130
144
  - Detailed feedback with scoring reasoning
@@ -133,23 +147,28 @@ Features:
133
147
 
134
148
  See the [Battle README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers, and detailed interface documentation.
135
149
 
136
- ### Data Ranking
150
+ ### Ranking
137
151
  The Ranking module provides competitive ranking through multi-stage evaluation:
138
152
 
139
-
140
153
  ```ruby
141
154
  require 'active_genie'
142
155
 
143
156
  players = ['REST API', 'GraphQL API', 'SOAP API', 'gRPC API', 'Websocket API']
144
157
  criteria = "Best one to be used into a high changing environment"
145
158
 
146
- result = ActiveGenie::Ranking.call(players, criteria)
159
+ result = ActiveGenie::Ranking.call(
160
+ players,
161
+ criteria,
162
+ config: { provider: :google, model: 'gemini-2.0-flash-lite' } # optional
163
+ )
147
164
  # => {
148
165
  # winner_player: "gRPC API",
149
166
  # reasoning: "gRPC API is the best one to be used into a high changing environment",
150
167
  # }
151
168
  ```
152
169
 
170
+ *Recommended model*: `gemini-2.0-flash-lite`
171
+
153
172
  - **Multi-phase ranking system** combining expert scoring and ELO algorithms
154
173
  - **Automatic elimination** of inconsistent performers using statistical analysis
155
174
  - **Dynamic ranking adjustments** based on simulated pairwise battles, from bottom to top
@@ -157,10 +176,34 @@ result = ActiveGenie::Ranking.call(players, criteria)
157
176
  See the [Ranking README](lib/active_genie/ranking/README.md) for implementation details, configuration, and advanced ranking strategies.
158
177
 
159
178
  ### Text Summarizer (Future)
179
+ ### Categorizer (Future)
160
180
  ### Language detector (Future)
161
181
  ### Translator (Future)
162
182
  ### Sentiment analyzer (Future)
163
183
 
184
+ ## Benchmarking 🧪
185
+
186
+ ActiveGenie includes a comprehensive benchmarking system to ensure consistent, high-quality outputs across different LLM models and providers.
187
+
188
+ ```ruby
189
+ # Run all benchmarks
190
+ bundle exec rake active_genie:benchmark
191
+
192
+ # Run benchmarks for a specific module
193
+ bundle exec rake active_genie:benchmark[data_extractor]
194
+ ```
195
+
196
+ ### Latest Results
197
+
198
+ | Model | Overall Precision |
199
+ |-------|-------------------|
200
+ | claude-3-5-haiku-20241022 | 92.25% |
201
+ | gemini-2.0-flash-lite | 84.25% |
202
+ | gpt-4o-mini | 62.75% |
203
+ | deepseek-chat | 57.25% |
204
+
205
+ See the [Benchmark README](benchmark/README.md) for detailed results, methodology, and how to contribute to our test suite.
206
+
164
207
  ## Configuration
165
208
 
166
209
  | Config | Description | Default |
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.12
1
+ 0.0.19
@@ -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
- player_a = "Implementation uses dependency injection for better testability"
16
- player_b = "Code has high test coverage but tightly coupled components"
15
+ player_1 = "Implementation uses dependency injection for better testability"
16
+ player_2 = "Code has high test coverage but tightly coupled components"
17
17
  criteria = "Evaluate code quality and maintainability"
18
18
 
19
- result = ActiveGenie::Battle::Basic.call(player_a, player_b, criteria)
19
+ result = ActiveGenie::Battle::Basic.call(player_1, player_2, 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::Basic.call(player_a, player_b, criteria)
27
27
  ```
28
28
 
29
29
  ## Interface
30
- ### Basic.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
30
+ ### Basic.call(player_1, player_2, criteria, config: {})
31
+ - `player_1` [String, Hash] - The content or submission from the first player
32
+ - `player_2` [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 player_a or player_b)
37
+ - `winner_player` [String, Hash] - The winning player's content (either player_1 or player_2)
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
@@ -18,17 +18,17 @@ module ActiveGenie::Battle
18
18
  new(...).call
19
19
  end
20
20
 
21
- # @param player_a [String] The content or submission from the first player
22
- # @param player_b [String] The content or submission from the second player
21
+ # @param player_1 [String] The content or submission from the first player
22
+ # @param player_2 [String] The content or submission from the second player
23
23
  # @param criteria [String] The evaluation criteria or rules to assess against
24
- # @param config [Hash] Additional configuration config that modify the battle evaluation behavior
24
+ # @param config [Hash] Additional configuration options that modify the battle evaluation behavior
25
25
  # @return [Hash] The evaluation result containing the winner and reasoning
26
- # @return [String] :winner The @param player_a or player_b
26
+ # @return [String] :winner The winner, either player_1 or player_2
27
27
  # @return [String] :reasoning Detailed explanation of why the winner was chosen
28
28
  # @return [String] :what_could_be_changed_to_avoid_draw A suggestion on how to avoid a draw
29
- def initialize(player_a, player_b, criteria, config: {})
30
- @player_a = player_a
31
- @player_b = player_b
29
+ def initialize(player_1, player_2, criteria, config: {})
30
+ @player_1 = player_1
31
+ @player_2 = player_2
32
32
  @criteria = criteria
33
33
  @config = ActiveGenie::Configuration.to_h(config)
34
34
  end
@@ -37,8 +37,8 @@ module ActiveGenie::Battle
37
37
  messages = [
38
38
  { role: 'system', content: PROMPT },
39
39
  { role: 'user', content: "criteria: #{@criteria}" },
40
- { role: 'user', content: "player_a: #{@player_a}" },
41
- { role: 'user', content: "player_b: #{@player_b}" },
40
+ { role: 'user', content: "player_1: #{@player_1}" },
41
+ { role: 'user', content: "player_2: #{@player_2}" },
42
42
  ]
43
43
 
44
44
  response = ::ActiveGenie::Clients::UnifiedClient.function_calling(
@@ -48,6 +48,15 @@ module ActiveGenie::Battle
48
48
  config: @config
49
49
  )
50
50
 
51
+ ActiveGenie::Logger.debug({
52
+ code: :battle,
53
+ player_1: @player_1[0..30],
54
+ player_2: @player_2[0..30],
55
+ criteria: @criteria[0..30],
56
+ winner: response['impartial_judge_winner'],
57
+ reasoning: response['impartial_judge_winner_reasoning']
58
+ })
59
+
51
60
  response_formatted(response)
52
61
  end
53
62
 
@@ -56,23 +65,22 @@ module ActiveGenie::Battle
56
65
  def response_formatted(response)
57
66
  winner = response['impartial_judge_winner']
58
67
  loser = case response['impartial_judge_winner']
59
- when 'player_a' then 'player_b'
60
- when 'player_b' then 'player_a'
61
- else 'draw'
68
+ when 'player_1' then 'player_2'
69
+ when 'player_2' then 'player_1'
62
70
  end
63
71
 
64
- { winner:, loser:, reasoning: response['impartial_judge_winner_reasoning'] }
72
+ { 'winner' => winner, 'loser' => loser, 'reasoning' => response['impartial_judge_winner_reasoning'] }
65
73
  end
66
74
 
67
75
  PROMPT = <<~PROMPT
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.
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.
69
77
 
70
78
  # Steps
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.
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.
76
84
 
77
85
  # Output Format
78
86
  - The impartial judge chooses this player as the winner.
@@ -85,25 +93,25 @@ module ActiveGenie::Battle
85
93
 
86
94
  FUNCTION = {
87
95
  name: 'battle_evaluation',
88
- description: 'Evaluate a battle between player_a and player_b using predefined criteria and identify the winner.',
89
- schema: {
96
+ description: 'Evaluate a battle between player_1 and player_2 using predefined criteria and identify the winner.',
97
+ parameters: {
90
98
  type: "object",
91
99
  properties: {
92
- player_a_sell_himself: {
100
+ player_1_sell_himself: {
93
101
  type: 'string',
94
- description: 'player_a sell himself, highlighting his strengths and how he meets the criteria. Max of 100 words.',
102
+ description: 'player_1 presents their strengths and how they meet the criteria. Max of 100 words.',
95
103
  },
96
- player_b_sell_himself: {
104
+ player_2_sell_himself: {
97
105
  type: 'string',
98
- description: 'player_b sell himself, highlighting his strengths and how he meets the criteria. Max of 100 words.',
106
+ description: 'player_2 presents their strengths and how they meet the criteria. Max of 100 words.',
99
107
  },
100
- player_a_arguments: {
108
+ player_1_arguments: {
101
109
  type: 'string',
102
- description: 'player_a arguments why he is the winner compared to player_b. Max of 100 words.',
110
+ description: 'player_1 arguments for why they should be the winner compared to player_2. Max of 100 words.',
103
111
  },
104
- player_b_counter: {
112
+ player_2_counter: {
105
113
  type: 'string',
106
- description: 'player_b counter arguments why he is the winner compared to player_a. Max of 100 words.',
114
+ description: 'player_2 counter arguments for why they should be the winner compared to player_1. Max of 100 words.',
107
115
  },
108
116
  impartial_judge_winner_reasoning: {
109
117
  type: 'string',
@@ -111,10 +119,18 @@ module ActiveGenie::Battle
111
119
  },
112
120
  impartial_judge_winner: {
113
121
  type: 'string',
114
- description: 'The impartial judge chose this player as the winner.',
115
- enum: ['player_a', 'player_b', 'draw']
122
+ description: 'Who is the winner based on the impartial judge reasoning?',
123
+ enum: ['player_1', 'player_2']
116
124
  },
117
- }
125
+ },
126
+ required: [
127
+ 'player_1_sell_himself',
128
+ 'player_2_sell_himself',
129
+ 'player_1_arguments',
130
+ 'player_2_counter',
131
+ 'impartial_judge_winner_reasoning',
132
+ 'impartial_judge_winner'
133
+ ]
118
134
  }
119
135
  }
120
136
  end
@@ -9,5 +9,9 @@ module ActiveGenie
9
9
  def basic(...)
10
10
  Basic.call(...)
11
11
  end
12
+
13
+ def call(...)
14
+ Basic.call(...)
15
+ end
12
16
  end
13
17
  end
@@ -0,0 +1,84 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require_relative './helpers/retry'
5
+ require_relative './base_client'
6
+
7
+ module ActiveGenie::Clients
8
+ # Client for interacting with the Anthropic (Claude) API with json response
9
+ class AnthropicClient < BaseClient
10
+ class AnthropicError < ClientError; end
11
+ class RateLimitError < AnthropicError; end
12
+
13
+ ANTHROPIC_VERSION = '2023-06-01'
14
+ ANTHROPIC_ENDPOINT = '/v1/messages'
15
+
16
+ def initialize(config)
17
+ super(config)
18
+ end
19
+
20
+ # Requests structured JSON output from the Anthropic Claude model based on a schema.
21
+ #
22
+ # @param messages [Array<Hash>] A list of messages representing the conversation history.
23
+ # Each hash should have :role ('user', 'assistant', or 'system') and :content (String).
24
+ # Claude uses 'user', 'assistant', and 'system' roles.
25
+ # @param function [Hash] A JSON schema definition describing the desired output format.
26
+ # @param model_tier [Symbol, nil] A symbolic representation of the model quality/size tier.
27
+ # @param config [Hash] Optional configuration overrides:
28
+ # - :api_key [String] Override the default API key.
29
+ # - :model [String] Override the model name directly.
30
+ # - :max_retries [Integer] Max retries for the request.
31
+ # - :retry_delay [Integer] Initial delay for retries.
32
+ # - :anthropic_version [String] Override the default Anthropic API version.
33
+ # @return [Hash, nil] The parsed JSON object matching the schema, or nil if parsing fails or content is empty.
34
+ def function_calling(messages, function, model_tier: nil, config: {})
35
+ model = config[:runtime][:model] || @app_config.tier_to_model(model_tier)
36
+
37
+ system_message = messages.find { |m| m[:role] == 'system' }&.dig(:content) || ''
38
+ user_messages = messages.select { |m| m[:role] == 'user' || m[:role] == 'assistant' }
39
+ .map { |m| { role: m[:role], content: m[:content] } }
40
+
41
+ anthropic_function = function.dup
42
+ anthropic_function[:input_schema] = function[:parameters]
43
+ anthropic_function.delete(:parameters)
44
+
45
+ payload = {
46
+ model:,
47
+ system: system_message,
48
+ messages: user_messages,
49
+ tools: [anthropic_function],
50
+ tool_choice: { name: anthropic_function[:name], type: 'tool' },
51
+ max_tokens: config[:runtime][:max_tokens],
52
+ temperature: config[:runtime][:temperature] || 0,
53
+ }
54
+
55
+ api_key = config[:runtime][:api_key] || @app_config.api_key
56
+ headers = {
57
+ 'x-api-key': api_key,
58
+ 'anthropic-version': config[:anthropic_version] || ANTHROPIC_VERSION
59
+ }.compact
60
+
61
+ retry_with_backoff(config:) do
62
+ start_time = Time.now
63
+
64
+ response = post(ANTHROPIC_ENDPOINT, payload, headers: headers, config: config)
65
+
66
+ content = response.dig('content', 0, 'input')
67
+
68
+ ActiveGenie::Logger.trace({
69
+ code: :llm_usage,
70
+ input_tokens: response.dig('usage', 'input_tokens'),
71
+ output_tokens: response.dig('usage', 'output_tokens'),
72
+ total_tokens: response.dig('usage', 'input_tokens') + response.dig('usage', 'output_tokens'),
73
+ model: payload[:model],
74
+ duration: Time.now - start_time,
75
+ usage: response.dig('usage')
76
+ })
77
+
78
+ ActiveGenie::Logger.trace({code: :function_calling, payload:, parsed_response: content})
79
+
80
+ content
81
+ end
82
+ end
83
+ end
84
+ end