diff --git a/app/controllers/discourse_ai/summarization/summary_controller.rb b/app/controllers/discourse_ai/summarization/summary_controller.rb
deleted file mode 100644
index 11677f62..00000000
--- a/app/controllers/discourse_ai/summarization/summary_controller.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Summarization
- class SummaryController < ::ApplicationController
- requires_plugin ::DiscourseAi::PLUGIN_NAME
- requires_login
-
- VALID_SINCE_VALUES = [1, 3, 6, 12, 24, 72, 168]
- VALID_TARGETS = %w[chat_channel topic]
-
- def show
- raise PluginDisabled unless SiteSetting.ai_summarization_enabled
- target_type = params[:target_type]
-
- raise Discourse::InvalidParameters.new(:target_type) if !VALID_TARGETS.include?(target_type)
-
- since = nil
-
- if target_type == "chat_channel"
- since = params[:since].to_i
- raise Discourse::InvalidParameters.new(:since) if !VALID_SINCE_VALUES.include?(since)
- target = Chat::Channel.find_by(id: params[:target_id])
- raise Discourse::NotFound.new(:chat_channel) if !target
- raise Discourse::InvalidAccess if !guardian.can_join_chat_channel?(target)
- else
- target = Topic.find_by(id: params[:target_id])
- raise Discourse::NotFound.new(:topic) if !target
- raise Discourse::InvalidAccess if !guardian.can_see_topic?(target)
- end
-
- RateLimiter.new(
- current_user,
- "ai_summarization",
- 6,
- SiteSetting.ai_summarization_rate_limit_minutes.minutes,
- ).performed!
-
- hijack do
- summary =
- DiscourseAi::Summarization::SummaryGenerator.new(target, current_user).summarize!(since)
-
- render json: { summary: summary }, status: 200
- end
- end
- end
- end
-end
diff --git a/assets/javascripts/discourse/components/ai-summary.hbs b/assets/javascripts/discourse/components/ai-summary.hbs
deleted file mode 100644
index d6902b01..00000000
--- a/assets/javascripts/discourse/components/ai-summary.hbs
+++ /dev/null
@@ -1,32 +0,0 @@
-
- {{#if @allowTimeframe}}
- {{i18n "discourse_ai.summarization.description"}}
-
- {{/if}}
-
-
-
-
- {{#unless this.loading}}
-
- {{/unless}}
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/javascripts/discourse/components/ai-summary.js b/assets/javascripts/discourse/components/ai-summary.js
deleted file mode 100644
index da00c497..00000000
--- a/assets/javascripts/discourse/components/ai-summary.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import Component from "@glimmer/component";
-import { tracked } from "@glimmer/tracking";
-import { action } from "@ember/object";
-import { ajax } from "discourse/lib/ajax";
-import { popupAjaxError } from "discourse/lib/ajax-error";
-import I18n from "I18n";
-
-export default class AiSummary extends Component {
- @tracked sinceHours = null;
- @tracked loading = false;
- @tracked availableSummaries = {};
- @tracked summary = null;
- sinceOptions = [
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 1 }),
- value: 1,
- },
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 3 }),
- value: 3,
- },
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 6 }),
- value: 6,
- },
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 12 }),
- value: 12,
- },
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 24 }),
- value: 24,
- },
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 72 }),
- value: 72,
- },
- {
- name: I18n.t("discourse_ai.summarization.since", { count: 168 }),
- value: 168,
- },
- ];
-
- get canSummarize() {
- return (!this.args.allowTimeframe || this.sinceHours) && !this.loading;
- }
-
- @action
- summarize(value) {
- this.loading = true;
- const attrs = {
- target_id: this.args.targetId,
- target_type: this.args.targetType,
- };
-
- if (this.args.allowTimeframe) {
- this.sinceHours = value;
-
- if (this.availableSummaries[this.sinceHours]) {
- this.summary = this.availableSummaries[this.sinceHours];
- this.loading = false;
- return;
- } else {
- attrs.since = this.sinceHours;
- }
- }
-
- ajax("/discourse-ai/summarization/summary", {
- method: "POST",
- data: attrs,
- })
- .then((data) => {
- if (this.args.allowTimeframe) {
- this.availableSummaries[this.sinceHours] = data.summary;
- this.summary = this.availableSummaries[this.sinceHours];
- } else {
- this.summary = data.summary;
- }
- })
- .catch(popupAjaxError)
- .finally(() => (this.loading = false));
- }
-}
diff --git a/assets/javascripts/discourse/templates/modal/ai-summary.hbs b/assets/javascripts/discourse/templates/modal/ai-summary.hbs
deleted file mode 100644
index 15b9273d..00000000
--- a/assets/javascripts/discourse/templates/modal/ai-summary.hbs
+++ /dev/null
@@ -1,6 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/javascripts/initializers/chat-summary.js b/assets/javascripts/initializers/chat-summary.js
deleted file mode 100644
index 97e78684..00000000
--- a/assets/javascripts/initializers/chat-summary.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { withPluginApi } from "discourse/lib/plugin-api";
-import showModal from "discourse/lib/show-modal";
-import { action } from "@ember/object";
-
-function initializeChatChannelSummary(api) {
- const chat = api.container.lookup("service:chat");
- if (chat) {
- api.registerChatComposerButton?.({
- translatedLabel: "discourse_ai.summarization.title",
- id: "chat_channel_summary",
- icon: "discourse-sparkles",
- action: "showChannelSummary",
- position: "dropdown",
- });
-
- api.modifyClass("component:chat-composer", {
- pluginId: "discourse-ai",
-
- @action
- showChannelSummary() {
- showModal("ai-summary").setProperties({
- targetId: this.args.channel.id,
- targetType: "chat_channel",
- allowTimeframe: true,
- });
- },
- });
- }
-}
-
-export default {
- name: "discourse-ai-chat-channel-summary",
-
- initialize(container) {
- const settings = container.lookup("service:site-settings");
- const summarizationEnabled =
- settings.discourse_ai_enabled && settings.ai_summarization_enabled;
-
- if (summarizationEnabled) {
- withPluginApi("1.6.0", initializeChatChannelSummary);
- }
- },
-};
diff --git a/assets/javascripts/initializers/topic-summary.js b/assets/javascripts/initializers/topic-summary.js
deleted file mode 100644
index cc3174d5..00000000
--- a/assets/javascripts/initializers/topic-summary.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { withPluginApi } from "discourse/lib/plugin-api";
-import showModal from "discourse/lib/show-modal";
-
-function initializeTopicSummary(api) {
- api.modifyClass("component:scrolling-post-stream", {
- pluginId: "discourse-ai",
-
- showAiSummary() {
- showModal("ai-summary").setProperties({
- targetId: this.posts["posts"][0].topic_id,
- targetType: "topic",
- allowTimeframe: false,
- });
- },
- });
-
- api.addTopicSummaryCallback((html, attrs, widget) => {
- html.push(
- widget.attach("button", {
- className: "btn btn-primary topic-ai-summarization",
- icon: "magic",
- title: "discourse_ai.summarization.title",
- label: "discourse_ai.summarization.title",
- action: "showAiSummary",
- })
- );
-
- return html;
- });
-}
-
-export default {
- name: "discourse-ai-topic-summary",
-
- initialize(container) {
- const user = container.lookup("service:current-user");
-
- if (user) {
- const settings = container.lookup("service:site-settings");
- const summarizationEnabled =
- settings.discourse_ai_enabled && settings.ai_summarization_enabled;
-
- if (summarizationEnabled) {
- withPluginApi("1.6.0", initializeTopicSummary);
- }
- }
- },
-};
diff --git a/assets/stylesheets/modules/summarization/common/summarization.scss b/assets/stylesheets/modules/summarization/common/summarization.scss
deleted file mode 100644
index d1bfce31..00000000
--- a/assets/stylesheets/modules/summarization/common/summarization.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-.ai-summary-modal {
- .summarization-since,
- .summary-area {
- margin: 10px 0 10px 0;
- }
-
- .summary-area {
- min-height: 200px;
- }
-}
diff --git a/assets/stylesheets/modules/summarization/desktop/summarization.scss b/assets/stylesheets/modules/summarization/desktop/summarization.scss
deleted file mode 100644
index 8d9a425c..00000000
--- a/assets/stylesheets/modules/summarization/desktop/summarization.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.topic-ai-summarization {
- margin-left: 10px;
-}
diff --git a/assets/stylesheets/modules/summarization/mobile/summarization.scss b/assets/stylesheets/modules/summarization/mobile/summarization.scss
deleted file mode 100644
index e40c1126..00000000
--- a/assets/stylesheets/modules/summarization/mobile/summarization.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.toggle-summary {
- button {
- width: 100%;
- }
-
- .topic-ai-summarization {
- margin-top: 10px;
- }
-}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 21b9d7f3..687461d5 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -17,14 +17,6 @@ en:
embeddings:
semantic_search: "Topics (Semantic)"
- summarization:
- title: "Summarize using AI"
- description: "Select an option below to summarize the conversation sent during the desired timeframe."
- summarize: "Summarize"
- since:
- one: "Last hour"
- other: "Last %{count} hours"
-
ai_bot:
cancel_streaming: "Stop reply"
default_pm_prefix: "[Untitled AI bot PM]"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index c344a7fc..68efd113 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -52,11 +52,8 @@ en:
ai_embeddings_semantic_search_enabled: "Enable full-page semantic search."
ai_embeddings_semantic_related_include_closed_topics: "Include closed topics in semantic search results"
- ai_summarization_enabled: "Enable the summarization module."
ai_summarization_discourse_service_api_endpoint: "URL where the Discourse summarization API is running."
ai_summarization_discourse_service_api_key: "API key for the Discourse summarization API."
- ai_summarization_model: "Model to use for summarization."
- ai_summarization_rate_limit_minutes: "Minutes to elapse after the summarization limit is reached (6 requests)."
ai_bot_enabled: "Enable the AI Bot module."
ai_bot_allowed_groups: "When the GPT Bot has access to the PM, it will reply to members of these groups."
@@ -116,3 +113,8 @@ en:
google:
one: "Found %{count} result for '%{query}'"
other: "Found %{count} results for '%{query}'"
+
+ summarization:
+ configuration_hint:
+ one: "Configure the `%{setting}` setting first."
+ other: "Configure these settings first: %{settings}"
diff --git a/config/routes.rb b/config/routes.rb
index 917a6510..ad451dea 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,10 +10,6 @@ DiscourseAi::Engine.routes.draw do
get "semantic-search" => "embeddings#search"
end
- scope module: :summarization, path: "/summarization", defaults: { format: :json } do
- post "summary" => "summary#show"
- end
-
scope module: :ai_bot, path: "/ai-bot", defaults: { format: :json } do
post "post/:post_id/stop-streaming" => "bot#stop_streaming_response"
get "bot-username" => "bot#show_bot_username"
diff --git a/config/settings.yml b/config/settings.yml
index 898126b0..077b97ea 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -182,26 +182,10 @@ plugins:
default: false
client: true
- ai_summarization_enabled:
- default: false
- client: true
ai_summarization_discourse_service_api_endpoint: ""
ai_summarization_discourse_service_api_key:
default: ""
secret: true
- ai_summarization_model:
- type: enum
- default: "bart-large-cnn-samsum"
- allow_any: false
- choices:
- - bart-large-cnn-samsum
- - flan-t5-base-samsum
- - long-t5-tglobal-base-16384-book-summary
- - gpt-3.5-turbo
- - gpt-4
- - claude-v1
- - claude-v1-100k
- ai_summarization_rate_limit_minutes: 10
ai_bot_enabled:
default: false
diff --git a/lib/modules/summarization/entry_point.rb b/lib/modules/summarization/entry_point.rb
index 9d18bc25..c4be1baf 100644
--- a/lib/modules/summarization/entry_point.rb
+++ b/lib/modules/summarization/entry_point.rb
@@ -4,10 +4,21 @@ module DiscourseAi
module Summarization
class EntryPoint
def load_files
- require_relative "summary_generator"
+ require_relative "strategies/anthropic"
+ require_relative "strategies/discourse_ai"
+ require_relative "strategies/open_ai"
end
def inject_into(plugin)
+ [
+ Strategies::OpenAi.new("gpt-4"),
+ Strategies::OpenAi.new("gpt-3.5-turbo"),
+ Strategies::DiscourseAi.new("bart-large-cnn-samsum"),
+ Strategies::DiscourseAi.new("flan-t5-base-samsum"),
+ Strategies::DiscourseAi.new("long-t5-tglobal-base-16384-book-summary"),
+ Strategies::Anthropic.new("claude-v1"),
+ Strategies::Anthropic.new("claude-v1-100k"),
+ ].each { |strategy| plugin.register_summarization_strategy(strategy) }
end
end
end
diff --git a/lib/modules/summarization/strategies/anthropic.rb b/lib/modules/summarization/strategies/anthropic.rb
new file mode 100644
index 00000000..57e08285
--- /dev/null
+++ b/lib/modules/summarization/strategies/anthropic.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Summarization
+ module Strategies
+ class Anthropic < ::Summarization::Base
+ def display_name
+ "Anthropic's #{model}"
+ end
+
+ def correctly_configured?
+ SiteSetting.ai_anthropic_api_key.present?
+ end
+
+ def configuration_hint
+ I18n.t(
+ "discourse_ai.summarization.configuration_hint",
+ count: 1,
+ setting: "ai_anthropic_api_key",
+ )
+ end
+
+ def summarize(content_text)
+ response =
+ ::DiscourseAi::Inference::AnthropicCompletions.perform!(
+ prompt(content_text),
+ model,
+ ).dig(:completion)
+
+ Nokogiri::HTML5.fragment(response).at("ai").text
+ end
+
+ def prompt(content)
+ truncated_content =
+ ::DiscourseAi::Tokenizer::AnthropicTokenizer.truncate(content, max_length - 50)
+
+ "Human: Summarize the following article that is inside tags.
+ Please include only the summary inside tags.
+
+ ##{truncated_content}
+
+
+ Assistant:
+ "
+ end
+
+ private
+
+ def max_length
+ lengths = { "claude-v1" => 9000, "claude-v1-100k" => 100_000 }
+
+ lengths[model]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/modules/summarization/strategies/discourse_ai.rb b/lib/modules/summarization/strategies/discourse_ai.rb
new file mode 100644
index 00000000..363b40b8
--- /dev/null
+++ b/lib/modules/summarization/strategies/discourse_ai.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Summarization
+ module Strategies
+ class DiscourseAi < ::Summarization::Base
+ def display_name
+ "Discourse AI's #{model}"
+ end
+
+ def correctly_configured?
+ SiteSetting.ai_summarization_discourse_service_api_endpoint.present? &&
+ SiteSetting.ai_summarization_discourse_service_api_key.present?
+ end
+
+ def configuration_hint
+ I18n.t(
+ "discourse_ai.summarization.configuration_hint",
+ count: 2,
+ settings:
+ "ai_summarization_discourse_service_api_endpoint, ai_summarization_discourse_service_api_key",
+ )
+ end
+
+ def summarize(content_text)
+ ::DiscourseAi::Inference::DiscourseClassifier.perform!(
+ "#{SiteSetting.ai_summarization_discourse_service_api_endpoint}/api/v1/classify",
+ model,
+ prompt(content_text),
+ SiteSetting.ai_summarization_discourse_service_api_key,
+ ).dig(:summary_text)
+ end
+
+ def prompt(text)
+ ::DiscourseAi::Tokenizer::BertTokenizer.truncate(text, max_length)
+ end
+
+ private
+
+ def max_length
+ lengths = {
+ "bart-large-cnn-samsum" => 1024,
+ "flan-t5-base-samsum" => 512,
+ "long-t5-tglobal-base-16384-book-summary" => 16_384,
+ }
+
+ lengths[model]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/modules/summarization/strategies/open_ai.rb b/lib/modules/summarization/strategies/open_ai.rb
new file mode 100644
index 00000000..2da7b508
--- /dev/null
+++ b/lib/modules/summarization/strategies/open_ai.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module DiscourseAi
+ module Summarization
+ module Strategies
+ class OpenAi < ::Summarization::Base
+ def display_name
+ "Open AI's #{model}"
+ end
+
+ def correctly_configured?
+ SiteSetting.ai_openai_api_key.present?
+ end
+
+ def configuration_hint
+ I18n.t(
+ "discourse_ai.summarization.configuration_hint",
+ count: 1,
+ setting: "ai_openai_api_key",
+ )
+ end
+
+ def summarize(content_text)
+ ::DiscourseAi::Inference::OpenAiCompletions.perform!(prompt(content_text), model).dig(
+ :choices,
+ 0,
+ :message,
+ :content,
+ )
+ end
+
+ def prompt(content)
+ truncated_content =
+ ::DiscourseAi::Tokenizer::OpenAiTokenizer.truncate(content, max_length - 50)
+
+ messages = [{ role: "system", content: <<~TEXT }]
+ Summarize the following article:\n\n#{truncated_content}
+ TEXT
+ end
+
+ private
+
+ def max_length
+ lengths = { "gpt-3.5-turbo" => 4096, "gpt-4" => 8192 }
+
+ lengths[model]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/modules/summarization/summary_generator.rb b/lib/modules/summarization/summary_generator.rb
deleted file mode 100644
index fe447d3b..00000000
--- a/lib/modules/summarization/summary_generator.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Summarization
- class SummaryGenerator
- def initialize(target, user)
- @target = target
- @user = user
- end
-
- def summarize!(content_since)
- content = get_content(content_since)
-
- send("#{summarization_provider}_summarization", content)
- end
-
- private
-
- attr_reader :target, :user
-
- def summarization_provider
- case model
- in "gpt-3.5-turbo" | "gpt-4"
- "openai"
- in "claude-v1" | "claude-v1-100k"
- "anthropic"
- else
- "discourse"
- end
- end
-
- def get_content(content_since)
- case target
- in Post
- target.raw
- in Topic
- TopicView
- .new(
- target,
- user,
- {
- filter: "summary",
- exclude_deleted_users: true,
- exclude_hidden: true,
- show_deleted: false,
- },
- )
- .posts
- .pluck(:raw)
- .join("\n")
- in ::Chat::Channel
- target
- .chat_messages
- .where("chat_messages.created_at > ?", content_since.hours.ago)
- .includes(:user)
- .order(created_at: :asc)
- .pluck(:username_lower, :message)
- .map { "#{_1}: #{_2}" }
- .join("\n")
- else
- raise "Can't find content to summarize"
- end
- end
-
- def discourse_summarization(content)
- truncated_content = DiscourseAi::Tokenizer::BertTokenizer.truncate(content, max_length)
-
- ::DiscourseAi::Inference::DiscourseClassifier.perform!(
- "#{SiteSetting.ai_summarization_discourse_service_api_endpoint}/api/v1/classify",
- model,
- truncated_content,
- SiteSetting.ai_summarization_discourse_service_api_key,
- ).dig(:summary_text)
- end
-
- def openai_summarization(content)
- truncated_content =
- DiscourseAi::Tokenizer::OpenAiTokenizer.truncate(content, max_length - 50)
-
- messages = [{ role: "system", content: <<~TEXT }]
- Summarize the following article:\n\n#{truncated_content}
- TEXT
-
- ::DiscourseAi::Inference::OpenAiCompletions.perform!(messages, model).dig(
- :choices,
- 0,
- :message,
- :content,
- )
- end
-
- def anthropic_summarization(content)
- truncated_content =
- DiscourseAi::Tokenizer::AnthropicTokenizer.truncate(content, max_length - 50)
-
- messages =
- "Human: Summarize the following article that is inside tags.
- Please include only the summary inside tags.
-
- ##{truncated_content}
-
-
- Assistant:
- "
-
- response =
- ::DiscourseAi::Inference::AnthropicCompletions.perform!(messages, model).dig(:completion)
-
- Nokogiri::HTML5.fragment(response).at("ai").text
- end
-
- def model
- SiteSetting.ai_summarization_model
- end
-
- def max_length
- lengths = {
- "bart-large-cnn-samsum" => 1024,
- "flan-t5-base-samsum" => 512,
- "long-t5-tglobal-base-16384-book-summary" => 16_384,
- "gpt-3.5-turbo" => 4096,
- "gpt-4" => 8192,
- "claude-v1" => 9000,
- "claude-v1-100k" => 100_000,
- }
-
- lengths[model]
- end
- end
- end
-end
diff --git a/plugin.rb b/plugin.rb
index 3ab95a48..73aaaf36 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -14,10 +14,6 @@ enabled_site_setting :discourse_ai_enabled
register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss"
-register_asset "stylesheets/modules/summarization/common/summarization.scss"
-register_asset "stylesheets/modules/summarization/desktop/summarization.scss", :desktop
-register_asset "stylesheets/modules/summarization/mobile/summarization.scss", :mobile
-
register_asset "stylesheets/modules/ai-bot/common/bot-replies.scss"
module ::DiscourseAi
diff --git a/spec/lib/modules/summarization/strategies/anthropic_spec.rb b/spec/lib/modules/summarization/strategies/anthropic_spec.rb
new file mode 100644
index 00000000..afd64f13
--- /dev/null
+++ b/spec/lib/modules/summarization/strategies/anthropic_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../../../../support/anthropic_completion_stubs"
+
+RSpec.describe DiscourseAi::Summarization::Strategies::Anthropic do
+ describe "#summarize" do
+ let(:model) { "claude-v1" }
+
+ subject { described_class.new(model) }
+
+ it "asks an Anthropic's model to summarize the content" do
+ summarization_text = "This is a text"
+ expected_response = "This is a summary"
+
+ AnthropicCompletionStubs.stub_response(
+ subject.prompt(summarization_text),
+ "#{expected_response}",
+ req_opts: {
+ max_tokens_to_sample: 300,
+ },
+ )
+
+ expect(subject.summarize(summarization_text)).to eq(expected_response)
+ end
+ end
+end
diff --git a/spec/lib/modules/summarization/strategies/discourse_spec.rb b/spec/lib/modules/summarization/strategies/discourse_spec.rb
new file mode 100644
index 00000000..8d52e069
--- /dev/null
+++ b/spec/lib/modules/summarization/strategies/discourse_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseAi::Summarization::Strategies::DiscourseAi do
+ describe "#summarize" do
+ let(:model) { "bart-large-cnn-samsum" }
+
+ subject { described_class.new(model) }
+
+ it "asks a Discourse's model to summarize the content" do
+ SiteSetting.ai_summarization_discourse_service_api_endpoint = "https://test.com"
+ summarization_text = "This is a text"
+ expected_response = "This is a summary"
+
+ WebMock
+ .stub_request(
+ :post,
+ "#{SiteSetting.ai_summarization_discourse_service_api_endpoint}/api/v1/classify",
+ )
+ .with(body: JSON.dump(model: model, content: subject.prompt(summarization_text)))
+ .to_return(status: 200, body: JSON.dump(summary_text: expected_response))
+
+ expect(subject.summarize(summarization_text)).to eq(expected_response)
+ end
+ end
+end
diff --git a/spec/lib/modules/summarization/strategies/open_ai_spec.rb b/spec/lib/modules/summarization/strategies/open_ai_spec.rb
new file mode 100644
index 00000000..a83ca4db
--- /dev/null
+++ b/spec/lib/modules/summarization/strategies/open_ai_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../../../../support/openai_completions_inference_stubs"
+
+RSpec.describe DiscourseAi::Summarization::Strategies::OpenAi do
+ let(:model) { "gpt-3.5-turbo" }
+
+ subject { described_class.new(model) }
+
+ it "asks a OpenAI's model to summarize the content" do
+ summarization_text = "This is a text"
+ expected_response = "This is a summary"
+
+ OpenAiCompletionsInferenceStubs.stub_response(
+ subject.prompt(summarization_text),
+ expected_response,
+ )
+
+ expect(subject.summarize(summarization_text)).to eq(expected_response)
+ end
+end
diff --git a/spec/requests/summarization/summary_controller_spec.rb b/spec/requests/summarization/summary_controller_spec.rb
deleted file mode 100644
index 609be193..00000000
--- a/spec/requests/summarization/summary_controller_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe DiscourseAi::Summarization::SummaryController do
- describe "#show" do
- fab!(:user) { Fabricate(:user) }
- let!(:channel_group) { Fabricate(:group) }
- let!(:chat_channel) { Fabricate(:private_category_channel, group: channel_group) }
-
- before do
- SiteSetting.ai_summarization_enabled = true
- sign_in(user)
- end
-
- context "when summarizing a chat channel" do
- context "if the user can see the channel" do
- before { channel_group.add(user) }
-
- describe "validating inputs" do
- it "returns a 404 if there is no chat channel" do
- post "/discourse-ai/summarization/summary",
- params: {
- target_type: "chat_channel",
- target_id: 99,
- since: 3,
- }
-
- expect(response.status).to eq(404)
- end
-
- it "returns a 400 if the since param is invalid" do
- post "/discourse-ai/summarization/summary",
- params: {
- target_type: "chat_channel",
- target_id: chat_channel.id,
- since: 0,
- }
-
- expect(response.status).to eq(400)
- end
-
- it "returns a 404 when the module is disabled" do
- SiteSetting.ai_summarization_enabled = false
-
- post "/discourse-ai/summarization/summary",
- params: {
- target_type: "chat_channel",
- target_id: chat_channel.id,
- since: 1,
- }
-
- expect(response.status).to eq(404)
- end
- end
-
- context "if the user can't see the channel" do
- before { channel_group.remove(user) }
-
- it "returns a 403 if the user can't see the chat channel" do
- post "/discourse-ai/summarization/summary",
- params: {
- target_type: "chat_channel",
- target_id: chat_channel.id,
- since: 1,
- }
-
- expect(response.status).to eq(403)
- end
- end
- end
- end
- end
-end
diff --git a/spec/support/summarization_stubs.rb b/spec/support/summarization_stubs.rb
deleted file mode 100644
index c46359b8..00000000
--- a/spec/support/summarization_stubs.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-class SummarizationStubs
- class << self
- def test_summary
- "This is a summary"
- end
-
- def openai_response(content)
- {
- id: "chatcmpl-6sZfAb30Rnv9Q7ufzFwvQsMpjZh8S",
- object: "chat.completion",
- created: 1_678_464_820,
- model: "gpt-3.5-turbo-0301",
- usage: {
- prompt_tokens: 337,
- completion_tokens: 162,
- total_tokens: 499,
- },
- choices: [
- { message: { role: "assistant", content: content }, finish_reason: "stop", index: 0 },
- ],
- }
- end
-
- def openai_chat_summarization_stub(chat_messages)
- prompt_messages =
- chat_messages
- .sort_by(&:created_at)
- .map { |m| "#{m.user.username_lower}: #{m.message}" }
- .join("\n")
-
- summary_prompt = [{ role: "system", content: <<~TEXT }]
- Summarize the following article:\n\n#{prompt_messages}
- TEXT
-
- WebMock
- .stub_request(:post, "https://api.openai.com/v1/chat/completions")
- .with(body: { model: "gpt-4", messages: summary_prompt }.to_json)
- .to_return(status: 200, body: JSON.dump(openai_response(test_summary)))
- end
-
- def openai_topic_summarization_stub(topic, user)
- prompt_posts = TopicView.new(topic, user, { filter: "summary" }).posts.map(&:raw).join("\n")
-
- summary_prompt = [{ role: "system", content: <<~TEXT }]
- Summarize the following article:\n\n#{prompt_posts}
- TEXT
-
- WebMock
- .stub_request(:post, "https://api.openai.com/v1/chat/completions")
- .with(body: { model: "gpt-4", messages: summary_prompt }.to_json)
- .to_return(status: 200, body: JSON.dump(openai_response(test_summary)))
- end
- end
-end
diff --git a/spec/system/page_objects/modals/summarization.rb b/spec/system/page_objects/modals/summarization.rb
deleted file mode 100644
index 8ab036cb..00000000
--- a/spec/system/page_objects/modals/summarization.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module PageObjects
- module Modals
- class Summarization < PageObjects::Modals::Base
- def visible?
- page.has_css?(".ai-summary-modal", wait: 5)
- end
-
- def select_timeframe(option)
- find(".summarization-since").click
- find(".select-kit-row[data-value=\"#{option}\"]").click
- end
-
- def summary_value
- find(".summary-area").value
- end
-
- def generate_summary
- find(".ai-summary-modal .create").click
- end
- end
- end
-end
diff --git a/spec/system/summarization/chat_channel_summarization_spec.rb b/spec/system/summarization/chat_channel_summarization_spec.rb
deleted file mode 100644
index 7e0a0fa5..00000000
--- a/spec/system/summarization/chat_channel_summarization_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../../support/summarization_stubs"
-
-RSpec.describe "AI chat channel summarization", type: :system, js: true do
- fab!(:user) { Fabricate(:leader) }
- fab!(:channel) { Fabricate(:chat_channel) }
-
- fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel) }
- fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel) }
- fab!(:message_3) { Fabricate(:chat_message, chat_channel: channel) }
-
- before do
- sign_in(user)
- chat_system_bootstrap(user, [channel])
- SiteSetting.ai_summarization_enabled = true
- SiteSetting.ai_summarization_model = "gpt-4"
- end
-
- let(:summarization_modal) { PageObjects::Modals::Summarization.new }
-
- it "returns a summary using the selected timeframe" do
- visit("/chat/c/-/#{channel.id}")
-
- SummarizationStubs.openai_chat_summarization_stub([message_1, message_2, message_3])
-
- find(".chat-composer-dropdown__trigger-btn").click
- find(".chat-composer-dropdown__action-btn.chat_channel_summary").click
-
- expect(summarization_modal).to be_visible
-
- summarization_modal.select_timeframe("3")
-
- expect(summarization_modal.summary_value).to eq(SummarizationStubs.test_summary)
- end
-end
diff --git a/spec/system/summarization/topic_summarization_spec.rb b/spec/system/summarization/topic_summarization_spec.rb
deleted file mode 100644
index df1c706c..00000000
--- a/spec/system/summarization/topic_summarization_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../../support/summarization_stubs"
-
-RSpec.describe "AI chat channel summarization", type: :system, js: true do
- fab!(:user) { Fabricate(:leader) }
- fab!(:topic) { Fabricate(:topic, has_summary: true) }
-
- fab!(:post_1) { Fabricate(:post, topic: topic) }
- fab!(:post_2) { Fabricate(:post, topic: topic) }
-
- before do
- sign_in(user)
- SiteSetting.ai_summarization_enabled = true
- SiteSetting.ai_summarization_model = "gpt-4"
- end
-
- let(:summarization_modal) { PageObjects::Modals::Summarization.new }
-
- it "returns a summary using the selected timeframe" do
- visit("/t/-/#{topic.id}")
-
- SummarizationStubs.openai_topic_summarization_stub(topic, user)
-
- find(".topic-ai-summarization").click
-
- expect(summarization_modal).to be_visible
-
- summarization_modal.generate_summary
-
- expect(summarization_modal.summary_value).to eq(SummarizationStubs.test_summary)
- end
-end