|
1 | | -using System.Text.RegularExpressions; |
2 | | -using Microsoft.Extensions.AI; |
3 | | -using Microsoft.Extensions.Logging; |
4 | 1 | using Umbraco.Ai.Core.Models; |
5 | | -using Umbraco.Cms.Core.Models.Membership; |
6 | 2 |
|
7 | 3 | namespace Umbraco.Ai.Core.AuditLog; |
8 | 4 |
|
@@ -144,119 +140,4 @@ public sealed class AiAuditLog |
144 | 140 | /// Stored as a JSON dictionary in the database. |
145 | 141 | /// </summary> |
146 | 142 | public IReadOnlyDictionary<string, string>? Metadata { get; init; } |
147 | | - |
148 | | - /// <summary> |
149 | | - /// Creates a new AiAuditLog instance from the given AiAuditContext. |
150 | | - /// </summary> |
151 | | - /// <param name="context">The AiAuditContext containing operation details.</param> |
152 | | - /// <param name="options">The AiAuditLogOptions for configuring audit-log behavior.</param> |
153 | | - /// <param name="metadata">Optional metadata to include in the audit-log.</param> |
154 | | - /// <param name="detailLevel">The detail level for this audit-log.</param> |
155 | | - /// <param name="user">The user who initiated the operation.</param> |
156 | | - /// <param name="id">Optional specific ID for the audit-log; a new GUID will be generated if not provided.</param> |
157 | | - /// <param name="parentId">Optional parent audit-log ID if this log is part of a nested operation.</param> |
158 | | - /// <param name="loggerFactory">Optional logger factory for logging during creation.</param> |
159 | | - /// <returns></returns> |
160 | | - public static AiAuditLog Create( |
161 | | - AiAuditContext context, |
162 | | - AiAuditLogOptions options, |
163 | | - IReadOnlyDictionary<string, string>? metadata = null, |
164 | | - AiAuditLogDetailLevel detailLevel = AiAuditLogDetailLevel.FailuresOnly, |
165 | | - IUser? user = null, |
166 | | - Guid? id = null, |
167 | | - Guid? parentId = null, |
168 | | - ILoggerFactory? loggerFactory = null) |
169 | | - { |
170 | | - if (!context.ProfileId.HasValue) throw new ArgumentException("ProfileId must be set in the AiAuditContext.", nameof(context)); |
171 | | - if (string.IsNullOrWhiteSpace(context.ProfileAlias)) throw new ArgumentException("ProfileAlias must be set in the AiAuditContext.", nameof(context)); |
172 | | - if (string.IsNullOrWhiteSpace(context.ProviderId)) throw new ArgumentException("ProviderId must be set in the AiAuditContext.", nameof(context)); |
173 | | - if (string.IsNullOrWhiteSpace(context.ModelId)) throw new ArgumentException("ModelId must be set in the AiAuditContext.", nameof(context)); |
174 | | - |
175 | | - var auditLog = new AiAuditLog |
176 | | - { |
177 | | - Id = id ?? Guid.NewGuid(), |
178 | | - StartTime = DateTime.UtcNow, |
179 | | - ParentAuditLogId = parentId, |
180 | | - UserId = user?.Id.ToString(), |
181 | | - UserName = user?.Name, |
182 | | - Capability = context.Capability, |
183 | | - ProfileId = context.ProfileId.Value, |
184 | | - ProfileAlias = context.ProfileAlias, |
185 | | - ProviderId = context.ProviderId, |
186 | | - ModelId = context.ModelId, |
187 | | - EntityId = context.EntityId, |
188 | | - EntityType = context.EntityType, |
189 | | - FeatureType = context.FeatureType, |
190 | | - FeatureId = context.FeatureId, |
191 | | - Metadata = metadata != null |
192 | | - ? new Dictionary<string, string>(metadata) |
193 | | - : null, |
194 | | - DetailLevel = detailLevel |
195 | | - }; |
196 | | - |
197 | | - // Set initial status to Running |
198 | | - auditLog.Status = AiAuditLogStatus.Running; |
199 | | - |
200 | | - var logger = loggerFactory?.CreateLogger<AiAuditLog>(); |
201 | | - |
202 | | - // Capture response snapshot if configured |
203 | | - if (options.PersistPrompts && context.Prompt is not null) |
204 | | - { |
205 | | - var prompt = FormatPromptSnapshot(context.Prompt, context.Capability); |
206 | | - prompt = ApplyRedaction(options, prompt, logger); |
207 | | - auditLog.PromptSnapshot = prompt; |
208 | | - logger?.LogDebug("Captured prompt snapshot for audit-log {AuditLogId}: {Length} characters", |
209 | | - auditLog.Id, prompt?.Length ?? 0); |
210 | | - } |
211 | | - |
212 | | - return auditLog; |
213 | | - } |
214 | | - |
215 | | - private static string? FormatPromptSnapshot(object? promptObj, AiCapability capability) |
216 | | - { |
217 | | - if (promptObj is null) |
218 | | - { |
219 | | - return null; |
220 | | - } |
221 | | - |
222 | | - try |
223 | | - { |
224 | | - return capability switch |
225 | | - { |
226 | | - AiCapability.Chat when promptObj is IEnumerable<ChatMessage> messages => |
227 | | - string.Join("\n", messages.Select(m => $"[{m.Role}] {m.Text}")), |
228 | | - |
229 | | - AiCapability.Embedding when promptObj is IEnumerable<string> values => |
230 | | - string.Join("\n", values.Select((v, i) => $"[{i}] {v}")), |
231 | | - |
232 | | - _ => promptObj.ToString() |
233 | | - }; |
234 | | - } |
235 | | - catch |
236 | | - { |
237 | | - // If formatting fails, return a fallback representation |
238 | | - return $"[Unable to format {capability} prompt]"; |
239 | | - } |
240 | | - } |
241 | | - |
242 | | - private static string? ApplyRedaction(AiAuditLogOptions options, string? input, |
243 | | - ILogger<AiAuditLog>? logger = null) |
244 | | - { |
245 | | - if (string.IsNullOrEmpty(input) || options.RedactionPatterns.Count == 0) |
246 | | - return input; |
247 | | - |
248 | | - var result = input; |
249 | | - foreach (var pattern in options.RedactionPatterns) |
250 | | - { |
251 | | - try |
252 | | - { |
253 | | - result = Regex.Replace(result, pattern, "[REDACTED]", RegexOptions.IgnoreCase); |
254 | | - } |
255 | | - catch (Exception ex) |
256 | | - { |
257 | | - logger?.LogWarning(ex, "Failed to apply redaction pattern: {Pattern}", pattern); |
258 | | - } |
259 | | - } |
260 | | - return result; |
261 | | - } |
262 | 143 | } |
0 commit comments