feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
471
backend/application/conversation/agent_run.go
Normal file
471
backend/application/conversation/agent_run.go
Normal file
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/message"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/agentrun"
|
||||
crossDomainMessage "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
|
||||
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
|
||||
saEntity "github.com/coze-dev/coze-studio/backend/domain/agent/singleagent/entity"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity"
|
||||
convEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
|
||||
msgEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/message/entity"
|
||||
cmdEntity "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/entity"
|
||||
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"
|
||||
"github.com/coze-dev/coze-studio/backend/types/consts"
|
||||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||||
)
|
||||
|
||||
func (c *ConversationApplicationService) Run(ctx context.Context, sseSender *sseImpl.SSenderImpl, ar *run.AgentRunRequest) error {
|
||||
agentInfo, caErr := c.checkAgent(ctx, ar)
|
||||
if caErr != nil {
|
||||
logs.CtxErrorf(ctx, "checkAgent err:%v", caErr)
|
||||
return caErr
|
||||
}
|
||||
|
||||
userID := ctxutil.MustGetUIDFromCtx(ctx)
|
||||
conversationData, ccErr := c.checkConversation(ctx, ar, userID)
|
||||
|
||||
if ccErr != nil {
|
||||
logs.CtxErrorf(ctx, "checkConversation err:%v", ccErr)
|
||||
return ccErr
|
||||
}
|
||||
|
||||
if ar.RegenMessageID != nil && ptr.From(ar.RegenMessageID) > 0 {
|
||||
msgMeta, err := c.MessageDomainSVC.GetByID(ctx, ptr.From(ar.RegenMessageID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if msgMeta != nil {
|
||||
if msgMeta.UserID != conv.Int64ToStr(userID) {
|
||||
return errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "message not match"))
|
||||
}
|
||||
delErr := c.MessageDomainSVC.Delete(ctx, &msgEntity.DeleteMeta{
|
||||
RunIDs: []int64{msgMeta.RunID},
|
||||
})
|
||||
if delErr != nil {
|
||||
return delErr
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
var shortcutCmd *cmdEntity.ShortcutCmd
|
||||
if ar.GetShortcutCmdID() > 0 {
|
||||
cmdID := ar.GetShortcutCmdID()
|
||||
cmdMeta, err := c.ShortcutDomainSVC.GetByCmdID(ctx, cmdID, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shortcutCmd = cmdMeta
|
||||
}
|
||||
|
||||
arr, err := c.buildAgentRunRequest(ctx, ar, userID, agentInfo.SpaceID, conversationData, shortcutCmd)
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "buildAgentRunRequest err:%v", err)
|
||||
return err
|
||||
}
|
||||
streamer, err := c.AgentRunDomainSVC.AgentRun(ctx, arr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.pullStream(ctx, sseSender, streamer, ar)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) pullStream(ctx context.Context, sseSender *sseImpl.SSenderImpl, arStream *schema.StreamReader[*entity.AgentRunResponse], req *run.AgentRunRequest) {
|
||||
var ackMessageInfo *entity.ChunkMessageItem
|
||||
for {
|
||||
chunk, recvErr := arStream.Recv()
|
||||
if recvErr != nil {
|
||||
if errors.Is(recvErr, io.EOF) {
|
||||
return
|
||||
}
|
||||
sseSender.Send(ctx, buildErrorEvent(errno.ErrConversationAgentRunError, recvErr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
switch chunk.Event {
|
||||
case entity.RunEventCreated, entity.RunEventInProgress, entity.RunEventCompleted:
|
||||
case entity.RunEventError:
|
||||
id, err := c.GenID(ctx)
|
||||
if err != nil {
|
||||
sseSender.Send(ctx, buildErrorEvent(errno.ErrConversationAgentRunError, err.Error()))
|
||||
|
||||
} else {
|
||||
sseSender.Send(ctx, buildMessageChunkEvent(run.RunEventMessage, buildErrMsg(ackMessageInfo, chunk.Error, id)))
|
||||
}
|
||||
case entity.RunEventStreamDone:
|
||||
sseSender.Send(ctx, buildDoneEvent(run.RunEventDone))
|
||||
case entity.RunEventAck:
|
||||
ackMessageInfo = chunk.ChunkMessageItem
|
||||
sseSender.Send(ctx, buildMessageChunkEvent(run.RunEventMessage, buildARSM2Message(chunk, req)))
|
||||
case entity.RunEventMessageDelta, entity.RunEventMessageCompleted:
|
||||
sseSender.Send(ctx, buildMessageChunkEvent(run.RunEventMessage, buildARSM2Message(chunk, req)))
|
||||
default:
|
||||
logs.CtxErrorf(ctx, "unknown handler event:%v", chunk.Event)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func buildARSM2Message(chunk *entity.AgentRunResponse, req *run.AgentRunRequest) []byte {
|
||||
chunkMessageItem := chunk.ChunkMessageItem
|
||||
|
||||
chunkMessage := &run.RunStreamResponse{
|
||||
ConversationID: strconv.FormatInt(chunkMessageItem.ConversationID, 10),
|
||||
IsFinish: ptr.Of(chunk.ChunkMessageItem.IsFinish),
|
||||
Message: &message.ChatMessage{
|
||||
Role: string(chunkMessageItem.Role),
|
||||
ContentType: string(chunkMessageItem.ContentType),
|
||||
MessageID: strconv.FormatInt(chunkMessageItem.ID, 10),
|
||||
SectionID: strconv.FormatInt(chunkMessageItem.SectionID, 10),
|
||||
ContentTime: chunkMessageItem.CreatedAt,
|
||||
ExtraInfo: buildExt(chunkMessageItem.Ext),
|
||||
ReplyID: strconv.FormatInt(chunkMessageItem.ReplyID, 10),
|
||||
|
||||
Status: "",
|
||||
Type: string(chunkMessageItem.MessageType),
|
||||
Content: chunkMessageItem.Content,
|
||||
ReasoningContent: chunkMessageItem.ReasoningContent,
|
||||
RequiredAction: chunkMessageItem.RequiredAction,
|
||||
},
|
||||
Index: int32(chunkMessageItem.Index),
|
||||
SeqID: int32(chunkMessageItem.SeqID),
|
||||
}
|
||||
if chunkMessageItem.MessageType == crossDomainMessage.MessageTypeAck {
|
||||
chunkMessage.Message.Content = req.GetQuery()
|
||||
chunkMessage.Message.ContentType = req.GetContentType()
|
||||
chunkMessage.Message.ExtraInfo = &message.ExtraInfo{
|
||||
LocalMessageID: req.GetLocalMessageID(),
|
||||
}
|
||||
} else {
|
||||
chunkMessage.Message.ExtraInfo = buildExt(chunkMessageItem.Ext)
|
||||
chunkMessage.Message.SenderID = ptr.Of(strconv.FormatInt(chunkMessageItem.AgentID, 10))
|
||||
chunkMessage.Message.Content = chunkMessageItem.Content
|
||||
|
||||
if chunkMessageItem.MessageType == crossDomainMessage.MessageTypeKnowledge {
|
||||
chunkMessage.Message.Type = string(crossDomainMessage.MessageTypeVerbose)
|
||||
}
|
||||
}
|
||||
|
||||
if chunk.ChunkMessageItem.IsFinish && chunkMessageItem.MessageType == crossDomainMessage.MessageTypeAnswer {
|
||||
chunkMessage.Message.Content = ""
|
||||
chunkMessage.Message.ReasoningContent = ptr.Of("")
|
||||
}
|
||||
|
||||
mCM, _ := json.Marshal(chunkMessage)
|
||||
return mCM
|
||||
}
|
||||
|
||||
func buildExt(extra map[string]string) *message.ExtraInfo {
|
||||
if extra == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &message.ExtraInfo{
|
||||
InputTokens: extra["input_tokens"],
|
||||
OutputTokens: extra["output_tokens"],
|
||||
Token: extra["token"],
|
||||
PluginStatus: extra["plugin_status"],
|
||||
TimeCost: extra["time_cost"],
|
||||
WorkflowTokens: extra["workflow_tokens"],
|
||||
BotState: extra["bot_state"],
|
||||
PluginRequest: extra["plugin_request"],
|
||||
ToolName: extra["tool_name"],
|
||||
Plugin: extra["plugin"],
|
||||
MockHitInfo: extra["mock_hit_info"],
|
||||
MessageTitle: extra["message_title"],
|
||||
StreamPluginRunning: extra["stream_plugin_running"],
|
||||
ExecuteDisplayName: extra["execute_display_name"],
|
||||
TaskType: extra["task_type"],
|
||||
ReferFormat: extra["refer_format"],
|
||||
}
|
||||
}
|
||||
func buildErrMsg(ackChunk *entity.ChunkMessageItem, err *entity.RunError, id int64) []byte {
|
||||
|
||||
chunkMessage := &run.RunStreamResponse{
|
||||
IsFinish: ptr.Of(true),
|
||||
ConversationID: strconv.FormatInt(ackChunk.ConversationID, 10),
|
||||
Message: &message.ChatMessage{
|
||||
Role: string(schema.Assistant),
|
||||
ContentType: string(crossDomainMessage.ContentTypeText),
|
||||
Type: string(crossDomainMessage.MessageTypeAnswer),
|
||||
MessageID: strconv.FormatInt(id, 10),
|
||||
SectionID: strconv.FormatInt(ackChunk.SectionID, 10),
|
||||
ReplyID: strconv.FormatInt(ackChunk.ReplyID, 10),
|
||||
Content: "Something error:" + err.Msg,
|
||||
ExtraInfo: &message.ExtraInfo{},
|
||||
},
|
||||
}
|
||||
|
||||
mCM, _ := json.Marshal(chunkMessage)
|
||||
return mCM
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) GenID(ctx context.Context) (int64, error) {
|
||||
id, err := c.appContext.IDGen.GenID(ctx)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) checkConversation(ctx context.Context, ar *run.AgentRunRequest, userID int64) (*convEntity.Conversation, error) {
|
||||
var conversationData *convEntity.Conversation
|
||||
if ar.ConversationID > 0 {
|
||||
|
||||
realCurrCon, err := c.ConversationDomainSVC.GetCurrentConversation(ctx, &convEntity.GetCurrent{
|
||||
UserID: userID,
|
||||
AgentID: ar.BotID,
|
||||
Scene: ptr.From(ar.Scene),
|
||||
ConnectorID: consts.CozeConnectorID,
|
||||
})
|
||||
logs.CtxInfof(ctx, "conversatioin data:%v", conv.DebugJsonToStr(realCurrCon))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if realCurrCon != nil {
|
||||
conversationData = realCurrCon
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ar.ConversationID == 0 || conversationData == nil {
|
||||
|
||||
conData, err := c.ConversationDomainSVC.Create(ctx, &convEntity.CreateMeta{
|
||||
AgentID: ar.BotID,
|
||||
UserID: userID,
|
||||
Scene: ptr.From(ar.Scene),
|
||||
ConnectorID: consts.CozeConnectorID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs.CtxInfof(ctx, "conversatioin create data:%v", conv.DebugJsonToStr(conData))
|
||||
conversationData = conData
|
||||
|
||||
ar.ConversationID = conversationData.ID
|
||||
}
|
||||
|
||||
if conversationData.CreatorID != userID {
|
||||
return nil, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "conversation not match"))
|
||||
}
|
||||
|
||||
return conversationData, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) checkAgent(ctx context.Context, ar *run.AgentRunRequest) (*saEntity.SingleAgent, error) {
|
||||
agentInfo, err := c.appContext.SingleAgentDomainSVC.GetSingleAgent(ctx, ar.BotID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if agentInfo == nil {
|
||||
return nil, errorx.New(errno.ErrAgentNotExists)
|
||||
}
|
||||
return agentInfo, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildAgentRunRequest(ctx context.Context, ar *run.AgentRunRequest, userID int64, spaceID int64, conversationData *convEntity.Conversation, shortcutCMD *cmdEntity.ShortcutCmd) (*entity.AgentRunMeta, error) {
|
||||
var contentType crossDomainMessage.ContentType
|
||||
contentType = crossDomainMessage.ContentTypeText
|
||||
|
||||
if ptr.From(ar.ContentType) != string(crossDomainMessage.ContentTypeText) {
|
||||
contentType = crossDomainMessage.ContentTypeMix
|
||||
}
|
||||
|
||||
shortcutCMDData, err := c.buildTools(ctx, ar.ToolList, shortcutCMD)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arm := &entity.AgentRunMeta{
|
||||
ConversationID: conversationData.ID,
|
||||
AgentID: ar.BotID,
|
||||
Content: c.buildMultiContent(ctx, ar),
|
||||
DisplayContent: c.buildDisplayContent(ctx, ar),
|
||||
SpaceID: spaceID,
|
||||
UserID: conv.Int64ToStr(userID),
|
||||
SectionID: conversationData.SectionID,
|
||||
PreRetrieveTools: shortcutCMDData,
|
||||
IsDraft: ptr.From(ar.DraftMode),
|
||||
ConnectorID: consts.CozeConnectorID,
|
||||
ContentType: contentType,
|
||||
Ext: ar.Extra,
|
||||
}
|
||||
return arm, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildDisplayContent(ctx context.Context, ar *run.AgentRunRequest) string {
|
||||
if *ar.ContentType == run.ContentTypeText {
|
||||
return ""
|
||||
}
|
||||
return ar.Query
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildTools(ctx context.Context, tools []*run.Tool, shortcutCMD *cmdEntity.ShortcutCmd) ([]*entity.Tool, error) {
|
||||
var ts []*entity.Tool
|
||||
for _, tool := range tools {
|
||||
if shortcutCMD != nil {
|
||||
|
||||
arguments := make(map[string]string)
|
||||
for key, parametersStruct := range tool.Parameters {
|
||||
if parametersStruct == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
arguments[key] = parametersStruct.Value
|
||||
// uri需要转换成url
|
||||
if parametersStruct.ResourceType == consts.ShortcutCommandResourceType {
|
||||
|
||||
resourceInfo, err := c.appContext.ImageX.GetResourceURL(ctx, parametersStruct.Value)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arguments[key] = resourceInfo.URL
|
||||
}
|
||||
}
|
||||
|
||||
argBytes, err := json.Marshal(arguments)
|
||||
if err == nil {
|
||||
ts = append(ts, &entity.Tool{
|
||||
PluginID: shortcutCMD.PluginID,
|
||||
Arguments: string(argBytes),
|
||||
ToolName: shortcutCMD.PluginToolName,
|
||||
ToolID: shortcutCMD.PluginToolID,
|
||||
Type: agentrun.ToolType(shortcutCMD.ToolType),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildMultiContent(ctx context.Context, ar *run.AgentRunRequest) []*crossDomainMessage.InputMetaData {
|
||||
var multiContents []*crossDomainMessage.InputMetaData
|
||||
|
||||
switch *ar.ContentType {
|
||||
case run.ContentTypeText:
|
||||
multiContents = append(multiContents, &crossDomainMessage.InputMetaData{
|
||||
Type: crossDomainMessage.InputTypeText,
|
||||
Text: ar.Query,
|
||||
})
|
||||
case run.ContentTypeImage, run.ContentTypeFile, run.ContentTypeMix, run.ContentTypeVideo, run.ContentTypeAudio:
|
||||
var mc *run.MixContentModel
|
||||
|
||||
err := json.Unmarshal([]byte(ar.Query), &mc)
|
||||
if err != nil {
|
||||
multiContents = append(multiContents, &crossDomainMessage.InputMetaData{
|
||||
Type: crossDomainMessage.InputTypeText,
|
||||
Text: ar.Query,
|
||||
})
|
||||
return multiContents
|
||||
}
|
||||
|
||||
mcContent, newItemList := c.parseMultiContent(ctx, mc.ItemList)
|
||||
|
||||
multiContents = append(multiContents, mcContent...)
|
||||
|
||||
mc.ItemList = newItemList
|
||||
mcByte, err := json.Marshal(mc)
|
||||
if err == nil {
|
||||
ar.Query = string(mcByte)
|
||||
}
|
||||
}
|
||||
|
||||
return multiContents
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) parseMultiContent(ctx context.Context, mc []*run.Item) (multiContents []*crossDomainMessage.InputMetaData, mcNew []*run.Item) {
|
||||
for index, item := range mc {
|
||||
switch item.Type {
|
||||
case run.ContentTypeText:
|
||||
multiContents = append(multiContents, &crossDomainMessage.InputMetaData{
|
||||
Type: crossDomainMessage.InputTypeText,
|
||||
Text: item.Text,
|
||||
})
|
||||
case run.ContentTypeImage:
|
||||
|
||||
resourceUrl, err := c.getUrlByUri(ctx, item.Image.Key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "failed to unescape resource url, err is %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
mc[index].Image.ImageThumb.URL = resourceUrl
|
||||
mc[index].Image.ImageOri.URL = resourceUrl
|
||||
|
||||
multiContents = append(multiContents, &crossDomainMessage.InputMetaData{
|
||||
Type: crossDomainMessage.InputTypeImage,
|
||||
FileData: []*crossDomainMessage.FileData{
|
||||
{
|
||||
Url: resourceUrl,
|
||||
},
|
||||
},
|
||||
})
|
||||
case run.ContentTypeFile, run.ContentTypeAudio, run.ContentTypeVideo:
|
||||
|
||||
resourceUrl, err := c.getUrlByUri(ctx, item.File.FileKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
mc[index].File.FileURL = resourceUrl
|
||||
|
||||
multiContents = append(multiContents, &crossDomainMessage.InputMetaData{
|
||||
Type: crossDomainMessage.InputType(item.Type),
|
||||
FileData: []*crossDomainMessage.FileData{
|
||||
{
|
||||
Url: resourceUrl,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return multiContents, mc
|
||||
}
|
||||
|
||||
func (s *ConversationApplicationService) getUrlByUri(ctx context.Context, uri string) (string, error) {
|
||||
|
||||
url, err := s.appContext.ImageX.GetResourceURL(ctx, uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return url.URL, nil
|
||||
}
|
||||
51
backend/application/conversation/build_chunk_event.go
Normal file
51
backend/application/conversation/build_chunk_event.go
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/hertz-contrib/sse"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
|
||||
)
|
||||
|
||||
func buildDoneEvent(event string) *sse.Event {
|
||||
return &sse.Event{
|
||||
Event: event,
|
||||
}
|
||||
}
|
||||
|
||||
func buildErrorEvent(errCode int64, errMsg string) *sse.Event {
|
||||
errData := run.ErrorData{
|
||||
Code: errCode,
|
||||
Msg: errMsg,
|
||||
}
|
||||
ed, _ := json.Marshal(errData)
|
||||
|
||||
return &sse.Event{
|
||||
Event: run.RunEventError,
|
||||
Data: ed,
|
||||
}
|
||||
}
|
||||
|
||||
func buildMessageChunkEvent(event string, chunkMsg []byte) *sse.Event {
|
||||
return &sse.Event{
|
||||
Event: event,
|
||||
Data: chunkMsg,
|
||||
}
|
||||
}
|
||||
188
backend/application/conversation/conversation.go
Normal file
188
backend/application/conversation/conversation.go
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/common"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/conversation"
|
||||
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
|
||||
agentrun "github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/service"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
|
||||
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"
|
||||
"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/consts"
|
||||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||||
)
|
||||
|
||||
type ConversationApplicationService struct {
|
||||
appContext *ServiceComponents
|
||||
|
||||
AgentRunDomainSVC agentrun.Run
|
||||
ConversationDomainSVC conversationService.Conversation
|
||||
MessageDomainSVC message.Message
|
||||
|
||||
ShortcutDomainSVC service.ShortcutCmd
|
||||
}
|
||||
|
||||
var ConversationSVC = new(ConversationApplicationService)
|
||||
|
||||
type OpenapiAgentRunApplication struct {
|
||||
ShortcutDomainSVC service.ShortcutCmd
|
||||
}
|
||||
|
||||
var ConversationOpenAPISVC = new(OpenapiAgentRunApplication)
|
||||
|
||||
func (c *ConversationApplicationService) ClearHistory(ctx context.Context, req *conversation.ClearConversationHistoryRequest) (*conversation.ClearConversationHistoryResponse, error) {
|
||||
resp := new(conversation.ClearConversationHistoryResponse)
|
||||
|
||||
conversationID := req.ConversationID
|
||||
|
||||
// get conversation
|
||||
currentRes, err := c.ConversationDomainSVC.GetByID(ctx, conversationID)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if currentRes == nil {
|
||||
return resp, errorx.New(errno.ErrConversationNotFound)
|
||||
}
|
||||
// check user
|
||||
userID := ctxutil.GetUIDFromCtx(ctx)
|
||||
if userID == nil || *userID != currentRes.CreatorID {
|
||||
return resp, errorx.New(errno.ErrConversationNotFound, errorx.KV("msg", "user not match"))
|
||||
}
|
||||
|
||||
// delete conversation
|
||||
err = c.ConversationDomainSVC.Delete(ctx, conversationID)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
// create new conversation
|
||||
convRes, err := c.ConversationDomainSVC.Create(ctx, &entity.CreateMeta{
|
||||
AgentID: currentRes.AgentID,
|
||||
UserID: currentRes.CreatorID,
|
||||
Scene: currentRes.Scene,
|
||||
ConnectorID: consts.CozeConnectorID,
|
||||
})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
resp.NewSectionID = convRes.SectionID
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) CreateSection(ctx context.Context, conversationID int64) (int64, error) {
|
||||
currentRes, err := c.ConversationDomainSVC.GetByID(ctx, conversationID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if currentRes == nil {
|
||||
return 0, errorx.New(errno.ErrConversationNotFound, errorx.KV("msg", "conversation not found"))
|
||||
}
|
||||
var userID int64
|
||||
if currentRes.ConnectorID == consts.CozeConnectorID {
|
||||
userID = ctxutil.MustGetUIDFromCtx(ctx)
|
||||
} else {
|
||||
userID = ctxutil.MustGetUIDFromApiAuthCtx(ctx)
|
||||
}
|
||||
|
||||
if userID != currentRes.CreatorID {
|
||||
return 0, errorx.New(errno.ErrConversationNotFound, errorx.KV("msg", "user not match"))
|
||||
}
|
||||
|
||||
convRes, err := c.ConversationDomainSVC.NewConversationCtx(ctx, &entity.NewConversationCtxRequest{
|
||||
ID: conversationID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return convRes.SectionID, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) CreateConversation(ctx context.Context, agentID int64, connectorID int64) (*conversation.CreateConversationResponse, error) {
|
||||
resp := new(conversation.CreateConversationResponse)
|
||||
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
|
||||
userID := apiKeyInfo.UserID
|
||||
if connectorID != consts.WebSDKConnectorID {
|
||||
connectorID = apiKeyInfo.ConnectorID
|
||||
}
|
||||
|
||||
conversationData, err := c.ConversationDomainSVC.Create(ctx, &entity.CreateMeta{
|
||||
AgentID: agentID,
|
||||
UserID: userID,
|
||||
ConnectorID: connectorID,
|
||||
Scene: common.Scene_SceneOpenApi,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.ConversationData = &conversation.ConversationData{
|
||||
Id: conversationData.ID,
|
||||
LastSectionID: &conversationData.SectionID,
|
||||
ConnectorID: &conversationData.ConnectorID,
|
||||
CreatedAt: conversationData.CreatedAt,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) ListConversation(ctx context.Context, req *conversation.ListConversationsApiRequest) (*conversation.ListConversationsApiResponse, error) {
|
||||
|
||||
resp := new(conversation.ListConversationsApiResponse)
|
||||
|
||||
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
|
||||
userID := apiKeyInfo.UserID
|
||||
connectorID := apiKeyInfo.ConnectorID
|
||||
|
||||
if userID == 0 {
|
||||
return resp, errorx.New(errno.ErrConversationNotFound)
|
||||
}
|
||||
if ptr.From(req.ConnectorID) == consts.WebSDKConnectorID {
|
||||
connectorID = ptr.From(req.ConnectorID)
|
||||
}
|
||||
|
||||
conversationDOList, hasMore, err := c.ConversationDomainSVC.List(ctx, &entity.ListMeta{
|
||||
UserID: userID,
|
||||
AgentID: req.GetBotID(),
|
||||
ConnectorID: connectorID,
|
||||
Scene: common.Scene_SceneOpenApi,
|
||||
Page: int(req.GetPageNum()),
|
||||
Limit: int(req.GetPageSize()),
|
||||
})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
conversationData := slices.Transform(conversationDOList, func(conv *entity.Conversation) *conversation.ConversationData {
|
||||
return &conversation.ConversationData{
|
||||
Id: conv.ID,
|
||||
LastSectionID: &conv.SectionID,
|
||||
ConnectorID: &conv.ConnectorID,
|
||||
CreatedAt: conv.CreatedAt,
|
||||
}
|
||||
})
|
||||
|
||||
resp.Data = &conversation.ListConversationData{
|
||||
Conversations: conversationData,
|
||||
HasMore: hasMore,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
76
backend/application/conversation/init.go
Normal file
76
backend/application/conversation/init.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/application/singleagent"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/repository"
|
||||
agentrun "github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/service"
|
||||
convRepo "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/repository"
|
||||
conversation "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/service"
|
||||
msgRepo "github.com/coze-dev/coze-studio/backend/domain/conversation/message/repository"
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
type ServiceComponents struct {
|
||||
IDGen idgen.IDGenerator
|
||||
DB *gorm.DB
|
||||
TosClient storage.Storage
|
||||
ImageX imagex.ImageX
|
||||
|
||||
SingleAgentDomainSVC singleagent.SingleAgent
|
||||
}
|
||||
|
||||
func InitService(s *ServiceComponents) *ConversationApplicationService {
|
||||
mDomainComponents := &message.Components{
|
||||
MessageRepo: msgRepo.NewMessageRepo(s.DB, s.IDGen),
|
||||
}
|
||||
messageDomainSVC := message.NewService(mDomainComponents)
|
||||
|
||||
cDomainComponents := &conversation.Components{
|
||||
ConversationRepo: convRepo.NewConversationRepo(s.DB, s.IDGen),
|
||||
}
|
||||
|
||||
conversationDomainSVC := conversation.NewService(cDomainComponents)
|
||||
|
||||
arDomainComponents := &agentrun.Components{
|
||||
RunRecordRepo: repository.NewRunRecordRepo(s.DB, s.IDGen),
|
||||
}
|
||||
|
||||
agentRunDomainSVC := agentrun.NewService(arDomainComponents)
|
||||
components := &service.Components{
|
||||
ShortCutCmdRepo: shortcutRepo.NewShortCutCmdRepo(s.DB, s.IDGen),
|
||||
}
|
||||
shortcutCmdDomainSVC := service.NewShortcutCommandService(components)
|
||||
|
||||
ConversationSVC.AgentRunDomainSVC = agentRunDomainSVC
|
||||
ConversationSVC.MessageDomainSVC = messageDomainSVC
|
||||
ConversationSVC.ConversationDomainSVC = conversationDomainSVC
|
||||
ConversationSVC.appContext = s
|
||||
ConversationSVC.ShortcutDomainSVC = shortcutCmdDomainSVC
|
||||
|
||||
ConversationOpenAPISVC.ShortcutDomainSVC = shortcutCmdDomainSVC
|
||||
|
||||
return ConversationSVC
|
||||
}
|
||||
293
backend/application/conversation/message.go
Normal file
293
backend/application/conversation/message.go
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/common"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/message"
|
||||
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
|
||||
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
|
||||
singleAgentEntity "github.com/coze-dev/coze-studio/backend/domain/agent/singleagent/entity"
|
||||
convEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/conversation/message/entity"
|
||||
"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/lang/slices"
|
||||
"github.com/coze-dev/coze-studio/backend/types/consts"
|
||||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||||
)
|
||||
|
||||
func (c *ConversationApplicationService) GetMessageList(ctx context.Context, mr *message.GetMessageListRequest) (*message.GetMessageListResponse, error) {
|
||||
// Get Conversation ID by agent id & userID & scene
|
||||
userID := ctxutil.GetUIDFromCtx(ctx)
|
||||
|
||||
agentID, err := strconv.ParseInt(mr.BotID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentConversation, isNewCreate, err := c.getCurrentConversation(ctx, *userID, agentID, *mr.Scene, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isNewCreate {
|
||||
return &message.GetMessageListResponse{
|
||||
MessageList: []*message.ChatMessage{},
|
||||
Cursor: mr.Cursor,
|
||||
NextCursor: "0",
|
||||
NextHasMore: false,
|
||||
ConversationID: strconv.FormatInt(currentConversation.ID, 10),
|
||||
LastSectionID: ptr.Of(strconv.FormatInt(currentConversation.SectionID, 10)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
cursor, err := strconv.ParseInt(mr.Cursor, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mListMessages, err := c.MessageDomainSVC.List(ctx, &entity.ListMeta{
|
||||
ConversationID: currentConversation.ID,
|
||||
AgentID: agentID,
|
||||
Limit: int(mr.Count),
|
||||
Cursor: cursor,
|
||||
Direction: loadDirectionToScrollDirection(mr.LoadDirection),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get agent id
|
||||
var agentIDs []int64
|
||||
for _, mOne := range mListMessages.Messages {
|
||||
agentIDs = append(agentIDs, mOne.AgentID)
|
||||
}
|
||||
|
||||
agentInfo, err := c.buildAgentInfo(ctx, agentIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := c.buildMessageListResponse(ctx, mListMessages, currentConversation)
|
||||
|
||||
resp.ParticipantInfoMap = map[string]*message.MsgParticipantInfo{}
|
||||
for _, aOne := range agentInfo {
|
||||
resp.ParticipantInfoMap[aOne.ID] = aOne
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildAgentInfo(ctx context.Context, agentIDs []int64) ([]*message.MsgParticipantInfo, error) {
|
||||
var result []*message.MsgParticipantInfo
|
||||
if len(agentIDs) > 0 {
|
||||
agentInfos, err := c.appContext.SingleAgentDomainSVC.MGetSingleAgentDraft(ctx, agentIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = slices.Transform(agentInfos, func(a *singleAgentEntity.SingleAgent) *message.MsgParticipantInfo {
|
||||
return &message.MsgParticipantInfo{
|
||||
ID: strconv.FormatInt(a.AgentID, 10),
|
||||
Name: a.Name,
|
||||
UserID: strconv.FormatInt(a.CreatorID, 10),
|
||||
Desc: a.Desc,
|
||||
AvatarURL: a.IconURI,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) getCurrentConversation(ctx context.Context, userID int64, agentID int64, scene common.Scene, connectorID *int64) (*convEntity.Conversation, bool, error) {
|
||||
var currentConversation *convEntity.Conversation
|
||||
var isNewCreate bool
|
||||
|
||||
if connectorID == nil && scene == common.Scene_Playground {
|
||||
connectorID = ptr.Of(consts.CozeConnectorID)
|
||||
}
|
||||
|
||||
currentConversation, err := c.ConversationDomainSVC.GetCurrentConversation(ctx, &convEntity.GetCurrent{
|
||||
UserID: userID,
|
||||
Scene: scene,
|
||||
AgentID: agentID,
|
||||
ConnectorID: ptr.From(connectorID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, isNewCreate, err
|
||||
}
|
||||
|
||||
if currentConversation == nil { // new conversation
|
||||
// create conversation
|
||||
ccNew, err := c.ConversationDomainSVC.Create(ctx, &convEntity.CreateMeta{
|
||||
AgentID: agentID,
|
||||
UserID: userID,
|
||||
Scene: scene,
|
||||
ConnectorID: ptr.From(connectorID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, isNewCreate, err
|
||||
}
|
||||
if ccNew == nil {
|
||||
return nil, isNewCreate,
|
||||
errorx.New(errno.ErrConversationNotFound)
|
||||
}
|
||||
isNewCreate = true
|
||||
currentConversation = ccNew
|
||||
}
|
||||
|
||||
return currentConversation, isNewCreate, nil
|
||||
}
|
||||
|
||||
func loadDirectionToScrollDirection(direction *message.LoadDirection) entity.ScrollPageDirection {
|
||||
if direction != nil && *direction == message.LoadDirection_Next {
|
||||
return entity.ScrollPageDirectionNext
|
||||
}
|
||||
return entity.ScrollPageDirectionPrev
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildMessageListResponse(ctx context.Context, mListMessages *entity.ListResult, currentConversation *convEntity.Conversation) *message.GetMessageListResponse {
|
||||
var messages []*message.ChatMessage
|
||||
runToQuestionIDMap := make(map[int64]int64)
|
||||
|
||||
for _, mMessage := range mListMessages.Messages {
|
||||
if mMessage.MessageType == model.MessageTypeQuestion {
|
||||
runToQuestionIDMap[mMessage.RunID] = mMessage.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, mMessage := range mListMessages.Messages {
|
||||
messages = append(messages, c.buildDomainMsg2VOMessage(ctx, mMessage, runToQuestionIDMap))
|
||||
}
|
||||
|
||||
resp := &message.GetMessageListResponse{
|
||||
MessageList: messages,
|
||||
Cursor: strconv.FormatInt(mListMessages.PrevCursor, 10),
|
||||
NextCursor: strconv.FormatInt(mListMessages.NextCursor, 10),
|
||||
ConversationID: strconv.FormatInt(currentConversation.ID, 10),
|
||||
LastSectionID: ptr.Of(strconv.FormatInt(currentConversation.SectionID, 10)),
|
||||
ConnectorConversationID: strconv.FormatInt(currentConversation.ID, 10),
|
||||
}
|
||||
|
||||
if mListMessages.Direction == entity.ScrollPageDirectionPrev {
|
||||
resp.Hasmore = mListMessages.HasMore
|
||||
} else {
|
||||
resp.NextHasMore = mListMessages.HasMore
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) buildDomainMsg2VOMessage(ctx context.Context, dm *entity.Message, runToQuestionIDMap map[int64]int64) *message.ChatMessage {
|
||||
cm := &message.ChatMessage{
|
||||
MessageID: strconv.FormatInt(dm.ID, 10),
|
||||
Role: string(dm.Role),
|
||||
Type: string(dm.MessageType),
|
||||
Content: dm.Content,
|
||||
ContentType: string(dm.ContentType),
|
||||
ReplyID: "0",
|
||||
SectionID: strconv.FormatInt(dm.SectionID, 10),
|
||||
ExtraInfo: buildDExt2ApiExt(dm.Ext),
|
||||
ContentTime: dm.CreatedAt,
|
||||
Status: "available",
|
||||
Source: 0,
|
||||
ReasoningContent: ptr.Of(dm.ReasoningContent),
|
||||
}
|
||||
|
||||
if dm.Status == model.MessageStatusBroken {
|
||||
cm.BrokenPos = ptr.Of(dm.Position)
|
||||
}
|
||||
|
||||
if dm.ContentType == model.ContentTypeMix && dm.DisplayContent != "" {
|
||||
cm.Content = dm.DisplayContent
|
||||
}
|
||||
|
||||
if dm.MessageType != model.MessageTypeQuestion {
|
||||
cm.ReplyID = strconv.FormatInt(runToQuestionIDMap[dm.RunID], 10)
|
||||
cm.SenderID = ptr.Of(strconv.FormatInt(dm.AgentID, 10))
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
func buildDExt2ApiExt(extra map[string]string) *message.ExtraInfo {
|
||||
return &message.ExtraInfo{
|
||||
InputTokens: extra["input_tokens"],
|
||||
OutputTokens: extra["output_tokens"],
|
||||
Token: extra["token"],
|
||||
PluginStatus: extra["plugin_status"],
|
||||
TimeCost: extra["time_cost"],
|
||||
WorkflowTokens: extra["workflow_tokens"],
|
||||
BotState: extra["bot_state"],
|
||||
PluginRequest: extra["plugin_request"],
|
||||
ToolName: extra["tool_name"],
|
||||
Plugin: extra["plugin"],
|
||||
MockHitInfo: extra["mock_hit_info"],
|
||||
MessageTitle: extra["message_title"],
|
||||
StreamPluginRunning: extra["stream_plugin_running"],
|
||||
ExecuteDisplayName: extra["execute_display_name"],
|
||||
TaskType: extra["task_type"],
|
||||
ReferFormat: extra["refer_format"],
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) DeleteMessage(ctx context.Context, mr *message.DeleteMessageRequest) (*message.DeleteMessageResponse, error) {
|
||||
resp := new(message.DeleteMessageResponse)
|
||||
messageInfo, err := c.MessageDomainSVC.GetByID(ctx, mr.MessageID)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if messageInfo == nil {
|
||||
return resp, errorx.New(errno.ErrConversationMessageNotFound)
|
||||
}
|
||||
|
||||
userID := ctxutil.GetUIDFromCtx(ctx)
|
||||
if messageInfo.UserID != conv.Int64ToStr(*userID) {
|
||||
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission denied"))
|
||||
}
|
||||
|
||||
err = c.AgentRunDomainSVC.Delete(ctx, []int64{messageInfo.RunID})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = c.MessageDomainSVC.Delete(ctx, &entity.DeleteMeta{
|
||||
RunIDs: []int64{messageInfo.RunID},
|
||||
})
|
||||
if err != nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *ConversationApplicationService) BreakMessage(ctx context.Context, mr *message.BreakMessageRequest) (*message.BreakMessageResponse, error) {
|
||||
resp := new(message.BreakMessageResponse)
|
||||
|
||||
err := c.MessageDomainSVC.Broken(ctx, &entity.BrokenMeta{
|
||||
ID: *mr.AnswerMessageID,
|
||||
Position: mr.BrokenPos,
|
||||
})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
329
backend/application/conversation/openapi_agent_run.go
Normal file
329
backend/application/conversation/openapi_agent_run.go
Normal file
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/common"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/agentrun"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/singleagent"
|
||||
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
|
||||
saEntity "github.com/coze-dev/coze-studio/backend/domain/agent/singleagent/entity"
|
||||
"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"
|
||||
sseImpl "github.com/coze-dev/coze-studio/backend/infra/impl/sse"
|
||||
"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"
|
||||
"github.com/coze-dev/coze-studio/backend/types/consts"
|
||||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||||
)
|
||||
|
||||
func (a *OpenapiAgentRunApplication) OpenapiAgentRun(ctx context.Context, sseSender *sseImpl.SSenderImpl, ar *run.ChatV3Request) error {
|
||||
|
||||
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
|
||||
creatorID := apiKeyInfo.UserID
|
||||
connectorID := apiKeyInfo.ConnectorID
|
||||
|
||||
if ptr.From(ar.ConnectorID) == consts.WebSDKConnectorID {
|
||||
connectorID = ptr.From(ar.ConnectorID)
|
||||
}
|
||||
agentInfo, caErr := a.checkAgent(ctx, ar, connectorID)
|
||||
if caErr != nil {
|
||||
logs.CtxErrorf(ctx, "checkAgent err:%v", caErr)
|
||||
return caErr
|
||||
}
|
||||
|
||||
conversationData, ccErr := a.checkConversation(ctx, ar, creatorID, connectorID)
|
||||
if ccErr != nil {
|
||||
logs.CtxErrorf(ctx, "checkConversation err:%v", ccErr)
|
||||
return ccErr
|
||||
}
|
||||
|
||||
spaceID := agentInfo.SpaceID
|
||||
arr, err := a.buildAgentRunRequest(ctx, ar, connectorID, spaceID, conversationData)
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "buildAgentRunRequest err:%v", err)
|
||||
return err
|
||||
}
|
||||
streamer, err := ConversationSVC.AgentRunDomainSVC.AgentRun(ctx, arr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.pullStream(ctx, sseSender, streamer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) checkConversation(ctx context.Context, ar *run.ChatV3Request, userID int64, connectorID int64) (*convEntity.Conversation, error) {
|
||||
var conversationData *convEntity.Conversation
|
||||
if ptr.From(ar.ConversationID) > 0 {
|
||||
conData, err := ConversationSVC.ConversationDomainSVC.GetByID(ctx, ptr.From(ar.ConversationID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conversationData = conData
|
||||
}
|
||||
|
||||
if ptr.From(ar.ConversationID) == 0 || conversationData == nil {
|
||||
|
||||
conData, err := ConversationSVC.ConversationDomainSVC.Create(ctx, &convEntity.CreateMeta{
|
||||
AgentID: ar.BotID,
|
||||
UserID: userID,
|
||||
ConnectorID: connectorID,
|
||||
Scene: common.Scene_SceneOpenApi,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conData == nil {
|
||||
return nil, errors.New("conversation data is nil")
|
||||
}
|
||||
conversationData = conData
|
||||
|
||||
ar.ConversationID = ptr.Of(conversationData.ID)
|
||||
}
|
||||
|
||||
if conversationData.CreatorID != userID {
|
||||
return nil, errors.New("conversation data not match")
|
||||
}
|
||||
|
||||
return conversationData, nil
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) checkAgent(ctx context.Context, ar *run.ChatV3Request, connectorID int64) (*saEntity.SingleAgent, error) {
|
||||
agentInfo, err := ConversationSVC.appContext.SingleAgentDomainSVC.ObtainAgentByIdentity(ctx, &singleagent.AgentIdentity{
|
||||
AgentID: ar.BotID,
|
||||
IsDraft: false,
|
||||
ConnectorID: connectorID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if agentInfo == nil {
|
||||
return nil, errors.New("agent info is nil")
|
||||
}
|
||||
return agentInfo, nil
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) buildAgentRunRequest(ctx context.Context, ar *run.ChatV3Request, connectorID int64, spaceID int64, conversationData *convEntity.Conversation) (*entity.AgentRunMeta, error) {
|
||||
|
||||
shortcutCMDData, err := a.buildTools(ctx, ar.ShortcutCommand)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
multiContent, contentType, err := a.buildMultiContent(ctx, ar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
displayContent := a.buildDisplayContent(ctx, ar)
|
||||
arm := &entity.AgentRunMeta{
|
||||
ConversationID: ptr.From(ar.ConversationID),
|
||||
AgentID: ar.BotID,
|
||||
Content: multiContent,
|
||||
DisplayContent: displayContent,
|
||||
SpaceID: spaceID,
|
||||
UserID: ar.User,
|
||||
SectionID: conversationData.SectionID,
|
||||
PreRetrieveTools: shortcutCMDData,
|
||||
IsDraft: false,
|
||||
ConnectorID: connectorID,
|
||||
ContentType: contentType,
|
||||
Ext: ar.ExtraParams,
|
||||
}
|
||||
return arm, nil
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) buildTools(ctx context.Context, shortcmd *run.ShortcutCommandDetail) ([]*entity.Tool, error) {
|
||||
var ts []*entity.Tool
|
||||
|
||||
if shortcmd == nil {
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
var shortcutCMD *cmdEntity.ShortcutCmd
|
||||
cmdMeta, err := a.ShortcutDomainSVC.GetByCmdID(ctx, shortcmd.CommandID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shortcutCMD = cmdMeta
|
||||
if shortcutCMD != nil {
|
||||
argBytes, err := json.Marshal(shortcmd.Parameters)
|
||||
if err == nil {
|
||||
ts = append(ts, &entity.Tool{
|
||||
PluginID: shortcutCMD.PluginID,
|
||||
Arguments: string(argBytes),
|
||||
ToolName: shortcutCMD.PluginToolName,
|
||||
ToolID: shortcutCMD.PluginToolID,
|
||||
Type: agentrun.ToolType(shortcutCMD.ToolType),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) buildDisplayContent(_ context.Context, ar *run.ChatV3Request) string {
|
||||
for _, item := range ar.AdditionalMessages {
|
||||
if item.ContentType == run.ContentTypeMixApi {
|
||||
return item.Content
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) buildMultiContent(ctx context.Context, ar *run.ChatV3Request) ([]*message.InputMetaData, message.ContentType, error) {
|
||||
var multiContents []*message.InputMetaData
|
||||
contentType := message.ContentTypeText
|
||||
|
||||
for _, item := range ar.AdditionalMessages {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if item.Role != string(schema.User) {
|
||||
return nil, contentType, errors.New("role not match")
|
||||
}
|
||||
if item.ContentType == run.ContentTypeText {
|
||||
if item.Content == "" {
|
||||
continue
|
||||
}
|
||||
multiContents = append(multiContents, &message.InputMetaData{
|
||||
Type: message.InputTypeText,
|
||||
Text: item.Content,
|
||||
})
|
||||
}
|
||||
|
||||
if item.ContentType == run.ContentTypeMixApi {
|
||||
contentType = message.ContentTypeMix
|
||||
var inputs []*run.AdditionalContent
|
||||
err := json.Unmarshal([]byte(item.Content), &inputs)
|
||||
|
||||
logs.CtxInfof(ctx, "inputs:%v, err:%v", conv.DebugJsonToStr(inputs), err)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, one := range inputs {
|
||||
if one == nil {
|
||||
continue
|
||||
}
|
||||
switch message.InputType(one.Type) {
|
||||
case message.InputTypeText:
|
||||
multiContents = append(multiContents, &message.InputMetaData{
|
||||
Type: message.InputTypeText,
|
||||
Text: ptr.From(one.Text),
|
||||
})
|
||||
case message.InputTypeImage, message.InputTypeFile:
|
||||
multiContents = append(multiContents, &message.InputMetaData{
|
||||
Type: message.InputType(one.Type),
|
||||
FileData: []*message.FileData{
|
||||
{
|
||||
Url: one.GetFileURL(),
|
||||
},
|
||||
},
|
||||
})
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return multiContents, contentType, nil
|
||||
}
|
||||
|
||||
func (a *OpenapiAgentRunApplication) pullStream(ctx context.Context, sseSender *sseImpl.SSenderImpl, streamer *schema.StreamReader[*entity.AgentRunResponse]) {
|
||||
for {
|
||||
chunk, recvErr := streamer.Recv()
|
||||
logs.CtxInfof(ctx, "chunk :%v, err:%v", conv.DebugJsonToStr(chunk), recvErr)
|
||||
if recvErr != nil {
|
||||
if errors.Is(recvErr, io.EOF) {
|
||||
return
|
||||
}
|
||||
sseSender.Send(ctx, buildErrorEvent(errno.ErrConversationAgentRunError, recvErr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
switch chunk.Event {
|
||||
|
||||
case entity.RunEventError:
|
||||
sseSender.Send(ctx, buildErrorEvent(chunk.Error.Code, chunk.Error.Msg))
|
||||
case entity.RunEventStreamDone:
|
||||
sseSender.Send(ctx, buildDoneEvent(string(entity.RunEventStreamDone)))
|
||||
case entity.RunEventAck:
|
||||
case entity.RunEventCreated, entity.RunEventCancelled, entity.RunEventInProgress, entity.RunEventFailed, entity.RunEventCompleted:
|
||||
sseSender.Send(ctx, buildMessageChunkEvent(string(chunk.Event), buildARSM2ApiChatMessage(chunk)))
|
||||
case entity.RunEventMessageDelta, entity.RunEventMessageCompleted:
|
||||
sseSender.Send(ctx, buildMessageChunkEvent(string(chunk.Event), buildARSM2ApiMessage(chunk)))
|
||||
|
||||
default:
|
||||
logs.CtxErrorf(ctx, "unknow handler event:%v", chunk.Event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildARSM2ApiMessage(chunk *entity.AgentRunResponse) []byte {
|
||||
chunkMessageItem := chunk.ChunkMessageItem
|
||||
chunkMessage := &run.ChatV3MessageDetail{
|
||||
ID: strconv.FormatInt(chunkMessageItem.ID, 10),
|
||||
ConversationID: strconv.FormatInt(chunkMessageItem.ConversationID, 10),
|
||||
BotID: strconv.FormatInt(chunkMessageItem.AgentID, 10),
|
||||
Role: string(chunkMessageItem.Role),
|
||||
Type: string(chunkMessageItem.MessageType),
|
||||
Content: chunkMessageItem.Content,
|
||||
ContentType: string(chunkMessageItem.ContentType),
|
||||
MetaData: chunkMessageItem.Ext,
|
||||
ChatID: strconv.FormatInt(chunkMessageItem.RunID, 10),
|
||||
ReasoningContent: chunkMessageItem.ReasoningContent,
|
||||
}
|
||||
|
||||
mCM, _ := json.Marshal(chunkMessage)
|
||||
return mCM
|
||||
}
|
||||
|
||||
func buildARSM2ApiChatMessage(chunk *entity.AgentRunResponse) []byte {
|
||||
chunkRunItem := chunk.ChunkRunItem
|
||||
chunkMessage := &run.ChatV3ChatDetail{
|
||||
ID: chunkRunItem.ID,
|
||||
ConversationID: chunkRunItem.ConversationID,
|
||||
BotID: chunkRunItem.AgentID,
|
||||
Status: string(chunkRunItem.Status),
|
||||
SectionID: ptr.Of(chunkRunItem.SectionID),
|
||||
CreatedAt: ptr.Of(int32(chunkRunItem.CreatedAt / 1000)),
|
||||
CompletedAt: ptr.Of(int32(chunkRunItem.CompletedAt / 1000)),
|
||||
FailedAt: ptr.Of(int32(chunkRunItem.FailedAt / 1000)),
|
||||
}
|
||||
if chunkRunItem.Usage != nil {
|
||||
chunkMessage.Usage = &run.Usage{
|
||||
TokenCount: ptr.Of(int32(chunkRunItem.Usage.LlmTotalTokens)),
|
||||
InputTokens: ptr.Of(int32(chunkRunItem.Usage.LlmPromptTokens)),
|
||||
OutputTokens: ptr.Of(int32(chunkRunItem.Usage.LlmCompletionTokens)),
|
||||
}
|
||||
}
|
||||
mCM, _ := json.Marshal(chunkMessage)
|
||||
return mCM
|
||||
}
|
||||
133
backend/application/conversation/openapi_message.go
Normal file
133
backend/application/conversation/openapi_message.go
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/message"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
|
||||
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"
|
||||
"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{}
|
||||
|
||||
var OpenapiMessageApplicationService = new(OpenapiMessageApplication)
|
||||
|
||||
func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *message.ListMessageApiRequest) (*message.ListMessageApiResponse, error) {
|
||||
// Get Conversation ID by agent id & userID & scene
|
||||
userID := ctxutil.MustGetUIDFromApiAuthCtx(ctx)
|
||||
|
||||
currentConversation, err := getConversation(ctx, mr.ConversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if currentConversation == nil {
|
||||
return nil, errorx.New(errno.ErrConversationNotFound)
|
||||
}
|
||||
|
||||
if currentConversation.CreatorID != userID {
|
||||
return nil, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission denied"))
|
||||
}
|
||||
|
||||
msgListMeta := &entity.ListMeta{
|
||||
ConversationID: currentConversation.ID,
|
||||
AgentID: currentConversation.AgentID,
|
||||
Limit: int(ptr.From(mr.Limit)),
|
||||
}
|
||||
|
||||
if mr.BeforeID != nil {
|
||||
msgListMeta.Direction = entity.ScrollPageDirectionPrev
|
||||
msgListMeta.Cursor = *mr.BeforeID
|
||||
} else {
|
||||
msgListMeta.Direction = entity.ScrollPageDirectionNext
|
||||
msgListMeta.Cursor = ptr.From(mr.AfterID)
|
||||
}
|
||||
if mr.Order == nil {
|
||||
msgListMeta.OrderBy = ptr.Of(message.OrderByDesc)
|
||||
} else {
|
||||
msgListMeta.OrderBy = mr.Order
|
||||
}
|
||||
|
||||
mListMessages, err := ConversationSVC.MessageDomainSVC.List(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
|
||||
}
|
||||
|
||||
func getConversation(ctx context.Context, conversationID int64) (*convEntity.Conversation, error) {
|
||||
conversationInfo, err := ConversationSVC.ConversationDomainSVC.GetByID(ctx, conversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conversationInfo, nil
|
||||
}
|
||||
|
||||
func (m *OpenapiMessageApplication) buildMessageListResponse(ctx context.Context, mListMessages *entity.ListResult, currentConversation *convEntity.Conversation) *message.ListMessageApiResponse {
|
||||
messagesVO := slices.Transform(mListMessages.Messages, func(dm *entity.Message) *message.OpenMessageApi {
|
||||
|
||||
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,
|
||||
UpdatedAt: dm.UpdatedAt,
|
||||
ChatID: dm.RunID,
|
||||
MetaData: dm.Ext,
|
||||
}
|
||||
if dm.ContentType == message3.ContentTypeMix && dm.DisplayContent != "" {
|
||||
msg.Content = dm.DisplayContent
|
||||
msg.ContentType = run.ContentTypeMixApi
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
resp := &message.ListMessageApiResponse{
|
||||
Messages: messagesVO,
|
||||
HasMore: ptr.Of(mListMessages.HasMore),
|
||||
FirstID: ptr.Of(mListMessages.PrevCursor),
|
||||
LastID: ptr.Of(mListMessages.NextCursor),
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user