yao/openapi/agent/robot/interact.go
Max bc4787f857 Update executor to support V2 execution model and enhance event handling
- Implement V2 execution model in the standard executor, simplifying task execution to a single call without validation loops.
- Introduce support for resuming suspended executions, allowing for human input during task processing.
- Enhance event handling by pushing task completion and failure events to the event bus for better tracking and integration.
- Update tests to reflect changes in execution flow and ensure robust handling of task statuses and results.
2026-02-25 18:40:48 +08:00

282 lines
8.8 KiB
Go

package robot
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/log"
robotapi "github.com/yaoapp/yao/agent/robot/api"
robottypes "github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/openapi/oauth/authorized"
"github.com/yaoapp/yao/openapi/response"
)
// InteractRequest - HTTP request for unified robot interaction
type InteractRequest struct {
ExecutionID string `json:"execution_id,omitempty"`
TaskID string `json:"task_id,omitempty"`
Source string `json:"source,omitempty"`
Message string `json:"message" binding:"required"`
Action string `json:"action,omitempty"`
}
// InteractResponse - HTTP response for interaction
type InteractResponse struct {
ExecutionID string `json:"execution_id,omitempty"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
ChatID string `json:"chat_id,omitempty"`
Reply string `json:"reply,omitempty"`
WaitForMore bool `json:"wait_for_more,omitempty"`
}
// ReplyRequest - HTTP request for replying to a waiting task
type ReplyRequest struct {
Message string `json:"message" binding:"required"`
}
// ConfirmRequest - HTTP request for confirming an execution
type ConfirmRequest struct {
Message string `json:"message,omitempty"`
}
// InteractRobot handles unified robot interaction
// POST /v1/agent/robots/:id/interact
func InteractRobot(c *gin.Context) {
authInfo := authorized.GetInfo(c)
if authInfo == nil || (authInfo.Subject == "" && authInfo.UserID == "") {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidToken.Code,
ErrorDescription: "Authentication required",
}
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
return
}
robotID := c.Param("id")
if robotID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "robot id is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
var req InteractRequest
if err := c.ShouldBindJSON(&req); err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid request body: " + err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
ctx := robottypes.NewContext(c.Request.Context(), authInfo)
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
if err != nil {
if errors.Is(err, robottypes.ErrRobotNotFound) {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Robot not found: " + robotID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get robot: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
errorResp := &response.ErrorResponse{
Code: response.ErrAccessDenied.Code,
ErrorDescription: "Forbidden: No permission to interact with this robot",
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
apiReq := &robotapi.InteractRequest{
ExecutionID: req.ExecutionID,
TaskID: req.TaskID,
Source: robottypes.InteractSource(req.Source),
Message: req.Message,
Action: req.Action,
}
result, err := robotapi.Interact(ctx, robotID, apiReq)
if err != nil {
log.Error("Failed to interact with robot %s: %v", robotID, err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to interact: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
resp := &InteractResponse{
ExecutionID: result.ExecutionID,
Status: result.Status,
Message: result.Message,
ChatID: result.ChatID,
Reply: result.Reply,
WaitForMore: result.WaitForMore,
}
response.RespondWithSuccess(c, response.StatusOK, resp)
}
// ReplyToTask handles replying to a specific waiting task
// POST /v1/agent/robots/:id/executions/:exec_id/tasks/:task_id/reply
func ReplyToTask(c *gin.Context) {
authInfo := authorized.GetInfo(c)
if authInfo == nil || (authInfo.Subject == "" && authInfo.UserID == "") {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidToken.Code,
ErrorDescription: "Authentication required",
}
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
return
}
robotID := c.Param("id")
execID := c.Param("exec_id")
taskID := c.Param("task_id")
if robotID == "" || execID == "" || taskID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "robot id, execution id, and task id are required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
var req ReplyRequest
if err := c.ShouldBindJSON(&req); err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid request body: " + err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
ctx := robottypes.NewContext(c.Request.Context(), authInfo)
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
if err != nil {
handleRobotError(c, robotID, err)
return
}
if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
errorResp := &response.ErrorResponse{
Code: response.ErrAccessDenied.Code,
ErrorDescription: "Forbidden",
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
result, err := robotapi.Reply(ctx, robotID, execID, taskID, req.Message)
if err != nil {
log.Error("Failed to reply to task: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to reply: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
resp := &InteractResponse{
ExecutionID: result.ExecutionID,
Status: result.Status,
Message: result.Message,
}
response.RespondWithSuccess(c, response.StatusOK, resp)
}
// ConfirmExecution handles confirming a pending execution
// POST /v1/agent/robots/:id/executions/:exec_id/confirm
func ConfirmExecution(c *gin.Context) {
authInfo := authorized.GetInfo(c)
if authInfo == nil || (authInfo.Subject == "" && authInfo.UserID == "") {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidToken.Code,
ErrorDescription: "Authentication required",
}
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
return
}
robotID := c.Param("id")
execID := c.Param("exec_id")
if robotID == "" || execID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "robot id and execution id are required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
var req ConfirmRequest
if err := c.ShouldBindJSON(&req); err != nil {
// Allow empty body for confirm
req = ConfirmRequest{}
}
ctx := robottypes.NewContext(c.Request.Context(), authInfo)
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
if err != nil {
handleRobotError(c, robotID, err)
return
}
if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
errorResp := &response.ErrorResponse{
Code: response.ErrAccessDenied.Code,
ErrorDescription: "Forbidden",
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
result, err := robotapi.Confirm(ctx, robotID, execID, req.Message)
if err != nil {
log.Error("Failed to confirm execution: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to confirm: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
resp := &InteractResponse{
ExecutionID: result.ExecutionID,
Status: result.Status,
Message: result.Message,
}
response.RespondWithSuccess(c, response.StatusOK, resp)
}
func handleRobotError(c *gin.Context, robotID string, err error) {
if errors.Is(err, robottypes.ErrRobotNotFound) {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Robot not found: " + robotID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get robot: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
}