activeagent 1.0.0.rc1 → 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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/lib/active_agent/providers/_base_provider.rb +92 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/request.rb +135 -81
  6. data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
  7. data/lib/active_agent/providers/anthropic_provider.rb +96 -53
  8. data/lib/active_agent/providers/common/messages/_types.rb +37 -1
  9. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  10. data/lib/active_agent/providers/common/usage.rb +385 -0
  11. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  12. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  13. data/lib/active_agent/providers/mock_provider.rb +23 -23
  14. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  15. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  16. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  17. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  18. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  19. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  20. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  21. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  22. data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
  23. data/lib/active_agent/providers/open_ai/chat_provider.rb +57 -36
  24. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  25. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  26. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  27. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  28. data/lib/active_agent/providers/open_ai/responses/request.rb +100 -134
  29. data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
  30. data/lib/active_agent/providers/open_ai/responses_provider.rb +77 -30
  31. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  32. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  33. data/lib/active_agent/providers/open_router/options.rb +49 -1
  34. data/lib/active_agent/providers/open_router/request.rb +232 -66
  35. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  36. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  37. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  38. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  39. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  40. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  41. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  42. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  43. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  44. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  45. data/lib/active_agent/providers/open_router/transforms.rb +134 -0
  46. data/lib/active_agent/providers/open_router_provider.rb +9 -0
  47. data/lib/active_agent/version.rb +1 -1
  48. metadata +15 -159
  49. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  50. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  51. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  52. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  53. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  54. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  55. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  56. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  57. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  58. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  59. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  60. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  61. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  62. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  78. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  79. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  80. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  81. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  82. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  83. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  84. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  85. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  86. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  87. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  88. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  91. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  92. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  93. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  94. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  95. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  96. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  97. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  98. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  99. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  100. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  101. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  102. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  116. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  117. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  118. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  119. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  120. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  121. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  122. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  123. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  124. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  125. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  126. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  127. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  128. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  129. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  130. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  131. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  162. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  163. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  166. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  167. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  168. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  170. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  171. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  172. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  173. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  174. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  175. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  184. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  185. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  186. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  187. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -5,6 +5,7 @@ require_relative "_base_provider"
5
5
  require_gem!(:anthropic, __FILE__)
6
6
 
7
7
  require_relative "anthropic/_types"
8
+ require_relative "anthropic/transforms"
8
9
 
9
10
  module ActiveAgent
10
11
  module Providers
@@ -24,10 +25,10 @@ module ActiveAgent
24
25
 
25
26
  protected
26
27
 
27
- # Prepares the request and handles tool choice cleanup.
28
+ # Removes forced tool choice after first use to prevent endless looping.
28
29
  #
29
- # Removes forced tool choice from subsequent requests to prevent endless looping
30
- # when the tool has already been used in the conversation.
30
+ # Clears tool_choice when the specified tool has already been called in the
31
+ # conversation, preventing the model from being forced to call it repeatedly.
31
32
  #
32
33
  # @see BaseProvider#prepare_prompt_request
33
34
  # @return [Request]
@@ -41,20 +42,24 @@ module ActiveAgent
41
42
  # @api private
42
43
  def prepare_prompt_request_tools
43
44
  return unless request.tool_choice
45
+ return unless request.tool_choice.respond_to?(:type)
44
46
 
45
47
  functions_used = message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name)
46
48
 
47
- if (request.tool_choice.type == "any" && functions_used.any?) ||
48
- (request.tool_choice.type == "tool" && functions_used.include?(request.tool_choice.name))
49
+ # tool_choice is always a gem model object (ToolChoiceAny, ToolChoiceTool, ToolChoiceAuto)
50
+ tool_choice_type = request.tool_choice.type
51
+ tool_choice_name = request.tool_choice.respond_to?(:name) ? request.tool_choice.name : nil
52
+
53
+ if (tool_choice_type == :any && functions_used.any?) ||
54
+ (tool_choice_type == :tool && tool_choice_name && functions_used.include?(tool_choice_name))
49
55
 
50
- instrument("tool_choice_removed.provider.active_agent")
51
56
  request.tool_choice = nil
52
57
  end
53
58
  end
54
59
 
55
60
  # @api private
56
61
  def prepare_prompt_request_response_format
57
- return unless request.response_format&.type == "json_object"
62
+ return unless request.response_format&.dig(:type) == "json_object"
58
63
 
59
64
  self.message_stack.push({
60
65
  role: "assistant",
@@ -62,50 +67,61 @@ module ActiveAgent
62
67
  })
63
68
  end
64
69
 
70
+ # @see BaseProvider#api_prompt_executer
71
+ # @return [Anthropic::Messages]
65
72
  def api_prompt_executer
66
73
  client.messages
67
74
  end
68
75
 
69
- # Processes streaming response chunks from Anthropic's API.
76
+ # @see BaseProvider#api_response_normalize
77
+ # @param api_response [Anthropic::Models::Message]
78
+ # @return [Hash] normalized response hash
79
+ def api_response_normalize(api_response)
80
+ return api_response unless api_response
81
+
82
+ Anthropic::Transforms.gem_to_hash(api_response)
83
+ end
84
+
85
+ # Processes streaming chunks and builds message incrementally in message_stack.
70
86
  #
71
87
  # Handles chunk types: message_start, content_block_start, content_block_delta,
72
88
  # content_block_stop, message_delta, message_stop. Manages text deltas,
73
89
  # tool use inputs, and Claude's thinking/signature blocks.
74
90
  #
75
- # @param api_response_chunk [Object]
91
+ # @see BaseProvider#process_stream_chunk
92
+ # @param api_response_chunk [Anthropic::StreamEvent]
76
93
  # @return [void]
77
94
  def process_stream_chunk(api_response_chunk)
78
- api_response_chunk = api_response_chunk.as_json.deep_symbolize_keys
79
- chunk_type = api_response_chunk[:type].to_sym
95
+ chunk_type = api_response_chunk[:type]&.to_sym
80
96
 
81
- instrument("stream_chunk_processing.provider.active_agent", chunk_type:)
97
+ instrument("stream_chunk.active_agent", chunk_type:)
82
98
 
83
99
  broadcast_stream_open
84
100
 
85
101
  case chunk_type
86
102
  # Message Created
87
103
  when :message_start
88
- api_message = api_response_chunk.fetch(:message)
104
+ api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
89
105
  self.message_stack.push(api_message)
90
106
  broadcast_stream_update(message_stack.last)
91
107
 
92
108
  # -> Content Block Create
93
109
  when :content_block_start
94
- api_content = api_response_chunk.fetch(:content_block)
110
+ api_content = Anthropic::Transforms.gem_to_hash(api_response_chunk.content_block)
95
111
  self.message_stack.last[:content].push(api_content)
96
112
  broadcast_stream_update(message_stack.last, api_content[:text])
97
113
 
98
114
  # -> -> Content Block Append
99
115
  when :content_block_delta
100
- index = api_response_chunk.fetch(:index)
116
+ index = api_response_chunk.index
101
117
  content = self.message_stack.last[:content][index]
102
- api_delta = api_response_chunk.fetch(:delta)
118
+ api_delta = api_response_chunk.delta
103
119
 
104
- case (delta_type = api_delta[:type].to_sym)
120
+ case api_delta.type.to_sym
105
121
  # -> -> -> Content Text Append
106
122
  when :text_delta
107
- content[:text] += api_delta[:text]
108
- broadcast_stream_update(message_stack.last, api_delta[:text])
123
+ content[:text] += api_delta.text
124
+ broadcast_stream_update(message_stack.last, api_delta.text)
109
125
 
110
126
  # -> -> -> Content Function Call Append
111
127
  when :input_json_delta
@@ -113,21 +129,29 @@ module ActiveAgent
113
129
  when :thinking_delta, :signature_delta
114
130
  # TODO: Add with thinking rendering support
115
131
  else
116
- fail "Unexpected Delta Type #{delta_type}"
132
+ raise "Unexpected delta type: #{api_delta.type}"
117
133
  end
118
134
  # -> Content Block Completed [Full Block]
119
135
  when :content_block_stop
120
- index = api_response_chunk.fetch(:index)
121
- api_content = api_response_chunk.fetch(:content_block)
136
+ index = api_response_chunk.index
137
+ api_content = Anthropic::Transforms.gem_to_hash(api_response_chunk.content_block)
122
138
  self.message_stack.last[:content][index] = api_content
123
139
 
124
140
  # Message Delta
125
141
  when :message_delta
126
- self.message_stack.last.merge!(api_response_chunk.fetch(:delta))
142
+ delta = Anthropic::Transforms.gem_to_hash(api_response_chunk.delta)
143
+ self.message_stack.last.merge!(delta)
127
144
 
128
145
  # Message Completed [Full Message]
129
146
  when :message_stop
130
- self.message_stack[-1] = api_response_chunk.fetch(:message)
147
+ api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
148
+
149
+ # Handle _json_buf (gem >= 1.14.0)
150
+ api_message[:content]&.each do |content_block|
151
+ content_block.delete(:_json_buf) if content_block[:type] == "tool_use"
152
+ end
153
+
154
+ self.message_stack[-1] = api_message
131
155
 
132
156
  # Once we are finished, close out and run tooling callbacks (Recursive)
133
157
  process_prompt_finished if message_stack.last[:stop_reason]
@@ -137,12 +161,12 @@ module ActiveAgent
137
161
  # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
138
162
  else
139
163
  # No-Op: Looks like internal tracking from gem wrapper
140
- return if api_response_chunk.key?(:snapshot)
141
- fail "Unexpected Chunk Type: #{api_response_chunk[:type]}"
164
+ return if api_response_chunk.respond_to?(:snapshot)
165
+ raise "Unexpected chunk type: #{api_response_chunk.type}"
142
166
  end
143
167
  end
144
168
 
145
- # Executes tool calls and creates user message with results.
169
+ # Executes tool calls and appends user message with results to message_stack.
146
170
  #
147
171
  # @param api_function_calls [Array<Hash>] with :name, :input, and :id keys
148
172
  # @return [void]
@@ -151,58 +175,77 @@ module ActiveAgent
151
175
  process_tool_call_function(api_function_call)
152
176
  end
153
177
 
154
- message = Anthropic::Requests::Messages::User.new(content:)
178
+ api_message = ::Anthropic::Models::MessageParam.new(role: "user", content:)
179
+ message = Anthropic::Transforms.gem_to_hash(api_message)
155
180
 
156
- message_stack.push(message.serialize)
181
+ message_stack.push(message)
157
182
  end
158
183
 
159
- # Executes a single tool call and returns the result.
184
+ # Executes a single tool call via callback.
160
185
  #
161
186
  # @param api_function_call [Hash] with :name, :input, and :id keys
162
- # @return [Anthropic::Requests::Content::ToolResult]
187
+ # @return [Anthropic::Models::ToolResultBlockParam]
163
188
  def process_tool_call_function(api_function_call)
164
- instrument("tool_execution.provider.active_agent", tool_name: api_function_call[:name])
165
-
166
- results = tools_function.call(
167
- api_function_call[:name], **api_function_call[:input]
168
- )
189
+ instrument("tool_call.active_agent", tool_name: api_function_call[:name]) do
190
+ results = tools_function.call(
191
+ api_function_call[:name], **api_function_call[:input]
192
+ )
193
+
194
+ ::Anthropic::Models::ToolResultBlockParam.new(
195
+ type: "tool_result",
196
+ tool_use_id: api_function_call[:id],
197
+ content: results.to_json,
198
+ is_error: false
199
+ )
200
+ end
201
+ end
169
202
 
170
- Anthropic::Requests::Content::ToolResult.new(
171
- tool_use_id: api_function_call[:id],
172
- content: results.to_json,
173
- )
203
+ # Converts API response message to hash for message_stack.
204
+ # Converts Anthropic gem response object to hash for storage.
205
+ #
206
+ # @param api_response [Anthropic::Models::Message]
207
+ # @return [Common::PromptResponse, nil]
208
+ def process_prompt_finished(api_response = nil)
209
+ # Convert gem object to hash so that raw_response[:usage] works
210
+ api_response_hash = api_response ? Anthropic::Transforms.gem_to_hash(api_response) : nil
211
+ super(api_response_hash)
174
212
  end
175
213
 
176
- # Extracts messages from completed API response.
177
214
  #
178
- # Handles JSON response format by merging the leading `{` prefix back into
179
- # the message content after removing the assistant lead-in message.
215
+ # Handles JSON response format simulation by prepending `{` to the response
216
+ # content after removing the assistant lead-in message.
180
217
  #
181
- # @param api_response [Object]
218
+ # @see BaseProvider#process_prompt_finished_extract_messages
219
+ # @param api_response [Hash] converted response hash
182
220
  # @return [Array<Hash>, nil]
183
221
  def process_prompt_finished_extract_messages(api_response)
184
222
  return unless api_response
185
223
 
186
- message = api_response.as_json.deep_symbolize_keys
187
-
188
- if request.response_format&.type == "json_object"
189
- request.messages.pop # Remove the `Here is the JSON requested:\n{` lead in message
190
- message[:content][0][:text] = "{#{message[:content][0][:text]}" # Merge in `{` prefix
224
+ # Handle JSON response format simulation
225
+ if request.response_format&.dig(:type) == "json_object"
226
+ request.pop_message!
227
+ api_response[:content][0][:text] = "{#{api_response[:content][0][:text]}"
191
228
  end
192
229
 
193
- [ message ]
230
+ [ api_response ]
194
231
  end
195
232
 
196
- # Extracts function calls from message stack.
233
+ # Extracts tool_use blocks from message_stack and parses JSON inputs.
197
234
  #
198
- # Processes tool_use content blocks and converts JSON buffers into proper
199
- # input parameters for function execution.
235
+ # Handles JSON buffer parsing for gem versions and string inputs for gem >= 1.14.0.
200
236
  #
237
+ # @see BaseProvider#process_prompt_finished_extract_function_calls
201
238
  # @return [Array<Hash>] with :name, :input, and :id keys
202
239
  def process_prompt_finished_extract_function_calls
203
240
  message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
204
241
  json_buf = api_function_call.delete(:json_buf)
205
242
  api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf
243
+
244
+ # Handle case where :input is still a JSON string (gem >= 1.14.0)
245
+ if api_function_call[:input].is_a?(String)
246
+ api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
247
+ end
248
+
206
249
  api_function_call
207
250
  end
208
251
  end
@@ -51,6 +51,12 @@ module ActiveAgent
51
51
  when "assistant"
52
52
  # Filter to only known attributes for Assistant
53
53
  filtered_hash = hash.slice(:role, :content, :name)
54
+
55
+ # Compress content array to string if needed (Anthropic format)
56
+ if filtered_hash[:content].is_a?(Array)
57
+ filtered_hash[:content] = compress_content_array(filtered_hash[:content])
58
+ end
59
+
54
60
  Common::Messages::Assistant.new(**filtered_hash)
55
61
  when "tool"
56
62
  # Filter to only known attributes for Tool
@@ -63,6 +69,13 @@ module ActiveAgent
63
69
  # Check if the value responds to to_common (provider-specific message)
64
70
  if value.respond_to?(:to_common)
65
71
  cast_message(value.to_common)
72
+ # Check if it's a gem model object that can be converted to hash
73
+ # Use JSON round-trip to ensure proper nested serialization
74
+ elsif value.respond_to?(:to_json)
75
+ hash = JSON.parse(value.to_json, symbolize_names: true)
76
+ cast_message(hash)
77
+ elsif value.respond_to?(:to_h)
78
+ cast_message(value.to_h)
66
79
  else
67
80
  raise ArgumentError, "Cannot cast #{value.class} to Message"
68
81
  end
@@ -78,9 +91,32 @@ module ActiveAgent
78
91
  when Hash
79
92
  value
80
93
  else
81
- raise ArgumentError, "Cannot serialize #{value.class}"
94
+ raise ArgumentError, "Cannot serialize #{value.class}"
82
95
  end
83
96
  end
97
+
98
+ # Compresses Anthropic-style content array into a string.
99
+ #
100
+ # Anthropic messages can have content as an array of blocks like:
101
+ # [{type: "text", text: "..."}, {type: "tool_use", ...}]
102
+ # This extracts and joins text blocks into a single string.
103
+ #
104
+ # @param content_array [Array<Hash>]
105
+ # @return [String]
106
+ def compress_content_array(content_array)
107
+ content_array.map do |block|
108
+ case block[:type]&.to_s
109
+ when "text"
110
+ block[:text]
111
+ when "tool_use"
112
+ # Tool use blocks don't have readable text content
113
+ nil
114
+ else
115
+ # Unknown block type, try to extract text if present
116
+ block[:text]
117
+ end
118
+ end.compact.join("\n")
119
+ end
84
120
  end
85
121
 
86
122
  # Type for Messages array
@@ -1,27 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_agent/providers/common/model"
4
+ require "active_agent/providers/common/usage"
4
5
 
5
6
  module ActiveAgent
6
7
  module Providers
7
8
  module Common
8
9
  module Responses
9
- # Base response model for provider responses.
10
- #
11
- # This class represents the standard response structure from AI providers
12
- # across different services (OpenAI, Anthropic, etc.). It provides a unified
13
- # interface for accessing response data, usage statistics, and request context.
10
+ # Provides unified interface for AI provider responses across OpenAI, Anthropic, etc.
14
11
  #
15
12
  # @abstract Subclass and override {#usage} if provider uses non-standard format
16
13
  #
17
- # @note This is a base class. Use specialized subclasses for specific response types:
18
- # - {Prompt} for conversational/completion responses with messages
19
- # - {Embed} for embedding responses with vector data
14
+ # @note Use specialized subclasses for specific response types:
15
+ # - {Prompt} for conversational/completion responses
16
+ # - {Embed} for embedding responses
20
17
  #
21
18
  # @example Accessing response data
22
19
  # response = agent.prompt.generate_now
23
20
  # response.success? #=> true
24
- # response.usage #=> { "prompt_tokens" => 10, "completion_tokens" => 20 }
21
+ # response.usage #=> Usage object with normalized fields
25
22
  # response.total_tokens #=> 30
26
23
  #
27
24
  # @example Inspecting raw provider data
@@ -33,117 +30,168 @@ module ActiveAgent
33
30
  # @see BaseModel
34
31
  class Base < BaseModel
35
32
  # @!attribute [r] context
36
- # The original context that was sent to the provider.
33
+ # Original request context sent to the provider.
37
34
  #
38
- # Contains structured information about the request including instructions,
39
- # messages, tools, and other configuration passed to the LLM.
35
+ # Includes instructions, messages, tools, and configuration.
40
36
  #
41
- # @return [Hash] the request context
37
+ # @return [Hash]
42
38
  attribute :context, writable: false
43
39
 
44
40
  # @!attribute [r] raw_request
45
- # The most recent request in provider-specific format.
41
+ # Most recent request in provider-specific format.
46
42
  #
47
- # Contains the actual API request payload sent to the provider,
48
- # useful for debugging and logging.
43
+ # Useful for debugging and logging.
49
44
  #
50
- # @return [Hash] the provider-formatted request
45
+ # @return [Hash]
51
46
  attribute :raw_request, writable: false
52
47
 
53
48
  # @!attribute [r] raw_response
54
- # The most recent response in provider-specific format.
49
+ # Most recent response in provider-specific format.
55
50
  #
56
- # Contains the raw API response from the provider, including all
57
- # metadata, usage stats, and provider-specific fields.
51
+ # Includes metadata, usage stats, and provider-specific fields.
52
+ # Hash keys are deep symbolized for consistent access.
58
53
  #
59
- # @return [Hash] the provider-formatted response
54
+ # @return [Hash]
60
55
  attribute :raw_response, writable: false
61
56
 
62
- # Initializes a new response object with deep-duplicated attributes.
57
+ # @!attribute [r] usages
58
+ # Usage objects from each API call in multi-turn conversations.
59
+ #
60
+ # Each call (e.g., for tool calling) tracks usage separately. These are
61
+ # summed to provide cumulative statistics via {#usage}.
63
62
  #
64
- # Deep duplication ensures that the response object maintains its own
65
- # independent copy of the data, preventing external modifications from
66
- # affecting the response's internal state.
63
+ # @return [Array<Usage>]
64
+ attribute :usages, default: -> { [] }, writable: false
65
+
66
+ # Initializes response with deep-duplicated attributes.
67
67
  #
68
- # @param kwargs [Hash] response attributes
69
- # @option kwargs [Hash] :context the original request context
70
- # @option kwargs [Hash] :raw_request the provider-formatted request
71
- # @option kwargs [Hash] :raw_response the provider-formatted response
68
+ # Deep duplication prevents external modifications from affecting internal state.
69
+ # The raw_response is deep symbolized for consistent key access across providers.
72
70
  #
73
- # @return [Base] the initialized response object
71
+ # @param kwargs [Hash]
72
+ # @option kwargs [Hash] :context
73
+ # @option kwargs [Hash] :raw_request
74
+ # @option kwargs [Hash] :raw_response
74
75
  def initialize(kwargs = {})
75
- super(kwargs.deep_dup) # Ensure that userland can't fuck with our memory space
76
+ kwargs = kwargs.deep_dup # Ensure that userland can't fuck with our memory space
77
+
78
+ # Deep symbolize raw_response for consistent access across all extraction methods
79
+ if kwargs[:raw_response].is_a?(Hash)
80
+ kwargs[:raw_response] = kwargs[:raw_response].deep_symbolize_keys
81
+ end
82
+
83
+ super(kwargs)
76
84
  end
77
85
 
78
- # Extracts instructions from the context.
79
- #
80
- # @return [String, Array<Hash>, nil] the instructions that were sent to the provider
86
+ # @return [String, Array<Hash>, nil]
81
87
  def instructions
82
88
  context[:instructions]
83
89
  end
84
90
 
85
- # Indicates whether the generation request was successful.
86
- #
87
91
  # @todo Better handling of failure flows
88
- #
89
- # @return [Boolean] true if successful, false otherwise
92
+ # @return [Boolean]
90
93
  def success?
91
94
  true
92
95
  end
93
96
 
94
- # Extracts usage statistics from the raw response.
97
+ # Normalized usage statistics across all providers.
98
+ #
99
+ # For multi-turn conversations with tool calling, returns cumulative
100
+ # usage across all API calls (sum of {#usages}).
95
101
  #
96
- # Most providers (OpenAI, Anthropic, etc.) return usage data in a
97
- # standardized format within the response. This method extracts that
98
- # information for token counting and billing purposes.
102
+ # @return [Usage, nil]
99
103
  #
100
- # @return [Hash, nil] usage statistics hash with keys like "prompt_tokens",
101
- # "completion_tokens", and "total_tokens", or nil if not available
104
+ # @example Single-turn usage
105
+ # response.usage.input_tokens #=> 100
106
+ # response.usage.output_tokens #=> 25
107
+ # response.usage.total_tokens #=> 125
102
108
  #
103
- # @example Usage data structure
104
- # {
105
- # "prompt_tokens" => 10,
106
- # "completion_tokens" => 20,
107
- # "total_tokens" => 30
108
- # }
109
+ # @example Multi-turn usage (cumulative)
110
+ # # After 3 API calls due to tool usage:
111
+ # response.usage.input_tokens #=> 350 (sum of all calls)
112
+ # response.usage.output_tokens #=> 120 (sum of all calls)
113
+ #
114
+ # @see Usage
109
115
  def usage
110
- return nil unless raw_response
111
-
112
- # Most providers store usage in the same format
113
- if raw_response.is_a?(Hash) && raw_response["usage"]
114
- raw_response["usage"]
116
+ @usage ||= begin
117
+ if usages.any?
118
+ usages.reduce(:+)
119
+ elsif raw_response
120
+ Usage.from_provider_usage(
121
+ raw_response.is_a?(Hash) ? raw_response[:usage] : raw_response.usage
122
+ )
123
+ end
115
124
  end
116
125
  end
117
126
 
118
- # Extracts the number of tokens used in the prompt/input.
127
+ # Response ID from provider, useful for tracking and debugging.
119
128
  #
120
- # @return [Integer, nil] number of prompt tokens used, or nil if unavailable
129
+ # @return [String, nil]
121
130
  #
122
131
  # @example
123
- # response.prompt_tokens #=> 10
124
- def prompt_tokens
125
- usage&.dig("prompt_tokens")
132
+ # response.id #=> "chatcmpl-CbDx1nXoNSBrNIMhiuy5fk7jXQjmT" (OpenAI)
133
+ # response.id #=> "msg_01RotDmSnYpKQjrTpaHUaEBz" (Anthropic)
134
+ # response.id #=> "gen-1761505659-yxgaVsqVABMQqw6oA7QF" (OpenRouter)
135
+ def id
136
+ @id ||= begin
137
+ return nil unless raw_response
138
+
139
+ if raw_response.is_a?(Hash)
140
+ raw_response[:id]
141
+ elsif raw_response.respond_to?(:id)
142
+ raw_response.id
143
+ end
144
+ end
126
145
  end
127
146
 
128
- # Extracts the number of tokens used in the completion/output.
147
+ # Model name from provider response.
148
+ #
149
+ # Useful for confirming which model was actually used, as providers may
150
+ # use different versions than requested.
129
151
  #
130
- # @return [Integer, nil] number of completion tokens used, or nil if unavailable
152
+ # @return [String, nil]
131
153
  #
132
154
  # @example
133
- # response.completion_tokens #=> 20
134
- def completion_tokens
135
- usage&.dig("completion_tokens")
155
+ # response.model #=> "gpt-4o-mini-2024-07-18"
156
+ # response.model #=> "claude-3-5-haiku-20241022"
157
+ def model
158
+ @model ||= begin
159
+ return nil unless raw_response
160
+
161
+ if raw_response.is_a?(Hash)
162
+ raw_response[:model]
163
+ elsif raw_response.respond_to?(:model)
164
+ raw_response.model
165
+ end
166
+ end
136
167
  end
137
168
 
138
- # Extracts the total number of tokens used (prompt + completion).
169
+ # Finish reason from provider response.
170
+ #
171
+ # Indicates why generation stopped (e.g., "stop", "length", "tool_calls").
172
+ # Normalizes access across providers that use different field names.
139
173
  #
140
- # @return [Integer, nil] total number of tokens used, or nil if unavailable
174
+ # @return [String, nil]
141
175
  #
142
176
  # @example
143
- # response.total_tokens #=> 30
144
- def total_tokens
145
- usage&.dig("total_tokens")
177
+ # response.finish_reason #=> "stop"
178
+ # response.finish_reason #=> "length"
179
+ # response.finish_reason #=> "tool_calls"
180
+ # response.stop_reason #=> "stop" (alias)
181
+ def finish_reason
182
+ @finish_reason ||= begin
183
+ return nil unless raw_response
184
+
185
+ if raw_response.is_a?(Hash)
186
+ # OpenAI format: choices[0].finish_reason or choices[0].message.finish_reason
187
+ raw_response.dig(:choices, 0, :finish_reason) ||
188
+ raw_response.dig(:choices, 0, :message, :finish_reason) ||
189
+ # Anthropic format: stop_reason
190
+ raw_response[:stop_reason]
191
+ end
192
+ end
146
193
  end
194
+ alias_method :stop_reason, :finish_reason
147
195
  end
148
196
  end
149
197
  end