activeagent 1.0.0.rc1 → 1.0.1

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/lib/active_agent/providers/_base_provider.rb +94 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  6. data/lib/active_agent/providers/anthropic/request.rb +157 -78
  7. data/lib/active_agent/providers/anthropic/transforms.rb +482 -0
  8. data/lib/active_agent/providers/anthropic_provider.rb +159 -59
  9. data/lib/active_agent/providers/common/messages/_types.rb +46 -3
  10. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  11. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  12. data/lib/active_agent/providers/common/usage.rb +385 -0
  13. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  14. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  15. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  16. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  17. data/lib/active_agent/providers/mock_provider.rb +23 -23
  18. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  19. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  20. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  21. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  22. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  23. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  24. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  25. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  26. data/lib/active_agent/providers/open_ai/chat/transforms.rb +444 -0
  27. data/lib/active_agent/providers/open_ai/chat_provider.rb +95 -36
  28. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  29. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  30. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  31. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  32. data/lib/active_agent/providers/open_ai/responses/request.rb +116 -135
  33. data/lib/active_agent/providers/open_ai/responses/transforms.rb +363 -0
  34. data/lib/active_agent/providers/open_ai/responses_provider.rb +115 -30
  35. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  36. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  37. data/lib/active_agent/providers/open_router/options.rb +49 -1
  38. data/lib/active_agent/providers/open_router/request.rb +252 -66
  39. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  40. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  41. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  42. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  43. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  44. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  45. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  46. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  47. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  48. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  49. data/lib/active_agent/providers/open_router/transforms.rb +164 -0
  50. data/lib/active_agent/providers/open_router_provider.rb +23 -0
  51. data/lib/active_agent/version.rb +1 -1
  52. metadata +17 -160
  53. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  54. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  55. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  56. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  57. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  58. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  59. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  60. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  61. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  62. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  78. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  79. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  80. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  81. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  82. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  83. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  84. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  85. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  86. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  87. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  88. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  91. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  92. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  93. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  94. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  95. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  96. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  97. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  98. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  99. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  100. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  101. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  102. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  116. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  117. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  118. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  119. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  120. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  121. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  122. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  123. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  124. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  125. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  126. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  127. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  128. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  129. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  130. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  131. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  162. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  163. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  166. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  167. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  168. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  170. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  171. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  172. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  173. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  174. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  175. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  184. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  185. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  186. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  187. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  188. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  189. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  190. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  191. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -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
@@ -86,7 +86,13 @@ module ActiveAgent
86
86
  "### Message #{index} (#{role.capitalize})\n#{content}"
87
87
  end
88
88
 
89
- # Renders available tools with descriptions and parameter schemas.
89
+ # Renders tools section for preview.
90
+ #
91
+ # Handles multiple tool formats:
92
+ # - Common format: {name: "...", description: "...", parameters: {...}}
93
+ # - Anthropic format: {name: "...", description: "...", input_schema: {...}}
94
+ # - Chat API format: {type: "function", function: {name: "...", description: "...", parameters: {...}}}
95
+ # - Responses API format: {type: "function", name: "...", description: "...", parameters: {...}}
90
96
  #
91
97
  # @param tools [Array<Hash>]
92
98
  # @return [String]
@@ -96,17 +102,45 @@ module ActiveAgent
96
102
  content = +"## Tools\n\n"
97
103
 
98
104
  tools.each_with_index do |tool, index|
99
- content << "### #{tool[:name] || "Tool #{index + 1}"}\n"
100
- content << "**Description:** #{tool[:description] || 'No description'}\n\n"
105
+ # Extract name and description from different formats
106
+ tool_name, tool_description, tool_params = extract_tool_details(tool)
107
+
108
+ content << "### #{tool_name || "Tool #{index + 1}"}\n"
109
+ content << "**Description:** #{tool_description || 'No description'}\n\n"
101
110
 
102
- if tool[:parameters]
103
- content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool[:parameters])}\n```\n\n"
111
+ if tool_params
112
+ content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool_params)}\n```\n\n"
104
113
  end
105
114
  end
106
115
 
107
116
  content.chomp
108
117
  end
109
118
 
119
+ # Extracts tool details from different formats.
120
+ #
121
+ # @param tool [Hash]
122
+ # @return [Array<String, String, Hash>] [name, description, parameters]
123
+ def extract_tool_details(tool)
124
+ tool_hash = tool.is_a?(Hash) ? tool : {}
125
+
126
+ # Chat API nested format: {type: "function", function: {...}}
127
+ if tool_hash[:type] == "function" && tool_hash[:function]
128
+ func = tool_hash[:function]
129
+ return [
130
+ func[:name],
131
+ func[:description],
132
+ func[:parameters] || func[:input_schema]
133
+ ]
134
+ end
135
+
136
+ # Flat formats (common, Anthropic, Responses)
137
+ [
138
+ tool_hash[:name],
139
+ tool_hash[:description],
140
+ tool_hash[:parameters] || tool_hash[:input_schema]
141
+ ]
142
+ end
143
+
110
144
  # Extracts text content from various message formats.
111
145
  #
112
146
  # Handles string messages, hash messages with :content key, and
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Providers
5
+ # Provides unified logic for clearing tool_choice after tool execution.
6
+ #
7
+ # When a tool_choice is set to "required" or to a specific tool name,
8
+ # it forces the model to use that tool. After the tool is executed,
9
+ # we need to clear the tool_choice to prevent infinite loops where
10
+ # the model keeps calling the same tool repeatedly.
11
+ #
12
+ # Each provider implements:
13
+ # - `extract_used_function_names`: Returns array of tool names that have been called
14
+ # - `tool_choice_forces_required?`: Returns true if tool_choice forces any tool use
15
+ # - `tool_choice_forces_specific?`: Returns [true, name] if tool_choice forces specific tool
16
+ module ToolChoiceClearing
17
+ extend ActiveSupport::Concern
18
+
19
+ # @api private
20
+ def prepare_prompt_request_tools
21
+ return unless request.tool_choice
22
+
23
+ functions_used = extract_used_function_names
24
+
25
+ # Clear if forcing required and any tool was used
26
+ if tool_choice_forces_required? && functions_used.any?
27
+ request.tool_choice = nil
28
+ return
29
+ end
30
+
31
+ # Clear if forcing specific tool and that tool was used
32
+ forces_specific, tool_name = tool_choice_forces_specific?
33
+ if forces_specific && tool_name && functions_used.include?(tool_name)
34
+ request.tool_choice = nil
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # Extracts the list of function names that have been called.
41
+ #
42
+ # @return [Array<String>] function names
43
+ def extract_used_function_names
44
+ raise NotImplementedError, "#{self.class} must implement #extract_used_function_names"
45
+ end
46
+
47
+ # Returns true if tool_choice forces any tool to be used (e.g., "required", "any").
48
+ #
49
+ # @return [Boolean]
50
+ def tool_choice_forces_required?
51
+ raise NotImplementedError, "#{self.class} must implement #tool_choice_forces_required?"
52
+ end
53
+
54
+ # Returns [true, tool_name] if tool_choice forces a specific tool, [false, nil] otherwise.
55
+ #
56
+ # @return [Array<Boolean, String|nil>]
57
+ def tool_choice_forces_specific?
58
+ raise NotImplementedError, "#{self.class} must implement #tool_choice_forces_specific?"
59
+ end
60
+ end
61
+ end
62
+ end