Enhance Robot Configuration with Agents and MCP Servers Integration

- Updated database queries to include 'agents' and 'mcp_servers' fields in robot data retrieval.
- Enhanced the robot configuration structure to merge agents and MCP servers from the member table into the robot's resources.
- Improved the input formatter to display time markers with check/cross indicators for better context awareness.
- Added tests to validate the new functionality and ensure proper formatting of robot identity when identity is nil.
This commit is contained in:
Max 2026-01-27 15:59:14 +08:00
parent 6e68b42128
commit 0f8287a51b
6 changed files with 182 additions and 31 deletions

View file

@ -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,

View file

@ -24,6 +24,8 @@ var memberFields = []interface{}{
"autonomous_mode",
"robot_config",
"robot_email",
"agents",
"mcp_servers",
}
// SetMemberModel sets the member model name

View file

@ -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 {

View file

@ -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)

View file

@ -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{

View file

@ -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 {