Skip to content

Commit afb6c62

Browse files
committed
feat: add skill tool type and upload functionality
1 parent d7c1aff commit afb6c62

File tree

31 files changed

+1082
-24
lines changed

31 files changed

+1082
-24
lines changed

apps/application/chat_pipeline/step/chat_step/i_chat_step.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class InstanceSerializer(serializers.Serializer):
8888
mcp_source = serializers.CharField(label="MCP Source", required=False, default="referencing")
8989
tool_ids = serializers.JSONField(label="工具ID列表", required=False, default=list)
9090
application_ids = serializers.JSONField(label="应用ID列表", required=False, default=list)
91+
skill_tool_ids = serializers.JSONField(label="技能ID列表", required=False, default=list)
9192
mcp_output_enable = serializers.BooleanField(label="MCP输出是否启用", required=False, default=True)
9293

9394
def is_valid(self, *, raise_exception=False):
@@ -115,6 +116,6 @@ def execute(self, message_list: List[BaseMessage],
115116
padding_problem_text: str = None, stream: bool = True, chat_user_id=None, chat_user_type=None,
116117
no_references_setting=None, model_params_setting=None, model_setting=None,
117118
mcp_tool_ids=None, mcp_servers='', mcp_source="referencing",
118-
tool_ids=None, application_ids=None, mcp_output_enable=True,
119+
tool_ids=None, application_ids=None, skill_tool_ids=None, mcp_output_enable=True,
119120
**kwargs):
120121
pass

apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def execute(self, message_list: List[BaseMessage],
178178
mcp_source="referencing",
179179
tool_ids=None,
180180
application_ids=None,
181+
skill_tool_ids=None,
181182
mcp_output_enable=True,
182183
**kwargs):
183184
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id,
@@ -190,6 +191,7 @@ def execute(self, message_list: List[BaseMessage],
190191
model_setting,
191192
mcp_tool_ids, mcp_servers, mcp_source, tool_ids,
192193
application_ids,
194+
skill_tool_ids,
193195
mcp_output_enable)
194196
else:
195197
return self.execute_block(message_list, chat_id, problem_text, post_response_handler, chat_model,
@@ -198,6 +200,7 @@ def execute(self, message_list: List[BaseMessage],
198200
model_setting,
199201
mcp_tool_ids, mcp_servers, mcp_source, tool_ids,
200202
application_ids,
203+
skill_tool_ids,
201204
mcp_output_enable)
202205

203206
def get_details(self, manage, **kwargs):
@@ -225,7 +228,7 @@ def reset_message_list(message_list: List[BaseMessage], answer_text):
225228
return result
226229

227230
def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_ids, tool_ids,
228-
application_ids, mcp_output_enable, chat_model, message_list, agent_id):
231+
application_ids, skill_tool_ids, mcp_output_enable, chat_model, message_list, agent_id):
229232

230233
mcp_servers_config = {}
231234

@@ -287,6 +290,27 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_ids, tool_ids,
287290
app_config = executor.get_app_mcp_config(api_key)
288291
mcp_servers_config[app.name] = app_config
289292

293+
if skill_tool_ids and len(skill_tool_ids) > 0:
294+
self.context['skill_tool_ids'] = skill_tool_ids
295+
skill_file_items = []
296+
297+
for tool_id in skill_tool_ids:
298+
tool = QuerySet(Tool).filter(id=tool_id, is_active=True).first()
299+
if tool is None or tool.is_active is False:
300+
continue
301+
if tool.init_params is not None:
302+
params = json.loads(rsa_long_decrypt(tool.init_params))
303+
tool_init_params = json.loads(rsa_long_decrypt(tool.init_params))
304+
else:
305+
params = {}
306+
307+
skill_file_items.append({
308+
'tool_id': str(tool.id),
309+
'file_id': tool.code,
310+
'params': params
311+
})
312+
mcp_servers_config['skills'] = skill_file_items
313+
290314
if len(mcp_servers_config) > 0:
291315
source_id = agent_id
292316
source_type = 'APPLICATION'
@@ -305,6 +329,7 @@ def get_stream_result(self, message_list: List[BaseMessage],
305329
mcp_source="referencing",
306330
tool_ids=None,
307331
application_ids=None,
332+
skill_tool_ids=None,
308333
mcp_output_enable=True,
309334
agent_id=None
310335
):
@@ -326,7 +351,7 @@ def get_stream_result(self, message_list: List[BaseMessage],
326351
# 处理 MCP 请求
327352
mcp_result = self._handle_mcp_request(
328353
mcp_source, mcp_servers, mcp_tool_ids, tool_ids,
329-
application_ids, mcp_output_enable, chat_model,
354+
application_ids, skill_tool_ids, mcp_output_enable, chat_model,
330355
message_list, agent_id
331356
)
332357
if mcp_result:
@@ -349,11 +374,12 @@ def execute_stream(self, message_list: List[BaseMessage],
349374
mcp_source="referencing",
350375
tool_ids=None,
351376
application_ids=None,
377+
skill_tool_ids=None,
352378
mcp_output_enable=True):
353379
chat_result, is_ai_chat = self.get_stream_result(message_list, chat_model, paragraph_list,
354380
no_references_setting, problem_text, mcp_tool_ids,
355381
mcp_servers, mcp_source, tool_ids,
356-
application_ids,
382+
application_ids, skill_tool_ids,
357383
mcp_output_enable, manage.context.get('application_id'))
358384
chat_record_id = self.context.get('step_args', {}).get('chat_record_id') if self.context.get('step_args',
359385
{}).get(
@@ -378,6 +404,7 @@ def get_block_result(self, message_list: List[BaseMessage],
378404
mcp_source="referencing",
379405
tool_ids=None,
380406
application_ids=None,
407+
skill_tool_ids=None,
381408
mcp_output_enable=True,
382409
application_id=None
383410
):
@@ -398,7 +425,7 @@ def get_block_result(self, message_list: List[BaseMessage],
398425
# 处理 MCP 请求
399426
mcp_result = self._handle_mcp_request(
400427
mcp_source, mcp_servers, mcp_tool_ids, tool_ids,
401-
application_ids, mcp_output_enable,
428+
application_ids, skill_tool_ids, mcp_output_enable,
402429
chat_model, message_list, application_id
403430
)
404431
if mcp_result:
@@ -420,6 +447,7 @@ def execute_block(self, message_list: List[BaseMessage],
420447
mcp_source="referencing",
421448
tool_ids=None,
422449
application_ids=None,
450+
skill_tool_ids=None,
423451
mcp_output_enable=True):
424452
reasoning_content_enable = model_setting.get('reasoning_content_enable', False)
425453
reasoning_content_start = model_setting.get('reasoning_content_start', '<think>')
@@ -432,7 +460,7 @@ def execute_block(self, message_list: List[BaseMessage],
432460
chat_result, is_ai_chat = self.get_block_result(message_list, chat_model, paragraph_list,
433461
no_references_setting, problem_text,
434462
mcp_tool_ids, mcp_servers, mcp_source,
435-
tool_ids, application_ids,
463+
tool_ids, application_ids, skill_tool_ids,
436464
mcp_output_enable, manage.context.get('application_id'))
437465
if is_ai_chat:
438466
request_token = chat_model.get_num_tokens_from_messages(message_list)

apps/application/flow/backend/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import getpass
2+
3+
from deepagents.backends import LocalShellBackend
4+
from deepagents.backends.protocol import ExecuteResponse
5+
6+
from maxkb.const import CONFIG
7+
8+
_enable_sandbox = bool(CONFIG.get('SANDBOX', 0))
9+
_run_user = 'sandbox' if _enable_sandbox else getpass.getuser()
10+
11+
12+
class SandboxShellBackend(LocalShellBackend):
13+
def __init__(self, root_dir: str, **kwargs):
14+
super().__init__(root_dir=root_dir, **kwargs)
15+
16+
def execute(
17+
self,
18+
command: str,
19+
*,
20+
timeout: int | None = None,
21+
) -> ExecuteResponse:
22+
if _enable_sandbox:
23+
# 用 runuser 在子进程里切换用户,父进程凭据保持不变,
24+
# 避免父进程 ruid/euid 不一致导致 execve 报 Permission denied
25+
command = f"runuser -u {_run_user} -- env -i LD_PRELOAD=/opt/maxkb-app/sandbox/lib/sandbox.so PATH=${{PATH}} {command}"
26+
# command = f"runuser -u {_run_user} -- env -i PATH=${{PATH}} {command}"
27+
28+
# print(f"Executing command in sandbox: {command}")
29+
return super().execute(command=command, timeout=timeout)

apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class ChatNodeSerializer(serializers.Serializer):
4242
label=_("Tool IDs"), )
4343
application_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
4444
label=_("App IDs"), )
45+
skill_tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
46+
label=_("Skill IDs"), )
4547
mcp_output_enable = serializers.BooleanField(required=False, default=True, label=_("Whether to enable MCP output"))
4648

4749

@@ -72,6 +74,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
7274
mcp_source=None,
7375
tool_ids=None,
7476
application_ids=None,
77+
skill_tool_ids=None,
7578
mcp_output_enable=True,
7679
**kwargs) -> NodeResult:
7780
pass

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
159159
mcp_source=None,
160160
tool_ids=None,
161161
application_ids=None,
162+
skill_tool_ids=None,
162163
mcp_output_enable=True,
163164
**kwargs) -> NodeResult:
164165
if dialogue_type is None:
@@ -187,7 +188,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
187188
# 处理 MCP 请求
188189
mcp_result = self._handle_mcp_request(
189190
mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
190-
application_ids, mcp_output_enable,
191+
application_ids, skill_tool_ids, mcp_output_enable,
191192
chat_model, message_list, history_message, question
192193
)
193194
if mcp_result:
@@ -207,7 +208,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
207208
_write_context=write_context)
208209

209210
def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
210-
application_ids,
211+
application_ids, skill_tool_ids,
211212
mcp_output_enable, chat_model, message_list, history_message, question):
212213

213214
mcp_servers_config = {}
@@ -273,6 +274,27 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids
273274
app_config = executor.get_app_mcp_config(api_key)
274275
mcp_servers_config[app.name] = app_config
275276

277+
if skill_tool_ids and len(skill_tool_ids) > 0:
278+
self.context['skill_tool_ids'] = skill_tool_ids
279+
skill_file_items = []
280+
281+
for tool_id in skill_tool_ids:
282+
tool = QuerySet(Tool).filter(id=tool_id, is_active=True).first()
283+
if tool is None or tool.is_active is False:
284+
continue
285+
if tool.init_params is not None:
286+
params = json.loads(rsa_long_decrypt(tool.init_params))
287+
tool_init_params = json.loads(rsa_long_decrypt(tool.init_params))
288+
else:
289+
params = {}
290+
291+
skill_file_items.append({
292+
'tool_id': str(tool.id),
293+
'file_id': tool.code,
294+
'params': params
295+
})
296+
mcp_servers_config['skills'] = skill_file_items
297+
276298
if len(mcp_servers_config) > 0:
277299
# 安全获取 application
278300
application_id = None

apps/application/flow/tools.py

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,32 @@
77
@desc:
88
"""
99
import asyncio
10+
import io
1011
import json
12+
import os
1113
import queue
1214
import re
15+
import shutil
16+
import tempfile
1317
import threading
18+
import zipfile
1419
from functools import reduce
1520
from typing import Iterator
1621

1722
import uuid_utils.compat as uuid
1823
from asgiref.sync import sync_to_async
24+
from deepagents import create_deep_agent
1925
from django.db.models import QuerySet
2026
from django.http import StreamingHttpResponse
2127
from langchain_core.messages import BaseMessageChunk, BaseMessage, ToolMessage, AIMessageChunk
2228
from langchain_mcp_adapters.client import MultiServerMCPClient
23-
from langgraph.prebuilt import create_react_agent
29+
from langgraph.checkpoint.memory import MemorySaver
2430

31+
from application.flow.backend.sandbox_shell import SandboxShellBackend
2532
from application.flow.i_step_node import WorkFlowPostHandler
2633
from common.result import result
2734
from common.utils.logger import maxkb_logger
35+
from knowledge.models import File
2836
from knowledge.models.knowledge_action import State
2937
from maxkb.const import CONFIG
3038
from tools.models import ToolRecord, Tool
@@ -321,15 +329,59 @@ def _extract_tool_id(raw_id):
321329
return tool_id or raw_id
322330

323331

332+
async def _initialize_skills(mcp_servers, temp_dir):
333+
skills_dir = os.path.join(temp_dir, 'skills')
334+
mcp_config = json.loads(mcp_servers)
335+
if "skills" in mcp_config:
336+
skill_file_items = mcp_config.pop('skills')
337+
for skill_file in skill_file_items:
338+
# 使用 sync_to_async 包装 ORM 查询
339+
file = await sync_to_async(lambda: QuerySet(File).filter(id=skill_file['file_id']).first())()
340+
if not file:
341+
continue
342+
# get_bytes 可能也涉及 IO,也用 sync_to_async 包装
343+
file_bytes = await sync_to_async(file.get_bytes)()
344+
with zipfile.ZipFile(io.BytesIO(file_bytes), 'r') as zip_ref:
345+
members = [
346+
m for m in zip_ref.namelist()
347+
if not m.startswith('__MACOSX/') and '__MACOSX' not in m
348+
]
349+
for member in members:
350+
if ".." in member or member.startswith("/"):
351+
raise ValueError(f"非法路径: {member}")
352+
zip_ref.extractall(skills_dir, members=members)
353+
354+
os.system("chmod -R g+rx " + temp_dir) # 确保技能目录可访问
355+
356+
client = MultiServerMCPClient(mcp_config)
357+
358+
return client, skills_dir
359+
360+
324361
async def _yield_mcp_response(chat_model, message_list, mcp_servers, mcp_output_enable=True, tool_init_params={},
325-
source_id=None, source_type=None):
362+
source_id=None, source_type=None, temp_dir=None):
326363
try:
327-
client = MultiServerMCPClient(json.loads(mcp_servers))
364+
checkpointer = MemorySaver()
365+
client, skills_dir = await _initialize_skills(mcp_servers, temp_dir)
328366
tools = await client.get_tools()
329-
agent = create_react_agent(chat_model, tools)
367+
agent = create_deep_agent(
368+
model=chat_model,
369+
backend=SandboxShellBackend(root_dir=temp_dir, virtual_mode=False),
370+
skills=[skills_dir],
371+
tools=tools,
372+
interrupt_on={
373+
"write_file": False, # Default: approve, edit, reject
374+
"read_file": False, # No interrupts needed
375+
"edit_file": False # Default: approve, edit, reject
376+
},
377+
checkpointer=checkpointer, # Required!
378+
)
330379
recursion_limit = int(CONFIG.get("LANGCHAIN_GRAPH_RECURSION_LIMIT", '25'))
331-
response = agent.astream({"messages": message_list}, config={"recursion_limit": recursion_limit},
332-
stream_mode='messages')
380+
response = agent.astream(
381+
{"messages": message_list},
382+
config={"recursion_limit": recursion_limit, "configurable": {"thread_id": str(uuid.uuid7())}},
383+
stream_mode='messages'
384+
)
333385

334386
# 用于存储工具调用信息
335387
tool_calls_info = {} # tool_id -> {'name': ..., 'input': ...}
@@ -466,11 +518,17 @@ def mcp_response_generator(chat_model, message_list, mcp_servers, mcp_output_ena
466518
"""使用全局事件循环,不创建新实例"""
467519
result_queue = queue.Queue()
468520
loop = get_global_loop() # 使用共享循环
521+
# 创建临时文件夹
522+
temp_dir = tempfile.mkdtemp(dir='/tmp')
523+
skills_dir = os.path.join(temp_dir, 'skills')
524+
os.makedirs(skills_dir, exist_ok=True)
525+
526+
print(f"Initializing skills in temporary directory: {skills_dir}")
469527

470528
async def _run():
471529
try:
472530
async_gen = _yield_mcp_response(chat_model, message_list, mcp_servers, mcp_output_enable, tool_init_params,
473-
source_id, source_type)
531+
source_id, source_type, temp_dir)
474532
async for chunk in async_gen:
475533
result_queue.put(('data', chunk))
476534
except Exception as e:
@@ -485,8 +543,12 @@ async def _run():
485543
while True:
486544
msg_type, data = result_queue.get()
487545
if msg_type == 'done':
546+
# 清理临时文件夹
547+
shutil.rmtree(temp_dir, ignore_errors=True)
488548
break
489549
if msg_type == 'error':
550+
# 清理临时文件夹
551+
shutil.rmtree(temp_dir, ignore_errors=True)
490552
raise data
491553
yield data
492554

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.11 on 2026-02-27 07:03
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('application', '0010_chatsharelink'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='application',
15+
name='skill_tool_ids',
16+
field=models.JSONField(default=list, verbose_name='技能ID列表'),
17+
),
18+
migrations.AddField(
19+
model_name='applicationversion',
20+
name='skill_tool_ids',
21+
field=models.JSONField(default=list, verbose_name='技能ID列表'),
22+
),
23+
]

0 commit comments

Comments
 (0)