Skip to content

Commit 3f5a803

Browse files
committed
[WIP] Boil the Ocean - OpenAI Stabilized (Native Format)
1 parent b6ed88d commit 3f5a803

File tree

31 files changed

+772
-492
lines changed

31 files changed

+772
-492
lines changed

lib/active_agent/providers/common/_base_model.rb

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ def self.attribute(name, type = nil, **options)
6868
required_attributes << name.to_s
6969

7070
define_method("#{name}=") do |value|
71-
next if value == default_value
71+
normalized_value = value.is_a?(String) ? value.to_sym : value
72+
normalized_default = default_value.is_a?(String) ? default_value.to_sym : default_value
73+
74+
next if normalized_value == normalized_default
75+
7276
raise ArgumentError, "Cannot set '#{name}' attribute (read-only with default value)"
7377
end
7478
else
@@ -112,16 +116,15 @@ def self.keys
112116
#
113117
# Settings can be provided as a hash or keyword arguments. Hash keys are
114118
# sorted to prioritize nested objects during initialization for backwards compatibility.
115-
#
116-
# @param hash [Hash, nil] attribute hash
117-
# @param kwargs [Hash] attribute keyword arguments
118-
def initialize(hash = nil, **kwargs)
119-
settings = hash || kwargs
119+
def initialize(kwargs = {})
120+
# To allow us to get a list of attribute defaults without initialized overrides
121+
return super(nil) if kwargs.key?(:'__default_values')
122+
120123
# Backwards Compatibility: This sorts object construction to the top to protect the assignment
121124
# of backward compatibility assignments.
122-
settings = settings.sort_by { |k, v| v.is_a?(Hash) ? 0 : 1 }.to_h if settings.is_a?(Hash)
125+
kwargs = kwargs.sort_by { |k, v| v.is_a?(Hash) ? 0 : 1 }.to_h if kwargs.is_a?(Hash)
123126

124-
super(settings)
127+
super(kwargs)
125128
end
126129

127130
# Merges the given attributes into the current instance.
@@ -131,8 +134,8 @@ def initialize(hash = nil, **kwargs)
131134
# @param hash [Hash, nil] attribute hash to merge
132135
# @param kwargs [Hash] attribute keyword arguments to merge
133136
# @return [BaseModel] self for method chaining
134-
def merge!(hash = nil, **kwargs)
135-
(hash || kwargs).deep_symbolize_keys.each do |key, value|
137+
def merge!(kwargs = {})
138+
kwargs.deep_symbolize_keys.each do |key, value|
136139
public_send("#{key}=", value) if respond_to?("#{key}=")
137140
end
138141

@@ -213,7 +216,7 @@ def to_hash
213216
# message.to_hash_compressed
214217
# #=> { role: "user" } # content omitted (matches default), role included (required)
215218
def to_hash_compressed
216-
default_values = self.class.new.attributes
219+
default_values = self.class.new(__default_values: true).attributes
217220
required_attrs = self.class.required_attributes
218221

219222
deep_compact(attribute_names.each_with_object({}) do |name, hash|

lib/active_agent/providers/open_ai/_base_provider.rb

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ module ActiveAgent
88
module Providers
99
module OpenAI
1010
class BaseProvider < ActiveAgent::Providers::BaseProvider
11-
attr_internal :stream_callback, :function_callback
11+
attr_internal :request, :message_stack,
12+
:stream_callback, :stream_finished,
13+
:function_callback
1214

1315
def initialize(kwargs = {})
1416
self.stream_callback = kwargs.delete(:stream_callback)
1517
self.function_callback = kwargs.delete(:function_callback)
18+
self.message_stack = []
1619

1720
super(kwargs)
1821
end
@@ -36,11 +39,42 @@ def service_name = "OpenAI"
3639
# @return [Class] The Options class for the specific provider, e.g., Anthropic::Options
3740
def options_type = OpenAI::Options
3841

42+
# @return response [ActiveAgent::Providers::Response]
43+
def resolve_prompt
44+
# Apply Tool/Function Messages and Reset Processing Buffer
45+
self.request.messages = [ *request.messages, *message_stack ]
46+
self.message_stack = []
47+
# @todo Validate Request
48+
49+
## Prepare Executation Environment
50+
parameters = request.to_hc
51+
if request.stream
52+
parameters[:stream] = process_stream
53+
self.stream_finished = false
54+
end
55+
56+
## Execute
57+
api_response = client_request_create(parameters:)
58+
process_finished(api_response.presence&.deep_symbolize_keys)
59+
end
60+
61+
def client_request_create(parameters:)
62+
fail(NotImplementedError)
63+
end
64+
3965
def process_stream
4066
proc do |api_response_chunk|
4167
process_stream_chunk(api_response_chunk.deep_symbolize_keys)
4268
end
4369
end
70+
71+
# @return result [Unknown]
72+
def process_tool_call_function(api_function_call)
73+
name = api_function_call[:name]
74+
kwargs = JSON.parse(api_function_call[:arguments], symbolize_names: true) if api_function_call[:arguments]
75+
76+
function_callback.call(name, **kwargs)
77+
end
4478
end
4579
end
4680
end

lib/active_agent/providers/open_ai/chat_provider.rb

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,16 @@ module ActiveAgent
55
module Providers
66
module OpenAI
77
class ChatProvider < BaseProvider
8-
attr_internal :request, :message_stack, :stream_finished
9-
108
def initialize(...)
119
super
1210

13-
self.request = Chat::Request.new(context)
14-
self.message_stack = []
11+
self.request = Chat::Request.new(context)
1512
end
1613

1714
protected
1815

19-
# @return response [ActiveAgent::Providers::Response]
20-
def resolve_prompt
21-
# Apply Tool/Function Messages
22-
request.messages = message_stack unless message_stack.empty?
23-
# @todo Validate Request
24-
25-
## Prepare Executation Environment
26-
parameters = request.to_hc
27-
if request.stream
28-
parameters[:stream] = process_stream
29-
self.stream_finished = false
30-
end
31-
message_stack.replace(parameters[:messages])
32-
33-
## Execute
34-
api_response = client.chat(parameters:)
35-
process_finished(api_response.presence&.deep_symbolize_keys)
16+
def client_request_create(parameters:)
17+
client.chat(parameters:)
3618
end
3719

3820
# @return void
@@ -72,19 +54,15 @@ def process_tool_calls(api_tool_calls)
7254
fail "Unexpected Tool Call Type: #{api_tool_call[:type]}"
7355
end
7456

75-
message = Chat::Requests::Messages::Tool.new(tool_call_id: api_tool_call[:id], content: content.to_json)
57+
message = Chat::Requests::Messages::Tool.new(
58+
tool_call_id: api_tool_call[:id],
59+
content: content.to_json
60+
)
61+
7662
message_stack.push(message.to_hc)
7763
end
7864
end
7965

80-
# @return result [Unknown]
81-
def process_tool_call_function(api_function_call)
82-
name = api_function_call[:name]
83-
kwargs = JSON.parse(api_function_call[:arguments], symbolize_names: true) if api_function_call[:arguments]
84-
85-
function_callback.call(name, **kwargs)
86-
end
87-
8866
# @return response [ActiveAgent::Providers::Response]
8967
def process_finished(api_response = nil)
9068
if (api_message = api_response&.dig(:choices, 0, :message))
@@ -104,16 +82,6 @@ def process_finished(api_response = nil)
10482
end
10583
end
10684

107-
# ActiveAgent::ActionPrompt::Message.new(
108-
# generation_id: api_message[:id] || api_response[:id],
109-
# content: api_message[:content],
110-
# role: api_message[:role].intern,
111-
# action_requested: api_message[:finish_reason] == "tool_calls",
112-
# raw_actions: api_message[:tool_calls] || [],
113-
# requested_actions: handle_actions(api_message[:tool_calls]),
114-
# content_type: context[:output_schema].present? ? "application/json" : "text/plain"
115-
# )
116-
11785
# def embeddings_response(response, request_params = nil)
11886
# message = ActiveAgent::ActionPrompt::Message.new(content: response.dig("data", 0, "embedding"), role: "assistant")
11987

lib/active_agent/providers/open_ai/responses/request.rb

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -143,40 +143,21 @@ def input=(value)
143143
def input
144144
value = super
145145

146-
if value && value.one? && value.first.is_a?(Requests::Inputs::UserMessage)
146+
if value && value.one? && value.first.is_a?(Requests::Inputs::UserMessage) && value.first.content.is_a?(String)
147147
value.first.content
148148
else
149149
value
150150
end
151151
end
152152

153+
153154
# For the message stack
154-
def input_list
155+
def messages
155156
attributes["input"]&.map { |it| it.to_h }
156157
end
157158

158-
# To handle Common Message [input] format
159-
def message=(value)
160-
case value.try(:role) || value[:role]
161-
when :system
162-
self.instructions = value.try(:content) || value[:content]
163-
when :user
164-
self.input = value.try(:content) || value[:content]
165-
end
166-
167-
# self.input = [
168-
# {
169-
# role: value.role,
170-
# content: message_content(value.content)
171-
# }
172-
# ]
173-
end
174-
175-
# To handle Common Messages [input] format
176-
def messages=(value)
177-
value.each do |message|
178-
self.message = message
179-
end
159+
def messages=(values)
160+
self.input = values
180161
end
181162

182163
private

lib/active_agent/providers/open_ai/responses/requests/input.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
require_relative "inputs/base"
44
require_relative "inputs/user_message"
55
require_relative "inputs/system_message"
6+
require_relative "inputs/developer_message"
67
require_relative "inputs/assistant_message"
78
require_relative "inputs/tool_message"
9+
require_relative "inputs/function_call_output"
10+
require_relative "inputs/reasoning"
11+
require_relative "inputs/item_reference"
812
require_relative "inputs/content_parts/base"
913
require_relative "inputs/content_parts/input_text"
1014
require_relative "inputs/content_parts/input_image"

lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require_relative "../../../../common/_base_model"
4+
require_relative "../types"
45

56
module ActiveAgent
67
module Providers
@@ -11,12 +12,20 @@ module Inputs
1112
# Base class for input items in Responses API
1213
class Base < Common::BaseModel
1314
attribute :role, :string
14-
attribute :content # Can be string or array of content parts
15+
attribute :content, Types::ContentType.new
1516

1617
validates :role, inclusion: {
17-
in: %w[system user assistant tool],
18+
in: %w[system user assistant developer tool],
1819
allow_nil: true
1920
}
21+
22+
def to_hc
23+
super.tap do |hash|
24+
if content.is_a?(Array) && content.one? && content.first.type == "input_text"
25+
hash[:content] = content.first.text
26+
end
27+
end
28+
end
2029
end
2130
end
2231
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "base"
4+
5+
module ActiveAgent
6+
module Providers
7+
module OpenAI
8+
module Responses
9+
module Requests
10+
module Inputs
11+
module ContentParts
12+
# Audio content part
13+
class InputAudio < Base
14+
attribute :type, :string, as: "input_audio"
15+
attribute :input_audio # Object containing audio properties
16+
17+
validates :type, presence: true
18+
validates :input_audio, presence: true
19+
end
20+
end
21+
end
22+
end
23+
end
24+
end
25+
end
26+
end

lib/active_agent/providers/open_ai/responses/requests/inputs/content_parts/input_file.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ module ContentParts
1212
# File content part
1313
class InputFile < Base
1414
attribute :type, :string, as: "input_file"
15-
attribute :filename, :string
16-
attribute :file_data, :string # Base64 encoded file data
15+
attribute :file_data, :string # Optional: content of file to send
16+
attribute :file_id, :string # Optional: ID of file to send
17+
attribute :file_url, :string # Optional: URL of file to send
18+
attribute :filename, :string # Optional: name of file
1719

18-
validates :filename, presence: true
19-
validates :file_data, presence: true
20+
validates :type, presence: true
2021
end
2122
end
2223
end

lib/active_agent/providers/open_ai/responses/requests/inputs/content_parts/input_image.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ module ContentParts
1212
# Image content part
1313
class InputImage < Base
1414
attribute :type, :string, as: "input_image"
15-
attribute :image_url # Can be string (URL or data URI) or object with url and detail
15+
attribute :detail, :string, default: "auto" # One of: high, low, auto
16+
attribute :file_id, :string # Optional: ID of file to send
17+
attribute :image_url, :string # Optional: URL or base64 data URL
1618

17-
validates :image_url, presence: true
19+
validates :type, presence: true
20+
validates :detail, presence: true
1821
end
1922
end
2023
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "base"
4+
5+
module ActiveAgent
6+
module Providers
7+
module OpenAI
8+
module Responses
9+
module Requests
10+
module Inputs
11+
# Developer message input (higher priority than system)
12+
class DeveloperMessage < Base
13+
attribute :role, :string, as: "developer"
14+
end
15+
end
16+
end
17+
end
18+
end
19+
end
20+
end

0 commit comments

Comments
 (0)