95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
package llm
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/redaction"
|
|
)
|
|
|
|
var stageSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_.-]+`)
|
|
|
|
// InteractionArtifacts contains file paths for written prompt/response
|
|
// diagnostics.
|
|
type InteractionArtifacts struct {
|
|
RequestMetadataPath string
|
|
RequestPayloadPath string
|
|
ResponsePayloadPath string
|
|
ErrorPayloadPath string
|
|
}
|
|
|
|
// DiagnosticsWriter writes generic machine-readable LLM interaction artifacts.
|
|
type DiagnosticsWriter struct {
|
|
dir string
|
|
secrets []string
|
|
}
|
|
|
|
// NewDiagnosticsWriter creates a diagnostics writer rooted at dir.
|
|
func NewDiagnosticsWriter(dir string, secrets []string) *DiagnosticsWriter {
|
|
return &DiagnosticsWriter{dir: dir, secrets: append([]string(nil), secrets...)}
|
|
}
|
|
|
|
// WriteInteraction writes request/response/error artifacts for a generic LLM
|
|
// interaction stage.
|
|
func (w *DiagnosticsWriter) WriteInteraction(stage string, requestMetadata any, requestPayload any, responsePayload any, errorPayload any) (InteractionArtifacts, error) {
|
|
if strings.TrimSpace(w.dir) == "" {
|
|
return InteractionArtifacts{}, fmt.Errorf("diagnostics directory must not be empty")
|
|
}
|
|
if err := os.MkdirAll(w.dir, 0o755); err != nil {
|
|
return InteractionArtifacts{}, fmt.Errorf("create diagnostics directory: %w", err)
|
|
}
|
|
|
|
base := sanitizeStageName(stage)
|
|
paths := InteractionArtifacts{
|
|
RequestMetadataPath: filepath.Join(w.dir, base+"-request-metadata.json"),
|
|
RequestPayloadPath: filepath.Join(w.dir, base+"-request-payload.json"),
|
|
ResponsePayloadPath: filepath.Join(w.dir, base+"-response-payload.json"),
|
|
}
|
|
|
|
if err := w.writeJSON(paths.RequestMetadataPath, requestMetadata); err != nil {
|
|
return InteractionArtifacts{}, err
|
|
}
|
|
if err := w.writeJSON(paths.RequestPayloadPath, requestPayload); err != nil {
|
|
return InteractionArtifacts{}, err
|
|
}
|
|
if err := w.writeJSON(paths.ResponsePayloadPath, responsePayload); err != nil {
|
|
return InteractionArtifacts{}, err
|
|
}
|
|
if errorPayload != nil {
|
|
paths.ErrorPayloadPath = filepath.Join(w.dir, base+"-error-payload.json")
|
|
if err := w.writeJSON(paths.ErrorPayloadPath, errorPayload); err != nil {
|
|
return InteractionArtifacts{}, err
|
|
}
|
|
}
|
|
|
|
return paths, nil
|
|
}
|
|
|
|
func sanitizeStageName(stage string) string {
|
|
stage = strings.TrimSpace(stage)
|
|
if stage == "" {
|
|
return "llm"
|
|
}
|
|
safe := stageSanitizer.ReplaceAllString(stage, "_")
|
|
safe = strings.Trim(safe, "._-")
|
|
if safe == "" {
|
|
return "llm"
|
|
}
|
|
return safe
|
|
}
|
|
|
|
func (w *DiagnosticsWriter) writeJSON(path string, payload any) error {
|
|
redacted, err := redaction.New(w.secrets).JSON(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal json artifact %q: %w", filepath.Base(path), err)
|
|
}
|
|
redacted = append(redacted, '\n')
|
|
if err := os.WriteFile(path, redacted, 0o644); err != nil {
|
|
return fmt.Errorf("write json artifact %q: %w", filepath.Base(path), err)
|
|
}
|
|
return nil
|
|
}
|