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:
+7
-14
@@ -1,18 +1,11 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.idea
|
||||
# Local runtime data (MCP registry, etc.)
|
||||
data/
|
||||
# 项目运行时数据:默认全部忽略,仅显式开放需要纳入版本控制的子目录
|
||||
data/*
|
||||
!data/toolset/
|
||||
!data/plugin/
|
||||
data/plugin/skill/
|
||||
!data/toolset/
|
||||
|
||||
tmp/
|
||||
.env
|
||||
|
||||
.idea/
|
||||
.venv/
|
||||
@@ -0,0 +1,26 @@
|
||||
"""add model_settings JSONB column to provider table
|
||||
|
||||
Revision ID: 0010
|
||||
Revises: 0009
|
||||
Create Date: 2026-06-17
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
revision = "0010"
|
||||
down_revision = "0009"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"provider",
|
||||
sa.Column("model_settings", JSONB, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("provider", "model_settings")
|
||||
@@ -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}"
|
||||
@@ -37,13 +37,14 @@ export function ProvidersSettings() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
||||
const [selectedTypeId, setSelectedTypeId] = useState<string>('openai');
|
||||
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '' });
|
||||
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '', model_settings: '' });
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
|
||||
const [modelSettingsError, setModelSettingsError] = useState('');
|
||||
|
||||
const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0];
|
||||
|
||||
@@ -65,8 +66,9 @@ export function ProvidersSettings() {
|
||||
const openAddModal = () => {
|
||||
setEditingProvider(null);
|
||||
setSelectedTypeId('openai');
|
||||
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '' });
|
||||
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '', model_settings: '' });
|
||||
setError('');
|
||||
setModelSettingsError('');
|
||||
setTestResult(null);
|
||||
setShowAdvanced(false);
|
||||
setIsModalOpen(true);
|
||||
@@ -81,8 +83,12 @@ export function ProvidersSettings() {
|
||||
provider_url: provider.provider_url || '',
|
||||
provider_apikey: '',
|
||||
custom_models: '',
|
||||
model_settings: provider.model_settings && Object.keys(provider.model_settings).length > 0
|
||||
? JSON.stringify(provider.model_settings, null, 2)
|
||||
: '',
|
||||
});
|
||||
setError('');
|
||||
setModelSettingsError('');
|
||||
setTestResult(null);
|
||||
setShowAdvanced(false);
|
||||
setIsModalOpen(true);
|
||||
@@ -97,6 +103,29 @@ export function ProvidersSettings() {
|
||||
setTestResult(null);
|
||||
};
|
||||
|
||||
const parseModelSettings = (): { ok: true; value: Record<string, Record<string, unknown>> | undefined } | { ok: false } => {
|
||||
const raw = formData.model_settings.trim();
|
||||
if (!raw) return { ok: true, value: undefined };
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
setModelSettingsError(t('agent.providerModelSettingsInvalid'));
|
||||
return { ok: false };
|
||||
}
|
||||
for (const v of Object.values(parsed)) {
|
||||
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
||||
setModelSettingsError(t('agent.providerModelSettingsInvalid'));
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
setModelSettingsError('');
|
||||
return { ok: true, value: parsed as Record<string, Record<string, unknown>> };
|
||||
} catch {
|
||||
setModelSettingsError(t('agent.providerModelSettingsInvalid'));
|
||||
return { ok: false };
|
||||
}
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const customModels = formData.custom_models
|
||||
.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
@@ -107,6 +136,8 @@ export function ProvidersSettings() {
|
||||
provider_apikey: formData.provider_apikey,
|
||||
};
|
||||
if (customModels.length > 0) payload.custom_models = customModels;
|
||||
const ms = parseModelSettings();
|
||||
if (ms.ok && ms.value) payload.model_settings = ms.value;
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -134,6 +165,8 @@ export function ProvidersSettings() {
|
||||
setError(t('agent.providerFillAll'));
|
||||
return;
|
||||
}
|
||||
const ms = parseModelSettings();
|
||||
if (!ms.ok) return;
|
||||
setSubmitLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
@@ -361,6 +394,31 @@ export function ProvidersSettings() {
|
||||
/>
|
||||
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('agent.providerModelSettings')}
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.model_settings}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, model_settings: e.target.value });
|
||||
if (modelSettingsError) setModelSettingsError('');
|
||||
}}
|
||||
placeholder={t('agent.providerModelSettingsPlaceholder')}
|
||||
rows={8}
|
||||
className={`w-full bg-bg-input border text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 text-text-primary placeholder:text-text-muted/50 font-mono resize-none ${
|
||||
modelSettingsError
|
||||
? 'border-danger/50 focus:ring-danger/20 focus:border-danger'
|
||||
: 'border-border-primary focus:ring-accent/20 focus:border-accent'
|
||||
}`}
|
||||
/>
|
||||
{modelSettingsError ? (
|
||||
<p className="text-[10px] text-danger mt-1">{modelSettingsError}</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerModelSettingsHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Boxes, Loader2, Package } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
interface HeavyPlugin {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function HeavyPluginList() {
|
||||
const { t } = useTranslation();
|
||||
const [plugins, setPlugins] = useState<HeavyPlugin[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlugins = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await apiClient.get('/api/v1/plugin/list');
|
||||
setPlugins(resp.data.plugins || []);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch heavy plugins:', err);
|
||||
setError(t('plugin.heavyPluginLoadFailed'));
|
||||
setPlugins([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlugins();
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-text-primary">{t('plugin.heavyPluginManagement')}</h3>
|
||||
<p className="text-sm text-text-muted mt-0.5">{t('plugin.heavyPluginDesc')}</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
||||
<Loader2 size={24} className="animate-spin mb-3" />
|
||||
<span className="text-sm">{t('common.loading')}</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>
|
||||
) : plugins.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 bg-bg-card rounded-2xl border border-border-primary border-dashed text-text-muted">
|
||||
<Boxes size={32} className="mb-3 opacity-40" />
|
||||
<span className="text-sm">{t('plugin.heavyPluginEmpty')}</span>
|
||||
<span className="text-[11px] mt-1.5 opacity-70">{t('plugin.heavyPluginEmptyHint')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{plugins.map((p) => (
|
||||
<div key={p.name} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center text-accent shrink-0">
|
||||
<Package size={18} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-sm text-text-primary truncate">{p.display_name || p.name}</h4>
|
||||
<span className="text-[10px] text-text-muted font-mono">{p.name}</span>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${
|
||||
p.status === 'running'
|
||||
? 'bg-success-bg text-success border-success/20'
|
||||
: 'bg-bg-secondary text-text-muted border-border-primary'
|
||||
}`}>
|
||||
{p.status === 'running' && <span className="w-1 h-1 rounded-full bg-success" />}
|
||||
{p.status}
|
||||
</span>
|
||||
</div>
|
||||
{p.description && (
|
||||
<p className="text-xs text-text-secondary leading-relaxed">{p.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Sparkles, Plug } from 'lucide-react';
|
||||
import { Sparkles, Plug, Boxes } from 'lucide-react';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { MCPSettings } from './MCPSettings';
|
||||
import { HeavyPluginList } from './HeavyPluginList';
|
||||
|
||||
type PluginTab = 'skill' | 'mcp';
|
||||
type PluginTab = 'skill' | 'mcp' | 'heavy';
|
||||
|
||||
export function PluginLayout() {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,6 +14,7 @@ export function PluginLayout() {
|
||||
const tabs: { key: PluginTab; label: string; icon: typeof Sparkles }[] = [
|
||||
{ key: 'skill', label: t('plugin.skillTab'), icon: Sparkles },
|
||||
{ key: 'mcp', label: t('plugin.mcpTab'), icon: Plug },
|
||||
{ key: 'heavy', label: t('plugin.heavyTab'), icon: Boxes },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -39,6 +41,7 @@ export function PluginLayout() {
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{tab === 'skill' && <SkillSettings />}
|
||||
{tab === 'mcp' && <MCPSettings />}
|
||||
{tab === 'heavy' && <HeavyPluginList />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -251,7 +251,11 @@
|
||||
"providerAdvanced": "Parameter Settings",
|
||||
"providerCustomModels": "Custom Model List",
|
||||
"providerCustomModelsPlaceholder": "Comma separated, e.g. gpt-4o, gpt-4o-mini",
|
||||
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider."
|
||||
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider.",
|
||||
"providerModelSettings": "Model Call Parameters",
|
||||
"providerModelSettingsHint": "JSON object. Keys are model_id or __default__, values are ModelSettings (temperature/max_tokens/thinking etc.). __default__ is the fallback; specific model_id overrides it.",
|
||||
"providerModelSettingsPlaceholder": "{\n \"__default__\": {\"temperature\": 0.7, \"max_tokens\": 4096},\n \"o1\": {\"thinking\": \"high\"}\n}",
|
||||
"providerModelSettingsInvalid": "Invalid JSON. Please check the syntax."
|
||||
},
|
||||
"plugin": {
|
||||
"toolManagement": "Toolset Center",
|
||||
@@ -284,6 +288,12 @@
|
||||
"install": "Install",
|
||||
"skillTab": "Skills",
|
||||
"mcpTab": "MCP Servers",
|
||||
"heavyTab": "Heavy Plugins",
|
||||
"heavyPluginManagement": "Heavy Plugin Management",
|
||||
"heavyPluginDesc": "Heavy plugins are extension modules with UI and multi-agent collaboration. Loaded from data/plugin/ directory.",
|
||||
"heavyPluginEmpty": "No heavy plugins loaded",
|
||||
"heavyPluginEmptyHint": "Place plugin directories in data/plugin/ and they will load on startup",
|
||||
"heavyPluginLoadFailed": "Failed to load heavy plugin list",
|
||||
"mcpManagement": "MCP Server Management",
|
||||
"mcpDesc": "Manage Model Context Protocol servers to extend agent tools",
|
||||
"mcpAdd": "Add MCP Server",
|
||||
|
||||
@@ -251,7 +251,11 @@
|
||||
"providerAdvanced": "参数设置",
|
||||
"providerCustomModels": "自定义模型列表",
|
||||
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
|
||||
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单"
|
||||
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单",
|
||||
"providerModelSettings": "模型调用参数",
|
||||
"providerModelSettingsHint": "JSON 对象,键是 model_id 或 __default__,值是 ModelSettings(temperature/max_tokens/thinking 等)。__default__ 是兜底参数,具体 model_id 会覆盖之。",
|
||||
"providerModelSettingsPlaceholder": "{\n \"__default__\": {\"temperature\": 0.7, \"max_tokens\": 4096},\n \"o1\": {\"thinking\": \"high\"}\n}",
|
||||
"providerModelSettingsInvalid": "JSON 格式错误,请检查"
|
||||
},
|
||||
"plugin": {
|
||||
"toolManagement": "工具集中心",
|
||||
@@ -284,6 +288,12 @@
|
||||
"install": "安装",
|
||||
"skillTab": "技能",
|
||||
"mcpTab": "MCP 服务",
|
||||
"heavyTab": "重型插件",
|
||||
"heavyPluginManagement": "重型插件管理",
|
||||
"heavyPluginDesc": "重型插件是带 UI 与多 Agent 协作能力的扩展模块,从 data/plugin/ 目录加载",
|
||||
"heavyPluginEmpty": "暂无已加载的重型插件",
|
||||
"heavyPluginEmptyHint": "把插件目录放进 data/plugin/,启动时会自动加载",
|
||||
"heavyPluginLoadFailed": "加载重型插件列表失败",
|
||||
"mcpManagement": "MCP 服务管理",
|
||||
"mcpDesc": "管理 Model Context Protocol 服务器,扩展 agent 工具能力",
|
||||
"mcpAdd": "添加 MCP 服务",
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface Provider {
|
||||
provider_status?: string;
|
||||
status?: string;
|
||||
model?: string;
|
||||
model_settings?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface ProviderRegisterRequest {
|
||||
@@ -29,6 +30,7 @@ export interface ProviderRegisterRequest {
|
||||
provider_title: string;
|
||||
provider_url: string;
|
||||
provider_apikey: string;
|
||||
model_settings?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface ProviderListResponse {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Sequence, Any
|
||||
from typing import Sequence, Any, Dict
|
||||
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.models.openai import OpenAIChatModel
|
||||
@@ -126,3 +126,19 @@ class AgentFactory:
|
||||
toolsets=toolsets or [],
|
||||
)
|
||||
return agent
|
||||
|
||||
@staticmethod
|
||||
def resolve_model_settings(provider: Provider, model_id: str) -> Dict[str, Any]:
|
||||
"""合并 provider.model_settings 中 ``__default__`` 与具体 model_id 的参数。
|
||||
|
||||
- ``__default__`` 是全 Provider 的兜底参数
|
||||
- ``model_id`` 键覆盖 default 中相同 key
|
||||
- 都缺省时返回空 dict(``agent.run(model_settings={})`` 等效于不传)
|
||||
"""
|
||||
settings = getattr(provider, "model_settings", None) or {}
|
||||
if not isinstance(settings, dict):
|
||||
return {}
|
||||
default = settings.get("__default__", {}) or {}
|
||||
specific = settings.get(model_id, {}) or {}
|
||||
merged: Dict[str, Any] = {**default, **specific}
|
||||
return merged
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
# limitations under the License.
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Dict, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any, Dict, Literal, Optional
|
||||
from kilostar.utils.access import TokenData, Accessor, RoleChecker
|
||||
from kilostar.core.postgres_database.model import UserAuthority
|
||||
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
|
||||
@@ -30,6 +30,7 @@ class ProviderRegister(BaseModel):
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
provider_apikey: str
|
||||
model_settings: Optional[Dict[str, Dict[str, Any]]] = Field(default=None)
|
||||
|
||||
|
||||
@provider_router.post("")
|
||||
@@ -45,6 +46,7 @@ async def create_provider(
|
||||
provider_url=provider_register.provider_url,
|
||||
provider_apikey=provider_register.provider_apikey,
|
||||
provider_owner=token_data.user_id,
|
||||
model_settings=provider_register.model_settings or {},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ class GlobalStateMachine:
|
||||
provider_url,
|
||||
provider_apikey,
|
||||
provider_owner,
|
||||
model_settings=None,
|
||||
):
|
||||
"""新增一个模型 Provider:内存注册 + 数据库持久化一并完成。"""
|
||||
result = await self._global_provider_manager.add_provider(
|
||||
@@ -193,6 +194,7 @@ class GlobalStateMachine:
|
||||
provider_apikey=provider_apikey,
|
||||
provider_owner=provider_owner,
|
||||
postgres_database=self.postgres_database,
|
||||
model_settings=model_settings or {},
|
||||
)
|
||||
self._publish_snapshot()
|
||||
return result
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
# limitations under the License.
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any, Dict, List
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class Provider(BaseModel):
|
||||
provider_type: str
|
||||
provider_owner: str | None = None
|
||||
provider_status: ProviderStatus = ProviderStatus.UP
|
||||
model_settings: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ProviderArgs(BaseModel):
|
||||
@@ -44,6 +45,7 @@ class ProviderArgs(BaseModel):
|
||||
provider_url: str
|
||||
provider_apikey: str
|
||||
provider_owner: str
|
||||
model_settings: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class BaseProvider(ABC):
|
||||
|
||||
@@ -78,4 +78,5 @@ class ClaudeProvider(BaseProvider):
|
||||
provider_url=provider_args.provider_url,
|
||||
provider_models=provider_models,
|
||||
provider_type="claude",
|
||||
model_settings=provider_args.model_settings,
|
||||
)
|
||||
|
||||
@@ -81,4 +81,5 @@ class DeepseekProvider(BaseProvider):
|
||||
provider_url=provider_args.provider_url,
|
||||
provider_models=provider_models,
|
||||
provider_type="deepseek",
|
||||
model_settings=provider_args.model_settings,
|
||||
)
|
||||
|
||||
@@ -78,4 +78,5 @@ class GeminiProvider(BaseProvider):
|
||||
provider_url=provider_args.provider_url,
|
||||
provider_models=provider_models,
|
||||
provider_type="gemini",
|
||||
model_settings=provider_args.model_settings,
|
||||
)
|
||||
|
||||
@@ -81,4 +81,5 @@ class OpenAIProvider(BaseProvider):
|
||||
provider_url=provider_args.provider_url,
|
||||
provider_models=provider_models,
|
||||
provider_type="openai",
|
||||
model_settings=provider_args.model_settings,
|
||||
)
|
||||
|
||||
@@ -58,6 +58,7 @@ class ProviderManager:
|
||||
provider_apikey,
|
||||
provider_owner,
|
||||
postgres_database,
|
||||
model_settings=None,
|
||||
) -> None:
|
||||
"""新增并落库一个 Provider:
|
||||
|
||||
@@ -77,6 +78,7 @@ class ProviderManager:
|
||||
provider_url=provider_url,
|
||||
provider_apikey=provider_apikey,
|
||||
provider_owner=provider_owner,
|
||||
model_settings=model_settings or {},
|
||||
)
|
||||
try:
|
||||
import ulid
|
||||
@@ -96,6 +98,7 @@ class ProviderManager:
|
||||
provider_models=provider.provider_models,
|
||||
provider_type=provider.provider_type,
|
||||
provider_owner=provider.provider_owner,
|
||||
model_settings=provider.model_settings,
|
||||
)
|
||||
|
||||
logger.info(f"已添加适配器{provider_title}")
|
||||
|
||||
@@ -40,6 +40,7 @@ class ConsciousnessNode:
|
||||
self.logger = get_logger("consciousness_node")
|
||||
self.agent: None | Agent = None
|
||||
self.locale: str = "zh"
|
||||
self._model_settings: dict = {}
|
||||
|
||||
async def create_agent(
|
||||
self,
|
||||
@@ -73,6 +74,7 @@ class ConsciousnessNode:
|
||||
tools=tools,
|
||||
toolsets=toolsets,
|
||||
)
|
||||
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
|
||||
@@ -221,7 +223,7 @@ class ConsciousnessNode:
|
||||
)
|
||||
self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)")
|
||||
prompt = "根据original_command制定严密的可执行workflow"
|
||||
result = await self.agent.run(prompt, deps=deps)
|
||||
result = await self.agent.run(prompt, deps=deps, model_settings=self._model_settings or None)
|
||||
return result.output
|
||||
|
||||
elif isinstance(payload, ForWorkflowInput):
|
||||
@@ -236,6 +238,7 @@ class ConsciousnessNode:
|
||||
result = await self.agent.run(
|
||||
f"处理此工作流步骤信息:\n{payload.workflow_step.model_dump_json()}",
|
||||
deps=deps,
|
||||
model_settings=self._model_settings or None,
|
||||
)
|
||||
return result.output
|
||||
|
||||
@@ -251,6 +254,7 @@ class ConsciousnessNode:
|
||||
result = await self.agent.run(
|
||||
f"基于以下工作流的执行记录,生成技术报告:\n{payload.workflow.model_dump_json()}",
|
||||
deps=deps,
|
||||
model_settings=self._model_settings or None,
|
||||
)
|
||||
return result.output
|
||||
except Exception as e:
|
||||
|
||||
@@ -38,6 +38,7 @@ class ControlNode:
|
||||
|
||||
self.logger = get_logger("control_node")
|
||||
self.agent: Agent | None = None
|
||||
self._model_settings: dict = {}
|
||||
|
||||
async def create_agent(
|
||||
self,
|
||||
@@ -87,6 +88,7 @@ class ControlNode:
|
||||
tools=callables,
|
||||
toolsets=toolsets,
|
||||
)
|
||||
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]):
|
||||
@@ -121,6 +123,7 @@ class ControlNode:
|
||||
result = await self.agent.run(
|
||||
f"请根据提供的 workflow_step 上下文,执行此步骤并输出结果。\n详细指令或附加数据:{payload.workflow_step.model_dump_json()}",
|
||||
deps=deps,
|
||||
model_settings=self._model_settings or None,
|
||||
)
|
||||
return result.output
|
||||
except Exception as e:
|
||||
|
||||
@@ -40,6 +40,7 @@ class RegulatoryNode:
|
||||
|
||||
self.logger = get_logger("regulatory_node")
|
||||
self.agent: None | Agent = None
|
||||
self._model_settings: dict = {}
|
||||
|
||||
async def create_agent(
|
||||
self,
|
||||
@@ -88,6 +89,7 @@ class RegulatoryNode:
|
||||
tools=tools,
|
||||
toolsets=toolsets,
|
||||
)
|
||||
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[RegulatoryNodeDeps]):
|
||||
@@ -178,6 +180,7 @@ class RegulatoryNode:
|
||||
instructions=self._CHAT_INSTRUCTIONS,
|
||||
event_stream_handler=_stream_handler,
|
||||
message_history=message_history,
|
||||
model_settings=self._model_settings or None,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception(f"RegulatoryNode.stream_working failed: {e}")
|
||||
@@ -204,6 +207,7 @@ class RegulatoryNode:
|
||||
user_prompt=message,
|
||||
deps=deps,
|
||||
message_history=message_history,
|
||||
model_settings=self._model_settings or None,
|
||||
)
|
||||
response: MessageResponse = agent_response.output
|
||||
response.platform = platform
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
from sqlalchemy import String, Text, Boolean, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB # 针对供应商模型列表优化
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@@ -41,3 +41,8 @@ class ProviderModel(BaseDataModel):
|
||||
server_default=text("true"),
|
||||
comment="该服务商节点是否在线/启用",
|
||||
)
|
||||
model_settings: Mapped[Optional[Dict[str, Any]]] = mapped_column(
|
||||
JSONB,
|
||||
nullable=True,
|
||||
comment="模型调用参数:{model_id 或 __default__: ModelSettings dict}",
|
||||
)
|
||||
|
||||
@@ -69,6 +69,7 @@ class ProviderDatabase:
|
||||
provider_type=provider.provider_type,
|
||||
provider_owner=provider.provider_owner,
|
||||
is_active=provider.is_active,
|
||||
model_settings=provider.model_settings,
|
||||
)
|
||||
for provider in results
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
from typing import Dict, List, Any, Optional, Sequence
|
||||
|
||||
from kilostar.utils.logger import get_logger
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
logger = get_logger("mcp_helper")
|
||||
|
||||
@@ -128,6 +129,16 @@ async def get_all_tools_and_toolsets_for_scope(
|
||||
logger.error(f"Failed to load tools from GSM ({scope}): {e}")
|
||||
|
||||
toolsets = await get_mcp_toolsets_from_gsm()
|
||||
|
||||
# 合入重型插件的 dispatch tools
|
||||
try:
|
||||
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
|
||||
dispatch = await pm.get_dispatch_tools.remote()
|
||||
if dispatch:
|
||||
tools.extend(dispatch.values())
|
||||
except Exception as e:
|
||||
logger.debug(f"No dispatch tools available: {e}")
|
||||
|
||||
return tools, toolsets
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ def regulatory_instance():
|
||||
from kilostar.utils.logger import get_logger
|
||||
obj.logger = get_logger("regulatory_node")
|
||||
obj.agent = None
|
||||
obj._model_settings = {}
|
||||
return obj
|
||||
|
||||
|
||||
@@ -86,6 +87,7 @@ def control_instance():
|
||||
from kilostar.utils.logger import get_logger
|
||||
obj.logger = get_logger("control_node")
|
||||
obj.agent = None
|
||||
obj._model_settings = {}
|
||||
return obj
|
||||
|
||||
|
||||
@@ -145,6 +147,7 @@ def consciousness_instance():
|
||||
obj.logger = get_logger("consciousness_node")
|
||||
obj.agent = None
|
||||
obj.locale = "zh"
|
||||
obj._model_settings = {}
|
||||
return obj
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
_toolset_dir = Path(__file__).parent.parent.parent / "data" / "toolset"
|
||||
_base_toolset_dir = _toolset_dir / "base_toolset"
|
||||
_interactive_toolset_dir = _toolset_dir / "interactive_toolset"
|
||||
_regulatory_toolset_dir = _toolset_dir / "regulatory_toolset"
|
||||
|
||||
|
||||
def _read_manifest(toolset_dir=_base_toolset_dir):
|
||||
@@ -26,6 +27,7 @@ def _get_tool_def(manifest, name):
|
||||
def test_manifest_json_exists():
|
||||
assert (_base_toolset_dir / "manifest.json").exists()
|
||||
assert (_interactive_toolset_dir / "manifest.json").exists()
|
||||
assert (_regulatory_toolset_dir / "manifest.json").exists()
|
||||
|
||||
|
||||
def test_manifest_has_all_tools():
|
||||
@@ -33,11 +35,13 @@ def test_manifest_has_all_tools():
|
||||
interactive_names = {
|
||||
t["name"] for t in _read_manifest(_interactive_toolset_dir)["tools"]
|
||||
}
|
||||
regulatory_names = {t["name"] for t in _read_manifest(_regulatory_toolset_dir)["tools"]}
|
||||
assert base_names == {
|
||||
"shell_executor", "file_reader", "edit_file", "write_file",
|
||||
"search_file", "python_executor", "tavily_search",
|
||||
}
|
||||
assert interactive_names == {"approval", "send_file"}
|
||||
assert interactive_names == {"approval"}
|
||||
assert regulatory_names == {"query_workflow_status", "query_task_list", "send_file"}
|
||||
|
||||
|
||||
def test_approval_metadata():
|
||||
@@ -48,6 +52,15 @@ def test_approval_metadata():
|
||||
assert tool["action_scope"] == []
|
||||
|
||||
|
||||
def test_regulatory_toolset_scope():
|
||||
manifest = _read_manifest(_regulatory_toolset_dir)
|
||||
for name in ("query_workflow_status", "query_task_list", "send_file"):
|
||||
tool = _get_tool_def(manifest, name)
|
||||
assert tool is not None
|
||||
assert tool["is_system"] is True
|
||||
assert tool["action_scope"] == ["regulatory_node"]
|
||||
|
||||
|
||||
def test_tavily_search_metadata():
|
||||
manifest = _read_manifest()
|
||||
tool = _get_tool_def(manifest, "tavily_search")
|
||||
@@ -59,7 +72,7 @@ def test_tavily_search_metadata():
|
||||
|
||||
|
||||
def test_all_tool_files_exist():
|
||||
for toolset_dir in (_base_toolset_dir, _interactive_toolset_dir):
|
||||
for toolset_dir in (_base_toolset_dir, _interactive_toolset_dir, _regulatory_toolset_dir):
|
||||
manifest = _read_manifest(toolset_dir)
|
||||
for tool in manifest["tools"]:
|
||||
file_path = toolset_dir / tool["file"]
|
||||
@@ -70,10 +83,14 @@ def test_tool_manager_loads_all_tools():
|
||||
from kilostar.core.global_state_machine.tool_manager import GlobalToolManager
|
||||
|
||||
tm = GlobalToolManager()
|
||||
assert len(tm.tool_metadata) == 9
|
||||
# base_toolset(7) + interactive_toolset(1) + regulatory_toolset(3) = 11
|
||||
assert len(tm.tool_metadata) == 11
|
||||
assert "shell_executor" in tm.tool_metadata
|
||||
assert "tavily_search" in tm.tool_metadata
|
||||
assert "approval" in tm.tool_metadata
|
||||
assert "query_workflow_status" in tm.tool_metadata
|
||||
assert "query_task_list" in tm.tool_metadata
|
||||
assert "send_file" in tm.tool_metadata
|
||||
|
||||
|
||||
def test_tool_manager_system_vs_third_party():
|
||||
|
||||
Reference in New Issue
Block a user