Files
selfheal-lab-apps/ai/internal/diag/diag.go
T
seon 683a2de530 style(ai): gofmt
gofmt 정렬만 적용. 동작 변경 없음.
2026-10-08 01:47:19 +09:00

100 lines
4.2 KiB
Go

// Package diag holds the strict diagnosis schema of experiment design 6.3 and the enum sources.
package diag
import (
"encoding/json"
"fmt"
"strings"
)
// Diagnosis is the submit_diagnosis payload (design 6.3). Every field is required under strict mode.
type Diagnosis struct {
RootCauseLabel string `json:"root_cause_label"`
Confidence float64 `json:"confidence"`
Evidence []string `json:"evidence"`
ProposedAction string `json:"proposed_action"`
Target string `json:"target"`
Rationale string `json:"rationale"`
}
// DefaultLabels mirrors the ground_truth_label values of the 13 scenario files (design 7); the agent refreshes the
// list from the harness GET /scenarios at start-up and falls back to this copy.
var DefaultLabels = []string{
"api_pod_failure", "api_pod_killed", "db_primary_down", "gitops_drift", "legacy_instance_lost", "legacy_process_killed",
"memory_pressure_api", "network_latency_api_to_db", "network_latency_legacy", "replication_paused", "schema_mismatch",
"transient_loss_legacy", "transient_partition",
}
// DefaultActions mirrors the layer-2 action_ids of design 2.3; refreshed from the harness GET /actions/catalog (layer contains "2").
var DefaultActions = []string{
"k8s.rollout_restart", "k8s.rollout_undo", "k8s.scale", "k8s.node_drain", "k8s.node_uncordon",
"gce.instance_reset", "mig.recreate_instance", "mig.resize", "db.replay_resume", "net.route_change",
}
// Enums are the closed sets the schema is built from.
type Enums struct {
Labels []string // + "unknown"
Actions []string // + "none", "escalate"
}
func (e Enums) LabelEnum() []string { return append(append([]string{}, e.Labels...), "unknown") }
func (e Enums) ActionEnum() []string {
return append(append([]string{}, e.Actions...), "none", "escalate")
}
// Schema is the JSON schema of the submit_diagnosis tool parameters (strict: additionalProperties false, all required).
func (e Enums) Schema() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"root_cause_label": map[string]any{"type": "string", "enum": e.LabelEnum(), "description": "Root cause label; unknown when the evidence does not support any label."},
"confidence": map[string]any{"type": "number", "description": "0.0 to 1.0. Below 0.5 the harness routes the case to layer 3 (human)."},
"evidence": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "Quotations of tool results that support the label. At least one."},
"proposed_action": map[string]any{"type": "string", "enum": e.ActionEnum(), "description": "Catalogue action_id (layer 2), none when no action is needed, escalate when a human must decide."},
"target": map[string]any{"type": "string", "description": "Action target: deployment name, instance name, legacy:N, or empty."},
"rationale": map[string]any{"type": "string", "description": "Why this label and action; at most 200 characters."},
},
"required": []string{"root_cause_label", "confidence", "evidence", "proposed_action", "target", "rationale"},
"additionalProperties": false,
}
}
// Validate enforces the schema constraints on the model output (strict mode guarantees shape; this checks the values).
func (e Enums) Validate(d Diagnosis) error {
if !contains(e.LabelEnum(), d.RootCauseLabel) {
return fmt.Errorf("root_cause_label %q not in enum", d.RootCauseLabel)
}
if d.Confidence < 0 || d.Confidence > 1 {
return fmt.Errorf("confidence %v out of [0,1]", d.Confidence)
}
if len(d.Evidence) == 0 {
return fmt.Errorf("evidence must have at least one item")
}
if !contains(e.ActionEnum(), d.ProposedAction) {
return fmt.Errorf("proposed_action %q not in enum", d.ProposedAction)
}
if n := len([]rune(d.Rationale)); n > 200 {
return fmt.Errorf("rationale is %d characters, limit 200", n)
}
return nil
}
func Parse(args string, e Enums) (Diagnosis, error) {
var d Diagnosis
dec := json.NewDecoder(strings.NewReader(args))
dec.DisallowUnknownFields()
if err := dec.Decode(&d); err != nil {
return d, fmt.Errorf("submit_diagnosis arguments: %w", err)
}
return d, e.Validate(d)
}
func contains(xs []string, s string) bool {
for _, x := range xs {
if x == s {
return true
}
}
return false
}