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,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
# Builds instrumentation event payloads for ActiveSupport::Notifications.
|
|
6
|
+
#
|
|
7
|
+
# Extracts request parameters and response metadata for monitoring, debugging,
|
|
8
|
+
# and APM integration (New Relic, DataDog, etc.).
|
|
9
|
+
#
|
|
10
|
+
# == Event Payloads
|
|
11
|
+
#
|
|
12
|
+
# Top-Level Events (overall request lifecycle):
|
|
13
|
+
#
|
|
14
|
+
# prompt.active_agent::
|
|
15
|
+
# Initial: `{ model:, temperature:, max_tokens:, message_count:, has_tools:, stream: }`
|
|
16
|
+
# Final: `{ usage: { input_tokens:, output_tokens:, total_tokens: }, finish_reason:, response_model:, response_id: }`
|
|
17
|
+
# Note: Usage is cumulative across all API calls in multi-turn conversations
|
|
18
|
+
#
|
|
19
|
+
# embed.active_agent::
|
|
20
|
+
# Initial: `{ model:, input_size:, encoding_format:, dimensions: }`
|
|
21
|
+
# Final: `{ usage: { input_tokens:, total_tokens: }, embedding_count:, response_model:, response_id: }`
|
|
22
|
+
#
|
|
23
|
+
# Provider-Level Events (per API call):
|
|
24
|
+
#
|
|
25
|
+
# prompt.provider.active_agent::
|
|
26
|
+
# Initial: `{ model:, temperature:, max_tokens:, message_count:, has_tools:, stream: }`
|
|
27
|
+
# Final: `{ usage: { input_tokens:, output_tokens:, total_tokens: }, finish_reason:, response_model:, response_id: }`
|
|
28
|
+
# Note: Usage is per individual API call
|
|
29
|
+
#
|
|
30
|
+
# embed.provider.active_agent::
|
|
31
|
+
# Initial: `{ model:, input_size:, encoding_format:, dimensions: }`
|
|
32
|
+
# Final: `{ usage: { input_tokens:, total_tokens: }, embedding_count:, response_model:, response_id: }`
|
|
33
|
+
module Instrumentation
|
|
34
|
+
extend ActiveSupport::Concern
|
|
35
|
+
|
|
36
|
+
# Builds and merges payload data for prompt instrumentation events.
|
|
37
|
+
#
|
|
38
|
+
# Populates both request parameters and response metadata for top-level and
|
|
39
|
+
# provider-level events. Usage data (tokens) is CRITICAL for APM cost tracking
|
|
40
|
+
# and performance monitoring.
|
|
41
|
+
#
|
|
42
|
+
# @param payload [Hash] instrumentation payload to merge into
|
|
43
|
+
# @param request [Request] request object with parameters
|
|
44
|
+
# @param response [Common::PromptResponse] completed response with normalized data
|
|
45
|
+
# @return [void]
|
|
46
|
+
def instrumentation_prompt_payload(payload, request, response)
|
|
47
|
+
# message_count: prefer the request/input messages (pre-call), fall back to
|
|
48
|
+
# response messages only if the request doesn't expose messages. New Relic
|
|
49
|
+
# expects parameters[:messages] to be the request messages and computes
|
|
50
|
+
# total message counts by adding response choices to that count.
|
|
51
|
+
message_count = safe_access(request, :messages)&.size
|
|
52
|
+
message_count = safe_access(response, :messages)&.size if message_count.nil?
|
|
53
|
+
|
|
54
|
+
payload.merge!(trace_id: trace_id, message_count: message_count || 0, stream: !!safe_access(request, :stream))
|
|
55
|
+
|
|
56
|
+
# Common parameters: prefer response-normalized values, then request
|
|
57
|
+
payload[:model] = safe_access(response, :model) || safe_access(request, :model)
|
|
58
|
+
payload[:temperature] = safe_access(request, :temperature)
|
|
59
|
+
payload[:max_tokens] = safe_access(request, :max_tokens)
|
|
60
|
+
payload[:top_p] = safe_access(request, :top_p)
|
|
61
|
+
|
|
62
|
+
# Tools / instructions
|
|
63
|
+
if (tools_val = safe_access(request, :tools))
|
|
64
|
+
payload[:has_tools] = tools_val.respond_to?(:present?) ? tools_val.present? : !!tools_val
|
|
65
|
+
payload[:tool_count] = tools_val&.size || 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if (instr_val = safe_access(request, :instructions))
|
|
69
|
+
payload[:has_instructions] = instr_val.respond_to?(:present?) ? instr_val.present? : !!instr_val
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Usage (normalized)
|
|
73
|
+
if response.usage
|
|
74
|
+
usage = response.usage
|
|
75
|
+
payload[:usage] = {
|
|
76
|
+
input_tokens: usage.input_tokens,
|
|
77
|
+
output_tokens: usage.output_tokens,
|
|
78
|
+
total_tokens: usage.total_tokens
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
payload[:usage][:cached_tokens] = usage.cached_tokens if usage.cached_tokens
|
|
82
|
+
payload[:usage][:cache_creation_tokens] = usage.cache_creation_tokens if usage.cache_creation_tokens
|
|
83
|
+
payload[:usage][:reasoning_tokens] = usage.reasoning_tokens if usage.reasoning_tokens
|
|
84
|
+
payload[:usage][:audio_tokens] = usage.audio_tokens if usage.audio_tokens
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Response metadata
|
|
88
|
+
payload[:finish_reason] = safe_access(response, :finish_reason) || response.finish_reason
|
|
89
|
+
payload[:response_model] = safe_access(response, :model) || response.model
|
|
90
|
+
payload[:response_id] = safe_access(response, :id) || response.id
|
|
91
|
+
|
|
92
|
+
# Build messages list: prefer request messages; if unavailable use prior
|
|
93
|
+
# response messages (all but the final generated message).
|
|
94
|
+
if (req_msgs = safe_access(request, :messages)).is_a?(Array)
|
|
95
|
+
payload[:messages] = req_msgs.map { |m| extract_message_hash(m, false) }
|
|
96
|
+
else
|
|
97
|
+
prior = safe_access(response, :messages)
|
|
98
|
+
prior = prior[0...-1] if prior.is_a?(Array) && prior.size > 1
|
|
99
|
+
if prior.is_a?(Array) && prior.any?
|
|
100
|
+
payload[:messages] = prior.map { |m| extract_message_hash(m, false) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Build a parameters hash that mirrors what New Relic's OpenAI
|
|
105
|
+
# instrumentation expects. This makes it easy for APM adapters to
|
|
106
|
+
# map our provider payload to their LLM event constructors.
|
|
107
|
+
parameters = {}
|
|
108
|
+
parameters[:model] = payload[:model] if payload[:model]
|
|
109
|
+
parameters[:max_tokens] = payload[:max_tokens] if payload[:max_tokens]
|
|
110
|
+
parameters[:temperature] = payload[:temperature] if payload[:temperature]
|
|
111
|
+
parameters[:top_p] = payload[:top_p] if payload[:top_p]
|
|
112
|
+
parameters[:stream] = payload[:stream]
|
|
113
|
+
parameters[:messages] = payload[:messages] if payload[:messages]
|
|
114
|
+
|
|
115
|
+
# Include tools/instructions where available — New Relic ignores unknown keys,
|
|
116
|
+
# but having them here makes the parameter shape closer to OpenAI's.
|
|
117
|
+
parameters[:tools] = begin request.tools rescue nil end if begin request.tools rescue nil end
|
|
118
|
+
parameters[:instructions] = begin request.instructions rescue nil end if begin request.instructions rescue nil end
|
|
119
|
+
|
|
120
|
+
payload[:parameters] = parameters
|
|
121
|
+
|
|
122
|
+
# Attach raw response (provider-specific) so downstream APM integrations
|
|
123
|
+
# can inspect the provider response if needed. Use the normalized raw_response
|
|
124
|
+
# available on the Common::Response when possible.
|
|
125
|
+
begin
|
|
126
|
+
payload[:response_raw] = response.raw_response if response.respond_to?(:raw_response) && response.raw_response
|
|
127
|
+
rescue StandardError
|
|
128
|
+
# ignore
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Safely attempt to call a method or lookup a key on an object. We avoid
|
|
135
|
+
# probing with `respond_to?` to prevent ActiveModel attribute casting side
|
|
136
|
+
# effects; instead we attempt the call and rescue failures.
|
|
137
|
+
def safe_access(obj, name)
|
|
138
|
+
return nil if obj.nil?
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
return obj.public_send(name)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
return obj[name]
|
|
147
|
+
rescue StandardError
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
return obj[name.to_s]
|
|
152
|
+
rescue StandardError
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# NOTE: message access is handled via `safe_access(obj, :messages)` to
|
|
159
|
+
# avoid duplicating guarded lookup logic.
|
|
160
|
+
|
|
161
|
+
# Extract a simple hash from a provider message object or hash-like value.
|
|
162
|
+
def extract_message_hash(msg, is_response = false)
|
|
163
|
+
role = begin
|
|
164
|
+
if msg.respond_to?(:[])
|
|
165
|
+
begin msg[:role] rescue (begin msg["role"] rescue nil end) end
|
|
166
|
+
elsif msg.respond_to?(:role)
|
|
167
|
+
msg.role
|
|
168
|
+
elsif msg.respond_to?(:type)
|
|
169
|
+
msg.type
|
|
170
|
+
end
|
|
171
|
+
rescue StandardError
|
|
172
|
+
begin msg.role rescue msg.type rescue nil end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
content = begin
|
|
176
|
+
if msg.respond_to?(:[])
|
|
177
|
+
begin msg[:content] rescue (begin msg["content"] rescue nil end) end
|
|
178
|
+
elsif msg.respond_to?(:content)
|
|
179
|
+
msg.content
|
|
180
|
+
elsif msg.respond_to?(:text)
|
|
181
|
+
msg.text
|
|
182
|
+
elsif msg.respond_to?(:to_h)
|
|
183
|
+
begin msg.to_h[:content] rescue (begin msg.to_h["content"] rescue nil end) end
|
|
184
|
+
elsif msg.respond_to?(:to_s)
|
|
185
|
+
msg.to_s
|
|
186
|
+
end
|
|
187
|
+
rescue StandardError
|
|
188
|
+
begin msg.to_s rescue nil end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
{ role: role, content: content, is_response: is_response }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Builds and merges payload data for embed instrumentation events.
|
|
195
|
+
#
|
|
196
|
+
# Embeddings typically only report input tokens (no output tokens).
|
|
197
|
+
#
|
|
198
|
+
# @param payload [Hash] instrumentation payload to merge into
|
|
199
|
+
# @param request [Request] request object with parameters
|
|
200
|
+
# @param response [Common::EmbedResponse] completed response with normalized data
|
|
201
|
+
# @return [void]
|
|
202
|
+
def instrumentation_embed_payload(payload, request, response)
|
|
203
|
+
# Add request parameters
|
|
204
|
+
payload[:trace_id] = trace_id
|
|
205
|
+
payload[:model] = request.model if request.respond_to?(:model)
|
|
206
|
+
|
|
207
|
+
# Add input size if available
|
|
208
|
+
if request.respond_to?(:input)
|
|
209
|
+
begin
|
|
210
|
+
input = request.input
|
|
211
|
+
if input.is_a?(String)
|
|
212
|
+
payload[:input_size] = 1
|
|
213
|
+
elsif input.is_a?(Array)
|
|
214
|
+
payload[:input_size] = input.size
|
|
215
|
+
end
|
|
216
|
+
rescue # OpenAI throws errors this for some reason when you try to look at the input.
|
|
217
|
+
payload[:input_size] = request[:input].size
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Expose embedding input content similarly to message content.
|
|
222
|
+
# Use guarded access to avoid provider-specific errors.
|
|
223
|
+
begin
|
|
224
|
+
if (emb_input = safe_access(request, :input))
|
|
225
|
+
# Keep the raw input (string or array) in the payload so APM adapters
|
|
226
|
+
# can inspect it. This matches how we include message content.
|
|
227
|
+
payload[:input] = emb_input
|
|
228
|
+
end
|
|
229
|
+
rescue StandardError
|
|
230
|
+
# ignore
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Add encoding format if available (OpenAI)
|
|
234
|
+
payload[:encoding_format] = request.encoding_format if request.respond_to?(:encoding_format)
|
|
235
|
+
|
|
236
|
+
# Add dimensions if available (OpenAI)
|
|
237
|
+
payload[:dimensions] = request.dimensions if request.respond_to?(:dimensions)
|
|
238
|
+
|
|
239
|
+
# Add response data
|
|
240
|
+
payload[:embedding_count] = response.data&.size || 0
|
|
241
|
+
|
|
242
|
+
# Add usage data if available (CRITICAL for APM integration)
|
|
243
|
+
# Embeddings typically only have input tokens
|
|
244
|
+
if response.usage
|
|
245
|
+
payload[:usage] = {
|
|
246
|
+
input_tokens: response.usage.input_tokens,
|
|
247
|
+
total_tokens: response.usage.total_tokens
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Add response metadata directly from response object
|
|
252
|
+
payload[:response_model] = response.model
|
|
253
|
+
payload[:response_id] = response.id
|
|
254
|
+
|
|
255
|
+
# Build a parameters hash for embeddings to match New Relic's shape.
|
|
256
|
+
emb_params = {}
|
|
257
|
+
emb_params[:model] = payload[:model] if payload[:model]
|
|
258
|
+
emb_params[:input] = payload[:input] if payload.key?(:input)
|
|
259
|
+
payload[:parameters] = emb_params unless emb_params.empty?
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
# Generates markdown previews of prompts for debugging and inspection.
|
|
6
|
+
#
|
|
7
|
+
# Renders request parameters, instructions, messages, and tools in a
|
|
8
|
+
# human-readable format without executing the actual API call.
|
|
9
|
+
module Previewable
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
# Generates a markdown preview of the prompt request.
|
|
13
|
+
#
|
|
14
|
+
# @return [String] markdown-formatted preview
|
|
15
|
+
def preview_prompt
|
|
16
|
+
request = prepare_prompt_request
|
|
17
|
+
|
|
18
|
+
# @todo Validate Request
|
|
19
|
+
api_parameters = api_request_build(request, prompt_request_type)
|
|
20
|
+
|
|
21
|
+
render_markdown_preview(api_parameters)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Renders markdown preview with YAML metadata, instructions, messages, and tools.
|
|
27
|
+
#
|
|
28
|
+
# Sections are separated by `---` dividers for readability.
|
|
29
|
+
#
|
|
30
|
+
# @param parameters [Hash]
|
|
31
|
+
# @return [String]
|
|
32
|
+
def render_markdown_preview(parameters)
|
|
33
|
+
sections = []
|
|
34
|
+
|
|
35
|
+
# Instructions section
|
|
36
|
+
if (instructions = parameters.delete(:instructions)).present?
|
|
37
|
+
sections << render_instructions_section(instructions)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Messages section
|
|
41
|
+
if (messages = parameters.delete(:messages) || parameters.delete(:input)).present?
|
|
42
|
+
sections << render_messages_section(messages)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Tools section
|
|
46
|
+
if (tools = parameters.delete(:tools)).present?
|
|
47
|
+
sections << render_tools_section(tools)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Prepend YAML section with request details
|
|
51
|
+
sections = [ render_yaml_section(parameters), "---" ] + sections
|
|
52
|
+
|
|
53
|
+
sections.compact.join("\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param instructions [String]
|
|
57
|
+
# @return [String]
|
|
58
|
+
def render_instructions_section(instructions)
|
|
59
|
+
"## Instructions\n#{instructions}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Renders conversation messages with role labels.
|
|
63
|
+
#
|
|
64
|
+
# @param messages [Array<Hash>]
|
|
65
|
+
# @return [String]
|
|
66
|
+
def render_messages_section(messages)
|
|
67
|
+
return "" if messages.nil? || messages.empty?
|
|
68
|
+
|
|
69
|
+
content = +"## Messages\n\n"
|
|
70
|
+
|
|
71
|
+
Array(messages).each_with_index do |message, index|
|
|
72
|
+
content << render_single_message(message, index + 1)
|
|
73
|
+
content << "\n\n" unless index == messages.size - 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
content
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param message [Hash]
|
|
80
|
+
# @param index [Integer] 1-based message number
|
|
81
|
+
# @return [String]
|
|
82
|
+
def render_single_message(message, index)
|
|
83
|
+
role = (message.is_a?(Hash) && message[:role]) || "user"
|
|
84
|
+
content = extract_message_content(message)
|
|
85
|
+
|
|
86
|
+
"### Message #{index} (#{role.capitalize})\n#{content}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Renders available tools with descriptions and parameter schemas.
|
|
90
|
+
#
|
|
91
|
+
# @param tools [Array<Hash>]
|
|
92
|
+
# @return [String]
|
|
93
|
+
def render_tools_section(tools)
|
|
94
|
+
return "" if tools.nil? || tools.empty?
|
|
95
|
+
|
|
96
|
+
content = +"## Tools\n\n"
|
|
97
|
+
|
|
98
|
+
tools.each_with_index do |tool, index|
|
|
99
|
+
content << "### #{tool[:name] || "Tool #{index + 1}"}\n"
|
|
100
|
+
content << "**Description:** #{tool[:description] || 'No description'}\n\n"
|
|
101
|
+
|
|
102
|
+
if tool[:parameters]
|
|
103
|
+
content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool[:parameters])}\n```\n\n"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
content.chomp
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Extracts text content from various message formats.
|
|
111
|
+
#
|
|
112
|
+
# Handles string messages, hash messages with :content key, and
|
|
113
|
+
# array content blocks (extracting only text-type blocks).
|
|
114
|
+
#
|
|
115
|
+
# @param message [Hash, String, nil]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def extract_message_content(message)
|
|
118
|
+
return "" if message.nil?
|
|
119
|
+
|
|
120
|
+
case message
|
|
121
|
+
when String
|
|
122
|
+
message
|
|
123
|
+
when Hash
|
|
124
|
+
content = message[:content]
|
|
125
|
+
case content
|
|
126
|
+
when String
|
|
127
|
+
content
|
|
128
|
+
when Array
|
|
129
|
+
content
|
|
130
|
+
.select { |block| block.is_a?(Hash) && block[:type] == "text" }
|
|
131
|
+
.map { |block| block[:text] }
|
|
132
|
+
.join(" ")
|
|
133
|
+
else
|
|
134
|
+
content.to_s
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
message.to_s
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Renders request metadata as YAML.
|
|
142
|
+
#
|
|
143
|
+
# @param parameters [Hash]
|
|
144
|
+
# @return [String]
|
|
145
|
+
def render_yaml_section(parameters)
|
|
146
|
+
parameters.to_yaml.chomp
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/log_subscriber"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
# Logs provider operations via ActiveSupport::Notifications events.
|
|
8
|
+
#
|
|
9
|
+
# Subscribes to instrumented provider events and formats them consistently.
|
|
10
|
+
# Customize by subclassing and attaching your subscriber, or adjust log levels.
|
|
11
|
+
#
|
|
12
|
+
# @example Custom log formatting
|
|
13
|
+
# class MyLogSubscriber < ActiveAgent::Providers::LogSubscriber
|
|
14
|
+
# def prompt(event)
|
|
15
|
+
# info "🚀 #{event.payload[:provider_module]}: #{event.duration}ms"
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# ActiveAgent::Providers::LogSubscriber.detach_from :active_agent
|
|
20
|
+
# MyLogSubscriber.attach_to :active_agent
|
|
21
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
22
|
+
# self.namespace = "active_agent" # Rails 8.1
|
|
23
|
+
|
|
24
|
+
# Logs completed prompt with model, message count, token usage, and duration.
|
|
25
|
+
#
|
|
26
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def prompt(event)
|
|
29
|
+
trace_id = event.payload[:trace_id]
|
|
30
|
+
provider_module = event.payload[:provider_module]
|
|
31
|
+
model = event.payload[:model]
|
|
32
|
+
message_count = event.payload[:message_count]
|
|
33
|
+
stream = event.payload[:stream]
|
|
34
|
+
usage = event.payload[:usage]
|
|
35
|
+
finish_reason = event.payload[:finish_reason]
|
|
36
|
+
duration = event.duration.round(1)
|
|
37
|
+
|
|
38
|
+
debug do
|
|
39
|
+
parts = [ "[#{trace_id}]", "[ActiveAgent]", "[#{provider_module}]" ]
|
|
40
|
+
parts << "Prompt completed:"
|
|
41
|
+
parts << "model=#{model}" if model
|
|
42
|
+
parts << "messages=#{message_count}"
|
|
43
|
+
parts << "stream=#{stream}"
|
|
44
|
+
|
|
45
|
+
if usage
|
|
46
|
+
tokens = "tokens=#{usage[:input_tokens]}/#{usage[:output_tokens]}"
|
|
47
|
+
tokens += " (cached: #{usage[:cached_tokens]})" if usage[:cached_tokens]&.positive?
|
|
48
|
+
tokens += " (reasoning: #{usage[:reasoning_tokens]})" if usage[:reasoning_tokens]&.positive?
|
|
49
|
+
parts << tokens
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
parts << "finish=#{finish_reason}" if finish_reason
|
|
53
|
+
parts << "#{duration}ms"
|
|
54
|
+
|
|
55
|
+
parts.join(" ")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
# event_log_level :prompt, :debug # Rails 8.1
|
|
59
|
+
|
|
60
|
+
# Logs completed embedding with model, input size, and token usage.
|
|
61
|
+
#
|
|
62
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
63
|
+
# @return [void]
|
|
64
|
+
def embed(event)
|
|
65
|
+
trace_id = event.payload[:trace_id]
|
|
66
|
+
provider_module = event.payload[:provider_module]
|
|
67
|
+
model = event.payload[:model]
|
|
68
|
+
input_size = event.payload[:input_size]
|
|
69
|
+
embedding_count = event.payload[:embedding_count]
|
|
70
|
+
usage = event.payload[:usage]
|
|
71
|
+
duration = event.duration.round(1)
|
|
72
|
+
|
|
73
|
+
debug do
|
|
74
|
+
parts = [ "[#{trace_id}]", "[ActiveAgent]", "[#{provider_module}]" ]
|
|
75
|
+
parts << "Embed completed:"
|
|
76
|
+
parts << "model=#{model}" if model
|
|
77
|
+
parts << "inputs=#{input_size}" if input_size
|
|
78
|
+
parts << "embeddings=#{embedding_count}" if embedding_count
|
|
79
|
+
parts << "tokens=#{usage[:input_tokens]}" if usage
|
|
80
|
+
parts << "#{duration}ms"
|
|
81
|
+
|
|
82
|
+
parts.join(" ")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
# event_log_level :embed, :debug # Rails 8.1
|
|
86
|
+
|
|
87
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
88
|
+
# @return [void]
|
|
89
|
+
def stream_open(event)
|
|
90
|
+
trace_id = event.payload[:trace_id]
|
|
91
|
+
provider_module = event.payload[:provider_module]
|
|
92
|
+
|
|
93
|
+
debug do
|
|
94
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Opening stream"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
# event_log_level :stream_open, :debug # Rails 8.1
|
|
98
|
+
|
|
99
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
100
|
+
# @return [void]
|
|
101
|
+
def stream_close(event)
|
|
102
|
+
trace_id = event.payload[:trace_id]
|
|
103
|
+
provider_module = event.payload[:provider_module]
|
|
104
|
+
|
|
105
|
+
debug do
|
|
106
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Closing stream"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
# event_log_level :stream_close, :debug # Rails 8.1
|
|
110
|
+
|
|
111
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
112
|
+
# @return [void]
|
|
113
|
+
def tool_call(event)
|
|
114
|
+
trace_id = event.payload[:trace_id]
|
|
115
|
+
provider_module = event.payload[:provider_module]
|
|
116
|
+
tool_name = event.payload[:tool_name]
|
|
117
|
+
duration = event.duration.round(1)
|
|
118
|
+
|
|
119
|
+
debug do
|
|
120
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Tool call: #{tool_name} (#{duration}ms)"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
# event_log_level :tool_call, :debug # Rails 8.1
|
|
124
|
+
|
|
125
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
126
|
+
# @return [void]
|
|
127
|
+
def stream_chunk(event)
|
|
128
|
+
trace_id = event.payload[:trace_id]
|
|
129
|
+
provider_module = event.payload[:provider_module]
|
|
130
|
+
chunk_type = event.payload[:chunk_type]
|
|
131
|
+
|
|
132
|
+
debug do
|
|
133
|
+
if chunk_type
|
|
134
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Stream chunk: #{chunk_type}"
|
|
135
|
+
else
|
|
136
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Stream chunk"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
# event_log_level :stream_chunk, :debug # Rails 8.1
|
|
141
|
+
|
|
142
|
+
# Logs connection failures with service URI and error details.
|
|
143
|
+
#
|
|
144
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
145
|
+
# @return [void]
|
|
146
|
+
def connection_error(event)
|
|
147
|
+
trace_id = event.payload[:trace_id]
|
|
148
|
+
provider_module = event.payload[:provider_module]
|
|
149
|
+
uri_base = event.payload[:uri_base]
|
|
150
|
+
exception = event.payload[:exception]
|
|
151
|
+
message = event.payload[:message]
|
|
152
|
+
|
|
153
|
+
debug do
|
|
154
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Unable to connect to #{uri_base}. Please ensure the service is running. Error: #{exception} - #{message}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
# event_log_level :connection_error, :debug # Rails 8.1
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# @return [Logger]
|
|
162
|
+
def logger
|
|
163
|
+
ActiveAgent::Base.logger
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# region log_subscriber_attach
|
|
170
|
+
# Subscribe to both top-level (.active_agent) and provider-level (.provider.active_agent) events
|
|
171
|
+
ActiveAgent::Providers::LogSubscriber.attach_to :active_agent
|
|
172
|
+
ActiveAgent::Providers::LogSubscriber.attach_to :"provider.active_agent"
|
|
173
|
+
# endregion log_subscriber_attach
|
|
174
|
+
|
|
175
|
+
# Rails 8.1
|
|
176
|
+
# ActiveSupport.event_reporter.subscribe(
|
|
177
|
+
# ActiveAgent::LogSubscriber.new, &ActiveAgent::LogSubscriber.subscription_filter
|
|
178
|
+
# )
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "options"
|
|
4
|
+
require_relative "request"
|
|
5
|
+
require_relative "embedding_request"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Providers
|
|
9
|
+
module Mock
|
|
10
|
+
# Type for Request model
|
|
11
|
+
class RequestType < ActiveModel::Type::Value
|
|
12
|
+
def cast(value)
|
|
13
|
+
case value
|
|
14
|
+
when Request
|
|
15
|
+
value
|
|
16
|
+
when Hash
|
|
17
|
+
Request.new(**value.deep_symbolize_keys)
|
|
18
|
+
when nil
|
|
19
|
+
nil
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "Cannot cast #{value.class} to Request"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def serialize(value)
|
|
26
|
+
case value
|
|
27
|
+
when Request
|
|
28
|
+
value.serialize
|
|
29
|
+
when Hash
|
|
30
|
+
value
|
|
31
|
+
when nil
|
|
32
|
+
nil
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deserialize(value)
|
|
39
|
+
cast(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Type for EmbeddingRequest model
|
|
44
|
+
class EmbeddingRequestType < ActiveModel::Type::Value
|
|
45
|
+
def cast(value)
|
|
46
|
+
case value
|
|
47
|
+
when EmbeddingRequest
|
|
48
|
+
value
|
|
49
|
+
when Hash
|
|
50
|
+
EmbeddingRequest.new(**value.deep_symbolize_keys)
|
|
51
|
+
when nil
|
|
52
|
+
nil
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "Cannot cast #{value.class} to EmbeddingRequest"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def serialize(value)
|
|
59
|
+
case value
|
|
60
|
+
when EmbeddingRequest
|
|
61
|
+
value.serialize
|
|
62
|
+
when Hash
|
|
63
|
+
value
|
|
64
|
+
when nil
|
|
65
|
+
nil
|
|
66
|
+
else
|
|
67
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def deserialize(value)
|
|
72
|
+
cast(value)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/model"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Mock
|
|
8
|
+
# Embedding request model for Mock provider.
|
|
9
|
+
class EmbeddingRequest < Common::BaseModel
|
|
10
|
+
attribute :model, :string, default: "mock-embedding-model"
|
|
11
|
+
attribute :input # String or array of strings to embed
|
|
12
|
+
attribute :encoding_format, :string
|
|
13
|
+
attribute :dimensions, :integer
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|