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

Commit 9be1049

Browse files
authored
DEV: Log AI related configuration to staff action log (#1416)
is update adds logging for changes made in the AI admin panel. When making configuration changes to Embeddings, LLMs, Personas, Tools, or Spam that aren't site setting related, changes will now be logged in Admin > Logs & Screening. This will help admins debug issues related to AI. In this update a helper lib is created called `AiStaffActionLogger` which can be easily used in the future to add logging support for any other admin config we need logged for AI.
1 parent fc83bed commit 9be1049

14 files changed

+1388
-0
lines changed

app/controllers/discourse_ai/admin/ai_embeddings_controller.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def create
4040
embedding_def = EmbeddingDefinition.new(ai_embeddings_params)
4141

4242
if embedding_def.save
43+
log_ai_embedding_creation(embedding_def)
4344
render json: AiEmbeddingDefinitionSerializer.new(embedding_def), status: :created
4445
else
4546
render_json_error embedding_def
@@ -55,7 +56,10 @@ def update
5556
)
5657
end
5758

59+
initial_attributes = embedding_def.attributes.dup
60+
5861
if embedding_def.update(ai_embeddings_params.except(:dimensions))
62+
log_ai_embedding_update(embedding_def, initial_attributes)
5963
render json: AiEmbeddingDefinitionSerializer.new(embedding_def)
6064
else
6165
render_json_error embedding_def
@@ -75,7 +79,16 @@ def destroy
7579
return render_json_error(I18n.t("discourse_ai.embeddings.delete_failed"), status: 409)
7680
end
7781

82+
embedding_details = {
83+
embedding_id: embedding_def.id,
84+
display_name: embedding_def.display_name,
85+
provider: embedding_def.provider,
86+
dimensions: embedding_def.dimensions,
87+
subject: embedding_def.display_name,
88+
}
89+
7890
if embedding_def.destroy
91+
log_ai_embedding_deletion(embedding_details)
7992
head :no_content
8093
else
8194
render_json_error embedding_def
@@ -128,6 +141,60 @@ def ai_embeddings_params
128141

129142
permitted
130143
end
144+
145+
def ai_embeddings_logger_fields
146+
{
147+
display_name: {
148+
},
149+
provider: {
150+
},
151+
dimensions: {
152+
},
153+
url: {
154+
},
155+
tokenizer_class: {
156+
},
157+
max_sequence_length: {
158+
},
159+
embed_prompt: {
160+
type: :large_text,
161+
},
162+
search_prompt: {
163+
type: :large_text,
164+
},
165+
matryoshka_dimensions: {
166+
},
167+
api_key: {
168+
type: :sensitive,
169+
},
170+
# JSON fields should be tracked as simple changes
171+
json_fields: [:provider_params],
172+
}
173+
end
174+
175+
def log_ai_embedding_creation(embedding_def)
176+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
177+
entity_details = { embedding_id: embedding_def.id, subject: embedding_def.display_name }
178+
logger.log_creation("embedding", embedding_def, ai_embeddings_logger_fields, entity_details)
179+
end
180+
181+
def log_ai_embedding_update(embedding_def, initial_attributes)
182+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
183+
entity_details = { embedding_id: embedding_def.id, subject: embedding_def.display_name }
184+
185+
logger.log_update(
186+
"embedding",
187+
embedding_def,
188+
initial_attributes,
189+
ai_embeddings_logger_fields,
190+
entity_details,
191+
)
192+
end
193+
194+
def log_ai_embedding_deletion(embedding_details)
195+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
196+
logger.log_deletion("embedding", embedding_details)
197+
end
131198
end
132199
end
133200
end

app/controllers/discourse_ai/admin/ai_llms_controller.rb

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def create
4747

4848
if llm_model.save
4949
llm_model.toggle_companion_user
50+
log_llm_model_creation(llm_model)
5051
render json: LlmModelSerializer.new(llm_model), status: :created
5152
else
5253
render_json_error llm_model
@@ -56,6 +57,10 @@ def create
5657
def update
5758
llm_model = LlmModel.find(params[:id])
5859

60+
# Capture initial state for logging
61+
initial_attributes = llm_model.attributes.dup
62+
initial_quotas = llm_model.llm_quotas.map(&:attributes)
63+
5964
if params[:ai_llm].key?(:llm_quotas)
6065
if quota_params
6166
existing_quota_group_ids = llm_model.llm_quotas.pluck(:group_id)
@@ -81,6 +86,7 @@ def update
8186

8287
if llm_model.update(ai_llm_params(updating: llm_model))
8388
llm_model.toggle_companion_user
89+
log_llm_model_update(llm_model, initial_attributes, initial_quotas)
8490
render json: LlmModelSerializer.new(llm_model)
8591
else
8692
render_json_error llm_model
@@ -109,11 +115,20 @@ def destroy
109115
)
110116
end
111117

118+
# Capture model details for logging before destruction
119+
model_details = {
120+
model_id: llm_model.id,
121+
display_name: llm_model.display_name,
122+
name: llm_model.name,
123+
provider: llm_model.provider,
124+
}
125+
112126
# Clean up companion users
113127
llm_model.enabled_chat_bot = false
114128
llm_model.toggle_companion_user
115129

116130
if llm_model.destroy
131+
log_llm_model_deletion(model_details)
117132
head :no_content
118133
else
119134
render_json_error llm_model
@@ -190,6 +205,89 @@ def ai_llm_params(updating: nil)
190205

191206
permitted
192207
end
208+
209+
def ai_llm_logger_fields
210+
{
211+
display_name: {
212+
},
213+
name: {
214+
},
215+
provider: {
216+
},
217+
tokenizer: {
218+
},
219+
url: {
220+
},
221+
max_prompt_tokens: {
222+
},
223+
max_output_tokens: {
224+
},
225+
enabled_chat_bot: {
226+
},
227+
vision_enabled: {
228+
},
229+
api_key: {
230+
type: :sensitive,
231+
},
232+
input_cost: {
233+
},
234+
output_cost: {
235+
},
236+
# JSON fields should be tracked as simple changes
237+
json_fields: [:provider_params],
238+
}
239+
end
240+
241+
def log_llm_model_creation(llm_model)
242+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
243+
entity_details = { model_id: llm_model.id, subject: llm_model.display_name }
244+
245+
# Add quota information as a special case
246+
if llm_model.llm_quotas.any?
247+
entity_details[:quotas] = llm_model
248+
.llm_quotas
249+
.map do |quota|
250+
"Group #{quota.group_id}: #{quota.max_tokens} tokens, #{quota.max_usages} usages, #{quota.duration_seconds}s"
251+
end
252+
.join("; ")
253+
end
254+
255+
logger.log_creation("llm_model", llm_model, ai_llm_logger_fields, entity_details)
256+
end
257+
258+
def log_llm_model_update(llm_model, initial_attributes, initial_quotas)
259+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
260+
entity_details = { model_id: llm_model.id, subject: llm_model.display_name }
261+
262+
# Track quota changes separately as they're a special case
263+
current_quotas = llm_model.llm_quotas.reload.map(&:attributes)
264+
if initial_quotas != current_quotas
265+
initial_quota_summary =
266+
initial_quotas
267+
.map { |q| "Group #{q["group_id"]}: #{q["max_tokens"]} tokens" }
268+
.join("; ")
269+
current_quota_summary =
270+
current_quotas
271+
.map { |q| "Group #{q["group_id"]}: #{q["max_tokens"]} tokens" }
272+
.join("; ")
273+
entity_details[:quotas_changed] = true
274+
entity_details[:quotas] = "#{initial_quota_summary}#{current_quota_summary}"
275+
end
276+
277+
logger.log_update(
278+
"llm_model",
279+
llm_model,
280+
initial_attributes,
281+
ai_llm_logger_fields,
282+
entity_details,
283+
)
284+
end
285+
286+
def log_llm_model_deletion(model_details)
287+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
288+
model_details[:subject] = model_details[:display_name]
289+
logger.log_deletion("llm_model", model_details)
290+
end
193291
end
194292
end
195293
end

app/controllers/discourse_ai/admin/ai_personas_controller.rb

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def create
6161
ai_persona = AiPersona.new(ai_persona_params.except(:rag_uploads))
6262
if ai_persona.save
6363
RagDocumentFragment.link_target_and_uploads(ai_persona, attached_upload_ids)
64+
log_ai_persona_creation(ai_persona)
6465

6566
render json: {
6667
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
@@ -77,8 +78,11 @@ def create_user
7778
end
7879

7980
def update
81+
initial_attributes = @ai_persona.attributes.dup
82+
8083
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
8184
RagDocumentFragment.update_target_uploads(@ai_persona, attached_upload_ids)
85+
log_ai_persona_update(@ai_persona, initial_attributes)
8286

8387
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
8488
else
@@ -87,7 +91,14 @@ def update
8791
end
8892

8993
def destroy
94+
persona_details = {
95+
persona_id: @ai_persona.id,
96+
name: @ai_persona.name,
97+
description: @ai_persona.description,
98+
}
99+
90100
if @ai_persona.destroy
101+
log_ai_persona_deletion(persona_details)
91102
head :no_content
92103
else
93104
render_json_error @ai_persona
@@ -264,6 +275,92 @@ def permit_examples(examples)
264275

265276
examples.map { |example_arr| example_arr.take(2).map(&:to_s) }
266277
end
278+
279+
def ai_persona_logger_fields
280+
{
281+
name: {
282+
},
283+
description: {
284+
},
285+
enabled: {
286+
},
287+
priority: {
288+
},
289+
system_prompt: {
290+
type: :large_text,
291+
},
292+
default_llm_id: {
293+
},
294+
temperature: {
295+
},
296+
top_p: {
297+
},
298+
user_id: {
299+
},
300+
max_context_posts: {
301+
},
302+
vision_enabled: {
303+
},
304+
vision_max_pixels: {
305+
},
306+
rag_chunk_tokens: {
307+
},
308+
rag_chunk_overlap_tokens: {
309+
},
310+
rag_conversation_chunks: {
311+
},
312+
rag_llm_model_id: {
313+
},
314+
question_consolidator_llm_id: {
315+
},
316+
allow_chat_channel_mentions: {
317+
},
318+
allow_chat_direct_messages: {
319+
},
320+
allow_topic_mentions: {
321+
},
322+
allow_personal_messages: {
323+
},
324+
tool_details: {
325+
type: :large_text,
326+
},
327+
forced_tool_count: {
328+
},
329+
force_default_llm: {
330+
},
331+
# JSON fields
332+
json_fields: %i[tools response_format examples allowed_group_ids],
333+
}
334+
end
335+
336+
def log_ai_persona_creation(ai_persona)
337+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
338+
entity_details = { persona_id: ai_persona.id, subject: ai_persona.name }
339+
entity_details[:tools_count] = (ai_persona.tools || []).size
340+
341+
logger.log_creation("persona", ai_persona, ai_persona_logger_fields, entity_details)
342+
end
343+
344+
def log_ai_persona_update(ai_persona, initial_attributes)
345+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
346+
entity_details = { persona_id: ai_persona.id, subject: ai_persona.name }
347+
entity_details[:tools_count] = ai_persona.tools.size if ai_persona.tools.present?
348+
349+
logger.log_update(
350+
"persona",
351+
ai_persona,
352+
initial_attributes,
353+
ai_persona_logger_fields,
354+
entity_details,
355+
)
356+
end
357+
358+
def log_ai_persona_deletion(persona_details)
359+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
360+
persona_details[:subject] = persona_details[:name]
361+
362+
logger.log_deletion("persona", persona_details)
363+
end
267364
end
268365
end
269366
end

0 commit comments

Comments
 (0)