Skip to content

Commit 72b91be

Browse files
committed
feat: 工作流增加嵌入应用
--story=1016666 --user=王孝刚 【北区-售前】工作流支持嵌入应用 https://www.tapd.cn/57709429/s/1608362
1 parent 6e6f564 commit 72b91be

File tree

22 files changed

+549
-38
lines changed

22 files changed

+549
-38
lines changed

apps/application/flow/step_node/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@desc:
88
"""
99
from .ai_chat_step_node import *
10+
from .application_node import BaseApplicationNode
1011
from .condition_node import *
1112
from .question_node import *
1213
from .search_dataset_node import *
@@ -17,7 +18,7 @@
1718
from .reranker_node import *
1819

1920
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode,
20-
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode]
21+
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode]
2122

2223

2324
def get_node(node_type):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# coding=utf-8
2+
from .impl import *
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# coding=utf-8
2+
from typing import Type
3+
4+
from rest_framework import serializers
5+
6+
from application.flow.i_step_node import INode, NodeResult
7+
from common.util.field_message import ErrMessage
8+
9+
10+
class ApplicationNodeSerializer(serializers.Serializer):
11+
application_id = serializers.CharField(required=True, error_messages=ErrMessage.char("应用id"))
12+
question_reference_address = serializers.ListField(required=True, error_messages=ErrMessage.list("用户问题"))
13+
api_input_field_list = serializers.ListField(required=False, error_messages=ErrMessage.list("api输入字段"))
14+
user_input_field_list = serializers.ListField(required=False, error_messages=ErrMessage.uuid("用户输入字段"))
15+
16+
17+
class IApplicationNode(INode):
18+
type = 'application-node'
19+
20+
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
21+
return ApplicationNodeSerializer
22+
23+
def _run(self):
24+
question = self.workflow_manage.get_reference_field(
25+
self.node_params_serializer.data.get('question_reference_address')[0],
26+
self.node_params_serializer.data.get('question_reference_address')[1:])
27+
kwargs = {}
28+
for api_input_field in self.node_params_serializer.data.get('api_input_field_list', []):
29+
kwargs[api_input_field['variable']] = self.workflow_manage.get_reference_field(api_input_field['value'][0],
30+
api_input_field['value'][1:])
31+
for user_input_field in self.node_params_serializer.data.get('user_input_field_list', []):
32+
kwargs[user_input_field['field']] = self.workflow_manage.get_reference_field(user_input_field['value'][0],
33+
user_input_field['value'][1:])
34+
35+
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data,
36+
message=str(question), **kwargs)
37+
38+
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
39+
**kwargs) -> NodeResult:
40+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# coding=utf-8
2+
from .base_application_node import BaseApplicationNode
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# coding=utf-8
2+
import json
3+
import time
4+
import uuid
5+
from typing import List, Dict
6+
from application.flow.i_step_node import NodeResult, INode
7+
from application.flow.step_node.application_node.i_application_node import IApplicationNode
8+
from application.models import Chat
9+
from common.handle.impl.response.openai_to_response import OpenaiToResponse
10+
11+
12+
def string_to_uuid(input_str):
13+
return str(uuid.uuid5(uuid.NAMESPACE_DNS, input_str))
14+
15+
16+
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str):
17+
result = node_variable.get('result')
18+
node.context['message_tokens'] = result.get('usage', {}).get('prompt_tokens', 0)
19+
node.context['answer_tokens'] = result.get('usage', {}).get('completion_tokens', 0)
20+
node.context['answer'] = answer
21+
node.context['question'] = node_variable['question']
22+
node.context['run_time'] = time.time() - node.context['start_time']
23+
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
24+
workflow.answer += answer
25+
26+
27+
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
28+
"""
29+
写入上下文数据 (流式)
30+
@param node_variable: 节点数据
31+
@param workflow_variable: 全局数据
32+
@param node: 节点
33+
@param workflow: 工作流管理器
34+
"""
35+
response = node_variable.get('result')
36+
answer = ''
37+
usage = {}
38+
for chunk in response:
39+
# 先把流转成字符串
40+
response_content = chunk.decode('utf-8')[6:]
41+
response_content = json.loads(response_content)
42+
choices = response_content.get('choices')
43+
if choices and isinstance(choices, list) and len(choices) > 0:
44+
content = choices[0].get('delta', {}).get('content', '')
45+
answer += content
46+
yield content
47+
usage = response_content.get('usage', {})
48+
node_variable['result'] = {'usage': usage}
49+
_write_context(node_variable, workflow_variable, node, workflow, answer)
50+
51+
52+
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
53+
"""
54+
写入上下文数据
55+
@param node_variable: 节点数据
56+
@param workflow_variable: 全局数据
57+
@param node: 节点实例对象
58+
@param workflow: 工作流管理器
59+
"""
60+
response = node_variable.get('result')['choices'][0]['message']
61+
answer = response.get('content', '') or "抱歉,没有查找到相关内容,请重新描述您的问题或提供更多信息。"
62+
_write_context(node_variable, workflow_variable, node, workflow, answer)
63+
64+
65+
class BaseApplicationNode(IApplicationNode):
66+
67+
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
68+
**kwargs) -> NodeResult:
69+
from application.serializers.chat_message_serializers import ChatMessageSerializer
70+
# 生成嵌入应用的chat_id
71+
current_chat_id = string_to_uuid(chat_id + application_id)
72+
Chat.objects.get_or_create(id=current_chat_id, defaults={
73+
'application_id': application_id,
74+
'abstract': message
75+
})
76+
response = ChatMessageSerializer(
77+
data={'chat_id': current_chat_id, 'message': message,
78+
're_chat': re_chat,
79+
'stream': stream,
80+
'application_id': application_id,
81+
'client_id': client_id,
82+
'client_type': client_type, 'form_data': kwargs}).chat(base_to_response=OpenaiToResponse())
83+
if response.status_code == 200:
84+
if stream:
85+
content_generator = response.streaming_content
86+
return NodeResult({'result': content_generator, 'question': message}, {},
87+
_write_context=write_context_stream)
88+
else:
89+
data = json.loads(response.content)
90+
return NodeResult({'result': data, 'question': message}, {},
91+
_write_context=write_context)
92+
93+
def get_details(self, index: int, **kwargs):
94+
global_fields = []
95+
for api_input_field in self.node_params_serializer.data.get('api_input_field_list', []):
96+
global_fields.append({
97+
'label': api_input_field['variable'],
98+
'key': api_input_field['variable'],
99+
'value': self.workflow_manage.get_reference_field(
100+
api_input_field['value'][0],
101+
api_input_field['value'][1:])
102+
})
103+
for user_input_field in self.node_params_serializer.data.get('user_input_field_list', []):
104+
global_fields.append({
105+
'label': user_input_field['label'],
106+
'key': user_input_field['field'],
107+
'value': self.workflow_manage.get_reference_field(
108+
user_input_field['value'][0],
109+
user_input_field['value'][1:])
110+
})
111+
return {
112+
'name': self.node.properties.get('stepName'),
113+
"index": index,
114+
"info": self.node.properties.get('node_data'),
115+
'run_time': self.context.get('run_time'),
116+
'question': self.context.get('question'),
117+
'answer': self.context.get('answer'),
118+
'type': self.node.type,
119+
'message_tokens': self.context.get('message_tokens'),
120+
'answer_tokens': self.context.get('answer_tokens'),
121+
'status': self.status,
122+
'err_message': self.err_message,
123+
'global_fields': global_fields
124+
}

apps/application/flow/workflow_manage.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(self, _id: str, _type: str, x: int, y: int, properties: dict, **kwa
5353
self.__setattr__(keyword, kwargs.get(keyword))
5454

5555

56-
end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node']
56+
end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node', 'application-node']
5757

5858

5959
class Flow:

apps/application/serializers/application_serializers.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,11 @@
1515
from functools import reduce
1616
from typing import Dict, List
1717

18-
from django.conf import settings
1918
from django.contrib.postgres.fields import ArrayField
2019
from django.core import cache, validators
2120
from django.core import signing
22-
from django.core.paginator import Paginator
2321
from django.db import transaction, models
2422
from django.db.models import QuerySet, Q
25-
from django.forms import CharField
2623
from django.http import HttpResponse
2724
from django.template import Template, Context
2825
from rest_framework import serializers
@@ -46,10 +43,9 @@
4643
from dataset.serializers.common_serializers import list_paragraph, get_embedding_model_by_dataset_id_list
4744
from embedding.models import SearchMode
4845
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer
49-
from setting.models import AuthOperate, TeamMember, TeamMemberPermission
46+
from setting.models import AuthOperate
5047
from setting.models.model_management import Model
5148
from setting.models_provider import get_model_credential
52-
from setting.models_provider.constants.model_provider_constants import ModelProvideConstants
5349
from setting.models_provider.tools import get_model_instance_by_model_user_id
5450
from setting.serializers.provider_serializers import ModelSerializer
5551
from smartdoc.conf import PROJECT_DIR
@@ -979,6 +975,17 @@ def play_demo_text(self, form_data, with_valid=True):
979975
model = get_model_instance_by_model_user_id(tts_model_id, application.user_id, **form_data)
980976
return model.text_to_speech(text)
981977

978+
def application_list(self, with_valid=True):
979+
if with_valid:
980+
self.is_valid(raise_exception=True)
981+
user_id = self.data.get('user_id')
982+
application_id = self.data.get('application_id')
983+
application = Application.objects.filter(user_id=user_id).exclude(id=application_id)
984+
# 把应用的type为WORK_FLOW的应用放到最上面 然后再按名称排序
985+
serialized_data = ApplicationSerializerModel(application, many=True).data
986+
application = sorted(serialized_data, key=lambda x: (x['type'] != 'WORK_FLOW', x['name']))
987+
return list(application)
988+
982989
class ApplicationKeySerializerModel(serializers.ModelSerializer):
983990
class Meta:
984991
model = ApplicationApiKey

apps/application/serializers/chat_message_serializers.py

+2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ def chat_work_flow(self, chat_info: ChatInfo, base_to_response):
305305
'chat_id': chat_info.chat_id, 'chat_record_id': str(uuid.uuid1()),
306306
'stream': stream,
307307
're_chat': re_chat,
308+
'client_id': client_id,
309+
'client_type': client_type,
308310
'user_id': user_id}, WorkFlowPostHandler(chat_info, client_id, client_type),
309311
base_to_response, form_data)
310312
r = work_flow_manage.run()

apps/application/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
path('application/<str:application_id>/function_lib', views.Application.FunctionLib.as_view()),
2323
path('application/<str:application_id>/function_lib/<str:function_lib_id>',
2424
views.Application.FunctionLib.Operate.as_view()),
25+
path('application/<str:application_id>/application', views.Application.Application.as_view()),
2526
path('application/<str:application_id>/model_params_form/<str:model_id>',
2627
views.Application.ModelParamsForm.as_view()),
2728
path('application/<str:application_id>/hit_test', views.Application.HitTest.as_view()),

apps/application/views/application_views.py

+18
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,24 @@ def get(self, request: Request, application_id: str, function_lib_id: str):
243243
data={'application_id': application_id,
244244
'user_id': request.user.id}).get_function_lib(function_lib_id))
245245

246+
class Application(APIView):
247+
authentication_classes = [TokenAuth]
248+
249+
@action(methods=['GET'], detail=False)
250+
@swagger_auto_schema(operation_summary="获取当前人创建的应用列表",
251+
operation_id="获取当前人创建的应用列表",
252+
tags=["应用/会话"])
253+
@has_permissions(ViewPermission(
254+
[RoleConstants.ADMIN, RoleConstants.USER],
255+
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
256+
dynamic_tag=keywords.get('application_id'))],
257+
compare=CompareConstants.AND))
258+
def get(self, request: Request, application_id: str):
259+
return result.success(
260+
ApplicationSerializer.Operate(
261+
data={'application_id': application_id,
262+
'user_id': request.user.id}).application_list())
263+
246264
class Profile(APIView):
247265
authentication_classes = [TokenAuth]
248266

apps/application/views/chat_views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ChatView(APIView):
4343
class Export(APIView):
4444
authentication_classes = [TokenAuth]
4545

46-
@action(methods=['GET'], detail=False)
46+
@action(methods=['POST'], detail=False)
4747
@swagger_auto_schema(operation_summary="导出对话",
4848
operation_id="导出对话",
4949
manual_parameters=ChatApi.get_request_params_api(),

ui/src/api/application.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,18 @@ const listFunctionLib: (application_id: String, loading?: Ref<boolean>) => Promi
309309
) => {
310310
return get(`${prefix}/${application_id}/function_lib`, undefined, loading)
311311
}
312+
/**
313+
* 获取当前人的所有应用列表
314+
* @param application_id 应用id
315+
* @param loading
316+
* @returns
317+
*/
318+
export const getApplicationList: (
319+
application_id: string,
320+
loading?: Ref<boolean>
321+
) => Promise<Result<any>> = (application_id, loading) => {
322+
return get(`${prefix}/${application_id}/application`, undefined, loading)
323+
}
312324
/**
313325
* 获取应用所属的函数库
314326
* @param application_id
@@ -500,5 +512,6 @@ export default {
500512
getWorkFlowVersionDetail,
501513
putWorkFlowVersion,
502514
playDemoText,
503-
getUserList
515+
getUserList,
516+
getApplicationList
504517
}

ui/src/components/ai-chat/ExecutionDetailDialog.vue

+28-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
<el-icon class="mr-8 arrow-icon" :class="current === index ? 'rotate-90' : ''"
1818
><CaretRight
1919
/></el-icon>
20-
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8" :size="24" />
20+
<component
21+
:is="iconComponent(`${item.type}-icon`)"
22+
class="mr-8"
23+
:size="24"
24+
:item="item.info"
25+
/>
2126
<h4>{{ item.name }}</h4>
2227
</div>
2328
<div class="flex align-center">
@@ -37,7 +42,11 @@
3742
<div class="mt-12" v-if="current === index">
3843
<template v-if="item.status === 200">
3944
<!-- 开始 -->
40-
<template v-if="item.type === WorkflowType.Start">
45+
<template
46+
v-if="
47+
item.type === WorkflowType.Start || item.type === WorkflowType.Application
48+
"
49+
>
4150
<div class="card-never border-r-4">
4251
<h5 class="p-8-12">参数输入</h5>
4352
<div class="p-8-12 border-t-dashed lighter">
@@ -84,15 +93,25 @@
8493
</template>
8594
<!-- AI 对话 / 问题优化-->
8695
<template
87-
v-if="item.type == WorkflowType.AiChat || item.type == WorkflowType.Question"
96+
v-if="
97+
item.type == WorkflowType.AiChat ||
98+
item.type == WorkflowType.Question ||
99+
item.type == WorkflowType.Application
100+
"
88101
>
89-
<div class="card-never border-r-4">
102+
<div
103+
class="card-never border-r-4"
104+
v-if="item.type !== WorkflowType.Application"
105+
>
90106
<h5 class="p-8-12">角色设定 (System)</h5>
91107
<div class="p-8-12 border-t-dashed lighter">
92108
{{ item.system || '-' }}
93109
</div>
94110
</div>
95-
<div class="card-never border-r-4 mt-8">
111+
<div
112+
class="card-never border-r-4 mt-8"
113+
v-if="item.type !== WorkflowType.Application"
114+
>
96115
<h5 class="p-8-12">历史记录</h5>
97116
<div class="p-8-12 border-t-dashed lighter">
98117
<template v-if="item.history_message?.length > 0">
@@ -115,7 +134,9 @@
115134
</div>
116135
</div>
117136
<div class="card-never border-r-4 mt-8">
118-
<h5 class="p-8-12">AI 回答</h5>
137+
<h5 class="p-8-12">
138+
{{ item.type == WorkflowType.Application ? '应用回答' : 'AI 回答' }}
139+
</h5>
119140
<div class="p-8-12 border-t-dashed lighter">
120141
<MdPreview
121142
v-if="item.answer"
@@ -269,7 +290,7 @@ watch(dialogVisible, (bool) => {
269290
270291
const open = (data: any) => {
271292
detail.value = cloneDeep(data)
272-
293+
console.log(detail.value)
273294
dialogVisible.value = true
274295
}
275296
onBeforeUnmount(() => {

0 commit comments

Comments
 (0)