Files
goclaw/internal/teamworkclassify/context.go
T
nguyenha935 f826738ee6 feat: add team work classification routing (#1379)
Approved by github-maintain bot. Clean team work classification feature with comprehensive tests and i18n.
2026-07-07 01:21:12 +07:00

210 lines
6.9 KiB
Go

package teamworkclassify
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type ProfileStores struct {
Agents store.AgentStore
Teams store.TeamStore
AgentLinks store.AgentLinkStore
}
type BuildInputOptions struct {
Mode Mode
Message string
AgentID uuid.UUID
ToolAllow []string
SkillFilter []string
Embedder Embedder
ExtraSelf []Profile
ExtraCollab []Profile
}
func BuildInputFromStores(ctx context.Context, stores ProfileStores, opts BuildInputOptions) Input {
input := Input{
Mode: opts.Mode,
Message: opts.Message,
Embedder: opts.Embedder,
}
if opts.Mode == "" || opts.Mode == ModeSpawn || opts.AgentID == uuid.Nil {
return input
}
if stores.Agents != nil {
if ag, err := stores.Agents.GetByID(ctx, opts.AgentID); err == nil && ag != nil {
input.CurrentAgent = Profile{
Kind: "agent",
Name: firstNonEmpty(ag.DisplayName, ag.AgentKey),
Text: strings.TrimSpace(strings.Join([]string{
ag.Frontmatter,
ag.AgentDescription,
}, "\n")),
}
}
}
input.SelfTools = append(input.SelfTools, effectiveSelfToolProfiles(opts.ToolAllow, opts.SkillFilter)...)
input.SelfTools = append(input.SelfTools, opts.ExtraSelf...)
if opts.Mode == ModeTeam && stores.Teams != nil {
if team, err := stores.Teams.GetTeamForAgent(ctx, opts.AgentID); err == nil && team != nil {
input.TeamRole = "member"
if team.LeadAgentID == opts.AgentID {
input.TeamRole = "lead"
input.CanAssignTeamTasks = true
}
memberRequestCfg := parseMemberRequestRoutingConfig(team.Settings)
input.MemberRequestsEnabled = memberRequestCfg.Enabled
input.MemberRequestsAutoDispatch = memberRequestCfg.AutoDispatch
input.Team = Profile{
Kind: "team",
Name: team.Name,
Text: strings.TrimSpace(strings.Join([]string{
team.Description,
fmt.Sprintf("lead_agent: %s %s", team.LeadAgentKey, team.LeadDisplayName),
}, "\n")),
}
if members, err := stores.Teams.ListMembers(ctx, team.ID); err == nil {
for _, member := range members {
if member.AgentID == opts.AgentID && !input.CanAssignTeamTasks {
if strings.TrimSpace(member.Role) != "" {
input.TeamRole = member.Role
}
}
input.Members = append(input.Members, Profile{
Kind: "team_member",
Name: firstNonEmpty(member.DisplayName, member.AgentKey),
Text: strings.TrimSpace(strings.Join([]string{
"role: " + member.Role,
"agent_key: " + member.AgentKey,
member.Frontmatter,
}, "\n")),
})
}
}
input.CollaborationTools = append(input.CollaborationTools, teamPermissionProfiles(input)...)
}
}
if stores.AgentLinks != nil {
if links, err := stores.AgentLinks.DelegateTargets(ctx, opts.AgentID); err == nil {
for _, link := range links {
input.Delegates = append(input.Delegates, Profile{
Kind: "delegate",
Name: firstNonEmpty(link.TargetDisplayName, link.TargetAgentKey),
Text: strings.TrimSpace(strings.Join([]string{
"agent_key: " + link.TargetAgentKey,
"link_direction: " + link.Direction,
"link_team: " + link.TeamName,
"target_team: " + link.TargetTeamName,
fmt.Sprintf("target_is_team_lead: %t", link.TargetIsTeamLead),
link.Description,
link.TargetDescription,
}, "\n")),
})
}
if opts.Mode == ModeDelegate && len(links) > 0 {
input.CollaborationTools = append(input.CollaborationTools,
Profile{Kind: "tool", Name: "delegate", Text: "delegate work to linked agents with matching expertise and receive their result"},
)
}
}
}
input.CollaborationTools = append(input.CollaborationTools, opts.ExtraCollab...)
return input
}
type memberRequestRoutingConfig struct {
Enabled bool
AutoDispatch bool
}
func parseMemberRequestRoutingConfig(settings json.RawMessage) memberRequestRoutingConfig {
var cfg memberRequestRoutingConfig
if len(settings) == 0 {
return cfg
}
var raw struct {
MemberRequests *struct {
Enabled *bool `json:"enabled"`
AutoDispatch *bool `json:"auto_dispatch"`
} `json:"member_requests"`
}
if json.Unmarshal(settings, &raw) != nil || raw.MemberRequests == nil {
return cfg
}
if raw.MemberRequests.Enabled != nil {
cfg.Enabled = *raw.MemberRequests.Enabled
}
if raw.MemberRequests.AutoDispatch != nil {
cfg.AutoDispatch = *raw.MemberRequests.AutoDispatch
}
return cfg
}
func teamPermissionProfiles(input Input) []Profile {
role := strings.ToLower(strings.TrimSpace(input.TeamRole))
if role == "" || role == "lead" || input.CanAssignTeamTasks {
return []Profile{
{Kind: "tool", Name: "team_tasks", Text: "lead can search existing tasks, create tasks, assign work to team members, track progress, review and complete team work"},
{Kind: "tool", Name: "ask_user", Text: "ask the user for decisions or missing information during team work"},
{Kind: "capability", Name: "shared team workspace", Text: "coordinate multi-step work through shared files, task board, and team member results"},
}
}
if input.MemberRequestsEnabled && input.MemberRequestsAutoDispatch {
return []Profile{
{Kind: "tool", Name: "team_tasks", Text: `member cannot assign general tasks; member may create task_type="request" to ask another teammate for help; requests auto-dispatch to the assignee`},
{Kind: "capability", Name: "member request workflow", Text: "request help from teammates without lead-style assignment authority"},
}
}
if input.MemberRequestsEnabled {
return []Profile{
{Kind: "tool", Name: "team_tasks", Text: "member request tasks are enabled but auto-dispatch is disabled; requests stay pending for leader review and should not be used as immediate routed workflow"},
{Kind: "capability", Name: "member limited team access", Text: "cannot coordinate new team work without the lead"},
}
}
return []Profile{
{Kind: "tool", Name: "team_tasks", Text: "member cannot create or assign general tasks; member request tasks are disabled; use comments/progress/current-task actions only"},
{Kind: "capability", Name: "member limited team access", Text: "cannot coordinate new team work without the lead"},
}
}
func effectiveSelfToolProfiles(toolAllow, skillFilter []string) []Profile {
var profiles []Profile
if len(toolAllow) > 0 {
profiles = append(profiles, Profile{
Kind: "tool_allow",
Name: "channel allowed tools",
Text: "effective channel/group allowed tools: " + strings.Join(toolAllow, ", "),
})
} else {
profiles = append(profiles, Profile{
Kind: "tool_allow",
Name: "default tools",
Text: "current agent may use its configured tools for direct small tasks",
})
}
if len(skillFilter) > 0 {
profiles = append(profiles, Profile{
Kind: "skill_filter",
Name: "topic skills",
Text: "effective topic skill filter: " + strings.Join(skillFilter, ", "),
})
}
return profiles
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}