扣子智能体
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1628 lines
48 KiB

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package workflow
import (
"context"
"errors"
"fmt"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/cloudwego/eino/schema"
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
"github.com/coze-dev/coze-studio/backend/api/model/workflow"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
crossagentrun "github.com/coze-dev/coze-studio/backend/crossdomain/contract/agentrun"
crossconversation "github.com/coze-dev/coze-studio/backend/crossdomain/contract/conversation"
crossmessage "github.com/coze-dev/coze-studio/backend/crossdomain/contract/message"
crossupload "github.com/coze-dev/coze-studio/backend/crossdomain/contract/upload"
agententity "github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity"
"github.com/coze-dev/coze-studio/backend/domain/upload/service"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/maps"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/safego"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/pkg/taskgroup"
"github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
const (
userRole = "user"
assistantRole = "assistant"
cardTemplate = `
{
"elements": {
"root": {
"id": "root",
"name": "Root",
"type": "@flowpd/cici-components/PageContainer",
"props": {
"backgroundColor": "grey",
"containerPadding": 16,
"containerRowGap": 12
},
"children": [
"OpfZnYNHby",
"70zV0Jp5vy"
],
"directives": {
}
},
"OpfZnYNHby": {
"id": "OpfZnYNHby",
"name": "FlowpdCiciComponentsColumnLayout",
"type": "@flowpd/cici-components/ColumnLayout",
"props": {
"backgroundColor": "transparent",
"layoutColumnGap": 4,
"layoutPaddingGap": 2,
"borderRadius": 0,
"enableClickEvent": false,
"action": "enableUrl",
"Columns": [
{
"type": "slot",
"children": [
"KPa0BqoODo"
],
"config": {
"width": "weighted",
"weight": 1,
"vertical": "top",
"horizontal": "left",
"columnElementGap": 4,
"columnElementPadding": 2,
"enableClickEvent": false
}
}
]
},
"children": [
],
"directives": {
"repeat": {
"type": "expression",
"value": "{{5fJt3qKpSz}}",
"replaceMap": {
"5fJt3qKpSz": "list"
}
}
}
},
"KPa0BqoODo": {
"id": "KPa0BqoODo",
"name": "FlowpdCiciComponentsInput",
"type": "@flowpd/cici-components/Input",
"props": {
"enableLabel": true,
"label": {
"type": "expression",
"value": "{{item.name}}"
},
"placeholder": "Please enter content.",
"maxLengthEnabled": false,
"maxLength": 140,
"required": false,
"enableSendIcon": true,
"actionType": "enableMessage",
"disableAfterAction": true,
"message": {
"type": "expression",
"value": "{{KPa0BqoODo_value}}"
}
},
"children": [
],
"directives": {
}
},
"70zV0Jp5vy": {
"id": "70zV0Jp5vy",
"name": "FlowpdCiciComponentsColumnLayout",
"type": "@flowpd/cici-components/ColumnLayout",
"props": {
"backgroundColor": "transparent",
"layoutColumnGap": 4,
"layoutPaddingGap": 2,
"borderRadius": 0,
"enableClickEvent": false,
"action": "enableUrl",
"Columns": [
{
"type": "slot",
"children": [
"mH5BNaFTl1"
],
"config": {
"width": "weighted",
"weight": 1,
"vertical": "top",
"horizontal": "right",
"columnElementGap": 4,
"columnElementPadding": 2,
"enableClickEvent": false
}
}
]
},
"children": [
],
"directives": {
}
},
"mH5BNaFTl1": {
"id": "mH5BNaFTl1",
"name": "FlowpdCiciComponentsButton",
"type": "@flowpd/cici-components/Button",
"props": {
"content": "Button",
"type": "primary",
"size": "small",
"width": "hug",
"widthPx": 160,
"textAlign": "center",
"enableLines": false,
"lines": 1,
"positionStyle": {
"type": "default"
},
"actionType": "enableMessage",
"disableAfterAction": true,
"message": {
"type": "expression",
"value": "{{KPa0BqoODo_value}}"
}
},
"children": [
],
"directives": {
}
}
},
"rootID": "root",
"variables": {
"5fJt3qKpSz": {
"id": "5fJt3qKpSz",
"name": "list",
"defaultValue": [
]
}
},
"actions": {
}
}`
)
type inputCard struct {
Elements any `json:"elements"`
RootID string `json:"rootID"`
Variables map[string]any `json:"variables"`
}
func defaultCard() *inputCard {
card := &inputCard{}
_ = sonic.UnmarshalString(cardTemplate, card)
return card
}
func (w *ApplicationService) CreateApplicationConversationDef(ctx context.Context, req *workflow.CreateProjectConversationDefRequest) (resp *workflow.CreateProjectConversationDefResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrConversationOfAppOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
var (
spaceID = mustParseInt64(req.GetSpaceID())
appID = mustParseInt64(req.GetProjectID())
userID = ctxutil.MustGetUIDFromCtx(ctx)
)
if err := checkUserSpace(ctx, userID, spaceID); err != nil {
return nil, err
}
uniqueID, err := GetWorkflowDomainSVC().CreateDraftConversationTemplate(ctx, &vo.CreateConversationTemplateMeta{
AppID: appID,
SpaceID: spaceID,
Name: req.GetConversationName(),
UserID: userID,
})
if err != nil {
return nil, err
}
return &workflow.CreateProjectConversationDefResponse{
UniqueID: strconv.FormatInt(uniqueID, 10),
SpaceID: req.GetSpaceID(),
}, err
}
func (w *ApplicationService) UpdateApplicationConversationDef(ctx context.Context, req *workflow.UpdateProjectConversationDefRequest) (resp *workflow.UpdateProjectConversationDefResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrConversationOfAppOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
var (
spaceID = mustParseInt64(req.GetSpaceID())
templateID = mustParseInt64(req.GetUniqueID())
appID = mustParseInt64(req.GetProjectID())
userID = ctxutil.MustGetUIDFromCtx(ctx)
)
if err := checkUserSpace(ctx, userID, spaceID); err != nil {
return nil, err
}
err = GetWorkflowDomainSVC().UpdateDraftConversationTemplateName(ctx, appID, userID, templateID, req.GetConversationName())
if err != nil {
return nil, err
}
return &workflow.UpdateProjectConversationDefResponse{}, err
}
func (w *ApplicationService) DeleteApplicationConversationDef(ctx context.Context, req *workflow.DeleteProjectConversationDefRequest) (resp *workflow.DeleteProjectConversationDefResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrConversationOfAppOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
var (
appID = mustParseInt64(req.GetProjectID())
templateID = mustParseInt64(req.GetUniqueID())
)
if err := checkUserSpace(ctx, ctxutil.MustGetUIDFromCtx(ctx), mustParseInt64(req.GetSpaceID())); err != nil {
return nil, err
}
if req.GetCheckOnly() {
wfs, err := GetWorkflowDomainSVC().CheckWorkflowsToReplace(ctx, appID, templateID)
if err != nil {
return nil, err
}
resp = &workflow.DeleteProjectConversationDefResponse{NeedReplace: make([]*workflow.Workflow, 0)}
for _, wf := range wfs {
resp.NeedReplace = append(resp.NeedReplace, &workflow.Workflow{
Name: wf.Name,
URL: wf.IconURL,
WorkflowID: strconv.FormatInt(wf.ID, 10),
})
}
return resp, nil
}
wfID2ConversationName, err := maps.TransformKeyWithErrorCheck(req.GetReplace(), func(k1 string) (int64, error) {
return strconv.ParseInt(k1, 10, 64)
})
rowsAffected, err := GetWorkflowDomainSVC().DeleteDraftConversationTemplate(ctx, templateID, wfID2ConversationName)
if err != nil {
return nil, err
}
if rowsAffected > 0 {
return &workflow.DeleteProjectConversationDefResponse{
Success: true,
}, err
}
rowsAffected, err = GetWorkflowDomainSVC().DeleteDynamicConversation(ctx, vo.Draft, templateID)
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, fmt.Errorf("delete conversation failed")
}
return &workflow.DeleteProjectConversationDefResponse{
Success: true,
}, nil
}
func (w *ApplicationService) ListApplicationConversationDef(ctx context.Context, req *workflow.ListProjectConversationRequest) (resp *workflow.ListProjectConversationResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrConversationOfAppOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
var connectorID int64
if len(req.GetConnectorID()) != 0 {
connectorID = mustParseInt64(req.GetConnectorID())
} else {
connectorID = consts.CozeConnectorID
}
var (
page = mustParseInt64(ternary.IFElse(req.GetCursor() == "", "0", req.GetCursor()))
size = req.GetLimit()
userID = ctxutil.MustGetUIDFromCtx(ctx)
spaceID = mustParseInt64(req.GetSpaceID())
appID = mustParseInt64(req.GetProjectID())
version = req.ProjectVersion
listConversationMeta = vo.ListConversationMeta{
APPID: appID,
UserID: userID,
ConnectorID: connectorID,
}
)
if err := checkUserSpace(ctx, userID, spaceID); err != nil {
return nil, err
}
env := ternary.IFElse(req.GetCreateEnv() == workflow.CreateEnv_Draft, vo.Draft, vo.Online)
if req.GetCreateMethod() == workflow.CreateMethod_ManualCreate {
templates, err := GetWorkflowDomainSVC().ListConversationTemplate(ctx, env, &vo.ListConversationTemplatePolicy{
AppID: appID,
Page: &vo.Page{
Page: int32(page),
Size: int32(size),
},
NameLike: ternary.IFElse(len(req.GetNameLike()) == 0, nil, ptr.Of(req.GetNameLike())),
Version: version,
})
if err != nil {
return nil, err
}
stsConversations, err := GetWorkflowDomainSVC().MGetStaticConversation(ctx, env, userID, connectorID, slices.Transform(templates, func(a *entity.ConversationTemplate) int64 {
return a.TemplateID
}))
if err != nil {
return nil, err
}
stsConversationMap := slices.ToMap(stsConversations, func(e *entity.StaticConversation) (int64, *entity.StaticConversation) {
return e.TemplateID, e
})
resp = &workflow.ListProjectConversationResponse{Data: make([]*workflow.ProjectConversation, 0)}
for _, tmpl := range templates {
conversationID := ""
if c, ok := stsConversationMap[tmpl.TemplateID]; ok {
conversationID = strconv.FormatInt(c.ConversationID, 10)
}
resp.Data = append(resp.Data, &workflow.ProjectConversation{
UniqueID: strconv.FormatInt(tmpl.TemplateID, 10),
ConversationName: tmpl.Name,
ConversationID: conversationID,
})
}
}
if req.GetCreateMethod() == workflow.CreateMethod_NodeCreate {
dyConversations, err := GetWorkflowDomainSVC().ListDynamicConversation(ctx, env, &vo.ListConversationPolicy{
ListConversationMeta: listConversationMeta,
Page: &vo.Page{
Page: int32(page),
Size: int32(size),
},
NameLike: ternary.IFElse(len(req.GetNameLike()) == 0, nil, ptr.Of(req.GetNameLike())),
})
if err != nil {
return nil, err
}
resp = &workflow.ListProjectConversationResponse{Data: make([]*workflow.ProjectConversation, 0, len(dyConversations))}
resp.Data = append(resp.Data, slices.Transform(dyConversations, func(a *entity.DynamicConversation) *workflow.ProjectConversation {
return &workflow.ProjectConversation{
UniqueID: strconv.FormatInt(a.ID, 10),
ConversationName: a.Name,
ConversationID: strconv.FormatInt(a.ConversationID, 10),
}
})...)
}
return resp, nil
}
func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workflow.ChatFlowRunRequest) (
_ *schema.StreamReader[[]*workflow.ChatFlowRunResponse], err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrWorkflowOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
if len(req.GetAdditionalMessages()) == 0 {
return nil, fmt.Errorf("additional_messages is requird")
}
messages := req.GetAdditionalMessages()
lastUserMessage := messages[len(req.GetAdditionalMessages())-1]
if lastUserMessage.Role != userRole {
return nil, errors.New("the role of the last day message must be user")
}
var parameters = make(map[string]any)
if len(req.GetParameters()) > 0 {
err := sonic.UnmarshalString(req.GetParameters(), &parameters)
if err != nil {
return nil, err
}
}
var (
workflowID = mustParseInt64(req.GetWorkflowID())
isDebug = req.GetExecuteMode() == "DEBUG"
appID, agentID *int64
bizID int64
conversationID int64
sectionID int64
version string
locator workflowModel.Locator
apiKeyInfo = ctxutil.GetApiAuthFromCtx(ctx)
userID = apiKeyInfo.UserID
connectorID int64
)
if len(req.GetConnectorID()) == 0 {
connectorID = ternary.IFElse(isDebug, consts.CozeConnectorID, apiKeyInfo.ConnectorID)
} else {
connectorID = mustParseInt64(req.GetConnectorID())
}
if req.IsSetAppID() {
appID = ptr.Of(mustParseInt64(req.GetAppID()))
bizID = mustParseInt64(req.GetAppID())
}
if req.IsSetBotID() {
agentID = ptr.Of(mustParseInt64(req.GetBotID()))
bizID = mustParseInt64(req.GetBotID())
}
if appID != nil && agentID != nil {
return nil, errors.New("project_id and bot_id cannot be set at the same time")
}
if isDebug {
locator = workflowModel.FromDraft
} else {
meta, err := GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: workflowID,
MetaOnly: true,
})
if err != nil {
return nil, err
}
if meta.LatestPublishedVersion == nil {
return nil, vo.NewError(errno.ErrWorkflowNotPublished)
}
if req.IsSetVersion() {
version = req.GetVersion()
locator = workflowModel.FromSpecificVersion
} else {
version = meta.GetLatestVersion()
locator = workflowModel.FromLatestVersion
}
}
if req.IsSetConversationID() && !req.IsSetBotID() {
conversationID = mustParseInt64(req.GetConversationID())
cInfo, err := crossconversation.DefaultSVC().GetByID(ctx, conversationID)
if err != nil {
return nil, err
}
sectionID = cInfo.SectionID
// only trust the conversation name under the app
conversationName, existed, err := GetWorkflowDomainSVC().GetConversationNameByID(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), bizID, connectorID, conversationID)
if err != nil {
return nil, err
}
if !existed {
return nil, fmt.Errorf("conversation not found")
}
parameters[vo.ConversationNameKey] = conversationName
} else if req.IsSetConversationID() && req.IsSetBotID() {
parameters[vo.ConversationNameKey] = "Default"
conversationID = mustParseInt64(req.GetConversationID())
cInfo, err := crossconversation.DefaultSVC().GetByID(ctx, conversationID)
if err != nil {
return nil, err
}
sectionID = cInfo.SectionID
} else {
conversationName, ok := parameters[vo.ConversationNameKey].(string)
if !ok {
return nil, fmt.Errorf("conversation name is requried")
}
cID, sID, err := GetWorkflowDomainSVC().GetOrCreateConversation(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), bizID, connectorID, userID, conversationName)
if err != nil {
return nil, err
}
conversationID = cID
sectionID = sID
}
runRecord, err := crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{
AgentID: bizID,
ConversationID: conversationID,
UserID: strconv.FormatInt(userID, 10),
ConnectorID: connectorID,
SectionID: sectionID,
})
if err != nil {
return nil, err
}
roundID := runRecord.ID
userMessage, err := toConversationMessage(ctx, bizID, conversationID, userID, roundID, sectionID, message.MessageTypeQuestion, lastUserMessage)
if err != nil {
return nil, err
}
messageClient := crossmessage.DefaultSVC()
_, err = messageClient.Create(ctx, userMessage)
if err != nil {
return nil, err
}
info, existed, unbinding, err := GetWorkflowDomainSVC().GetConvRelatedInfo(ctx, conversationID)
if err != nil {
return nil, err
}
userSchemaMessage, err := toSchemaMessage(ctx, lastUserMessage)
if err != nil {
return nil, err
}
if existed {
sr, err := GetWorkflowDomainSVC().StreamResume(ctx, &entity.ResumeRequest{
EventID: info.EventID,
ExecuteID: info.ExecID,
ResumeData: lastUserMessage.Content,
}, workflowModel.ExecuteConfig{
Operator: userID,
Mode: ternary.IFElse(isDebug, workflowModel.ExecuteModeDebug, workflowModel.ExecuteModeRelease),
ConnectorID: connectorID,
ConnectorUID: strconv.FormatInt(userID, 10),
BizType: workflowModel.BizTypeWorkflow,
})
if err != nil {
unErr := unbinding()
if unErr != nil {
logs.CtxErrorf(ctx, "unbinding failed, error: %v", unErr)
}
return nil, err
}
return schema.StreamReaderWithConvert(sr, w.convertToChatFlowRunResponseList(ctx, convertToChatFlowInfo{
bizID: bizID,
conversationID: conversationID,
roundID: roundID,
workflowID: workflowID,
sectionID: sectionID,
unbinding: unbinding,
userMessage: userSchemaMessage,
suggestReplyInfo: req.GetSuggestReplyInfo(),
})), nil
}
exeCfg := workflowModel.ExecuteConfig{
ID: mustParseInt64(req.GetWorkflowID()),
From: locator,
Version: version,
Operator: userID,
Mode: ternary.IFElse(isDebug, workflowModel.ExecuteModeDebug, workflowModel.ExecuteModeRelease),
AppID: appID,
AgentID: agentID,
ConnectorID: connectorID,
ConnectorUID: strconv.FormatInt(userID, 10),
TaskType: workflowModel.TaskTypeForeground,
SyncPattern: workflowModel.SyncPatternStream,
InputFailFast: true,
BizType: workflowModel.BizTypeWorkflow,
ConversationID: ptr.Of(conversationID),
RoundID: ptr.Of(roundID),
InitRoundID: ptr.Of(roundID),
SectionID: ptr.Of(sectionID),
UserMessage: userSchemaMessage,
Cancellable: isDebug,
}
historyMessages, err := makeChatFlowHistoryMessages(ctx, bizID, conversationID, userID, sectionID, connectorID, messages[:len(req.GetAdditionalMessages())-1])
if err != nil {
return nil, err
}
if len(historyMessages) > 0 {
g := taskgroup.NewTaskGroup(ctx, len(historyMessages))
for _, hm := range historyMessages {
hMsg := hm
g.Go(func() error {
_, err := messageClient.Create(ctx, hMsg)
if err != nil {
return err
}
return nil
})
}
err = g.Wait()
if err != nil {
logs.CtxWarnf(ctx, "create history message failed, err=%v", err)
}
}
parameters[vo.UserInputKey], err = w.makeChatFlowUserInput(ctx, lastUserMessage)
if err != nil {
return nil, err
}
sr, err := GetWorkflowDomainSVC().StreamExecute(ctx, exeCfg, parameters)
if err != nil {
return nil, err
}
return schema.StreamReaderWithConvert(sr, w.convertToChatFlowRunResponseList(ctx, convertToChatFlowInfo{
bizID: bizID,
conversationID: conversationID,
roundID: roundID,
workflowID: workflowID,
sectionID: sectionID,
unbinding: unbinding,
userMessage: userSchemaMessage,
suggestReplyInfo: req.GetSuggestReplyInfo(),
})), nil
}
func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Context, info convertToChatFlowInfo) func(msg *entity.Message) (responses []*workflow.ChatFlowRunResponse, err error) {
var (
bizID = info.bizID
conversationID = info.conversationID
roundID = info.roundID
workflowID = info.workflowID
sectionID = info.sectionID
unbinding = info.unbinding
userMessage = info.userMessage
spaceID int64
executeID int64
outputCount int32
inputCount int32
intermediateMessage *message.Message
needRegeneratedMessage = true
messageDetailID int64
)
return func(msg *entity.Message) (responses []*workflow.ChatFlowRunResponse, err error) {
defer func() {
if err != nil {
if unbinding != nil {
unErr := unbinding()
if unErr != nil {
logs.CtxErrorf(ctx, "unbinding failed, error: %v", unErr)
}
}
if intermediateMessage != nil {
_, mErr := crossmessage.DefaultSVC().Create(ctx, intermediateMessage)
if mErr != nil {
logs.CtxWarnf(ctx, "create message faield, err: %v", err)
}
}
}
}()
if msg.StateMessage != nil {
if executeID > 0 && executeID != msg.StateMessage.ExecuteID {
return nil, schema.ErrNoValue
}
switch msg.StateMessage.Status {
case entity.WorkflowSuccess:
suggestWorkflowResponse := make([]*workflow.ChatFlowRunResponse, 0, 3)
if info.suggestReplyInfo != nil && info.suggestReplyInfo.IsSetSuggestReplyMode() && info.suggestReplyInfo.GetSuggestReplyMode() != workflow.SuggestReplyInfoMode_Disable {
sInfo := &vo.SuggestInfo{
UserInput: userMessage,
AnswerInput: schema.AssistantMessage(intermediateMessage.Content, nil),
PersonaInput: info.suggestReplyInfo.CustomizedSuggestPrompt,
}
suggests, err := GetWorkflowDomainSVC().Suggest(ctx, sInfo)
if err != nil {
return nil, err
}
for index, s := range suggests {
suggestWorkflowResponse = append(suggestWorkflowResponse, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowMessageCompleted),
Data: func() string {
s, _ := sonic.MarshalString(&vo.MessageDetail{
ID: strconv.FormatInt(time.Now().UnixNano()+int64(index), 10),
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(schema.Assistant),
Type: "follow_up",
ContentType: "text",
Content: s,
})
return s
}(),
})
}
}
chatDoneEvent := &vo.ChatFlowDetail{
ID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(bizID, 10),
Status: vo.Completed,
ExecuteID: strconv.FormatInt(executeID, 10),
Usage: &vo.Usage{
InputTokens: ptr.Of(inputCount),
OutputTokens: ptr.Of(outputCount),
TokenCount: ptr.Of(outputCount + inputCount),
},
}
data, err := sonic.MarshalString(chatDoneEvent)
if err != nil {
return nil, err
}
doneData, err := sonic.MarshalString(map[string]interface{}{
"debug_url": fmt.Sprintf(workflowModel.DebugURLTpl, executeID, spaceID, workflowID),
})
if err != nil {
return nil, err
}
if unbinding != nil {
unErr := unbinding()
if unErr != nil {
logs.CtxErrorf(ctx, "unbinding failed, error: %v", unErr)
}
}
return append(suggestWorkflowResponse, []*workflow.ChatFlowRunResponse{
{
Event: string(vo.ChatFlowCompleted),
Data: data,
},
{
Event: string(vo.ChatFlowDone),
Data: doneData,
},
}...), nil
case entity.WorkflowFailed:
var wfe vo.WorkflowError
if !errors.As(msg.StateMessage.LastError, &wfe) {
panic("stream run last error is not a WorkflowError")
}
chatFailedEvent := &vo.ErrorDetail{
Code: strconv.Itoa(int(wfe.Code())),
Msg: wfe.Msg(),
DebugUrl: wfe.DebugURL(),
}
data, err := sonic.MarshalString(chatFailedEvent)
if err != nil {
return nil, err
}
if intermediateMessage != nil {
_, err := crossmessage.DefaultSVC().Create(ctx, intermediateMessage)
if err != nil {
return nil, err
}
}
if unbinding != nil {
unErr := unbinding()
if unErr != nil {
logs.CtxErrorf(ctx, "unbinding failed, error: %v", unErr)
}
}
return []*workflow.ChatFlowRunResponse{
{
Event: string(vo.ChatFlowError),
Data: data,
},
}, err
case entity.WorkflowCancel:
if intermediateMessage != nil {
_, err := crossmessage.DefaultSVC().Create(ctx, intermediateMessage)
if err != nil {
return nil, err
}
}
if unbinding != nil {
unErr := unbinding()
if unErr != nil {
logs.CtxErrorf(ctx, "unbinding failed, error: %v", unErr)
}
}
case entity.WorkflowInterrupted:
var (
interruptEvent = msg.StateMessage.InterruptEvent
interruptData = interruptEvent.InterruptData
msgContent string
contentType message.ContentType
)
if interruptEvent.EventType == entity.InterruptEventInput {
msgContent, contentType, err = renderInputCardDSL(interruptData)
if err != nil {
return nil, err
}
} else if interruptEvent.EventType == entity.InterruptEventQuestion {
msgContent, contentType, err = renderQACardDSL(interruptData)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("unsupported interrupt event type: %s", interruptEvent.EventType)
}
_, err = crossmessage.DefaultSVC().Create(ctx, &message.Message{
AgentID: bizID,
RunID: roundID,
SectionID: sectionID,
Content: msgContent,
ConversationID: conversationID,
Role: schema.Assistant,
MessageType: message.MessageTypeAnswer,
ContentType: contentType,
})
if err != nil {
return nil, err
}
completeData, _ := sonic.MarshalString(&vo.MessageDetail{
ID: strconv.FormatInt(interruptEvent.ID, 10),
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(schema.Assistant),
Type: string(entity.Answer),
ContentType: string(contentType),
Content: msgContent,
})
if contentType == message.ContentTypeText {
responses = append(responses, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowMessageDelta),
Data: completeData,
})
}
responses = append(responses, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowMessageCompleted),
Data: completeData,
})
data, _ := sonic.MarshalString(&vo.ChatFlowDetail{
ID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
Status: vo.RequiresAction,
ExecuteID: strconv.FormatInt(executeID, 10),
})
doneData, _ := sonic.MarshalString(map[string]interface{}{
"debug_url": fmt.Sprintf(workflowModel.DebugURLTpl, executeID, spaceID, workflowID),
})
responses = append(responses, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowRequiresAction),
Data: data,
}, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowDone),
Data: doneData,
})
err = GetWorkflowDomainSVC().BindConvRelatedInfo(ctx, conversationID, entity.ConvRelatedInfo{
EventID: msg.StateMessage.InterruptEvent.ID, ExecID: executeID, NodeType: msg.StateMessage.InterruptEvent.NodeType,
})
if err != nil {
return nil, err
}
return responses, nil
case entity.WorkflowRunning:
executeID = msg.StateMessage.ExecuteID
spaceID = msg.StateMessage.SpaceID
responses = make([]*workflow.ChatFlowRunResponse, 0)
chatEvent := &vo.ChatFlowDetail{
ID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
Status: vo.Created,
ExecuteID: strconv.FormatInt(executeID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
}
data, _ := sonic.MarshalString(chatEvent)
responses = append(responses, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowCreated),
Data: data,
})
chatEvent.Status = vo.InProgress
data, _ = sonic.MarshalString(chatEvent)
responses = append(responses, &workflow.ChatFlowRunResponse{
Event: string(vo.ChatFlowInProgress),
Data: data,
})
return responses, nil
default:
return nil, schema.ErrNoValue
}
}
if msg.DataMessage != nil {
if msg.Type != entity.Answer {
return nil, schema.ErrNoValue
}
if executeID > 0 && executeID != msg.DataMessage.ExecuteID {
return nil, schema.ErrNoValue
}
if msg.DataMessage.NodeType == entity.NodeTypeQuestionAnswer || msg.DataMessage.NodeType == entity.NodeTypeInputReceiver {
return nil, schema.ErrNoValue
}
dataMessage := msg.DataMessage
if needRegeneratedMessage {
id, err := w.IDGenerator.GenID(ctx)
if err != nil {
return nil, err
}
intermediateMessage = &message.Message{
ID: id,
AgentID: bizID,
RunID: roundID,
SectionID: sectionID,
ConversationID: conversationID,
Role: schema.Assistant,
MessageType: message.MessageTypeAnswer,
ContentType: message.ContentTypeText,
}
messageDetailID = id
needRegeneratedMessage = false
}
intermediateMessage.Content += msg.Content
deltaData, _ := sonic.MarshalString(&vo.MessageDetail{
ID: strconv.FormatInt(messageDetailID, 10),
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(dataMessage.Role),
Type: string(dataMessage.Type),
ContentType: string(message.ContentTypeText),
Content: msg.Content,
})
if !msg.Last {
return []*workflow.ChatFlowRunResponse{
{
Event: string(vo.ChatFlowMessageDelta),
Data: deltaData,
},
}, nil
}
_, err = crossmessage.DefaultSVC().Create(ctx, intermediateMessage)
if err != nil {
return nil, err
}
completeData, _ := sonic.MarshalString(&vo.MessageDetail{
ID: strconv.FormatInt(messageDetailID, 10),
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(dataMessage.Role),
Type: string(dataMessage.Type),
ContentType: string(message.ContentTypeText),
Content: intermediateMessage.Content,
})
needRegeneratedMessage = true
return []*workflow.ChatFlowRunResponse{
{
Event: string(vo.ChatFlowMessageDelta),
Data: deltaData,
},
{
Event: string(vo.ChatFlowMessageCompleted),
Data: completeData,
},
}, nil
}
return nil, err
}
}
func (w *ApplicationService) makeChatFlowUserInput(ctx context.Context, message *workflow.EnterMessage) (string, error) {
type content struct {
Type string `json:"type"`
FileID *string `json:"file_id"`
Text *string `json:"text"`
}
if message.ContentType == "text" {
return message.Content, nil
} else if message.ContentType == "object_string" {
contents := make([]content, 0)
err := sonic.UnmarshalString(message.Content, &contents)
if err != nil {
return "", err
}
texts := make([]string, 0)
urls := make([]string, 0)
for _, ct := range contents {
if ct.Text != nil && len(*ct.Text) > 0 {
texts = append(texts, *ct.Text)
}
if ct.FileID != nil && len(*ct.FileID) > 0 {
fileID := mustParseInt64(*ct.FileID)
file, err := crossupload.DefaultSVC().GetFile(ctx, &service.GetFileRequest{ID: fileID})
if err != nil {
return "", err
}
if file.File == nil {
return "", fmt.Errorf("file not found")
}
urls = append(urls, file.File.Url)
}
}
return strings.Join(append(texts, urls...), ","), nil
} else {
return "", fmt.Errorf("invalid message ccontent type %v", message.ContentType)
}
}
func makeChatFlowHistoryMessages(ctx context.Context, bizID, conversationID, userID, sectionID, connectorID int64, messages []*workflow.EnterMessage) ([]*message.Message, error) {
var (
rID int64
err error
runRecord *agententity.RunRecordMeta
)
historyMessages := make([]*message.Message, 0, len(messages))
for _, msg := range messages {
if msg.Role == userRole {
runRecord, err = crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{
AgentID: bizID,
ConversationID: conversationID,
UserID: strconv.FormatInt(userID, 10),
ConnectorID: connectorID,
SectionID: sectionID,
})
if err != nil {
return nil, err
}
rID = runRecord.ID
} else if msg.Role == assistantRole {
if rID == 0 {
continue
}
} else {
return nil, fmt.Errorf("invalid role type %v", msg.Role)
}
m, err := toConversationMessage(ctx, bizID, conversationID, userID, rID, sectionID, ternary.IFElse(msg.Role == userRole, message.MessageTypeQuestion, message.MessageTypeAnswer), msg)
if err != nil {
return nil, err
}
historyMessages = append(historyMessages, m)
}
return historyMessages, nil
}
func (w *ApplicationService) OpenAPICreateConversation(ctx context.Context, req *workflow.CreateConversationRequest) (resp *workflow.CreateConversationResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrWorkflowOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
var (
appID = mustParseInt64(req.GetAppID())
apiKeyInfo = ctxutil.GetApiAuthFromCtx(ctx)
userID = apiKeyInfo.UserID
env = ternary.IFElse(req.GetDraftMode(), vo.Draft, vo.Online)
cID int64
//spaceID = mustParseInt64(req.GetSpaceID())
//_ = spaceID
)
// todo check permission
if !req.GetGetOrCreate() {
cID, err = GetWorkflowDomainSVC().UpdateConversation(ctx, env, appID, req.GetConnectorId(), userID, req.GetConversationMame())
} else {
var tplExisted, dcExisted bool
var tplErr, dcErr error
var wg sync.WaitGroup
wg.Add(2)
safego.Go(ctx, func() {
defer wg.Done()
_, tplExisted, tplErr = GetWorkflowDomainSVC().GetTemplateByName(ctx, env, appID, req.GetConversationMame())
})
safego.Go(ctx, func() {
defer wg.Done()
_, dcExisted, dcErr = GetWorkflowDomainSVC().GetDynamicConversationByName(ctx, env, appID, req.GetConnectorId(), userID, req.GetConversationMame())
})
wg.Wait()
if tplErr != nil {
return nil, tplErr
}
if dcErr != nil {
return nil, dcErr
}
if !tplExisted && !dcExisted {
return &workflow.CreateConversationResponse{
Code: errno.ErrConversationNotFoundForOperation,
Msg: "Conversation not found. Please create a conversation before attempting to perform any related operations.",
}, nil
}
cID, _, err = GetWorkflowDomainSVC().GetOrCreateConversation(ctx, env, appID, req.GetConnectorId(), userID, req.GetConversationMame())
}
if err != nil {
return nil, err
}
cInfo, err := crossconversation.DefaultSVC().GetByID(ctx, cID)
if err != nil {
return nil, err
}
return &workflow.CreateConversationResponse{
ConversationData: &workflow.ConversationData{
Id: cID,
LastSectionID: ptr.Of(cInfo.SectionID),
},
}, nil
}
func toConversationMessage(ctx context.Context, bizID, cid, userID, roundID, sectionID int64, messageType message.MessageType, msg *workflow.EnterMessage) (*message.Message, error) {
type content struct {
Type string `json:"type"`
FileID *string `json:"file_id"`
Text *string `json:"text"`
}
if msg.ContentType == "text" {
return &message.Message{
Role: schema.User,
ConversationID: cid,
AgentID: bizID,
RunID: roundID,
Content: msg.Content,
ContentType: message.ContentTypeText,
MessageType: messageType,
UserID: strconv.FormatInt(userID, 10),
SectionID: sectionID,
}, nil
} else if msg.ContentType == "object_string" {
contents := make([]*content, 0)
err := sonic.UnmarshalString(msg.Content, &contents)
if err != nil {
return nil, err
}
m := &message.Message{
Role: schema.User,
MessageType: messageType,
ConversationID: cid,
AgentID: bizID,
UserID: strconv.FormatInt(userID, 10),
RunID: roundID,
ContentType: message.ContentTypeMix,
MultiContent: make([]*message.InputMetaData, 0, len(contents)),
SectionID: sectionID,
}
for _, ct := range contents {
if ct.Text != nil {
m.MultiContent = append(m.MultiContent, &message.InputMetaData{
Type: message.InputTypeText,
Text: *ct.Text,
})
} else if ct.FileID != nil {
fileID := mustParseInt64(*ct.FileID)
file, err := crossupload.DefaultSVC().GetFile(ctx, &service.GetFileRequest{ID: fileID})
if err != nil {
return nil, err
}
if file.File == nil {
return nil, fmt.Errorf("file not found")
}
m.MultiContent = append(m.MultiContent, &message.InputMetaData{
Type: message.InputType(ct.Type),
FileData: []*message.FileData{
{
Url: file.File.Url,
URI: file.File.TosURI,
Name: file.File.Name,
},
},
})
} else {
return nil, fmt.Errorf("invalid input type %v", ct.Type)
}
}
return m, nil
} else {
return nil, fmt.Errorf("invalid message content type %v", msg.ContentType)
}
}
func toSchemaMessage(ctx context.Context, msg *workflow.EnterMessage) (*schema.Message, error) {
type content struct {
Type string `json:"type"`
FileID *string `json:"file_id"`
Text *string `json:"text"`
}
if msg.ContentType == "text" {
return &schema.Message{
Role: schema.User,
Content: msg.Content,
}, nil
} else if msg.ContentType == "object_string" {
contents := make([]*content, 0)
err := sonic.UnmarshalString(msg.Content, &contents)
if err != nil {
return nil, err
}
m := &schema.Message{
Role: schema.User,
MultiContent: make([]schema.ChatMessagePart, 0, len(contents)),
}
for _, ct := range contents {
if ct.Text != nil {
if len(*ct.Text) == 0 {
continue
}
m.MultiContent = append(m.MultiContent, schema.ChatMessagePart{
Type: schema.ChatMessagePartTypeText,
Text: *ct.Text,
})
} else if ct.FileID != nil {
fileID := mustParseInt64(*ct.FileID)
file, err := crossupload.DefaultSVC().GetFile(ctx, &service.GetFileRequest{ID: fileID})
if err != nil {
return nil, err
}
if file.File == nil {
return nil, fmt.Errorf("file not found")
}
switch ct.Type {
case "file":
m.MultiContent = append(m.MultiContent, schema.ChatMessagePart{
Type: schema.ChatMessagePartTypeFileURL,
FileURL: &schema.ChatMessageFileURL{
URL: file.File.Url,
},
})
case "image":
m.MultiContent = append(m.MultiContent, schema.ChatMessagePart{
Type: schema.ChatMessagePartTypeImageURL,
ImageURL: &schema.ChatMessageImageURL{
URL: file.File.Url,
},
})
case "audio":
m.MultiContent = append(m.MultiContent, schema.ChatMessagePart{
Type: schema.ChatMessagePartTypeAudioURL,
AudioURL: &schema.ChatMessageAudioURL{
URL: file.File.Url,
},
})
case "video":
m.MultiContent = append(m.MultiContent, schema.ChatMessagePart{
Type: schema.ChatMessagePartTypeVideoURL,
VideoURL: &schema.ChatMessageVideoURL{
URL: file.File.Url,
},
})
}
} else {
return nil, fmt.Errorf("invalid input type %v", ct.Type)
}
}
return m, nil
} else {
return nil, fmt.Errorf("invalid message content type %v", msg.ContentType)
}
}
type convertToChatFlowInfo struct {
userMessage *schema.Message
bizID int64
conversationID int64
roundID int64
workflowID int64
sectionID int64
unbinding func() error
suggestReplyInfo *workflow.SuggestReplyInfo
}
func parserInput(inputString string) string {
result := map[string]any{}
lines := strings.Split(inputString, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
keyValue := strings.SplitN(line, ":", 2)
if len(keyValue) == 2 {
result[keyValue[0]] = keyValue[1]
}
}
str, _ := sonic.MarshalString(result)
return str
}
func renderInputCardDSL(c string) (string, message.ContentType, error) {
type contentInfo struct {
Content string `json:"content"`
}
type field struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}
type inputCard struct {
CardType int64 `json:"card_type"`
ContentType int64 `json:"content_type"`
ResponseType string `json:"response_type"`
TemplateId int64 `json:"template_id"`
TemplateURL string `json:"template_url"`
Data string `json:"data"`
XProperties map[string]string `json:"x_properties"`
}
info := &contentInfo{}
err := sonic.UnmarshalString(c, info)
if err != nil {
return "", "", err
}
fields := make([]*field, 0)
err = sonic.UnmarshalString(info.Content, &fields)
if err != nil {
return "", "", err
}
iCard := defaultCard()
iCard.Variables["5fJt3qKpSz"].(map[string]any)["defaultValue"] = fields
iCardString, _ := sonic.MarshalString(iCard)
rCard := &inputCard{
CardType: 3,
ContentType: 50,
ResponseType: "card",
TemplateId: 7383997384420262000,
TemplateURL: "",
Data: iCardString,
}
type props struct {
CardType string `json:"card_type"`
InputCardData []*field `json:"input_card_data"`
}
propsString, _ := sonic.MarshalString(props{
CardType: "INPUT",
InputCardData: fields,
})
rCard.XProperties = map[string]string{
"workflow_card_info": propsString,
}
rCardString, _ := sonic.MarshalString(rCard)
return rCardString, message.ContentTypeCard, nil
}
func renderQACardDSL(c string) (string, message.ContentType, error) {
type contentInfo struct {
Messages []struct {
Type string `json:"type"`
ContentType string `json:"content_type"`
Content any `json:"content"`
} `json:"messages"`
}
info := &contentInfo{}
err := sonic.UnmarshalString(c, info)
if err != nil {
return "", "", err
}
if len(info.Messages) == 0 {
return "", "", fmt.Errorf("no input card data")
}
if info.Messages[0].ContentType == "text" {
return info.Messages[0].Content.(string), message.ContentTypeText, nil
}
type field struct {
Name string `json:"name"`
}
type key struct {
Key string `json:"key"`
}
type inputCard struct {
CardType int64 `json:"card_type"`
ContentType int64 `json:"content_type"`
ResponseType string `json:"response_type"`
TemplateId int64 `json:"template_id"`
TemplateURL string `json:"template_url"`
Data string `json:"data"`
XProperties map[string]string `json:"x_properties"`
}
iCard := defaultCard()
keys := make([]*key, 0)
fields := make([]*field, 0)
content := info.Messages[0].Content
type contentOption struct {
Options []*field `json:"options"`
Question string `json:"question"`
}
contentString, err := sonic.MarshalString(content)
if err != nil {
return "", "", err
}
contentOptionInfo := &contentOption{}
err = sonic.UnmarshalString(contentString, contentOptionInfo)
if err != nil {
return "", "", err
}
for _, op := range contentOptionInfo.Options {
keys = append(keys, &key{Key: op.Name})
fields = append(fields, &field{Name: op.Name})
}
iCard.Variables["5fJt3qKpSz"].(map[string]any)["defaultValue"] = map[string]any{
"description": contentOptionInfo.Question,
"list": keys,
}
iCardString, _ := sonic.MarshalString(iCard)
rCard := &inputCard{
CardType: 3,
ContentType: 50,
ResponseType: "card",
TemplateId: 7383997384420262000,
TemplateURL: "",
Data: iCardString,
}
type props struct {
CardType string `json:"card_type"`
QuestionCardData struct {
Title string `json:"Title"`
Options []*field `json:"Options"`
} `json:"question_card_data"`
}
propsString, _ := sonic.MarshalString(props{
CardType: "QUESTION",
QuestionCardData: struct {
Title string `json:"Title"`
Options []*field `json:"Options"`
}{Title: contentOptionInfo.Question, Options: fields},
})
rCard.XProperties = map[string]string{
"workflow_card_info": propsString,
}
rCardString, _ := sonic.MarshalString(rCard)
return rCardString, message.ContentTypeCard, nil
}