diff --git a/agent/robot/DESIGN.md b/agent/robot/DESIGN.md index 571b9129..12dd5b72 100644 --- a/agent/robot/DESIGN.md +++ b/agent/robot/DESIGN.md @@ -262,7 +262,7 @@ Human/Event: P1 → P2 → P3 → P4 → P5 | P1 | Goal Gen | Report + history | Goals | Always | | P2 | Task Plan | Goals + tools | Tasks | Always | | P3 | Run + Valid | Tasks + Experts | TaskResults | Always | -| P4 | Delivery | All results | Email/Webhook | Always | +| P4 | Delivery | All results | Email/Webhook/Process | Always | | P5 | Learning | Summary | KB entries | Always | ### 4.2 P0: Inspiration (Clock only) @@ -514,7 +514,7 @@ P4 generates delivery content and pushes to Delivery Center. **Agent only genera │ Role: │ │ 1. Read Robot/User delivery preferences │ │ 2. Decide which channels to use │ -│ 3. Execute delivery (email, webhook) │ +│ 3. Execute delivery (email, webhook, process) │ │ 4. Future: auto-notify based on user subscriptions │ │ │ │ (Current: internal, future: yao/delivery) │ @@ -571,11 +571,12 @@ Attachments use the standard `yao/attachment` wrapper format: **Delivery Channels (Delivery Center decides):** -| Channel | Description | When | -|---------|-------------|------| -| `email` | Send via yao/messenger | If configured in preferences | -| `webhook` | POST to URL (Slack, 飞书, etc.) | If configured, every execution | -| `notify` | In-app push notification | Based on user subscriptions (future) | +| Channel | Description | Multiple Targets | +|---------|-------------|------------------| +| `email` | Send via yao/messenger | ✅ Multiple recipients/emails | +| `webhook` | POST to external URL | ✅ Multiple URLs | +| `process` | Yao Process call | ✅ Multiple processes | +| `notify` | In-app notification | Future (auto by subscriptions) | **Delivery Agent:** @@ -625,12 +626,13 @@ type DeliveryResult struct { Error string `json:"error,omitempty"` } -// ChannelResult - result for a single channel +// ChannelResult - result for a single delivery target type ChannelResult struct { - Type DeliveryType `json:"type"` + Type DeliveryType `json:"type"` // email | webhook | process | notify + Target string `json:"target,omitempty"` // Target identifier Success bool `json:"success"` Recipients []string `json:"recipients,omitempty"` // For email - Details interface{} `json:"details,omitempty"` + Details interface{} `json:"details,omitempty"` // Channel-specific response Error string `json:"error,omitempty"` SentAt *time.Time `json:"sent_at,omitempty"` } @@ -638,22 +640,45 @@ type ChannelResult struct { **Config (Delivery Preferences):** -Robot config defines delivery **preferences** (Delivery Center reads and executes): +Robot config defines delivery **preferences** (Delivery Center reads and executes). +Each channel supports **multiple targets**: ```yaml delivery: preferences: email: enabled: true - to: ["manager@company.com"] - cc: ["team@company.com"] + targets: # Multiple email targets + - to: ["manager@company.com"] + cc: ["team@company.com"] + - to: ["ceo@company.com"] + subject_template: "Executive Summary" + webhook: enabled: true - url: "https://slack.com/webhook/reports" - # Every execution pushes to webhook automatically + targets: # Multiple webhook URLs + - url: "https://slack.com/webhook/sales" + - url: "https://feishu.cn/webhook/reports" + headers: {"X-Custom": "value"} + + process: + enabled: true + targets: # Multiple Yao Process calls + - name: "orders.UpdateStatus" + args: ["completed"] + - name: "audit.LogDelivery" + # Note: notify handled by Delivery Center based on user subscriptions (future) ``` +**Use Cases:** + +| Scenario | Channels | Description | +|----------|----------|-------------| +| Event callback | `process` | DB change → Robot → Update data via Process | +| Multi-channel notify | `email` + `webhook` | Send to multiple emails and Slack/飞书 | +| Data pipeline | `process` | Robot result → Save to DB → Update dashboard | + ### 4.7 P5: Learn Save to KB: @@ -680,7 +705,7 @@ type Config struct { DB *DB `json:"db,omitempty"` // shared DB (same as assistant) Learn *Learn `json:"learn,omitempty"` // learning for private KB Resources *Resources `json:"resources"` - Delivery *Delivery `json:"delivery"` + Delivery *DeliveryPreferences `json:"delivery,omitempty"` Events []Event `json:"events,omitempty"` Executor *Executor `json:"executor,omitempty"` // executor mode settings } @@ -721,7 +746,8 @@ type DeliveryType string const ( DeliveryEmail DeliveryType = "email" // Email via yao/messenger - DeliveryWebhook DeliveryType = "webhook" // POST to URL + DeliveryWebhook DeliveryType = "webhook" // POST to external URL + DeliveryProcess DeliveryType = "process" // Yao Process call DeliveryNotify DeliveryType = "notify" // In-app notification (future) ) @@ -815,10 +841,44 @@ type MCP struct { Tools []string `json:"tools,omitempty"` // empty = all } -// Delivery -type Delivery struct { - Type DeliveryType `json:"type"` - Opts map[string]interface{} `json:"opts"` +// DeliveryPreferences - Robot delivery preferences (read by Delivery Center) +// Each channel supports multiple targets +type DeliveryPreferences struct { + Email *EmailPreference `json:"email,omitempty"` + Webhook *WebhookPreference `json:"webhook,omitempty"` + Process *ProcessPreference `json:"process,omitempty"` + // notify is handled automatically based on user subscriptions +} + +type EmailPreference struct { + Enabled bool `json:"enabled"` + Targets []EmailTarget `json:"targets"` +} + +type EmailTarget struct { + To []string `json:"to"` + CC []string `json:"cc,omitempty"` + SubjectTemplate string `json:"subject_template,omitempty"` +} + +type WebhookPreference struct { + Enabled bool `json:"enabled"` + Targets []WebhookTarget `json:"targets"` +} + +type WebhookTarget struct { + URL string `json:"url"` + Headers map[string]string `json:"headers,omitempty"` +} + +type ProcessPreference struct { + Enabled bool `json:"enabled"` + Targets []ProcessTarget `json:"targets"` +} + +type ProcessTarget struct { + Name string `json:"name"` // Process name, e.g., "orders.UpdateStatus" + Args []any `json:"args,omitempty"` } // ExecutorMode - executor mode enum diff --git a/agent/robot/TECHNICAL.md b/agent/robot/TECHNICAL.md index e9757468..86b4e6c9 100644 --- a/agent/robot/TECHNICAL.md +++ b/agent/robot/TECHNICAL.md @@ -855,7 +855,8 @@ type DeliveryType string const ( DeliveryEmail DeliveryType = "email" // Email via yao/messenger - DeliveryWebhook DeliveryType = "webhook" // POST to URL + DeliveryWebhook DeliveryType = "webhook" // POST to external URL + DeliveryProcess DeliveryType = "process" // Yao Process call DeliveryNotify DeliveryType = "notify" // In-app notification (future) ) @@ -953,7 +954,7 @@ type Config struct { DB *DB `json:"db,omitempty"` // shared database (same as assistant) Learn *Learn `json:"learn,omitempty"` // learning config for private KB Resources *Resources `json:"resources,omitempty"` - Delivery *Delivery `json:"delivery,omitempty"` + Delivery *DeliveryPreferences `json:"delivery,omitempty"` // see section 6.2 Events []Event `json:"events,omitempty"` } @@ -1133,11 +1134,7 @@ type MCPConfig struct { Tools []string `json:"tools,omitempty"` // empty = all } -// Delivery - output delivery -type Delivery struct { - Type DeliveryType `json:"type"` - Opts map[string]interface{} `json:"opts,omitempty"` -} +// Note: Delivery preferences moved to DeliveryPreferences (see section 6.2) // Event - event trigger config type Event struct { @@ -1435,22 +1432,46 @@ type DeliveryContext struct { } // DeliveryPreferences - Robot/User delivery preferences (read by Delivery Center) +// Each channel supports multiple targets type DeliveryPreferences struct { Email *EmailPreference `json:"email,omitempty"` Webhook *WebhookPreference `json:"webhook,omitempty"` + Process *ProcessPreference `json:"process,omitempty"` // notify is handled automatically based on user subscriptions } +// EmailPreference - multiple email targets type EmailPreference struct { - Enabled bool `json:"enabled"` - To []string `json:"to"` - CC []string `json:"cc,omitempty"` + Enabled bool `json:"enabled"` + Targets []EmailTarget `json:"targets"` } +type EmailTarget struct { + To []string `json:"to"` + CC []string `json:"cc,omitempty"` + SubjectTemplate string `json:"subject_template,omitempty"` // Optional, default: content.Summary +} + +// WebhookPreference - multiple webhook targets type WebhookPreference struct { - Enabled bool `json:"enabled"` - URL string `json:"url"` - // If enabled, every execution pushes automatically + Enabled bool `json:"enabled"` + Targets []WebhookTarget `json:"targets"` +} + +type WebhookTarget struct { + URL string `json:"url"` + Headers map[string]string `json:"headers,omitempty"` +} + +// ProcessPreference - multiple Yao Process targets +type ProcessPreference struct { + Enabled bool `json:"enabled"` + Targets []ProcessTarget `json:"targets"` +} + +type ProcessTarget struct { + Name string `json:"name"` // Process name, e.g., "orders.UpdateStatus" + Args []any `json:"args,omitempty"` // Additional args (DeliveryContent passed as first arg) } // DeliveryResult - P4 delivery output (returned by Delivery Center) @@ -1462,9 +1483,10 @@ type DeliveryResult struct { Error string `json:"error,omitempty"` // Overall error if any } -// ChannelResult - result for a single delivery channel +// ChannelResult - result for a single delivery target type ChannelResult struct { - Type DeliveryType `json:"type"` // email | webhook | notify + Type DeliveryType `json:"type"` // email | webhook | process | notify + Target string `json:"target,omitempty"` // Target identifier (email, URL, process name) Success bool `json:"success"` Recipients []string `json:"recipients,omitempty"` // Who received (for email) Details interface{} `json:"details,omitempty"` // Channel-specific response @@ -2040,25 +2062,29 @@ type DeliveryContext struct { **Channel Decision by Delivery Center:** -Delivery Center reads Robot/User preferences and decides channels: +Delivery Center reads Robot/User preferences and executes delivery to all enabled targets: ```go -// DeliveryPreferences - from Robot config +// DeliveryPreferences - from Robot config (each channel supports multiple targets) type DeliveryPreferences struct { Email *EmailPreference `json:"email,omitempty"` Webhook *WebhookPreference `json:"webhook,omitempty"` + Process *ProcessPreference `json:"process,omitempty"` } type EmailPreference struct { - Enabled bool `json:"enabled"` - To []string `json:"to"` - CC []string `json:"cc,omitempty"` + Enabled bool `json:"enabled"` + Targets []EmailTarget `json:"targets"` // Multiple email targets } type WebhookPreference struct { - Enabled bool `json:"enabled"` - URL string `json:"url"` - // If enabled, every execution pushes automatically + Enabled bool `json:"enabled"` + Targets []WebhookTarget `json:"targets"` // Multiple webhook URLs +} + +type ProcessPreference struct { + Enabled bool `json:"enabled"` + Targets []ProcessTarget `json:"targets"` // Multiple Yao Process calls } ``` @@ -2139,57 +2165,59 @@ The agent focuses on content generation: ### 6.5 Delivery Center -The Delivery Center receives `DeliveryRequest`, **decides channels based on preferences**, and executes delivery. +The Delivery Center receives `DeliveryRequest`, reads preferences, and executes delivery to **all enabled targets**. **Current implementation:** Internal to P4 (in `executor/delivery.go`) **Future:** Can be extracted to standalone `yao/delivery` package ```go -// DeliveryCenter - handles channel decision and delivery execution +// DeliveryCenter - handles delivery execution to multiple targets type DeliveryCenter struct { - handlers map[DeliveryType]ChannelHandler -} - -// ChannelHandler - interface for channel implementations -type ChannelHandler interface { - Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) + messenger *messenger.Manager } // Deliver - main entry point func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *DeliveryResult { requestID := generateID() - - // 1. Get Robot/User delivery preferences prefs := dc.getDeliveryPreferences(ctx, req.Context.RobotID) - // 2. Decide channels based on preferences - channels := dc.decideChannels(prefs) - - // 3. Execute delivery to each channel var results []ChannelResult allSuccess := true - for _, ch := range channels { - handler, ok := dc.handlers[ch.Type] - if !ok { - results = append(results, ChannelResult{ - Type: ch.Type, - Success: false, - Error: fmt.Sprintf("unsupported channel: %s", ch.Type), - }) - allSuccess = false - continue + // Email - send to all targets + if prefs.Email != nil && prefs.Email.Enabled { + for _, target := range prefs.Email.Targets { + result := dc.sendEmail(ctx, req.Content, target) + results = append(results, result) + if !result.Success { + allSuccess = false + } } - - result, err := handler.Deliver(ctx, req.Content, ch.Options) - if err != nil { - result = &ChannelResult{Type: ch.Type, Success: false, Error: err.Error()} - allSuccess = false - } - results = append(results, *result) } - // 4. Future: check user subscriptions and send notifications + // Webhook - POST to all targets + if prefs.Webhook != nil && prefs.Webhook.Enabled { + for _, target := range prefs.Webhook.Targets { + result := dc.postWebhook(ctx, req.Content, target) + results = append(results, result) + if !result.Success { + allSuccess = false + } + } + } + + // Process - call all targets + if prefs.Process != nil && prefs.Process.Enabled { + for _, target := range prefs.Process.Targets { + result := dc.callProcess(ctx, req.Content, target) + results = append(results, result) + if !result.Success { + allSuccess = false + } + } + } + + // Future: auto-notify based on user subscriptions // dc.sendNotifications(ctx, req) return &DeliveryResult{ @@ -2199,40 +2227,15 @@ func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *De Results: results, } } - -// decideChannels - decide which channels to use based on preferences -func (dc *DeliveryCenter) decideChannels(prefs *DeliveryPreferences) []channelWithOpts { - var channels []channelWithOpts - - if prefs.Email != nil && prefs.Email.Enabled { - channels = append(channels, channelWithOpts{ - Type: DeliveryEmail, - Options: map[string]interface{}{"to": prefs.Email.To, "cc": prefs.Email.CC}, - }) - } - - if prefs.Webhook != nil && prefs.Webhook.Enabled { - channels = append(channels, channelWithOpts{ - Type: DeliveryWebhook, - Options: map[string]interface{}{"url": prefs.Webhook.URL}, - }) - } - - return channels -} ``` ### 6.6 Channel Handlers -Each delivery channel has a dedicated handler implementing `ChannelHandler`: +Each delivery channel is handled by dedicated methods in DeliveryCenter: ```go -// EmailHandler - uses yao/messenger -type EmailHandler struct { - messenger *messenger.Manager -} - -func (h *EmailHandler) Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) { +// sendEmail - send to a single email target +func (dc *DeliveryCenter) sendEmail(ctx context.Context, content *DeliveryContent, target EmailTarget) ChannelResult { // Convert attachments to messenger format var attachments []messenger.Attachment for _, att := range content.Attachments { @@ -2248,50 +2251,87 @@ func (h *EmailHandler) Deliver(ctx context.Context, content *DeliveryContent, op }) } - to := opts["to"].([]string) - err := h.messenger.Send(ctx, &messenger.Message{ - To: to, - Subject: content.Summary, // Use summary as subject + subject := content.Summary + if target.SubjectTemplate != "" { + subject = target.SubjectTemplate + } + + err := dc.messenger.Send(ctx, &messenger.Message{ + To: target.To, + CC: target.CC, + Subject: subject, Body: content.Body, Attachments: attachments, }) now := time.Now() - return &ChannelResult{ + return ChannelResult{ Type: DeliveryEmail, + Target: strings.Join(target.To, ","), Success: err == nil, - Recipients: to, + Recipients: target.To, SentAt: &now, - }, err + Error: errStr(err), + } } -// WebhookHandler - POST JSON to URL -type WebhookHandler struct{} - -func (h *WebhookHandler) Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) { +// postWebhook - POST to a single webhook target +func (dc *DeliveryCenter) postWebhook(ctx context.Context, content *DeliveryContent, target WebhookTarget) ChannelResult { payload, _ := json.Marshal(content) - req, _ := http.NewRequestWithContext(ctx, "POST", opts["url"].(string), bytes.NewReader(payload)) + req, _ := http.NewRequestWithContext(ctx, "POST", target.URL, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") + // Add custom headers + for k, v := range target.Headers { + req.Header.Set(k, v) + } + resp, err := http.DefaultClient.Do(req) + now := time.Now() + if err != nil { - return &ChannelResult{Type: DeliveryWebhook, Success: false}, err + return ChannelResult{ + Type: DeliveryWebhook, + Target: target.URL, + Success: false, + Error: err.Error(), + SentAt: &now, + } } defer resp.Body.Close() - if resp.StatusCode >= 400 { - return &ChannelResult{Type: DeliveryWebhook, Success: false}, fmt.Errorf("webhook failed: %d", resp.StatusCode) - } - - now := time.Now() - return &ChannelResult{ + success := resp.StatusCode < 400 + return ChannelResult{ Type: DeliveryWebhook, - Success: true, + Target: target.URL, + Success: success, Details: map[string]interface{}{"status_code": resp.StatusCode}, + Error: ternary(!success, fmt.Sprintf("HTTP %d", resp.StatusCode), ""), SentAt: &now, - }, nil + } } +// callProcess - call a single Yao Process target +func (dc *DeliveryCenter) callProcess(ctx context.Context, content *DeliveryContent, target ProcessTarget) ChannelResult { + // DeliveryContent as first arg, then additional args + args := append([]interface{}{content}, target.Args...) + + proc := process.Of(target.Name, args...) + result, err := proc.Execute() + + now := time.Now() + return ChannelResult{ + Type: DeliveryProcess, + Target: target.Name, + Success: err == nil, + Details: map[string]interface{}{ + "process": target.Name, + "result": result, + }, + Error: errStr(err), + SentAt: &now, + } +} ``` **Note on Notifications:** diff --git a/agent/robot/TODO.md b/agent/robot/TODO.md index 5d4d8643..3007fd9c 100644 --- a/agent/robot/TODO.md +++ b/agent/robot/TODO.md @@ -62,7 +62,7 @@ - [x] `RobotStatus` - robot status (idle, working, paused, error, maintenance) - [x] `InterventionAction` - human actions (task.add, goal.adjust, etc.) - [x] `Priority` - priority levels (high, normal, low) -- [x] `DeliveryType` - delivery types (email, webhook, notify) +- [x] `DeliveryType` - delivery types (email, webhook, process, notify) - [x] `DedupResult` - dedup results (skip, merge, proceed) - [x] `EventSource` - event sources (webhook, database) - [x] `LearningType` - learning types (execution, feedback, insight) @@ -916,16 +916,21 @@ Supported channels: ### 10.3 Type Updates (Prerequisite) -- [ ] Update `types/enums.go` - Remove `DeliveryFile` from `DeliveryType` +- [ ] Update `types/enums.go` - Update `DeliveryType` enum + - [ ] Remove `DeliveryFile` + - [ ] Add `DeliveryProcess` - [ ] Update `types/robot.go` - Delivery types for new architecture - [ ] `DeliveryResult` - update to new structure (RequestID, Content, Results[]) - [ ] Add `DeliveryContent` struct - [ ] Add `DeliveryAttachment` struct - [ ] Add `DeliveryRequest` struct - [ ] Add `DeliveryContext` struct - - [ ] Add `DeliveryPreferences` struct - - [ ] Add `ChannelResult` struct -- [ ] Update `types/enums_test.go` - Remove `DeliveryFile` test + - [ ] Add `DeliveryPreferences` struct (with Email, Webhook, Process) + - [ ] Add `EmailPreference`, `EmailTarget` structs + - [ ] Add `WebhookPreference`, `WebhookTarget` structs + - [ ] Add `ProcessPreference`, `ProcessTarget` structs + - [ ] Add `ChannelResult` struct (with Target field) +- [ ] Update `types/enums_test.go` - Update DeliveryType tests - [ ] Update `types/robot_test.go` - Update delivery result tests ### 10.4 Delivery Agent Setup @@ -980,12 +985,13 @@ type DeliveryContext struct { - Parse: `attachment.Parse(value)` → `(uploader, fileID, isWrapper)` - Read: `attachment.Base64(ctx, value)` → base64 content -**Delivery Channels (Delivery Center decides):** -| Channel | Description | -|---------|-------------| -| `email` | Send via yao/messenger (if configured in preferences) | -| `webhook` | POST JSON to URL (if configured, every execution) | -| `notify` | In-app notification based on user subscriptions (future) | +**Delivery Channels (each supports multiple targets):** +| Channel | Description | Multiple Targets | +|---------|-------------|------------------| +| `email` | Send via yao/messenger | ✅ Multiple recipients | +| `webhook` | POST to external URL | ✅ Multiple URLs | +| `process` | Yao Process call | ✅ Multiple processes | +| `notify` | In-app notification | Future (auto by subscriptions) | ### 10.6 Implementation @@ -999,30 +1005,36 @@ type DeliveryContext struct { **Delivery Center (executor/delivery.go, future: yao/delivery):** - [ ] `DeliveryCenter.Deliver(ctx, request)` - main entry - [ ] Read Robot/User delivery preferences - - [ ] Decide which channels to use based on preferences - - [ ] Call appropriate handler for each channel + - [ ] Iterate through all enabled targets for each channel - [ ] Aggregate ChannelResults into DeliveryResult -- [ ] `ChannelHandler` interface - - [ ] `Deliver(ctx, content, opts) (*ChannelResult, error)` -**Channel Handlers:** -- [ ] `EmailHandler` - uses yao/messenger +**Channel Handlers (each supports multiple targets):** +- [ ] `sendEmail()` - uses yao/messenger - [ ] Convert DeliveryAttachment to messenger.Attachment - - [ ] Use Summary as email subject - - [ ] Support to, cc from preferences -- [ ] `WebhookHandler` - POST JSON + - [ ] Support multiple EmailTarget + - [ ] Support custom subject_template per target +- [ ] `postWebhook()` - POST JSON - [ ] POST DeliveryContent as JSON payload - - [ ] If enabled, every execution pushes automatically + - [ ] Support multiple WebhookTarget + - [ ] Support custom headers per target +- [ ] `callProcess()` - Yao Process call + - [ ] DeliveryContent as first arg + - [ ] Support multiple ProcessTarget + - [ ] Support additional args per target ### 10.7 Tests - [ ] `executor/delivery_test.go` - P4 delivery - [ ] Test: Delivery Agent generates content (only content) -- [ ] Test: DeliveryCenter reads preferences and decides channels -- [ ] Test: DeliveryCenter dispatches to multiple channels -- [ ] Test: EmailHandler with attachments -- [ ] Test: WebhookHandler POST JSON -- [ ] Test: Partial success (some channels fail) +- [ ] Test: DeliveryCenter reads preferences +- [ ] Test: Multiple email targets +- [ ] Test: Multiple webhook targets +- [ ] Test: Multiple process targets +- [ ] Test: Mixed channels (email + webhook + process) +- [ ] Test: sendEmail with attachments +- [ ] Test: postWebhook with custom headers +- [ ] Test: callProcess with args +- [ ] Test: Partial success (some targets fail) - [ ] Test: DeliveryResult aggregation --- @@ -1239,7 +1251,7 @@ func TestWithLLM(t *testing.T) { | 7. P1 Goals | ✅ | Goal Generation Agent integration | | 8. P2 Tasks | ✅ | Task Planning Agent integration | | 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation | -| 10. P4 Delivery | ⬜ | Output delivery (email/webhook, notify future) | +| 10. P4 Delivery | ⬜ | Output delivery (email/webhook/process, notify future) | | 11. API & Integration | ⬜ | Complete API, end-to-end tests (main flow: P0→P1→P2→P3→P4) | | 12. Advanced | ⬜ | P5 Learning, dedup, plan queue, Sandbox mode |