138 lines
4.3 KiB
Go
138 lines
4.3 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"ai-operator/internal/audit/redaction"
|
|
"ai-operator/internal/config"
|
|
)
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
cfg config.AuditConfig
|
|
redactor redaction.DefaultRedactor
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewService(repo Repository, cfg config.AuditConfig, logger *slog.Logger) *Service {
|
|
if repo == nil || !cfg.Enabled {
|
|
repo = NoopRepository{}
|
|
}
|
|
return &Service{repo: repo, cfg: cfg, redactor: redaction.New(), logger: logger}
|
|
}
|
|
func (s *Service) Health(ctx context.Context) error { return s.repo.Health(ctx) }
|
|
func (s *Service) UpsertCall(ctx context.Context, c CallRecord) error {
|
|
c.CallerNumberMasked = s.redact(c.CallerNumberMasked, s.cfg.MaxEventMetadataChars)
|
|
c.Metadata = s.redactJSON(c.Metadata)
|
|
return s.handle("audit upsert call", s.repo.UpsertCall(ctx, c))
|
|
}
|
|
func (s *Service) EndCall(ctx context.Context, callID, reason string) error {
|
|
return s.handle("audit end call", s.repo.EndCall(ctx, callID, s.redact(reason, 512)))
|
|
}
|
|
func (s *Service) AddEvent(ctx context.Context, e EventRecord) error {
|
|
e.Message = s.redact(e.Message, s.cfg.MaxEventMetadataChars)
|
|
e.Metadata = s.redactJSON(e.Metadata)
|
|
return s.handle("audit event", s.repo.AddEvent(ctx, e))
|
|
}
|
|
func (s *Service) AddTranscript(ctx context.Context, t TranscriptRecord) error {
|
|
if !s.cfg.StoreTranscripts {
|
|
return nil
|
|
}
|
|
if t.EventType == "transcript.user.delta" || t.EventType == "transcript.assistant.delta" {
|
|
if !s.cfg.StoreTranscriptDeltas {
|
|
return nil
|
|
}
|
|
}
|
|
t.Text = s.redact(t.Text, s.cfg.MaxTranscriptChars)
|
|
t.Metadata = s.redactJSON(t.Metadata)
|
|
return s.handle("audit transcript", s.repo.AddTranscript(ctx, t))
|
|
}
|
|
func (s *Service) AddToolAudit(ctx context.Context, t ToolAuditRecord) error {
|
|
t.Args = s.redactJSON(t.Args)
|
|
t.Result = s.redactJSON(t.Result)
|
|
t.Metadata = s.redactJSON(t.Metadata)
|
|
return s.handle("audit tool", s.repo.AddToolAudit(ctx, t))
|
|
}
|
|
func (s *Service) AddKBAudit(ctx context.Context, k KBAuditRecord) error {
|
|
k.Query = s.redact(k.Query, s.cfg.MaxTranscriptChars)
|
|
k.Metadata = s.redactJSON(k.Metadata)
|
|
return s.handle("audit kb", s.repo.AddKBAudit(ctx, k))
|
|
}
|
|
func (s *Service) AddHandoffAudit(ctx context.Context, h HandoffAuditRecord) error {
|
|
h.Target = s.redact(h.Target, 512)
|
|
h.Summary = s.redact(h.Summary, s.cfg.MaxEventMetadataChars)
|
|
h.Metadata = s.redactJSON(h.Metadata)
|
|
return s.handle("audit handoff", s.repo.AddHandoffAudit(ctx, h))
|
|
}
|
|
func (s *Service) AddProviderAudit(ctx context.Context, p ProviderAuditRecord) error {
|
|
if !s.cfg.StoreProviderEvents {
|
|
return nil
|
|
}
|
|
p.Error = s.redact(p.Error, s.cfg.MaxEventMetadataChars)
|
|
p.Metadata = s.redactJSON(p.Metadata)
|
|
return s.handle("audit provider", s.repo.AddProviderAudit(ctx, p))
|
|
}
|
|
func (s *Service) AddMediaAudit(ctx context.Context, m MediaAuditRecord) error {
|
|
if !s.cfg.StoreMediaStats {
|
|
return nil
|
|
}
|
|
m.Error = s.redact(m.Error, s.cfg.MaxEventMetadataChars)
|
|
m.Metadata = s.redactJSON(m.Metadata)
|
|
return s.handle("audit media", s.repo.AddMediaAudit(ctx, m))
|
|
}
|
|
func (s *Service) ExportCall(ctx context.Context, callID string) (CallAuditExport, error) {
|
|
return s.repo.ExportCall(ctx, callID)
|
|
}
|
|
func (s *Service) Prune(ctx context.Context, req RetentionPruneRequest) (RetentionPruneResult, error) {
|
|
if req.Now.IsZero() {
|
|
req.Now = time.Now().UTC()
|
|
}
|
|
if req.RetentionDays == 0 {
|
|
req.RetentionDays = s.cfg.RetentionDays
|
|
}
|
|
if req.TranscriptRetentionDays == 0 {
|
|
req.TranscriptRetentionDays = s.cfg.TranscriptRetentionDays
|
|
}
|
|
if req.ToolAuditRetentionDays == 0 {
|
|
req.ToolAuditRetentionDays = s.cfg.ToolAuditRetentionDays
|
|
}
|
|
if req.ErrorAuditRetentionDays == 0 {
|
|
req.ErrorAuditRetentionDays = s.cfg.ErrorAuditRetentionDays
|
|
}
|
|
return s.repo.Prune(ctx, req)
|
|
}
|
|
|
|
func (s *Service) redact(v string, max int) string {
|
|
if max > 0 && len([]rune(v)) > max {
|
|
r := []rune(v)
|
|
v = string(r[:max])
|
|
}
|
|
if !s.cfg.RedactionEnabled {
|
|
return v
|
|
}
|
|
return s.redactor.RedactText(v).Text
|
|
}
|
|
func (s *Service) redactJSON(v map[string]any) map[string]any {
|
|
if v == nil {
|
|
return map[string]any{}
|
|
}
|
|
if !s.cfg.RedactionEnabled {
|
|
return v
|
|
}
|
|
return s.redactor.RedactJSON(v)
|
|
}
|
|
func (s *Service) handle(msg string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if s.logger != nil {
|
|
s.logger.Warn(msg, "error", s.redact(err.Error(), 1024))
|
|
}
|
|
if s.cfg.FailClosed {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|