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
|
@@ -1,90 +1,184 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/messages/_types"
|
|
3
4
|
|
|
4
5
|
module ActiveAgent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# Deferred agent action ready for synchronous or asynchronous execution.
|
|
7
|
+
#
|
|
8
|
+
# Returned when calling agent actions. Provides methods to execute immediately
|
|
9
|
+
# or queue for background processing, plus access to prompt properties before execution.
|
|
10
|
+
#
|
|
11
|
+
# @example Synchronous generation
|
|
12
|
+
# generation = MyAgent.with(message: "Hello").greet
|
|
13
|
+
# response = generation.prompt_now
|
|
14
|
+
#
|
|
15
|
+
# @example Asynchronous generation
|
|
16
|
+
# MyAgent.with(message: "Hello").greet.prompt_later(queue: :prompts)
|
|
17
|
+
#
|
|
18
|
+
# @example Accessing prompt properties before generation
|
|
19
|
+
# generation = MyAgent.prompt(message: "Hello")
|
|
20
|
+
# generation.message.content # => "Hello"
|
|
21
|
+
# generation.messages # => [...]
|
|
22
|
+
class Generation
|
|
23
|
+
attr_internal :agent_class, :action_name, :args, :kwargs
|
|
24
|
+
|
|
25
|
+
# @param agent_class [Class]
|
|
26
|
+
# @param action_name [Symbol]
|
|
27
|
+
# @param args [Array]
|
|
28
|
+
# @param kwargs [Hash]
|
|
29
|
+
def initialize(agent_class, action_name, *args, **kwargs)
|
|
30
|
+
self.agent_class, self.action_name, self.args, self.kwargs = agent_class, action_name, args, kwargs
|
|
10
31
|
end
|
|
11
|
-
ruby2_keywords(:initialize)
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def processed?
|
|
35
|
+
!!@agent
|
|
15
36
|
end
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
# Accesses prompt options by processing the agent if needed.
|
|
39
|
+
#
|
|
40
|
+
# Lazily processes the agent on first access, allowing inspection of
|
|
41
|
+
# prompt properties before executing generation.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] with :messages, :actions, and configuration keys
|
|
44
|
+
def prompt_options
|
|
45
|
+
agent.prompt_options
|
|
19
46
|
end
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
|
|
48
|
+
# @return [Hash] configuration options excluding messages and actions
|
|
49
|
+
def options
|
|
50
|
+
prompt_options.except(:messages, :actions)
|
|
23
51
|
end
|
|
24
52
|
|
|
25
|
-
def
|
|
26
|
-
|
|
53
|
+
def instructions
|
|
54
|
+
agent.prompt_view_instructions(prompt_options[:instructions])
|
|
27
55
|
end
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
# @return [Array]
|
|
58
|
+
def messages
|
|
59
|
+
prompt_options[:messages] || []
|
|
31
60
|
end
|
|
32
61
|
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
# Returns the last message with consistent `.content` access.
|
|
63
|
+
#
|
|
64
|
+
# Wraps various message formats (String, Hash, objects) using the common
|
|
65
|
+
# MessageType for uniform access patterns.
|
|
66
|
+
#
|
|
67
|
+
# @return [ActiveAgent::Providers::Common::Messages::Base, nil]
|
|
68
|
+
def message
|
|
69
|
+
last_message = messages.last
|
|
70
|
+
return nil unless last_message
|
|
71
|
+
|
|
72
|
+
message_type.cast(last_message)
|
|
35
73
|
end
|
|
36
74
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
processed_agent.perform_generation!
|
|
41
|
-
end
|
|
42
|
-
end
|
|
75
|
+
# @return [Array]
|
|
76
|
+
def actions
|
|
77
|
+
prompt_options[:actions] || []
|
|
43
78
|
end
|
|
44
79
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
# Executes prompt generation synchronously with immediate processing.
|
|
81
|
+
#
|
|
82
|
+
# @return [ActiveAgent::Providers::Response]
|
|
83
|
+
def prompt_now!
|
|
84
|
+
agent.process_prompt!
|
|
85
|
+
end
|
|
86
|
+
alias generate_now! prompt_now!
|
|
87
|
+
|
|
88
|
+
# Executes prompt generation synchronously.
|
|
89
|
+
#
|
|
90
|
+
# @return [ActiveAgent::Providers::Response]
|
|
91
|
+
def prompt_now
|
|
92
|
+
agent.process_prompt
|
|
93
|
+
end
|
|
94
|
+
alias generate_now prompt_now
|
|
95
|
+
|
|
96
|
+
# Queues for background execution.
|
|
97
|
+
#
|
|
98
|
+
# @param options [Hash] job options (queue, priority, wait, etc.)
|
|
99
|
+
# @return [Object] enqueued job instance
|
|
100
|
+
# @raise [RuntimeError] if agent was accessed before queueing
|
|
101
|
+
def prompt_later(options = {})
|
|
102
|
+
enqueue_generation :prompt_now, options
|
|
103
|
+
end
|
|
104
|
+
alias generate_later prompt_later
|
|
105
|
+
|
|
106
|
+
# Generates a preview of the prompt without executing generation.
|
|
107
|
+
#
|
|
108
|
+
# Processes the agent action and renders the prompt configuration as
|
|
109
|
+
# markdown for debugging and inspection.
|
|
110
|
+
#
|
|
111
|
+
# @return [String] markdown-formatted preview
|
|
112
|
+
def prompt_preview
|
|
113
|
+
agent.preview_prompt
|
|
51
114
|
end
|
|
115
|
+
alias preview_prompt prompt_preview
|
|
52
116
|
|
|
117
|
+
# Executes embedding generation synchronously.
|
|
118
|
+
#
|
|
119
|
+
# @return [ActiveAgent::Providers::Response] embedding response with vector data
|
|
53
120
|
def embed_now
|
|
54
|
-
|
|
55
|
-
processed_agent.run_callbacks(:embedding) do
|
|
56
|
-
processed_agent.embed
|
|
57
|
-
end
|
|
58
|
-
end
|
|
121
|
+
agent.process_embed
|
|
59
122
|
end
|
|
60
123
|
|
|
124
|
+
# Queues embedding generation for background execution.
|
|
125
|
+
#
|
|
126
|
+
# @param options [Hash] job options (queue, priority, wait, etc.)
|
|
127
|
+
# @return [Object] enqueued job instance
|
|
128
|
+
# @raise [RuntimeError] if agent was accessed before queueing
|
|
61
129
|
def embed_later(options = {})
|
|
62
130
|
enqueue_generation :embed_now, options
|
|
63
131
|
end
|
|
64
132
|
|
|
65
133
|
private
|
|
66
134
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
135
|
+
# Lazily instantiates and processes the agent instance.
|
|
136
|
+
#
|
|
137
|
+
# Cached after first call.
|
|
138
|
+
#
|
|
139
|
+
# @return [ActiveAgent::Base]
|
|
140
|
+
# @api private
|
|
141
|
+
def agent
|
|
142
|
+
@agent ||= agent_class.new.tap do |agent|
|
|
143
|
+
agent.params = @params
|
|
144
|
+
agent.process(action_name, *args, **kwargs)
|
|
70
145
|
end
|
|
71
146
|
end
|
|
72
147
|
|
|
148
|
+
# Enqueues for background processing.
|
|
149
|
+
#
|
|
150
|
+
# Prevents enqueuing if the agent has been accessed, as local changes
|
|
151
|
+
# would be lost. Only method arguments are passed to the job, not the
|
|
152
|
+
# agent instance state.
|
|
153
|
+
#
|
|
154
|
+
# @param generation_method [Symbol, String]
|
|
155
|
+
# @param options [Hash]
|
|
156
|
+
# @return [Object] enqueued job
|
|
157
|
+
# @raise [RuntimeError] when agent already processed to prevent data loss
|
|
158
|
+
# @api private
|
|
73
159
|
def enqueue_generation(generation_method, options = {})
|
|
74
160
|
if processed?
|
|
75
|
-
::Kernel.raise "You've accessed the
|
|
161
|
+
::Kernel.raise "You've accessed the agent before asking to " \
|
|
76
162
|
"generate it later, so you may have made local changes that would " \
|
|
77
163
|
"be silently lost if we enqueued a job to generate it. Why? Only " \
|
|
78
164
|
"the agent method *arguments* are passed with the generation job! " \
|
|
79
|
-
"Do not access the
|
|
80
|
-
"later. Workarounds: 1. don't touch the
|
|
81
|
-
"#
|
|
82
|
-
"method*, or 3. use a custom Active Job instead of #
|
|
165
|
+
"Do not access the agent in any way if you mean to generate it " \
|
|
166
|
+
"later. Workarounds: 1. don't touch the agent before calling " \
|
|
167
|
+
"#prompt_later, 2. only touch the agent *within your agent " \
|
|
168
|
+
"method*, or 3. use a custom Active Job instead of #prompt_later."
|
|
83
169
|
else
|
|
84
|
-
|
|
85
|
-
|
|
170
|
+
agent_class.generation_job.set(options).perform_later(
|
|
171
|
+
agent_class.name, action_name.to_s, generation_method.to_s, args: args, kwargs: kwargs
|
|
86
172
|
)
|
|
87
173
|
end
|
|
88
174
|
end
|
|
175
|
+
|
|
176
|
+
# Lazy-loaded message type instance for casting messages.
|
|
177
|
+
#
|
|
178
|
+
# @return [ActiveAgent::Providers::Common::Messages::Types::MessageType]
|
|
179
|
+
# @api private
|
|
180
|
+
def message_type
|
|
181
|
+
@message_type ||= ActiveAgent::Providers::Common::Messages::Types::MessageType.new
|
|
182
|
+
end
|
|
89
183
|
end
|
|
90
184
|
end
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
require "active_support/delegation"
|
|
2
|
+
|
|
3
|
+
require_relative "common/response"
|
|
4
|
+
require_relative "concerns/exception_handler"
|
|
5
|
+
require_relative "concerns/instrumentation"
|
|
6
|
+
require_relative "concerns/previewable"
|
|
7
|
+
|
|
8
|
+
# @private
|
|
9
|
+
GEM_LOADERS = {
|
|
10
|
+
anthropic: [ "anthropic", "~> 1.12", "anthropic" ],
|
|
11
|
+
openai: [ "openai", "~> 0.34", "openai" ]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Requires a provider's gem dependency.
|
|
15
|
+
#
|
|
16
|
+
# @param type [Symbol] provider type (:anthropic, :openai)
|
|
17
|
+
# @param file_name [String] for error context
|
|
18
|
+
# @return [void]
|
|
19
|
+
# @raise [LoadError] when required gem is not installed
|
|
20
|
+
def require_gem!(type, file_name)
|
|
21
|
+
gem_name, requirement, package_name = GEM_LOADERS.fetch(type)
|
|
22
|
+
provider_name = file_name.split("/").last.delete_suffix(".rb").camelize
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
gem(gem_name, requirement)
|
|
26
|
+
require(package_name)
|
|
27
|
+
rescue LoadError
|
|
28
|
+
raise LoadError, "The '#{gem_name}' gem is required for #{provider_name}. Please add it to your Gemfile and run `bundle install`."
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ActiveAgent
|
|
33
|
+
module Providers
|
|
34
|
+
# Orchestrates LLM provider API requests, streaming, and multi-turn tool calling.
|
|
35
|
+
#
|
|
36
|
+
# Each provider (OpenAI, Anthropic, etc.) subclasses this to implement
|
|
37
|
+
# provider-specific API interactions.
|
|
38
|
+
#
|
|
39
|
+
# @abstract Subclasses must implement {#api_prompt_execute},
|
|
40
|
+
# {#process_stream_chunk}, {#process_prompt_finished_extract_messages},
|
|
41
|
+
# and {#process_prompt_finished_extract_function_calls}
|
|
42
|
+
class BaseProvider
|
|
43
|
+
extend ActiveSupport::Delegation
|
|
44
|
+
|
|
45
|
+
include ExceptionHandler
|
|
46
|
+
include Instrumentation
|
|
47
|
+
include Previewable
|
|
48
|
+
|
|
49
|
+
class ProvidersError < StandardError; end
|
|
50
|
+
|
|
51
|
+
attr_internal :options, :context, :trace_id, # Setup
|
|
52
|
+
:request, :message_stack, # Runtime
|
|
53
|
+
:stream_broadcaster, :streaming, # Callback (Streams)
|
|
54
|
+
:tools_function, # Callback (Tools)
|
|
55
|
+
:usage_stack # Usage Tracking
|
|
56
|
+
|
|
57
|
+
# @return [String] e.g., "Anthropic", "OpenAI"
|
|
58
|
+
def self.service_name
|
|
59
|
+
name.split("::").last.delete_suffix("Provider")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [String] e.g., "Anthropic", "OpenAI::Chat"
|
|
63
|
+
def self.tag_name
|
|
64
|
+
name.delete_prefix("ActiveAgent::Providers::").delete_suffix("Provider")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Module] e.g., ActiveAgent::Providers::OpenAI
|
|
68
|
+
def self.namespace
|
|
69
|
+
"#{name.deconstantize}::#{service_name}".safe_constantize
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Class]
|
|
73
|
+
def self.options_klass
|
|
74
|
+
namespace::Options
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [ActiveModel::Type::Value] for prompt casting/serialization
|
|
78
|
+
def self.prompt_request_type
|
|
79
|
+
namespace::RequestType.new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [ActiveModel::Type::Value] for embedding casting/serialization
|
|
83
|
+
# @raise [NotImplementedError] when provider doesn't support embeddings
|
|
84
|
+
def self.embed_request_type
|
|
85
|
+
fail(NotImplementedError)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
delegate :service_name, :tag_name, :namespace, :options_klass, :prompt_request_type, :embed_request_type, to: :class
|
|
89
|
+
|
|
90
|
+
# @param kwargs [Hash] configuration and callbacks
|
|
91
|
+
# @option kwargs [Symbol] :service validates against provider's service name
|
|
92
|
+
# @option kwargs [Proc] :stream_broadcaster for streaming events (:open, :update, :close)
|
|
93
|
+
# @option kwargs [Proc] :tools_function to execute tool/function calls
|
|
94
|
+
# @raise [RuntimeError] when service name doesn't match provider
|
|
95
|
+
def initialize(kwargs = {})
|
|
96
|
+
assert_service!(kwargs.delete(:service))
|
|
97
|
+
|
|
98
|
+
configure_exception_handler(
|
|
99
|
+
exception_handler: kwargs.delete(:exception_handler)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.trace_id = kwargs[:trace_id]
|
|
103
|
+
self.stream_broadcaster = kwargs.delete(:stream_broadcaster)
|
|
104
|
+
self.streaming = false
|
|
105
|
+
self.tools_function = kwargs.delete(:tools_function)
|
|
106
|
+
self.options = options_klass.new(kwargs.extract!(*options_klass.keys))
|
|
107
|
+
self.context = kwargs
|
|
108
|
+
self.message_stack = []
|
|
109
|
+
self.usage_stack = []
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Generates prompt preview without executing the API call.
|
|
113
|
+
#
|
|
114
|
+
# @return [String] markdown-formatted preview
|
|
115
|
+
def preview
|
|
116
|
+
self.request = prompt_request_type.cast(context.except(:trace_id))
|
|
117
|
+
preview_prompt
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Executes prompt request with error handling and instrumentation.
|
|
121
|
+
#
|
|
122
|
+
# @return [ActiveAgent::Providers::Common::PromptResponse]
|
|
123
|
+
def prompt
|
|
124
|
+
self.request = prompt_request_type.cast(context.except(:trace_id))
|
|
125
|
+
|
|
126
|
+
instrument("prompt.active_agent") do |payload|
|
|
127
|
+
response = resolve_prompt
|
|
128
|
+
instrumentation_prompt_payload(payload, request, response)
|
|
129
|
+
|
|
130
|
+
response
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Executes embedding request with error handling and instrumentation.
|
|
135
|
+
#
|
|
136
|
+
# @return [ActiveAgent::Providers::Common::EmbedResponse]
|
|
137
|
+
def embed
|
|
138
|
+
self.request = embed_request_type.cast(context.except(:trace_id))
|
|
139
|
+
|
|
140
|
+
instrument("embed.active_agent") do |payload|
|
|
141
|
+
response = resolve_embed
|
|
142
|
+
instrumentation_embed_payload(payload, request, response)
|
|
143
|
+
|
|
144
|
+
response
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
protected
|
|
149
|
+
|
|
150
|
+
# @param name [String, nil]
|
|
151
|
+
# @raise [RuntimeError] when service name doesn't match provider
|
|
152
|
+
def assert_service!(name)
|
|
153
|
+
fail "Unexpected Service Name: #{name} != #{service_name}" if name && name != service_name
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @param name [String]
|
|
157
|
+
# @param payload [Hash]
|
|
158
|
+
# @yield block to instrument
|
|
159
|
+
# @return [Object] block result
|
|
160
|
+
def instrument(name, payload = {}, &block)
|
|
161
|
+
full_payload = { provider: service_name, provider_module: tag_name, trace_id: }.merge(payload)
|
|
162
|
+
ActiveSupport::Notifications.instrument(name, full_payload, &block)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Orchestrates complete prompt request lifecycle.
|
|
166
|
+
#
|
|
167
|
+
# Handles recursive tool/function calling until completion.
|
|
168
|
+
#
|
|
169
|
+
# @return [ActiveAgent::Providers::Common::PromptResponse]
|
|
170
|
+
def resolve_prompt
|
|
171
|
+
api_parameters = api_request_build(prepare_prompt_request, prompt_request_type)
|
|
172
|
+
api_response = instrument("prompt.provider.active_agent") do |payload|
|
|
173
|
+
raw_response = with_exception_handling { api_prompt_execute(api_parameters) }
|
|
174
|
+
|
|
175
|
+
# Instrumentation Context Building
|
|
176
|
+
# Normalize response for instrumentation (providers may return gem objects)
|
|
177
|
+
normalized_response = api_response_normalize(raw_response)
|
|
178
|
+
common_response = Common::PromptResponse.new(raw_response: normalized_response)
|
|
179
|
+
instrumentation_prompt_payload(payload, self.request, common_response)
|
|
180
|
+
usage_stack.push(common_response.usage) if common_response&.usage
|
|
181
|
+
|
|
182
|
+
raw_response
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
process_prompt_finished(api_response)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Orchestrates complete embedding request lifecycle.
|
|
189
|
+
#
|
|
190
|
+
# @return [ActiveAgent::Providers::Common::EmbedResponse]
|
|
191
|
+
def resolve_embed
|
|
192
|
+
api_parameters = api_request_build(self.request, embed_request_type)
|
|
193
|
+
api_response = instrument("embed.provider.active_agent") do |payload|
|
|
194
|
+
raw_response = with_exception_handling { api_embed_execute(api_parameters) }
|
|
195
|
+
|
|
196
|
+
# Instrumentation Context Building
|
|
197
|
+
common_response = Common::EmbedResponse.new(raw_response:)
|
|
198
|
+
instrumentation_embed_payload(payload, self.request, common_response)
|
|
199
|
+
|
|
200
|
+
raw_response
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
process_embed_finished(api_response)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Prepares request for next iteration in multi-turn conversation.
|
|
207
|
+
#
|
|
208
|
+
# Appends accumulated messages and resets buffer for next cycle.
|
|
209
|
+
#
|
|
210
|
+
# @return [Request]
|
|
211
|
+
def prepare_prompt_request
|
|
212
|
+
self.request.messages = [ *request.messages, *message_stack ]
|
|
213
|
+
self.message_stack = []
|
|
214
|
+
|
|
215
|
+
self.request
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @param request [Request]
|
|
219
|
+
# @param request_type [ActiveModel::Type::Value] for serialization
|
|
220
|
+
# @return [Hash] API request parameters
|
|
221
|
+
def api_request_build(request, request_type)
|
|
222
|
+
parameters = request_type.serialize(request)
|
|
223
|
+
parameters[:stream] = process_stream if request.try(:stream)
|
|
224
|
+
|
|
225
|
+
if options.extra_headers.present?
|
|
226
|
+
parameters[:request_options] = { extra_headers: options.extra_headers }.deep_merge(parameters[:request_options] || {})
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
parameters
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# @return [Proc] for each response chunk
|
|
233
|
+
def process_stream
|
|
234
|
+
proc do |api_response_chunk|
|
|
235
|
+
process_stream_chunk(api_response_chunk)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Executes prompt request against provider's API.
|
|
240
|
+
#
|
|
241
|
+
# @abstract
|
|
242
|
+
# @param parameters [Hash]
|
|
243
|
+
# @return [Object] provider-specific API response
|
|
244
|
+
# @raise [NotImplementedError]
|
|
245
|
+
def api_prompt_execute(parameters)
|
|
246
|
+
unless parameters[:stream]
|
|
247
|
+
api_prompt_executer.create(**parameters)
|
|
248
|
+
else
|
|
249
|
+
api_prompt_executer.stream(**parameters.except(:stream)).each(¶meters[:stream])
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Returns provider-specific API executer for prompt requests.
|
|
255
|
+
#
|
|
256
|
+
# Since all currently implemented providers use stainless gems, subclasses
|
|
257
|
+
# only need to override endpoint selection.
|
|
258
|
+
#
|
|
259
|
+
# @abstract
|
|
260
|
+
# @return [Object] provider-specific API client
|
|
261
|
+
# @raise [NotImplementedError]
|
|
262
|
+
def api_prompt_executer
|
|
263
|
+
fail NotImplementedError, "Subclass expected to implement"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Normalizes API response for instrumentation.
|
|
267
|
+
#
|
|
268
|
+
# Providers that return gem objects (like Anthropic::Models::Message) should
|
|
269
|
+
# override this to convert to a hash so usage data can be extracted.
|
|
270
|
+
# By default, returns the response as-is (for providers returning hashes).
|
|
271
|
+
#
|
|
272
|
+
# @param api_response [Object] provider-specific API response
|
|
273
|
+
# @return [Hash, Object] normalized response (preferably hash)
|
|
274
|
+
def api_response_normalize(api_response)
|
|
275
|
+
api_response
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Executes embedding request against provider's API.
|
|
279
|
+
#
|
|
280
|
+
# @abstract
|
|
281
|
+
# @param request_parameters [Hash]
|
|
282
|
+
# @return [Object] provider-specific embedding response
|
|
283
|
+
# @raise [NotImplementedError]
|
|
284
|
+
def api_embed_execute(request_parameters)
|
|
285
|
+
fail NotImplementedError, "Subclass expected to implement"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Processes a single streaming response chunk.
|
|
289
|
+
#
|
|
290
|
+
# @abstract
|
|
291
|
+
# @param api_response_chunk [Object] provider-specific chunk format
|
|
292
|
+
# @raise [NotImplementedError]
|
|
293
|
+
def process_stream_chunk(api_response_chunk)
|
|
294
|
+
fail NotImplementedError, "Subclass expected to implement"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Broadcasts stream open event.
|
|
298
|
+
#
|
|
299
|
+
# Fires once per request cycle, even during multi-turn tool calling.
|
|
300
|
+
#
|
|
301
|
+
# @return [void]
|
|
302
|
+
def broadcast_stream_open
|
|
303
|
+
return if streaming
|
|
304
|
+
self.streaming = true
|
|
305
|
+
|
|
306
|
+
instrument("stream_open.active_agent")
|
|
307
|
+
stream_broadcaster.call(nil, nil, :open)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Broadcasts stream update with message content delta.
|
|
311
|
+
#
|
|
312
|
+
# @param message [Hash, Object]
|
|
313
|
+
# @param delta [String, nil]
|
|
314
|
+
# @return [void]
|
|
315
|
+
def broadcast_stream_update(message, delta = nil)
|
|
316
|
+
stream_broadcaster.call(message, delta, :update)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Broadcasts stream close event.
|
|
320
|
+
#
|
|
321
|
+
# Fires once per request cycle, even during multi-turn tool calling.
|
|
322
|
+
#
|
|
323
|
+
# @return [void]
|
|
324
|
+
def broadcast_stream_close
|
|
325
|
+
return unless streaming
|
|
326
|
+
self.streaming = false
|
|
327
|
+
|
|
328
|
+
instrument("stream_close.active_agent")
|
|
329
|
+
stream_broadcaster.call(message_stack.last, nil, :close)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Processes completed API response and handles tool calling recursion.
|
|
333
|
+
#
|
|
334
|
+
# Extracts messages and function calls. If tools were invoked,
|
|
335
|
+
# executes them and recursively continues until completion.
|
|
336
|
+
#
|
|
337
|
+
# @param api_response [Object, nil] provider-specific response
|
|
338
|
+
# @return [Common::PromptResponse, nil]
|
|
339
|
+
def process_prompt_finished(api_response = nil)
|
|
340
|
+
if (api_messages = process_prompt_finished_extract_messages(api_response))
|
|
341
|
+
message_stack.push(*api_messages)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
if (tool_calls = process_prompt_finished_extract_function_calls)&.any?
|
|
345
|
+
process_function_calls(tool_calls)
|
|
346
|
+
resolve_prompt
|
|
347
|
+
else
|
|
348
|
+
|
|
349
|
+
# During a multi iteration process, we will internally open/close the stream
|
|
350
|
+
# with the provider, but this should all look like one big stream to the agents
|
|
351
|
+
# as they continue to work.
|
|
352
|
+
broadcast_stream_close
|
|
353
|
+
|
|
354
|
+
# To convert the messages into common format we first need to merge the current
|
|
355
|
+
# stack and then cast them to the provider type, so we can cast them out to common.
|
|
356
|
+
messages = prompt_request_type.cast(
|
|
357
|
+
messages: [ *request.messages, *message_stack ]
|
|
358
|
+
).messages
|
|
359
|
+
|
|
360
|
+
# Create response object with usage_stack array for multi-turn cumulative tracking.
|
|
361
|
+
# This will returned as it closes up the recursive stack
|
|
362
|
+
Common::PromptResponse.new(
|
|
363
|
+
context:,
|
|
364
|
+
format: request.response_format,
|
|
365
|
+
messages:,
|
|
366
|
+
raw_request: prompt_request_type.serialize(request),
|
|
367
|
+
raw_response: api_response,
|
|
368
|
+
usages: usage_stack
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# @abstract
|
|
374
|
+
# @param api_response [Object]
|
|
375
|
+
# @return [Array<Message>, nil]
|
|
376
|
+
# @raise [NotImplementedError]
|
|
377
|
+
def process_prompt_finished_extract_messages(api_response)
|
|
378
|
+
fail NotImplementedError, "Subclass expected to implement"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# @abstract
|
|
382
|
+
# @return [Array<Hash>, nil]
|
|
383
|
+
# @raise [NotImplementedError]
|
|
384
|
+
def process_prompt_finished_extract_function_calls
|
|
385
|
+
fail NotImplementedError, "Subclass expected to implement"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# @param api_response [Hash]
|
|
389
|
+
# @return [Common::EmbedResponse]
|
|
390
|
+
def process_embed_finished(api_response)
|
|
391
|
+
Common::EmbedResponse.new(
|
|
392
|
+
context:,
|
|
393
|
+
raw_request: embed_request_type.serialize(request),
|
|
394
|
+
raw_response: api_response,
|
|
395
|
+
data: process_embed_finished_data(api_response)
|
|
396
|
+
)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Extracts embedding vectors from API response.
|
|
400
|
+
#
|
|
401
|
+
# Handles both list and single embedding response formats:
|
|
402
|
+
# - List: `{ "data": [{ "embedding": [...] }] }`
|
|
403
|
+
# - Single: `{ "embedding": [...] }`
|
|
404
|
+
#
|
|
405
|
+
# @param api_response [Hash]
|
|
406
|
+
# @return [Array<Hash>] embedding objects with :index, :object, :embedding keys
|
|
407
|
+
# @raise [RuntimeError] when response format is unexpected
|
|
408
|
+
def process_embed_finished_data(api_response)
|
|
409
|
+
case (type = api_response[:object].to_sym)
|
|
410
|
+
when :list
|
|
411
|
+
api_response[:data]
|
|
412
|
+
when :embedding
|
|
413
|
+
[ { index: 0 }.merge(api_response.slice(:index, :object, :embedding)) ]
|
|
414
|
+
else
|
|
415
|
+
fail "Unexpected Embed Object Type: #{type}"
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|