activeagent 0.6.3 → 1.0.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/CHANGELOG.md +240 -2
- data/README.md +15 -24
- data/lib/active_agent/base.rb +389 -39
- data/lib/active_agent/concerns/callbacks.rb +251 -0
- data/lib/active_agent/concerns/observers.rb +147 -0
- data/lib/active_agent/concerns/parameterized.rb +292 -0
- data/lib/active_agent/concerns/provider.rb +120 -0
- data/lib/active_agent/concerns/queueing.rb +36 -0
- data/lib/active_agent/concerns/rescue.rb +64 -0
- data/lib/active_agent/concerns/streaming.rb +282 -0
- data/lib/active_agent/concerns/tooling.rb +23 -0
- data/lib/active_agent/concerns/view.rb +150 -0
- data/lib/active_agent/configuration.rb +442 -20
- data/lib/active_agent/generation.rb +141 -47
- data/lib/active_agent/providers/_base_provider.rb +420 -0
- data/lib/active_agent/providers/anthropic/_types.rb +63 -0
- data/lib/active_agent/providers/anthropic/options.rb +53 -0
- data/lib/active_agent/providers/anthropic/request.rb +163 -0
- data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
- data/lib/active_agent/providers/anthropic_provider.rb +254 -0
- data/lib/active_agent/providers/common/messages/_types.rb +160 -0
- data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
- data/lib/active_agent/providers/common/messages/base.rb +17 -0
- data/lib/active_agent/providers/common/messages/system.rb +20 -0
- data/lib/active_agent/providers/common/messages/tool.rb +21 -0
- data/lib/active_agent/providers/common/messages/user.rb +20 -0
- data/lib/active_agent/providers/common/model.rb +361 -0
- data/lib/active_agent/providers/common/response.rb +13 -0
- data/lib/active_agent/providers/common/responses/_types.rb +51 -0
- data/lib/active_agent/providers/common/responses/base.rb +199 -0
- data/lib/active_agent/providers/common/responses/embed.rb +33 -0
- data/lib/active_agent/providers/common/responses/format.rb +31 -0
- data/lib/active_agent/providers/common/responses/message.rb +3 -0
- data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
- data/lib/active_agent/providers/common/usage.rb +385 -0
- data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
- data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
- data/lib/active_agent/providers/concerns/previewable.rb +150 -0
- data/lib/active_agent/providers/log_subscriber.rb +178 -0
- data/lib/active_agent/providers/mock/_types.rb +77 -0
- data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
- data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
- data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
- data/lib/active_agent/providers/mock/messages/base.rb +63 -0
- data/lib/active_agent/providers/mock/messages/user.rb +18 -0
- data/lib/active_agent/providers/mock/options.rb +30 -0
- data/lib/active_agent/providers/mock/request.rb +38 -0
- data/lib/active_agent/providers/mock_provider.rb +311 -0
- data/lib/active_agent/providers/ollama/_types.rb +5 -0
- data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
- data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
- data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
- data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
- data/lib/active_agent/providers/ollama/options.rb +27 -0
- data/lib/active_agent/providers/ollama_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/_base.rb +59 -0
- data/lib/active_agent/providers/open_ai/_types.rb +5 -0
- data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
- data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
- data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
- data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
- data/lib/active_agent/providers/open_ai/options.rb +74 -0
- data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
- data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
- data/lib/active_agent/providers/open_ai_provider.rb +94 -0
- data/lib/active_agent/providers/open_router/_types.rb +71 -0
- data/lib/active_agent/providers/open_router/options.rb +141 -0
- data/lib/active_agent/providers/open_router/request.rb +249 -0
- data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
- data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
- data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
- data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
- data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
- data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
- data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
- data/lib/active_agent/providers/open_router/transforms.rb +134 -0
- data/lib/active_agent/providers/open_router_provider.rb +62 -0
- data/lib/active_agent/providers/openai_provider.rb +2 -0
- data/lib/active_agent/providers/openrouter_provider.rb +2 -0
- data/lib/active_agent/railtie.rb +8 -6
- data/lib/active_agent/schema_generator.rb +333 -166
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +112 -36
- data/lib/generators/active_agent/agent/USAGE +78 -0
- data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
- data/lib/generators/active_agent/install/USAGE +25 -0
- data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
- data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
- data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
- data/lib/generators/erb/agent_generator.rb +31 -16
- data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
- data/lib/generators/erb/templates/instructions.md.tt +3 -0
- data/lib/generators/erb/templates/instructions.text.tt +1 -0
- data/lib/generators/erb/templates/message.md.erb.tt +5 -0
- data/lib/generators/erb/templates/schema.json.tt +10 -0
- data/lib/generators/test_unit/agent_generator.rb +1 -1
- data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
- metadata +182 -71
- data/lib/active_agent/action_prompt/action.rb +0 -13
- data/lib/active_agent/action_prompt/base.rb +0 -623
- data/lib/active_agent/action_prompt/message.rb +0 -126
- data/lib/active_agent/action_prompt/prompt.rb +0 -136
- data/lib/active_agent/action_prompt.rb +0 -19
- data/lib/active_agent/callbacks.rb +0 -33
- data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
- data/lib/active_agent/generation_provider/base.rb +0 -55
- data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
- data/lib/active_agent/generation_provider/error_handling.rb +0 -167
- data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
- data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
- data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
- data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
- data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
- data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
- data/lib/active_agent/generation_provider/response.rb +0 -75
- data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
- data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
- data/lib/active_agent/generation_provider/tool_management.rb +0 -142
- data/lib/active_agent/generation_provider.rb +0 -67
- data/lib/active_agent/log_subscriber.rb +0 -44
- data/lib/active_agent/parameterized.rb +0 -75
- data/lib/active_agent/prompt_helper.rb +0 -19
- data/lib/active_agent/queued_generation.rb +0 -12
- data/lib/active_agent/rescuable.rb +0 -34
- data/lib/active_agent/sanitizers.rb +0 -40
- data/lib/active_agent/streaming.rb +0 -34
- data/lib/active_agent/test_case.rb +0 -125
- data/lib/generators/USAGE +0 -47
- data/lib/generators/active_agent/USAGE +0 -56
- data/lib/generators/erb/install_generator.rb +0 -44
- data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
- data/lib/generators/erb/templates/view.html.erb.tt +0 -5
- data/lib/generators/erb/templates/view.json.erb.tt +0 -16
- /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
- /data/lib/generators/erb/templates/{view.text.erb.tt → message.text.erb.tt} +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "_types"
|
|
5
|
+
require_relative "message"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Providers
|
|
9
|
+
module Common
|
|
10
|
+
module Responses
|
|
11
|
+
# Response model for prompt/completion responses
|
|
12
|
+
#
|
|
13
|
+
# This class represents responses from conversational/completion endpoints.
|
|
14
|
+
# It includes the generated messages, the original context, raw API data,
|
|
15
|
+
# and usage statistics.
|
|
16
|
+
#
|
|
17
|
+
# == Example
|
|
18
|
+
#
|
|
19
|
+
# response = PromptResponse.new(
|
|
20
|
+
# context: context_hash,
|
|
21
|
+
# messages: [message_object],
|
|
22
|
+
# raw_response: { "usage" => { "prompt_tokens" => 10 } }
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# response.message #=> <Message>
|
|
26
|
+
# response.prompt_tokens #=> 10
|
|
27
|
+
# response.usage #=> { "prompt_tokens" => 10, ... }
|
|
28
|
+
class Prompt < Base
|
|
29
|
+
# The list of messages from this conversation
|
|
30
|
+
attribute :messages, Types::MessagesType.new, writable: false
|
|
31
|
+
|
|
32
|
+
attribute :format, Types::FormatType.new, writable: false, default: {}
|
|
33
|
+
|
|
34
|
+
# The most recent message in the conversational stack
|
|
35
|
+
def message
|
|
36
|
+
messages.last
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/model"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Common
|
|
8
|
+
# Normalizes token usage statistics across AI providers.
|
|
9
|
+
#
|
|
10
|
+
# Providers return usage data in different formats with different field names.
|
|
11
|
+
# This model normalizes them into a consistent structure, automatically calculating
|
|
12
|
+
# +total_tokens+ if not provided.
|
|
13
|
+
#
|
|
14
|
+
# @example Accessing normalized usage data
|
|
15
|
+
# usage = response.normalized_usage
|
|
16
|
+
# usage.input_tokens #=> 100
|
|
17
|
+
# usage.output_tokens #=> 25
|
|
18
|
+
# usage.total_tokens #=> 125
|
|
19
|
+
# usage.cached_tokens #=> 20 (if available)
|
|
20
|
+
#
|
|
21
|
+
# @example Provider-specific details
|
|
22
|
+
# usage.provider_details #=> { "completion_tokens_details" => {...}, ... }
|
|
23
|
+
# usage.duration_ms #=> 5000 (for Ollama)
|
|
24
|
+
# usage.service_tier #=> "standard" (for Anthropic)
|
|
25
|
+
#
|
|
26
|
+
# @see https://platform.openai.com/docs/api-reference/chat/object OpenAI Chat Completion
|
|
27
|
+
# @see https://docs.anthropic.com/en/api/messages Anthropic Messages API
|
|
28
|
+
# @see https://github.com/ollama/ollama/blob/main/docs/api.md Ollama API
|
|
29
|
+
class Usage < BaseModel
|
|
30
|
+
# @!attribute [rw] input_tokens
|
|
31
|
+
# Normalized from:
|
|
32
|
+
# - OpenAI Chat/Embeddings: prompt_tokens
|
|
33
|
+
# - OpenAI Responses API: input_tokens
|
|
34
|
+
# - Anthropic: input_tokens
|
|
35
|
+
# - Ollama: prompt_eval_count
|
|
36
|
+
# - OpenRouter: prompt_tokens
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer]
|
|
39
|
+
attribute :input_tokens, :integer, default: 0
|
|
40
|
+
|
|
41
|
+
# @!attribute [rw] output_tokens
|
|
42
|
+
# Normalized from:
|
|
43
|
+
# - OpenAI Chat: completion_tokens
|
|
44
|
+
# - OpenAI Responses API: output_tokens
|
|
45
|
+
# - Anthropic: output_tokens
|
|
46
|
+
# - Ollama: eval_count
|
|
47
|
+
# - OpenRouter: completion_tokens
|
|
48
|
+
# - OpenAI Embeddings: 0 (no output tokens)
|
|
49
|
+
#
|
|
50
|
+
# @return [Integer]
|
|
51
|
+
attribute :output_tokens, :integer, default: 0
|
|
52
|
+
|
|
53
|
+
# @!attribute [rw] total_tokens
|
|
54
|
+
# Automatically calculated as input_tokens + output_tokens if not provided by provider.
|
|
55
|
+
#
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
attribute :total_tokens, :integer
|
|
58
|
+
|
|
59
|
+
# @!attribute [rw] cached_tokens
|
|
60
|
+
# Available from:
|
|
61
|
+
# - OpenAI: prompt_tokens_details.cached_tokens or input_tokens_details.cached_tokens
|
|
62
|
+
# - Anthropic: cache_read_input_tokens
|
|
63
|
+
#
|
|
64
|
+
# @return [Integer, nil]
|
|
65
|
+
attribute :cached_tokens, :integer
|
|
66
|
+
|
|
67
|
+
# @!attribute [rw] reasoning_tokens
|
|
68
|
+
# Available from:
|
|
69
|
+
# - OpenAI Chat: completion_tokens_details.reasoning_tokens
|
|
70
|
+
# - OpenAI Responses: output_tokens_details.reasoning_tokens
|
|
71
|
+
#
|
|
72
|
+
# @return [Integer, nil]
|
|
73
|
+
attribute :reasoning_tokens, :integer
|
|
74
|
+
|
|
75
|
+
# @!attribute [rw] audio_tokens
|
|
76
|
+
# Available from:
|
|
77
|
+
# - OpenAI: sum of prompt_tokens_details.audio_tokens and completion_tokens_details.audio_tokens
|
|
78
|
+
#
|
|
79
|
+
# @return [Integer, nil]
|
|
80
|
+
attribute :audio_tokens, :integer
|
|
81
|
+
|
|
82
|
+
# @!attribute [rw] cache_creation_tokens
|
|
83
|
+
# Available from:
|
|
84
|
+
# - Anthropic: cache_creation_input_tokens
|
|
85
|
+
#
|
|
86
|
+
# @return [Integer, nil]
|
|
87
|
+
attribute :cache_creation_tokens, :integer
|
|
88
|
+
|
|
89
|
+
# @!attribute [rw] service_tier
|
|
90
|
+
# Available from:
|
|
91
|
+
# - Anthropic: service_tier ("standard", "priority", "batch")
|
|
92
|
+
#
|
|
93
|
+
# @return [String, nil]
|
|
94
|
+
attribute :service_tier, :string
|
|
95
|
+
|
|
96
|
+
# @!attribute [rw] duration_ms
|
|
97
|
+
# Available from:
|
|
98
|
+
# - Ollama: total_duration (converted from nanoseconds)
|
|
99
|
+
#
|
|
100
|
+
# @return [Integer, nil]
|
|
101
|
+
attribute :duration_ms, :integer
|
|
102
|
+
|
|
103
|
+
# @!attribute [rw] provider_details
|
|
104
|
+
# Preserves provider-specific information that doesn't fit the normalized structure.
|
|
105
|
+
# Useful for debugging or provider-specific features.
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash]
|
|
108
|
+
attribute :provider_details, default: -> { {} }
|
|
109
|
+
|
|
110
|
+
# Automatically calculates total_tokens if not provided.
|
|
111
|
+
#
|
|
112
|
+
# @param attributes [Hash]
|
|
113
|
+
# @option attributes [Integer] :input_tokens
|
|
114
|
+
# @option attributes [Integer] :output_tokens
|
|
115
|
+
# @option attributes [Integer] :total_tokens (calculated if not provided)
|
|
116
|
+
# @option attributes [Integer] :cached_tokens
|
|
117
|
+
# @option attributes [Integer] :reasoning_tokens
|
|
118
|
+
# @option attributes [Integer] :audio_tokens
|
|
119
|
+
# @option attributes [Integer] :cache_creation_tokens
|
|
120
|
+
# @option attributes [String] :service_tier
|
|
121
|
+
# @option attributes [Integer] :duration_ms
|
|
122
|
+
# @option attributes [Hash] :provider_details
|
|
123
|
+
def initialize(attributes = {})
|
|
124
|
+
super
|
|
125
|
+
# Calculate total_tokens if not provided
|
|
126
|
+
self.total_tokens ||= (input_tokens || 0) + (output_tokens || 0)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Sums all token counts from two Usage objects.
|
|
130
|
+
#
|
|
131
|
+
# @param other [Usage]
|
|
132
|
+
# @return [Usage]
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# usage1 = Usage.new(input_tokens: 100, output_tokens: 50)
|
|
136
|
+
# usage2 = Usage.new(input_tokens: 75, output_tokens: 25)
|
|
137
|
+
# combined = usage1 + usage2
|
|
138
|
+
# combined.input_tokens #=> 175
|
|
139
|
+
# combined.output_tokens #=> 75
|
|
140
|
+
# combined.total_tokens #=> 250
|
|
141
|
+
def +(other)
|
|
142
|
+
return self unless other
|
|
143
|
+
|
|
144
|
+
self.class.new(
|
|
145
|
+
input_tokens: self.input_tokens + other.input_tokens,
|
|
146
|
+
output_tokens: self.output_tokens + other.output_tokens,
|
|
147
|
+
total_tokens: self.total_tokens + other.total_tokens,
|
|
148
|
+
cached_tokens: sum_optional(self.cached_tokens, other.cached_tokens),
|
|
149
|
+
cache_creation_tokens: sum_optional(self.cache_creation_tokens, other.cache_creation_tokens),
|
|
150
|
+
reasoning_tokens: sum_optional(self.reasoning_tokens, other.reasoning_tokens),
|
|
151
|
+
audio_tokens: sum_optional(self.audio_tokens, other.audio_tokens)
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Creates a Usage object from OpenAI Chat Completion usage data.
|
|
156
|
+
#
|
|
157
|
+
# @param usage_hash [Hash]
|
|
158
|
+
# @return [Usage]
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# Usage.from_openai_chat({
|
|
162
|
+
# "prompt_tokens" => 100,
|
|
163
|
+
# "completion_tokens" => 25,
|
|
164
|
+
# "total_tokens" => 125,
|
|
165
|
+
# "prompt_tokens_details" => { "cached_tokens" => 20 },
|
|
166
|
+
# "completion_tokens_details" => { "reasoning_tokens" => 3 }
|
|
167
|
+
# })
|
|
168
|
+
def self.from_openai_chat(usage_hash)
|
|
169
|
+
return nil unless usage_hash
|
|
170
|
+
|
|
171
|
+
usage = usage_hash.deep_symbolize_keys
|
|
172
|
+
prompt_details = usage[:prompt_tokens_details] || {}
|
|
173
|
+
completion_details = usage[:completion_tokens_details] || {}
|
|
174
|
+
|
|
175
|
+
audio_sum = [
|
|
176
|
+
prompt_details[:audio_tokens],
|
|
177
|
+
completion_details[:audio_tokens]
|
|
178
|
+
].compact.sum
|
|
179
|
+
|
|
180
|
+
new(
|
|
181
|
+
**usage.slice(:total_tokens),
|
|
182
|
+
input_tokens: usage[:prompt_tokens] || 0,
|
|
183
|
+
output_tokens: usage[:completion_tokens] || 0,
|
|
184
|
+
cached_tokens: prompt_details[:cached_tokens],
|
|
185
|
+
reasoning_tokens: completion_details[:reasoning_tokens],
|
|
186
|
+
audio_tokens: audio_sum > 0 ? audio_sum : nil,
|
|
187
|
+
provider_details: usage.slice(:prompt_tokens_details, :completion_tokens_details).compact
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Creates a Usage object from OpenAI Embedding API usage data.
|
|
192
|
+
#
|
|
193
|
+
# @param usage_hash [Hash]
|
|
194
|
+
# @return [Usage]
|
|
195
|
+
#
|
|
196
|
+
# @example
|
|
197
|
+
# Usage.from_openai_embedding({
|
|
198
|
+
# "prompt_tokens" => 8,
|
|
199
|
+
# "total_tokens" => 8
|
|
200
|
+
# })
|
|
201
|
+
def self.from_openai_embedding(usage_hash)
|
|
202
|
+
return nil unless usage_hash
|
|
203
|
+
|
|
204
|
+
usage = usage_hash.deep_symbolize_keys
|
|
205
|
+
|
|
206
|
+
new(
|
|
207
|
+
**usage.slice(:total_tokens),
|
|
208
|
+
input_tokens: usage[:prompt_tokens] || 0,
|
|
209
|
+
output_tokens: 0, # Embeddings don't generate output tokens
|
|
210
|
+
provider_details: usage.except(:prompt_tokens, :total_tokens)
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Creates a Usage object from OpenAI Responses API usage data.
|
|
215
|
+
#
|
|
216
|
+
# @param usage_hash [Hash]
|
|
217
|
+
# @return [Usage]
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# Usage.from_openai_responses({
|
|
221
|
+
# "input_tokens" => 150,
|
|
222
|
+
# "output_tokens" => 75,
|
|
223
|
+
# "total_tokens" => 225,
|
|
224
|
+
# "input_tokens_details" => { "cached_tokens" => 50 },
|
|
225
|
+
# "output_tokens_details" => { "reasoning_tokens" => 10 }
|
|
226
|
+
# })
|
|
227
|
+
def self.from_openai_responses(usage_hash)
|
|
228
|
+
return nil unless usage_hash
|
|
229
|
+
|
|
230
|
+
usage = usage_hash.deep_symbolize_keys
|
|
231
|
+
input_details = usage[:input_tokens_details] || {}
|
|
232
|
+
output_details = usage[:output_tokens_details] || {}
|
|
233
|
+
|
|
234
|
+
new(
|
|
235
|
+
**usage.slice(:input_tokens, :output_tokens, :total_tokens),
|
|
236
|
+
input_tokens: usage[:input_tokens] || 0,
|
|
237
|
+
output_tokens: usage[:output_tokens] || 0,
|
|
238
|
+
cached_tokens: input_details[:cached_tokens],
|
|
239
|
+
reasoning_tokens: output_details[:reasoning_tokens],
|
|
240
|
+
provider_details: usage.slice(:input_tokens_details, :output_tokens_details).compact
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Creates a Usage object from Anthropic usage data.
|
|
245
|
+
#
|
|
246
|
+
# @param usage_hash [Hash]
|
|
247
|
+
# @return [Usage]
|
|
248
|
+
#
|
|
249
|
+
# @example
|
|
250
|
+
# Usage.from_anthropic({
|
|
251
|
+
# "input_tokens" => 2095,
|
|
252
|
+
# "output_tokens" => 503,
|
|
253
|
+
# "cache_read_input_tokens" => 1500,
|
|
254
|
+
# "cache_creation_input_tokens" => 2051,
|
|
255
|
+
# "service_tier" => "standard"
|
|
256
|
+
# })
|
|
257
|
+
def self.from_anthropic(usage_hash)
|
|
258
|
+
return nil unless usage_hash
|
|
259
|
+
|
|
260
|
+
usage = usage_hash.deep_symbolize_keys
|
|
261
|
+
|
|
262
|
+
new(
|
|
263
|
+
**usage.slice(:input_tokens, :output_tokens, :service_tier),
|
|
264
|
+
input_tokens: usage[:input_tokens] || 0,
|
|
265
|
+
output_tokens: usage[:output_tokens] || 0,
|
|
266
|
+
cached_tokens: usage[:cache_read_input_tokens],
|
|
267
|
+
cache_creation_tokens: usage[:cache_creation_input_tokens],
|
|
268
|
+
provider_details: usage.slice(:cache_creation, :server_tool_use).compact
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Creates a Usage object from Ollama usage data.
|
|
273
|
+
#
|
|
274
|
+
# @param usage_hash [Hash]
|
|
275
|
+
# @return [Usage]
|
|
276
|
+
#
|
|
277
|
+
# @example
|
|
278
|
+
# Usage.from_ollama({
|
|
279
|
+
# "prompt_eval_count" => 50,
|
|
280
|
+
# "eval_count" => 25,
|
|
281
|
+
# "total_duration" => 5000000000,
|
|
282
|
+
# "load_duration" => 1000000000
|
|
283
|
+
# })
|
|
284
|
+
def self.from_ollama(usage_hash)
|
|
285
|
+
return nil unless usage_hash
|
|
286
|
+
|
|
287
|
+
usage = usage_hash.deep_symbolize_keys
|
|
288
|
+
|
|
289
|
+
new(
|
|
290
|
+
input_tokens: usage[:prompt_eval_count] || 0,
|
|
291
|
+
output_tokens: usage[:eval_count] || 0,
|
|
292
|
+
duration_ms: convert_nanoseconds_to_ms(usage[:total_duration]),
|
|
293
|
+
provider_details: {
|
|
294
|
+
load_duration_ms: convert_nanoseconds_to_ms(usage[:load_duration]),
|
|
295
|
+
prompt_eval_duration_ms: convert_nanoseconds_to_ms(usage[:prompt_eval_duration]),
|
|
296
|
+
eval_duration_ms: convert_nanoseconds_to_ms(usage[:eval_duration]),
|
|
297
|
+
tokens_per_second: calculate_tokens_per_second(usage[:eval_count], usage[:eval_duration])
|
|
298
|
+
}.compact
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Creates a Usage object from OpenRouter usage data.
|
|
303
|
+
#
|
|
304
|
+
# OpenRouter uses the same format as OpenAI Chat Completion.
|
|
305
|
+
#
|
|
306
|
+
# @param usage_hash [Hash]
|
|
307
|
+
# @return [Usage]
|
|
308
|
+
#
|
|
309
|
+
# @example
|
|
310
|
+
# Usage.from_openrouter({
|
|
311
|
+
# "prompt_tokens" => 14,
|
|
312
|
+
# "completion_tokens" => 4,
|
|
313
|
+
# "total_tokens" => 18
|
|
314
|
+
# })
|
|
315
|
+
def self.from_openrouter(usage_hash)
|
|
316
|
+
from_openai_chat(usage_hash)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Auto-detects the provider format and creates a normalized Usage object.
|
|
320
|
+
#
|
|
321
|
+
# @note Detection is based on hash structure rather than native gem types
|
|
322
|
+
# because we cannot force-load all provider gems. This allows the framework
|
|
323
|
+
# to work with only the gems the user has installed.
|
|
324
|
+
#
|
|
325
|
+
# @param usage_hash [Hash]
|
|
326
|
+
# @return [Usage, nil]
|
|
327
|
+
#
|
|
328
|
+
# @example
|
|
329
|
+
# Usage.from_provider_usage(some_usage_hash)
|
|
330
|
+
def self.from_provider_usage(usage_hash)
|
|
331
|
+
return nil unless usage_hash.is_a?(Hash)
|
|
332
|
+
|
|
333
|
+
usage = usage_hash.deep_symbolize_keys
|
|
334
|
+
|
|
335
|
+
# Detect Ollama by presence of nanosecond duration fields
|
|
336
|
+
if usage.key?(:total_duration)
|
|
337
|
+
from_ollama(usage_hash)
|
|
338
|
+
# Detect Anthropic by presence of cache_creation or service_tier
|
|
339
|
+
elsif usage.key?(:cache_creation) || usage.key?(:service_tier)
|
|
340
|
+
from_anthropic(usage_hash)
|
|
341
|
+
# Detect OpenAI Responses API by input_tokens/output_tokens with details
|
|
342
|
+
elsif usage.key?(:input_tokens) && usage.key?(:input_tokens_details)
|
|
343
|
+
from_openai_responses(usage_hash)
|
|
344
|
+
# Detect OpenAI Chat/OpenRouter by prompt_tokens/completion_tokens
|
|
345
|
+
elsif usage.key?(:completion_tokens)
|
|
346
|
+
from_openai_chat(usage_hash)
|
|
347
|
+
# Detect OpenAI Embedding by prompt_tokens without completion_tokens
|
|
348
|
+
elsif usage.key?(:prompt_tokens)
|
|
349
|
+
from_openai_embedding(usage_hash)
|
|
350
|
+
# Default to raw initialization
|
|
351
|
+
else
|
|
352
|
+
new(usage_hash)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
private
|
|
357
|
+
|
|
358
|
+
# @param a [Integer, nil]
|
|
359
|
+
# @param b [Integer, nil]
|
|
360
|
+
# @return [Integer, nil] nil if both inputs are nil
|
|
361
|
+
def sum_optional(a, b)
|
|
362
|
+
return nil if a.nil? && b.nil?
|
|
363
|
+
(a || 0) + (b || 0)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# @param nanoseconds [Integer, nil]
|
|
367
|
+
# @return [Integer, nil]
|
|
368
|
+
def self.convert_nanoseconds_to_ms(nanoseconds)
|
|
369
|
+
return nil unless nanoseconds
|
|
370
|
+
|
|
371
|
+
(nanoseconds / 1_000_000.0).round
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# @param tokens [Integer, nil]
|
|
375
|
+
# @param duration_ns [Integer, nil]
|
|
376
|
+
# @return [Float, nil]
|
|
377
|
+
def self.calculate_tokens_per_second(tokens, duration_ns)
|
|
378
|
+
return nil unless tokens && duration_ns && duration_ns > 0
|
|
379
|
+
|
|
380
|
+
(tokens.to_f / (duration_ns / 1_000_000_000.0)).round(2)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
# Provides exception handling for provider operations.
|
|
6
|
+
#
|
|
7
|
+
# This concern implements basic exception handling that allows agents to
|
|
8
|
+
# define custom error handling logic via rescue_from callbacks. The actual
|
|
9
|
+
# retry logic is now handled by the underlying provider gems (ruby-openai,
|
|
10
|
+
# anthropic-rb, etc.) which provide their own retry mechanisms.
|
|
11
|
+
#
|
|
12
|
+
# @example Using exception handler
|
|
13
|
+
# class MyProvider
|
|
14
|
+
# include ActiveAgent::Providers::ExceptionHandler
|
|
15
|
+
#
|
|
16
|
+
# def call
|
|
17
|
+
# with_exception_handling do
|
|
18
|
+
# # API call that may fail
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Agent-level error handling
|
|
24
|
+
# class MyAgent < ActiveAgent::Base
|
|
25
|
+
# rescue_from SomeError do |exception|
|
|
26
|
+
# # Handle the error
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
module ExceptionHandler
|
|
30
|
+
extend ActiveSupport::Concern
|
|
31
|
+
|
|
32
|
+
included do
|
|
33
|
+
# @!attribute [rw] exception_handler
|
|
34
|
+
# @return [Proc, nil] Callback for handling exceptions
|
|
35
|
+
attr_internal :exception_handler
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Configures instance-level exception handling.
|
|
39
|
+
#
|
|
40
|
+
# @param exception_handler [Proc, nil] callback for handling exceptions
|
|
41
|
+
# @return [void]
|
|
42
|
+
def configure_exception_handler(exception_handler: nil)
|
|
43
|
+
self.exception_handler = exception_handler
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Executes a block with exception handling.
|
|
47
|
+
#
|
|
48
|
+
# @yield Block to execute with exception protection
|
|
49
|
+
# @return [Object] The result of the block execution
|
|
50
|
+
# @raise [StandardError] Any unhandled exception from the block
|
|
51
|
+
#
|
|
52
|
+
# @example Basic usage
|
|
53
|
+
# with_exception_handling { api_call }
|
|
54
|
+
def with_exception_handling(&block)
|
|
55
|
+
yield
|
|
56
|
+
rescue => exception
|
|
57
|
+
rescue_with_handler(exception) || raise
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Bubbles up exceptions to the Agent's rescue_from if a handler is defined.
|
|
61
|
+
#
|
|
62
|
+
# This method delegates exception handling to the configured exception handler,
|
|
63
|
+
# allowing agents to define custom error handling logic.
|
|
64
|
+
#
|
|
65
|
+
# @param exception [StandardError] The exception to handle
|
|
66
|
+
# @return [Object, nil] Result from the exception handler, or nil if no handler
|
|
67
|
+
def rescue_with_handler(exception)
|
|
68
|
+
exception_handler&.call(exception)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|