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.
This commit is contained in:
parent
235084dbae
commit
ecb95f4cc8
7 changed files with 133 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
1
agent/robot/cache/load.go
vendored
1
agent/robot/cache/load.go
vendored
|
|
@ -26,6 +26,7 @@ var memberFields = []interface{}{
|
|||
"robot_email",
|
||||
"agents",
|
||||
"mcp_servers",
|
||||
"manager_id",
|
||||
}
|
||||
|
||||
// SetMemberModel sets the member model name
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue