Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/active_agent/providers/open_ai/chat_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def process_stream_chunk(api_response_event)

# If we have a delta, we need to update a message in the stack
message = find_or_create_message(api_message.index)
message = message_merge_delta(message, api_message.delta.as_json.deep_symbolize_keys)
message_merge_delta(message, api_message.delta.deep_to_h)

# Stream back content changes as they come in
if api_message.delta.content
Expand Down Expand Up @@ -138,7 +138,7 @@ def process_stream_chunk(api_response_event)
def process_function_calls(api_function_calls)
api_function_calls.each do |api_function_call|
content = instrument("tool_call.active_agent", tool_name: api_function_call.dig(:function, :name)) do
case api_function_call[:type]
case api_function_call[:type].to_s
when "function"
process_tool_call_function(api_function_call[:function])
else
Expand Down Expand Up @@ -243,7 +243,7 @@ def hash_merge_delta(hash, delta)
end
end
when String
hash[key] += value
hash[key] += value.to_s
else
hash[key] = value
end
Expand Down
126 changes: 126 additions & 0 deletions test/providers/open_ai/chat_provider_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# frozen_string_literal: true

require "test_helper"

begin
require "openai"
rescue LoadError
puts "OpenAI gem not available, skipping OpenAI Chat provider tests"
return
end

require_relative "../../../lib/active_agent/providers/open_ai/chat_provider"

module Providers
module OpenAI
module Chat
class ChatProviderTest < ActiveSupport::TestCase

include WebMock::API

setup do
WebMock.enable!
@client = ::OpenAI::Client.new(base_url: "http://localhost", api_key: "test-key")
end

teardown do
WebMock.disable!
end

test "accumulates streaming tool call deltas into message_stack" do
stub_streaming_response(tool_calls_sse_response)

stream = @client.chat.completions.stream(
messages: [{ content: "What's the weather in Boston?", role: :user }],
model: "qwen-plus",
tools: weather_tool
)

chat_provider = ActiveAgent::Providers::OpenAI::ChatProvider.new

stream.each do |event|
chat_provider.send(:process_stream_chunk, event)
end

expected_message = {
index: 0,
role: :assistant,
tool_calls: [
{
index: 0,
id: "call_123",
function: {
name: "get_weather",
arguments: '{"city":"Paris","units":"celsius"}'
},
type: :function
}
]
}

assert_equal(
[expected_message],
chat_provider.send(:message_stack),
"message_stack should contain one assistant message with merged tool_calls"
)
end

private

def stub_streaming_response(response_body, request_options = {})
default_request = {
messages: [{ content: "What's the weather in Boston?",role: "user" }],
model: "qwen-plus",
stream: true
}

stub_request(:post, "http://localhost/chat/completions")
.with(body: hash_including(default_request.merge(request_options)))
.to_return(
status: 200,
headers: { "Content-Type" => "text/event-stream" },
body: response_body
)
end

def tool_calls_sse_response
<<~SSE
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1234567890,"model":"qwen-plus","choices":[{"index":0,"content":null,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1234567890,"model":"qwen-plus","choices":[{"index":0,"content":null,"delta":{"tool_calls":[{"index":0,"type":"function","function":{"arguments":"{\\"city\\":"}}]},"finish_reason":null}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1234567890,"model":"qwen-plus","choices":[{"index":0,"content":null,"delta":{"tool_calls":[{"index":0,"type":"function","function":{"arguments":"\\"Paris\\","}}]},"finish_reason":null}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1234567890,"model":"qwen-plus","choices":[{"index":0,"content":null,"delta":{"tool_calls":[{"index":0,"type":"function","function":{"arguments":"\\"units\\":\\"celsius\\"}"}}]},"finish_reason":null}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1234567890,"model":"qwen-plus","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}

data: [DONE]

SSE
end

def weather_tool
[
{
type: :function,
function: {
name: "get_weather",
parameters: {
type: "object",
properties: {
city: { type: "string" },
units: { type: "string" }
},
required: ["city", "units"],
additionalProperties: false
},
strict: true
}
}
]
end
end
end
end
end