activeagent 1.0.1 → 1.0.2
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 +10 -4
- data/lib/active_agent/base.rb +3 -2
- data/lib/active_agent/concerns/provider.rb +6 -2
- data/lib/active_agent/concerns/rescue.rb +39 -0
- data/lib/active_agent/concerns/streaming.rb +2 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
- data/lib/active_agent/dashboard/config/routes.rb +78 -0
- data/lib/active_agent/dashboard/engine.rb +39 -0
- data/lib/active_agent/dashboard.rb +151 -0
- data/lib/active_agent/providers/_base_provider.rb +2 -1
- data/lib/active_agent/providers/anthropic_provider.rb +14 -4
- data/lib/active_agent/providers/azure/_types.rb +5 -0
- data/lib/active_agent/providers/azure/options.rb +111 -0
- data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_provider.rb +133 -0
- data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
- data/lib/active_agent/providers/bedrock/_types.rb +8 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
- data/lib/active_agent/providers/bedrock/options.rb +77 -0
- data/lib/active_agent/providers/bedrock_provider.rb +84 -0
- data/lib/active_agent/providers/common/messages/_types.rb +6 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
- data/lib/active_agent/providers/gemini/_types.rb +19 -0
- data/lib/active_agent/providers/gemini/options.rb +41 -0
- data/lib/active_agent/providers/gemini_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
- data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
- data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
- data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
- data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
- data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
- data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
- data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
- data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
- data/lib/active_agent/railtie.rb +32 -1
- data/lib/active_agent/telemetry/configuration.rb +213 -0
- data/lib/active_agent/telemetry/instrumentation.rb +155 -0
- data/lib/active_agent/telemetry/reporter.rb +176 -0
- data/lib/active_agent/telemetry/span.rb +267 -0
- data/lib/active_agent/telemetry/tracer.rb +184 -0
- data/lib/active_agent/telemetry.rb +162 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
- metadata +99 -13
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_base_provider"
|
|
4
|
+
|
|
5
|
+
require_gem!(:ruby_llm, __FILE__) unless defined?(::RubyLLM)
|
|
6
|
+
|
|
7
|
+
require_relative "ruby_llm/_types"
|
|
8
|
+
require_relative "ruby_llm/tool_proxy"
|
|
9
|
+
|
|
10
|
+
module ActiveAgent
|
|
11
|
+
module Providers
|
|
12
|
+
# Provider for RubyLLM's unified API, supporting 15+ LLM providers
|
|
13
|
+
# (OpenAI, Anthropic, Gemini, Bedrock, Azure, Ollama, etc.).
|
|
14
|
+
#
|
|
15
|
+
# Uses RubyLLM's provider-level API (provider.complete()) rather than
|
|
16
|
+
# the high-level Chat object to avoid conflicts with ActiveAgent's own
|
|
17
|
+
# conversation management and tool execution loop.
|
|
18
|
+
#
|
|
19
|
+
# @see BaseProvider
|
|
20
|
+
class RubyLLMProvider < BaseProvider
|
|
21
|
+
# @return [RubyLLM::EmbeddingRequestType] embedding request type
|
|
22
|
+
def self.embed_request_type
|
|
23
|
+
RubyLLM::EmbeddingRequestType.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
# Clears tool_choice between turns to prevent infinite tool-calling loops.
|
|
29
|
+
def prepare_prompt_request
|
|
30
|
+
prepare_prompt_request_tools
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Executes a prompt request via RubyLLM's provider-level API.
|
|
35
|
+
#
|
|
36
|
+
# Resolves the appropriate provider from the model ID, converts
|
|
37
|
+
# ActiveAgent messages/tools to RubyLLM format, and calls
|
|
38
|
+
# provider.complete().
|
|
39
|
+
#
|
|
40
|
+
# @param parameters [Hash] serialized request parameters
|
|
41
|
+
# @return [Hash, nil] normalized API response hash, or nil for streaming
|
|
42
|
+
def api_prompt_execute(parameters)
|
|
43
|
+
@resolved_model_id = parameters[:model] || options.model
|
|
44
|
+
resolve_ruby_llm_provider!(@resolved_model_id)
|
|
45
|
+
|
|
46
|
+
# Convert messages to RubyLLM format
|
|
47
|
+
messages = build_ruby_llm_messages(parameters)
|
|
48
|
+
|
|
49
|
+
# Convert tools to RubyLLM format
|
|
50
|
+
tools = build_ruby_llm_tools(parameters[:tools])
|
|
51
|
+
|
|
52
|
+
# Build kwargs for provider.complete (tools, temperature, model are required)
|
|
53
|
+
kwargs = {
|
|
54
|
+
model: @ruby_llm_model,
|
|
55
|
+
tools: tools || {},
|
|
56
|
+
temperature: parameters[:temperature]
|
|
57
|
+
}
|
|
58
|
+
kwargs[:schema] = parameters[:response_format] if parameters[:response_format]
|
|
59
|
+
|
|
60
|
+
# Pass extra params (max_tokens, etc.) via RubyLLM's params: deep-merge
|
|
61
|
+
max_tokens = parameters[:max_tokens] || options.max_tokens
|
|
62
|
+
if max_tokens
|
|
63
|
+
kwargs[:params] = { max_tokens: max_tokens }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if parameters[:stream]
|
|
67
|
+
stream_proc = parameters[:stream]
|
|
68
|
+
|
|
69
|
+
# For streaming, pass a block that forwards chunks
|
|
70
|
+
@ruby_llm_provider.complete(messages, **kwargs) do |chunk|
|
|
71
|
+
stream_proc.call(chunk)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
nil
|
|
75
|
+
else
|
|
76
|
+
response = @ruby_llm_provider.complete(messages, **kwargs)
|
|
77
|
+
normalize_ruby_llm_response(response, @resolved_model_id)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Executes an embedding request via RubyLLM.
|
|
82
|
+
#
|
|
83
|
+
# @param parameters [Hash] serialized embedding request parameters
|
|
84
|
+
# @return [Hash] normalized embedding response with symbol keys
|
|
85
|
+
def api_embed_execute(parameters)
|
|
86
|
+
model_id = parameters[:model] || options.model
|
|
87
|
+
resolve_ruby_llm_provider!(model_id)
|
|
88
|
+
|
|
89
|
+
input = parameters[:input]
|
|
90
|
+
inputs = input.is_a?(Array) ? input : [ input ]
|
|
91
|
+
|
|
92
|
+
data = inputs.map.with_index do |text, index|
|
|
93
|
+
embedding = @ruby_llm_provider.embed(text, model: model_id, dimensions: parameters[:dimensions])
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
object: "embedding",
|
|
97
|
+
index: index,
|
|
98
|
+
embedding: embedding.vectors
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
object: :list,
|
|
104
|
+
data: data,
|
|
105
|
+
model: model_id
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Processes streaming chunks from RubyLLM.
|
|
110
|
+
#
|
|
111
|
+
# Handles RubyLLM::Chunk objects, building up the message in message_stack.
|
|
112
|
+
#
|
|
113
|
+
# @param chunk [RubyLLM::Chunk] streaming chunk
|
|
114
|
+
# @return [void]
|
|
115
|
+
def process_stream_chunk(chunk)
|
|
116
|
+
instrument("stream_chunk.active_agent")
|
|
117
|
+
|
|
118
|
+
broadcast_stream_open
|
|
119
|
+
|
|
120
|
+
if message_stack.empty? || !message_stack.last.is_a?(Hash) || message_stack.last[:role] != "assistant"
|
|
121
|
+
message_stack.push({ role: "assistant", content: "" })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
message = message_stack.last
|
|
125
|
+
|
|
126
|
+
# Append content delta
|
|
127
|
+
if chunk.content
|
|
128
|
+
message[:content] ||= ""
|
|
129
|
+
message[:content] += chunk.content
|
|
130
|
+
broadcast_stream_update(message, chunk.content)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Handle tool calls in chunk
|
|
134
|
+
if chunk.tool_calls&.any?
|
|
135
|
+
message[:tool_calls] ||= []
|
|
136
|
+
chunk.tool_calls.each do |_id, tool_call|
|
|
137
|
+
existing = message[:tool_calls].find { |tc| tc[:id] == tool_call.id }
|
|
138
|
+
if existing
|
|
139
|
+
existing[:function][:arguments] += tool_call.arguments.to_s if tool_call.arguments
|
|
140
|
+
else
|
|
141
|
+
message[:tool_calls] << {
|
|
142
|
+
id: tool_call.id,
|
|
143
|
+
type: "function",
|
|
144
|
+
function: {
|
|
145
|
+
name: tool_call.name,
|
|
146
|
+
arguments: tool_call.arguments.to_s
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Stream completion is handled by the base provider after
|
|
154
|
+
# api_prompt_execute returns nil. No action needed here.
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Extracts messages from the completed API response.
|
|
158
|
+
#
|
|
159
|
+
# @param api_response [Hash, nil] normalized response hash
|
|
160
|
+
# @return [Array<Hash>, nil]
|
|
161
|
+
def process_prompt_finished_extract_messages(api_response)
|
|
162
|
+
return nil unless api_response
|
|
163
|
+
[ api_response ]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Extracts tool/function calls from the last message in the stack.
|
|
167
|
+
#
|
|
168
|
+
# Converts RubyLLM's tool_calls format to ActiveAgent's expected format
|
|
169
|
+
# with parsed JSON arguments.
|
|
170
|
+
#
|
|
171
|
+
# @return [Array<Hash>, nil] tool calls or nil
|
|
172
|
+
def process_prompt_finished_extract_function_calls
|
|
173
|
+
last_message = message_stack.last
|
|
174
|
+
return nil unless last_message.is_a?(Hash)
|
|
175
|
+
|
|
176
|
+
tool_calls = last_message[:tool_calls]
|
|
177
|
+
return nil unless tool_calls&.any?
|
|
178
|
+
|
|
179
|
+
tool_calls.map do |tc|
|
|
180
|
+
args = tc.dig(:function, :arguments)
|
|
181
|
+
parsed_args = if args.is_a?(String) && args.present?
|
|
182
|
+
JSON.parse(args, symbolize_names: true)
|
|
183
|
+
elsif args.is_a?(Hash)
|
|
184
|
+
args.deep_symbolize_keys
|
|
185
|
+
else
|
|
186
|
+
{}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
id: tc[:id],
|
|
191
|
+
name: tc.dig(:function, :name),
|
|
192
|
+
input: parsed_args
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Extracts function names from tool_calls in assistant messages on the stack.
|
|
198
|
+
#
|
|
199
|
+
# @return [Array<String>]
|
|
200
|
+
def extract_used_function_names
|
|
201
|
+
message_stack
|
|
202
|
+
.select { |msg| msg[:role] == "assistant" && msg[:tool_calls] }
|
|
203
|
+
.flat_map { |msg| msg[:tool_calls] }
|
|
204
|
+
.map { |tc| tc.dig(:function, :name) }
|
|
205
|
+
.compact
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Returns true if tool_choice forces any tool to be used.
|
|
209
|
+
#
|
|
210
|
+
# Handles both string ("required") and hash ({name: "..."}) formats.
|
|
211
|
+
#
|
|
212
|
+
# @return [Boolean]
|
|
213
|
+
def tool_choice_forces_required?
|
|
214
|
+
request.tool_choice == "required"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Returns [true, name] if tool_choice forces a specific tool.
|
|
218
|
+
#
|
|
219
|
+
# @return [Array<Boolean, String|nil>]
|
|
220
|
+
def tool_choice_forces_specific?
|
|
221
|
+
if request.tool_choice.is_a?(Hash)
|
|
222
|
+
[ true, request.tool_choice[:name] ]
|
|
223
|
+
else
|
|
224
|
+
[ false, nil ]
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Executes tool calls and pushes results to message_stack.
|
|
229
|
+
#
|
|
230
|
+
# @param tool_calls [Array<Hash>] with :id, :name, :input keys
|
|
231
|
+
# @return [void]
|
|
232
|
+
def process_function_calls(tool_calls)
|
|
233
|
+
tool_calls.each do |tool_call|
|
|
234
|
+
content = instrument("tool_call.active_agent", tool_name: tool_call[:name]) do
|
|
235
|
+
tools_function.call(tool_call[:name], **tool_call[:input])
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
message_stack.push({
|
|
239
|
+
role: "tool",
|
|
240
|
+
tool_call_id: tool_call[:id],
|
|
241
|
+
content: content.to_json
|
|
242
|
+
})
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# api_prompt_execute always returns a normalized Hash or nil (streaming),
|
|
247
|
+
# so no additional normalization is needed for instrumentation.
|
|
248
|
+
# Inherits default api_response_normalize from BaseProvider.
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
# Resolves and caches the RubyLLM provider for the given model.
|
|
253
|
+
#
|
|
254
|
+
# Reuses the cached provider if the model hasn't changed (e.g., during
|
|
255
|
+
# multi-turn tool calling loops).
|
|
256
|
+
#
|
|
257
|
+
# @param model_id [String] model identifier
|
|
258
|
+
# @return [void]
|
|
259
|
+
def resolve_ruby_llm_provider!(model_id)
|
|
260
|
+
return if @ruby_llm_provider && @cached_model_id == model_id
|
|
261
|
+
|
|
262
|
+
@cached_model_id = model_id
|
|
263
|
+
@ruby_llm_model, @ruby_llm_provider = ::RubyLLM::Models.resolve(model_id, config: ::RubyLLM.config)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Converts ActiveAgent messages to RubyLLM message format.
|
|
267
|
+
#
|
|
268
|
+
# Prepends system instructions as the first message if present.
|
|
269
|
+
#
|
|
270
|
+
# @param parameters [Hash] request parameters
|
|
271
|
+
# @return [Array<Hash>] RubyLLM-formatted messages
|
|
272
|
+
def build_ruby_llm_messages(parameters)
|
|
273
|
+
messages = []
|
|
274
|
+
|
|
275
|
+
# Add system instructions
|
|
276
|
+
if parameters[:instructions].present?
|
|
277
|
+
messages << ::RubyLLM::Message.new(
|
|
278
|
+
role: :system,
|
|
279
|
+
content: parameters[:instructions]
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Convert each message
|
|
284
|
+
(parameters[:messages] || []).each do |msg|
|
|
285
|
+
ruby_llm_msg = if msg[:tool_call_id]
|
|
286
|
+
::RubyLLM::Message.new(
|
|
287
|
+
role: :tool,
|
|
288
|
+
content: msg[:content].to_s,
|
|
289
|
+
tool_call_id: msg[:tool_call_id]
|
|
290
|
+
)
|
|
291
|
+
else
|
|
292
|
+
attrs = {
|
|
293
|
+
role: msg[:role].to_sym,
|
|
294
|
+
content: extract_content_text(msg[:content])
|
|
295
|
+
}
|
|
296
|
+
attrs[:tool_calls] = convert_tool_calls_for_ruby_llm(msg[:tool_calls]) if msg[:tool_calls]
|
|
297
|
+
::RubyLLM::Message.new(**attrs)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
messages << ruby_llm_msg
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
messages
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Extracts plain text from various content formats.
|
|
307
|
+
#
|
|
308
|
+
# @param content [String, Array, Object] message content
|
|
309
|
+
# @return [String]
|
|
310
|
+
def extract_content_text(content)
|
|
311
|
+
case content
|
|
312
|
+
when String
|
|
313
|
+
content
|
|
314
|
+
when Array
|
|
315
|
+
content.select { |block| block.is_a?(Hash) && block[:type] == "text" }
|
|
316
|
+
.map { |block| block[:text] }
|
|
317
|
+
.join("\n")
|
|
318
|
+
else
|
|
319
|
+
content.to_s
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Converts ActiveAgent tool_calls to RubyLLM's ToolCall format.
|
|
324
|
+
#
|
|
325
|
+
# @param tool_calls [Array<Hash>] ActiveAgent format tool calls
|
|
326
|
+
# @return [Hash] RubyLLM format { id => ToolCall }
|
|
327
|
+
def convert_tool_calls_for_ruby_llm(tool_calls)
|
|
328
|
+
return nil unless tool_calls
|
|
329
|
+
|
|
330
|
+
tool_calls.each_with_object({}) do |tc, hash|
|
|
331
|
+
id = tc[:id]
|
|
332
|
+
call = ::RubyLLM::ToolCall.new(
|
|
333
|
+
id: id,
|
|
334
|
+
name: tc.dig(:function, :name) || tc[:name],
|
|
335
|
+
arguments: tc.dig(:function, :arguments) || tc[:input]&.to_json || "{}"
|
|
336
|
+
)
|
|
337
|
+
hash[id] = call
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Converts ActiveAgent tool definitions to RubyLLM ToolProxy objects.
|
|
342
|
+
#
|
|
343
|
+
# @param tools [Array<Hash>, nil] ActiveAgent tool definitions
|
|
344
|
+
# @return [Hash, nil] { "name" => ToolProxy }
|
|
345
|
+
def build_ruby_llm_tools(tools)
|
|
346
|
+
return nil unless tools&.any?
|
|
347
|
+
|
|
348
|
+
tools.each_with_object({}) do |tool, hash|
|
|
349
|
+
func = tool[:function] || tool
|
|
350
|
+
proxy = RubyLLM::ToolProxy.new(
|
|
351
|
+
name: func[:name],
|
|
352
|
+
description: func[:description] || "",
|
|
353
|
+
parameters: func[:parameters] || {}
|
|
354
|
+
)
|
|
355
|
+
hash[proxy.name] = proxy
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Converts a RubyLLM::Message response to a normalized hash.
|
|
360
|
+
#
|
|
361
|
+
# @param response [RubyLLM::Message] the response message
|
|
362
|
+
# @param model_id [String, nil] the model used
|
|
363
|
+
# @return [Hash] normalized response hash
|
|
364
|
+
def normalize_ruby_llm_response(response, model_id)
|
|
365
|
+
hash = {
|
|
366
|
+
role: "assistant",
|
|
367
|
+
content: response.content.to_s
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Handle tool calls
|
|
371
|
+
if response.tool_calls&.any?
|
|
372
|
+
hash[:tool_calls] = response.tool_calls.map do |id, tc|
|
|
373
|
+
{
|
|
374
|
+
id: id,
|
|
375
|
+
type: "function",
|
|
376
|
+
function: {
|
|
377
|
+
name: tc.name,
|
|
378
|
+
arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Add stop_reason if available
|
|
385
|
+
if response.respond_to?(:stop_reason) && response.stop_reason
|
|
386
|
+
hash[:stop_reason] = response.stop_reason
|
|
387
|
+
elsif response.tool_calls&.any?
|
|
388
|
+
hash[:stop_reason] = "tool_use"
|
|
389
|
+
else
|
|
390
|
+
hash[:stop_reason] = "end_turn"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Add usage info if available
|
|
394
|
+
if response.respond_to?(:input_tokens) && response.input_tokens
|
|
395
|
+
hash[:usage] = {
|
|
396
|
+
input_tokens: response.input_tokens,
|
|
397
|
+
output_tokens: response.output_tokens
|
|
398
|
+
}
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
hash[:model] = model_id if model_id
|
|
402
|
+
|
|
403
|
+
hash
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
data/lib/active_agent/railtie.rb
CHANGED
|
@@ -10,6 +10,7 @@ module ActiveAgent
|
|
|
10
10
|
class Railtie < Rails::Railtie # :nodoc:
|
|
11
11
|
config.active_agent = ActiveSupport::OrderedOptions.new
|
|
12
12
|
config.active_agent.preview_paths = []
|
|
13
|
+
config.active_agent.telemetry = ActiveSupport::OrderedOptions.new
|
|
13
14
|
config.eager_load_namespaces << ActiveAgent
|
|
14
15
|
|
|
15
16
|
initializer "active_agent.deprecator", before: :load_environment_config do |app|
|
|
@@ -40,6 +41,35 @@ module ActiveAgent
|
|
|
40
41
|
ActiveAgent.configuration_load(Rails.root.join("config", "active_agent.yml"))
|
|
41
42
|
# endregion configuration_load
|
|
42
43
|
|
|
44
|
+
# region telemetry_configuration
|
|
45
|
+
# Load telemetry configuration from activeagent.yml or Rails config
|
|
46
|
+
telemetry_config = ActiveAgent.configuration[:telemetry]
|
|
47
|
+
if telemetry_config.is_a?(Hash)
|
|
48
|
+
ActiveAgent::Telemetry.configure do |config|
|
|
49
|
+
config.load_from_hash(telemetry_config)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Also support Rails config.active_agent.telemetry
|
|
54
|
+
if options.telemetry.present?
|
|
55
|
+
ActiveAgent::Telemetry.configure do |config|
|
|
56
|
+
config.enabled = options.telemetry[:enabled] if options.telemetry.key?(:enabled)
|
|
57
|
+
config.endpoint = options.telemetry[:endpoint] if options.telemetry.key?(:endpoint)
|
|
58
|
+
config.api_key = options.telemetry[:api_key] if options.telemetry.key?(:api_key)
|
|
59
|
+
config.sample_rate = options.telemetry[:sample_rate] if options.telemetry.key?(:sample_rate)
|
|
60
|
+
config.service_name = options.telemetry[:service_name] if options.telemetry.key?(:service_name)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Apply instrumentation to ActiveAgent::Base when telemetry is enabled
|
|
65
|
+
if ActiveAgent::Telemetry.enabled?
|
|
66
|
+
ActiveSupport.on_load(:active_agent) do
|
|
67
|
+
include ActiveAgent::Telemetry::Instrumentation
|
|
68
|
+
instrument_telemetry!
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
# endregion telemetry_configuration
|
|
72
|
+
|
|
43
73
|
ActiveSupport.on_load(:active_agent) do
|
|
44
74
|
include AbstractController::UrlFor
|
|
45
75
|
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
|
|
@@ -55,7 +85,8 @@ module ActiveAgent
|
|
|
55
85
|
self.generation_job = generation_job.constantize
|
|
56
86
|
end
|
|
57
87
|
|
|
58
|
-
|
|
88
|
+
# Skip telemetry config - it's handled separately above
|
|
89
|
+
options.except(:telemetry).each { |k, v| send(:"#{k}=", v) }
|
|
59
90
|
end
|
|
60
91
|
|
|
61
92
|
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Telemetry
|
|
5
|
+
# Configuration for telemetry collection and reporting.
|
|
6
|
+
#
|
|
7
|
+
# Stores settings for endpoint, authentication, sampling, and batching.
|
|
8
|
+
# Configuration can be set programmatically or loaded from YAML.
|
|
9
|
+
#
|
|
10
|
+
# @example Programmatic configuration
|
|
11
|
+
# ActiveAgent::Telemetry.configure do |config|
|
|
12
|
+
# config.enabled = true
|
|
13
|
+
# config.endpoint = "https://api.activeagents.ai/v1/traces"
|
|
14
|
+
# config.api_key = "your-api-key"
|
|
15
|
+
# config.sample_rate = 1.0
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example YAML configuration (config/activeagent.yml)
|
|
19
|
+
# telemetry:
|
|
20
|
+
# enabled: true
|
|
21
|
+
# endpoint: https://api.activeagents.ai/v1/traces
|
|
22
|
+
# api_key: <%= ENV["ACTIVEAGENTS_API_KEY"] %>
|
|
23
|
+
# sample_rate: 1.0
|
|
24
|
+
# batch_size: 100
|
|
25
|
+
# flush_interval: 5
|
|
26
|
+
#
|
|
27
|
+
class Configuration
|
|
28
|
+
# @return [Boolean] Whether telemetry is enabled (default: false)
|
|
29
|
+
attr_accessor :enabled
|
|
30
|
+
|
|
31
|
+
# @return [String] The endpoint URL for sending traces
|
|
32
|
+
attr_accessor :endpoint
|
|
33
|
+
|
|
34
|
+
# @return [String] API key for authentication
|
|
35
|
+
attr_accessor :api_key
|
|
36
|
+
|
|
37
|
+
# @return [Float] Sampling rate from 0.0 to 1.0 (default: 1.0)
|
|
38
|
+
attr_accessor :sample_rate
|
|
39
|
+
|
|
40
|
+
# @return [Integer] Number of traces to batch before sending (default: 100)
|
|
41
|
+
attr_accessor :batch_size
|
|
42
|
+
|
|
43
|
+
# @return [Integer] Seconds between automatic flushes (default: 5)
|
|
44
|
+
attr_accessor :flush_interval
|
|
45
|
+
|
|
46
|
+
# @return [Integer] HTTP timeout in seconds (default: 10)
|
|
47
|
+
attr_accessor :timeout
|
|
48
|
+
|
|
49
|
+
# @return [Boolean] Whether to capture request/response bodies (default: false)
|
|
50
|
+
attr_accessor :capture_bodies
|
|
51
|
+
|
|
52
|
+
# @return [Array<String>] Attributes to redact from traces
|
|
53
|
+
attr_accessor :redact_attributes
|
|
54
|
+
|
|
55
|
+
# @return [String] Service name for trace attribution
|
|
56
|
+
attr_accessor :service_name
|
|
57
|
+
|
|
58
|
+
# @return [String] Environment name (development, staging, production)
|
|
59
|
+
attr_accessor :environment
|
|
60
|
+
|
|
61
|
+
# @return [Hash] Additional resource attributes to include in all traces
|
|
62
|
+
attr_accessor :resource_attributes
|
|
63
|
+
|
|
64
|
+
# @return [Logger] Logger for telemetry operations
|
|
65
|
+
attr_accessor :logger
|
|
66
|
+
|
|
67
|
+
# @return [Boolean] Whether to store traces locally in the app's database
|
|
68
|
+
attr_accessor :local_storage
|
|
69
|
+
|
|
70
|
+
# Default ActiveAgents.ai endpoint for hosted observability.
|
|
71
|
+
DEFAULT_ENDPOINT = "https://api.activeagents.ai/v1/traces"
|
|
72
|
+
|
|
73
|
+
# Local dashboard endpoint path (relative to app root)
|
|
74
|
+
LOCAL_ENDPOINT_PATH = "/active_agent/api/traces"
|
|
75
|
+
|
|
76
|
+
def initialize
|
|
77
|
+
@enabled = false
|
|
78
|
+
@endpoint = DEFAULT_ENDPOINT
|
|
79
|
+
@api_key = nil
|
|
80
|
+
@sample_rate = 1.0
|
|
81
|
+
@batch_size = 100
|
|
82
|
+
@flush_interval = 5
|
|
83
|
+
@timeout = 10
|
|
84
|
+
@capture_bodies = false
|
|
85
|
+
@redact_attributes = %w[password secret token key credential api_key]
|
|
86
|
+
@service_name = nil
|
|
87
|
+
@environment = Rails.env if defined?(Rails)
|
|
88
|
+
@resource_attributes = {}
|
|
89
|
+
@logger = nil
|
|
90
|
+
@local_storage = false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns whether telemetry collection is enabled.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def enabled?
|
|
97
|
+
@enabled == true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns whether telemetry is properly configured.
|
|
101
|
+
#
|
|
102
|
+
# Checks that endpoint and api_key are present, or local_storage is enabled.
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean]
|
|
105
|
+
def configured?
|
|
106
|
+
local_storage? || (endpoint.present? && api_key.present?)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns whether local storage mode is enabled.
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def local_storage?
|
|
113
|
+
@local_storage == true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns the resolved endpoint for trace reporting.
|
|
117
|
+
#
|
|
118
|
+
# Uses local endpoint when local_storage is enabled.
|
|
119
|
+
#
|
|
120
|
+
# @return [String]
|
|
121
|
+
def resolved_endpoint
|
|
122
|
+
if local_storage?
|
|
123
|
+
LOCAL_ENDPOINT_PATH
|
|
124
|
+
else
|
|
125
|
+
endpoint
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns whether a trace should be sampled.
|
|
130
|
+
#
|
|
131
|
+
# Uses sample_rate to determine if trace should be collected.
|
|
132
|
+
#
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def should_sample?
|
|
135
|
+
return true if sample_rate >= 1.0
|
|
136
|
+
return false if sample_rate <= 0.0
|
|
137
|
+
|
|
138
|
+
rand < sample_rate
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Resolves the service name for traces.
|
|
142
|
+
#
|
|
143
|
+
# Falls back to Rails application name or "activeagent".
|
|
144
|
+
#
|
|
145
|
+
# @return [String]
|
|
146
|
+
def resolved_service_name
|
|
147
|
+
@service_name || rails_app_name || "activeagent"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns the logger for telemetry operations.
|
|
151
|
+
#
|
|
152
|
+
# Falls back to Rails.logger or a null logger.
|
|
153
|
+
#
|
|
154
|
+
# @return [Logger]
|
|
155
|
+
def resolved_logger
|
|
156
|
+
@logger || (defined?(Rails) && Rails.logger) || Logger.new(File::NULL)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Loads configuration from a hash (typically from YAML).
|
|
160
|
+
#
|
|
161
|
+
# @param hash [Hash] Configuration hash
|
|
162
|
+
# @return [self]
|
|
163
|
+
def load_from_hash(hash)
|
|
164
|
+
hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
|
|
165
|
+
|
|
166
|
+
@enabled = hash[:enabled] if hash.key?(:enabled)
|
|
167
|
+
@endpoint = hash[:endpoint] if hash.key?(:endpoint)
|
|
168
|
+
@api_key = hash[:api_key] if hash.key?(:api_key)
|
|
169
|
+
@sample_rate = hash[:sample_rate].to_f if hash.key?(:sample_rate)
|
|
170
|
+
@batch_size = hash[:batch_size].to_i if hash.key?(:batch_size)
|
|
171
|
+
@flush_interval = hash[:flush_interval].to_i if hash.key?(:flush_interval)
|
|
172
|
+
@timeout = hash[:timeout].to_i if hash.key?(:timeout)
|
|
173
|
+
@capture_bodies = hash[:capture_bodies] if hash.key?(:capture_bodies)
|
|
174
|
+
@redact_attributes = hash[:redact_attributes] if hash.key?(:redact_attributes)
|
|
175
|
+
@service_name = hash[:service_name] if hash.key?(:service_name)
|
|
176
|
+
@environment = hash[:environment] if hash.key?(:environment)
|
|
177
|
+
@resource_attributes = hash[:resource_attributes] if hash.key?(:resource_attributes)
|
|
178
|
+
@local_storage = hash[:local_storage] if hash.key?(:local_storage)
|
|
179
|
+
|
|
180
|
+
self
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns configuration as a hash for serialization.
|
|
184
|
+
#
|
|
185
|
+
# @return [Hash]
|
|
186
|
+
def to_h
|
|
187
|
+
{
|
|
188
|
+
enabled: enabled,
|
|
189
|
+
endpoint: endpoint,
|
|
190
|
+
api_key: api_key ? "[REDACTED]" : nil,
|
|
191
|
+
sample_rate: sample_rate,
|
|
192
|
+
batch_size: batch_size,
|
|
193
|
+
flush_interval: flush_interval,
|
|
194
|
+
timeout: timeout,
|
|
195
|
+
capture_bodies: capture_bodies,
|
|
196
|
+
service_name: resolved_service_name,
|
|
197
|
+
environment: environment,
|
|
198
|
+
local_storage: local_storage
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def rails_app_name
|
|
205
|
+
return nil unless defined?(Rails) && Rails.application
|
|
206
|
+
|
|
207
|
+
Rails.application.class.module_parent_name.underscore
|
|
208
|
+
rescue StandardError
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|