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,254 @@
1
+ # lib/active_agent/providers/anthropic_provider.rb
2
+
3
+ require_relative "_base_provider"
4
+
5
+ require_gem!(:anthropic, __FILE__)
6
+
7
+ require_relative "anthropic/_types"
8
+ require_relative "anthropic/transforms"
9
+
10
+ module ActiveAgent
11
+ module Providers
12
+ # Handles communication with Anthropic's Claude models.
13
+ #
14
+ # Supports message creation, streaming responses, tool calling, and Claude-specific
15
+ # features like thinking mode and content blocks. Manages tool choice cleanup to
16
+ # prevent endless looping when tools have been used in conversation history.
17
+ #
18
+ # @see BaseProvider
19
+ class AnthropicProvider < BaseProvider
20
+ # @todo Add support for Anthropic::BedrockClient and Anthropic::VertexClient
21
+ # @return [Anthropic::Client]
22
+ def client
23
+ ::Anthropic::Client.new(**options.serialize)
24
+ end
25
+
26
+ protected
27
+
28
+ # Removes forced tool choice after first use to prevent endless looping.
29
+ #
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.
32
+ #
33
+ # @see BaseProvider#prepare_prompt_request
34
+ # @return [Request]
35
+ def prepare_prompt_request
36
+ prepare_prompt_request_tools
37
+ prepare_prompt_request_response_format
38
+
39
+ super
40
+ end
41
+
42
+ # @api private
43
+ def prepare_prompt_request_tools
44
+ return unless request.tool_choice
45
+ return unless request.tool_choice.respond_to?(:type)
46
+
47
+ functions_used = message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name)
48
+
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))
55
+
56
+ request.tool_choice = nil
57
+ end
58
+ end
59
+
60
+ # @api private
61
+ def prepare_prompt_request_response_format
62
+ return unless request.response_format&.dig(:type) == "json_object"
63
+
64
+ self.message_stack.push({
65
+ role: "assistant",
66
+ content: "Here is the JSON requested:\n{"
67
+ })
68
+ end
69
+
70
+ # @see BaseProvider#api_prompt_executer
71
+ # @return [Anthropic::Messages]
72
+ def api_prompt_executer
73
+ client.messages
74
+ end
75
+
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.
86
+ #
87
+ # Handles chunk types: message_start, content_block_start, content_block_delta,
88
+ # content_block_stop, message_delta, message_stop. Manages text deltas,
89
+ # tool use inputs, and Claude's thinking/signature blocks.
90
+ #
91
+ # @see BaseProvider#process_stream_chunk
92
+ # @param api_response_chunk [Anthropic::StreamEvent]
93
+ # @return [void]
94
+ def process_stream_chunk(api_response_chunk)
95
+ chunk_type = api_response_chunk[:type]&.to_sym
96
+
97
+ instrument("stream_chunk.active_agent", chunk_type:)
98
+
99
+ broadcast_stream_open
100
+
101
+ case chunk_type
102
+ # Message Created
103
+ when :message_start
104
+ api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
105
+ self.message_stack.push(api_message)
106
+ broadcast_stream_update(message_stack.last)
107
+
108
+ # -> Content Block Create
109
+ when :content_block_start
110
+ api_content = Anthropic::Transforms.gem_to_hash(api_response_chunk.content_block)
111
+ self.message_stack.last[:content].push(api_content)
112
+ broadcast_stream_update(message_stack.last, api_content[:text])
113
+
114
+ # -> -> Content Block Append
115
+ when :content_block_delta
116
+ index = api_response_chunk.index
117
+ content = self.message_stack.last[:content][index]
118
+ api_delta = api_response_chunk.delta
119
+
120
+ case api_delta.type.to_sym
121
+ # -> -> -> Content Text Append
122
+ when :text_delta
123
+ content[:text] += api_delta.text
124
+ broadcast_stream_update(message_stack.last, api_delta.text)
125
+
126
+ # -> -> -> Content Function Call Append
127
+ when :input_json_delta
128
+ # No-Op; Wait for Function call to be complete
129
+ when :thinking_delta, :signature_delta
130
+ # TODO: Add with thinking rendering support
131
+ else
132
+ raise "Unexpected delta type: #{api_delta.type}"
133
+ end
134
+ # -> Content Block Completed [Full Block]
135
+ when :content_block_stop
136
+ index = api_response_chunk.index
137
+ api_content = Anthropic::Transforms.gem_to_hash(api_response_chunk.content_block)
138
+ self.message_stack.last[:content][index] = api_content
139
+
140
+ # Message Delta
141
+ when :message_delta
142
+ delta = Anthropic::Transforms.gem_to_hash(api_response_chunk.delta)
143
+ self.message_stack.last.merge!(delta)
144
+
145
+ # Message Completed [Full Message]
146
+ when :message_stop
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
155
+
156
+ # Once we are finished, close out and run tooling callbacks (Recursive)
157
+ process_prompt_finished if message_stack.last[:stop_reason]
158
+ when :ping
159
+ # No-Op Keep Awake
160
+ when :overloaded_error
161
+ # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
162
+ else
163
+ # No-Op: Looks like internal tracking from gem wrapper
164
+ return if api_response_chunk.respond_to?(:snapshot)
165
+ raise "Unexpected chunk type: #{api_response_chunk.type}"
166
+ end
167
+ end
168
+
169
+ # Executes tool calls and appends user message with results to message_stack.
170
+ #
171
+ # @param api_function_calls [Array<Hash>] with :name, :input, and :id keys
172
+ # @return [void]
173
+ def process_function_calls(api_function_calls)
174
+ content = api_function_calls.map do |api_function_call|
175
+ process_tool_call_function(api_function_call)
176
+ end
177
+
178
+ api_message = ::Anthropic::Models::MessageParam.new(role: "user", content:)
179
+ message = Anthropic::Transforms.gem_to_hash(api_message)
180
+
181
+ message_stack.push(message)
182
+ end
183
+
184
+ # Executes a single tool call via callback.
185
+ #
186
+ # @param api_function_call [Hash] with :name, :input, and :id keys
187
+ # @return [Anthropic::Models::ToolResultBlockParam]
188
+ def process_tool_call_function(api_function_call)
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
202
+
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)
212
+ end
213
+
214
+ #
215
+ # Handles JSON response format simulation by prepending `{` to the response
216
+ # content after removing the assistant lead-in message.
217
+ #
218
+ # @see BaseProvider#process_prompt_finished_extract_messages
219
+ # @param api_response [Hash] converted response hash
220
+ # @return [Array<Hash>, nil]
221
+ def process_prompt_finished_extract_messages(api_response)
222
+ return unless api_response
223
+
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]}"
228
+ end
229
+
230
+ [ api_response ]
231
+ end
232
+
233
+ # Extracts tool_use blocks from message_stack and parses JSON inputs.
234
+ #
235
+ # Handles JSON buffer parsing for gem versions and string inputs for gem >= 1.14.0.
236
+ #
237
+ # @see BaseProvider#process_prompt_finished_extract_function_calls
238
+ # @return [Array<Hash>] with :name, :input, and :id keys
239
+ def process_prompt_finished_extract_function_calls
240
+ message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
241
+ json_buf = api_function_call.delete(:json_buf)
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
+
249
+ api_function_call
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "user"
4
+ require_relative "assistant"
5
+ require_relative "tool"
6
+
7
+ module ActiveAgent
8
+ module Providers
9
+ module Common
10
+ module Messages
11
+ module Types
12
+ # Type for a single Message
13
+ class MessageType < ActiveModel::Type::Value
14
+ def cast(value)
15
+ cast_message(value)
16
+ end
17
+
18
+ def serialize(value)
19
+ serialize_message(value)
20
+ end
21
+
22
+ def deserialize(value)
23
+ cast(value)
24
+ end
25
+
26
+ private
27
+
28
+ def cast_message(value)
29
+ case value
30
+ when Common::Messages::Base
31
+ value
32
+ when String
33
+ # Convert bare strings to user messages
34
+ Common::Messages::User.new(content: value)
35
+ when Hash
36
+ hash = value.deep_symbolize_keys
37
+ role = hash[:role]&.to_s
38
+
39
+ case role
40
+ when "system"
41
+ nil # System messages are dropped in common format, replaced by Instructions
42
+ when "user", nil
43
+ # Handle both standard format and format with `text` key
44
+ if hash[:text] && !hash[:content]
45
+ Common::Messages::User.new(content: hash[:text])
46
+ else
47
+ # Filter to only known attributes for User
48
+ filtered_hash = hash.slice(:role, :content, :name)
49
+ Common::Messages::User.new(**filtered_hash.merge(role: "user"))
50
+ end
51
+ when "assistant"
52
+ # Filter to only known attributes for Assistant
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
+
60
+ Common::Messages::Assistant.new(**filtered_hash)
61
+ when "tool"
62
+ # Filter to only known attributes for Tool
63
+ filtered_hash = hash.slice(:role, :content, :tool_call_id)
64
+ Common::Messages::Tool.new(**filtered_hash)
65
+ else
66
+ raise ArgumentError, "Unknown message role: #{role}"
67
+ end
68
+ else
69
+ # Check if the value responds to to_common (provider-specific message)
70
+ if value.respond_to?(:to_common)
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)
79
+ else
80
+ raise ArgumentError, "Cannot cast #{value.class} to Message"
81
+ end
82
+ end
83
+ end
84
+
85
+ def serialize_message(value)
86
+ case value
87
+ when nil
88
+ nil
89
+ when Common::Messages::Base
90
+ value.to_h
91
+ when Hash
92
+ value
93
+ else
94
+ raise ArgumentError, "Cannot serialize #{value.class}"
95
+ end
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
120
+ end
121
+
122
+ # Type for Messages array
123
+ class MessagesType < ActiveModel::Type::Value
124
+ def cast(value)
125
+ case value
126
+ when Array
127
+ value.map { |v| message_type.cast(v) }.compact
128
+ when nil
129
+ []
130
+ else
131
+ raise ArgumentError, "Cannot cast #{value.class} to Messages array"
132
+ end
133
+ end
134
+
135
+ def serialize(value)
136
+ case value
137
+ when Array
138
+ value.map { |v| message_type.serialize(v) }.compact
139
+ when nil
140
+ []
141
+ else
142
+ raise ArgumentError, "Cannot serialize #{value.class}"
143
+ end
144
+ end
145
+
146
+ def deserialize(value)
147
+ cast(value)
148
+ end
149
+
150
+ private
151
+
152
+ def message_type
153
+ @message_type ||= MessageType.new
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Common
8
+ module Messages
9
+ # Represents messages sent by the AI assistant in a conversation.
10
+ class Assistant < Base
11
+ attribute :role, :string, as: "assistant"
12
+ attribute :content, :string
13
+ attribute :name, :string
14
+
15
+ validates :content, presence: true
16
+
17
+ # Extracts and parses JSON object or array from message content.
18
+ #
19
+ # Searches for the first occurrence of `{` or `[` and last occurrence of `}` or `]`,
20
+ # then parses the content between them. Useful for extracting structured data from
21
+ # assistant messages that may contain additional text around the JSON.
22
+ #
23
+ # @param symbolize_names [Boolean] whether to symbolize hash keys
24
+ # @param normalize_names [Symbol, nil] key normalization method (e.g., :underscore)
25
+ # @return [Hash, Array, nil] parsed JSON structure or nil if parsing fails
26
+ def parsed_json(symbolize_names: true, normalize_names: :underscore)
27
+ start_char = [ content.index("{"), content.index("[") ].compact.min
28
+ end_char = [ content.rindex("}"), content.rindex("]") ].compact.max
29
+ content_stripped = content[start_char..end_char] if start_char && end_char
30
+ return unless content_stripped
31
+
32
+ content_parsed = JSON.parse(content_stripped)
33
+
34
+ transform_hash = ->(hash) do
35
+ next if hash.nil?
36
+
37
+ hash = hash.deep_transform_keys(&normalize_names) if normalize_names
38
+ hash = hash.deep_symbolize_keys if symbolize_names
39
+ hash
40
+ end
41
+
42
+ case content_parsed
43
+ when Hash then transform_hash.call(content_parsed)
44
+ when Array then content_parsed.map { |item| item.is_a?(Hash) ? transform_hash.call(item) : item }
45
+ else content_parsed
46
+ end
47
+ rescue JSON::ParserError
48
+ nil
49
+ end
50
+
51
+ alias_method :json_object, :parsed_json
52
+ alias_method :parse_json, :parsed_json
53
+ end
54
+ end
55
+ end
56
+ end
57
+ 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 Common
8
+ module Messages
9
+ class Base < Common::BaseModel
10
+ attribute :role, :string
11
+
12
+ validates :role, presence: true
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Common
8
+ module Messages
9
+ # System message - provides instructions and context to the AI
10
+ class System < Base
11
+ attribute :role, :string, as: "system"
12
+ attribute :content, :string # Text content
13
+ attribute :name, :string # Optional name for the system message
14
+
15
+ validates :content, presence: true
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Common
8
+ module Messages
9
+ # Tool message - messages containing tool call results
10
+ class Tool < Base
11
+ attribute :role, :string, as: "tool"
12
+ attribute :content, :string # Tool result content
13
+ attribute :tool_call_id, :string # ID of the tool call this is responding to
14
+ attribute :name, :string # Optional name of the tool
15
+
16
+ validates :content, presence: true
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Common
8
+ module Messages
9
+ # Represents a message sent by the user in a conversation
10
+ class User < Base
11
+ attribute :role, :string, as: "user"
12
+ attribute :content, :string
13
+ attribute :name, :string
14
+
15
+ validates :content, presence: true
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end