mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-04 06:13:15 +00:00
Approved by github-maintain bot. Clean team work classification feature with comprehensive tests and i18n.
210 lines
6.9 KiB
Go
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 ""
|
|
}
|