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,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Ollama
|
|
8
|
+
# Handles transformation and normalization of embedding request parameters
|
|
9
|
+
# for the Ollama Embeddings API
|
|
10
|
+
module Embedding
|
|
11
|
+
# Provides transformation methods for normalizing embedding parameters
|
|
12
|
+
# to Ollama API format with OpenAI gem compatibility
|
|
13
|
+
module Transforms
|
|
14
|
+
class << self
|
|
15
|
+
# Converts gem objects to hash representation
|
|
16
|
+
#
|
|
17
|
+
# @param obj [Object] gem object or primitive value
|
|
18
|
+
# @return [Hash, Object] hash if object supports JSON serialization
|
|
19
|
+
def gem_to_hash(obj)
|
|
20
|
+
if obj.respond_to?(:to_json)
|
|
21
|
+
JSON.parse(obj.to_json, symbolize_names: true)
|
|
22
|
+
else
|
|
23
|
+
obj
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Normalizes all embedding request parameters
|
|
28
|
+
#
|
|
29
|
+
# Ollama-specific parameters (options, keep_alive, truncate) are extracted
|
|
30
|
+
# and returned separately from OpenAI-compatible parameters.
|
|
31
|
+
#
|
|
32
|
+
# @param params [Hash] raw request parameters
|
|
33
|
+
# @return [Array<Hash, Hash>] tuple of [openai_params, ollama_params]
|
|
34
|
+
def normalize_params(params)
|
|
35
|
+
params = params.dup
|
|
36
|
+
|
|
37
|
+
# Extract Ollama-specific parameters
|
|
38
|
+
ollama_params = {}
|
|
39
|
+
ollama_params[:options] = params.delete(:options) if params.key?(:options)
|
|
40
|
+
ollama_params[:keep_alive] = params.delete(:keep_alive) if params.key?(:keep_alive)
|
|
41
|
+
ollama_params[:truncate] = params.delete(:truncate) if params.key?(:truncate)
|
|
42
|
+
|
|
43
|
+
# Extract options attributes that can be at top level
|
|
44
|
+
extract_option_attributes(params, ollama_params)
|
|
45
|
+
|
|
46
|
+
# Normalize input - Ollama only accepts strings, not token arrays
|
|
47
|
+
if params[:input]
|
|
48
|
+
params[:input] = normalize_input(params[:input])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
[ params, ollama_params ]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Normalizes input parameter to Ollama format
|
|
55
|
+
#
|
|
56
|
+
# Ollama only accepts strings or arrays of strings (no token arrays).
|
|
57
|
+
# Converts single string to array internally for consistency.
|
|
58
|
+
#
|
|
59
|
+
# @param input [String, Array<String>]
|
|
60
|
+
# @return [Array<String>] normalized input as array of strings
|
|
61
|
+
# @raise [ArgumentError] if input contains non-string values
|
|
62
|
+
def normalize_input(input)
|
|
63
|
+
case input
|
|
64
|
+
when String
|
|
65
|
+
[ input.presence ].compact
|
|
66
|
+
when Array
|
|
67
|
+
# Validate all elements are strings
|
|
68
|
+
input.each_with_index do |item, index|
|
|
69
|
+
unless item.is_a?(String)
|
|
70
|
+
raise ArgumentError, "Ollama embedding input must contain only strings, got #{item.class} at index #{index}"
|
|
71
|
+
end
|
|
72
|
+
if item.empty?
|
|
73
|
+
raise ArgumentError, "Ollama embedding input cannot contain empty strings at index #{index}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
input.compact
|
|
77
|
+
when nil
|
|
78
|
+
nil
|
|
79
|
+
else
|
|
80
|
+
raise ArgumentError, "Cannot normalize #{input.class} to Ollama input (expected String or Array)"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Serializes input for API submission
|
|
85
|
+
#
|
|
86
|
+
# Returns single string if array has only one element, otherwise array.
|
|
87
|
+
#
|
|
88
|
+
# @param input [Array<String>, nil]
|
|
89
|
+
# @return [String, Array<String>, nil]
|
|
90
|
+
def serialize_input(input)
|
|
91
|
+
return nil if input.nil?
|
|
92
|
+
|
|
93
|
+
# Return single string if array has only one element
|
|
94
|
+
if input.is_a?(Array) && input.length == 1
|
|
95
|
+
input.first
|
|
96
|
+
else
|
|
97
|
+
input
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Cleans up serialized request for API submission
|
|
102
|
+
#
|
|
103
|
+
# Merges OpenAI-compatible params with Ollama-specific params.
|
|
104
|
+
#
|
|
105
|
+
# @param openai_hash [Hash] serialized OpenAI-compatible request
|
|
106
|
+
# @param ollama_params [Hash] Ollama-specific parameters
|
|
107
|
+
# @param defaults [Hash] default values to remove
|
|
108
|
+
# @return [Hash] cleaned and merged request hash
|
|
109
|
+
def cleanup_serialized_request(openai_hash, ollama_params, defaults)
|
|
110
|
+
# Remove nil values
|
|
111
|
+
cleaned = openai_hash.compact
|
|
112
|
+
|
|
113
|
+
# Serialize input (convert single-element array to string)
|
|
114
|
+
if cleaned[:input]
|
|
115
|
+
cleaned[:input] = serialize_input(cleaned[:input])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Merge Ollama-specific params, skip defaults
|
|
119
|
+
ollama_params.each do |key, value|
|
|
120
|
+
next if value.nil?
|
|
121
|
+
next if value.respond_to?(:empty?) && value.empty?
|
|
122
|
+
next if defaults.key?(key) && defaults[key] == value
|
|
123
|
+
|
|
124
|
+
# Serialize options object if present
|
|
125
|
+
cleaned[key] = if value.respond_to?(:serialize)
|
|
126
|
+
value.serialize
|
|
127
|
+
else
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
cleaned
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# Extracts option attributes that can be specified at the top level
|
|
138
|
+
#
|
|
139
|
+
# @param params [Hash] request parameters
|
|
140
|
+
# @param ollama_params [Hash] ollama-specific parameters to populate
|
|
141
|
+
def extract_option_attributes(params, ollama_params)
|
|
142
|
+
option_keys = [
|
|
143
|
+
:mirostat, :mirostat_eta, :mirostat_tau, :num_ctx,
|
|
144
|
+
:repeat_last_n, :repeat_penalty, :temperature, :seed,
|
|
145
|
+
:num_predict, :top_k, :top_p, :min_p, :stop
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
option_keys.each do |key|
|
|
149
|
+
if params.key?(key)
|
|
150
|
+
ollama_params[:options] ||= {}
|
|
151
|
+
ollama_params[:options][key] = params.delete(key)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/options"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Ollama
|
|
8
|
+
class Options < ActiveAgent::Providers::OpenAI::Options
|
|
9
|
+
attribute :base_url, :string, fallback: "http://127.0.0.1:11434/v1"
|
|
10
|
+
attribute :api_key, :string, fallback: "ollama"
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def resolve_api_key(kwargs)
|
|
15
|
+
kwargs[:api_key] ||
|
|
16
|
+
kwargs[:access_token] ||
|
|
17
|
+
ENV["OLLAMA_API_KEY"] ||
|
|
18
|
+
ENV["OLLAMA_ACCESS_TOKEN"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Not Used as Part of Ollama
|
|
22
|
+
def resolve_organization_id(settings) = nil
|
|
23
|
+
def resolve_project_id(settings) = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require_relative "_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "open_ai_provider"
|
|
6
|
+
require_relative "ollama/_types"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
module Providers
|
|
10
|
+
# Connects to local Ollama instances via OpenAI-compatible API.
|
|
11
|
+
#
|
|
12
|
+
# Provides chat completion and embedding functionality through locally-hosted
|
|
13
|
+
# Ollama models. Handles connection errors specific to local deployments.
|
|
14
|
+
#
|
|
15
|
+
# @see OpenAI::ChatProvider
|
|
16
|
+
class OllamaProvider < OpenAI::ChatProvider
|
|
17
|
+
# @return [String]
|
|
18
|
+
def self.service_name
|
|
19
|
+
"Ollama"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Class]
|
|
23
|
+
def self.options_klass
|
|
24
|
+
namespace::Options
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [ActiveModel::Type::Value]
|
|
28
|
+
def self.prompt_request_type
|
|
29
|
+
namespace::Chat::RequestType.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [ActiveModel::Type::Value]
|
|
33
|
+
def self.embed_request_type
|
|
34
|
+
namespace::Embedding::RequestType.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
# Executes chat completion request with Ollama-specific error handling.
|
|
40
|
+
#
|
|
41
|
+
# @see OpenAI::ChatProvider#api_prompt_execute
|
|
42
|
+
# @param parameters [Hash]
|
|
43
|
+
# @return [Object, nil] response object or nil for streaming
|
|
44
|
+
# @raise [OpenAI::Errors::APIConnectionError] when Ollama server unreachable
|
|
45
|
+
def api_prompt_execute(parameters)
|
|
46
|
+
super
|
|
47
|
+
|
|
48
|
+
rescue ::OpenAI::Errors::APIConnectionError => exception
|
|
49
|
+
log_connection_error(exception)
|
|
50
|
+
raise exception
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Executes embedding request with Ollama-specific error handling.
|
|
54
|
+
#
|
|
55
|
+
# @param parameters [Hash]
|
|
56
|
+
# @return [Hash] symbolized API response
|
|
57
|
+
# @raise [OpenAI::Errors::APIConnectionError] when Ollama server unreachable
|
|
58
|
+
def api_embed_execute(parameters)
|
|
59
|
+
client.embeddings.create(**parameters).as_json.deep_symbolize_keys
|
|
60
|
+
|
|
61
|
+
rescue ::OpenAI::Errors::APIConnectionError => exception
|
|
62
|
+
log_connection_error(exception)
|
|
63
|
+
raise exception
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Handles role duplication bug in Ollama's OpenAI-compatible streaming.
|
|
67
|
+
#
|
|
68
|
+
# Ollama duplicates role information in streaming deltas, requiring
|
|
69
|
+
# manual cleanup to prevent message corruption. This fixes the
|
|
70
|
+
# "role appears in every chunk" issue when using streaming responses.
|
|
71
|
+
#
|
|
72
|
+
# @see OpenAI::ChatProvider#message_merge_delta
|
|
73
|
+
# @param message [Hash]
|
|
74
|
+
# @param delta [Hash]
|
|
75
|
+
# @return [Hash]
|
|
76
|
+
def message_merge_delta(message, delta)
|
|
77
|
+
message[:role] = delta.delete(:role) if delta[:role] # Copy a Bad Design (OpenAI's Chat API) Badly, Win Bad Prizes
|
|
78
|
+
|
|
79
|
+
hash_merge_delta(message, delta)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Logs connection failures with Ollama server details for debugging.
|
|
83
|
+
#
|
|
84
|
+
# @param error [Exception]
|
|
85
|
+
# @return [void]
|
|
86
|
+
def log_connection_error(error)
|
|
87
|
+
instrument("connection_error.provider.active_agent",
|
|
88
|
+
uri_base: options.uri_base,
|
|
89
|
+
exception: error.class,
|
|
90
|
+
message: error.message)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative "../_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "options"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Providers
|
|
9
|
+
module OpenAI
|
|
10
|
+
# Base provider class for OpenAI API implementations.
|
|
11
|
+
#
|
|
12
|
+
# Provides common functionality for OpenAI-based providers including
|
|
13
|
+
# client configuration, service identification, and tool call processing.
|
|
14
|
+
#
|
|
15
|
+
# @see ActiveAgent::Providers::BaseProvider
|
|
16
|
+
class Base < ActiveAgent::Providers::BaseProvider
|
|
17
|
+
# Returns the service name for OpenAI providers.
|
|
18
|
+
#
|
|
19
|
+
# @return [String] Always returns "OpenAI"
|
|
20
|
+
def self.service_name
|
|
21
|
+
"OpenAI"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns a configured OpenAI client instance.
|
|
25
|
+
#
|
|
26
|
+
# @return [OpenAI::Client] The configured OpenAI client
|
|
27
|
+
def client
|
|
28
|
+
::OpenAI::Client.new(**options.serialize)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
|
|
33
|
+
# Processes a tool call function from the API response.
|
|
34
|
+
#
|
|
35
|
+
# This method extracts the function name and arguments from an API function call,
|
|
36
|
+
# parses the arguments as JSON, and invokes the function callback with the parsed parameters.
|
|
37
|
+
#
|
|
38
|
+
# @param api_function_call [Hash] The function call data from the API response
|
|
39
|
+
# @option api_function_call [String] :name The name of the function to call
|
|
40
|
+
# @option api_function_call [String] :arguments JSON string containing the function arguments
|
|
41
|
+
#
|
|
42
|
+
# @return [Object] The result of the function callback invocation
|
|
43
|
+
#
|
|
44
|
+
# @example Processing a tool call
|
|
45
|
+
# api_call = { name: "get_weather", arguments: '{"location":"NYC"}' }
|
|
46
|
+
# process_tool_call_function(api_call)
|
|
47
|
+
# # => calls tools_function.call("get_weather", location: "NYC")
|
|
48
|
+
def process_tool_call_function(api_function_call)
|
|
49
|
+
name = api_function_call[:name]
|
|
50
|
+
kwargs = JSON.parse(api_function_call[:arguments], symbolize_names: true) if api_function_call[:arguments]
|
|
51
|
+
|
|
52
|
+
instrument("tool_call.active_agent", tool_name: name) do
|
|
53
|
+
tools_function.call(name, **kwargs)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "request"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenAI
|
|
8
|
+
module Chat
|
|
9
|
+
# ActiveModel type for casting and serializing chat requests
|
|
10
|
+
class RequestType < ActiveModel::Type::Value
|
|
11
|
+
# Casts value to Request object
|
|
12
|
+
#
|
|
13
|
+
# @param value [Request, Hash, nil]
|
|
14
|
+
# @return [Request, nil]
|
|
15
|
+
# @raise [ArgumentError] when value cannot be cast
|
|
16
|
+
def cast(value)
|
|
17
|
+
case value
|
|
18
|
+
when Request
|
|
19
|
+
value
|
|
20
|
+
when Hash
|
|
21
|
+
Request.new(**value.deep_symbolize_keys)
|
|
22
|
+
when nil
|
|
23
|
+
nil
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "Cannot cast #{value.class} to Request"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Serializes Request to hash for API submission
|
|
30
|
+
#
|
|
31
|
+
# @param value [Request, Hash, nil]
|
|
32
|
+
# @return [Hash, nil]
|
|
33
|
+
# @raise [ArgumentError] when value cannot be serialized
|
|
34
|
+
def serialize(value)
|
|
35
|
+
case value
|
|
36
|
+
when Request
|
|
37
|
+
value.serialize
|
|
38
|
+
when Hash
|
|
39
|
+
value
|
|
40
|
+
when nil
|
|
41
|
+
nil
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param value [Object]
|
|
48
|
+
# @return [Request, nil]
|
|
49
|
+
def deserialize(value)
|
|
50
|
+
cast(value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "transforms"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Providers
|
|
9
|
+
module OpenAI
|
|
10
|
+
module Chat
|
|
11
|
+
# Wraps OpenAI gem's CompletionCreateParams with normalization
|
|
12
|
+
#
|
|
13
|
+
# Delegates to OpenAI::Models::Chat::CompletionCreateParams while providing
|
|
14
|
+
# parameter normalization and shorthand format support via the Transforms module.
|
|
15
|
+
#
|
|
16
|
+
# All OpenAI Chat API fields are available via delegation:
|
|
17
|
+
# model, messages, temperature, max_tokens, max_completion_tokens, top_p,
|
|
18
|
+
# frequency_penalty, presence_penalty, tools, tool_choice, response_format,
|
|
19
|
+
# stream_options, audio, prediction, metadata, modalities, service_tier, store,
|
|
20
|
+
# parallel_tool_calls, reasoning_effort, verbosity, stop, seed, logit_bias,
|
|
21
|
+
# logprobs, top_logprobs, prompt_cache_key, safety_identifier, user,
|
|
22
|
+
# web_search_options, function_call, functions
|
|
23
|
+
#
|
|
24
|
+
# @example Basic usage
|
|
25
|
+
# request = Request.new(
|
|
26
|
+
# model: "gpt-4o",
|
|
27
|
+
# messages: [{role: "user", content: "Hello"}]
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @example String message normalization
|
|
31
|
+
# Request.new(model: "gpt-4o", messages: "Hello")
|
|
32
|
+
# # Normalized to: [{role: "user", content: "Hello"}]
|
|
33
|
+
#
|
|
34
|
+
# @example Instructions support
|
|
35
|
+
# Request.new(
|
|
36
|
+
# model: "gpt-4o",
|
|
37
|
+
# messages: [{role: "user", content: "Hi"}],
|
|
38
|
+
# instructions: ["You are helpful", "Be concise"]
|
|
39
|
+
# )
|
|
40
|
+
# # instructions converted to developer messages and prepended
|
|
41
|
+
class Request < SimpleDelegator
|
|
42
|
+
# Default parameter values applied during initialization
|
|
43
|
+
DEFAULTS = {
|
|
44
|
+
frequency_penalty: 0,
|
|
45
|
+
logprobs: false,
|
|
46
|
+
modalities: [ "text" ],
|
|
47
|
+
n: 1,
|
|
48
|
+
parallel_tool_calls: true,
|
|
49
|
+
presence_penalty: 0,
|
|
50
|
+
service_tier: "auto",
|
|
51
|
+
store: false,
|
|
52
|
+
stream: false,
|
|
53
|
+
temperature: 1,
|
|
54
|
+
top_p: 1
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# @return [Boolean, nil]
|
|
58
|
+
attr_reader :stream
|
|
59
|
+
|
|
60
|
+
# Creates a new chat completion request
|
|
61
|
+
#
|
|
62
|
+
# @param params [Hash] request parameters
|
|
63
|
+
# @option params [String] :model required model identifier
|
|
64
|
+
# @option params [Array, String, Hash] :messages required conversation messages
|
|
65
|
+
# @option params [Array<String>, String] :instructions system/developer prompts
|
|
66
|
+
# @option params [Hash, String, Symbol] :response_format
|
|
67
|
+
# @option params [Float] :temperature (1) sampling temperature 0-2
|
|
68
|
+
# @option params [Integer] :max_tokens maximum tokens to generate
|
|
69
|
+
# @option params [Array] :tools available tool definitions
|
|
70
|
+
# @raise [ArgumentError] when parameters are invalid
|
|
71
|
+
def initialize(**params)
|
|
72
|
+
# Step 1: Extract stream flag
|
|
73
|
+
@stream = params[:stream]
|
|
74
|
+
|
|
75
|
+
# Step 2: Apply defaults
|
|
76
|
+
params = apply_defaults(params)
|
|
77
|
+
|
|
78
|
+
# Step 3: Normalize all parameters (instructions, messages, response_format)
|
|
79
|
+
params = Chat::Transforms.normalize_params(params)
|
|
80
|
+
|
|
81
|
+
# Step 4: Create gem model - this validates all parameters!
|
|
82
|
+
gem_model = ::OpenAI::Models::Chat::CompletionCreateParams.new(**params)
|
|
83
|
+
|
|
84
|
+
# Step 5: Delegate all method calls to gem model
|
|
85
|
+
super(gem_model)
|
|
86
|
+
rescue ArgumentError => e
|
|
87
|
+
# Re-raise with more context
|
|
88
|
+
raise ArgumentError, "Invalid OpenAI Chat request parameters: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Serializes request for API call
|
|
92
|
+
#
|
|
93
|
+
# Uses gem's JSON serialization, removes default values for minimal
|
|
94
|
+
# request body, and simplifies messages where possible.
|
|
95
|
+
#
|
|
96
|
+
# @return [Hash] cleaned request hash
|
|
97
|
+
def serialize
|
|
98
|
+
# Use gem's JSON serialization (handles all nested objects)
|
|
99
|
+
hash = Chat::Transforms.gem_to_hash(__getobj__)
|
|
100
|
+
|
|
101
|
+
# Cleanup and simplify for API request
|
|
102
|
+
Chat::Transforms.cleanup_serialized_request(hash, DEFAULTS, __getobj__)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Array<Hash>, nil]
|
|
106
|
+
def messages
|
|
107
|
+
__getobj__.instance_variable_get(:@data)[:messages]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Sets messages with normalization
|
|
111
|
+
#
|
|
112
|
+
# @param value [Array, String, Hash]
|
|
113
|
+
# @return [void]
|
|
114
|
+
def messages=(value)
|
|
115
|
+
normalized_value = Chat::Transforms.normalize_messages(value)
|
|
116
|
+
__getobj__.instance_variable_get(:@data)[:messages] = normalized_value
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Alias for messages (common format compatibility)
|
|
120
|
+
#
|
|
121
|
+
# @return [Array<Hash>, nil]
|
|
122
|
+
def message
|
|
123
|
+
messages
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @param value [Array, String, Hash]
|
|
127
|
+
# @return [void]
|
|
128
|
+
def message=(value)
|
|
129
|
+
self.messages = value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Sets instructions as developer messages
|
|
133
|
+
#
|
|
134
|
+
# Prepends developer messages to the messages array for common format compatibility.
|
|
135
|
+
#
|
|
136
|
+
# @param values [Array<String>, String]
|
|
137
|
+
# @return [void]
|
|
138
|
+
def instructions=(*values)
|
|
139
|
+
instructions_messages = Chat::Transforms.normalize_instructions(values.flatten)
|
|
140
|
+
current_messages = messages || []
|
|
141
|
+
self.messages = instructions_messages + current_messages
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# @api private
|
|
147
|
+
# @param params [Hash]
|
|
148
|
+
# @return [Hash]
|
|
149
|
+
def apply_defaults(params)
|
|
150
|
+
# Only apply defaults for keys that aren't present
|
|
151
|
+
DEFAULTS.each do |key, value|
|
|
152
|
+
params[key] = value unless params.key?(key)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
params
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|