Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
593e8dc2a5 | ||
|
|
0abd4b1244 | ||
|
|
120a20c5cd | ||
|
|
0cb2c413ba | ||
|
|
80adefa1c1 | ||
|
|
bc0657f478 | ||
|
|
c980c34d77 | ||
|
|
6456a4f44a | ||
|
|
eacc4ed945 | ||
|
|
4a2369356f | ||
|
|
4980a4b2f7 |
+7
-7
@@ -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
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -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")
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -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")
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
#
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
+6
@@ -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 {
|
||||
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user