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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +240 -2
- data/README.md +15 -24
- data/lib/active_agent/base.rb +389 -39
- data/lib/active_agent/concerns/callbacks.rb +251 -0
- data/lib/active_agent/concerns/observers.rb +147 -0
- data/lib/active_agent/concerns/parameterized.rb +292 -0
- data/lib/active_agent/concerns/provider.rb +120 -0
- data/lib/active_agent/concerns/queueing.rb +36 -0
- data/lib/active_agent/concerns/rescue.rb +64 -0
- data/lib/active_agent/concerns/streaming.rb +282 -0
- data/lib/active_agent/concerns/tooling.rb +23 -0
- data/lib/active_agent/concerns/view.rb +150 -0
- data/lib/active_agent/configuration.rb +442 -20
- data/lib/active_agent/generation.rb +141 -47
- data/lib/active_agent/providers/_base_provider.rb +420 -0
- data/lib/active_agent/providers/anthropic/_types.rb +63 -0
- data/lib/active_agent/providers/anthropic/options.rb +53 -0
- data/lib/active_agent/providers/anthropic/request.rb +163 -0
- data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
- data/lib/active_agent/providers/anthropic_provider.rb +254 -0
- data/lib/active_agent/providers/common/messages/_types.rb +160 -0
- data/lib/active_agent/providers/common/messages/assistant.rb +57 -0
- data/lib/active_agent/providers/common/messages/base.rb +17 -0
- data/lib/active_agent/providers/common/messages/system.rb +20 -0
- data/lib/active_agent/providers/common/messages/tool.rb +21 -0
- data/lib/active_agent/providers/common/messages/user.rb +20 -0
- data/lib/active_agent/providers/common/model.rb +361 -0
- data/lib/active_agent/providers/common/response.rb +13 -0
- data/lib/active_agent/providers/common/responses/_types.rb +51 -0
- data/lib/active_agent/providers/common/responses/base.rb +199 -0
- data/lib/active_agent/providers/common/responses/embed.rb +33 -0
- data/lib/active_agent/providers/common/responses/format.rb +31 -0
- data/lib/active_agent/providers/common/responses/message.rb +3 -0
- data/lib/active_agent/providers/common/responses/prompt.rb +42 -0
- data/lib/active_agent/providers/common/usage.rb +385 -0
- data/lib/active_agent/providers/concerns/exception_handler.rb +72 -0
- data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
- data/lib/active_agent/providers/concerns/previewable.rb +150 -0
- data/lib/active_agent/providers/log_subscriber.rb +178 -0
- data/lib/active_agent/providers/mock/_types.rb +77 -0
- data/lib/active_agent/providers/mock/embedding_request.rb +17 -0
- data/lib/active_agent/providers/mock/messages/_types.rb +103 -0
- data/lib/active_agent/providers/mock/messages/assistant.rb +26 -0
- data/lib/active_agent/providers/mock/messages/base.rb +63 -0
- data/lib/active_agent/providers/mock/messages/user.rb +18 -0
- data/lib/active_agent/providers/mock/options.rb +30 -0
- data/lib/active_agent/providers/mock/request.rb +38 -0
- data/lib/active_agent/providers/mock_provider.rb +311 -0
- data/lib/active_agent/providers/ollama/_types.rb +5 -0
- data/lib/active_agent/providers/ollama/chat/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/chat/request.rb +249 -0
- data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
- data/lib/active_agent/providers/ollama/embedding/_types.rb +44 -0
- data/lib/active_agent/providers/ollama/embedding/request.rb +190 -0
- data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
- data/lib/active_agent/providers/ollama/options.rb +27 -0
- data/lib/active_agent/providers/ollama_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/_base.rb +59 -0
- data/lib/active_agent/providers/open_ai/_types.rb +5 -0
- data/lib/active_agent/providers/open_ai/chat/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/chat/request.rb +161 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
- data/lib/active_agent/providers/open_ai/chat_provider.rb +219 -0
- data/lib/active_agent/providers/open_ai/embedding/_types.rb +56 -0
- data/lib/active_agent/providers/open_ai/embedding/request.rb +53 -0
- data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
- data/lib/active_agent/providers/open_ai/options.rb +74 -0
- data/lib/active_agent/providers/open_ai/responses/_types.rb +44 -0
- data/lib/active_agent/providers/open_ai/responses/request.rb +129 -0
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +200 -0
- data/lib/active_agent/providers/open_ai_provider.rb +94 -0
- data/lib/active_agent/providers/open_router/_types.rb +71 -0
- data/lib/active_agent/providers/open_router/options.rb +141 -0
- data/lib/active_agent/providers/open_router/request.rb +249 -0
- data/lib/active_agent/providers/open_router/requests/_types.rb +197 -0
- data/lib/active_agent/providers/open_router/requests/messages/_types.rb +56 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/_types.rb +97 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +43 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/_types.rb +61 -0
- data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +37 -0
- data/lib/active_agent/providers/open_router/requests/plugin.rb +41 -0
- data/lib/active_agent/providers/open_router/requests/plugins/_types.rb +46 -0
- data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +51 -0
- data/lib/active_agent/providers/open_router/requests/prediction.rb +34 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/_types.rb +44 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +64 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +105 -0
- data/lib/active_agent/providers/open_router/requests/response_format.rb +77 -0
- data/lib/active_agent/providers/open_router/transforms.rb +134 -0
- data/lib/active_agent/providers/open_router_provider.rb +62 -0
- data/lib/active_agent/providers/openai_provider.rb +2 -0
- data/lib/active_agent/providers/openrouter_provider.rb +2 -0
- data/lib/active_agent/railtie.rb +8 -6
- data/lib/active_agent/schema_generator.rb +333 -166
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +112 -36
- data/lib/generators/active_agent/agent/USAGE +78 -0
- data/lib/generators/active_agent/{agent_generator.rb → agent/agent_generator.rb} +14 -4
- data/lib/generators/active_agent/install/USAGE +25 -0
- data/lib/generators/active_agent/{install_generator.rb → install/install_generator.rb} +1 -19
- data/lib/generators/active_agent/templates/agent.rb.tt +7 -3
- data/lib/generators/active_agent/templates/application_agent.rb.tt +0 -2
- data/lib/generators/erb/agent_generator.rb +31 -16
- data/lib/generators/erb/templates/instructions.md.erb.tt +3 -0
- data/lib/generators/erb/templates/instructions.md.tt +3 -0
- data/lib/generators/erb/templates/instructions.text.tt +1 -0
- data/lib/generators/erb/templates/message.md.erb.tt +5 -0
- data/lib/generators/erb/templates/schema.json.tt +10 -0
- data/lib/generators/test_unit/agent_generator.rb +1 -1
- data/lib/generators/test_unit/templates/functional_test.rb.tt +4 -2
- metadata +182 -71
- data/lib/active_agent/action_prompt/action.rb +0 -13
- data/lib/active_agent/action_prompt/base.rb +0 -623
- data/lib/active_agent/action_prompt/message.rb +0 -126
- data/lib/active_agent/action_prompt/prompt.rb +0 -136
- data/lib/active_agent/action_prompt.rb +0 -19
- data/lib/active_agent/callbacks.rb +0 -33
- data/lib/active_agent/generation_provider/anthropic_provider.rb +0 -163
- data/lib/active_agent/generation_provider/base.rb +0 -55
- data/lib/active_agent/generation_provider/base_adapter.rb +0 -19
- data/lib/active_agent/generation_provider/error_handling.rb +0 -167
- data/lib/active_agent/generation_provider/log_subscriber.rb +0 -92
- data/lib/active_agent/generation_provider/message_formatting.rb +0 -107
- data/lib/active_agent/generation_provider/ollama_provider.rb +0 -66
- data/lib/active_agent/generation_provider/open_ai_provider.rb +0 -279
- data/lib/active_agent/generation_provider/open_router_provider.rb +0 -385
- data/lib/active_agent/generation_provider/parameter_builder.rb +0 -119
- data/lib/active_agent/generation_provider/response.rb +0 -75
- data/lib/active_agent/generation_provider/responses_adapter.rb +0 -44
- data/lib/active_agent/generation_provider/stream_processing.rb +0 -58
- data/lib/active_agent/generation_provider/tool_management.rb +0 -142
- data/lib/active_agent/generation_provider.rb +0 -67
- data/lib/active_agent/log_subscriber.rb +0 -44
- data/lib/active_agent/parameterized.rb +0 -75
- data/lib/active_agent/prompt_helper.rb +0 -19
- data/lib/active_agent/queued_generation.rb +0 -12
- data/lib/active_agent/rescuable.rb +0 -34
- data/lib/active_agent/sanitizers.rb +0 -40
- data/lib/active_agent/streaming.rb +0 -34
- data/lib/active_agent/test_case.rb +0 -125
- data/lib/generators/USAGE +0 -47
- data/lib/generators/active_agent/USAGE +0 -56
- data/lib/generators/erb/install_generator.rb +0 -44
- data/lib/generators/erb/templates/layout.html.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.json.erb.tt +0 -1
- data/lib/generators/erb/templates/layout.text.erb.tt +0 -1
- data/lib/generators/erb/templates/view.html.erb.tt +0 -5
- data/lib/generators/erb/templates/view.json.erb.tt +0 -16
- /data/lib/active_agent/{preview.rb → concerns/preview.rb} +0 -0
- /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
|