Add Phase 9 LLM scheduler, config resolution, diagnostics primitives
This commit is contained in:
126
internal/framework/llm/diagnostics.go
Normal file
126
internal/framework/llm/diagnostics.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const redactedSecret = "[REDACTED]"
|
||||
|
||||
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 {
|
||||
filtered := make([]string, 0, len(secrets))
|
||||
for _, secret := range secrets {
|
||||
secret = strings.TrimSpace(secret)
|
||||
if secret != "" {
|
||||
filtered = append(filtered, secret)
|
||||
}
|
||||
}
|
||||
|
||||
return &DiagnosticsWriter{
|
||||
dir: dir,
|
||||
secrets: filtered,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
raw, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal json artifact %q: %w", filepath.Base(path), err)
|
||||
}
|
||||
redacted := redactJSONSecrets(raw, w.secrets)
|
||||
if !json.Valid(redacted) {
|
||||
return fmt.Errorf("redacted json artifact %q is invalid", filepath.Base(path))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func redactJSONSecrets(raw []byte, secrets []string) []byte {
|
||||
if len(secrets) == 0 {
|
||||
return raw
|
||||
}
|
||||
|
||||
result := string(raw)
|
||||
for _, secret := range secrets {
|
||||
if secret == "" {
|
||||
continue
|
||||
}
|
||||
result = strings.ReplaceAll(result, "Bearer "+secret, "Bearer "+redactedSecret)
|
||||
result = strings.ReplaceAll(result, secret, redactedSecret)
|
||||
}
|
||||
return []byte(result)
|
||||
}
|
||||
Reference in New Issue
Block a user