diff --git a/agent/robot/api/robot.go b/agent/robot/api/robot.go index 2f1f1ea8..d9180885 100644 --- a/agent/robot/api/robot.go +++ b/agent/robot/api/robot.go @@ -166,7 +166,7 @@ func loadRobotFromDB(memberID string) (*types.Robot, error) { Select: []interface{}{ "id", "member_id", "team_id", "display_name", "bio", "system_prompt", "robot_status", "autonomous_mode", - "robot_config", "robot_email", + "robot_config", "robot_email", "agents", "mcp_servers", }, Wheres: []model.QueryWhere{ {Column: "member_id", Value: memberID}, @@ -228,7 +228,7 @@ func listRobotsFromDB(query *ListQuery) (*ListResult, error) { Select: []interface{}{ "id", "member_id", "team_id", "display_name", "bio", "system_prompt", "robot_status", "autonomous_mode", - "robot_config", "robot_email", + "robot_config", "robot_email", "agents", "mcp_servers", }, Wheres: wheres, Orders: orders, diff --git a/agent/robot/cache/load.go b/agent/robot/cache/load.go index 25189ec7..d9a455c3 100644 --- a/agent/robot/cache/load.go +++ b/agent/robot/cache/load.go @@ -24,6 +24,8 @@ var memberFields = []interface{}{ "autonomous_mode", "robot_config", "robot_email", + "agents", + "mcp_servers", } // SetMemberModel sets the member model name diff --git a/agent/robot/executor/standard/input.go b/agent/robot/executor/standard/input.go index e1ac8242..3317a5a0 100644 --- a/agent/robot/executor/standard/input.go +++ b/agent/robot/executor/standard/input.go @@ -43,40 +43,47 @@ func (f *InputFormatter) FormatClockContext(clock *robottypes.ClockContext, robo sb.WriteString(fmt.Sprintf("- **Day**: %s\n", clock.DayOfWeek)) sb.WriteString(fmt.Sprintf("- **Date**: %d/%d/%d\n", clock.Year, clock.Month, clock.DayOfMonth)) sb.WriteString(fmt.Sprintf("- **Week**: %d of year\n", clock.WeekOfYear)) + sb.WriteString(fmt.Sprintf("- **Hour**: %d\n", clock.Hour)) sb.WriteString(fmt.Sprintf("- **Timezone**: %s\n", clock.TZ)) - // Time markers + // Time markers - show all markers with check/cross for context awareness sb.WriteString("\n### Time Markers\n") - if clock.IsWeekend { - sb.WriteString("- ✓ Weekend\n") - } - if clock.IsMonthStart { - sb.WriteString("- ✓ Month Start (1st-3rd)\n") - } - if clock.IsMonthEnd { - sb.WriteString("- ✓ Month End (last 3 days)\n") - } - if clock.IsQuarterEnd { - sb.WriteString("- ✓ Quarter End\n") - } - if clock.IsYearEnd { - sb.WriteString("- ✓ Year End\n") - } + sb.WriteString(fmt.Sprintf("- %s Weekend\n", boolMark(clock.IsWeekend))) + sb.WriteString(fmt.Sprintf("- %s Month Start (1st-3rd)\n", boolMark(clock.IsMonthStart))) + sb.WriteString(fmt.Sprintf("- %s Month End (last 3 days)\n", boolMark(clock.IsMonthEnd))) + sb.WriteString(fmt.Sprintf("- %s Quarter End\n", boolMark(clock.IsQuarterEnd))) + sb.WriteString(fmt.Sprintf("- %s Year End\n", boolMark(clock.IsYearEnd))) - // Robot identity section (if available) - if robot != nil && robot.Config != nil && robot.Config.Identity != nil { - sb.WriteString("\n## Robot Identity\n\n") - sb.WriteString(fmt.Sprintf("- **Role**: %s\n", robot.Config.Identity.Role)) - if len(robot.Config.Identity.Duties) > 0 { - sb.WriteString("- **Duties**:\n") - for _, duty := range robot.Config.Identity.Duties { - sb.WriteString(fmt.Sprintf(" - %s\n", duty)) + // Robot identity section + // Priority: Config.Identity > Robot fields (DisplayName, Bio, SystemPrompt) + if robot != nil { + if robot.Config != nil && robot.Config.Identity != nil { + // Use structured identity from config + sb.WriteString("\n## Robot Identity\n\n") + sb.WriteString(fmt.Sprintf("- **Role**: %s\n", robot.Config.Identity.Role)) + if len(robot.Config.Identity.Duties) > 0 { + sb.WriteString("- **Duties**:\n") + for _, duty := range robot.Config.Identity.Duties { + sb.WriteString(fmt.Sprintf(" - %s\n", duty)) + } } - } - if len(robot.Config.Identity.Rules) > 0 { - sb.WriteString("- **Rules**:\n") - for _, rule := range robot.Config.Identity.Rules { - sb.WriteString(fmt.Sprintf(" - %s\n", rule)) + if len(robot.Config.Identity.Rules) > 0 { + sb.WriteString("- **Rules**:\n") + for _, rule := range robot.Config.Identity.Rules { + sb.WriteString(fmt.Sprintf(" - %s\n", rule)) + } + } + } else if robot.DisplayName != "" || robot.Bio != "" || robot.SystemPrompt != "" { + // Fallback: build identity from Robot fields (from __yao.member table) + sb.WriteString("\n## Robot Identity\n\n") + if robot.DisplayName != "" { + sb.WriteString(fmt.Sprintf("- **Role**: %s\n", robot.DisplayName)) + } + if robot.Bio != "" { + sb.WriteString(fmt.Sprintf("- **Description**: %s\n", robot.Bio)) + } + if robot.SystemPrompt != "" { + sb.WriteString(fmt.Sprintf("- **Instructions**:\n%s\n", robot.SystemPrompt)) } } } @@ -84,6 +91,14 @@ func (f *InputFormatter) FormatClockContext(clock *robottypes.ClockContext, robo return sb.String() } +// boolMark returns ✓ for true and ✗ for false +func boolMark(v bool) string { + if v { + return "✓" + } + return "✗" +} + // FormatRobotIdentity formats robot identity as user message content // Used to provide context about the robot's role and duties func (f *InputFormatter) FormatRobotIdentity(robot *robottypes.Robot) string { diff --git a/agent/robot/executor/standard/input_test.go b/agent/robot/executor/standard/input_test.go index 57c4a1b6..1e1a6963 100644 --- a/agent/robot/executor/standard/input_test.go +++ b/agent/robot/executor/standard/input_test.go @@ -28,9 +28,25 @@ func TestInputFormatterFormatClockContext(t *testing.T) { assert.Contains(t, result, "2024-01-15 09:30:00") assert.Contains(t, result, "Monday") assert.Contains(t, result, "UTC") + assert.Contains(t, result, "**Hour**: 9") assert.Contains(t, result, "### Time Markers") }) + t.Run("shows all time markers with check/cross", func(t *testing.T) { + // Regular weekday, not month start/end + now := time.Date(2024, 1, 15, 14, 0, 0, 0, time.UTC) + clock := types.NewClockContext(now, "UTC") + + result := formatter.FormatClockContext(clock, nil) + + // Should show all markers, even when false + assert.Contains(t, result, "✗ Weekend") + assert.Contains(t, result, "✗ Month Start") + assert.Contains(t, result, "✗ Month End") + assert.Contains(t, result, "✗ Quarter End") + assert.Contains(t, result, "✗ Year End") + }) + t.Run("includes robot identity when provided", func(t *testing.T) { now := time.Now() clock := types.NewClockContext(now, "UTC") @@ -58,6 +74,26 @@ func TestInputFormatterFormatClockContext(t *testing.T) { assert.Empty(t, result) }) + t.Run("uses DisplayName/Bio/SystemPrompt when Identity is nil", func(t *testing.T) { + now := time.Now() + clock := types.NewClockContext(now, "UTC") + robot := &types.Robot{ + MemberID: "test-robot", + DisplayName: "SEO Specialist", + Bio: "Focuses on content optimization", + SystemPrompt: "You are an SEO assistant.\n\n## Core Duties\n- Analyze keywords", + Config: &types.Config{}, // Identity is nil + } + + result := formatter.FormatClockContext(clock, robot) + + assert.Contains(t, result, "## Robot Identity") + assert.Contains(t, result, "SEO Specialist") + assert.Contains(t, result, "Focuses on content optimization") + assert.Contains(t, result, "SEO assistant") + assert.Contains(t, result, "Analyze keywords") + }) + t.Run("marks weekend correctly", func(t *testing.T) { // Saturday saturday := time.Date(2024, 1, 13, 10, 0, 0, 0, time.UTC) diff --git a/agent/robot/store/robot.go b/agent/robot/store/robot.go index 81ec3518..983115b0 100644 --- a/agent/robot/store/robot.go +++ b/agent/robot/store/robot.go @@ -614,9 +614,59 @@ func (r *RobotRecord) ToRobot() (*types.Robot, error) { robot.Config = config } + // Ensure Config exists for merging agents/mcp_servers + if robot.Config == nil { + robot.Config = &types.Config{} + } + if robot.Config.Resources == nil { + robot.Config.Resources = &types.Resources{} + } + + // Merge agents from member table into Config.Resources.Agents + if r.Agents != nil { + agents := parseStringSlice(r.Agents) + if len(agents) > 0 { + robot.Config.Resources.Agents = agents + } + } + + // Merge mcp_servers from member table into Config.Resources.MCP + if r.MCPServers != nil { + mcpServers := parseStringSlice(r.MCPServers) + if len(mcpServers) > 0 { + // Convert string slice to MCPConfig slice (each server ID becomes an MCPConfig) + for _, serverID := range mcpServers { + robot.Config.Resources.MCP = append(robot.Config.Resources.MCP, types.MCPConfig{ + ID: serverID, + // Tools empty means all tools available + }) + } + } + } + return robot, nil } +// parseStringSlice converts interface{} to []string +func parseStringSlice(v interface{}) []string { + if v == nil { + return nil + } + switch val := v.(type) { + case []string: + return val + case []interface{}: + result := make([]string, 0, len(val)) + for _, item := range val { + if s, ok := item.(string); ok { + result = append(result, s) + } + } + return result + } + return nil +} + // FromRobot creates a RobotRecord from types.Robot func FromRobot(robot *types.Robot) *RobotRecord { record := &RobotRecord{ diff --git a/agent/robot/types/robot.go b/agent/robot/types/robot.go index 339472f2..50f7f170 100644 --- a/agent/robot/types/robot.go +++ b/agent/robot/types/robot.go @@ -407,9 +407,57 @@ func NewRobotFromMap(m map[string]interface{}) (*Robot, error) { robot.Config = config } + // Ensure Config exists for merging agents/mcp_servers + if robot.Config == nil { + robot.Config = &Config{} + } + if robot.Config.Resources == nil { + robot.Config.Resources = &Resources{} + } + + // Merge agents from member table into Config.Resources.Agents + if agentsData, ok := m["agents"]; ok && agentsData != nil { + agents := getStringSlice(agentsData) + if len(agents) > 0 { + robot.Config.Resources.Agents = agents + } + } + + // Merge mcp_servers from member table into Config.Resources.MCP + if mcpData, ok := m["mcp_servers"]; ok && mcpData != nil { + mcpServers := getStringSlice(mcpData) + if len(mcpServers) > 0 { + for _, serverID := range mcpServers { + robot.Config.Resources.MCP = append(robot.Config.Resources.MCP, MCPConfig{ + ID: serverID, + }) + } + } + } + return robot, nil } +// getStringSlice converts interface{} to []string +func getStringSlice(v interface{}) []string { + if v == nil { + return nil + } + switch val := v.(type) { + case []string: + return val + case []interface{}: + result := make([]string, 0, len(val)) + for _, item := range val { + if s, ok := item.(string); ok { + result = append(result, s) + } + } + return result + } + return nil +} + // getString safely gets a string value from map func getString(m map[string]interface{}, key string) string { if m == nil {