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

Commit 2a5c60d

Browse files
authored
FEATURE: display more places where AI is used / Chat streamer (#1278)
* FEATURE: display more places where AI is used - Usage was not showing automation or image caption in llm list. - Also: FIX - reasoning models would time out incorrectly after 60 seconds (raised to 10 minutes) * correct enum not to enumerate non configured models * FEATURE: implement chat streamer This implements a basic chat streamer, it provides 2 things: 1. Gives feedback to the user when LLM is generating 2. Streams stuff much more efficiently to client (given it may take 100ms or so per call to update chat)
1 parent e1731dc commit 2a5c60d

File tree

10 files changed

+215
-81
lines changed

10 files changed

+215
-81
lines changed

assets/javascripts/discourse/components/ai-llms-list-editor.gjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export default class AiLlmsListEditor extends Component {
116116
return i18n("discourse_ai.llms.usage.ai_persona", {
117117
persona: usage.name,
118118
});
119+
} else if (usage.type === "automation") {
120+
return i18n("discourse_ai.llms.usage.automation", {
121+
name: usage.name,
122+
});
119123
} else {
120124
return i18n("discourse_ai.llms.usage." + usage.type);
121125
}

assets/stylesheets/modules/llms/common/ai-llms-editor.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
list-style: none;
118118
margin: 0.5em 0 0 0;
119119
display: flex;
120+
flex-wrap: wrap;
120121

121122
li {
122123
font-size: var(--font-down-2);
@@ -125,6 +126,7 @@
125126
border: 1px solid var(--primary-low);
126127
padding: 1px 3px;
127128
margin-right: 0.5em;
129+
margin-bottom: 0.5em;
128130
}
129131
}
130132

config/locales/client.en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,12 @@ en:
439439
usage:
440440
ai_bot: "AI bot"
441441
ai_helper: "Helper"
442+
ai_helper_image_caption: "Image caption"
442443
ai_persona: "Persona (%{persona})"
443444
ai_summarization: "Summarize"
444445
ai_embeddings_semantic_search: "AI search"
445446
ai_spam: "Spam"
447+
automation: "Automation (%{name})"
446448
in_use_warning:
447449
one: "This model is currently used by %{settings}. If misconfigured, the feature won't work as expected."
448450
other: "This model is currently used by the following: %{settings}. If misconfigured, features won't work as expected. "

config/settings.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,6 @@ discourse_ai:
312312
default: "1|2" # 1: admins, 2: moderators
313313
allow_any: false
314314
refresh: true
315-
ai_bot_enabled_chat_bots: # TODO(roman): Deprecated. Remove by Sept 2024
316-
type: list
317-
default: "gpt-3.5-turbo"
318-
hidden: true
319-
choices: "DiscourseAi::Configuration::LlmEnumerator.available_ai_bots"
320315
ai_bot_add_to_header:
321316
default: true
322317
client: true
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
class RemoveOldSettings < ActiveRecord::Migration[7.2]
3+
def up
4+
execute <<~SQL
5+
DELETE FROM site_settings
6+
WHERE name IN ('ai_bot_enabled_chat_bots')
7+
SQL
8+
end
9+
10+
def down
11+
raise ActiveRecord::IrreversibleMigration
12+
end
13+
end

lib/ai_bot/chat_streamer.rb

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# frozen_string_literal: true
2+
#
3+
# Chat streaming APIs are a bit slow, this ensures we properly buffer results
4+
# and stream as quickly as possible.
5+
6+
module DiscourseAi
7+
module AiBot
8+
class ChatStreamer
9+
attr_accessor :cancel
10+
attr_reader :reply,
11+
:guardian,
12+
:thread_id,
13+
:force_thread,
14+
:in_reply_to_id,
15+
:channel,
16+
:cancelled
17+
18+
def initialize(message:, channel:, guardian:, thread_id:, in_reply_to_id:, force_thread:)
19+
@message = message
20+
@channel = channel
21+
@guardian = guardian
22+
@thread_id = thread_id
23+
@force_thread = force_thread
24+
@in_reply_to_id = in_reply_to_id
25+
26+
@queue = Queue.new
27+
28+
db = RailsMultisite::ConnectionManagement.current_db
29+
@worker_thread =
30+
Thread.new { RailsMultisite::ConnectionManagement.with_connection(db) { run } }
31+
32+
@client_id =
33+
ChatSDK::Channel.start_reply(
34+
channel_id: message.chat_channel_id,
35+
guardian: guardian,
36+
thread_id: thread_id,
37+
)
38+
end
39+
40+
def <<(partial)
41+
return if partial.to_s.empty?
42+
43+
if @client_id
44+
ChatSDK::Channel.stop_reply(
45+
channel_id: @message.chat_channel_id,
46+
client_id: @client_id,
47+
guardian: @guardian,
48+
thread_id: @thread_id,
49+
)
50+
@client_id = nil
51+
end
52+
53+
if @reply
54+
@queue << partial
55+
else
56+
create_reply(partial)
57+
end
58+
end
59+
60+
def create_reply(message)
61+
@reply =
62+
ChatSDK::Message.create(
63+
raw: message,
64+
channel_id: channel.id,
65+
guardian: guardian,
66+
force_thread: force_thread,
67+
in_reply_to_id: in_reply_to_id,
68+
enforce_membership: !channel.direct_message_channel?,
69+
)
70+
71+
ChatSDK::Message.start_stream(message_id: @reply.id, guardian: @guardian)
72+
73+
if trailing = message.scan(/\s*\z/).first
74+
@queue << trailing
75+
end
76+
end
77+
78+
def done
79+
@queue << :done
80+
@worker_thread.join
81+
ChatSDK::Message.stop_stream(message_id: @reply.id, guardian: @guardian)
82+
@reply
83+
end
84+
85+
private
86+
87+
def run
88+
done = false
89+
while !done
90+
buffer = +""
91+
popped = @queue.pop
92+
break if popped == :done
93+
94+
buffer << popped
95+
96+
begin
97+
while true
98+
popped = @queue.pop(true)
99+
if popped == :done
100+
done = true
101+
break
102+
end
103+
buffer << popped
104+
end
105+
rescue ThreadError
106+
end
107+
108+
streaming = ChatSDK::Message.stream(message_id: reply.id, raw: buffer, guardian: guardian)
109+
if !streaming
110+
cancel.call
111+
@cancelled = true
112+
end
113+
end
114+
end
115+
end
116+
end
117+
end

lib/ai_bot/playground.rb

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ module AiBot
55
class Playground
66
BYPASS_AI_REPLY_CUSTOM_FIELD = "discourse_ai_bypass_ai_reply"
77
BOT_USER_PREF_ID_CUSTOM_FIELD = "discourse_ai_bot_user_pref_id"
8+
# 10 minutes is enough for vast majority of cases
9+
# there is a small chance that some reasoning models may take longer
10+
MAX_STREAM_DELAY_SECONDS = 600
811

912
attr_reader :bot
1013

@@ -334,42 +337,38 @@ def reply_to_chat_message(message, channel, context_post_ids)
334337
force_thread = message.thread_id.nil? && channel.direct_message_channel?
335338
in_reply_to_id = channel.direct_message_channel? ? message.id : nil
336339

340+
streamer =
341+
ChatStreamer.new(
342+
message: message,
343+
channel: channel,
344+
guardian: guardian,
345+
thread_id: message.thread_id,
346+
in_reply_to_id: in_reply_to_id,
347+
force_thread: force_thread,
348+
)
349+
337350
new_prompts =
338351
bot.reply(context) do |partial, cancel, placeholder, type|
339352
# no support for tools or thinking by design
340353
next if type == :thinking || type == :tool_details || type == :partial_tool
341-
if !reply
342-
# just eat all leading spaces we can not create the message
343-
next if partial.blank?
344-
reply =
345-
ChatSDK::Message.create(
346-
raw: partial,
347-
thread_id: message.thread_id,
348-
channel_id: channel.id,
349-
guardian: guardian,
350-
in_reply_to_id: in_reply_to_id,
351-
force_thread: force_thread,
352-
enforce_membership: !channel.direct_message_channel?,
353-
)
354-
ChatSDK::Message.start_stream(message_id: reply.id, guardian: guardian)
355-
else
356-
streaming =
357-
ChatSDK::Message.stream(message_id: reply.id, raw: partial, guardian: guardian)
358-
359-
if !streaming
360-
cancel&.call
361-
break
362-
end
363-
end
354+
streamer.cancel = cancel
355+
streamer << partial
356+
break if streamer.cancelled
364357
end
365358

366-
if new_prompts.length > 1 && reply.id
359+
reply = streamer.reply
360+
if new_prompts.length > 1 && reply
367361
ChatMessageCustomPrompt.create!(message_id: reply.id, custom_prompt: new_prompts)
368362
end
369363

370-
ChatSDK::Message.stop_stream(message_id: reply.id, guardian: guardian) if reply
364+
if streamer
365+
streamer.done
366+
streamer = nil
367+
end
371368

372369
reply
370+
ensure
371+
streamer.done if streamer
373372
end
374373

375374
def reply_to(
@@ -464,7 +463,7 @@ def reply_to(
464463
publish_update(reply_post, { raw: reply_post.cooked })
465464

466465
redis_stream_key = "gpt_cancel:#{reply_post.id}"
467-
Discourse.redis.setex(redis_stream_key, 60, 1)
466+
Discourse.redis.setex(redis_stream_key, MAX_STREAM_DELAY_SECONDS, 1)
468467
end
469468

470469
context.skip_tool_details ||= !bot.persona.class.tool_details
@@ -504,7 +503,7 @@ def reply_to(
504503

505504
if post_streamer
506505
post_streamer.run_later do
507-
Discourse.redis.expire(redis_stream_key, 60)
506+
Discourse.redis.expire(redis_stream_key, MAX_STREAM_DELAY_SECONDS)
508507
publish_update(reply_post, { raw: raw })
509508
end
510509
end

lib/configuration/llm_enumerator.rb

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,22 @@ def self.global_usage
1313
.where("enabled_chat_bot = ?", true)
1414
.pluck(:id)
1515
.each { |llm_id| rval[llm_id] << { type: :ai_bot } }
16-
17-
AiPersona
18-
.where("force_default_llm = ?", true)
19-
.pluck(:default_llm_id, :name, :id)
20-
.each { |llm_id, name, id| rval[llm_id] << { type: :ai_persona, name: name, id: id } }
2116
end
2217

18+
# this is unconditional, so it is clear that we always signal configuration
19+
AiPersona
20+
.where("default_llm_id IS NOT NULL")
21+
.pluck(:default_llm_id, :name, :id)
22+
.each { |llm_id, name, id| rval[llm_id] << { type: :ai_persona, name: name, id: id } }
23+
2324
if SiteSetting.ai_helper_enabled
2425
model_id = SiteSetting.ai_helper_model.split(":").last.to_i
25-
rval[model_id] << { type: :ai_helper }
26+
rval[model_id] << { type: :ai_helper } if model_id != 0
27+
end
28+
29+
if SiteSetting.ai_helper_image_caption_model
30+
model_id = SiteSetting.ai_helper_image_caption_model.split(":").last.to_i
31+
rval[model_id] << { type: :ai_helper_image_caption } if model_id != 0
2632
end
2733

2834
if SiteSetting.ai_summarization_enabled
@@ -42,6 +48,25 @@ def self.global_usage
4248
rval[model_id] << { type: :ai_spam }
4349
end
4450

51+
if defined?(DiscourseAutomation::Automation)
52+
DiscourseAutomation::Automation
53+
.joins(:fields)
54+
.where(script: %w[llm_report llm_triage])
55+
.where("discourse_automation_fields.name = ?", "model")
56+
.pluck(
57+
"metadata ->> 'value', discourse_automation_automations.name, discourse_automation_automations.id",
58+
)
59+
.each do |model_text, name, id|
60+
next if model_text.blank?
61+
model_id = model_text.split("custom:").last.to_i
62+
if model_id.present?
63+
if model_text =~ /custom:(\d+)/
64+
rval[model_id] << { type: :automation, name: name, id: id }
65+
end
66+
end
67+
end
68+
end
69+
4570
rval
4671
end
4772

@@ -85,45 +110,6 @@ def self.values(allowed_seeded_llms: nil)
85110
values.each { |value_h| value_h[:value] = "custom:#{value_h[:value]}" }
86111
values
87112
end
88-
89-
# TODO(roman): Deprecated. Remove by Sept 2024
90-
def self.old_summarization_options
91-
%w[
92-
gpt-4
93-
gpt-4-32k
94-
gpt-4-turbo
95-
gpt-4o
96-
gpt-3.5-turbo
97-
gpt-3.5-turbo-16k
98-
gemini-pro
99-
gemini-1.5-pro
100-
gemini-1.5-flash
101-
claude-2
102-
claude-instant-1
103-
claude-3-haiku
104-
claude-3-sonnet
105-
claude-3-opus
106-
mistralai/Mixtral-8x7B-Instruct-v0.1
107-
mistralai/Mixtral-8x7B-Instruct-v0.1
108-
]
109-
end
110-
111-
# TODO(roman): Deprecated. Remove by Sept 2024
112-
def self.available_ai_bots
113-
%w[
114-
gpt-3.5-turbo
115-
gpt-4
116-
gpt-4-turbo
117-
gpt-4o
118-
claude-2
119-
gemini-1.5-pro
120-
mixtral-8x7B-Instruct-V0.1
121-
claude-3-opus
122-
claude-3-sonnet
123-
claude-3-haiku
124-
cohere-command-r-plus
125-
]
126-
end
127113
end
128114
end
129115
end

0 commit comments

Comments
 (0)