Compare commits

...
Author SHA1 Message Date
David Taylor 593e8dc2a5 DEV: Update modifyClass calls
- Update `controller:topic` modification to native class syntax
- Remove search-result-entry modification in favour of plugin-outlet-based solution
2024-12-02 11:49:22 +00:00
Roman Rizzi 0abd4b1244 FIX: Sentiment classification results needs to be transformed before saving (#983) 2024-11-29 17:31:56 -03:00
David Taylor 120a20c5cd DEV: Convert to native class syntax (#981) 2024-11-29 16:41:09 +01:00
Sam 0cb2c413ba FEATURE: exclude muted categories from category suggester (#979)
The logic here is that users do not particularly care about
topics in the category so we can exclude them from tag
and category suggestions
2024-11-29 12:17:28 +11:00
Rafael dos Santos Silva 80adefa1c1 FIX: Move count logic to the end for tag suggestions (#978) 2024-11-28 16:27:38 -03:00
Sam bc0657f478 FEATURE: AI Usage page (#964)
- Added a new admin interface to track AI usage metrics, including tokens, features, and models.
- Introduced a new route `/admin/plugins/discourse-ai/ai-usage` and supporting API endpoint in `AiUsageController`.
- Implemented `AiUsageSerializer` for structuring AI usage data.
- Integrated CSS stylings for charts and tables under `stylesheets/modules/llms/common/usage.scss`.
- Enhanced backend with `AiApiAuditLog` model changes: added `cached_tokens` column  (implemented with OpenAI for now) with relevant DB migration and indexing.
- Created `Report` module for efficient aggregation and filtering of AI usage metrics.
- Updated AI Bot title generation logic to log correctly to user vs bot
- Extended test coverage for the new tracking features, ensuring data consistency and access controls.
2024-11-29 06:26:48 +11:00
Roman Rizzi c980c34d77 REFACTOR: Simplify sentiment classification (#977)
This change adds a simpler class for sentiment classification, replacing the soon-to-be removed `Classificator` hierarchy. Additionally, it adds a method for classifying concurrently, speeding up the backfill rake task.
2024-11-28 15:38:23 -03:00
Rafael dos Santos Silva 6456a4f44a FIX: Keep related topics data cached when a user scrolls up (#976)
See https://meta.discourse.org/t/related-topics-missing-after-rereading-a-topic-with-more-than-20-posts/306147/6
2024-11-28 14:25:15 -03:00
Rafael dos Santos Silva eacc4ed945 UX: Fix styling of AI Helper on quick edit (#975) 2024-11-28 13:06:48 -03:00
David Taylor 4a2369356f DEV: Update linting (#974) 2024-11-28 16:07:42 +01:00
Rafael dos Santos Silva 4980a4b2f7 FIX: Multiple concurrent summaries could result in pg index errors (#973) 2024-11-28 11:53:04 -03:00
48 changed files with 1827 additions and 241 deletions
@@ -1,18 +1,18 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
queryParams: {
export default class AdminPluginsShowDiscourseAiLlmsNew extends DiscourseRoute {
queryParams = {
llmTemplate: { refreshModel: true },
},
};
async model() {
const record = this.store.createRecord("ai-llm");
record.provider_params = {};
return record;
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
@@ -21,5 +21,5 @@ export default DiscourseRoute.extend({
"llmTemplate",
this.paramsFor(this.routeName).llmTemplate || null
);
},
});
}
}
@@ -1,19 +1,19 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
export default class AdminPluginsShowDiscourseAiLlmsShow extends DiscourseRoute {
async model(params) {
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const id = parseInt(params.id, 10);
const record = allLlms.findBy("id", id);
record.provider_params = record.provider_params || {};
return record;
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
);
},
});
}
}
@@ -1,7 +1,7 @@
import { AUTO_GROUPS } from "discourse/lib/constants";
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRoute {
async model() {
const record = this.store.createRecord("ai-persona");
record.set("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
@@ -13,13 +13,13 @@ export default DiscourseRoute.extend({
record.set("allow_personal_messages", true);
record.set("tool_details", false);
return record;
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allPersonas",
this.modelFor("adminPlugins.show.discourse-ai-personas")
);
},
});
}
}
@@ -1,19 +1,19 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
export default class AdminPluginsShowDiscourseAiPersonasShow extends DiscourseRoute {
async model(params) {
const allPersonas = this.modelFor(
"adminPlugins.show.discourse-ai-personas"
);
const id = parseInt(params.id, 10);
return allPersonas.findBy("id", id);
},
}
setupController(controller, model) {
this._super(controller, model);
super.setupController(controller, model);
controller.set(
"allPersonas",
this.modelFor("adminPlugins.show.discourse-ai-personas")
);
},
});
}
}
@@ -0,0 +1,11 @@
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiUsageRoute extends DiscourseRoute {
@service store;
model() {
return ajax("/admin/plugins/discourse-ai/ai-usage.json");
}
}
@@ -0,0 +1 @@
<AiUsage @model={{this.model}} />
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class AiUsageController < ::Admin::AdminController
requires_plugin "discourse-ai"
def show
render json: AiUsageSerializer.new(create_report, root: false)
end
private
def create_report
report =
DiscourseAi::Completions::Report.new(
start_date: params[:start_date]&.to_date || 30.days.ago,
end_date: params[:end_date]&.to_date || Time.current,
)
report = report.filter_by_feature(params[:feature]) if params[:feature].present?
report = report.filter_by_model(params[:model]) if params[:model].present?
report
end
end
end
end
+1 -3
View File
@@ -9,9 +9,7 @@ module ::Jobs
post = Post.find_by(id: post_id, post_type: Post.types[:regular])
return if post&.raw.blank?
DiscourseAi::PostClassificator.new(
DiscourseAi::Sentiment::SentimentClassification.new,
).classify!(post)
DiscourseAi::Sentiment::PostClassification.new.classify!(post)
end
end
end
+8
View File
@@ -3,6 +3,7 @@
class AiApiAuditLog < ActiveRecord::Base
belongs_to :post
belongs_to :topic
belongs_to :user
module Provider
OpenAI = 1
@@ -43,3 +44,10 @@ end
# feature_name :string(255)
# language_model :string(255)
# feature_context :jsonb
# cached_tokens :integer
#
# Indexes
#
# index_ai_api_audit_logs_on_created_at_and_feature_name (created_at,feature_name)
# index_ai_api_audit_logs_on_created_at_and_language_model (created_at,language_model)
#
+17 -9
View File
@@ -9,15 +9,23 @@ class AiSummary < ActiveRecord::Base
def self.store!(strategy, llm_model, summary, og_content, human:)
content_ids = og_content.map { |c| c[:id] }
AiSummary.create!(
target: strategy.target,
algorithm: llm_model.name,
content_range: (content_ids.first..content_ids.last),
summarized_text: summary,
original_content_sha: build_sha(content_ids.join),
summary_type: strategy.type,
origin: !!human ? origins[:human] : origins[:system],
)
AiSummary
.upsert(
{
target_id: strategy.target.id,
target_type: strategy.target.class.name,
algorithm: llm_model.name,
content_range: (content_ids.first..content_ids.last),
summarized_text: summary,
original_content_sha: build_sha(content_ids.join),
summary_type: strategy.type,
origin: !!human ? origins[:human] : origins[:system],
},
unique_by: %i[target_id target_type summary_type],
update_only: %i[summarized_text original_content_sha algorithm origin content_range],
)
.first
.then { AiSummary.find_by(id: _1["id"]) }
end
def self.build_sha(joined_ids)
+69
View File
@@ -0,0 +1,69 @@
# frozen_string_literal: true
class AiUsageSerializer < ApplicationSerializer
attributes :data, :features, :models, :users, :summary, :period
def data
object.tokens_by_period.as_json(
only: %i[period total_tokens total_cached_tokens total_request_tokens total_response_tokens],
)
end
def period
object.guess_period
end
def features
object.feature_breakdown.as_json(
only: %i[
feature_name
usage_count
total_tokens
total_cached_tokens
total_request_tokens
total_response_tokens
],
)
end
def models
object.model_breakdown.as_json(
only: %i[
llm
usage_count
total_tokens
total_cached_tokens
total_request_tokens
total_response_tokens
],
)
end
def users
object.user_breakdown.map do |user|
{
avatar_template: User.avatar_template(user.username, user.uploaded_avatar_id),
username: user.username,
usage_count: user.usage_count,
total_tokens: user.total_tokens,
total_cached_tokens: user.total_cached_tokens,
total_request_tokens: user.total_request_tokens,
total_response_tokens: user.total_response_tokens,
}
end
end
def summary
{
total_tokens: object.total_tokens,
total_cached_tokens: object.total_cached_tokens,
total_request_tokens: object.total_request_tokens,
total_response_tokens: object.total_response_tokens,
total_requests: object.total_requests,
date_range: {
start: object.start_date,
end: object.end_date,
},
}
end
end
@@ -18,5 +18,6 @@ export default {
this.route("new");
this.route("show", { path: "/:id" });
});
this.route("discourse-ai-usage", { path: "ai-usage" });
},
};
@@ -1,22 +1,25 @@
import { computed, observer } from "@ember/object";
import { computed } from "@ember/object";
import { observes } from "@ember-decorators/object";
import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
import { selectKitOptions } from "select-kit/components/select-kit";
export default ComboBox.extend({
_modelDisabledChanged: observer("attrs.disabled", function () {
@selectKitOptions({
filterable: true,
})
export default class AiLlmSelector extends ComboBox {
@observes("attrs.disabled")
_modelDisabledChanged() {
this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
}),
}
content: computed(function () {
@computed
get content() {
return [
{
id: "blank",
name: I18n.t("discourse_ai.ai_persona.no_llm_selected"),
},
].concat(this.llms);
}),
selectKitOptions: {
filterable: true,
},
});
}
}
@@ -1,17 +1,18 @@
import { observer } from "@ember/object";
import { readOnly } from "@ember/object/computed";
import { observes } from "@ember-decorators/object";
import MultiSelectComponent from "select-kit/components/multi-select";
import { selectKitOptions } from "select-kit/components/select-kit";
export default MultiSelectComponent.extend({
_modelDisabledChanged: observer("attrs.disabled", function () {
@selectKitOptions({
filterable: true,
})
export default class AiToolSelector extends MultiSelectComponent {
@readOnly("tools") content;
value = "";
@observes("attrs.disabled")
_modelDisabledChanged() {
this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
}),
content: readOnly("tools"),
value: "",
selectKitOptions: {
filterable: true,
},
});
}
}
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,16 @@
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
function addResultClass(element) {
element.closest(".fps-result")?.classList.add("ai-result");
}
const SearchResultDecoration = <template>
<div
class="ai-result__icon"
title={{i18n "discourse_ai.embeddings.ai_generated_result"}}
{{didInsert addResultClass}}
>
{{icon "discourse-sparkles"}}
</div>
@@ -24,6 +24,10 @@ export default {
label: "discourse_ai.tools.short_title",
route: "adminPlugins.show.discourse-ai-tools",
},
{
label: "discourse_ai.usage.short_title",
route: "adminPlugins.show.discourse-ai-usage",
},
]);
});
},
@@ -26,34 +26,38 @@ function attachHeaderIcon(api) {
function initializeAIBotReplies(api) {
initializePauseButton(api);
api.modifyClass("controller:topic", {
pluginId: "discourse-ai",
api.modifyClass(
"controller:topic",
(Superclass) =>
class extends Superclass {
onAIBotStreamedReply(data) {
streamPostText(this.model.postStream, data);
}
onAIBotStreamedReply: function (data) {
streamPostText(this.model.postStream, data);
},
subscribe: function () {
this._super();
subscribe() {
super.subscribe();
if (
this.model.isPrivateMessage &&
this.model.details.allowed_users &&
this.model.details.allowed_users.filter(isGPTBot).length >= 1
) {
// we attempt to recover the last message in the bus
// so we subscribe at -2
this.messageBus.subscribe(
`discourse-ai/ai-bot/topic/${this.model.id}`,
this.onAIBotStreamedReply.bind(this),
-2
);
if (
this.model.isPrivateMessage &&
this.model.details.allowed_users &&
this.model.details.allowed_users.filter(isGPTBot).length >= 1
) {
// we attempt to recover the last message in the bus
// so we subscribe at -2
this.messageBus.subscribe(
`discourse-ai/ai-bot/topic/${this.model.id}`,
this.onAIBotStreamedReply.bind(this),
-2
);
}
}
unsubscribe() {
this.messageBus.unsubscribe("discourse-ai/ai-bot/topic/*");
super.unsubscribe();
}
}
},
unsubscribe: function () {
this.messageBus.unsubscribe("discourse-ai/ai-bot/topic/*");
this._super();
},
});
);
}
function initializePersonaDecorator(api) {
@@ -1,9 +0,0 @@
import { apiInitializer } from "discourse/lib/api";
export default apiInitializer("1.15.0", (api) => {
api.modifyClass("component:search-result-entry", {
pluginId: "discourse-ai",
classNameBindings: ["bulkSelectEnabled", "post.generatedByAi:ai-result"],
});
});
@@ -47,10 +47,16 @@ export default {
(Superclass) =>
class extends Superclass {
@tracked related_topics;
relatedTopicsCache = [];
@cached
get relatedTopics() {
return this.related_topics?.map((topic) =>
// Used to keep related topics when a user scrolls up from the
// bottom of the topic and then scrolls back down
if (this.related_topics) {
this.relatedTopicsCache = this.related_topics;
}
return this.relatedTopicsCache?.map((topic) =>
this.store.createRecord("topic", topic)
);
}
@@ -272,13 +272,15 @@
.edit-topic-title {
.suggestion-button {
margin: 0;
padding: 0.45rem;
padding: 0.465rem;
}
}
#topic-title .edit-topic-title.showing-ai-suggestions {
#edit-title {
flex: 1 1 90%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.suggest-titles-button {
@@ -42,3 +42,30 @@
}
}
}
#topic-title .edit-topic-title.showing-ai-suggestions {
.category-chooser {
flex: 1 1 90%;
}
.ai-category-suggester-trigger {
padding: 0.425em;
}
.ai-tag-suggester-trigger {
padding: 0.45em;
}
}
.ios-device #topic-title .edit-topic-title.showing-ai-suggestions button {
&.ai-title-suggester-trigger {
padding-top: 0.4em;
padding-bottom: 0.4em;
}
&.ai-category-suggester-trigger {
padding-top: 0.4em;
padding-bottom: 0.25em;
}
&.ai-tag-suggester-trigger {
padding-top: 0.4em;
padding-bottom: 0.3em;
}
}
@@ -0,0 +1,168 @@
.ai-usage {
--chart-response-color: rgba(75, 192, 192, 0.8);
--chart-request-color: rgba(153, 102, 255, 0.8);
--chart-cached-color: rgba(153, 102, 255, 0.4);
padding: 1em;
&__filters-dates {
display: flex;
flex-direction: column;
gap: 1em;
margin-bottom: 1em;
}
&__period-buttons {
display: flex;
gap: 0.5em;
align-items: center;
.btn {
padding: 0.5em 1em;
&.btn-primary {
background: var(--tertiary);
color: var(--secondary);
}
}
}
&__custom-date-pickers {
display: flex;
gap: 1em;
align-items: center;
margin-top: 0.5em;
}
&__filters {
margin-bottom: 2em;
}
&__filters-period {
display: flex;
align-items: center;
gap: 1em;
}
.d-date-time-input-range {
display: flex;
gap: 1em;
align-items: center;
}
.d-date-time-input-range .from {
margin: 0;
}
&__period-label {
font-weight: bold;
}
&__summary {
margin: 2em 0;
padding: 1.5em;
background: var(--primary-very-low);
border-radius: 0.5em;
}
&__summary-title {
margin-bottom: 1em;
color: var(--primary);
font-size: 1.2em;
}
&__summary-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1em;
}
&__summary-stat {
display: flex;
flex-direction: column;
padding: 1em;
background: var(--secondary);
border-radius: 0.25em;
.label {
color: var(--primary-medium);
font-size: 0.875em;
margin-bottom: 0.5em;
}
.value {
color: var(--primary);
font-size: 1.5em;
font-weight: bold;
}
}
&__charts {
margin-top: 2em;
}
&__chart {
position: relative;
}
&__chart-container {
margin-bottom: 2em;
}
&__chart-title {
margin-bottom: 1em;
}
&__breakdowns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2em;
margin-top: 2em;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
&__features,
&__users,
&__models {
background: var(--primary-very-low);
padding: 1em;
border-radius: 0.5em;
}
&__features-title,
&__users-title,
&__models-title {
margin-bottom: 1em;
}
&__features-table,
&__users-table,
&__models-table {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
padding: 0.5em;
border-bottom: 2px solid var(--primary-low);
}
}
&__features-row,
&__users-row,
&__models-row {
&:hover {
background: var(--primary-low);
}
}
&__features-cell,
&__users-cell,
&__models-cell {
padding: 0.5em;
border-bottom: 1px solid var(--primary-low);
}
}
+20
View File
@@ -126,6 +126,26 @@ en:
modals:
select_option: "Select an option..."
usage:
short_title: "Usage"
summary: "Summary"
total_tokens: "Total tokens"
tokens_over_time: "Tokens over time"
features_breakdown: "Usage per feature"
feature: "Feature"
usage_count: "Usage count"
model: "Model"
models_breakdown: "Usage per model"
users_breakdown: "Usage per user"
all_features: "All features"
all_models: "All models"
username: "Username"
total_requests: "Total requests"
request_tokens: "Request tokens"
response_tokens: "Response tokens"
cached_tokens: "Cached tokens"
ai_persona:
tool_strategies:
all: "Apply to all replies"
+2
View File
@@ -77,6 +77,8 @@ Discourse::Application.routes.draw do
get "/rag-document-fragments/files/status",
to: "discourse_ai/admin/rag_document_fragments#indexing_status_check"
get "/ai-usage", to: "discourse_ai/admin/ai_usage#show"
resources :ai_llms,
only: %i[index create show update destroy],
path: "ai-llms",
@@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddCachedTokensToAiApiAuditLog < ActiveRecord::Migration[7.2]
def change
add_column :ai_api_audit_logs, :cached_tokens, :integer
add_index :ai_api_audit_logs, %i[created_at feature_name]
add_index :ai_api_audit_logs, %i[created_at language_model]
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class FixClassificationData < ActiveRecord::Migration[7.2]
def up
classifications = DB.query(<<~SQL)
SELECT id, classification
FROM classification_results
WHERE classification_type = 'sentiment'
AND SUBSTRING(LTRIM(classification::text), 1, 1) = '['
SQL
transformed =
classifications.reduce([]) do |memo, c|
hash_result = {}
c.classification.each { |r| hash_result[r["label"]] = r["score"] }
memo << { id: c.id, fixed_classification: hash_result }
end
transformed_json = transformed.to_json
DB.exec(<<~SQL, values: transformed_json)
UPDATE classification_results
SET classification = N.fixed_classification
FROM (
SELECT (value::jsonb->'id')::integer AS id, (value::jsonb->'fixed_classification')::jsonb AS fixed_classification
FROM jsonb_array_elements(:values::jsonb)
) N
WHERE classification_results.id = N.id
AND classification_type = 'sentiment'
SQL
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
+2 -2
View File
@@ -22,7 +22,7 @@ module DiscourseAi
attr_reader :bot_user
attr_accessor :persona
def get_updated_title(conversation_context, post)
def get_updated_title(conversation_context, post, user)
system_insts = <<~TEXT.strip
You are titlebot. Given a conversation, you will suggest a title.
@@ -61,7 +61,7 @@ module DiscourseAi
DiscourseAi::Completions::Llm
.proxy(model)
.generate(title_prompt, user: post.user, feature_name: "bot_title")
.generate(title_prompt, user: user, feature_name: "bot_title")
.strip
.split("\n")
.last
+3 -3
View File
@@ -245,11 +245,11 @@ module DiscourseAi
builder.to_a
end
def title_playground(post)
def title_playground(post, user)
context = conversation_context(post)
bot
.get_updated_title(context, post)
.get_updated_title(context, post, user)
.tap do |new_title|
PostRevisor.new(post.topic.first_post, post.topic).revise!(
bot.bot_user,
@@ -544,7 +544,7 @@ module DiscourseAi
post_streamer&.finish(skip_callback: true)
publish_final_update(reply_post) if stream_reply
if reply_post && post.post_number == 1 && post.topic.private_message?
title_playground(reply_post)
title_playground(reply_post, post.user)
end
end
+33 -20
View File
@@ -12,6 +12,8 @@ module DiscourseAi
return [] unless SiteSetting.ai_embeddings_enabled
candidates = nearest_neighbors(limit: 100)
return [] if candidates.empty?
candidate_ids = candidates.map(&:first)
::Topic
@@ -52,6 +54,8 @@ module DiscourseAi
return [] unless SiteSetting.ai_embeddings_enabled
candidates = nearest_neighbors(limit: 100)
return [] if candidates.empty?
candidate_ids = candidates.map(&:first)
count_column = Tag.topic_count_column(@user.guardian) # Determine the count column
@@ -60,30 +64,29 @@ module DiscourseAi
.joins(:topic_tags, :tags)
.where(id: candidate_ids)
.where("tags.id IN (?)", DiscourseTagging.visible_tags(@user.guardian).pluck(:id))
.group("topics.id, tags.id, tags.name") # Group by topics.id and tags.id
.group("topics.id")
.order("array_position(ARRAY#{candidate_ids}, topics.id)")
.pluck(
"tags.id",
"tags.name",
"tags.#{count_column}",
"MIN(array_position(ARRAY#{candidate_ids}, topics.id))", # Get minimum index for ordering
)
.uniq # Ensure unique tags per topic
.pluck("array_agg(tags.name)")
.map(&:uniq)
.map
.with_index do |(id, name, count, index), idx|
{
id: id,
name: name,
count: count,
score: 1 / (candidates[idx].last + 1), # Inverse of the distance for score
}
.with_index { |tag_list, index| { tags: tag_list, score: candidates[index].last } }
.flat_map { |c| c[:tags].map { |t| { name: t, score: c[:score] } } }
.map do |c|
c[:score] = 1 / (c[:score] + 1) # inverse of the distance
c
end
.group_by { |tag| tag[:name] }
.map do |name, tags|
tags.first.merge(score: tags.sum { |t| t[:score] })
end # Aggregate scores per tag
.sort_by { |tag| -tag[:score] }
.group_by { |c| c[:name] }
.map { |name, scores| { name: name, score: scores.sum { |s| s[:score] } } }
.sort_by { |c| -c[:score] }
.take(5)
.then do |tags|
models = Tag.where(name: tags.map { _1[:name] }).index_by(&:name)
tags.map do |tag|
tag[:id] = models.dig(tag[:name])&.id
tag[:count] = models.dig(tag[:name])&.public_send(count_column) || 0
tag
end
end
end
private
@@ -95,11 +98,21 @@ module DiscourseAi
raw_vector = vector_rep.vector_from(@text)
muted_category_ids = nil
if @user.present?
muted_category_ids =
CategoryUser.where(
user: @user,
notification_level: CategoryUser.notification_levels[:muted],
).pluck(:category_id)
end
vector_rep.asymmetric_topics_similarity_search(
raw_vector,
limit: limit,
offset: 0,
return_distance: true,
exclude_category_ids: muted_category_ids,
)
end
end

Some files were not shown because too many files have changed in this diff Show More