package multiagent import ( "context" "encoding/json" "strings" "github.com/cloudwego/eino/adk/prebuilt/planexecute" ) // lenientPlan keeps plan_execute running even when model tool arguments contain minor JSON defects. // It first tries strict JSON, then falls back to lightweight step extraction heuristics. type lenientPlan struct { Steps []string `json:"steps"` } func newLenientPlan(context.Context) planexecute.Plan { return &lenientPlan{} } func (p *lenientPlan) FirstStep() string { if p == nil || len(p.Steps) == 0 { return "" } return p.Steps[0] } func (p *lenientPlan) MarshalJSON() ([]byte, error) { type alias lenientPlan return json.Marshal((*alias)(p)) } func (p *lenientPlan) UnmarshalJSON(b []byte) error { type alias lenientPlan var strict alias if err := json.Unmarshal(b, &strict); err == nil { strict.Steps = normalizePlanSteps(strict.Steps) if len(strict.Steps) > 0 { *p = lenientPlan(strict) return nil } } steps := extractPlanStepsLenient(string(b)) if len(steps) == 0 { steps = []string{"继续按当前目标执行下一步,并输出可验证证据。"} } p.Steps = steps return nil } func extractPlanStepsLenient(raw string) []string { s := strings.TrimSpace(stripCodeFence(raw)) if s == "" { return nil } if extracted, ok := sliceByStepsArray(s); ok { var arr []string if err := json.Unmarshal([]byte(extracted), &arr); err == nil { arr = normalizePlanSteps(arr) if len(arr) > 0 { return arr } } if arr := splitStepsHeuristically(strings.Trim(extracted, "[]")); len(arr) > 0 { return arr } } // Last-resort: treat plaintext body as one actionable step. s = strings.TrimSpace(s) if s == "" { return nil } return []string{s} } func sliceByStepsArray(s string) (string, bool) { lower := strings.ToLower(s) key := `"steps"` i := strings.Index(lower, key) if i < 0 { return "", false } start := strings.Index(s[i:], "[") if start < 0 { return "", false } start += i depth := 0 for j := start; j < len(s); j++ { switch s[j] { case '[': depth++ case ']': depth-- if depth == 0 { return s[start : j+1], true } } } return "", false } func splitStepsHeuristically(body string) []string { body = strings.ReplaceAll(body, "\r\n", "\n") body = strings.ReplaceAll(body, "\\n", "\n") var parts []string if strings.Contains(body, "\n") { for _, line := range strings.Split(body, "\n") { parts = append(parts, line) } } else { for _, seg := range strings.Split(body, ",") { parts = append(parts, seg) } } out := make([]string, 0, len(parts)) for _, part := range parts { t := strings.TrimSpace(part) t = strings.Trim(t, "\"'`") t = strings.TrimLeft(t, "-*0123456789.、 \t") t = strings.TrimSpace(strings.ReplaceAll(t, `\"`, `"`)) if t == "" { continue } out = append(out, t) } return normalizePlanSteps(out) } func normalizePlanSteps(in []string) []string { out := make([]string, 0, len(in)) for _, step := range in { t := strings.TrimSpace(step) if t == "" { continue } out = append(out, t) } return out } func stripCodeFence(s string) string { s = strings.TrimSpace(s) if !strings.HasPrefix(s, "```") { return s } s = strings.TrimPrefix(s, "```json") s = strings.TrimPrefix(s, "```JSON") s = strings.TrimPrefix(s, "```") s = strings.TrimSuffix(strings.TrimSpace(s), "```") return strings.TrimSpace(s) }