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

Commit a0c0139

Browse files
committed
DEV: Embeddings, tools, personas
1 parent 718da58 commit a0c0139

File tree

11 files changed

+1075
-146
lines changed

11 files changed

+1075
-146
lines changed

app/controllers/discourse_ai/admin/ai_embeddings_controller.rb

Lines changed: 83 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,11 @@ def update
5556
)
5657
end
5758

59+
# Capture initial state for logging
60+
initial_attributes = embedding_def.attributes.dup
61+
5862
if embedding_def.update(ai_embeddings_params.except(:dimensions))
63+
log_ai_embedding_update(embedding_def, initial_attributes)
5964
render json: AiEmbeddingDefinitionSerializer.new(embedding_def)
6065
else
6166
render_json_error embedding_def
@@ -75,7 +80,16 @@ def destroy
7580
return render_json_error(I18n.t("discourse_ai.embeddings.delete_failed"), status: 409)
7681
end
7782

83+
# Capture embedding details for logging before destruction
84+
embedding_details = {
85+
embedding_id: embedding_def.id,
86+
display_name: embedding_def.display_name,
87+
provider: embedding_def.provider,
88+
dimensions: embedding_def.dimensions
89+
}
90+
7891
if embedding_def.destroy
92+
log_ai_embedding_deletion(embedding_details)
7993
head :no_content
8094
else
8195
render_json_error embedding_def
@@ -128,6 +142,75 @@ def ai_embeddings_params
128142

129143
permitted
130144
end
145+
146+
def log_ai_embedding_creation(embedding_def)
147+
# Create log details
148+
log_details = {
149+
embedding_id: embedding_def.id,
150+
display_name: embedding_def.display_name,
151+
provider: embedding_def.provider,
152+
dimensions: embedding_def.dimensions
153+
}
154+
155+
# Only include tokenizer if present
156+
if embedding_def.tokenizer_class.present?
157+
log_details[:tokenizer] = embedding_def.tokenizer_class
158+
end
159+
160+
# For sensitive fields, don't include the actual content
161+
if embedding_def.api_key.present?
162+
log_details[:api_key_set] = true
163+
end
164+
165+
# Log the action
166+
StaffActionLogger.new(current_user).log_custom("create_ai_embedding", log_details)
167+
end
168+
169+
def log_ai_embedding_update(embedding_def, initial_attributes)
170+
# Create log details
171+
log_details = {
172+
embedding_id: embedding_def.id,
173+
display_name: embedding_def.display_name
174+
}
175+
176+
# Track changes in fields
177+
changed_fields = []
178+
179+
# Fields to check for changes
180+
%w[display_name provider url tokenizer_class max_sequence_length embed_prompt search_prompt matryoshka_dimensions].each do |field|
181+
if initial_attributes[field] != embedding_def.attributes[field]
182+
changed_fields << field
183+
log_details["#{field}_changed"] = true
184+
end
185+
end
186+
187+
# Special handling for API key (sensitive)
188+
if initial_attributes['api_key'].present? != embedding_def.api_key.present?
189+
changed_fields << 'api_key'
190+
191+
if embedding_def.api_key.present?
192+
log_details[:api_key_set] = true
193+
else
194+
log_details[:api_key_removed] = true
195+
end
196+
end
197+
198+
# Special handling for provider_params (JSON)
199+
if initial_attributes['provider_params'].to_s != embedding_def.provider_params.to_s
200+
changed_fields << 'provider_params'
201+
log_details[:provider_params_changed] = true
202+
end
203+
204+
# Only log if there are actual changes
205+
if changed_fields.any?
206+
log_details[:changed_fields] = changed_fields
207+
StaffActionLogger.new(current_user).log_custom("update_ai_embedding", log_details)
208+
end
209+
end
210+
211+
def log_ai_embedding_deletion(embedding_details)
212+
StaffActionLogger.new(current_user).log_custom("delete_ai_embedding", embedding_details)
213+
end
131214
end
132215
end
133216
end

app/controllers/discourse_ai/admin/ai_llms_controller.rb

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -207,81 +207,95 @@ def ai_llm_params(updating: nil)
207207
end
208208

209209
def log_llm_model_creation(llm_model)
210-
log_details = {
210+
# Create field configuration with appropriate types
211+
field_config = {
212+
display_name: {},
213+
name: {},
214+
provider: {},
215+
tokenizer: {},
216+
url: {},
217+
api_key: { type: :sensitive },
218+
max_prompt_tokens: {},
219+
max_output_tokens: {},
220+
enabled_chat_bot: {},
221+
vision_enabled: {},
222+
input_cost: {},
223+
output_cost: {},
224+
cached_input_cost: {},
225+
provider_params: {}
226+
}
227+
228+
# Create basic entity details
229+
entity_details = {
211230
model_id: llm_model.id,
212-
display_name: llm_model.display_name,
213-
name: llm_model.name,
214-
provider: llm_model.provider,
215-
tokenizer: llm_model.tokenizer,
216-
enabled_chat_bot: llm_model.enabled_chat_bot,
217-
vision_enabled: llm_model.vision_enabled,
231+
model_name: llm_model.name,
232+
display_name: llm_model.display_name
218233
}
219-
220-
# Add cost details if present
221-
if llm_model.input_cost.present?
222-
log_details[:input_cost] = llm_model.input_cost
223-
end
224-
if llm_model.output_cost.present?
225-
log_details[:output_cost] = llm_model.output_cost
226-
end
227-
if llm_model.cached_input_cost.present?
228-
log_details[:cached_input_cost] = llm_model.cached_input_cost
229-
end
230-
231-
# Add quota information if present
234+
235+
# Create logger instance
236+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
237+
238+
# Extract attributes based on field configuration
239+
log_details = entity_details.dup
240+
log_details.merge!(logger.send(:extract_entity_attributes, llm_model, field_config))
241+
242+
# Add quota information as a special case
232243
if llm_model.llm_quotas.any?
233244
log_details[:quotas] = llm_model.llm_quotas.map do |quota|
234245
"Group #{quota.group_id}: #{quota.max_tokens} tokens, #{quota.max_usages} usages, #{quota.duration_seconds}s"
235246
end.join("; ")
236247
end
237-
238-
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
248+
239249
logger.log_custom("create_ai_llm_model", log_details)
240250
end
241251

242252
def log_llm_model_update(llm_model, initial_attributes, initial_quotas)
243-
current_attributes = llm_model.attributes
244-
current_quotas = llm_model.llm_quotas.reload.map(&:attributes)
245-
246-
# Track changes to main attributes
253+
# Create field configuration with appropriate types
254+
field_config = {
255+
display_name: {},
256+
name: {},
257+
provider: {},
258+
tokenizer: {},
259+
url: {},
260+
api_key: { type: :sensitive },
261+
max_prompt_tokens: {},
262+
max_output_tokens: {},
263+
enabled_chat_bot: {},
264+
vision_enabled: {},
265+
input_cost: {},
266+
output_cost: {},
267+
cached_input_cost: {},
268+
provider_params: {},
269+
json_fields: %w[provider_params]
270+
}
271+
272+
# Create basic entity details
273+
entity_details = {
274+
model_id: llm_model.id,
275+
model_name: llm_model.name,
276+
display_name: llm_model.display_name
277+
}
278+
279+
# Create logger instance
280+
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
281+
282+
# Create a changes hash to track all changes
247283
changes = {}
248-
trackable_fields = %w[
249-
display_name name provider tokenizer url max_prompt_tokens max_output_tokens
250-
enabled_chat_bot vision_enabled input_cost output_cost cached_input_cost
251-
provider_params
252-
]
253-
254-
trackable_fields.each do |field|
255-
initial_value = initial_attributes[field]
256-
current_value = current_attributes[field]
257-
258-
if initial_value != current_value
259-
# Handle API key specially - don't log the actual values for security
260-
if field == "api_key"
261-
changes[field] = initial_value.present? && current_value.present? ?
262-
"updated" : (current_value.present? ? "set" : "removed")
263-
else
264-
changes[field] = "#{initial_value}#{current_value}"
265-
end
266-
end
267-
end
268-
269-
# Track quota changes
284+
current_quotas = llm_model.llm_quotas.reload.map(&:attributes)
285+
286+
# Track quota changes separately as they're a special case
270287
if initial_quotas != current_quotas
271288
initial_quota_summary = initial_quotas.map { |q| "Group #{q['group_id']}: #{q['max_tokens']} tokens" }.join("; ")
272289
current_quota_summary = current_quotas.map { |q| "Group #{q['group_id']}: #{q['max_tokens']} tokens" }.join("; ")
273290
changes[:quotas] = "#{initial_quota_summary}#{current_quota_summary}"
274291
end
275-
276-
# Only log if there are actual changes
277-
if changes.any?
278-
log_details = {
279-
model_id: llm_model.id,
280-
model_name: llm_model.display_name || llm_model.name,
281-
}.merge(changes)
282-
283-
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
284-
logger.log_custom("update_ai_llm_model", log_details)
292+
293+
# Let the logger handle standard field changes
294+
logger.log_update("llm_model", llm_model, initial_attributes, field_config, entity_details)
295+
296+
# If we have quota changes but no other changes were detected, log them separately
297+
if changes.key?(:quotas)
298+
logger.log_custom("update_ai_llm_model", entity_details.merge(changes))
285299
end
286300
end
287301

app/controllers/discourse_ai/admin/ai_personas_controller.rb

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -276,40 +276,93 @@ def permit_examples(examples)
276276
end
277277

278278
def log_ai_persona_creation(ai_persona)
279-
# Extract standard attributes
280-
log_details = {
279+
# Create field configuration with appropriate types
280+
field_config = {
281+
name: {},
282+
description: {},
283+
enabled: {},
284+
priority: {},
285+
system_prompt: { type: :large_text },
286+
default_llm_id: {},
287+
temperature: {},
288+
top_p: {},
289+
user_id: {},
290+
vision_enabled: {},
291+
vision_max_pixels: {},
292+
max_context_posts: {},
293+
rag_chunk_tokens: {},
294+
rag_chunk_overlap_tokens: {},
295+
rag_conversation_chunks: {},
296+
rag_llm_model_id: {},
297+
question_consolidator_llm_id: {},
298+
tool_details: {},
299+
forced_tool_count: {},
300+
allow_chat_channel_mentions: {},
301+
allow_chat_direct_messages: {},
302+
allow_topic_mentions: {},
303+
allow_personal_messages: {}
304+
}
305+
306+
# Create basic entity details
307+
entity_details = {
281308
persona_id: ai_persona.id,
282-
name: ai_persona.name,
283-
description: ai_persona.description,
284-
enabled: ai_persona.enabled,
285-
priority: ai_persona.priority,
286-
system_prompt: ai_persona.system_prompt&.truncate(100),
287-
default_llm_id: ai_persona.default_llm_id,
288-
temperature: ai_persona.temperature,
289-
top_p: ai_persona.top_p,
290-
user_id: ai_persona.user_id,
291-
vision_enabled: ai_persona.vision_enabled,
292-
tools_count: (ai_persona.tools || []).size,
293-
allowed_group_ids: ai_persona.allowed_group_ids
309+
persona_name: ai_persona.name
294310
}
295311

312+
# Create logger instance
296313
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
314+
315+
# Extract attributes based on field configuration
316+
log_details = entity_details.dup
317+
log_details.merge!(logger.send(:extract_entity_attributes, ai_persona, field_config))
318+
319+
# Add tools count separately as it's not a direct attribute
320+
log_details[:tools_count] = (ai_persona.tools || []).size
321+
322+
# Add allowed_group_ids
323+
log_details[:allowed_group_ids] = ai_persona.allowed_group_ids if ai_persona.allowed_group_ids.present?
324+
297325
logger.log_custom("create_ai_persona", log_details)
298326
end
299327

300328
def log_ai_persona_update(ai_persona, initial_attributes)
301-
trackable_fields = %w[
302-
name description enabled system_prompt priority temperature top_p default_llm_id
303-
user_id max_context_posts vision_enabled vision_max_pixels rag_chunk_tokens
304-
rag_chunk_overlap_tokens rag_conversation_chunks rag_llm_model_id
305-
question_consolidator_llm_id tool_details forced_tool_count
306-
allow_chat_channel_mentions allow_chat_direct_messages allow_topic_mentions allow_personal_messages
307-
]
308-
309-
json_fields = %w[allowed_group_ids tools response_format examples]
329+
# Create field configuration with appropriate types
330+
field_config = {
331+
name: {},
332+
description: {},
333+
enabled: {},
334+
priority: {},
335+
system_prompt: { type: :large_text },
336+
default_llm_id: {},
337+
temperature: {},
338+
top_p: {},
339+
user_id: {},
340+
vision_enabled: {},
341+
vision_max_pixels: {},
342+
max_context_posts: {},
343+
rag_chunk_tokens: {},
344+
rag_chunk_overlap_tokens: {},
345+
rag_conversation_chunks: {},
346+
rag_llm_model_id: {},
347+
question_consolidator_llm_id: {},
348+
tool_details: {},
349+
forced_tool_count: {},
350+
allow_chat_channel_mentions: {},
351+
allow_chat_direct_messages: {},
352+
allow_topic_mentions: {},
353+
allow_personal_messages: {},
354+
json_fields: %w[allowed_group_ids tools response_format examples]
355+
}
356+
357+
# Create basic entity details
358+
entity_details = {
359+
persona_id: ai_persona.id,
360+
persona_name: ai_persona.name
361+
}
310362

363+
# Create logger instance and log the update
311364
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
312-
logger.log_update("persona", ai_persona, initial_attributes, trackable_fields, json_fields)
365+
logger.log_update("persona", ai_persona, initial_attributes, field_config, entity_details)
313366
end
314367

315368
def log_ai_persona_deletion(persona_details)

0 commit comments

Comments
 (0)