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:
2026-06-17 13:10:31 +00:00
parent 6d658b4f4d
commit 005ce566a8
49 changed files with 1093 additions and 30 deletions
+7 -14
View File
@@ -1,18 +1,11 @@
# Python-generated files # 项目运行时数据:默认全部忽略,仅显式开放需要纳入版本控制的子目录
__pycache__/ data/*
*.py[oc] !data/toolset/
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
.idea
# Local runtime data (MCP registry, etc.)
data/
!data/plugin/ !data/plugin/
data/plugin/skill/ data/plugin/skill/
!data/toolset/
tmp/ tmp/
.env .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")
+16
View File
@@ -0,0 +1,16 @@
# 示例部门 (Example Dept)
演示用的重型插件骨架。包含两个平级 agentanalyst + executor),
可作为开发新组织插件的模板。
## 目录结构
```
example_dept/
├── manifest.json # 插件元数据
├── agents.json # agent 定义
├── core/ # 业务逻辑
├── toolset/ # 本地工具
├── skills/ # 本地技能
└── dashboard/ # 前端面板(占位)
```
+32
View File
@@ -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
+19
View File
@@ -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
}
}
+30
View File
@@ -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` 会修改本地文件系统,注意权限范围
+17
View 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",
]
+43
View File
@@ -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}"
+23
View File
@@ -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)}"
+68
View File
@@ -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)
+55
View 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)}"
+31
View File
@@ -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 [isModalOpen, setIsModalOpen] = useState(false);
const [editingProvider, setEditingProvider] = useState<string | null>(null); const [editingProvider, setEditingProvider] = useState<string | null>(null);
const [selectedTypeId, setSelectedTypeId] = useState<string>('openai'); 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 [showAdvanced, setShowAdvanced] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false);
const [testLoading, setTestLoading] = useState(false); const [testLoading, setTestLoading] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null); const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [expandedProvider, setExpandedProvider] = useState<string | null>(null); const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
const [modelSettingsError, setModelSettingsError] = useState('');
const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0]; const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0];
@@ -65,8 +66,9 @@ export function ProvidersSettings() {
const openAddModal = () => { const openAddModal = () => {
setEditingProvider(null); setEditingProvider(null);
setSelectedTypeId('openai'); 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(''); setError('');
setModelSettingsError('');
setTestResult(null); setTestResult(null);
setShowAdvanced(false); setShowAdvanced(false);
setIsModalOpen(true); setIsModalOpen(true);
@@ -81,8 +83,12 @@ export function ProvidersSettings() {
provider_url: provider.provider_url || '', provider_url: provider.provider_url || '',
provider_apikey: '', provider_apikey: '',
custom_models: '', custom_models: '',
model_settings: provider.model_settings && Object.keys(provider.model_settings).length > 0
? JSON.stringify(provider.model_settings, null, 2)
: '',
}); });
setError(''); setError('');
setModelSettingsError('');
setTestResult(null); setTestResult(null);
setShowAdvanced(false); setShowAdvanced(false);
setIsModalOpen(true); setIsModalOpen(true);
@@ -97,6 +103,29 @@ export function ProvidersSettings() {
setTestResult(null); 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 buildPayload = () => {
const customModels = formData.custom_models const customModels = formData.custom_models
.split(',').map((s) => s.trim()).filter(Boolean); .split(',').map((s) => s.trim()).filter(Boolean);
@@ -107,6 +136,8 @@ export function ProvidersSettings() {
provider_apikey: formData.provider_apikey, provider_apikey: formData.provider_apikey,
}; };
if (customModels.length > 0) payload.custom_models = customModels; if (customModels.length > 0) payload.custom_models = customModels;
const ms = parseModelSettings();
if (ms.ok && ms.value) payload.model_settings = ms.value;
return payload; return payload;
}; };
@@ -134,6 +165,8 @@ export function ProvidersSettings() {
setError(t('agent.providerFillAll')); setError(t('agent.providerFillAll'));
return; return;
} }
const ms = parseModelSettings();
if (!ms.ok) return;
setSubmitLoading(true); setSubmitLoading(true);
setError(''); setError('');
try { try {
@@ -361,6 +394,31 @@ export function ProvidersSettings() {
/> />
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p> <p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p>
</div> </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>
)} )}
</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 { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Sparkles, Plug } from 'lucide-react'; import { Sparkles, Plug, Boxes } from 'lucide-react';
import { SkillSettings } from './SkillSettings'; import { SkillSettings } from './SkillSettings';
import { MCPSettings } from './MCPSettings'; import { MCPSettings } from './MCPSettings';
import { HeavyPluginList } from './HeavyPluginList';
type PluginTab = 'skill' | 'mcp'; type PluginTab = 'skill' | 'mcp' | 'heavy';
export function PluginLayout() { export function PluginLayout() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -13,6 +14,7 @@ export function PluginLayout() {
const tabs: { key: PluginTab; label: string; icon: typeof Sparkles }[] = [ const tabs: { key: PluginTab; label: string; icon: typeof Sparkles }[] = [
{ key: 'skill', label: t('plugin.skillTab'), icon: Sparkles }, { key: 'skill', label: t('plugin.skillTab'), icon: Sparkles },
{ key: 'mcp', label: t('plugin.mcpTab'), icon: Plug }, { key: 'mcp', label: t('plugin.mcpTab'), icon: Plug },
{ key: 'heavy', label: t('plugin.heavyTab'), icon: Boxes },
]; ];
return ( return (
@@ -39,6 +41,7 @@ export function PluginLayout() {
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-8">
{tab === 'skill' && <SkillSettings />} {tab === 'skill' && <SkillSettings />}
{tab === 'mcp' && <MCPSettings />} {tab === 'mcp' && <MCPSettings />}
{tab === 'heavy' && <HeavyPluginList />}
</div> </div>
</div> </div>
); );
+11 -1
View File
@@ -251,7 +251,11 @@
"providerAdvanced": "Parameter Settings", "providerAdvanced": "Parameter Settings",
"providerCustomModels": "Custom Model List", "providerCustomModels": "Custom Model List",
"providerCustomModelsPlaceholder": "Comma separated, e.g. gpt-4o, gpt-4o-mini", "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": { "plugin": {
"toolManagement": "Toolset Center", "toolManagement": "Toolset Center",
@@ -284,6 +288,12 @@
"install": "Install", "install": "Install",
"skillTab": "Skills", "skillTab": "Skills",
"mcpTab": "MCP Servers", "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", "mcpManagement": "MCP Server Management",
"mcpDesc": "Manage Model Context Protocol servers to extend agent tools", "mcpDesc": "Manage Model Context Protocol servers to extend agent tools",
"mcpAdd": "Add MCP Server", "mcpAdd": "Add MCP Server",
+11 -1
View File
@@ -251,7 +251,11 @@
"providerAdvanced": "参数设置", "providerAdvanced": "参数设置",
"providerCustomModels": "自定义模型列表", "providerCustomModels": "自定义模型列表",
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini", "providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单" "providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单",
"providerModelSettings": "模型调用参数",
"providerModelSettingsHint": "JSON 对象,键是 model_id 或 __default__,值是 ModelSettingstemperature/max_tokens/thinking 等)。__default__ 是兜底参数,具体 model_id 会覆盖之。",
"providerModelSettingsPlaceholder": "{\n \"__default__\": {\"temperature\": 0.7, \"max_tokens\": 4096},\n \"o1\": {\"thinking\": \"high\"}\n}",
"providerModelSettingsInvalid": "JSON 格式错误,请检查"
}, },
"plugin": { "plugin": {
"toolManagement": "工具集中心", "toolManagement": "工具集中心",
@@ -284,6 +288,12 @@
"install": "安装", "install": "安装",
"skillTab": "技能", "skillTab": "技能",
"mcpTab": "MCP 服务", "mcpTab": "MCP 服务",
"heavyTab": "重型插件",
"heavyPluginManagement": "重型插件管理",
"heavyPluginDesc": "重型插件是带 UI 与多 Agent 协作能力的扩展模块,从 data/plugin/ 目录加载",
"heavyPluginEmpty": "暂无已加载的重型插件",
"heavyPluginEmptyHint": "把插件目录放进 data/plugin/,启动时会自动加载",
"heavyPluginLoadFailed": "加载重型插件列表失败",
"mcpManagement": "MCP 服务管理", "mcpManagement": "MCP 服务管理",
"mcpDesc": "管理 Model Context Protocol 服务器,扩展 agent 工具能力", "mcpDesc": "管理 Model Context Protocol 服务器,扩展 agent 工具能力",
"mcpAdd": "添加 MCP 服务", "mcpAdd": "添加 MCP 服务",
+2
View File
@@ -22,6 +22,7 @@ export interface Provider {
provider_status?: string; provider_status?: string;
status?: string; status?: string;
model?: string; model?: string;
model_settings?: Record<string, Record<string, unknown>>;
} }
export interface ProviderRegisterRequest { export interface ProviderRegisterRequest {
@@ -29,6 +30,7 @@ export interface ProviderRegisterRequest {
provider_title: string; provider_title: string;
provider_url: string; provider_url: string;
provider_apikey: string; provider_apikey: string;
model_settings?: Record<string, Record<string, unknown>>;
} }
export interface ProviderListResponse { export interface ProviderListResponse {
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Sequence, Any from typing import Sequence, Any, Dict
from pydantic_ai import Agent from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel
@@ -126,3 +126,19 @@ class AgentFactory:
toolsets=toolsets or [], toolsets=toolsets or [],
) )
return agent 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
+4 -2
View File
@@ -13,8 +13,8 @@
# limitations under the License. # limitations under the License.
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel from pydantic import BaseModel, Field
from typing import Any, Dict, Literal from typing import Any, Dict, Literal, Optional
from kilostar.utils.access import TokenData, Accessor, RoleChecker from kilostar.utils.access import TokenData, Accessor, RoleChecker
from kilostar.core.postgres_database.model import UserAuthority from kilostar.core.postgres_database.model import UserAuthority
from kilostar.core.global_state_machine.model_provider.base_provider import Provider from kilostar.core.global_state_machine.model_provider.base_provider import Provider
@@ -30,6 +30,7 @@ class ProviderRegister(BaseModel):
provider_title: str provider_title: str
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str
model_settings: Optional[Dict[str, Dict[str, Any]]] = Field(default=None)
@provider_router.post("") @provider_router.post("")
@@ -45,6 +46,7 @@ async def create_provider(
provider_url=provider_register.provider_url, provider_url=provider_register.provider_url,
provider_apikey=provider_register.provider_apikey, provider_apikey=provider_register.provider_apikey,
provider_owner=token_data.user_id, provider_owner=token_data.user_id,
model_settings=provider_register.model_settings or {},
) )
@@ -184,6 +184,7 @@ class GlobalStateMachine:
provider_url, provider_url,
provider_apikey, provider_apikey,
provider_owner, provider_owner,
model_settings=None,
): ):
"""新增一个模型 Provider:内存注册 + 数据库持久化一并完成。""" """新增一个模型 Provider:内存注册 + 数据库持久化一并完成。"""
result = await self._global_provider_manager.add_provider( result = await self._global_provider_manager.add_provider(
@@ -193,6 +194,7 @@ class GlobalStateMachine:
provider_apikey=provider_apikey, provider_apikey=provider_apikey,
provider_owner=provider_owner, provider_owner=provider_owner,
postgres_database=self.postgres_database, postgres_database=self.postgres_database,
model_settings=model_settings or {},
) )
self._publish_snapshot() self._publish_snapshot()
return result return result
@@ -13,8 +13,8 @@
# limitations under the License. # limitations under the License.
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pydantic import BaseModel from pydantic import BaseModel, Field
from typing import List from typing import Any, Dict, List
from enum import Enum from enum import Enum
@@ -35,6 +35,7 @@ class Provider(BaseModel):
provider_type: str provider_type: str
provider_owner: str | None = None provider_owner: str | None = None
provider_status: ProviderStatus = ProviderStatus.UP provider_status: ProviderStatus = ProviderStatus.UP
model_settings: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
class ProviderArgs(BaseModel): class ProviderArgs(BaseModel):
@@ -44,6 +45,7 @@ class ProviderArgs(BaseModel):
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str
provider_owner: str provider_owner: str
model_settings: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
class BaseProvider(ABC): class BaseProvider(ABC):
@@ -78,4 +78,5 @@ class ClaudeProvider(BaseProvider):
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
provider_models=provider_models, provider_models=provider_models,
provider_type="claude", provider_type="claude",
model_settings=provider_args.model_settings,
) )
@@ -81,4 +81,5 @@ class DeepseekProvider(BaseProvider):
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
provider_models=provider_models, provider_models=provider_models,
provider_type="deepseek", provider_type="deepseek",
model_settings=provider_args.model_settings,
) )
@@ -78,4 +78,5 @@ class GeminiProvider(BaseProvider):
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
provider_models=provider_models, provider_models=provider_models,
provider_type="gemini", provider_type="gemini",
model_settings=provider_args.model_settings,
) )
@@ -81,4 +81,5 @@ class OpenAIProvider(BaseProvider):
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
provider_models=provider_models, provider_models=provider_models,
provider_type="openai", provider_type="openai",
model_settings=provider_args.model_settings,
) )
@@ -58,6 +58,7 @@ class ProviderManager:
provider_apikey, provider_apikey,
provider_owner, provider_owner,
postgres_database, postgres_database,
model_settings=None,
) -> None: ) -> None:
"""新增并落库一个 Provider """新增并落库一个 Provider
@@ -77,6 +78,7 @@ class ProviderManager:
provider_url=provider_url, provider_url=provider_url,
provider_apikey=provider_apikey, provider_apikey=provider_apikey,
provider_owner=provider_owner, provider_owner=provider_owner,
model_settings=model_settings or {},
) )
try: try:
import ulid import ulid
@@ -96,6 +98,7 @@ class ProviderManager:
provider_models=provider.provider_models, provider_models=provider.provider_models,
provider_type=provider.provider_type, provider_type=provider.provider_type,
provider_owner=provider.provider_owner, provider_owner=provider.provider_owner,
model_settings=provider.model_settings,
) )
logger.info(f"已添加适配器{provider_title}") logger.info(f"已添加适配器{provider_title}")
@@ -40,6 +40,7 @@ class ConsciousnessNode:
self.logger = get_logger("consciousness_node") self.logger = get_logger("consciousness_node")
self.agent: None | Agent = None self.agent: None | Agent = None
self.locale: str = "zh" self.locale: str = "zh"
self._model_settings: dict = {}
async def create_agent( async def create_agent(
self, self,
@@ -73,6 +74,7 @@ class ConsciousnessNode:
tools=tools, tools=tools,
toolsets=toolsets, toolsets=toolsets,
) )
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]): async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
@@ -221,7 +223,7 @@ class ConsciousnessNode:
) )
self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)") self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)")
prompt = "根据original_command制定严密的可执行workflow" 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 return result.output
elif isinstance(payload, ForWorkflowInput): elif isinstance(payload, ForWorkflowInput):
@@ -236,6 +238,7 @@ class ConsciousnessNode:
result = await self.agent.run( result = await self.agent.run(
f"处理此工作流步骤信息:\n{payload.workflow_step.model_dump_json()}", f"处理此工作流步骤信息:\n{payload.workflow_step.model_dump_json()}",
deps=deps, deps=deps,
model_settings=self._model_settings or None,
) )
return result.output return result.output
@@ -251,6 +254,7 @@ class ConsciousnessNode:
result = await self.agent.run( result = await self.agent.run(
f"基于以下工作流的执行记录,生成技术报告:\n{payload.workflow.model_dump_json()}", f"基于以下工作流的执行记录,生成技术报告:\n{payload.workflow.model_dump_json()}",
deps=deps, deps=deps,
model_settings=self._model_settings or None,
) )
return result.output return result.output
except Exception as e: except Exception as e:
@@ -38,6 +38,7 @@ class ControlNode:
self.logger = get_logger("control_node") self.logger = get_logger("control_node")
self.agent: Agent | None = None self.agent: Agent | None = None
self._model_settings: dict = {}
async def create_agent( async def create_agent(
self, self,
@@ -87,6 +88,7 @@ class ControlNode:
tools=callables, tools=callables,
toolsets=toolsets, toolsets=toolsets,
) )
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]): async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]):
@@ -121,6 +123,7 @@ class ControlNode:
result = await self.agent.run( result = await self.agent.run(
f"请根据提供的 workflow_step 上下文,执行此步骤并输出结果。\n详细指令或附加数据:{payload.workflow_step.model_dump_json()}", f"请根据提供的 workflow_step 上下文,执行此步骤并输出结果。\n详细指令或附加数据:{payload.workflow_step.model_dump_json()}",
deps=deps, deps=deps,
model_settings=self._model_settings or None,
) )
return result.output return result.output
except Exception as e: except Exception as e:
@@ -40,6 +40,7 @@ class RegulatoryNode:
self.logger = get_logger("regulatory_node") self.logger = get_logger("regulatory_node")
self.agent: None | Agent = None self.agent: None | Agent = None
self._model_settings: dict = {}
async def create_agent( async def create_agent(
self, self,
@@ -88,6 +89,7 @@ class RegulatoryNode:
tools=tools, tools=tools,
toolsets=toolsets, toolsets=toolsets,
) )
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[RegulatoryNodeDeps]): async def dynamic_prompt(ctx: RunContext[RegulatoryNodeDeps]):
@@ -178,6 +180,7 @@ class RegulatoryNode:
instructions=self._CHAT_INSTRUCTIONS, instructions=self._CHAT_INSTRUCTIONS,
event_stream_handler=_stream_handler, event_stream_handler=_stream_handler,
message_history=message_history, message_history=message_history,
model_settings=self._model_settings or None,
) )
except Exception as e: except Exception as e:
self.logger.exception(f"RegulatoryNode.stream_working failed: {e}") self.logger.exception(f"RegulatoryNode.stream_working failed: {e}")
@@ -204,6 +207,7 @@ class RegulatoryNode:
user_prompt=message, user_prompt=message,
deps=deps, deps=deps,
message_history=message_history, message_history=message_history,
model_settings=self._model_settings or None,
) )
response: MessageResponse = agent_response.output response: MessageResponse = agent_response.output
response.platform = platform response.platform = platform
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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 import String, Text, Boolean, text
from sqlalchemy.dialects.postgresql import JSONB # 针对供应商模型列表优化 from sqlalchemy.dialects.postgresql import JSONB # 针对供应商模型列表优化
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@@ -41,3 +41,8 @@ class ProviderModel(BaseDataModel):
server_default=text("true"), server_default=text("true"),
comment="该服务商节点是否在线/启用", 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_type=provider.provider_type,
provider_owner=provider.provider_owner, provider_owner=provider.provider_owner,
is_active=provider.is_active, is_active=provider.is_active,
model_settings=provider.model_settings,
) )
for provider in results for provider in results
] ]
+11
View File
@@ -17,6 +17,7 @@
from typing import Dict, List, Any, Optional, Sequence from typing import Dict, List, Any, Optional, Sequence
from kilostar.utils.logger import get_logger from kilostar.utils.logger import get_logger
from kilostar.utils.ray_hook import ray_actor_hook
logger = get_logger("mcp_helper") 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}") logger.error(f"Failed to load tools from GSM ({scope}): {e}")
toolsets = await get_mcp_toolsets_from_gsm() 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 return tools, toolsets
+3
View File
@@ -25,6 +25,7 @@ def regulatory_instance():
from kilostar.utils.logger import get_logger from kilostar.utils.logger import get_logger
obj.logger = get_logger("regulatory_node") obj.logger = get_logger("regulatory_node")
obj.agent = None obj.agent = None
obj._model_settings = {}
return obj return obj
@@ -86,6 +87,7 @@ def control_instance():
from kilostar.utils.logger import get_logger from kilostar.utils.logger import get_logger
obj.logger = get_logger("control_node") obj.logger = get_logger("control_node")
obj.agent = None obj.agent = None
obj._model_settings = {}
return obj return obj
@@ -145,6 +147,7 @@ def consciousness_instance():
obj.logger = get_logger("consciousness_node") obj.logger = get_logger("consciousness_node")
obj.agent = None obj.agent = None
obj.locale = "zh" obj.locale = "zh"
obj._model_settings = {}
return obj return obj
+20 -3
View File
@@ -9,6 +9,7 @@ from pathlib import Path
_toolset_dir = Path(__file__).parent.parent.parent / "data" / "toolset" _toolset_dir = Path(__file__).parent.parent.parent / "data" / "toolset"
_base_toolset_dir = _toolset_dir / "base_toolset" _base_toolset_dir = _toolset_dir / "base_toolset"
_interactive_toolset_dir = _toolset_dir / "interactive_toolset" _interactive_toolset_dir = _toolset_dir / "interactive_toolset"
_regulatory_toolset_dir = _toolset_dir / "regulatory_toolset"
def _read_manifest(toolset_dir=_base_toolset_dir): def _read_manifest(toolset_dir=_base_toolset_dir):
@@ -26,6 +27,7 @@ def _get_tool_def(manifest, name):
def test_manifest_json_exists(): def test_manifest_json_exists():
assert (_base_toolset_dir / "manifest.json").exists() assert (_base_toolset_dir / "manifest.json").exists()
assert (_interactive_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(): def test_manifest_has_all_tools():
@@ -33,11 +35,13 @@ def test_manifest_has_all_tools():
interactive_names = { interactive_names = {
t["name"] for t in _read_manifest(_interactive_toolset_dir)["tools"] 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 == { assert base_names == {
"shell_executor", "file_reader", "edit_file", "write_file", "shell_executor", "file_reader", "edit_file", "write_file",
"search_file", "python_executor", "tavily_search", "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(): def test_approval_metadata():
@@ -48,6 +52,15 @@ def test_approval_metadata():
assert tool["action_scope"] == [] 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(): def test_tavily_search_metadata():
manifest = _read_manifest() manifest = _read_manifest()
tool = _get_tool_def(manifest, "tavily_search") tool = _get_tool_def(manifest, "tavily_search")
@@ -59,7 +72,7 @@ def test_tavily_search_metadata():
def test_all_tool_files_exist(): 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) manifest = _read_manifest(toolset_dir)
for tool in manifest["tools"]: for tool in manifest["tools"]:
file_path = toolset_dir / tool["file"] 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 from kilostar.core.global_state_machine.tool_manager import GlobalToolManager
tm = 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 "shell_executor" in tm.tool_metadata
assert "tavily_search" in tm.tool_metadata assert "tavily_search" in tm.tool_metadata
assert "approval" 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(): def test_tool_manager_system_vs_third_party():