Files
Agent-Squad/cli/demo.go
T
weichen b6ff0ab55f
ci / vet-and-test (push) Failing after 2s
ci / docker-checkout-hostgateway (push) Failing after 2s
Add catalog-driven orchestration: intent catalog injection
Details (original message):
Add catalog-driven orchestration: intent catalog injection, deterministic plan validation, and system-function steps.
2026-09-29 16:37:46 +00:00

218 lines
8.4 KiB
Go

package cli
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"agent-native/exec"
"agent-squad/squad"
)
// demoBackend 脚本化后端:按子工作区目录名回放预设产出(离线演示/测试)。
type demoBackend struct {
// script: 子目录名(如 01_researcher)→ target → 文件内容。
script map[string]map[string]string
}
func (b *demoBackend) Name() string { return "demo" }
func (b *demoBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
key := filepath.Base(filepath.Clean(req.WorkDir))
files, ok := b.script[key]
if !ok {
// 未脚本化的步骤退化为写一个说明文件到首个 target。
files = map[string]string{}
for i, t := range req.Targets {
if i == 0 {
files[t] = fmt.Sprintf("# 演示产出\n\n步骤目录 %s 未脚本化,这是默认产出。\n", key)
}
}
}
patches := map[string][]byte{}
for _, t := range req.Targets {
content := files[t]
if content == "" {
content = "\n"
}
out := filepath.Join(req.WorkDir, t)
_ = os.MkdirAll(filepath.Dir(out), 0o755)
if err := os.WriteFile(out, []byte(content), 0o644); err != nil {
return nil, err
}
patches[t] = []byte(content)
}
return &exec.Result{Success: true, Patches: patches, Logs: "demo backend replay"}, nil
}
// cmdDemo 离线全链路演示:注册角色 → 规划 → 执行 → HITL 阻塞缺口挂起 →
// 人工回应 → 继续 → 质检 → 交付。全程 stub/脚本驱动,无需模型与外部智能体。
func cmdDemo(args []string) int {
fs := flag.NewFlagSet("demo", flag.ContinueOnError)
keep := fs.Bool("keep", false, "保留演示数据目录")
if err := fs.Parse(args); err != nil {
return 2
}
dir, err := os.MkdirTemp("", "agent-squad-demo-")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
defer func() {
if *keep {
fmt.Println("\n演示工作区保留于:", dir)
} else {
_ = os.RemoveAll(dir)
}
}()
fmt.Println("═══ agent-squad 离线演示(stub 智能体 + mock 规划器 + HITL)═══")
fmt.Println("数据目录:", dir)
// 1. 组装:空注册表起步。
reg := squad.NewRoleRegistry()
bps := squad.NewBlueprintRegistry()
backends := squad.NewBackendResolver()
demoScript := map[string]map[string]string{
"01_researcher": {
"notes.md": "# 调研笔记\n\n- 目标产品:便携式超声诊断仪\n- 竞品:A/B/C 三款\n- 法规路径:二类器械,注册检验 + 临床评价\n",
"fields.json": `{"target_market":"中国","device_class":"二类","budget":"15万"}`,
"gaps.json": `[{"item":"目标年产量未提供,影响成本测算","blocking":true}]`,
},
"02_writer": {
"output.md": "# 便携式超声诊断仪调研报告\n\n## 概述与背景\n\n本报告基于输入材料与调研产出,梳理便携式超声诊断仪的市场与技术背景。目标市场为中国(二类器械),预算 15 万元。\n\n## 核心内容\n\n竞品 A/B/C 在成像质量与便携性上各有取舍;技术路线建议探头一体化设计与电池供电优化。注册路径按二类器械执行注册检验与临床评价。\n\n## 风险与注意事项\n\n- 供应链对探头晶片依赖较高\n- 注册周期受检验排队影响\n\n## 基准假设与待确认清单\n\n- 年产量按人工澄清结论执行\n",
},
"03_reviewer": {
"output.md": "# 便携式超声诊断仪调研报告(终审版)\n\n## 概述与背景\n\n本报告由小队协作产出:调研专员梳理事实与缺口,撰写专员成文,审核专员终审。目标市场为中国(二类器械),预算 15 万元,年产量 10 万套。\n\n## 核心内容\n\n竞品 A/B/C 在成像质量与便携性上各有取舍;技术路线建议探头一体化设计与电池供电优化。注册路径按二类器械执行注册检验与临床评价。成本测算基于年产量 10 万套摊薄产线投入。\n\n## 风险与注意事项\n\n- 供应链对探头晶片依赖较高\n- 注册周期受检验排队影响\n- 年产量波动超过 ±30% 时需重估成本模型\n\n## 基准假设与待确认清单\n\n- 灭菌方式采用环氧乙烷(EO)灭菌(行业常规)\n",
"fields.json": `{"annual_volume":"100000","review":"passed"}`,
},
}
backends.RegisterInstance("demo", &demoBackend{script: demoScript})
planner := &squad.MockPlanner{Script: []squad.StepSpec{
{ID: "step_01_research", Role: "researcher", Title: "市场与技术调研,回填事实与缺口",
Targets: []string{"notes.md", "fields.json", "gaps.json"}},
{ID: "step_02_draft", Role: "writer", Title: "汇总调研产出,撰写报告初稿",
Targets: []string{"output.md"}},
{ID: "step_03_review", Role: "reviewer", Title: "一致性终审并产出终版",
Targets: []string{"output.md", "fields.json"}},
}}
sink := squad.NewFanoutSink(4096)
engine, err := squad.NewEngine(squad.Options{
DataDir: dir, Registry: reg, Blueprints: bps, Planner: planner,
Backends: backends, Sink: sink,
})
if err != nil {
fmt.Fprintln(os.Stderr, "组装失败:", err)
return 1
}
// 2. 注册角色(演示小队编制——底座本身无预设角色)。
fmt.Println("\n[1] 注册角色(手动注册,backend=demo 脚本智能体)")
for _, spec := range []squad.RoleSpec{
{Role: "researcher", Name: "调研专员", Backend: "demo", Keywords: []string{"调研", "市场", "竞品"},
Responsibilities: []string{"市场与技术调研", "事实回填"}},
{Role: "writer", Name: "撰写专员", Backend: "demo", Keywords: []string{"撰写", "报告"},
Responsibilities: []string{"汇总产出并成文"}},
{Role: "reviewer", Name: "审核专员", Backend: "demo", Keywords: []string{"审核", "终审"},
Responsibilities: []string{"一致性终审"}},
} {
if err := reg.Register(spec); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Printf(" + %-12s %s\n", spec.Role, spec.Name)
}
// 3. 发起运行。
goal := squad.Goal{Text: "调研便携式超声诊断仪并产出报告"}
fmt.Printf("\n[2] 发起运行:%s\n", goal.Text)
res, err := engine.Run(context.Background(), goal, squad.RunOptions{})
if err != nil {
fmt.Fprintln(os.Stderr, "运行失败:", err)
return 1
}
reportSteps(res)
// 4. HITL 挂起 → 回应 → 续跑。
for res.Status.IsHeld() && res.Pending != nil {
fmt.Printf("\n[3] ⏸ HITL 挂起(%s):%s\n", res.Pending.Kind, res.Pending.Question)
for _, line := range strings.Split(res.Pending.Context, "\n") {
if line != "" {
fmt.Println(" ", line)
}
}
answer := demoAnswer(res.Pending)
fmt.Printf(" → 自动回应:choice=%q answer=%q\n", answer.Choice, answer.Answer)
res, err = engine.Respond(context.Background(), res.RunID, answer)
if err != nil {
fmt.Fprintln(os.Stderr, "回应失败:", err)
return 1
}
reportSteps(res)
}
// 5. 结果。
fmt.Printf("\n[4] 运行终态:%s\n", res.Status)
if res.Summary != "" {
fmt.Println(" ", res.Summary)
}
if res.Deliverable != "" {
path := filepath.Join(res.WorkDir, res.Deliverable)
fmt.Println(" 交付物:", path)
if head := headLines(path, 6); head != "" {
fmt.Println(" ┌─ 交付物预览")
for _, l := range strings.Split(strings.TrimRight(head, "\n"), "\n") {
fmt.Println(" │", l)
}
fmt.Println(" └─")
}
}
bb, _ := engine.Blackboard(res.RunID)
if bb != nil {
enc, _ := json.MarshalIndent(bb.Fields, "", " ")
fmt.Println(" 黑板字段:", string(enc))
}
fmt.Println("\n演示完成。真实使用:agent-squad role add / run / serve。")
return 0
}
func demoAnswer(req *squad.HumanRequest) squad.RespondDecision {
switch req.Kind {
case squad.HitlGap:
return squad.RespondDecision{RequestID: req.ID, Choice: "resolved", Answer: "年产量 10 万套"}
default:
choice := "approve"
if len(req.Options) > 0 {
choice = req.Options[0].Value
}
return squad.RespondDecision{RequestID: req.ID, Choice: choice}
}
}
func reportSteps(res *squad.RunResult) {
if res.Plan == nil {
return
}
for _, s := range res.Plan.Steps {
mark := map[string]string{"completed": "✔", "failed": "✘", "skipped": "⤼", "running": "…", "pending": " "}[s.Status]
fmt.Printf(" %s %-22s %-12s %s\n", mark, s.ID, s.Role, s.Title)
}
}
func headLines(path string, n int) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
lines := strings.Split(string(data), "\n")
if len(lines) > n {
lines = lines[:n]
}
return strings.Join(lines, "\n")
}