Files
ai-operator/internal/audit/service.go
T

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
}