feat(ai): add Jev shadow gates (triage before, review after the LLM diagnosis)
TypeSafe System One(Jev)への最小クライアントと、診断ループに並走する shadow ゲート 2 つを追加した。triage はアラートと事前取得サンプルからラベル・ 回復中か・層を判定し、review は診断の根拠・ランブック・target を検証して best_action を選ぶ。JEV_MODE=shadow のときだけ動き、答えは ai.jev_shadow と 診断結果の jev フィールドに記録するだけで、処理の分岐は一切変えない。
This commit is contained in:
1 parent
af1cb4f6dc
commit
91cdd9ada5
6 files changed
+479
-19
No files matched your search
+38
-17
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/diag"
|
||||
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/harness"
|
||||
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/jev"
|
||||
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/openai"
|
||||
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/rag"
|
||||
"github.com/selfheal-lab/selfheal-lab-apps/ai/internal/tools"
|
||||
@@ -43,6 +44,7 @@ type Agent struct {
|
||||
enums diag.Enums
|
||||
promptV string
|
||||
http *http.Client
|
||||
jev *jev.Client // nil = shadow gates off
|
||||
}
|
||||
|
||||
func New(cfg Config, oai *openai.Client, tr *tools.Runner, hc *harness.Client, store *rag.Store, enums diag.Enums, promptVersion string) *Agent {
|
||||
@@ -61,6 +63,9 @@ func New(cfg Config, oai *openai.Client, tr *tools.Runner, hc *harness.Client, s
|
||||
return &Agent{cfg: cfg, oai: oai, tools: tr, harness: hc, store: store, enums: enums, promptV: promptVersion, http: &http.Client{Timeout: 30 * time.Second}}
|
||||
}
|
||||
|
||||
// EnableShadow turns the Jev shadow gates on (JEV_MODE=shadow). Answers are recorded only.
|
||||
func (a *Agent) EnableShadow(c *jev.Client) { a.jev = c }
|
||||
|
||||
// Request is the harness -> AI VM contract (harness alerts.go diagnose()).
|
||||
type Request struct {
|
||||
Alert map[string]any `json:"alert"`
|
||||
@@ -72,14 +77,15 @@ type Request struct {
|
||||
// diagnosis event payload and reads root_cause_label / proposed_action from it) plus bookkeeping.
|
||||
type Result struct {
|
||||
diag.Diagnosis
|
||||
SessionID string `json:"session_id"`
|
||||
Model string `json:"model"`
|
||||
PromptVersion string `json:"prompt_version"`
|
||||
Turns int `json:"turns"`
|
||||
ToolCalls int `json:"tool_calls"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Disposition string `json:"disposition"` // proposal | escalated | none
|
||||
ProposalToken string `json:"proposal_token,omitempty"`
|
||||
SessionID string `json:"session_id"`
|
||||
Model string `json:"model"`
|
||||
PromptVersion string `json:"prompt_version"`
|
||||
Turns int `json:"turns"`
|
||||
ToolCalls int `json:"tool_calls"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Disposition string `json:"disposition"` // proposal | escalated | none
|
||||
ProposalToken string `json:"proposal_token,omitempty"`
|
||||
Jev map[string]any `json:"jev,omitempty"` // shadow gate answers (recorded, never acted on)
|
||||
}
|
||||
|
||||
type toolCallLog struct {
|
||||
@@ -97,7 +103,10 @@ func (a *Agent) toolDefs() []openai.Tool {
|
||||
return defs
|
||||
}
|
||||
|
||||
func (a *Agent) userMessage(ctx context.Context, req Request) string {
|
||||
func (a *Agent) userMessage(ctx context.Context, req Request, prefetched string) string {
|
||||
if prefetched == "" {
|
||||
prefetched = a.Prefetch(ctx)
|
||||
}
|
||||
alert, _ := json.MarshalIndent(req.Alert, "", " ")
|
||||
var b strings.Builder
|
||||
b.WriteString("## Topology\n")
|
||||
@@ -107,6 +116,20 @@ func (a *Agent) userMessage(ctx context.Context, req Request) string {
|
||||
b.WriteString("\n\n## Alert payload\n```json\n")
|
||||
b.Write(alert)
|
||||
b.WriteString("\n```\n\n## Last 10 minutes (pre-fetched)\n")
|
||||
b.WriteString(prefetched)
|
||||
if req.RunID != nil {
|
||||
fmt.Fprintf(&b, "\nrun_id=%s", *req.RunID)
|
||||
}
|
||||
if req.ScenarioID != nil {
|
||||
fmt.Fprintf(&b, " scenario_id=%s", *req.ScenarioID)
|
||||
}
|
||||
b.WriteString("\n\nInvestigate with the tools, then call submit_diagnosis exactly once.")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Prefetch returns the four pre-fetched 10-minute series (probe, p95, error ratio, legacy up) as markdown lines.
|
||||
func (a *Agent) Prefetch(ctx context.Context) string {
|
||||
var b strings.Builder
|
||||
for _, q := range []struct{ name, promql string }{
|
||||
{"probe_success", `probe_success`},
|
||||
{"api_p95_latency_s", `histogram_quantile(0.95, sum by (le) (rate(demo_http_request_duration_seconds_bucket[5m])))`},
|
||||
@@ -119,25 +142,23 @@ func (a *Agent) userMessage(ctx context.Context, req Request) string {
|
||||
}
|
||||
fmt.Fprintf(&b, "- %s: %s\n", q.name, out)
|
||||
}
|
||||
if req.RunID != nil {
|
||||
fmt.Fprintf(&b, "\nrun_id=%s", *req.RunID)
|
||||
}
|
||||
if req.ScenarioID != nil {
|
||||
fmt.Fprintf(&b, " scenario_id=%s", *req.ScenarioID)
|
||||
}
|
||||
b.WriteString("\n\nInvestigate with the tools, then call submit_diagnosis exactly once.")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Diagnose runs the tool loop and returns the validated diagnosis. The session is stored whatever the outcome.
|
||||
func (a *Agent) Diagnose(ctx context.Context, req Request) (*Result, error) {
|
||||
return a.DiagnoseWith(ctx, req, "")
|
||||
}
|
||||
|
||||
// DiagnoseWith runs Diagnose with an already pre-fetched sample block (empty = fetch now).
|
||||
func (a *Agent) DiagnoseWith(ctx context.Context, req Request, prefetched string) (*Result, error) {
|
||||
start := time.Now()
|
||||
sid := "ses-" + uuid.NewString()[:8]
|
||||
res := &Result{SessionID: sid, Model: a.cfg.Model, PromptVersion: a.promptV}
|
||||
var calls []toolCallLog
|
||||
input := []openai.Item{
|
||||
{Type: "message", Role: "system", Content: a.cfg.SystemPrompt},
|
||||
{Type: "message", Role: "user", Content: a.userMessage(ctx, req)},
|
||||
{Type: "message", Role: "user", Content: a.userMessage(ctx, req, prefetched)},
|
||||
}
|
||||
session := rag.Session{SessionID: sid, RunID: req.RunID, ScenarioID: req.ScenarioID, Model: a.cfg.Model, PromptVersion: a.promptV, Input: map[string]any{"alert": req.Alert, "user_message": input[1].Content}}
|
||||
finish := func(err error) {
|
||||
|
||||
Reference in new issue
Block a user