Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit d56ed53

Browse files
authored
FIX: cancel functionality regressed (#938)
The cancel messaging was not floating correctly to the HTTP call leading to impossible to cancel completions This is now fully tested as well.
1 parent d83248c commit d56ed53

File tree

4 files changed

+97
-4
lines changed

4 files changed

+97
-4
lines changed

lib/ai_bot/playground.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,10 @@ def reply_to(post, custom_instructions: nil, &blk)
461461
if stream_reply && !Discourse.redis.get(redis_stream_key)
462462
cancel&.call
463463
reply_post.update!(raw: reply, cooked: PrettyText.cook(reply))
464+
# we do not break out, cause if we do
465+
# we will not get results from bot
466+
# leading to broken context
467+
# we need to trust it to cancel at the endpoint
464468
end
465469

466470
if post_streamer

lib/ai_bot/post_streamer.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
module DiscourseAi
44
module AiBot
55
class PostStreamer
6+
# test only
7+
def self.on_callback=(on_callback)
8+
@on_callback = on_callback
9+
end
10+
11+
def self.on_callback
12+
@on_callback
13+
end
14+
615
def initialize(delay: 0.5)
716
@mutex = Mutex.new
817
@callback = nil
@@ -11,6 +20,7 @@ def initialize(delay: 0.5)
1120
end
1221

1322
def run_later(&callback)
23+
self.class.on_callback.call(callback) if self.class.on_callback
1424
@mutex.synchronize { @callback = callback }
1525
ensure_worker!
1626
end

lib/completions/endpoints/base.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,20 @@ def perform_completion!(
139139

140140
begin
141141
cancelled = false
142-
cancel = -> { cancelled = true }
143-
if cancelled
142+
cancel = -> do
143+
cancelled = true
144144
http.finish
145-
break
146145
end
147146

147+
break if cancelled
148+
148149
response.read_body do |chunk|
150+
break if cancelled
151+
149152
response_raw << chunk
153+
150154
decode_chunk(chunk).each do |partial|
155+
break if cancelled
151156
partials_raw << partial.to_s
152157
response_data << partial if partial.is_a?(String)
153158
partials = [partial]

spec/lib/modules/ai_bot/playground_spec.rb

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
RSpec.describe DiscourseAi::AiBot::Playground do
44
subject(:playground) { described_class.new(bot) }
55

6-
fab!(:claude_2) { Fabricate(:llm_model, name: "claude-2") }
6+
fab!(:claude_2) do
7+
Fabricate(
8+
:llm_model,
9+
provider: "anthropic",
10+
url: "https://api.anthropic.com/v1/messages",
11+
name: "claude-2",
12+
)
13+
end
714
fab!(:opus_model) { Fabricate(:anthropic_model) }
815

916
fab!(:bot_user) do
@@ -948,6 +955,73 @@
948955
end
949956
end
950957

958+
describe "#canceling a completions" do
959+
after { DiscourseAi::AiBot::PostStreamer.on_callback = nil }
960+
961+
it "should be able to cancel a completion halfway through" do
962+
body = (<<~STRING).strip
963+
event: message_start
964+
data: {"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-opus-20240229", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 25, "output_tokens": 1}}}
965+
966+
event: content_block_start
967+
data: {"type": "content_block_start", "index":0, "content_block": {"type": "text", "text": ""}}
968+
969+
event: ping
970+
data: {"type": "ping"}
971+
972+
|event: content_block_delta
973+
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}}
974+
975+
|event: content_block_delta
976+
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "1"}}
977+
978+
|event: content_block_delta
979+
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "2"}}
980+
981+
|event: content_block_delta
982+
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "3"}}
983+
984+
event: content_block_stop
985+
data: {"type": "content_block_stop", "index": 0}
986+
987+
event: message_delta
988+
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence":null, "usage":{"output_tokens": 15}}}
989+
990+
event: message_stop
991+
data: {"type": "message_stop"}
992+
STRING
993+
994+
split = body.split("|")
995+
996+
count = 0
997+
DiscourseAi::AiBot::PostStreamer.on_callback =
998+
proc do |callback|
999+
count += 1
1000+
if count == 2
1001+
last_post = third_post.topic.posts.order(:id).last
1002+
Discourse.redis.del("gpt_cancel:#{last_post.id}")
1003+
end
1004+
raise "this should not happen" if count > 2
1005+
end
1006+
1007+
require_relative("../../completions/endpoints/endpoint_compliance")
1008+
EndpointMock.with_chunk_array_support do
1009+
stub_request(:post, "https://api.anthropic.com/v1/messages").to_return(
1010+
status: 200,
1011+
body: split,
1012+
)
1013+
# we are going to need to use real data here cause we want to trigger the
1014+
# base endpoint to cancel part way through
1015+
playground.reply_to(third_post)
1016+
end
1017+
1018+
last_post = third_post.topic.posts.order(:id).last
1019+
1020+
# not Hello123, we cancelled at 1 which means we may get 2 and then be done
1021+
expect(last_post.raw).to eq("Hello12")
1022+
end
1023+
end
1024+
9511025
describe "#available_bot_usernames" do
9521026
it "includes persona users" do
9531027
persona = Fabricate(:ai_persona)

0 commit comments

Comments
 (0)