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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +240 -2
  3. data/README.md +15 -24
  4. data/lib/active_agent/base.rb +389 -39
  5. data/lib/active_agent/concerns/callbacks.rb +251 -0
  6. data/lib/active_agent/concerns/observers.rb +147 -0
  7. data/lib/active_agent/concerns/parameterized.rb +292 -0
  8. data/lib/active_agent/concerns/provider.rb +120 -0
  9. data/lib/active_agent/concerns/queueing.rb +36 -0
  10. data/lib/active_agent/concerns/rescue.rb +64 -0
  11. data/lib/active_agent/concerns/streaming.rb +282 -0
  12. data/lib/active_agent/concerns/tooling.rb +23 -0
  13. data/lib/active_agent/concerns/view.rb +150 -0
  14. data/lib/active_agent/configuration.rb +442 -20
  15. data/lib/active_agent/generation.rb +141 -47
  16. data/lib/active_agent/providers/_base_provider.rb +420 -0
  17. data/lib/active_agent/providers/anthropic/_types.rb +63 -0
  18. data/lib/active_agent/providers/anthropic/options.rb +53 -0
  19. data/lib/active_agent/providers/anthropic/request.rb +163 -0
  20. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  21. data/lib/active_agent/providers/anthropic_provider.rb +254 -0
  22. data/lib/active_agent/providers/common/messages/_types.rb +160 -0
  23. data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
  24. data/lib/active_agent/providers/common/messages/base.rb +17 -0
  25. data/lib/active_agent/providers/common/messages/system.rb +20 -0
  26. data/lib/active_agent/providers/common/messages/tool.rb +21 -0
  27. data/lib/active_agent/providers/common/messages/user.rb +20 -0
  28. data/lib/active_agent/providers/common/model.rb +361 -0
  29. data/lib/active_agent/providers/common/response.rb +13 -0
  30. data/lib/active_agent/providers/common/responses/_types.rb +51 -0
  31. data/lib/active_agent/providers/common/responses/base.rb +199 -0
  32. data/lib/active_agent/providers/common/responses/embed.rb +33 -0
  33. data/lib/active_agent/providers/common/responses/format.rb +31 -0
  34. data/lib/active_agent/providers/common/responses/message.rb +3 -0
  35. data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
  36. data/lib/active_agent/providers/common/usage.rb +385 -0
  37. data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
  38. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  39. data/lib/active_agent/providers/concerns/previewable.rb +150 -0
  40. data/lib/active_agent/providers/log_subscriber.rb +178 -0
  41. data/lib/active_agent/providers/mock/_types.rb +77 -0
  42. data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
  43. data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
  44. data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
  45. data/lib/active_agent/providers/mock/messages/base.rb +63 -0
  46. data/lib/active_agent/providers/mock/messages/user.rb +18 -0
  47. data/lib/active_agent/providers/mock/options.rb +30 -0
  48. data/lib/active_agent/providers/mock/request.rb +38 -0
  49. data/lib/active_agent/providers/mock_provider.rb +311 -0
  50. data/lib/active_agent/providers/ollama/_types.rb +5 -0
  51. data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
  52. data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
  53. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  54. data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
  55. data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
  56. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  57. data/lib/active_agent/providers/ollama/options.rb +27 -0
  58. data/lib/active_agent/providers/ollama_provider.rb +94 -0
  59. data/lib/active_agent/providers/open_ai/_base.rb +59 -0
  60. data/lib/active_agent/providers/open_ai/_types.rb +5 -0
  61. data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
  62. data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
  63. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  64. data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
  65. data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
  66. data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
  67. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  68. data/lib/active_agent/providers/open_ai/options.rb +74 -0
  69. data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
  70. data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
  71. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  72. data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
  73. data/lib/active_agent/providers/open_ai_provider.rb +94 -0
  74. data/lib/active_agent/providers/open_router/_types.rb +71 -0
  75. data/lib/active_agent/providers/open_router/options.rb +141 -0
  76. data/lib/active_agent/providers/open_router/request.rb +249 -0
  77. data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
  78. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
  79. data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
  80. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
  81. data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
  82. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
  83. data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
  84. data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
  85. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
  86. data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
  87. data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
  88. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
  89. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
  90. data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
  91. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  92. data/lib/active_agent/providers/open_router_provider.rb +62 -0
  93. data/lib/active_agent/providers/openai_provider.rb +2 -0
  94. data/lib/active_agent/providers/openrouter_provider.rb +2 -0
  95. data/lib/active_agent/railtie.rb +8 -6
  96. data/lib/active_agent/schema_generator.rb +333 -166
  97. data/lib/active_agent/version.rb +1 -1
  98. data/lib/active_agent.rb +112 -36
  99. data/lib/generators/active_agent/agent/USAGE +78 -0
  100. data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
  101. data/lib/generators/active_agent/install/USAGE +25 -0
  102. data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
  103. data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
  104. data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
  105. data/lib/generators/erb/agent_generator.rb +31 -16
  106. data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
  107. data/lib/generators/erb/templates/instructions.md.tt +3 -0
  108. data/lib/generators/erb/templates/instructions.text.tt +1 -0
  109. data/lib/generators/erb/templates/message.md.erb.tt +5 -0
  110. data/lib/generators/erb/templates/schema.json.tt +10 -0
  111. data/lib/generators/test_unit/agent_generator.rb +1 -1
  112. data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
  113. metadata +182 -71
  114. data/lib/active_agent/action_prompt/action.rb +0 -13
  115. data/lib/active_agent/action_prompt/base.rb +0 -623
  116. data/lib/active_agent/action_prompt/message.rb +0 -126
  117. data/lib/active_agent/action_prompt/prompt.rb +0 -136
  118. data/lib/active_agent/action_prompt.rb +0 -19
  119. data/lib/active_agent/callbacks.rb +0 -33
  120. data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
  121. data/lib/active_agent/generation_provider/base.rb +0 -55
  122. data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
  123. data/lib/active_agent/generation_provider/error_handling.rb +0 -167
  124. data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
  125. data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
  126. data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
  127. data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
  128. data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
  129. data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
  130. data/lib/active_agent/generation_provider/response.rb +0 -75
  131. data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
  132. data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
  133. data/lib/active_agent/generation_provider/tool_management.rb +0 -142
  134. data/lib/active_agent/generation_provider.rb +0 -67
  135. data/lib/active_agent/log_subscriber.rb +0 -44
  136. data/lib/active_agent/parameterized.rb +0 -75
  137. data/lib/active_agent/prompt_helper.rb +0 -19
  138. data/lib/active_agent/queued_generation.rb +0 -12
  139. data/lib/active_agent/rescuable.rb +0 -34
  140. data/lib/active_agent/sanitizers.rb +0 -40
  141. data/lib/active_agent/streaming.rb +0 -34
  142. data/lib/active_agent/test_case.rb +0 -125
  143. data/lib/generators/USAGE +0 -47
  144. data/lib/generators/active_agent/USAGE +0 -56
  145. data/lib/generators/erb/install_generator.rb +0 -44
  146. data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
  147. data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
  148. data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
  149. data/lib/generators/erb/templates/view.html.erb.tt +0 -5
  150. data/lib/generators/erb/templates/view.json.erb.tt +0 -16
  151. /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
  152. /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