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

Commit 62cd2da

Browse files
committed
FEATURE: AI Usage page
This is a work in progress PR to provide admins with an AI usage page
1 parent f1c7ee8 commit 62cd2da

File tree

11 files changed

+282
-0
lines changed

11 files changed

+282
-0
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+
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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
26+
def time_series_data(report)
27+
case params[:period]
28+
when "hour"
29+
report.tokens_per_hour
30+
when "month"
31+
report.tokens_per_month
32+
else
33+
report.tokens_per_day
34+
end
35+
end
36+
end
37+
end
38+
end
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+
class AiUsageSerializer < ApplicationSerializer
4+
attributes :data, :features, :models, :summary
5+
6+
def data
7+
object.tokens_by_period.to_json
8+
end
9+
10+
def features
11+
object.feature_breakdown.as_json(only: %i[feature_name usage_count total_tokens])
12+
end
13+
14+
def models
15+
object.model_breakdown.as_json(only: %i[llm usage_count total_tokens])
16+
end
17+
18+
def summary
19+
{
20+
total_tokens: object.total_tokens,
21+
date_range: {
22+
start: object.start_date,
23+
end: object.end_date,
24+
},
25+
}
26+
end
27+
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
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Component from "@glimmer/component";
2+
3+
export default class AiUsage extends Component {
4+
hello() {}
5+
<template>
6+
testing
7+
</template>
8+
}

assets/javascripts/initializers/admin-plugin-configuration-nav.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export default {
2424
label: "discourse_ai.tools.short_title",
2525
route: "adminPlugins.show.discourse-ai-tools",
2626
},
27+
{
28+
label: "discourse_ai.usage.short_title",
29+
route: "adminPlugins.show.discourse-ai-usage",
30+
},
2731
]);
2832
});
2933
},

config/locales/client.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ en:
126126
modals:
127127
select_option: "Select an option..."
128128

129+
usage:
130+
short_title: "Usage"
131+
129132
ai_persona:
130133
tool_strategies:
131134
all: "Apply to all replies"

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777
get "/rag-document-fragments/files/status",
7878
to: "discourse_ai/admin/rag_document_fragments#indexing_status_check"
7979

80+
get "/ai-usage", to: "discourse_ai/admin/ai_usage#show"
81+
8082
resources :ai_llms,
8183
only: %i[index create show update destroy],
8284
path: "ai-llms",

lib/completions/report.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
module DiscourseAi
2+
module Completions
3+
class Report
4+
attr_reader :start_date, :end_date, :base_query
5+
6+
def initialize(start_date: 30.days.ago, end_date: Time.current)
7+
@start_date = start_date.beginning_of_day
8+
@end_date = end_date.end_of_day
9+
@base_query = AiApiAuditLog.where(created_at: @start_date..@end_date)
10+
end
11+
12+
def total_tokens
13+
base_query.sum("request_tokens + response_tokens")
14+
end
15+
16+
def guess_period(period)
17+
period = nil if %i[day month hour].include?(period)
18+
period ||
19+
case @end_date - @start_date
20+
when 0..7.days
21+
:hour
22+
when 7.days..90.days
23+
:day
24+
else
25+
:month
26+
end
27+
end
28+
29+
def tokens_by_period(period = nil)
30+
period = guess_period(period)
31+
base_query.group("DATE_TRUNC('#{period}', created_at)").sum(
32+
"request_tokens + response_tokens",
33+
)
34+
end
35+
36+
def feature_breakdown
37+
base_query.group(:feature_name).select(
38+
"feature_name",
39+
"COUNT(*) as usage_count",
40+
"SUM(request_tokens + response_tokens) as total_tokens",
41+
)
42+
end
43+
44+
def model_breakdown
45+
base_query.group(:language_model).select(
46+
"language_model as llm",
47+
"COUNT(*) as usage_count",
48+
"SUM(request_tokens + response_tokens) as total_tokens",
49+
)
50+
end
51+
52+
def tokens_per_hour
53+
tokens_by_period(:hour)
54+
end
55+
56+
def tokens_per_day
57+
tokens_by_period(:day)
58+
end
59+
60+
def tokens_per_month
61+
tokens_by_period(:month)
62+
end
63+
64+
def filter_by_feature(feature_name)
65+
@base_query = base_query.where(feature_name: feature_name)
66+
self
67+
end
68+
69+
def filter_by_model(model_name)
70+
@base_query = base_query.where(language_model: model_name)
71+
self
72+
end
73+
end
74+
end
75+
end

0 commit comments

Comments
 (0)