Merge pull request #1434 from trheyi/main
Enhance Robot Delivery Logic and Context Management
This commit is contained in:
commit
25c532d17e
10 changed files with 200 additions and 30 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 {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package standard
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -11,6 +14,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/gou/text"
|
||||
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||||
"github.com/yaoapp/yao/attachment"
|
||||
"github.com/yaoapp/yao/messenger"
|
||||
|
|
@ -116,11 +120,13 @@ func (dc *DeliveryCenter) sendEmail(
|
|||
return result
|
||||
}
|
||||
|
||||
// Build email message
|
||||
// Build email message with HTML content
|
||||
htmlBody, plainBody := buildEmailBody(target.Template, content)
|
||||
msg := &messengerTypes.Message{
|
||||
To: target.To,
|
||||
Subject: buildEmailSubject(target.Subject, target.Template, content, deliveryCtx),
|
||||
Body: buildEmailBody(target.Template, content),
|
||||
Body: plainBody, // Plain text fallback
|
||||
HTML: htmlBody, // HTML content for rich email display
|
||||
Type: messengerTypes.MessageTypeEmail,
|
||||
}
|
||||
|
||||
|
|
@ -217,10 +223,11 @@ func (dc *DeliveryCenter) postWebhook(
|
|||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// Add secret if configured (for signature verification)
|
||||
// Add HMAC signature if secret is configured
|
||||
if target.Secret != "" {
|
||||
// TODO: Implement HMAC signature
|
||||
req.Header.Set("X-Webhook-Secret", target.Secret)
|
||||
signature := computeHMACSignature(payloadBytes, target.Secret)
|
||||
req.Header.Set("X-Yao-Signature", signature)
|
||||
req.Header.Set("X-Yao-Signature-Algorithm", "HMAC-SHA256")
|
||||
}
|
||||
|
||||
// Send request
|
||||
|
|
@ -295,11 +302,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
|
||||
|
|
@ -322,13 +348,24 @@ func buildEmailSubject(subject, template string, content *robottypes.DeliveryCon
|
|||
}
|
||||
|
||||
// buildEmailBody builds the email body content
|
||||
func buildEmailBody(template string, content *robottypes.DeliveryContent) string {
|
||||
// buildEmailBody returns HTML and plain text versions of the email body
|
||||
// Returns: (htmlBody, plainBody)
|
||||
func buildEmailBody(template string, content *robottypes.DeliveryContent) (string, string) {
|
||||
// TODO: Implement template rendering
|
||||
// For now, just use the body directly
|
||||
if content.Body != "" {
|
||||
return content.Body
|
||||
// Get markdown content (used as plain text fallback)
|
||||
markdown := content.Body
|
||||
if markdown == "" {
|
||||
markdown = content.Summary
|
||||
}
|
||||
return content.Summary
|
||||
|
||||
// Convert Markdown to HTML for rich email display
|
||||
html, err := text.MarkdownToHTML(markdown)
|
||||
if err != nil {
|
||||
// Fallback: use markdown as both HTML and plain text
|
||||
return markdown, markdown
|
||||
}
|
||||
|
||||
return html, markdown
|
||||
}
|
||||
|
||||
// convertAttachments converts DeliveryAttachment to messenger Attachment format
|
||||
|
|
@ -376,3 +413,26 @@ func convertAttachments(ctx context.Context, attachments []robottypes.DeliveryAt
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Webhook Signature
|
||||
// ============================================================================
|
||||
|
||||
// computeHMACSignature computes HMAC-SHA256 signature for webhook payload
|
||||
// Returns hex-encoded signature string
|
||||
func computeHMACSignature(payload []byte, secret string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(payload)
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// VerifyHMACSignature verifies the HMAC-SHA256 signature of a webhook payload
|
||||
// Headers:
|
||||
// - X-Yao-Signature: hex-encoded HMAC-SHA256 signature
|
||||
// - X-Yao-Signature-Algorithm: "HMAC-SHA256"
|
||||
//
|
||||
// Returns true if the signature is valid
|
||||
func VerifyHMACSignature(payload []byte, secret, signature string) bool {
|
||||
expected := computeHMACSignature(payload, secret)
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import "sync"
|
|||
var (
|
||||
// defaultEmailChannel - default messenger channel name for sending emails
|
||||
// Can be configured via SetDefaultEmailChannel()
|
||||
// Default: "email" (maps to messengers/channels.yao configuration)
|
||||
defaultEmailChannel = "email"
|
||||
// Default: "default" (maps to messengers/channels.yao configuration)
|
||||
defaultEmailChannel = "default"
|
||||
|
||||
// configMu protects global configuration
|
||||
configMu sync.RWMutex
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
go.mod
5
go.mod
|
|
@ -47,6 +47,8 @@ require (
|
|||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 // indirect
|
||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
|
|
@ -124,7 +126,7 @@ require (
|
|||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||
|
|
@ -147,6 +149,7 @@ require (
|
|||
github.com/xuri/nfp v0.0.1 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
|
|
|
|||
12
go.sum
12
go.sum
|
|
@ -1,5 +1,9 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
|
||||
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 h1:mklaPbT4f/EiDr1Q+zPrEt9lgKAkVrIBtWf33d9GpVA=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0/go.mod h1:D56Cl9r8M5i3UwAchE+LlLc5hPN3kJtdZNVJn06lSHU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
||||
|
|
@ -272,8 +276,10 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
|||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
|
||||
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
|
|
@ -344,6 +350,8 @@ github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT0
|
|||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
|
|
|
|||
|
|
@ -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