Skip to content

Commit 0e1bc7e

Browse files
Merge remote-tracking branch 'upstream/v2' into variable-assign-support-type-convert
2 parents 136c30e + fe7e42c commit 0e1bc7e

File tree

77 files changed

+2207
-354
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2207
-354
lines changed

.github/workflows/build-and-push-python-pg.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
run: |
2626
DOCKER_IMAGE=ghcr.io/1panel-dev/maxkb-base
2727
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
28-
TAG_NAME=python3.11-pg17.7-20260323
28+
TAG_NAME=python3.11-pg17.9-20260326
2929
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
3030
echo ::set-output name=docker_image::${DOCKER_IMAGE}
3131
echo ::set-output name=version::${TAG_NAME}

apps/application/api/application_api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
ApplicationImportRequest, ApplicationEditSerializer, TextToSpeechRequest, SpeechToTextRequest, PlayDemoTextRequest
1616
from common.mixins.api_mixin import APIMixin
1717
from common.result import ResultSerializer, ResultPageSerializer, DefaultResultSerializer
18+
from knowledge.serializers.common import BatchSerializer, BatchMoveSerializer
1819

1920

2021
class ApplicationCreateRequest(ApplicationCreateSerializer.SimplateRequest):
@@ -160,6 +161,27 @@ def get_parameters():
160161
]
161162

162163

164+
class ApplicationBatchOperateAPI(APIMixin):
165+
@staticmethod
166+
def get_parameters():
167+
return [
168+
OpenApiParameter(
169+
name="workspace_id",
170+
description="工作空间id",
171+
type=OpenApiTypes.STR,
172+
location='path',
173+
required=True,
174+
)
175+
]
176+
@staticmethod
177+
def get_request():
178+
return BatchSerializer
179+
180+
@staticmethod
181+
def get_move_request():
182+
return BatchMoveSerializer
183+
184+
163185
class ApplicationExportAPI(APIMixin):
164186
@staticmethod
165187
def get_parameters():

apps/application/flow/i_step_node.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ def get_tool_workflow_state(workflow):
128128
return State.SUCCESS
129129

130130

131+
class ToolWorkflowCallPostHandler(WorkFlowPostHandler):
132+
def __init__(self, chat_info, tool_id):
133+
super().__init__(chat_info)
134+
self.tool_id = tool_id
135+
136+
def handler(self, workflow):
137+
self.chat_info = None
138+
self.tool_id = None
139+
140+
131141
class ToolWorkflowPostHandler(WorkFlowPostHandler):
132142
def __init__(self, chat_info, tool_id):
133143
super().__init__(chat_info)
@@ -140,7 +150,12 @@ def handler(self, workflow):
140150
source_type=self.chat_info.source_type,
141151
source_id=self.chat_info.source_id,
142152
state=state,
153+
run_time=time.time() - workflow.context.get('start_time') if workflow.context.get(
154+
'start_time') is not None else 0,
143155
meta={
156+
'input_field_list': workflow.get_input_field_list(),
157+
'output_field_list': workflow.get_output_field_list(),
158+
'input': workflow.get_input(),
144159
'output': workflow.out_context,
145160
'details': workflow.get_runtime_details(),
146161
'answer_text_list': workflow.get_answer_text_list()

apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,123 @@
99
import json
1010
import re
1111
import time
12+
import uuid
1213
from functools import reduce
1314
from typing import List, Dict
1415

15-
from application.flow.i_step_node import NodeResult, INode
16+
import uuid_utils.compat as uuid
17+
from django.db.models import QuerySet, OuterRef, Subquery
18+
from django.utils.translation import gettext as _
19+
from langchain_core.messages import BaseMessage, AIMessage, HumanMessage, SystemMessage
20+
from langchain_core.tools import StructuredTool
21+
from pydantic import Field, create_model
22+
23+
from application.flow.common import Workflow, WorkflowMode
24+
from application.flow.i_step_node import NodeResult, INode, ToolWorkflowPostHandler
1625
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
1726
from application.flow.tools import Reasoning, mcp_response_generator
1827
from application.models import Application, ApplicationApiKey, ApplicationAccessToken
28+
from application.serializers.common import ToolExecute
1929
from common.exception.app_exception import AppApiException
2030
from common.utils.rsa_util import rsa_long_decrypt
2131
from common.utils.shared_resource_auth import filter_authorized_ids
2232
from common.utils.tool_code import ToolExecutor
23-
from django.db.models import QuerySet
24-
from django.utils.translation import gettext as _
25-
from langchain_core.messages import BaseMessage, AIMessage, HumanMessage, SystemMessage
2633
from models_provider.models import Model
2734
from models_provider.tools import get_model_credential, get_model_instance_by_model_workspace_id
28-
from tools.models import Tool
35+
from tools.models import Tool, ToolWorkflowVersion, ToolType
36+
37+
38+
def build_schema(fields: dict):
39+
return create_model("dynamicSchema", **fields)
40+
41+
42+
def get_type(_type: str):
43+
if _type == 'float':
44+
return float
45+
if _type == 'string':
46+
return str
47+
if _type == 'int':
48+
return int
49+
if _type == 'dict':
50+
return dict
51+
if _type == 'array':
52+
return list
53+
if _type == 'boolean':
54+
return bool
55+
return object
56+
57+
58+
def get_workflow_args(tool, qv):
59+
for node in qv.work_flow.get('nodes'):
60+
if node.get('type') == 'tool-base-node':
61+
input_field_list = node.get('properties').get('user_input_field_list')
62+
return build_schema(
63+
{field.get('field'): (get_type(field.get('type')), Field(..., description=field.get('desc')))
64+
for field in input_field_list})
65+
66+
return build_schema({})
67+
68+
69+
def get_workflow_func(node, tool, qv, workspace_id):
70+
tool_id = tool.id
71+
tool_record_id = str(uuid.uuid7())
72+
took_execute = ToolExecute(tool_id, tool_record_id,
73+
workspace_id,
74+
node.workflow_manage.get_source_type(),
75+
node.workflow_manage.get_source_id(),
76+
False)
77+
78+
def inner(**kwargs):
79+
from application.flow.tool_workflow_manage import ToolWorkflowManage
80+
work_flow_manage = ToolWorkflowManage(
81+
Workflow.new_instance(qv.work_flow, WorkflowMode.TOOL),
82+
{
83+
'chat_record_id': tool_record_id,
84+
'tool_id': tool_id,
85+
'stream': True,
86+
'workspace_id': workspace_id,
87+
**kwargs},
88+
89+
ToolWorkflowPostHandler(took_execute, tool_id),
90+
is_the_task_interrupted=lambda: False,
91+
child_node=None,
92+
start_node_id=None,
93+
start_node_data=None,
94+
chat_record=None
95+
)
96+
res = work_flow_manage.run()
97+
for r in res:
98+
pass
99+
return work_flow_manage.out_context
100+
101+
return inner
102+
103+
104+
def get_tools(node, tool_workflow_ids, workspace_id):
105+
tools = QuerySet(Tool).filter(id__in=tool_workflow_ids, tool_type=ToolType.WORKFLOW, workspace_id=workspace_id)
106+
latest_subquery = ToolWorkflowVersion.objects.filter(
107+
tool_id=OuterRef('tool_id')
108+
).order_by('-create_time')
109+
110+
qs = ToolWorkflowVersion.objects.filter(
111+
tool_id__in=[t.id for t in tools],
112+
id=Subquery(latest_subquery.values('id')[:1])
113+
)
114+
qd = {q.tool_id: q for q in qs}
115+
results = []
116+
for tool in tools:
117+
qv = qd.get(tool.id)
118+
func = get_workflow_func(node, tool, qv, workspace_id)
119+
args = get_workflow_args(tool, qv)
120+
tool = StructuredTool.from_function(
121+
func=func,
122+
name=tool.name,
123+
description=tool.desc,
124+
args_schema=args,
125+
)
126+
results.append(tool)
127+
128+
return results
29129

30130

31131
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
@@ -178,7 +278,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
178278
model_id = reference_data.get('model_id', model_id)
179279
model_params_setting = reference_data.get('model_params_setting')
180280

181-
if model_params_setting is None and model_id:
281+
if model_params_setting is None and model_id:
182282
model_params_setting = get_default_model_params_setting(model_id)
183283

184284
if model_setting is None:
@@ -187,7 +287,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
187287
self.context['model_setting'] = model_setting
188288
workspace_id = self.workflow_manage.get_body().get('workspace_id')
189289
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id,
190-
**model_params_setting)
290+
**(model_params_setting or {}))
191291
history_message = self.get_history_message(history_chat_record, dialogue_number, dialogue_type,
192292
self.runtime_node_id)
193293
self.context['history_message'] = [{'content': message.content, 'role': message.type} for message in
@@ -216,7 +316,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
216316
mcp_result = self._handle_mcp_request(
217317
mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
218318
application_ids, skill_tool_ids, mcp_output_enable,
219-
chat_model, message_list, history_message, question, chat_id
319+
chat_model, message_list, history_message, question, chat_id, workspace_id
220320
)
221321
if mcp_result:
222322
return mcp_result
@@ -236,7 +336,8 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
236336

237337
def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
238338
application_ids, skill_tool_ids,
239-
mcp_output_enable, chat_model, message_list, history_message, question, chat_id):
339+
mcp_output_enable, chat_model, message_list, history_message, question, chat_id,
340+
workspace_id):
240341

241342
mcp_servers_config = {}
242343

@@ -259,11 +360,12 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids
259360
mcp_servers_config = {**mcp_servers_config, **json.loads(mcp_tool['code'])}
260361
mcp_servers_config = self.handle_variables(mcp_servers_config)
261362
tool_init_params = {}
363+
tools = get_tools(self, tool_ids, workspace_id)
262364
if tool_ids and len(tool_ids) > 0: # 如果有工具ID,则将其转换为MCP
263365
self.context['tool_ids'] = tool_ids
264366
for tool_id in tool_ids:
265-
tool = QuerySet(Tool).filter(id=tool_id).first()
266-
if not tool.is_active:
367+
tool = QuerySet(Tool).filter(id=tool_id, tool_type=ToolType.CUSTOM).first()
368+
if tool is None or not tool.is_active:
267369
continue
268370
executor = ToolExecutor()
269371
if tool.init_params is not None:
@@ -323,7 +425,7 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids
323425
})
324426
mcp_servers_config['skills'] = skill_file_items
325427

326-
if len(mcp_servers_config) > 0:
428+
if len(mcp_servers_config) > 0 or len(tools) > 0:
327429
# 安全获取 application
328430
application_id = None
329431
if (self.workflow_manage and
@@ -334,7 +436,7 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids
334436
source_id = application_id or knowledge_id
335437
source_type = 'APPLICATION' if application_id else 'KNOWLEDGE'
336438
r = mcp_response_generator(chat_model, message_list, json.dumps(mcp_servers_config), mcp_output_enable,
337-
tool_init_params, source_id, source_type, chat_id)
439+
tool_init_params, source_id, source_type, chat_id, tools)
338440
return NodeResult(
339441
{'result': r, 'chat_model': chat_model, 'message_list': message_list,
340442
'history_message': [{'content': message.content, 'role': message.type} for message in

apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from application.flow.step_node.image_generate_step_node.i_image_generate_node import IImageGenerateNode
1111
from common.utils.common import bytes_to_uploaded_file
1212
from knowledge.models import FileSourceType
13-
from oss.serializers.file import FileSerializer
1413
from models_provider.tools import get_model_instance_by_model_workspace_id
14+
from oss.serializers.file import FileSerializer
1515

1616

1717
class BaseImageGenerateNode(IImageGenerateNode):
@@ -117,6 +117,8 @@ def upload_file(self, file):
117117
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__(
118118
self.workflow_manage.flow.workflow_mode):
119119
return self.upload_knowledge_file(file)
120+
if [WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP].__contains__(self.workflow_manage.flow.workflow_mode):
121+
return self.upload_tool_file(file)
120122
return self.upload_application_file(file)
121123

122124
def upload_knowledge_file(self, file):
@@ -133,6 +135,20 @@ def upload_knowledge_file(self, file):
133135
}).upload()
134136
return file_url
135137

138+
def upload_tool_file(self, file):
139+
tool_id = self.workflow_params.get('tool_id')
140+
meta = {
141+
'debug': False,
142+
'tool_id': tool_id,
143+
}
144+
file_url = FileSerializer(data={
145+
'file': file,
146+
'meta': meta,
147+
'source_id': tool_id,
148+
'source_type': FileSourceType.TOOL.value
149+
}).upload()
150+
return file_url
151+
136152
def upload_application_file(self, file):
137153
application = self.workflow_manage.work_flow_post_handler.chat_info.application
138154
chat_id = self.workflow_params.get('chat_id')

apps/application/flow/step_node/image_to_video_step_node/i_image_to_video_node.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111

1212
class ImageToVideoNodeSerializer(serializers.Serializer):
13-
model_id = serializers.CharField(required=True, label=_("Model id"))
13+
model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
14+
model_id_type = serializers.CharField(required=False, default='custom', label=_("Model id type"))
15+
model_id_reference = serializers.ListField(required=False, child=serializers.CharField(), allow_empty=True,
16+
label=_("Reference Field"))
1417

1518
prompt = serializers.CharField(required=True, label=_("Prompt word (positive)"))
1619

@@ -69,5 +72,6 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t
6972
model_params_setting,
7073
chat_record_id,
7174
first_frame_url, last_frame_url,
75+
model_id_type=None, model_id_reference=None,
7276
**kwargs) -> NodeResult:
7377
pass

apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,21 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t
2929
model_params_setting,
3030
chat_record_id,
3131
first_frame_url, last_frame_url=None,
32+
model_id_type=None, model_id_reference=None,
3233
**kwargs) -> NodeResult:
34+
# 处理引用类型
35+
if model_id_type == 'reference' and model_id_reference:
36+
reference_data = self.workflow_manage.get_reference_field(
37+
model_id_reference[0],
38+
model_id_reference[1:],
39+
)
40+
if reference_data and isinstance(reference_data, dict):
41+
model_id = reference_data.get('model_id', model_id)
42+
model_params_setting = reference_data.get('model_params_setting')
43+
3344
workspace_id = self.workflow_manage.get_body().get('workspace_id')
3445
ttv_model = get_model_instance_by_model_workspace_id(model_id, workspace_id,
35-
**model_params_setting)
46+
**(model_params_setting or {}))
3647
history_message = self.get_history_message(history_chat_record, dialogue_number)
3748
self.context['history_message'] = history_message
3849
question = self.generate_prompt_question(prompt)
@@ -83,6 +94,8 @@ def upload_file(self, file):
8394
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__(
8495
self.workflow_manage.flow.workflow_mode):
8596
return self.upload_knowledge_file(file)
97+
if [WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP].__contains__(self.workflow_manage.flow.workflow_mode):
98+
return self.upload_tool_file(file)
8699
return self.upload_application_file(file)
87100

88101
def upload_knowledge_file(self, file):
@@ -99,6 +112,20 @@ def upload_knowledge_file(self, file):
99112
}).upload()
100113
return file_url
101114

115+
def upload_tool_file(self, file):
116+
tool_id = self.workflow_params.get('tool_id')
117+
meta = {
118+
'debug': False,
119+
'tool_id': tool_id,
120+
}
121+
file_url = FileSerializer(data={
122+
'file': file,
123+
'meta': meta,
124+
'source_id': tool_id,
125+
'source_type': FileSourceType.TOOL.value
126+
}).upload()
127+
return file_url
128+
102129
def upload_application_file(self, file):
103130
application = self.workflow_manage.work_flow_post_handler.chat_info.application
104131
chat_id = self.workflow_params.get('chat_id')

0 commit comments

Comments
 (0)