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

@@ -24,6 +24,7 @@ import (
"github.com/hertz-contrib/sse"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/protocol/consts"
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
@@ -123,3 +124,23 @@ func checkParamsV3(_ context.Context, ar *run.ChatV3Request) error {
}
return nil
}
// CancelChatApi .
// @router /v3/chat/cancel [POST]
func CancelChatApi(ctx context.Context, c *app.RequestContext) {
var err error
var req run.CancelChatApiRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
resp, err := conversation.ConversationOpenAPISVC.CancelRun(ctx, &req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
c.JSON(consts.StatusOK, resp)
}

View File

@@ -22,6 +22,7 @@ import (
"context"
"fmt"
openapiauthApp "github.com/coze-dev/coze-studio/backend/application/openauth"
"github.com/coze-dev/coze-studio/backend/application/plugin"
"github.com/coze-dev/coze-studio/backend/application/singleagent"
"github.com/coze-dev/coze-studio/backend/application/upload"
@@ -73,7 +74,7 @@ func UploadFileOpen(ctx context.Context, c *app.RequestContext) {
var req bot_open_api.UploadFileOpenRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
invalidParamRequestResponse(c, err.Error())
return
}
@@ -83,6 +84,7 @@ func UploadFileOpen(ctx context.Context, c *app.RequestContext) {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -93,7 +95,7 @@ func GetBotOnlineInfo(ctx context.Context, c *app.RequestContext) {
var req bot_open_api.GetBotOnlineInfoReq
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
invalidParamRequestResponse(c, err.Error())
return
}
@@ -105,3 +107,43 @@ func GetBotOnlineInfo(ctx context.Context, c *app.RequestContext) {
}
c.JSON(consts.StatusOK, resp)
}
// ImpersonateCozeUser .
// @router /api/permission_api/coze_web_app/impersonate_coze_user [POST]
func ImpersonateCozeUser(ctx context.Context, c *app.RequestContext) {
var err error
var req bot_open_api.ImpersonateCozeUserRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
resp, err := openapiauthApp.OpenAuthApplication.ImpersonateCozeUserAccessToken(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
// OpenGetBotInfo .
// @router /v1/bots/:bot_id [GET]
func OpenGetBotInfo(ctx context.Context, c *app.RequestContext) {
var err error
var req bot_open_api.OpenGetBotInfoRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
resp, err := singleagent.SingleAgentSVC.OpenGetBotInfo(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}

View File

@@ -170,3 +170,42 @@ func ListConversationsApi(ctx context.Context, c *app.RequestContext) {
c.JSON(consts.StatusOK, resp)
}
// UpdateConversationApi .
// @router /v1/conversations/:conversation_id [PUT]
func UpdateConversationApi(ctx context.Context, c *app.RequestContext) {
var err error
var req conversation.UpdateConversationApiRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
resp, err := application.ConversationSVC.UpdateConversation(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
// DeleteConversationApi .
// @router /v1/conversations/:conversation_id [DELETE]
func DeleteConversationApi(ctx context.Context, c *app.RequestContext) {
var err error
var req conversation.DeleteConversationApiRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
resp, err := application.ConversationSVC.DeleteConversation(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}

View File

@@ -404,3 +404,23 @@ func DraftProjectCopy(ctx context.Context, c *app.RequestContext) {
c.JSON(consts.StatusOK, resp)
}
// GetOnlineAppData .
// @router /v1/apps/:app_id [GET]
func GetOnlineAppData(ctx context.Context, c *app.RequestContext) {
var err error
var req project.GetOnlineAppDataRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp, err := appApplication.APPApplicationSVC.GetOnlineAppData(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}

View File

@@ -140,7 +140,7 @@ func GetApiMessageList(ctx context.Context, c *app.RequestContext) {
return
}
resp, err := application.OpenapiMessageApplicationService.GetApiMessageList(ctx, &req)
resp, err := application.OpenapiMessageSVC.GetApiMessageList(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return

View File

@@ -1,3 +1,19 @@
/*
* 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.
*/
// Code generated by hertz generator.
package coze
@@ -21,7 +37,7 @@ func CommonUpload(ctx context.Context, c *app.RequestContext) {
var req upload.CommonUploadRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
invalidParamRequestResponse(c, err.Error())
return
}
fullUrl := string(c.Request.URI().FullURI())
@@ -41,7 +57,7 @@ func ApplyUploadAction(ctx context.Context, c *app.RequestContext) {
var req upload.ApplyUploadActionRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
invalidParamRequestResponse(c, err.Error())
return
}
resp := new(upload.ApplyUploadActionResponse)

View File

@@ -23,6 +23,8 @@ import (
"errors"
"fmt"
"io"
"net/url"
"strconv"
"github.com/cloudwego/eino/schema"
"github.com/cloudwego/hertz/pkg/app"
@@ -555,7 +557,11 @@ func CreateProjectConversationDef(ctx context.Context, c *app.RequestContext) {
return
}
resp := new(workflow.CreateProjectConversationDefResponse)
resp, err := appworkflow.SVC.CreateApplicationConversationDef(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -570,8 +576,11 @@ func UpdateProjectConversationDef(ctx context.Context, c *app.RequestContext) {
invalidParamRequestResponse(c, err.Error())
return
}
resp := new(workflow.UpdateProjectConversationDefResponse)
resp, err := appworkflow.SVC.UpdateApplicationConversationDef(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -587,7 +596,11 @@ func DeleteProjectConversationDef(ctx context.Context, c *app.RequestContext) {
return
}
resp := new(workflow.DeleteProjectConversationDefResponse)
resp, err := appworkflow.SVC.DeleteApplicationConversationDef(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -603,7 +616,11 @@ func ListProjectConversationDef(ctx context.Context, c *app.RequestContext) {
return
}
resp := new(workflow.ListProjectConversationResponse)
resp, err := appworkflow.SVC.ListApplicationConversationDef(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -723,7 +740,11 @@ func GetChatFlowRole(ctx context.Context, c *app.RequestContext) {
return
}
resp := new(workflow.GetChatFlowRoleResponse)
resp, err := appworkflow.SVC.GetChatFlowRole(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -738,8 +759,11 @@ func CreateChatFlowRole(ctx context.Context, c *app.RequestContext) {
invalidParamRequestResponse(c, err.Error())
return
}
resp := new(workflow.CreateChatFlowRoleResponse)
resp, err := appworkflow.SVC.CreateChatFlowRole(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -755,7 +779,11 @@ func DeleteChatFlowRole(ctx context.Context, c *app.RequestContext) {
return
}
resp := new(workflow.DeleteChatFlowRoleResponse)
resp, err := appworkflow.SVC.DeleteChatFlowRole(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
@@ -1061,6 +1089,11 @@ func OpenAPIGetWorkflowRunHistory(ctx context.Context, c *app.RequestContext) {
// @router /v1/workflows/chat [POST]
func OpenAPIChatFlowRun(ctx context.Context, c *app.RequestContext) {
var err error
if err = preprocessWorkflowRequestBody(ctx, c); err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
var req workflow.ChatFlowRunRequest
err = c.BindAndValidate(&req)
if err != nil {
@@ -1068,27 +1101,107 @@ func OpenAPIChatFlowRun(ctx context.Context, c *app.RequestContext) {
return
}
resp := new(workflow.ChatFlowRunResponse)
w := sse.NewWriter(c)
c.SetContentType("text/event-stream; charset=utf-8")
c.Response.Header.Set("Cache-Control", "no-cache")
c.Response.Header.Set("Connection", "keep-alive")
c.Response.Header.Set("Access-Control-Allow-Origin", "*")
c.JSON(consts.StatusOK, resp)
sr, err := appworkflow.SVC.OpenAPIChatFlowRun(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
sendChatFlowStreamRunSSE(ctx, w, sr)
}
func sendChatFlowStreamRunSSE(ctx context.Context, w *sse.Writer, sr *schema.StreamReader[[]*workflow.ChatFlowRunResponse]) {
defer func() {
_ = w.Close()
sr.Close()
}()
seq := int64(1)
for {
respList, err := sr.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
// finish
break
}
event := &sse.Event{
Type: "error",
Data: []byte(err.Error()),
}
if err = w.Write(event); err != nil {
logs.CtxErrorf(ctx, "publish stream event failed, err:%v", err)
}
return
}
for _, resp := range respList {
event := &sse.Event{
ID: strconv.FormatInt(seq, 10),
Type: resp.Event,
Data: []byte(resp.Data),
}
if err = w.Write(event); err != nil {
logs.CtxErrorf(ctx, "publish stream event failed, err:%v", err)
return
}
seq++
}
}
}
// OpenAPIGetWorkflowInfo .
// @router /v1/workflows/:workflow_id [GET]
func OpenAPIGetWorkflowInfo(ctx context.Context, c *app.RequestContext) {
var err error
if err = processOpenAPIGetWorkflowInfoRequest(ctx, c); err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
var req workflow.OpenAPIGetWorkflowInfoRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
resp := new(workflow.OpenAPIGetWorkflowInfoResponse)
resp, err := appworkflow.SVC.OpenAPIGetWorkflowInfo(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
func processOpenAPIGetWorkflowInfoRequest(_ context.Context, c *app.RequestContext) error {
queryString := c.Request.QueryString()
values, err := url.ParseQuery(string(queryString))
if err != nil {
return fmt.Errorf("parse query parameter failed, err:%v", err)
}
isDebug := values.Get("is_debug")
if len(isDebug) == 0 {
values.Set("is_debug", "false")
}
c.Request.SetQueryString(values.Encode())
return nil
}
// GetHistorySchema .
// @router /api/workflow_api/history_schema [POST]
func GetHistorySchema(ctx context.Context, c *app.RequestContext) {
@@ -1128,3 +1241,22 @@ func GetExampleWorkFlowList(ctx context.Context, c *app.RequestContext) {
c.JSON(consts.StatusOK, resp)
}
// OpenAPICreateConversation .
// @router /v1/workflow/conversation/create [POST]
func OpenAPICreateConversation(ctx context.Context, c *app.RequestContext) {
var err error
var req workflow.CreateConversationRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp, err := appworkflow.SVC.OpenAPICreateConversation(ctx, &req)
if err != nil {
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}

View File

@@ -21,6 +21,7 @@ import (
"context"
"errors"
"fmt"
"net/http"
"os"
"reflect"
@@ -42,6 +43,7 @@ import (
"github.com/cloudwego/hertz/pkg/common/ut"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/protocol/sse"
"github.com/coze-dev/coze-studio/backend/domain/workflow/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
@@ -250,7 +252,10 @@ func newWfTestRunner(t *testing.T) *wfTestRunner {
mockTos := storageMock.NewMockStorage(ctrl)
mockTos.EXPECT().GetObjectUrl(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
workflowRepo := service.NewWorkflowRepository(mockIDGen, db, redisClient, mockTos, cpStore, utChatModel, nil)
workflowRepo, _ := service.NewWorkflowRepository(mockIDGen, db, redisClient, mockTos, cpStore, utChatModel, &config.WorkflowConfig{
NodeOfCodeConfig: &config.NodeOfCodeConfig{},
})
mockey.Mock(appworkflow.GetWorkflowDomainSVC).Return(service.NewWorkflowService(workflowRepo)).Build()
mockey.Mock(workflow2.GetRepository).Return(workflowRepo).Build()
publishPatcher := mockey.Mock(appworkflow.PublishWorkflowResource).Return(nil).Build()
@@ -4100,13 +4105,13 @@ func TestCopyWorkflowAppToLibrary(t *testing.T) {
assert.NoError(t, err)
validateSubWorkflowIDs(subworkflowCanvas.Nodes)
case entity.NodeTypeLLM:
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
assert.True(t, copiedIDMap[w.WorkflowID])
}
}
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
for _, p := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
if p.PluginVersion == "0" {
assert.Equal(t, "100100", p.PluginID)
@@ -4114,7 +4119,7 @@ func TestCopyWorkflowAppToLibrary(t *testing.T) {
}
}
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
for _, k := range node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList {
assert.Equal(t, "100100", k.ID)
}