feat: Provider model_settings 全链路 + 监管节点工具集 + 重型插件注入 + 前端打磨
- Provider model_settings (Provider+Model 级别参数配置): DB JSONB → API → GSM → AgentFactory.resolve → 三节点 agent.run 注入 - 新增 data/toolset/regulatory_toolset/: 监管节点专属工具(query_workflow_status / query_task_list / send_file) - send_file 从 interactive_toolset 迁移至 regulatory_toolset,interactive 仅保留 approval - mcp_helper 合入 GlobalPluginManager dispatch tools - 前端 Provider 弹窗参数设置区加 JSON 编辑器(model_settings) - 前端 Plugin 页面新增"重型插件"Tab(HeavyPluginList 占位) - .gitignore 精简:去除系统默认项,修复 data/ 子目录追踪 - data/toolset/ 与 data/plugin/ 首次纳入版本控制 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# 示例部门 (Example Dept)
|
||||
|
||||
演示用的重型插件骨架。包含两个平级 agent(analyst + executor),
|
||||
可作为开发新组织插件的模板。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
example_dept/
|
||||
├── manifest.json # 插件元数据
|
||||
├── agents.json # agent 定义
|
||||
├── core/ # 业务逻辑
|
||||
├── toolset/ # 本地工具
|
||||
├── skills/ # 本地技能
|
||||
└── dashboard/ # 前端面板(占位)
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"name": "analyst",
|
||||
"role": "数据分析专家",
|
||||
"system_prompt": "你是一位数据分析专家,负责理解用户需求并给出分析方案。",
|
||||
"model": {
|
||||
"provider_title": "",
|
||||
"model_id": ""
|
||||
},
|
||||
"tools": [],
|
||||
"skills": [],
|
||||
"peers": ["executor"]
|
||||
},
|
||||
{
|
||||
"name": "executor",
|
||||
"role": "执行专家",
|
||||
"system_prompt": "你是一位执行专家,负责将分析方案转化为具体操作。",
|
||||
"model": {
|
||||
"provider_title": "",
|
||||
"model_id": ""
|
||||
},
|
||||
"tools": ["shell_executor", "python_executor"],
|
||||
"skills": [],
|
||||
"peers": ["analyst"]
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"type": "react",
|
||||
"entry": "analyst"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
from kilostar.plugin_runtime.base_organization import BaseOrganization
|
||||
|
||||
|
||||
class ExampleOrganization(BaseOrganization):
|
||||
"""示例组织 — 直接使用基类的 react 逻辑。"""
|
||||
pass
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "example_dept",
|
||||
"version": "0.1.0",
|
||||
"display_name": "示例部门",
|
||||
"description": "演示用的重型插件骨架,可作为开发模板。",
|
||||
"entry": "core.organization:ExampleOrganization",
|
||||
"concurrency": "queue",
|
||||
"node_affinity": "cpu",
|
||||
"api_prefix": "/plugin/example_dept",
|
||||
"capabilities": ["text_processing"],
|
||||
"dependencies": {
|
||||
"python": [],
|
||||
"plugins": []
|
||||
},
|
||||
"ui": {
|
||||
"entry": "dashboard/index.html",
|
||||
"icon": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# base_toolset
|
||||
|
||||
KiloStar 内置基础工具集。提供文件操作、命令执行、搜索等通用能力,所有 Agent 默认可用。
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具 | 说明 |
|
||||
|------|------|
|
||||
| `shell_executor` | 执行 shell 命令,返回 stdout/stderr |
|
||||
| `file_reader` | 读取文件内容(支持按行偏移和行数限制) |
|
||||
| `edit_file` | 按 old_string → new_string 的方式精确替换文件内容 |
|
||||
| `write_file` | 整体写入或覆盖文件 |
|
||||
| `search_file` | 在目录树内按 glob/正则搜索文件名或内容 |
|
||||
| `python_executor` | 在沙箱中运行 Python 代码片段 |
|
||||
| `tavily_search` | 调用 Tavily API 进行联网搜索(需配置 `api_key`) |
|
||||
|
||||
## 配置说明
|
||||
|
||||
`tavily_search` 需要在工具配置中填入 `api_key`,可选参数:
|
||||
|
||||
- `max_results`:返回结果条数,默认 `5`
|
||||
- `search_depth`:`basic` 或 `advanced`
|
||||
- `include_answer`:是否带 LLM 摘要,默认 `true`
|
||||
|
||||
其他工具开箱即用,无需配置。
|
||||
|
||||
## 安全提示
|
||||
|
||||
- `shell_executor` / `python_executor` 会在受限沙箱内执行,但仍建议在受信环境下使用
|
||||
- `edit_file` / `write_file` 会修改本地文件系统,注意权限范围
|
||||
@@ -0,0 +1,17 @@
|
||||
from .shell_executor import shell_executor
|
||||
from .file_reader import file_reader
|
||||
from .edit_file import edit_file
|
||||
from .write_file import write_file
|
||||
from .search_file import search_file
|
||||
from .python_executor import python_executor
|
||||
from .tavily_search import tavily_search
|
||||
|
||||
__all__ = [
|
||||
"shell_executor",
|
||||
"file_reader",
|
||||
"edit_file",
|
||||
"write_file",
|
||||
"search_file",
|
||||
"python_executor",
|
||||
"tavily_search",
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
|
||||
|
||||
async def edit_file(
|
||||
file_path: str,
|
||||
old_content: str,
|
||||
new_content: str,
|
||||
) -> str:
|
||||
"""通过查找替换的方式编辑文件内容。
|
||||
|
||||
Args:
|
||||
file_path: 文件的路径
|
||||
old_content: 要被替换的原始内容片段
|
||||
new_content: 替换后的新内容
|
||||
|
||||
Returns:
|
||||
操作结果描述
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
file_path = validate_path(file_path, write=True)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
return f"[Error] 文件不存在: {file_path}"
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
if old_content not in content:
|
||||
return f"[Error] 未在文件中找到要替换的内容片段"
|
||||
|
||||
new_file_content = content.replace(old_content, new_content, 1)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_file_content)
|
||||
|
||||
return f"已成功编辑文件: {file_path}"
|
||||
except Exception as e:
|
||||
return f"[Error] 编辑文件失败: {e}"
|
||||
@@ -0,0 +1,23 @@
|
||||
async def file_reader(file_path: str) -> str:
|
||||
"""读取本地文件的内容。
|
||||
|
||||
Args:
|
||||
file_path: 文件的绝对路径或相对路径
|
||||
|
||||
Returns:
|
||||
文件内容文本,若文件不存在则返回错误信息
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
file_path = validate_path(file_path, write=False)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return f"[Error] File not found: {file_path}"
|
||||
except Exception as e:
|
||||
return f"[Error] Failed to read file: {str(e)}"
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "基础工具集",
|
||||
"version": "0.1.0",
|
||||
"description": "文件读写、命令执行、Python/搜索等通用能力",
|
||||
"tools": [
|
||||
{
|
||||
"name": "shell_executor",
|
||||
"file": "shell_executor.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "file_reader",
|
||||
"file": "file_reader.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"file": "edit_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"file": "write_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "search_file",
|
||||
"file": "search_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "python_executor",
|
||||
"file": "python_executor.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "tavily_search",
|
||||
"file": "tavily_search.py",
|
||||
"is_system": false,
|
||||
"action_scope": ["control_node", "consciousness_node", "regulatory_node"],
|
||||
"config_args": {
|
||||
"api_key": "",
|
||||
"max_results": "5",
|
||||
"search_depth": "basic",
|
||||
"include_answer": "true"
|
||||
},
|
||||
"category": "search"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
async def python_executor(code: str, timeout: int = 30) -> str:
|
||||
"""执行 Python 代码片段并返回输出。
|
||||
|
||||
Args:
|
||||
code: 要执行的 Python 代码
|
||||
timeout: 超时秒数,默认 30 秒
|
||||
|
||||
Returns:
|
||||
代码的标准输出 + 标准错误
|
||||
"""
|
||||
from kilostar.utils.sandbox import (
|
||||
validate_python_code, CodeViolation, get_python_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
code = validate_python_code(code)
|
||||
except CodeViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
timeout = get_python_timeout(timeout)
|
||||
|
||||
tmp_file = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".py", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
f.write(code)
|
||||
tmp_file = f.name
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
sys.executable, tmp_file,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
output = stdout.decode("utf-8", errors="replace")
|
||||
err_output = stderr.decode("utf-8", errors="replace")
|
||||
result = ""
|
||||
if output:
|
||||
result += output
|
||||
if err_output:
|
||||
result += f"\n[stderr]\n{err_output}"
|
||||
if proc.returncode != 0:
|
||||
result += f"\n[exit code: {proc.returncode}]"
|
||||
return result.strip() or "(no output)"
|
||||
except asyncio.TimeoutError:
|
||||
return f"[Error] Python 代码执行超时({timeout}s)"
|
||||
except Exception as e:
|
||||
return f"[Error] 执行失败: {e}"
|
||||
finally:
|
||||
if tmp_file and os.path.exists(tmp_file):
|
||||
os.unlink(tmp_file)
|
||||
@@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
async def search_file(
|
||||
keyword: str,
|
||||
directory: str = ".",
|
||||
file_pattern: str = "*",
|
||||
max_results: int = 20,
|
||||
) -> str:
|
||||
"""在指定目录下递归搜索包含关键字的文件内容。
|
||||
|
||||
Args:
|
||||
keyword: 要搜索的关键字或正则表达式
|
||||
directory: 搜索的根目录,默认当前目录
|
||||
file_pattern: 文件名匹配模式,如 "*.py"
|
||||
max_results: 最大返回结果数
|
||||
|
||||
Returns:
|
||||
匹配的文件名和行内容
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
directory = validate_path(directory, write=False)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
max_results = min(max_results, 100)
|
||||
|
||||
try:
|
||||
grep_args = [
|
||||
"grep", "-rn",
|
||||
f"--include={file_pattern}",
|
||||
"-m", str(max_results),
|
||||
"--", keyword, directory,
|
||||
]
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*grep_args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=30
|
||||
)
|
||||
output = stdout.decode("utf-8", errors="replace").strip()
|
||||
if not output:
|
||||
return f"未找到包含 '{keyword}' 的匹配项"
|
||||
lines = output.split("\n")
|
||||
if len(lines) > max_results:
|
||||
output = "\n".join(lines[:max_results])
|
||||
return output
|
||||
except asyncio.TimeoutError:
|
||||
return "[Error] 搜索超时"
|
||||
except Exception as e:
|
||||
return f"[Error] 搜索失败: {e}"
|
||||
@@ -0,0 +1,46 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
async def shell_executor(command: str, timeout: int = 30) -> str:
|
||||
"""在服务器上执行 shell 命令并返回输出。
|
||||
|
||||
Args:
|
||||
command: 要执行的 shell 命令
|
||||
timeout: 超时秒数,默认 30 秒
|
||||
|
||||
Returns:
|
||||
命令的 stdout + stderr 输出
|
||||
"""
|
||||
from kilostar.utils.sandbox import (
|
||||
validate_shell_command, CommandViolation, get_shell_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
command = validate_shell_command(command)
|
||||
except CommandViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
timeout = get_shell_timeout(timeout)
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
output = stdout.decode("utf-8", errors="replace")
|
||||
err_output = stderr.decode("utf-8", errors="replace")
|
||||
result = ""
|
||||
if output:
|
||||
result += output
|
||||
if err_output:
|
||||
result += f"\n[stderr]\n{err_output}"
|
||||
if proc.returncode != 0:
|
||||
result += f"\n[exit code: {proc.returncode}]"
|
||||
return result.strip() or "(no output)"
|
||||
except asyncio.TimeoutError:
|
||||
return f"[Error] 命令执行超时({timeout}s)"
|
||||
except Exception as e:
|
||||
return f"[Error] 执行失败: {e}"
|
||||
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from tavily import AsyncTavilyClient
|
||||
|
||||
|
||||
async def _resolve_api_key(explicit: Optional[str]) -> Optional[str]:
|
||||
"""按优先级解析 Tavily API key:显式参数 > GSM 配置 > 环境变量。"""
|
||||
if explicit:
|
||||
return explicit
|
||||
try:
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
|
||||
|
||||
snapshot = await fetch_snapshot()
|
||||
cfg = snapshot.tool_configs.get("tavily_search") or {}
|
||||
if isinstance(cfg, dict) and cfg.get("api_key"):
|
||||
return cfg["api_key"]
|
||||
except Exception:
|
||||
pass
|
||||
return os.environ.get("TAVILY_API_KEY")
|
||||
|
||||
|
||||
async def tavily_search(
|
||||
query: str,
|
||||
max_results: int = 5,
|
||||
search_depth: str = "basic",
|
||||
include_answer: bool = True,
|
||||
api_key: Optional[str] = None,
|
||||
) -> str:
|
||||
"""使用 Tavily 进行网络搜索,获取高质量的网络搜索结果。
|
||||
|
||||
Args:
|
||||
query: 搜索查询内容
|
||||
max_results: 返回的最大结果数量(1-10)
|
||||
search_depth: 搜索深度,"basic" 或 "advanced"
|
||||
include_answer: 是否包含 AI 生成的答案摘要
|
||||
api_key: 可选;不传则按 GSM 配置 → 环境变量顺序解析
|
||||
|
||||
Returns:
|
||||
格式化的搜索结果文本,包含标题、URL、摘要和可选的 AI 答案
|
||||
"""
|
||||
resolved_key = await _resolve_api_key(api_key)
|
||||
if not resolved_key:
|
||||
return (
|
||||
"[Error] Tavily API key 未配置。"
|
||||
"请在 ``/api/v1/resource/tool/config`` 写入或设置环境变量 ``TAVILY_API_KEY``。"
|
||||
)
|
||||
|
||||
try:
|
||||
client = AsyncTavilyClient(api_key=resolved_key)
|
||||
result = await client.search(
|
||||
query=query,
|
||||
max_results=min(max_results, 10),
|
||||
search_depth=search_depth,
|
||||
include_answer=include_answer,
|
||||
)
|
||||
|
||||
lines = []
|
||||
if include_answer and result.get("answer"):
|
||||
lines.append(f"【AI 摘要】{result['answer']}\n")
|
||||
|
||||
results = result.get("results", [])
|
||||
if not results:
|
||||
return "No results found for the query."
|
||||
|
||||
lines.append("【搜索结果】")
|
||||
for i, item in enumerate(results, 1):
|
||||
title = item.get("title", "Untitled")
|
||||
url = item.get("url", "")
|
||||
content = item.get("content", "").strip()
|
||||
lines.append(f"\n{i}. {title}")
|
||||
lines.append(f" URL: {url}")
|
||||
if content:
|
||||
lines.append(f" {content[:300]}{'...' if len(content) > 300 else ''}")
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"[Error] Tavily search failed: {str(e)}"
|
||||
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
|
||||
async def write_file(file_path: str, content: str) -> str:
|
||||
"""将内容写入指定文件(会覆盖已有内容,自动创建目录)。
|
||||
|
||||
Args:
|
||||
file_path: 文件的路径
|
||||
content: 要写入的内容
|
||||
|
||||
Returns:
|
||||
操作结果描述
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
file_path = validate_path(file_path, write=True)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
try:
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if dir_path:
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return f"已成功写入文件: {file_path}({len(content)} 字符)"
|
||||
except Exception as e:
|
||||
return f"[Error] 写入文件失败: {e}"
|
||||
@@ -0,0 +1,26 @@
|
||||
# interactive__toolset
|
||||
|
||||
KiloStar 工作流交互工具集。这些工具用于 Agent 与用户/前端之间的实时交互,依赖 `global_workflow_manager` 的消息通道。
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具 | 说明 |
|
||||
|------|------|
|
||||
| `approval` | 在执行高风险操作前向用户发送审批请求,阻塞等待用户回复 |
|
||||
| `send_file` | 把 Agent 生成的文件作为附件推送到当前对话窗口,前端渲染为可下载卡片 |
|
||||
|
||||
## 使用前提
|
||||
|
||||
这两个工具需要工作流上下文:调用方必须在 deps 中传入 `trace_id`,工具会通过 `global_workflow_manager` 的 pending 队列与前端通信。
|
||||
|
||||
- 在普通聊天场景下,`send_file` 在 `trace_id` 为空时会退化为直接返回文件内容字符串
|
||||
- `approval` 在没有合法 `trace_id` 时会一直阻塞,建议仅在工作流节点中绑定
|
||||
|
||||
## 配置说明
|
||||
|
||||
无需任何配置,开箱即用。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- Agent 计划执行删除、转账等高风险操作前的人工确认
|
||||
- 让 Agent 把生成的报告、代码片段、图表数据以文件形式投递给用户
|
||||
@@ -0,0 +1,5 @@
|
||||
from .approval import approval
|
||||
|
||||
__all__ = [
|
||||
"approval",
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
|
||||
async def approval(message: str, trace_id: str) -> str:
|
||||
"""当任务存在某些高风险操作或者计划需要让用户审批,发送请求给用户等待用户审批。
|
||||
|
||||
Args:
|
||||
message: 发送给用户的请求
|
||||
trace_id: 当前工作流的 trace_id
|
||||
|
||||
Returns:
|
||||
用户的审批结果
|
||||
"""
|
||||
actor_list = ray_actor_hook("global_workflow_manager")
|
||||
await actor_list.global_workflow_manager.put_pending.remote(trace_id, message)
|
||||
reply = await actor_list.global_workflow_manager.get_received.remote(trace_id)
|
||||
return reply
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "交互工具集",
|
||||
"version": "0.1.0",
|
||||
"description": "工作流场景下与用户/前端交互的工具(HITL 审批)",
|
||||
"tools": [
|
||||
{
|
||||
"name": "approval",
|
||||
"file": "approval.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
from .query_workflow_status import query_workflow_status
|
||||
from .query_task_list import query_task_list
|
||||
from .send_file import send_file
|
||||
|
||||
__all__ = [
|
||||
"query_workflow_status",
|
||||
"query_task_list",
|
||||
"send_file",
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "监管节点工具集",
|
||||
"version": "0.1.0",
|
||||
"description": "监管节点(regulatory_node)专属能力:查询工作流、查询任务列表、发送文件",
|
||||
"tools": [
|
||||
{
|
||||
"name": "query_workflow_status",
|
||||
"file": "query_workflow_status.py",
|
||||
"is_system": true,
|
||||
"action_scope": ["regulatory_node"],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "query_task_list",
|
||||
"file": "query_task_list.py",
|
||||
"is_system": true,
|
||||
"action_scope": ["regulatory_node"],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "send_file",
|
||||
"file": "send_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": ["regulatory_node"],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"""query_task_list:列出当前用户的所有工作流任务。
|
||||
|
||||
regulatory_node 用以回答"我有哪些任务/正在跑什么"。返回精简后的任务列表,
|
||||
不包含 graph state、context 等大字段。
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
|
||||
async def query_task_list(
|
||||
user_id: str,
|
||||
status_filter: Optional[str] = None,
|
||||
limit: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
"""列出当前用户的工作流任务。
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID(通常由调用方从对话上下文中带入)
|
||||
status_filter: 可选,按状态过滤(pending/running/completed/failed)
|
||||
limit: 最多返回条数,默认 20
|
||||
|
||||
Returns:
|
||||
{
|
||||
"user_id": str,
|
||||
"tasks": [
|
||||
{"trace_id": ..., "title": ..., "status": ..., "command": ..., "created_at": ...}
|
||||
],
|
||||
"total": int
|
||||
}
|
||||
"""
|
||||
pg = ray_actor_hook("postgres_database").postgres_database
|
||||
workflows = await pg.list_workflows.remote(user_id) or []
|
||||
|
||||
tasks: List[Dict[str, Any]] = []
|
||||
for wf in workflows:
|
||||
status = getattr(wf, "status", None)
|
||||
if status_filter and status != status_filter:
|
||||
continue
|
||||
tasks.append(
|
||||
{
|
||||
"trace_id": getattr(wf, "trace_id", None),
|
||||
"title": getattr(wf, "title", None),
|
||||
"status": status,
|
||||
"command": getattr(wf, "command", None),
|
||||
"created_at": str(getattr(wf, "created_at", "")),
|
||||
}
|
||||
)
|
||||
if len(tasks) >= limit:
|
||||
break
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"tasks": tasks,
|
||||
"total": len(tasks),
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"""query_workflow_status:查询某个 trace_id 对应工作流的最近事件。
|
||||
|
||||
regulatory_node 在与用户对话时,可以借此工具回答"我那个任务跑到哪一步了"
|
||||
之类的问题。返回最近 N 条事件 + 当前工作流 status。
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
|
||||
async def query_workflow_status(trace_id: str, limit: int = 10) -> Dict[str, Any]:
|
||||
"""查询指定工作流 trace_id 的状态与最近事件。
|
||||
|
||||
Args:
|
||||
trace_id: 工作流追踪 ID
|
||||
limit: 返回最近多少条事件,默认 10
|
||||
|
||||
Returns:
|
||||
{
|
||||
"trace_id": str,
|
||||
"status": str | None, # 工作流当前状态(pending/running/completed/failed)
|
||||
"title": str | None,
|
||||
"recent_events": [ # 最近事件,按时间倒序
|
||||
{"event_type": ..., "level": ..., "message": ..., "node_name": ..., "created_at": ...}
|
||||
]
|
||||
}
|
||||
"""
|
||||
pg = ray_actor_hook("postgres_database").postgres_database
|
||||
|
||||
workflow = await pg.get_workflow.remote(trace_id)
|
||||
events = await pg.query_event_logs.remote(trace_id=trace_id, limit=limit)
|
||||
|
||||
recent: List[Dict[str, Any]] = []
|
||||
for e in events or []:
|
||||
recent.append(
|
||||
{
|
||||
"event_type": getattr(e, "event_type", None),
|
||||
"level": getattr(e, "level", None),
|
||||
"message": getattr(e, "message", None),
|
||||
"node_name": getattr(e, "node_name", None),
|
||||
"created_at": str(getattr(e, "created_at", "")),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"trace_id": trace_id,
|
||||
"status": getattr(workflow, "status", None) if workflow else None,
|
||||
"title": getattr(workflow, "title", None) if workflow else None,
|
||||
"recent_events": recent,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"""send_file:在对话/工作流场景下投递一份文件给用户。
|
||||
|
||||
regulatory_node 直接对话场景下用此工具把生成的文件发给用户:
|
||||
- 工作流场景(带 trace_id):写入 data/artifact/<trace_id>/,前端通过 SSE
|
||||
收到带下载链接的卡片。
|
||||
- 直接对话场景(无 trace_id):退化为把文件内容拼回字符串返回给 agent,
|
||||
让 agent 再以代码块形式吐给用户。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
from kilostar.utils.settings import get_artifact_dir
|
||||
|
||||
|
||||
_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._-]+")
|
||||
|
||||
|
||||
def _sanitize_filename(name: str) -> str:
|
||||
name = name.strip().replace("\\", "/").split("/")[-1]
|
||||
name = _SAFE_NAME_RE.sub("_", name)
|
||||
return name or "file"
|
||||
|
||||
|
||||
async def send_file(filename: str, content: str, trace_id: str = "") -> str:
|
||||
"""把 agent 生成的文件作为附件投递给用户。
|
||||
|
||||
Args:
|
||||
filename: 文件名(含扩展名),如 "report.md" / "main.py"
|
||||
content: 文件内容(UTF-8 文本)
|
||||
trace_id: 当前会话/工作流的 trace_id;为空时退化为直接返回内容
|
||||
|
||||
Returns:
|
||||
发送结果说明或文件内容
|
||||
"""
|
||||
if not trace_id:
|
||||
return f"文件 {filename} 内容如下:\n\n```\n{content}\n```"
|
||||
|
||||
safe_name = _sanitize_filename(filename)
|
||||
artifact_id = uuid.uuid4().hex[:12]
|
||||
trace_dir: Path = get_artifact_dir() / trace_id
|
||||
trace_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = trace_dir / f"{artifact_id}_{safe_name}"
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
|
||||
payload = json.dumps(
|
||||
{
|
||||
"type": "file",
|
||||
"filename": safe_name,
|
||||
"artifact_id": artifact_id,
|
||||
"url": f"/api/v1/resource/artifact/{trace_id}/{artifact_id}",
|
||||
"size": len(content.encode("utf-8")),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
actor_list = ray_actor_hook("global_workflow_manager")
|
||||
await actor_list.global_workflow_manager.put_pending.remote(
|
||||
trace_id, f"__FILE__{payload}"
|
||||
)
|
||||
return f"已发送文件: {safe_name}"
|
||||
Reference in New Issue
Block a user