diff --git a/internal/squad/roles.go b/internal/squad/roles.go new file mode 100644 index 00000000..437f0094 --- /dev/null +++ b/internal/squad/roles.go @@ -0,0 +1,272 @@ +package squad + +import ( + "fmt" + "strings" + "sync" +) + +// AdHocRoleRequest 特设临时角色派生请求 +type AdHocRoleRequest struct { + RoleKey string `json:"role_key"` + Name string `json:"name"` + BaseRole string `json:"base_role"` + DomainTopic string `json:"domain_topic"` + Responsibilities []string `json:"responsibilities"` + ForbiddenActions []string `json:"forbidden_actions"` +} + +// RoleRegistry 智能体小队角色资产中心 +type RoleRegistry struct { + mu sync.RWMutex + roles map[string]AgentSpec +} + +// NewRoleRegistry 初始化角色资产中心并载入 8 大标准专员 +func NewRoleRegistry() *RoleRegistry { + r := &RoleRegistry{ + roles: make(map[string]AgentSpec), + } + r.registerDefaults() + return r +} + +func (r *RoleRegistry) registerDefaults() { + standards := []AgentSpec{ + { + Role: "organizer", + Name: "材料整理专员", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"材料事实抽取", "缺口识别", "字段结构化回填", "材料提取与整理"}, + ForbiddenActions: []string{"严禁测算价格", "严禁出具法规终结裁定", "严禁撰写正文终稿"}, + InstructionsFile: "configs/agents/organizer.md", + }, + { + Role: "searcher", + Name: "资料检索专员", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"公开资料检索", "法规标准核对", "出处与日期标注", "文献溯源与检索"}, + ForbiddenActions: []string{"严禁编造URL与标准号", "未检索到须标明尝试路径"}, + InstructionsFile: "configs/agents/searcher.md", + }, + { + Role: "regulatory", + Name: "医疗器械法规专员", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"器械分类初筛", "注册路径比对", "关键标准提炼", "法规审评路径"}, + ForbiddenActions: []string{"严禁代做行政终局裁定", "必须标注需向窗口确认"}, + InstructionsFile: "configs/agents/regulatory.md", + }, + { + Role: "engineer", + Name: "系统与方案工程师", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"系统架构设计", "关键技术选型", "工程实现可行性推导", "硬件模块与嵌入式结构方案设计"}, + ForbiddenActions: []string{"严禁虚构未验证技术可行性", "严禁忽视安全隔离要求"}, + InstructionsFile: "configs/agents/engineer.md", + }, + { + Role: "market", + Name: "市场与竞品分析师", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"竞品同维度对比", "市场规模测算", "机会与风险分析", "竞品横向功能对比与市场容量测算"}, + ForbiddenActions: []string{"严禁编造市场份额与价格", "必须采用统一对比维度"}, + InstructionsFile: "configs/agents/market.md", + }, + { + Role: "finance", + Name: "成本与定价测算专员", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"CDO定价核算", "WBS工时拆解", "成本核算与商务条款", "CDO成本核算与定价工时拆解"}, + ForbiddenActions: []string{"严禁脱离工时模型拍脑袋报价", "严禁擅自承诺超额商业折扣"}, + InstructionsFile: "configs/agents/finance.md", + }, + { + Role: "writer", + Name: "文档撰写专员", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"严格四段式成文", "依据材料出处引用", "模板各章节写满", "撰写正式交付文档正文终稿"}, + ForbiddenActions: []string{"严禁写不做展开或从略", "严禁随意篡改前序事实"}, + InstructionsFile: "configs/agents/writer.md", + }, + { + Role: "leader", + Name: "商务对接负责人", + Source: RoleSourceStandard, + Model: "MiniMax-M3", + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", + Responsibilities: []string{"计划审核", "正文终审", "待确认清单汇总", "对外汇报", "商务合同把关与终审交付"}, + ForbiddenActions: []string{"严禁代用户做商业拍板", "严禁直接交付未审核初稿"}, + InstructionsFile: "configs/agents/leader.md", + }, + } + + for _, s := range standards { + r.roles[s.Role] = s + } +} + +// GetRole 根据 Key 获取角色 +func (r *RoleRegistry) GetRole(key string) (AgentSpec, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + spec, ok := r.roles[key] + return spec, ok +} + +// ListRoles 列出当前注册的所有角色 +func (r *RoleRegistry) ListRoles() []AgentSpec { + r.mu.RLock() + defer r.mu.RUnlock() + res := make([]AgentSpec, 0, len(r.roles)) + for _, spec := range r.roles { + res = append(res, spec) + } + return res +} + +// RegisterRole 显式注册或更新角色 +func (r *RoleRegistry) RegisterRole(spec AgentSpec) { + r.mu.Lock() + defer r.mu.Unlock() + if spec.BashPermission == "" { + spec.BashPermission = "deny" + } + r.roles[spec.Role] = spec +} + +// RecommendRole 根据领域任务或文本描述推荐最贴切的标准角色 +func (r *RoleRegistry) RecommendRole(query string) (AgentSpec, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + q := strings.ToLower(query) + + type scoreItem struct { + spec AgentSpec + score int + } + + var best scoreItem + + // 规则特征优先字典 + ruleWeights := map[string][]string{ + "regulatory": {"法规", "注册", "审评", "分类界定", "临床评价", "二类", "三类", "药监局", "nmpa", "ce", "fda"}, + "searcher": {"检索", "文献", "标准", "标准号", "查证", "公开资料", "溯源"}, + "market": {"竞品", "市场", "容量", "份额", "对标", "优劣势", "商业模式"}, + "engineer": {"工程", "硬件", "软件", "结构", "架构", "技术路线", "嵌入式", "算法", "系统", "模块", "选型"}, + "organizer": {"整理", "事实", "缺口", "字段", "回填", "抽取", "材料梳理", "基线"}, + "finance": {"成本", "工时", "核算", "报价", "定价", "cdo", "价格", "利润", "商务条款"}, + "writer": {"撰写", "成文", "起草", "初稿", "正文", "章节", "四段式"}, + "leader": {"终审", "审核", "把关", "总览", "交付", "待确认清单", "对外汇报", "决策"}, + } + + for roleKey, keywords := range ruleWeights { + spec, exists := r.roles[roleKey] + if !exists { + continue + } + curScore := 0 + for _, kw := range keywords { + if strings.Contains(q, kw) { + curScore += 10 + } + } + // 职责辅助匹配 + for _, resp := range spec.Responsibilities { + if strings.Contains(q, strings.ToLower(resp)) { + curScore += 5 + } + } + + if curScore > best.score { + best = scoreItem{spec: spec, score: curScore} + } + } + + if best.score > 0 { + return best.spec, true + } + + // 兜底退回 engineer 或 organizer + if eng, ok := r.roles["engineer"]; ok { + return eng, true + } + return AgentSpec{}, false +} + +// SpawnAdHocRole 自治派生特设临时专员角色 +func (r *RoleRegistry) SpawnAdHocRole(req AdHocRoleRequest) (AgentSpec, error) { + if req.RoleKey == "" { + return AgentSpec{}, fmt.Errorf("role_key cannot be empty") + } + + baseRoleKey := req.BaseRole + if baseRoleKey == "" { + baseRoleKey = "engineer" + } + + r.mu.Lock() + defer r.mu.Unlock() + + baseSpec, exists := r.roles[baseRoleKey] + if !exists { + baseSpec = r.roles["engineer"] + } + + model := baseSpec.Model + if model == "" { + model = "MiniMax-M3" + } + + name := req.Name + if name == "" { + name = fmt.Sprintf("%s专项专员", req.RoleKey) + } + + responsibilities := append([]string{}, req.Responsibilities...) + if len(responsibilities) == 0 && req.DomainTopic != "" { + responsibilities = append(responsibilities, fmt.Sprintf("专责推演与论证【%s】", req.DomainTopic)) + } + + forbidden := append([]string{}, req.ForbiddenActions...) + forbidden = append(forbidden, "严禁违规越权执行未经规划的破坏性命令", "严禁直接代用户做终审拍板") + + adHocSpec := AgentSpec{ + Role: req.RoleKey, + Name: name, + Source: RoleSourceAdHoc, + BaseRole: baseRoleKey, + Model: model, + Tools: []string{"read", "write", "edit"}, + BashPermission: "deny", // 强制只读沙箱安全 + Responsibilities: responsibilities, + ForbiddenActions: forbidden, + InstructionsFile: baseSpec.InstructionsFile, + } + + r.roles[adHocSpec.Role] = adHocSpec + return adHocSpec, nil +} diff --git a/internal/squad/roles_test.go b/internal/squad/roles_test.go new file mode 100644 index 00000000..a1a8b5fe --- /dev/null +++ b/internal/squad/roles_test.go @@ -0,0 +1,101 @@ +package squad + +import ( + "testing" +) + +func TestRoleRegistryDefaultRoles(t *testing.T) { + reg := NewRoleRegistry() + roles := reg.ListRoles() + + if len(roles) < 8 { + t.Fatalf("expected at least 8 standard roles, got %d", len(roles)) + } + + expectedRoles := []string{ + "organizer", + "searcher", + "regulatory", + "engineer", + "market", + "finance", + "writer", + "leader", + } + + for _, r := range expectedRoles { + spec, found := reg.GetRole(r) + if !found { + t.Errorf("expected role %s to be registered", r) + } + if spec.Source != RoleSourceStandard { + t.Errorf("role %s source expected standard, got %s", r, spec.Source) + } + if spec.BashPermission != "deny" { + t.Errorf("role %s bash_permission must be deny, got %s", r, spec.BashPermission) + } + } +} + +func TestRoleRegistryRecommend(t *testing.T) { + reg := NewRoleRegistry() + + tests := []struct { + query string + expected string + }{ + {"医疗器械分类界定与注册审评路径", "regulatory"}, + {"公开文献、标准号溯源检索", "searcher"}, + {"竞品横向功能对比与市场容量测算", "market"}, + {"系统硬件模块与嵌入式结构方案设计", "engineer"}, + {"材料提取、缺口与字段回填", "organizer"}, + {"CDO成本核算与定价工时拆解", "finance"}, + {"撰写正式交付文档正文终稿", "writer"}, + {"商务合同把关与终审交付", "leader"}, + } + + for _, tc := range tests { + rec, found := reg.RecommendRole(tc.query) + if !found { + t.Errorf("recommendation failed for query: %s", tc.query) + } + if rec.Role != tc.expected { + t.Errorf("for query '%s', expected %s, got %s", tc.query, tc.expected, rec.Role) + } + } +} + +func TestRoleRegistrySpawnAdHocRole(t *testing.T) { + reg := NewRoleRegistry() + + adHoc, err := reg.SpawnAdHocRole(AdHocRoleRequest{ + RoleKey: "biocompatibility_expert", + Name: "生物相容性评估专员", + BaseRole: "engineer", + DomainTopic: "动物源性材料与生物学评价GB/T 16886", + Responsibilities: []string{"生物相容性接触类型识别", "植入与血液相容性风险评估"}, + ForbiddenActions: []string{"严禁代替国家局检验中心出具最终报告"}, + }) + if err != nil { + t.Fatalf("spawn ad hoc role failed: %v", err) + } + + if adHoc.Source != RoleSourceAdHoc { + t.Errorf("expected source ad_hoc, got %s", adHoc.Source) + } + if adHoc.BaseRole != "engineer" { + t.Errorf("expected base_role engineer, got %s", adHoc.BaseRole) + } + if adHoc.BashPermission != "deny" { + t.Errorf("ad-hoc role must deny bash permission, got %s", adHoc.BashPermission) + } + + // 确认派生角色被登记入注册中心 + fetched, found := reg.GetRole("biocompatibility_expert") + if !found { + t.Fatalf("expected spawned role to be retrievable from registry") + } + if fetched.Name != "生物相容性评估专员" { + t.Errorf("expected name 生物相容性评估专员, got %s", fetched.Name) + } +}