feat: Support for Chat Flow & Agent Support for binding a single chat flow (#765)

Co-authored-by: Yu Yang <72337138+tomasyu985@users.noreply.github.com>
Co-authored-by: zengxiaohui <csu.zengxiaohui@gmail.com>
Co-authored-by: lijunwen.gigoo <lijunwen.gigoo@bytedance.com>
Co-authored-by: lvxinyu.1117 <lvxinyu.1117@bytedance.com>
Co-authored-by: liuyunchao.0510 <liuyunchao.0510@bytedance.com>
Co-authored-by: haozhenfei <37089575+haozhenfei@users.noreply.github.com>
Co-authored-by: July <jiangxujin@bytedance.com>
Co-authored-by: tecvan-fe <fanwenjie.fe@bytedance.com>
This commit is contained in:
Zhj
2025-08-28 21:53:32 +08:00
committed by GitHub
parent bbc615a18e
commit d70101c979
503 changed files with 48036 additions and 3427 deletions

View File

@@ -27,6 +27,7 @@ import (
conversationService "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/service"
message "github.com/coze-dev/coze-studio/backend/domain/conversation/message/service"
"github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/service"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
@@ -48,6 +49,7 @@ var ConversationSVC = new(ConversationApplicationService)
type OpenapiAgentRunApplication struct {
ShortcutDomainSVC service.ShortcutCmd
UploaodDomainSVC uploadService.UploadService
}
var ConversationOpenAPISVC = new(OpenapiAgentRunApplication)
@@ -177,6 +179,7 @@ func (c *ConversationApplicationService) ListConversation(ctx context.Context, r
LastSectionID: &conv.SectionID,
ConnectorID: &conv.ConnectorID,
CreatedAt: conv.CreatedAt / 1000,
Name: ptr.Of(conv.Name),
}
})
@@ -186,3 +189,70 @@ func (c *ConversationApplicationService) ListConversation(ctx context.Context, r
}
return resp, nil
}
func (c *ConversationApplicationService) DeleteConversation(ctx context.Context, req *conversation.DeleteConversationApiRequest) (*conversation.DeleteConversationApiResponse, error) {
resp := new(conversation.DeleteConversationApiResponse)
convID := req.GetConversationID()
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
userID := apiKeyInfo.UserID
if userID == 0 {
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission check failed"))
}
conversationDO, err := c.ConversationDomainSVC.GetByID(ctx, convID)
if err != nil {
return resp, err
}
if conversationDO == nil {
return resp, errorx.New(errno.ErrConversationNotFound)
}
if conversationDO.CreatorID != userID {
return resp, errorx.New(errno.ErrConversationNotFound, errorx.KV("msg", "user not match"))
}
err = c.ConversationDomainSVC.Delete(ctx, convID)
if err != nil {
return resp, err
}
return resp, nil
}
func (c *ConversationApplicationService) UpdateConversation(ctx context.Context, req *conversation.UpdateConversationApiRequest) (*conversation.UpdateConversationApiResponse, error) {
resp := new(conversation.UpdateConversationApiResponse)
convID := req.GetConversationID()
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
userID := apiKeyInfo.UserID
if userID == 0 {
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission check failed"))
}
conversationDO, err := c.ConversationDomainSVC.GetByID(ctx, convID)
if err != nil {
return resp, err
}
if conversationDO == nil {
return resp, errorx.New(errno.ErrConversationNotFound)
}
if conversationDO.CreatorID != userID {
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "user not match"))
}
updateResult, err := c.ConversationDomainSVC.Update(ctx, &entity.UpdateMeta{
ID: convID,
Name: req.GetName(),
})
if err != nil {
return resp, err
}
resp.ConversationData = &conversation.ConversationData{
Id: updateResult.ID,
LastSectionID: &updateResult.SectionID,
ConnectorID: &updateResult.ConnectorID,
CreatedAt: updateResult.CreatedAt / 1000,
Name: ptr.Of(updateResult.Name),
}
return resp, nil
}

View File

@@ -28,6 +28,7 @@ import (
message "github.com/coze-dev/coze-studio/backend/domain/conversation/message/service"
shortcutRepo "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/repository"
"github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/service"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
"github.com/coze-dev/coze-studio/backend/infra/contract/idgen"
"github.com/coze-dev/coze-studio/backend/infra/contract/imagex"
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
@@ -56,6 +57,7 @@ func InitService(s *ServiceComponents) *ConversationApplicationService {
arDomainComponents := &agentrun.Components{
RunRecordRepo: repository.NewRunRecordRepo(s.DB, s.IDGen),
ImagexSVC: s.ImageX,
}
agentRunDomainSVC := agentrun.NewService(arDomainComponents)
@@ -71,6 +73,9 @@ func InitService(s *ServiceComponents) *ConversationApplicationService {
ConversationSVC.ShortcutDomainSVC = shortcutCmdDomainSVC
ConversationOpenAPISVC.ShortcutDomainSVC = shortcutCmdDomainSVC
uploadSVC := uploadService.NewUploadSVC(s.DB, s.IDGen, s.TosClient)
ConversationOpenAPISVC.UploaodDomainSVC = uploadSVC
OpenapiMessageSVC.UploaodDomainSVC = uploadSVC
return ConversationSVC
}

View File

@@ -35,7 +35,9 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity"
convEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
cmdEntity "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/entity"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
sseImpl "github.com/coze-dev/coze-studio/backend/infra/impl/sse"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
@@ -237,11 +239,26 @@ func (a *OpenapiAgentRunApplication) buildMultiContent(ctx context.Context, ar *
Text: ptr.From(one.Text),
})
case message.InputTypeImage, message.InputTypeFile:
var fileUrl, fileURI string
if one.GetFileURL() != "" {
fileUrl = one.GetFileURL()
} else if one.GetFileID() != 0 {
fileInfo, err := a.UploaodDomainSVC.GetFile(ctx, &uploadService.GetFileRequest{
ID: one.GetFileID(),
})
if err != nil {
return nil, contentType, err
}
fileUrl = fileInfo.File.Url
fileURI = fileInfo.File.TosURI
}
multiContents = append(multiContents, &message.InputMetaData{
Type: message.InputType(one.Type),
FileData: []*message.FileData{
{
Url: one.GetFileURL(),
Url: fileUrl,
URI: fileURI,
},
},
})
@@ -328,3 +345,62 @@ func buildARSM2ApiChatMessage(chunk *entity.AgentRunResponse) []byte {
mCM, _ := json.Marshal(chunkMessage)
return mCM
}
func (a *OpenapiAgentRunApplication) CancelRun(ctx context.Context, req *run.CancelChatApiRequest) (*run.CancelChatApiResponse, error) {
resp := new(run.CancelChatApiResponse)
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
userID := apiKeyInfo.UserID
runRecord, err := ConversationSVC.AgentRunDomainSVC.GetByID(ctx, req.ChatID)
if err != nil {
return nil, err
}
if runRecord == nil {
return nil, errorx.New(errno.ErrRecordNotFound)
}
conversationData, err := ConversationSVC.ConversationDomainSVC.GetByID(ctx, req.ConversationID)
if err != nil {
return nil, err
}
if userID != conversationData.CreatorID {
return nil, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "user not match"))
}
if runRecord.Status != entity.RunStatusInProgress && runRecord.Status != entity.RunStatusCreated {
return nil, errorx.New(errno.ErrInProgressCanNotCancel)
}
runMeta, err := ConversationSVC.AgentRunDomainSVC.Cancel(ctx, &entity.CancelRunMeta{
RunID: req.ChatID,
ConversationID: req.ConversationID,
})
if err != nil {
return nil, err
}
if runMeta == nil {
return nil, errorx.New(errno.ErrConversationAgentRunError)
}
resp.ChatV3ChatDetail = &run.ChatV3ChatDetail{
ID: runMeta.ID,
ConversationID: runMeta.ConversationID,
BotID: runMeta.AgentID,
Status: string(runMeta.Status),
SectionID: ptr.Of(runMeta.SectionID),
CreatedAt: ptr.Of(int32(runMeta.CreatedAt / 1000)),
CompletedAt: ptr.Of(int32(runMeta.CompletedAt / 1000)),
FailedAt: ptr.Of(int32(runMeta.FailedAt / 1000)),
}
if runMeta.Usage != nil {
resp.ChatV3ChatDetail.Usage = &run.Usage{
TokenCount: ptr.Of(int32(runMeta.Usage.LlmTotalTokens)),
InputTokens: ptr.Of(int32(runMeta.Usage.LlmPromptTokens)),
OutputTokens: ptr.Of(int32(runMeta.Usage.LlmCompletionTokens)),
}
}
return resp, nil
}

View File

@@ -18,23 +18,28 @@ package conversation
import (
"context"
"encoding/json"
"strconv"
"github.com/coze-dev/coze-studio/backend/api/model/conversation/message"
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
apiMessage "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
message3 "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
convEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
"github.com/coze-dev/coze-studio/backend/domain/conversation/message/entity"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"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/types/errno"
)
type OpenapiMessageApplication struct{}
type OpenapiMessageApplication struct {
UploaodDomainSVC uploadService.UploadService
}
var OpenapiMessageApplicationService = new(OpenapiMessageApplication)
var OpenapiMessageSVC = new(OpenapiMessageApplication)
func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *message.ListMessageApiRequest) (*message.ListMessageApiResponse, error) {
// Get Conversation ID by agent id & userID & scene
@@ -61,13 +66,21 @@ func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *m
ConversationID: currentConversation.ID,
AgentID: currentConversation.AgentID,
Limit: int(ptr.From(mr.Limit)),
MessageType: []*message3.MessageType{
ptr.Of(message3.MessageTypeQuestion),
ptr.Of(message3.MessageTypeAnswer),
},
}
if mr.ChatID != nil {
msgListMeta.RunID = []*int64{mr.ChatID}
}
if mr.BeforeID != nil {
msgListMeta.Direction = entity.ScrollPageDirectionPrev
msgListMeta.Direction = entity.ScrollPageDirectionNext
msgListMeta.Cursor = *mr.BeforeID
} else {
msgListMeta.Direction = entity.ScrollPageDirectionNext
msgListMeta.Direction = entity.ScrollPageDirectionPrev
msgListMeta.Cursor = ptr.From(mr.AfterID)
}
if mr.Order == nil {
@@ -76,20 +89,14 @@ func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *m
msgListMeta.OrderBy = mr.Order
}
mListMessages, err := ConversationSVC.MessageDomainSVC.List(ctx, msgListMeta)
mListMessages, err := ConversationSVC.MessageDomainSVC.ListWithoutPair(ctx, msgListMeta)
if err != nil {
return nil, err
}
// get agent id
var agentIDs []int64
for _, mOne := range mListMessages.Messages {
agentIDs = append(agentIDs, mOne.AgentID)
}
resp := m.buildMessageListResponse(ctx, mListMessages, currentConversation)
return resp, err
return resp, nil
}
func getConversation(ctx context.Context, conversationID int64) (*convEntity.Conversation, error) {
@@ -106,21 +113,22 @@ func (m *OpenapiMessageApplication) buildMessageListResponse(ctx context.Context
content := dm.Content
msg := &message.OpenMessageApi{
ID: dm.ID,
ConversationID: dm.ConversationID,
BotID: dm.AgentID,
Role: string(dm.Role),
Type: string(dm.MessageType),
Content: content,
ContentType: string(dm.ContentType),
SectionID: strconv.FormatInt(dm.SectionID, 10),
CreatedAt: dm.CreatedAt / 1000,
UpdatedAt: dm.UpdatedAt / 1000,
ChatID: dm.RunID,
MetaData: dm.Ext,
ID: dm.ID,
ConversationID: dm.ConversationID,
BotID: dm.AgentID,
Role: string(dm.Role),
Type: string(dm.MessageType),
Content: content,
ContentType: string(dm.ContentType),
SectionID: strconv.FormatInt(dm.SectionID, 10),
CreatedAt: dm.CreatedAt / 1000,
UpdatedAt: dm.UpdatedAt / 1000,
ChatID: dm.RunID,
MetaData: dm.Ext,
ReasoningContent: ptr.Of(dm.ReasoningContent),
}
if dm.ContentType == message3.ContentTypeMix && dm.DisplayContent != "" {
msg.Content = dm.DisplayContent
msg.Content = m.parseDisplayContent(ctx, dm)
msg.ContentType = run.ContentTypeMixApi
}
return msg
@@ -135,3 +143,38 @@ func (m *OpenapiMessageApplication) buildMessageListResponse(ctx context.Context
return resp
}
func (m *OpenapiMessageApplication) parseDisplayContent(ctx context.Context, dm *entity.Message) string {
var inputs []*run.AdditionalContent
err := json.Unmarshal([]byte(dm.DisplayContent), &inputs)
if err != nil {
return dm.DisplayContent
}
for k, one := range inputs {
if one == nil {
continue
}
switch apiMessage.InputType(one.Type) {
case apiMessage.InputTypeText:
continue
case apiMessage.InputTypeImage, apiMessage.InputTypeFile:
if one.GetFileID() != 0 {
fileInfo, err := m.UploaodDomainSVC.GetFile(ctx, &uploadService.GetFileRequest{
ID: one.GetFileID(),
})
if err == nil {
inputs[k].FileURL = ptr.Of(fileInfo.File.Url)
}
}
default:
continue
}
}
content, err := json.Marshal(inputs)
if err == nil {
dm.DisplayContent = string(content)
}
return dm.DisplayContent
}