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

Commit bc0657f

Browse files
authored
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.
1 parent c980c34 commit bc0657f

File tree

22 files changed

+1108
-9
lines changed

22 files changed

+1108
-9
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { service } from "@ember/service";
2+
import { ajax } from "discourse/lib/ajax";
3+
import DiscourseRoute from "discourse/routes/discourse";
4+
5+
export default class DiscourseAiUsageRoute extends DiscourseRoute {
6+
@service store;
7+
8+
model() {
9+
return ajax("/admin/plugins/discourse-ai/ai-usage.json");
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<AiUsage @model={{this.model}} />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module Admin
5+
class AiUsageController < ::Admin::AdminController
6+
requires_plugin "discourse-ai"
7+
8+
def show
9+
render json: AiUsageSerializer.new(create_report, root: false)
10+
end
11+
12+
private
13+
14+
def create_report
15+
report =
16+
DiscourseAi::Completions::Report.new(
17+
start_date: params[:start_date]&.to_date || 30.days.ago,
18+
end_date: params[:end_date]&.to_date || Time.current,
19+
)
20+
21+
report = report.filter_by_feature(params[:feature]) if params[:feature].present?
22+
report = report.filter_by_model(params[:model]) if params[:model].present?
23+
report
24+
end
25+
end
26+
end
27+
end

app/models/ai_api_audit_log.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
class AiApiAuditLog < ActiveRecord::Base
44
belongs_to :post
55
belongs_to :topic
6+
belongs_to :user
67

78
module Provider
89
OpenAI = 1
@@ -43,3 +44,10 @@ def prev_log_id
4344
# feature_name :string(255)
4445
# language_model :string(255)
4546
# feature_context :jsonb
47+
# cached_tokens :integer
48+
#
49+
# Indexes
50+
#
51+
# index_ai_api_audit_logs_on_created_at_and_feature_name (created_at,feature_name)
52+
# index_ai_api_audit_logs_on_created_at_and_language_model (created_at,language_model)
53+
#
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# frozen_string_literal: true
2+
3+
class AiUsageSerializer < ApplicationSerializer
4+
attributes :data, :features, :models, :users, :summary, :period
5+
6+
def data
7+
object.tokens_by_period.as_json(
8+
only: %i[period total_tokens total_cached_tokens total_request_tokens total_response_tokens],
9+
)
10+
end
11+
12+
def period
13+
object.guess_period
14+
end
15+
16+
def features
17+
object.feature_breakdown.as_json(
18+
only: %i[
19+
feature_name
20+
usage_count
21+
total_tokens
22+
total_cached_tokens
23+
total_request_tokens
24+
total_response_tokens
25+
],
26+
)
27+
end
28+
29+
def models
30+
object.model_breakdown.as_json(
31+
only: %i[
32+
llm
33+
usage_count
34+
total_tokens
35+
total_cached_tokens
36+
total_request_tokens
37+
total_response_tokens
38+
],
39+
)
40+
end
41+
42+
def users
43+
object.user_breakdown.map do |user|
44+
{
45+
avatar_template: User.avatar_template(user.username, user.uploaded_avatar_id),
46+
username: user.username,
47+
usage_count: user.usage_count,
48+
total_tokens: user.total_tokens,
49+
total_cached_tokens: user.total_cached_tokens,
50+
total_request_tokens: user.total_request_tokens,
51+
total_response_tokens: user.total_response_tokens,
52+
}
53+
end
54+
end
55+
56+
def summary
57+
{
58+
total_tokens: object.total_tokens,
59+
total_cached_tokens: object.total_cached_tokens,
60+
total_request_tokens: object.total_request_tokens,
61+
total_response_tokens: object.total_response_tokens,
62+
total_requests: object.total_requests,
63+
date_range: {
64+
start: object.start_date,
65+
end: object.end_date,
66+
},
67+
}
68+
end
69+
end

assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export default {
1818
this.route("new");
1919
this.route("show", { path: "/:id" });
2020
});
21+
this.route("discourse-ai-usage", { path: "ai-usage" });
2122
},
2223
};

0 commit comments

Comments
 (0)