FEATURE: Continue conversation from Discobot discovery (#1234)

This feature update allows for continuing the conversation with Discobot Discoveries in an AI bot chat. After discoveries gives you a response to your search you can continue with the existing context.
This commit is contained in:
Keegan George 2025-04-01 10:22:39 -07:00 committed by GitHub
parent 5331b6dd8e
commit bf5ccb452c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 192 additions and 0 deletions

View File

@ -69,6 +69,40 @@ module DiscourseAi
render json: {}, status: 200
end
def discover_continue_convo
raise Discourse::InvalidParameters.new("user_id") if !params[:user_id]
raise Discourse::InvalidParameters.new("query") if !params[:query]
raise Discourse::InvalidParameters.new("context") if !params[:context]
user = User.find(params[:user_id])
bot_user_id = AiPersona.find_by(id: SiteSetting.ai_bot_discover_persona).user_id
bot_username = User.find_by(id: bot_user_id).username
query = params[:query]
context = "[quote]\n#{params[:context]}\n[/quote]"
post =
PostCreator.create!(
user,
title:
I18n.t("discourse_ai.ai_bot.discoveries.continue_conversation.title", query: query),
raw:
I18n.t(
"discourse_ai.ai_bot.discoveries.continue_conversation.raw",
query: query,
context: context,
),
archetype: Archetype.private_message,
target_usernames: bot_username,
skip_validations: true,
)
render json: success_json.merge(topic_id: post.topic_id)
rescue StandardError => e
render json: failed_json.merge(errors: [e.message]), status: 422
end
end
end
end

View File

@ -10,11 +10,15 @@ import CookText from "discourse/components/cook-text";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse/lib/decorators";
import { withPluginApi } from "discourse/lib/plugin-api";
import DiscourseURL from "discourse/lib/url";
import Topic from "discourse/models/topic";
import { i18n } from "discourse-i18n";
import SmoothStreamer from "../lib/smooth-streamer";
import AiBlinkingAnimation from "./ai-blinking-animation";
import AiIndicatorWave from "./ai-indicator-wave";
const DISCOVERY_TIMEOUT_MS = 10000;
@ -23,7 +27,11 @@ export default class AiSearchDiscoveries extends Component {
@service messageBus;
@service discobotDiscoveries;
@service appEvents;
@service currentUser;
@service siteSettings;
@service composer;
@tracked loadingConversationTopic = false;
@tracked hideDiscoveries = false;
@tracked fullDiscoveryToggled = false;
@tracked discoveryPreviewLength = this.args.discoveryPreviewLength || 150;
@ -145,6 +153,28 @@ export default class AiSearchDiscoveries extends Component {
return !this.fullDiscoveryToggled && this.canShowExpandtoggle;
}
get canContinueConversation() {
const personas = this.currentUser?.ai_enabled_personas;
const discoverPersona = personas.find(
(persona) => persona.id === this.siteSettings?.ai_bot_discover_persona
);
const discoverPersonaHasBot = discoverPersona?.username;
return (
this.discobotDiscoveries.discovery?.length > 0 &&
!this.smoothStreamer.isStreaming &&
discoverPersonaHasBot
);
}
get continueConvoBtnLabel() {
if (this.loadingConversationTopic) {
return "discourse_ai.discobot_discoveries.loading_convo";
}
return "discourse_ai.discobot_discoveries.continue_convo";
}
@action
async triggerDiscovery() {
if (this.discobotDiscoveries.lastQuery === this.query) {
@ -180,6 +210,43 @@ export default class AiSearchDiscoveries extends Component {
this.fullDiscoveryToggled = !this.fullDiscoveryToggled;
}
@action
async continueConversation() {
const data = {
user_id: this.currentUser.id,
query: this.query,
context: this.discobotDiscoveries.discovery,
};
try {
this.loadingConversationTopic = true;
const continueRequest = await ajax(
`/discourse-ai/ai-bot/discover/continue-convo`,
{
type: "POST",
data,
}
);
const topicJSON = await Topic.find(continueRequest.topic_id, {});
const topic = Topic.create(topicJSON);
DiscourseURL.routeTo(`/t/${continueRequest.topic_id}`, {
afterRouteComplete: () => {
if (this.args.closeSearchMenu) {
this.args.closeSearchMenu();
}
this.composer.focusComposer({
topic,
});
},
});
} catch (e) {
popupAjaxError(e);
} finally {
this.loadingConversationTopic = false;
}
}
timeoutDiscovery() {
this.discobotDiscoveries.loadingDiscoveries = false;
this.discobotDiscoveries.discovery = "";
@ -226,6 +293,18 @@ export default class AiSearchDiscoveries extends Component {
{{/if}}
{{/if}}
</div>
{{#if this.canContinueConversation}}
<div class="ai-search-discoveries__continue-conversation">
<DButton
@action={{this.continueConversation}}
@label={{this.continueConvoBtnLabel}}
class="btn-small"
>
<AiIndicatorWave @loading={{this.loadingConversationTopic}} />
</DButton>
</div>
{{/if}}
</div>
</template>
}

View File

@ -33,6 +33,7 @@ export default class AiDiscobotDiscoveries extends Component {
<AiSearchDiscoveries
@searchTerm={{@outletArgs.searchTerm}}
@discoveryPreviewLength={{50}}
@closeSearchMenu={{@outletArgs.closeSearchMenu}}
/>
{{#if this.search.results.topics.length}}

View File

@ -62,6 +62,10 @@
.cooked p:first-child {
margin-top: 0;
}
&__continue-conversation {
margin-block: 1rem;
}
}
.ai-search-discoveries-tooltip {

View File

@ -731,6 +731,8 @@ en:
main_title: "Discobot discoveries"
regular_results: "Topics"
tell_me_more: "Tell me more..."
continue_convo: "Continue conversation..."
loading_convo: "Loading conversation"
collapse: "Collapse"
timed_out: "Discobot couldn't find any discoveries. Something went wrong."
user_setting: "Enable search discoveries"

View File

@ -422,6 +422,10 @@ en:
search_settings:
one: "Found %{count} result for '%{query}'"
other: "Found %{count} results for '%{query}'"
discoveries:
continue_conversation:
title: "Discovery conversation: Search for %{query}"
raw: "In my search for %{query}, you showed me the following information:\n\n%{context}\n\nLet's continue the conversation."
summarization:
configuration_hint:

View File

@ -26,6 +26,7 @@ DiscourseAi::Engine.routes.draw do
post "post/:post_id/stop-streaming" => "bot#stop_streaming_response"
get "discover" => "bot#discover"
post "discover/continue-convo" => "bot#discover_continue_convo"
end
scope module: :ai_bot, path: "/ai-bot/shared-ai-conversations" do

View File

@ -168,4 +168,71 @@ RSpec.describe DiscourseAi::AiBot::BotController do
end
end
end
describe "#discover_continue_convo" do
before { SiteSetting.ai_bot_enabled = true }
fab!(:group)
fab!(:llm_model)
fab!(:ai_persona) do
persona = Fabricate(:ai_persona, allowed_group_ids: [group.id], default_llm_id: llm_model.id)
persona.create_user!
persona
end
let(:query) { "What is Discourse?" }
let(:context) { "Discourse is an open-source discussion platform." }
context "when the user is allowed to discover" do
before do
SiteSetting.ai_bot_discover_persona = ai_persona.id
group.add(user)
end
it "returns a 200 and creates a private message topic" do
expect {
post "/discourse-ai/ai-bot/discover/continue-convo",
params: {
user_id: user.id,
query: query,
context: context,
}
}.to change(Topic, :count).by(1)
expect(response.status).to eq(200)
expect(response.parsed_body["topic_id"]).to be_present
end
it "returns invalid parameters if the user_id is missing" do
post "/discourse-ai/ai-bot/discover/continue-convo",
params: {
query: query,
context: context,
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include("user_id")
end
it "returns invalid parameters if the query is missing" do
post "/discourse-ai/ai-bot/discover/continue-convo",
params: {
user_id: user.id,
context: context,
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include("query")
end
it "returns invalid parameters if the context is missing" do
post "/discourse-ai/ai-bot/discover/continue-convo",
params: {
user_id: user.id,
query: query,
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include("context")
end
end
end
end