Rebase PR #784 and fix OpenAI model/chat regressions (#1384)

* Wire conversation history through OpenAI responses API

* Fix RuboCop hash brace spacing in assistant tests

* Pipelock ignores

* Batch fixes

---------

Co-authored-by: sokiee <sokysrm@gmail.com>
This commit is contained in:
Juan José Mata
2026-04-15 18:45:24 +02:00
committed by GitHub
parent 53ea0375db
commit 7b2b1dd367
24 changed files with 937 additions and 90 deletions

View File

@@ -14,6 +14,10 @@ class AssistantTest < ActiveSupport::TestCase
@provider = mock
@expected_session_id = @chat.id.to_s
@expected_user_identifier = ::Digest::SHA256.hexdigest(@chat.user_id.to_s)
@expected_conversation_history = [
{ role: "user", content: "Can you help me understand my spending habits?" },
{ role: "user", content: "What is my net worth?" }
]
end
test "errors get added to chat" do
@@ -100,6 +104,7 @@ class AssistantTest < ActiveSupport::TestCase
@provider.expects(:chat_response).with do |message, **options|
assert_equal @expected_session_id, options[:session_id]
assert_equal @expected_user_identifier, options[:user_identifier]
assert_equal @expected_conversation_history, options[:messages]
text_chunks.each do |text_chunk|
options[:streamer].call(text_chunk)
end
@@ -154,6 +159,7 @@ class AssistantTest < ActiveSupport::TestCase
@provider.expects(:chat_response).with do |message, **options|
assert_equal @expected_session_id, options[:session_id]
assert_equal @expected_user_identifier, options[:user_identifier]
assert_equal @expected_conversation_history, options[:messages]
call2_text_chunks.each do |text_chunk|
options[:streamer].call(text_chunk)
end
@@ -165,6 +171,7 @@ class AssistantTest < ActiveSupport::TestCase
@provider.expects(:chat_response).with do |message, **options|
assert_equal @expected_session_id, options[:session_id]
assert_equal @expected_user_identifier, options[:user_identifier]
assert_equal @expected_conversation_history, options[:messages]
options[:streamer].call(call1_response_chunk)
true
end.returns(call1_response).once.in_sequence(sequence)
@@ -418,6 +425,95 @@ class AssistantTest < ActiveSupport::TestCase
assert_raises(Assistant::Error) { Assistant.for_chat(nil) }
end
test "builtin demotes a partially-streamed assistant message to failed on error" do
@assistant.expects(:get_model_provider).with("gpt-4.1").returns(@provider)
boom = StandardError.new("boom mid-stream")
@provider.expects(:chat_response).with do |_prompt, **options|
# Simulate a partial text chunk landing before the error propagates.
options[:streamer].call(provider_text_chunk("partial tokens "))
true
end.returns(provider_error_response(boom))
@assistant.respond_to(@message)
partial = @chat.messages.where(type: "AssistantMessage").order(:created_at).last
assert partial.present?, "partial assistant message should be persisted"
assert_equal "failed", partial.status
assert_equal "partial tokens ", partial.content
end
test "conversation_history excludes failed and pending messages" do
# Add a failed assistant turn; it must NOT leak into history.
AssistantMessage.create!(
chat: @chat,
content: "partial error response",
ai_model: "gpt-4.1",
status: "failed"
)
@assistant.expects(:get_model_provider).with("gpt-4.1").returns(@provider)
captured_history = nil
@provider.expects(:chat_response).with do |_prompt, **options|
captured_history = options[:messages]
options[:streamer].call(
provider_response_chunk(id: "1", model: "gpt-4.1", messages: [ provider_message(id: "1", text: "ok") ], function_requests: [])
)
true
end.returns(provider_success_response(
provider_response_chunk(id: "1", model: "gpt-4.1", messages: [ provider_message(id: "1", text: "ok") ], function_requests: []).data
))
@assistant.respond_to(@message)
contents = captured_history.map { |m| m[:content] }
assert_not_includes contents, "partial error response"
end
test "conversation_history serializes assistant tool_calls with paired tool result" do
assistant_msg = AssistantMessage.create!(
chat: @chat,
content: "Looking that up",
ai_model: "gpt-4.1",
status: "complete"
)
ToolCall::Function.create!(
message: assistant_msg,
provider_id: "call_abc",
provider_call_id: "call_abc",
function_name: "get_net_worth",
function_arguments: { foo: "bar" },
function_result: { amount: 1000, currency: "USD" }
)
@assistant.expects(:get_model_provider).with("gpt-4.1").returns(@provider)
captured_history = nil
@provider.expects(:chat_response).with do |_prompt, **options|
captured_history = options[:messages]
options[:streamer].call(
provider_response_chunk(id: "1", model: "gpt-4.1", messages: [ provider_message(id: "1", text: "ok") ], function_requests: [])
)
true
end.returns(provider_success_response(
provider_response_chunk(id: "1", model: "gpt-4.1", messages: [ provider_message(id: "1", text: "ok") ], function_requests: []).data
))
@assistant.respond_to(@message)
tool_call_entry = captured_history.find { |m| m[:role] == "assistant" && m[:tool_calls].present? }
tool_result_entry = captured_history.find { |m| m[:role] == "tool" }
assert_not_nil tool_call_entry, "tool_call message missing from history"
assert_not_nil tool_result_entry, "tool_result message missing from history"
assert_equal "call_abc", tool_call_entry[:tool_calls].first[:id]
assert_equal "call_abc", tool_result_entry[:tool_call_id]
assert_equal "get_net_worth", tool_result_entry[:name]
end
private
def mock_external_sse_response(sse_body)