From ecb95f4cc875d4799251d3d835a613253672ebce Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 28 Jan 2026 14:44:42 +0800 Subject: [PATCH] Enhance Robot Delivery Logic and Context Management - Introduced manager ID and email fields in the Robot struct to facilitate mandatory delivery to managers. - Updated delivery routing logic to ensure manager email is always included if manager ID is set, improving delivery reliability. - Added JSON serialization handling in the Delivery Center to ensure compatibility with various data types. - Enhanced context ID generation by replacing time-based IDs with NanoID for improved uniqueness and consistency. --- agent/context/context.go | 7 +- agent/robot/api/robot.go | 1 + agent/robot/cache/load.go | 1 + agent/robot/executor/standard/delivery.go | 107 ++++++++++++++++-- .../executor/standard/delivery_center.go | 21 +++- agent/robot/types/robot.go | 6 + trace/manager.go | 5 +- 7 files changed, 133 insertions(+), 15 deletions(-) diff --git a/agent/context/context.go b/agent/context/context.go index ceac1610..5933318f 100644 --- a/agent/context/context.go +++ b/agent/context/context.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sync" - "time" "github.com/yaoapp/yao/agent/memory" "github.com/yaoapp/yao/agent/output/message" @@ -411,12 +410,12 @@ func SendInterrupt(contextID string, signal *InterruptSignal) error { // generateContextID generates a unique context ID func generateContextID() string { - return fmt.Sprintf("ctx-%d", time.Now().UnixNano()) + return message.GenerateNanoID() } -// RequestID returns the request ID for the context +// RequestID returns a unique request ID using NanoID func (ctx *Context) RequestID() string { - return fmt.Sprintf("%s", ctx.ID) + return message.GenerateNanoID() } // TraceID returns the trace ID for the context diff --git a/agent/robot/api/robot.go b/agent/robot/api/robot.go index d9180885..15432a9c 100644 --- a/agent/robot/api/robot.go +++ b/agent/robot/api/robot.go @@ -167,6 +167,7 @@ func loadRobotFromDB(memberID string) (*types.Robot, error) { "id", "member_id", "team_id", "display_name", "bio", "system_prompt", "robot_status", "autonomous_mode", "robot_config", "robot_email", "agents", "mcp_servers", + "manager_id", }, Wheres: []model.QueryWhere{ {Column: "member_id", Value: memberID}, diff --git a/agent/robot/cache/load.go b/agent/robot/cache/load.go index d9a455c3..600c79b0 100644 --- a/agent/robot/cache/load.go +++ b/agent/robot/cache/load.go @@ -26,6 +26,7 @@ var memberFields = []interface{}{ "robot_email", "agents", "mcp_servers", + "manager_id", } // SetMemberModel sets the member model name diff --git a/agent/robot/executor/standard/delivery.go b/agent/robot/executor/standard/delivery.go index 4d312c69..f80a9ac0 100644 --- a/agent/robot/executor/standard/delivery.go +++ b/agent/robot/executor/standard/delivery.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/yaoapp/gou/model" robottypes "github.com/yaoapp/yao/agent/robot/types" ) @@ -94,20 +95,18 @@ func (e *Executor) RunDelivery(ctx *robottypes.Context, exec *robottypes.Executi // routeToDeliveryCenter sends content to the Delivery Center for actual delivery // The Delivery Center decides which channels to use based on robot/user preferences +// +// Delivery logic: +// 1. Manager email: ALWAYS send to manager if manager_id is set (mandatory) +// 2. Additional targets: Append configured email/webhook/process targets func (e *Executor) routeToDeliveryCenter(ctx *robottypes.Context, exec *robottypes.Execution, robot *robottypes.Robot) error { if exec.Delivery == nil || exec.Delivery.Content == nil { return fmt.Errorf("no delivery content to route") } - // Get delivery preferences from robot config - var prefs *robottypes.DeliveryPreferences - if robot.Config != nil { - prefs = robot.Config.Delivery - } - - // If no preferences configured, skip delivery (content is still saved in exec.Delivery) + // Build final delivery preferences by merging manager email + configured targets + prefs := buildDeliveryPreferences(robot) if prefs == nil || !hasActiveChannels(prefs) { - // No channels configured - mark as success but with no results exec.Delivery.Success = true return nil } @@ -283,6 +282,98 @@ func hasActiveChannels(prefs *robottypes.DeliveryPreferences) bool { return false } +// buildDeliveryPreferences builds the final delivery preferences by: +// 1. Always including manager email if manager_id is set (mandatory) +// 2. Appending all configured email/webhook/process targets +func buildDeliveryPreferences(robot *robottypes.Robot) *robottypes.DeliveryPreferences { + if robot == nil { + return nil + } + + prefs := &robottypes.DeliveryPreferences{} + + // Step 1: Get manager email (mandatory if manager_id is set) + managerEmail := robot.ManagerEmail + if managerEmail == "" && robot.ManagerID != "" { + managerEmail = getManagerEmail(robot.ManagerID) + if managerEmail != "" { + robot.ManagerEmail = managerEmail // Cache for future use + } + } + + // Step 2: Build email targets (manager first, then configured targets) + var emailTargets []robottypes.EmailTarget + + // Add manager email as first target (mandatory) + if managerEmail != "" { + emailTargets = append(emailTargets, robottypes.EmailTarget{ + To: []string{managerEmail}, + }) + } + + // Append configured email targets + if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Email != nil { + for _, target := range robot.Config.Delivery.Email.Targets { + if len(target.To) > 0 { + emailTargets = append(emailTargets, target) + } + } + } + + // Set email preference if we have any targets + if len(emailTargets) > 0 { + prefs.Email = &robottypes.EmailPreference{ + Enabled: true, + Targets: emailTargets, + } + } + + // Step 3: Copy webhook preferences from config (if enabled) + if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Webhook != nil { + if robot.Config.Delivery.Webhook.Enabled && len(robot.Config.Delivery.Webhook.Targets) > 0 { + prefs.Webhook = robot.Config.Delivery.Webhook + } + } + + // Step 4: Copy process preferences from config (if enabled) + if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Process != nil { + if robot.Config.Delivery.Process.Enabled && len(robot.Config.Delivery.Process.Targets) > 0 { + prefs.Process = robot.Config.Delivery.Process + } + } + + return prefs +} + +// getManagerEmail retrieves the manager's email from __yao.member table by member_id +// manager_id in Robot refers to a member_id in __yao.member table +func getManagerEmail(managerID string) string { + if managerID == "" { + return "" + } + + m := model.Select("__yao.member") + if m == nil { + return "" + } + + records, err := m.Get(model.QueryParam{ + Select: []interface{}{"email"}, + Wheres: []model.QueryWhere{ + {Column: "member_id", Value: managerID}, + }, + Limit: 1, + }) + if err != nil || len(records) == 0 { + return "" + } + + if email, ok := records[0]["email"].(string); ok { + return email + } + return "" +} + // FormatDeliveryInput formats the full execution context for the Delivery Agent func (f *InputFormatter) FormatDeliveryInput(exec *robottypes.Execution, robot *robottypes.Robot) string { if exec == nil { diff --git a/agent/robot/executor/standard/delivery_center.go b/agent/robot/executor/standard/delivery_center.go index 0009d7db..f8562d14 100644 --- a/agent/robot/executor/standard/delivery_center.go +++ b/agent/robot/executor/standard/delivery_center.go @@ -295,11 +295,30 @@ func (dc *DeliveryCenter) callProcess( } result.Success = true - result.Details = proc.Value + // Convert proc.Value to JSON-serializable format to avoid func type issues + result.Details = toJSONSerializable(proc.Value) return result } +// toJSONSerializable ensures the value can be JSON serialized +// Returns the original value if serializable, or a string fallback if not +func toJSONSerializable(v interface{}) interface{} { + if v == nil { + return nil + } + + // Try to marshal to check if it's JSON serializable + _, err := json.Marshal(v) + if err != nil { + // If it can't be serialized (e.g., contains func), return a string representation + return fmt.Sprintf("%v", v) + } + + // Return original value if it's serializable + return v +} + // buildEmailSubject builds the email subject line func buildEmailSubject(subject, template string, content *robottypes.DeliveryContent, ctx *robottypes.DeliveryContext) string { // Use explicit subject if provided diff --git a/agent/robot/types/robot.go b/agent/robot/types/robot.go index 768a336a..ffad6da1 100644 --- a/agent/robot/types/robot.go +++ b/agent/robot/types/robot.go @@ -23,6 +23,10 @@ type Robot struct { AutonomousMode bool `json:"autonomous_mode"` RobotEmail string `json:"robot_email"` // Robot's email address for sending emails + // Manager info (from __yao.member) + ManagerID string `json:"manager_id"` // Direct manager user_id (who manages this robot) + ManagerEmail string `json:"manager_email"` // Manager's email address (for default delivery) + // Parsed config (from robot_config JSON field) Config *Config `json:"-"` @@ -393,6 +397,8 @@ func NewRobotFromMap(m map[string]interface{}) (*Robot, error) { SystemPrompt: getString(m, "system_prompt"), AutonomousMode: getBool(m, "autonomous_mode"), RobotEmail: getString(m, "robot_email"), + ManagerID: getString(m, "manager_id"), + ManagerEmail: getString(m, "manager_email"), } // Parse robot_status diff --git a/trace/manager.go b/trace/manager.go index ee249903..01e2d69a 100644 --- a/trace/manager.go +++ b/trace/manager.go @@ -93,9 +93,10 @@ func (m *manager) addUpdateAndBroadcast(update *types.TraceUpdate) { // Persist to driver (synchronous - no race) if err := m.driver.SaveUpdate(context.Background(), m.traceID, update); err != nil { log.Trace("[MANAGER] addUpdateAndBroadcast: failed to save update type=%s for trace %s: %v", update.Type, m.traceID, err) - } else { - log.Trace("[MANAGER] addUpdateAndBroadcast: successfully saved update type=%s for trace %s", update.Type, m.traceID) } + // else { + // log.Trace("[MANAGER] addUpdateAndBroadcast: successfully saved update type=%s for trace %s", update.Type, m.traceID) + // } // Add to in-memory history m.stateAddUpdate(update)