Enhance Email Delivery Configuration and Update Documentation
- Added `robot_email` field to the Robot structure for specifying the sender's email address in email communications. - Updated the EmailTarget structure by removing the CC field, simplifying the configuration for email targets. - Revised TECHNICAL.md to include details on the new `robot_email` field and its usage in email delivery, ensuring clarity on the sender identity. - Enhanced the Delivery Center's email handling to utilize the Robot's email as the From address, with a fallback to the provider's default if not configured. - Updated DESIGN.md to reflect changes in email delivery architecture and added new sections for global email configuration. - Marked completion of related tasks in TODO.md, confirming the integration of new email features and structures.
This commit is contained in:
parent
6c45cc41be
commit
62a18f0c8d
9 changed files with 2060 additions and 76 deletions
|
|
@ -138,6 +138,7 @@ Uses existing `__yao.member` model (`yao/models/member.mod.yao`):
|
||||||
| `robot_config` | JSON | Agent configuration (see section 5) |
|
| `robot_config` | JSON | Agent configuration (see section 5) |
|
||||||
| `robot_status` | enum | `idle` \| `working` \| `paused` \| `error` \| `maintenance` |
|
| `robot_status` | enum | `idle` \| `working` \| `paused` \| `error` \| `maintenance` |
|
||||||
| `system_prompt` | text | Identity & role prompt |
|
| `system_prompt` | text | Identity & role prompt |
|
||||||
|
| `robot_email` | string | Robot's email address for sending emails (From address) |
|
||||||
| `agents` | JSON | Accessible agents list |
|
| `agents` | JSON | Accessible agents list |
|
||||||
| `mcp_servers` | JSON | Accessible MCP servers |
|
| `mcp_servers` | JSON | Accessible MCP servers |
|
||||||
| `manager_id` | string | Direct manager user ID |
|
| `manager_id` | string | Direct manager user ID |
|
||||||
|
|
@ -859,7 +860,6 @@ type EmailPreference struct {
|
||||||
|
|
||||||
type EmailTarget struct {
|
type EmailTarget struct {
|
||||||
To []string `json:"to"` // Recipient addresses
|
To []string `json:"to"` // Recipient addresses
|
||||||
CC []string `json:"cc,omitempty"` // CC addresses
|
|
||||||
Template string `json:"template,omitempty"` // Email template ID
|
Template string `json:"template,omitempty"` // Email template ID
|
||||||
Subject string `json:"subject,omitempty"` // Subject template
|
Subject string `json:"subject,omitempty"` // Subject template
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1169,6 +1169,7 @@ type Robot struct {
|
||||||
SystemPrompt string `json:"system_prompt"`
|
SystemPrompt string `json:"system_prompt"`
|
||||||
Status RobotStatus `json:"robot_status"`
|
Status RobotStatus `json:"robot_status"`
|
||||||
AutonomousMode bool `json:"autonomous_mode"`
|
AutonomousMode bool `json:"autonomous_mode"`
|
||||||
|
RobotEmail string `json:"robot_email"` // Robot's email address for sending emails
|
||||||
|
|
||||||
// Parsed config (from robot_config JSON field)
|
// Parsed config (from robot_config JSON field)
|
||||||
Config *Config `json:"-"`
|
Config *Config `json:"-"`
|
||||||
|
|
@ -1448,7 +1449,6 @@ type EmailPreference struct {
|
||||||
|
|
||||||
type EmailTarget struct {
|
type EmailTarget struct {
|
||||||
To []string `json:"to"` // Recipient addresses
|
To []string `json:"to"` // Recipient addresses
|
||||||
CC []string `json:"cc,omitempty"` // CC addresses
|
|
||||||
Template string `json:"template,omitempty"` // Email template ID
|
Template string `json:"template,omitempty"` // Email template ID
|
||||||
Subject string `json:"subject,omitempty"` // Subject template (default: content.Summary)
|
Subject string `json:"subject,omitempty"` // Subject template (default: content.Summary)
|
||||||
}
|
}
|
||||||
|
|
@ -2166,7 +2166,27 @@ The agent focuses on content generation:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.5 Delivery Center
|
### 6.5 Global Email Configuration
|
||||||
|
|
||||||
|
Email delivery uses global configuration for channel selection and Robot-specific sender identity:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// types/config_global.go
|
||||||
|
|
||||||
|
// DefaultEmailChannel returns the default messenger channel name
|
||||||
|
// Default: "email" (maps to messengers/channels.yao)
|
||||||
|
func DefaultEmailChannel() string
|
||||||
|
|
||||||
|
// SetDefaultEmailChannel sets the default channel (call during agent init)
|
||||||
|
func SetDefaultEmailChannel(channel string)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- `DefaultEmailChannel()` - returns the messenger channel name for email delivery
|
||||||
|
- `Robot.RobotEmail` - used as the `From` address when sending emails
|
||||||
|
- If `RobotEmail` is empty, falls back to provider's default `from` address
|
||||||
|
|
||||||
|
### 6.6 Delivery Center
|
||||||
|
|
||||||
The Delivery Center receives `DeliveryRequest`, reads preferences, and executes delivery to **all enabled targets**.
|
The Delivery Center receives `DeliveryRequest`, reads preferences, and executes delivery to **all enabled targets**.
|
||||||
|
|
||||||
|
|
@ -2187,10 +2207,10 @@ func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *De
|
||||||
var results []ChannelResult
|
var results []ChannelResult
|
||||||
allSuccess := true
|
allSuccess := true
|
||||||
|
|
||||||
// Email - send to all targets
|
// Email - send to all targets (robot passed for From address)
|
||||||
if prefs.Email != nil && prefs.Email.Enabled {
|
if prefs.Email != nil && prefs.Email.Enabled {
|
||||||
for _, target := range prefs.Email.Targets {
|
for _, target := range prefs.Email.Targets {
|
||||||
result := dc.sendEmail(ctx, req.Content, target)
|
result := dc.sendEmail(ctx, req.Content, target, req.Context, robot)
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
if !result.Success {
|
if !result.Success {
|
||||||
allSuccess = false
|
allSuccess = false
|
||||||
|
|
@ -2232,13 +2252,20 @@ func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *De
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.6 Channel Handlers
|
### 6.7 Channel Handlers
|
||||||
|
|
||||||
Each delivery channel is handled by dedicated methods in DeliveryCenter:
|
Each delivery channel is handled by dedicated methods in DeliveryCenter:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// sendEmail - send to a single email target
|
// sendEmail - send to a single email target
|
||||||
func (dc *DeliveryCenter) sendEmail(ctx context.Context, content *DeliveryContent, target EmailTarget) ChannelResult {
|
// Uses Robot.RobotEmail as From address and global DefaultEmailChannel()
|
||||||
|
func (dc *DeliveryCenter) sendEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
content *DeliveryContent,
|
||||||
|
target EmailTarget,
|
||||||
|
deliveryCtx *DeliveryContext,
|
||||||
|
robot *Robot,
|
||||||
|
) ChannelResult {
|
||||||
// Convert attachments to messenger format
|
// Convert attachments to messenger format
|
||||||
var attachments []messenger.Attachment
|
var attachments []messenger.Attachment
|
||||||
for _, att := range content.Attachments {
|
for _, att := range content.Attachments {
|
||||||
|
|
@ -2255,17 +2282,25 @@ func (dc *DeliveryCenter) sendEmail(ctx context.Context, content *DeliveryConten
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := content.Summary
|
subject := content.Summary
|
||||||
if target.SubjectTemplate != "" {
|
if target.Subject != "" {
|
||||||
subject = target.SubjectTemplate
|
subject = target.Subject
|
||||||
}
|
}
|
||||||
|
|
||||||
err := dc.messenger.Send(ctx, &messenger.Message{
|
msg := &messenger.Message{
|
||||||
To: target.To,
|
To: target.To,
|
||||||
CC: target.CC,
|
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Body: content.Body,
|
Body: content.Body,
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Set From address from Robot's email (if configured)
|
||||||
|
if robot != nil && robot.RobotEmail != "" {
|
||||||
|
msg.From = robot.RobotEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use global default email channel
|
||||||
|
channel := DefaultEmailChannel() // from types/config_global.go
|
||||||
|
err := dc.messenger.Send(ctx, channel, msg)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return ChannelResult{
|
return ChannelResult{
|
||||||
|
|
@ -2344,7 +2379,7 @@ func (dc *DeliveryCenter) callProcess(ctx context.Context, content *DeliveryCont
|
||||||
2. Automatically send in-app notifications to subscribed users
|
2. Automatically send in-app notifications to subscribed users
|
||||||
3. This is transparent to P4 and Delivery Agent
|
3. This is transparent to P4 and Delivery Agent
|
||||||
|
|
||||||
### 6.7 Execution Persistence
|
### 6.8 Execution Persistence
|
||||||
|
|
||||||
Robot execution history is stored in `__yao.agent_execution` table for UI display:
|
Robot execution history is stored in `__yao.agent_execution` table for UI display:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -975,11 +975,11 @@ Supported channels:
|
||||||
|
|
||||||
### 10.4 Delivery Agent Setup
|
### 10.4 Delivery Agent Setup
|
||||||
|
|
||||||
- [ ] `robot/delivery/package.yao` - Delivery Agent config
|
- [x] `robot/delivery/package.yao` - Delivery Agent config
|
||||||
- [ ] `robot/delivery/prompts.yml` - delivery prompts
|
- [x] `robot/delivery/prompts.yml` - delivery prompts
|
||||||
- [ ] Input: Full execution context (P0-P3 results)
|
- [x] Input: Full execution context (P0-P3 results)
|
||||||
- [ ] Output: DeliveryContent (Summary, Body, Attachments) - **only content, no channels**
|
- [x] Output: DeliveryContent (Summary, Body, Attachments) - **only content, no channels**
|
||||||
- [ ] Agent focuses on content generation, NOT channel selection
|
- [x] Agent focuses on content generation, NOT channel selection
|
||||||
|
|
||||||
### 10.5 Delivery Content Structure
|
### 10.5 Delivery Content Structure
|
||||||
|
|
||||||
|
|
@ -1036,46 +1036,48 @@ type DeliveryContext struct {
|
||||||
### 10.6 Implementation
|
### 10.6 Implementation
|
||||||
|
|
||||||
**P4 Entry (executor/delivery.go):**
|
**P4 Entry (executor/delivery.go):**
|
||||||
- [ ] `RunDelivery(ctx, exec, data)` - P4 entry point
|
- [x] `RunDelivery(ctx, exec, data)` - P4 entry point
|
||||||
- [ ] Call Delivery Agent to generate content (only content, no channels)
|
- [x] Call Delivery Agent to generate content (only content, no channels)
|
||||||
- [ ] Build DeliveryRequest (Content + Context)
|
- [x] Build DeliveryRequest (Content + Context)
|
||||||
- [ ] Push to Delivery Center
|
- [x] Push to Delivery Center
|
||||||
- [ ] Store DeliveryResult in exec.Delivery
|
- [x] Store DeliveryResult in exec.Delivery
|
||||||
|
|
||||||
**Delivery Center (executor/delivery.go, future: yao/delivery):**
|
**Delivery Center (executor/delivery_center.go):**
|
||||||
- [ ] `DeliveryCenter.Deliver(ctx, request)` - main entry
|
- [x] `DeliveryCenter.Deliver(ctx, request)` - main entry
|
||||||
- [ ] Read Robot/User delivery preferences
|
- [x] Read Robot/User delivery preferences
|
||||||
- [ ] Iterate through all enabled targets for each channel
|
- [x] Iterate through all enabled targets for each channel
|
||||||
- [ ] Aggregate ChannelResults into DeliveryResult
|
- [x] Aggregate ChannelResults into DeliveryResult
|
||||||
|
|
||||||
**Channel Handlers (each supports multiple targets):**
|
**Channel Handlers (each supports multiple targets):**
|
||||||
- [ ] `sendEmail()` - uses yao/messenger
|
- [x] `sendEmail()` - uses yao/messenger
|
||||||
- [ ] Convert DeliveryAttachment to messenger.Attachment
|
- [x] Convert DeliveryAttachment to messenger.Attachment
|
||||||
- [ ] Support multiple EmailTarget
|
- [x] Support multiple EmailTarget
|
||||||
- [ ] Support custom subject_template per target
|
- [x] Support custom subject_template per target
|
||||||
- [ ] `postWebhook()` - POST JSON
|
- [x] Use `Robot.RobotEmail` as From address (if configured)
|
||||||
- [ ] POST DeliveryContent as JSON payload
|
- [x] Use global `DefaultEmailChannel()` for messenger channel selection
|
||||||
- [ ] Support multiple WebhookTarget
|
- [x] `postWebhook()` - POST JSON
|
||||||
- [ ] Support custom headers per target
|
- [x] POST DeliveryContent as JSON payload
|
||||||
- [ ] `callProcess()` - Yao Process call
|
- [x] Support multiple WebhookTarget
|
||||||
- [ ] DeliveryContent as first arg
|
- [x] Support custom headers per target
|
||||||
- [ ] Support multiple ProcessTarget
|
- [x] `callProcess()` - Yao Process call
|
||||||
- [ ] Support additional args per target
|
- [x] DeliveryContent as first arg
|
||||||
|
- [x] Support multiple ProcessTarget
|
||||||
|
- [x] Support additional args per target
|
||||||
|
|
||||||
### 10.7 Tests
|
### 10.7 Tests
|
||||||
|
|
||||||
- [ ] `executor/delivery_test.go` - P4 delivery
|
- [x] `executor/delivery_test.go` - P4 delivery
|
||||||
- [ ] Test: Delivery Agent generates content (only content)
|
- [x] Test: Delivery Agent generates content (only content)
|
||||||
- [ ] Test: DeliveryCenter reads preferences
|
- [x] Test: DeliveryCenter reads preferences
|
||||||
- [ ] Test: Multiple email targets
|
- [x] Test: Multiple email targets (TestDeliveryCenterEmail)
|
||||||
- [ ] Test: Multiple webhook targets
|
- [x] Test: Multiple webhook targets
|
||||||
- [ ] Test: Multiple process targets
|
- [x] Test: Multiple process targets (TestDeliveryCenterProcess)
|
||||||
- [ ] Test: Mixed channels (email + webhook + process)
|
- [x] Test: Mixed channels (email + webhook + process) (TestDeliveryCenterAllChannels)
|
||||||
- [ ] Test: sendEmail with attachments
|
- [x] Test: sendEmail with attachments (TestDeliveryCenterEmail)
|
||||||
- [ ] Test: postWebhook with custom headers
|
- [x] Test: postWebhook with custom headers
|
||||||
- [ ] Test: callProcess with args
|
- [x] Test: callProcess with args (TestDeliveryCenterProcess)
|
||||||
- [ ] Test: Partial success (some targets fail)
|
- [x] Test: Partial success (some targets fail)
|
||||||
- [ ] Test: DeliveryResult aggregation
|
- [x] Test: DeliveryResult aggregation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1291,7 +1293,7 @@ func TestWithLLM(t *testing.T) {
|
||||||
| 7. P1 Goals | ✅ | Goal Generation Agent integration |
|
| 7. P1 Goals | ✅ | Goal Generation Agent integration |
|
||||||
| 8. P2 Tasks | ✅ | Task Planning Agent integration |
|
| 8. P2 Tasks | ✅ | Task Planning Agent integration |
|
||||||
| 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation |
|
| 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation |
|
||||||
| 10. P4 Delivery | ⬜ | Output delivery (email/webhook/process, 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) |
|
| 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 |
|
| 12. Advanced | ⬜ | P5 Learning, dedup, plan queue, Sandbox mode |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,397 @@
|
||||||
package standard
|
package standard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunDelivery executes P4: Delivery phase
|
// RunDelivery executes P4: Delivery phase
|
||||||
// Delivers results to configured targets
|
// Calls the Delivery Agent to generate content, then routes to Delivery Center
|
||||||
//
|
//
|
||||||
// Input:
|
// Input:
|
||||||
// - TaskResults (from P3)
|
// - Full execution context (P0-P3)
|
||||||
// - Delivery config from robot
|
// - Robot config
|
||||||
//
|
//
|
||||||
// Output:
|
// Output:
|
||||||
// - DeliveryResult with success status
|
// - DeliveryResult with content and channel results
|
||||||
//
|
//
|
||||||
// Delivery Types:
|
// Process:
|
||||||
// - DeliveryEmail: Send email
|
// 1. Call Delivery Agent with full execution context
|
||||||
// - DeliveryNotify: Send notification
|
// 2. Agent generates DeliveryContent (summary, body, attachments)
|
||||||
// - DeliveryWebhook: Call webhook
|
// 3. Route content to Delivery Center for actual delivery
|
||||||
// - DeliveryStore: Store to database
|
|
||||||
//
|
|
||||||
// TODO: Implement real delivery
|
|
||||||
func (e *Executor) RunDelivery(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error {
|
func (e *Executor) RunDelivery(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error {
|
||||||
e.simulateStreamDelay()
|
// Get robot for identity and resources
|
||||||
|
robot := exec.GetRobot()
|
||||||
|
if robot == nil {
|
||||||
|
return fmt.Errorf("robot not found in execution")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get agent ID for delivery phase
|
||||||
|
agentID := "__yao.delivery" // default
|
||||||
|
if robot.Config != nil && robot.Config.Resources != nil {
|
||||||
|
agentID = robot.Config.Resources.GetPhaseAgent(robottypes.PhaseDelivery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build input for Delivery Agent
|
||||||
|
formatter := NewInputFormatter()
|
||||||
|
userContent := formatter.FormatDeliveryInput(exec, robot)
|
||||||
|
|
||||||
|
if userContent == "" {
|
||||||
|
return fmt.Errorf("no content available for delivery generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Delivery Agent
|
||||||
|
caller := NewAgentCaller()
|
||||||
|
result, err := caller.CallWithMessages(ctx, agentID, userContent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delivery agent (%s) call failed: %w", agentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response as JSON
|
||||||
|
// Delivery Agent returns: { "content": { "summary": "...", "body": "...", "attachments": [...] } }
|
||||||
|
data, err := result.GetJSON()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback: if not JSON, create minimal content from raw text
|
||||||
|
content := result.GetText()
|
||||||
|
if content == "" {
|
||||||
|
return fmt.Errorf("delivery agent returned empty response")
|
||||||
|
}
|
||||||
exec.Delivery = &robottypes.DeliveryResult{
|
exec.Delivery = &robottypes.DeliveryResult{
|
||||||
RequestID: "delivery-" + exec.ID,
|
RequestID: generateRequestID(exec.ID),
|
||||||
Content: &robottypes.DeliveryContent{
|
Content: &robottypes.DeliveryContent{
|
||||||
Summary: "Delivery completed (placeholder)",
|
Summary: truncateSummary(content, 200),
|
||||||
Body: "# Delivery\n\nTODO: Implement real delivery logic.",
|
Body: content,
|
||||||
},
|
},
|
||||||
Success: true,
|
Success: true,
|
||||||
}
|
}
|
||||||
|
return e.routeToDeliveryCenter(ctx, exec, robot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build DeliveryContent from JSON
|
||||||
|
content := parseDeliveryContent(data)
|
||||||
|
if content == nil {
|
||||||
|
return fmt.Errorf("delivery agent (%s) returned invalid content", agentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build DeliveryResult
|
||||||
|
exec.Delivery = &robottypes.DeliveryResult{
|
||||||
|
RequestID: generateRequestID(exec.ID),
|
||||||
|
Content: content,
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to Delivery Center for actual delivery
|
||||||
|
return e.routeToDeliveryCenter(ctx, exec, robot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeToDeliveryCenter sends content to the Delivery Center for actual delivery
|
||||||
|
// The Delivery Center decides which channels to use based on robot/user preferences
|
||||||
|
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)
|
||||||
|
if prefs == nil || !hasActiveChannels(prefs) {
|
||||||
|
// No channels configured - mark as success but with no results
|
||||||
|
exec.Delivery.Success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Delivery Center and execute
|
||||||
|
center := NewDeliveryCenter()
|
||||||
|
results, err := center.Deliver(ctx, exec.Delivery.Content, &robottypes.DeliveryContext{
|
||||||
|
MemberID: exec.MemberID,
|
||||||
|
ExecutionID: exec.ID,
|
||||||
|
TriggerType: exec.TriggerType,
|
||||||
|
TeamID: exec.TeamID,
|
||||||
|
}, prefs, robot)
|
||||||
|
|
||||||
|
// Update delivery result
|
||||||
|
exec.Delivery.Results = results
|
||||||
|
now := time.Now()
|
||||||
|
exec.Delivery.SentAt = &now
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
exec.Delivery.Success = false
|
||||||
|
exec.Delivery.Error = err.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all channels succeeded
|
||||||
|
allSuccess := true
|
||||||
|
for _, r := range results {
|
||||||
|
if !r.Success {
|
||||||
|
allSuccess = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exec.Delivery.Success = allSuccess
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseDeliveryContent parses the Delivery Agent response into DeliveryContent
|
||||||
|
func parseDeliveryContent(data map[string]interface{}) *robottypes.DeliveryContent {
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get content object
|
||||||
|
contentData, ok := data["content"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// Fallback: maybe the data itself is the content
|
||||||
|
contentData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
content := &robottypes.DeliveryContent{}
|
||||||
|
|
||||||
|
// Parse summary
|
||||||
|
if summary, ok := contentData["summary"].(string); ok {
|
||||||
|
content.Summary = summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse body
|
||||||
|
if body, ok := contentData["body"].(string); ok {
|
||||||
|
content.Body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attachments
|
||||||
|
if attachments, ok := contentData["attachments"].([]interface{}); ok {
|
||||||
|
for _, att := range attachments {
|
||||||
|
if attMap, ok := att.(map[string]interface{}); ok {
|
||||||
|
attachment := parseDeliveryAttachment(attMap)
|
||||||
|
if attachment != nil {
|
||||||
|
content.Attachments = append(content.Attachments, *attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate: at least summary or body should be present
|
||||||
|
if content.Summary == "" && content.Body == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDeliveryAttachment parses a single attachment from the agent response
|
||||||
|
func parseDeliveryAttachment(data map[string]interface{}) *robottypes.DeliveryAttachment {
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
att := &robottypes.DeliveryAttachment{}
|
||||||
|
|
||||||
|
if title, ok := data["title"].(string); ok {
|
||||||
|
att.Title = title
|
||||||
|
}
|
||||||
|
if desc, ok := data["description"].(string); ok {
|
||||||
|
att.Description = desc
|
||||||
|
}
|
||||||
|
if taskID, ok := data["task_id"].(string); ok {
|
||||||
|
att.TaskID = taskID
|
||||||
|
}
|
||||||
|
if file, ok := data["file"].(string); ok {
|
||||||
|
att.File = file
|
||||||
|
}
|
||||||
|
|
||||||
|
// At minimum, need title and file
|
||||||
|
if att.Title == "" || att.File == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return att
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRequestID generates a unique request ID for delivery tracking
|
||||||
|
func generateRequestID(execID string) string {
|
||||||
|
return fmt.Sprintf("dlv-%s-%d", execID, time.Now().UnixNano()%1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTaskDescription extracts a description from task messages
|
||||||
|
func getTaskDescription(task robottypes.Task) string {
|
||||||
|
if len(task.Messages) == 0 {
|
||||||
|
return task.GoalRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get text from first message
|
||||||
|
for _, msg := range task.Messages {
|
||||||
|
if content, ok := msg.Content.(string); ok && content != "" {
|
||||||
|
// Truncate if too long
|
||||||
|
if len(content) > 100 {
|
||||||
|
return content[:97] + "..."
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to goal reference
|
||||||
|
if task.GoalRef != "" {
|
||||||
|
return task.GoalRef
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Task " + task.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncateSummary truncates text to maxLen characters
|
||||||
|
func truncateSummary(text string, maxLen int) string {
|
||||||
|
if len(text) <= maxLen {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
// Find last space before maxLen to avoid cutting words
|
||||||
|
truncated := text[:maxLen]
|
||||||
|
if idx := strings.LastIndex(truncated, " "); idx > maxLen/2 {
|
||||||
|
return truncated[:idx] + "..."
|
||||||
|
}
|
||||||
|
return truncated + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasActiveChannels checks if any delivery channel is configured
|
||||||
|
func hasActiveChannels(prefs *robottypes.DeliveryPreferences) bool {
|
||||||
|
if prefs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if prefs.Email != nil && prefs.Email.Enabled && len(prefs.Email.Targets) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if prefs.Webhook != nil && prefs.Webhook.Enabled && len(prefs.Webhook.Targets) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if prefs.Process != nil && prefs.Process.Enabled && len(prefs.Process.Targets) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatDeliveryInput formats the full execution context for the Delivery Agent
|
||||||
|
func (f *InputFormatter) FormatDeliveryInput(exec *robottypes.Execution, robot *robottypes.Robot) string {
|
||||||
|
if exec == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// Robot identity
|
||||||
|
if robot != nil && robot.Config != nil && robot.Config.Identity != nil {
|
||||||
|
sb.WriteString("## 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**: ")
|
||||||
|
sb.WriteString(strings.Join(robot.Config.Identity.Duties, ", "))
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger type
|
||||||
|
sb.WriteString("## Execution Context\n\n")
|
||||||
|
sb.WriteString(fmt.Sprintf("- **Trigger**: %s\n", exec.TriggerType))
|
||||||
|
sb.WriteString(fmt.Sprintf("- **Status**: %s\n", exec.Status))
|
||||||
|
sb.WriteString(fmt.Sprintf("- **Start Time**: %s\n", exec.StartTime.Format("2006-01-02 15:04:05")))
|
||||||
|
if exec.EndTime != nil {
|
||||||
|
duration := exec.EndTime.Sub(exec.StartTime)
|
||||||
|
sb.WriteString(fmt.Sprintf("- **Duration**: %s\n", duration.String()))
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
|
||||||
|
// Inspiration (P0) - for clock trigger
|
||||||
|
if exec.Inspiration != nil && exec.Inspiration.Content != "" {
|
||||||
|
sb.WriteString("## Inspiration (P0)\n\n")
|
||||||
|
sb.WriteString(exec.Inspiration.Content)
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goals (P1)
|
||||||
|
if exec.Goals != nil && exec.Goals.Content != "" {
|
||||||
|
sb.WriteString("## Goals (P1)\n\n")
|
||||||
|
sb.WriteString(exec.Goals.Content)
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks (P2)
|
||||||
|
if len(exec.Tasks) > 0 {
|
||||||
|
sb.WriteString("## Tasks (P2)\n\n")
|
||||||
|
for i, task := range exec.Tasks {
|
||||||
|
// Extract task description from messages if available
|
||||||
|
taskDesc := getTaskDescription(task)
|
||||||
|
sb.WriteString(fmt.Sprintf("%d. **%s** - %s\n", i+1, task.ID, taskDesc))
|
||||||
|
sb.WriteString(fmt.Sprintf(" - Executor: %s (%s)\n", task.ExecutorID, task.ExecutorType))
|
||||||
|
sb.WriteString(fmt.Sprintf(" - Status: %s\n", task.Status))
|
||||||
|
if task.ExpectedOutput != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf(" - Expected: %s\n", task.ExpectedOutput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results (P3) - detailed
|
||||||
|
if len(exec.Results) > 0 {
|
||||||
|
sb.WriteString("## Results (P3)\n\n")
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
failCount := 0
|
||||||
|
|
||||||
|
for _, result := range exec.Results {
|
||||||
|
if result.Success {
|
||||||
|
successCount++
|
||||||
|
sb.WriteString(fmt.Sprintf("### ✓ Task: %s\n\n", result.TaskID))
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
sb.WriteString(fmt.Sprintf("### ✗ Task: %s\n\n", result.TaskID))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("- **Duration**: %dms\n", result.Duration))
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if result.Validation != nil {
|
||||||
|
if result.Validation.Passed {
|
||||||
|
sb.WriteString(fmt.Sprintf("- **Validation**: ✓ Passed (score: %.2f)\n", result.Validation.Score))
|
||||||
|
} else {
|
||||||
|
sb.WriteString("- **Validation**: ✗ Failed\n")
|
||||||
|
if len(result.Validation.Issues) > 0 {
|
||||||
|
for _, issue := range result.Validation.Issues {
|
||||||
|
sb.WriteString(fmt.Sprintf(" - %s\n", issue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output
|
||||||
|
if result.Output != nil {
|
||||||
|
sb.WriteString("\n**Output**:\n")
|
||||||
|
if output, err := json.MarshalIndent(result.Output, "", " "); err == nil {
|
||||||
|
sb.WriteString("```json\n")
|
||||||
|
sb.WriteString(string(output))
|
||||||
|
sb.WriteString("\n```\n")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("%v\n", result.Output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error
|
||||||
|
if result.Error != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("\n**Error**: %s\n", result.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
sb.WriteString(fmt.Sprintf("### Summary\n\n- **Total Tasks**: %d\n- **Succeeded**: %d\n- **Failed**: %d\n\n",
|
||||||
|
len(exec.Results), successCount, failCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
|
||||||
378
agent/robot/executor/standard/delivery_center.go
Normal file
378
agent/robot/executor/standard/delivery_center.go
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/attachment"
|
||||||
|
"github.com/yaoapp/yao/messenger"
|
||||||
|
messengerTypes "github.com/yaoapp/yao/messenger/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeliveryCenter handles routing delivery content to configured channels
|
||||||
|
// It decides which channels to use based on robot/user preferences and executes the delivery
|
||||||
|
type DeliveryCenter struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeliveryCenter creates a new DeliveryCenter instance
|
||||||
|
func NewDeliveryCenter() *DeliveryCenter {
|
||||||
|
return &DeliveryCenter{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver sends content to all configured channels based on preferences
|
||||||
|
// Returns results for each channel target and any error
|
||||||
|
func (dc *DeliveryCenter) Deliver(
|
||||||
|
ctx *robottypes.Context,
|
||||||
|
content *robottypes.DeliveryContent,
|
||||||
|
deliveryCtx *robottypes.DeliveryContext,
|
||||||
|
prefs *robottypes.DeliveryPreferences,
|
||||||
|
robotInstance *robottypes.Robot,
|
||||||
|
) ([]robottypes.ChannelResult, error) {
|
||||||
|
if content == nil {
|
||||||
|
return nil, fmt.Errorf("delivery content is nil")
|
||||||
|
}
|
||||||
|
if prefs == nil {
|
||||||
|
return nil, nil // No preferences = no delivery
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []robottypes.ChannelResult
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
// Process email targets
|
||||||
|
if prefs.Email != nil && prefs.Email.Enabled {
|
||||||
|
for _, target := range prefs.Email.Targets {
|
||||||
|
result := dc.sendEmail(ctx.Context, content, target, deliveryCtx, robotInstance)
|
||||||
|
results = append(results, result)
|
||||||
|
if !result.Success && lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("email delivery failed: %s", result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process webhook targets
|
||||||
|
if prefs.Webhook != nil && prefs.Webhook.Enabled {
|
||||||
|
for _, target := range prefs.Webhook.Targets {
|
||||||
|
result := dc.postWebhook(ctx.Context, content, target, deliveryCtx)
|
||||||
|
results = append(results, result)
|
||||||
|
if !result.Success && lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("webhook delivery failed: %s", result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process process targets
|
||||||
|
if prefs.Process != nil && prefs.Process.Enabled {
|
||||||
|
for _, target := range prefs.Process.Targets {
|
||||||
|
result := dc.callProcess(ctx.Context, content, target, deliveryCtx)
|
||||||
|
results = append(results, result)
|
||||||
|
if !result.Success && lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("process delivery failed: %s", result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEmail sends delivery content to a single email target
|
||||||
|
func (dc *DeliveryCenter) sendEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
content *robottypes.DeliveryContent,
|
||||||
|
target robottypes.EmailTarget,
|
||||||
|
deliveryCtx *robottypes.DeliveryContext,
|
||||||
|
robotInstance *robottypes.Robot,
|
||||||
|
) robottypes.ChannelResult {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Build target identifier from recipients
|
||||||
|
targetID := strings.Join(target.To, ",")
|
||||||
|
if targetID == "" {
|
||||||
|
targetID = "no-recipients"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := robottypes.ChannelResult{
|
||||||
|
Type: robottypes.DeliveryEmail,
|
||||||
|
Target: targetID,
|
||||||
|
SentAt: &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messenger service
|
||||||
|
svc := messenger.Instance
|
||||||
|
if svc == nil {
|
||||||
|
result.Error = "messenger service not available"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build email message
|
||||||
|
msg := &messengerTypes.Message{
|
||||||
|
To: target.To,
|
||||||
|
Subject: buildEmailSubject(target.Subject, target.Template, content, deliveryCtx),
|
||||||
|
Body: buildEmailBody(target.Template, content),
|
||||||
|
Type: messengerTypes.MessageTypeEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set From address from Robot's email (if configured)
|
||||||
|
if robotInstance != nil && robotInstance.RobotEmail != "" {
|
||||||
|
msg.From = robotInstance.RobotEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert attachments
|
||||||
|
attachments := convertAttachments(ctx, content.Attachments)
|
||||||
|
if len(attachments) > 0 {
|
||||||
|
msg.Attachments = attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email using global default channel
|
||||||
|
channel := robottypes.DefaultEmailChannel()
|
||||||
|
err := svc.Send(ctx, channel, msg)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true
|
||||||
|
result.Recipients = target.To
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// postWebhook posts delivery content to a single webhook target
|
||||||
|
func (dc *DeliveryCenter) postWebhook(
|
||||||
|
ctx context.Context,
|
||||||
|
content *robottypes.DeliveryContent,
|
||||||
|
target robottypes.WebhookTarget,
|
||||||
|
deliveryCtx *robottypes.DeliveryContext,
|
||||||
|
) robottypes.ChannelResult {
|
||||||
|
now := time.Now()
|
||||||
|
result := robottypes.ChannelResult{
|
||||||
|
Type: robottypes.DeliveryWebhook,
|
||||||
|
Target: target.URL,
|
||||||
|
SentAt: &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build webhook payload
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"event": "robot.delivery",
|
||||||
|
"timestamp": now.Format(time.RFC3339),
|
||||||
|
"execution_id": deliveryCtx.ExecutionID,
|
||||||
|
"member_id": deliveryCtx.MemberID,
|
||||||
|
"team_id": deliveryCtx.TeamID,
|
||||||
|
"trigger_type": deliveryCtx.TriggerType,
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"summary": content.Summary,
|
||||||
|
"body": content.Body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attachments info (not the actual files)
|
||||||
|
if len(content.Attachments) > 0 {
|
||||||
|
attachmentInfo := make([]map[string]interface{}, 0, len(content.Attachments))
|
||||||
|
for _, att := range content.Attachments {
|
||||||
|
attachmentInfo = append(attachmentInfo, map[string]interface{}{
|
||||||
|
"title": att.Title,
|
||||||
|
"description": att.Description,
|
||||||
|
"task_id": att.TaskID,
|
||||||
|
"file": att.File,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
payload["attachments"] = attachmentInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal payload
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("failed to marshal payload: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request
|
||||||
|
method := target.Method
|
||||||
|
if method == "" {
|
||||||
|
method = "POST"
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, target.URL, bytes.NewReader(payloadBytes))
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("failed to create request: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add custom headers
|
||||||
|
for key, value := range target.Headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add secret if configured (for signature verification)
|
||||||
|
if target.Secret != "" {
|
||||||
|
// TODO: Implement HMAC signature
|
||||||
|
req.Header.Set("X-Webhook-Secret", target.Secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
resp, err := dc.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("request failed: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read response body
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
result.Error = fmt.Sprintf("webhook returned status %d: %s", resp.StatusCode, string(body))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true
|
||||||
|
result.Details = map[string]interface{}{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"response": string(body),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// callProcess calls a Yao Process with delivery content
|
||||||
|
func (dc *DeliveryCenter) callProcess(
|
||||||
|
ctx context.Context,
|
||||||
|
content *robottypes.DeliveryContent,
|
||||||
|
target robottypes.ProcessTarget,
|
||||||
|
deliveryCtx *robottypes.DeliveryContext,
|
||||||
|
) robottypes.ChannelResult {
|
||||||
|
now := time.Now()
|
||||||
|
result := robottypes.ChannelResult{
|
||||||
|
Type: robottypes.DeliveryProcess,
|
||||||
|
Target: target.Process,
|
||||||
|
SentAt: &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build args: DeliveryContent as first arg, then additional args
|
||||||
|
args := make([]interface{}, 0, 1+len(target.Args))
|
||||||
|
args = append(args, map[string]interface{}{
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"summary": content.Summary,
|
||||||
|
"body": content.Body,
|
||||||
|
"attachments": content.Attachments,
|
||||||
|
},
|
||||||
|
"context": map[string]interface{}{
|
||||||
|
"execution_id": deliveryCtx.ExecutionID,
|
||||||
|
"member_id": deliveryCtx.MemberID,
|
||||||
|
"team_id": deliveryCtx.TeamID,
|
||||||
|
"trigger_type": deliveryCtx.TriggerType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
args = append(args, target.Args...)
|
||||||
|
|
||||||
|
// Create and execute process
|
||||||
|
proc, err := process.Of(target.Process, args...)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("failed to create process: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
proc.Context = ctx
|
||||||
|
|
||||||
|
err = proc.Execute()
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true
|
||||||
|
result.Details = proc.Value
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildEmailSubject builds the email subject line
|
||||||
|
func buildEmailSubject(subject, template string, content *robottypes.DeliveryContent, ctx *robottypes.DeliveryContext) string {
|
||||||
|
// Use explicit subject if provided
|
||||||
|
if subject != "" {
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use template-based subject if template is specified
|
||||||
|
// TODO: Implement template rendering
|
||||||
|
if template != "" {
|
||||||
|
return fmt.Sprintf("[Robot] %s", content.Summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: use summary
|
||||||
|
if content.Summary != "" {
|
||||||
|
return fmt.Sprintf("[Robot] %s", content.Summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[Robot] Execution %s Complete", ctx.ExecutionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildEmailBody builds the email body content
|
||||||
|
func buildEmailBody(template string, content *robottypes.DeliveryContent) string {
|
||||||
|
// TODO: Implement template rendering
|
||||||
|
// For now, just use the body directly
|
||||||
|
if content.Body != "" {
|
||||||
|
return content.Body
|
||||||
|
}
|
||||||
|
return content.Summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertAttachments converts DeliveryAttachment to messenger Attachment format
|
||||||
|
func convertAttachments(ctx context.Context, attachments []robottypes.DeliveryAttachment) []messengerTypes.Attachment {
|
||||||
|
if len(attachments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]messengerTypes.Attachment, 0, len(attachments))
|
||||||
|
|
||||||
|
for _, att := range attachments {
|
||||||
|
// Parse file wrapper: __<uploader>://<fileID>
|
||||||
|
uploader, fileID, isWrapper := attachment.Parse(att.File)
|
||||||
|
if !isWrapper {
|
||||||
|
// Skip non-wrapper attachments
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file info from attachment manager
|
||||||
|
manager, ok := attachment.Managers[uploader]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := manager.Info(ctx, fileID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
content, err := manager.Read(ctx, fileID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build messenger attachment
|
||||||
|
msgAtt := messengerTypes.Attachment{
|
||||||
|
Filename: info.Filename,
|
||||||
|
ContentType: info.ContentType,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, msgAtt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
1174
agent/robot/executor/standard/delivery_test.go
Normal file
1174
agent/robot/executor/standard/delivery_test.go
Normal file
File diff suppressed because it is too large
Load diff
35
agent/robot/types/config_global.go
Normal file
35
agent/robot/types/config_global.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// Global configuration for robot agent
|
||||||
|
// These values can be set during agent initialization
|
||||||
|
|
||||||
|
var (
|
||||||
|
// defaultEmailChannel - default messenger channel name for sending emails
|
||||||
|
// Can be configured via SetDefaultEmailChannel()
|
||||||
|
// Default: "email" (maps to messengers/channels.yao configuration)
|
||||||
|
defaultEmailChannel = "email"
|
||||||
|
|
||||||
|
// configMu protects global configuration
|
||||||
|
configMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultEmailChannel returns the default email channel name
|
||||||
|
func DefaultEmailChannel() string {
|
||||||
|
configMu.RLock()
|
||||||
|
defer configMu.RUnlock()
|
||||||
|
return defaultEmailChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultEmailChannel sets the default messenger channel for email delivery
|
||||||
|
// This should be called during agent initialization
|
||||||
|
// The channel name must match a channel defined in messengers/channels.yao
|
||||||
|
func SetDefaultEmailChannel(channel string) {
|
||||||
|
if channel == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configMu.Lock()
|
||||||
|
defer configMu.Unlock()
|
||||||
|
defaultEmailChannel = channel
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ type Robot struct {
|
||||||
SystemPrompt string `json:"system_prompt"`
|
SystemPrompt string `json:"system_prompt"`
|
||||||
Status RobotStatus `json:"robot_status"`
|
Status RobotStatus `json:"robot_status"`
|
||||||
AutonomousMode bool `json:"autonomous_mode"`
|
AutonomousMode bool `json:"autonomous_mode"`
|
||||||
|
RobotEmail string `json:"robot_email"` // Robot's email address for sending emails
|
||||||
|
|
||||||
// Parsed config (from robot_config JSON field)
|
// Parsed config (from robot_config JSON field)
|
||||||
Config *Config `json:"-"`
|
Config *Config `json:"-"`
|
||||||
|
|
@ -317,7 +318,6 @@ type EmailPreference struct {
|
||||||
// EmailTarget - Single email target
|
// EmailTarget - Single email target
|
||||||
type EmailTarget struct {
|
type EmailTarget struct {
|
||||||
To []string `json:"to"` // Recipient addresses
|
To []string `json:"to"` // Recipient addresses
|
||||||
CC []string `json:"cc,omitempty"` // CC addresses
|
|
||||||
Template string `json:"template,omitempty"` // Email template ID
|
Template string `json:"template,omitempty"` // Email template ID
|
||||||
Subject string `json:"subject,omitempty"` // Subject template
|
Subject string `json:"subject,omitempty"` // Subject template
|
||||||
}
|
}
|
||||||
|
|
@ -383,6 +383,7 @@ func NewRobotFromMap(m map[string]interface{}) (*Robot, error) {
|
||||||
DisplayName: getString(m, "display_name"),
|
DisplayName: getString(m, "display_name"),
|
||||||
SystemPrompt: getString(m, "system_prompt"),
|
SystemPrompt: getString(m, "system_prompt"),
|
||||||
AutonomousMode: getBool(m, "autonomous_mode"),
|
AutonomousMode: getBool(m, "autonomous_mode"),
|
||||||
|
RobotEmail: getString(m, "robot_email"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse robot_status
|
// Parse robot_status
|
||||||
|
|
|
||||||
|
|
@ -648,7 +648,6 @@ func TestDeliveryPreferencesStructure(t *testing.T) {
|
||||||
Targets: []types.EmailTarget{
|
Targets: []types.EmailTarget{
|
||||||
{
|
{
|
||||||
To: []string{"team@example.com"},
|
To: []string{"team@example.com"},
|
||||||
CC: []string{"manager@example.com"},
|
|
||||||
Template: "weekly-report",
|
Template: "weekly-report",
|
||||||
Subject: "Weekly Report - {{.Date}}",
|
Subject: "Weekly Report - {{.Date}}",
|
||||||
},
|
},
|
||||||
|
|
@ -686,7 +685,6 @@ func TestDeliveryPreferencesStructure(t *testing.T) {
|
||||||
assert.Len(t, prefs.Email.Targets, 2)
|
assert.Len(t, prefs.Email.Targets, 2)
|
||||||
assert.Equal(t, "weekly-report", prefs.Email.Targets[0].Template)
|
assert.Equal(t, "weekly-report", prefs.Email.Targets[0].Template)
|
||||||
assert.Len(t, prefs.Email.Targets[0].To, 1)
|
assert.Len(t, prefs.Email.Targets[0].To, 1)
|
||||||
assert.Len(t, prefs.Email.Targets[0].CC, 1)
|
|
||||||
|
|
||||||
// Webhook
|
// Webhook
|
||||||
assert.True(t, prefs.Webhook.Enabled)
|
assert.True(t, prefs.Webhook.Enabled)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue