yao/agent/robot/events/message.go
Max 421d946971 feat(buffer): refine step retrieval for resume functionality
- Updated the GetStepsForResume method to return only steps with valid resume statuses (failed or interrupted), improving recovery context handling.
- Enhanced error handling in message processing across various integrations, including friendly error messages for users in different locales.
- Modified file handling in message integrations (Dingtalk, Discord, Feishu, Telegram, Weixin) to differentiate between image and file types, ensuring appropriate content delivery.
2026-05-08 10:14:26 +08:00

257 lines
7.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package events
import (
"context"
"fmt"
"strings"
agent "github.com/yaoapp/yao/agent"
"github.com/yaoapp/yao/agent/assistant"
agentcontext "github.com/yaoapp/yao/agent/context"
robotstore "github.com/yaoapp/yao/agent/robot/store"
robottypes "github.com/yaoapp/yao/agent/robot/types"
eventtypes "github.com/yaoapp/yao/event/types"
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
)
// handleMessage processes messages from external integrations (Telegram, etc.).
// It calls the Host Agent with the provided messages and returns a MessageResult.
// Action detection is done via the Host Agent's Next hook return value.
func (h *robotHandler) handleMessage(ctx context.Context, ev *eventtypes.Event, resp chan<- eventtypes.Result) {
var payload MessagePayload
if err := ev.Should(&payload); err != nil {
log.Error("message handler: invalid payload: %v", err)
if ev.IsCall {
resp <- eventtypes.Result{Err: err}
}
return
}
log.Info("message handler: robot=%s channel=%s msg_id=%s",
payload.RobotID, payload.Metadata.Channel, payload.Metadata.MessageID)
result, err := callHostAgent(ctx, &payload)
if err != nil {
log.Error("message handler: host agent call failed robot=%s: %v", payload.RobotID, err)
if reply := getReplyFunc(); reply != nil {
errMsg := friendlyErrorMessage(payload.Metadata.Locale)
_ = reply(ctx, &agentcontext.Message{
Role: agentcontext.RoleAssistant,
Content: errMsg,
}, payload.Metadata)
}
if ev.IsCall {
resp <- eventtypes.Result{Err: err}
}
return
}
if reply := getReplyFunc(); reply != nil && result.Message != nil {
if err := reply(ctx, result.Message, payload.Metadata); err != nil {
log.Error("message handler: reply failed robot=%s channel=%s: %v",
payload.RobotID, payload.Metadata.Channel, err)
}
}
if ev.IsCall {
resp <- eventtypes.Result{Data: result}
}
}
// callHostAgent resolves the Host Agent for the robot and calls it with messages.
// This avoids importing executor/standard to prevent import cycles; instead it
// calls the assistant directly via assistant.Get + ast.Stream (same as AgentCaller.Call).
func callHostAgent(ctx context.Context, payload *MessagePayload) (*MessageResult, error) {
hostID, record, err := resolveHostAssistantID(ctx, payload.RobotID)
if err != nil {
return nil, fmt.Errorf("failed to resolve host agent: %w", err)
}
ast, err := assistant.Get(hostID)
if err != nil {
return nil, fmt.Errorf("assistant not found: %s: %w", hostID, err)
}
opts := &agentcontext.Options{
Skip: &agentcontext.Skip{
Search: false,
},
}
authorized := &oauthtypes.AuthorizedInfo{
UserID: payload.Metadata.SenderID,
TeamID: record.TeamID,
}
chatID := fmt.Sprintf("%s:%s", payload.Metadata.Channel, payload.Metadata.ChatID)
agentCtx := agentcontext.New(ctx, authorized, chatID)
agentCtx.AssistantID = hostID
agentCtx.Referer = "integration"
agentCtx.Locale = payload.Metadata.Locale
agentCtx.Metadata = map[string]interface{}{
"robot_id": payload.RobotID,
"channel": payload.Metadata.Channel,
}
if dsl := agent.GetAgent(); dsl != nil {
if cache, err := dsl.GetCacheStore(); err == nil {
agentCtx.Cache = cache
}
}
defer agentCtx.Release()
response, err := ast.Stream(agentCtx, payload.Messages, opts)
if err != nil {
return nil, fmt.Errorf("host agent call failed: %w", err)
}
result := &MessageResult{
Metadata: payload.Metadata,
}
if response.Completion != nil {
result.Message = &agentcontext.Message{
Role: agentcontext.RoleAssistant,
Content: response.Completion.Content,
}
}
// Detect action from Next hook return value
log.Debug("response.Next type=%T value=%+v", response.Next, response.Next)
if action := detectAction(response.Next); action != nil {
log.Info("action detected: name=%s payload=%+v", action.Name, action.Payload)
result.Action = action
if action.Name == "robot.execute" {
if execID := executeAction(ctx, payload, record, action); execID != "" {
result.ExecutionID = execID
result.Message = &agentcontext.Message{
Role: agentcontext.RoleAssistant,
Content: taskDeployedMessage(execID, payload.Metadata.Locale),
}
}
}
} else {
log.Debug("no action detected from response.Next")
}
return result, nil
}
// executeAction triggers robot execution when the Host Agent returns a
// confirmed action. Uses the injected TriggerFunc to call robotapi.TriggerManual
// without creating a circular import.
func executeAction(ctx context.Context, payload *MessagePayload, record *robotstore.RobotRecord, action *ActionResult) string {
trigger := getTriggerFunc()
if trigger == nil {
log.Warn("message handler: trigger func not registered, cannot execute action for robot=%s", payload.RobotID)
return ""
}
data, _ := action.Payload.(map[string]interface{})
goals, _ := data["goals"].(string)
if goals == "" {
log.Warn("message handler: confirmed action has no goals, robot=%s", payload.RobotID)
return ""
}
triggerData := &robottypes.TriggerInput{
Data: map[string]interface{}{
"goals": goals,
"channel": payload.Metadata.Channel,
"chat_id": payload.Metadata.ChatID,
"extra": payload.Metadata.Extra,
},
}
authorized := &oauthtypes.AuthorizedInfo{
UserID: record.MemberID,
TeamID: record.TeamID,
}
rCtx := robottypes.NewContext(ctx, authorized)
execID, accepted, err := trigger(rCtx, payload.RobotID, robottypes.TriggerHuman, triggerData)
if err != nil {
log.Error("message handler: execute action failed robot=%s: %v", payload.RobotID, err)
return ""
}
if !accepted {
log.Warn("message handler: execute action not accepted robot=%s", payload.RobotID)
return ""
}
log.Info("message handler: execution triggered robot=%s exec_id=%s", payload.RobotID, execID)
return execID
}
// detectAction checks the Next hook return value for a confirmed action.
// The Host Agent returns { data: { confirmed: true, robot_id: "...", goals: "..." } }
// when it detects a confirm_task tool call.
func detectAction(next interface{}) *ActionResult {
if next == nil {
return nil
}
m, ok := next.(map[string]interface{})
if !ok {
return nil
}
// Next hook may return { data: { confirmed, ... } } or flat { confirmed, ... }
data, _ := m["data"].(map[string]interface{})
if data == nil {
data = m
}
confirmed, _ := data["confirmed"].(bool)
if !confirmed {
return nil
}
return &ActionResult{
Name: "robot.execute",
Payload: data,
}
}
// resolveHostAssistantID resolves the host assistant ID from a robot member ID.
// Mirrors the logic in openapi/agent/robot/completions.go.
func resolveHostAssistantID(ctx context.Context, memberID string) (string, *robotstore.RobotRecord, error) {
store := robotstore.NewRobotStore()
record, err := store.Get(ctx, memberID)
if err != nil {
return "", nil, fmt.Errorf("failed to get robot: %w", err)
}
if record == nil {
return "", nil, fmt.Errorf("robot not found: %s", memberID)
}
config, err := robottypes.ParseConfig(record.RobotConfig)
if err != nil {
return "", nil, fmt.Errorf("failed to parse robot config: %w", err)
}
var hostID string
if config != nil && config.Resources != nil {
hostID = config.Resources.GetPhaseAgent(robottypes.PhaseHost)
} else {
hostID = "__yao." + string(robottypes.PhaseHost)
}
return hostID, record, nil
}
func friendlyErrorMessage(locale string) string {
if strings.HasPrefix(locale, "zh") {
return "抱歉,处理您的消息时出现了问题,请稍后重试。"
}
return "Sorry, there was a problem processing your message. Please try again later."
}
func taskDeployedMessage(execID string, locale string) string {
if strings.HasPrefix(locale, "zh") {
return fmt.Sprintf("任务已部署(执行编号: %s完成后会将结果发送给你。", execID)
}
return fmt.Sprintf("Task deployed (execution: %s). You will receive results once completed.", execID)
}