/* * 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(), ¶meters) if err != nil { return nil, err } } var ( workflowID = mustParseInt64(req.GetWorkflowID()) isDebug = req.GetExecuteMode() == "DEBUG" appID, agentID *int64 resolveAppID 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())) resolveAppID = mustParseInt64(req.GetAppID()) } if req.IsSetBotID() { agentID = ptr.Of(mustParseInt64(req.GetBotID())) resolveAppID = 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), resolveAppID, connectorID, conversationID) if err != nil { return nil, err } if !existed { return nil, fmt.Errorf("conversation not found") } parameters["CONVERSATION_NAME"] = conversationName } else if req.IsSetConversationID() && req.IsSetBotID() { parameters["CONVERSATION_NAME"] = "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["CONVERSATION_NAME"].(string) if !ok { return nil, fmt.Errorf("conversation name is requried") } cID, sID, err := GetWorkflowDomainSVC().GetOrCreateConversation(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), resolveAppID, connectorID, userID, conversationName) if err != nil { return nil, err } conversationID = cID sectionID = sID } runRecord, err := crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{ AgentID: resolveAppID, ConversationID: conversationID, UserID: strconv.FormatInt(userID, 10), ConnectorID: connectorID, SectionID: sectionID, }) if err != nil { return nil, err } roundID := runRecord.ID userMessage, err := toConversationMessage(ctx, resolveAppID, 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 { var data = lastUserMessage.Content if info.NodeType == entity.NodeTypeInputReceiver { data = parserInput(lastUserMessage.Content) } sr, err := GetWorkflowDomainSVC().StreamResume(ctx, &entity.ResumeRequest{ EventID: info.EventID, ExecuteID: info.ExecID, ResumeData: data, }, 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{ appID: resolveAppID, 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, resolveAppID, 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["USER_INPUT"], 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{ appID: resolveAppID, 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 ( appID = info.appID 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(appID, 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(appID, 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: appID, 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(appID, 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: appID, 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(appID, 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(appID, 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, appID, 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: appID, 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 && rID == 0 { continue } else { return nil, fmt.Errorf("invalid role type %v", msg.Role) } m, err := toConversationMessage(ctx, appID, 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, appID, 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: appID, 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: appID, 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 appID 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 }