feat: 工具系统迁移 + 重型插件骨架 + 前端交互增强

- 工具系统从 kilostar/plugin/tool_plugin/ 迁移到 data/toolset/(manifest.json 声明式)
- 新增 plugin_runtime 模块:BaseOrganization / GlobalPluginManager / loader / tool_bridge
- 新增 org_task + org_task_event 表及 DAO(alembic 0009)
- 新增 /api/v1/plugin 路由(submit/status/stream/install/reload)
- 新增 data/plugin/example_dept 示例重型插件
- regulatory_node 支持聊天历史上下文注入
- send_file 改为 artifact 存盘 + SSE 推送下载链接
- 前端 WorkflowFileCard 组件 + ToolSettings README 渲染
- utils 整理:合并 access/role_check、standalone_proxy→ray_compat、删除废弃模块
- 项目结构文档移至 docs/STRUCTURE.md 并详细展开

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 05:20:00 +00:00
parent 9b73ae4db4
commit 6d658b4f4d
74 changed files with 2591 additions and 1308 deletions
+61 -2
View File
@@ -16,14 +16,15 @@ from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Annotated, Optional
import jwt
from fastapi import HTTPException, Request, status
from fastapi import Depends, HTTPException, Request, status
from pydantic import BaseModel, ValidationError
from pwdlib import PasswordHash
if TYPE_CHECKING:
from kilostar.core.postgres_database.model import UserAuthority
from kilostar.core.postgres_database.model.user import User
@@ -174,3 +175,61 @@ class Accessor:
if not (has_alpha and has_digit):
raise ValueError("密码必须同时包含字母和数字")
return password_hasher.hash(password)
# ─── Role Check ──────────────────────────────────────────────────────────────
def _user_not_found_detail(request: Request | None = None) -> str:
from kilostar.utils.i18n import t
loc = request.headers.get("accept-language") if request else None
return t("user_not_found", accept_language=loc)
async def get_authority(user_id: str) -> "UserAuthority":
"""通过 PostgresDatabase Actor 查出指定用户的 ``UserAuthority``;用户不存在时抛 401。"""
from kilostar.utils.error import UserNotExistError
from kilostar.utils.i18n import t
from kilostar.utils.ray_hook import ray_actor_hook
postgres_database = ray_actor_hook("postgres_database").postgres_database
try:
user_authority = await postgres_database.get_user_authority.remote(
user_id=user_id
)
return user_authority
except UserNotExistError:
raise HTTPException(status_code=401, detail=t("user_not_found"))
except Exception as e:
if "UserNotExistError" in str(e):
raise HTTPException(
status_code=401, detail=t("user_not_found")
)
raise
class RoleChecker:
"""FastAPI 依赖:在路由级别按 ``UserAuthority`` 做最低权限校验。
例:``Depends(RoleChecker(allowed_roles=UserAuthority.ADMINISTRATOR))``。
"""
def __init__(self, **kwargs):
self.allowed_roles = kwargs.get(
"allowed_roles",
)
async def __call__(
self, token_data: Annotated[TokenData, Depends(Accessor.get_current_user)]
):
"""对当前请求执行权限比较,权限不足抛 403,否则把 ``TokenData`` 透传给路由。"""
user_authority = await get_authority(token_data.user_id)
if user_authority < self.allowed_roles:
raise HTTPException(
status_code=403,
detail={
"message": f"User {token_data.user_id} does not have allowed roles"
},
)
return token_data
+3 -4
View File
@@ -14,14 +14,13 @@
from rich.console import Console
from rich.text import Text
import yaml
from kilostar.utils.config_loader import get_app_config
def print_banner() -> None:
"""在启动阶段输出 KiloStar 的 ASCII 横幅与版本/作者元信息。"""
with open("config/config.yml", "r") as config:
config = yaml.load(config, Loader=yaml.FullLoader)
version = config.get("version", "unknown")
version = get_app_config().app.version
kilostar_banner = r"""
██╗ ██╗██╗██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗
██║ ██╔╝██║██║ ██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗
-71
View File
@@ -1,71 +0,0 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Annotated
from fastapi import Depends, HTTPException, Request
from kilostar.utils.access import Accessor, TokenData
from kilostar.core.postgres_database.model import UserAuthority
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.i18n import t
def _user_not_found_detail(request: Request | None = None) -> str:
loc = request.headers.get("accept-language") if request else None
return t("user_not_found", accept_language=loc)
async def get_authority(user_id: str) -> UserAuthority:
"""通过 PostgresDatabase Actor 查出指定用户的 ``UserAuthority``;用户不存在时抛 401。"""
from kilostar.utils.error import UserNotExistError
postgres_database = ray_actor_hook("postgres_database").postgres_database
try:
user_authority = await postgres_database.get_user_authority.remote(
user_id=user_id
)
return user_authority
except UserNotExistError:
raise HTTPException(status_code=401, detail=t("user_not_found"))
except Exception as e:
# Check if it's a RayTaskError wrapping UserNotExistError
if "UserNotExistError" in str(e):
raise HTTPException(
status_code=401, detail=t("user_not_found")
)
raise
class RoleChecker:
"""FastAPI 依赖:在路由级别按 ``UserAuthority`` 做最低权限校验。
例:``Depends(RoleChecker(allowed_roles=UserAuthority.ADMINISTRATOR))``。
"""
def __init__(self, **kwargs):
self.allowed_roles = kwargs.get(
"allowed_roles",
)
async def __call__(
self, token_data: Annotated[TokenData, Depends(Accessor.get_current_user)]
):
"""对当前请求执行权限比较,权限不足抛 403,否则把 ``TokenData`` 透传给路由。"""
user_authority = await get_authority(token_data.user_id)
if user_authority < self.allowed_roles:
raise HTTPException(
status_code=403,
detail={
"message": f"User {token_data.user_id} does not have allowed roles"
},
)
return token_data
+57 -34
View File
@@ -1,57 +1,74 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib.util
import json
import os
import sys
from typing import Callable, Dict, List
from typing import Callable, Dict, List, Optional
from kilostar.utils.logger import get_logger
from kilostar.utils.settings import get_toolset_dir
logger = get_logger("get_tool")
_tool_cache: Dict[str, Callable] = {}
_manifest_cache: Optional[Dict[str, Dict]] = None
def _load_manifests() -> Dict[str, Dict]:
"""扫描所有 toolset 的 manifest.json,建立 tool_name → {toolset_dir, file} 的映射。"""
global _manifest_cache
if _manifest_cache is not None:
return _manifest_cache
_manifest_cache = {}
toolset_root = get_toolset_dir()
if not toolset_root.exists():
return _manifest_cache
for item in toolset_root.iterdir():
if not item.is_dir() or item.name.startswith("__"):
continue
manifest_path = item / "manifest.json"
if not manifest_path.exists():
continue
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = json.load(f)
for tool in manifest.get("tools", []):
tool_name = tool.get("name")
if tool_name:
_manifest_cache[tool_name] = {
"toolset_dir": str(item),
"toolset_name": item.name,
"file": tool.get("file", f"{tool_name}.py"),
}
except Exception as e:
logger.error(f"Failed to read manifest {manifest_path}: {e}")
return _manifest_cache
def _get_tool_func(tool_name: str) -> Callable | None:
"""按名字从 ``kilostar/plugin/tool_plugin/<tool_name>/__init__.py`` 中加载工具函数。
"""按名字从 toolset 中加载工具函数。
加载成功后会被缓存到模块级 ``_tool_cache``;找不到目录、找不到同名函数
导入失败都会记录日志并返回 ``None``。
根据 manifest 找到工具所在的 toolset 和文件,动态加载模块并取出同名函数
"""
func = _tool_cache.get(tool_name, None)
func = _tool_cache.get(tool_name)
if func:
return func
tool_plugin_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"plugin",
"tool_plugin",
tool_name,
)
if not os.path.exists(tool_plugin_dir) or not os.path.isdir(tool_plugin_dir):
logger.error(f"Tool directory not found: {tool_plugin_dir}")
manifests = _load_manifests()
info = manifests.get(tool_name)
if not info:
logger.error(f"Tool '{tool_name}' not found in any toolset manifest")
return None
init_file = os.path.join(tool_plugin_dir, "__init__.py")
if not os.path.exists(init_file):
logger.error(f"Tool init file not found: {init_file}")
tool_file = os.path.join(info["toolset_dir"], info["file"])
if not os.path.exists(tool_file):
logger.error(f"Tool file not found: {tool_file}")
return None
try:
module_name = f"kilostar.plugin.tool_plugin.{tool_name}"
spec = importlib.util.spec_from_file_location(module_name, init_file)
module_name = f"data.toolset.{info['toolset_name']}.{tool_name}"
spec = importlib.util.spec_from_file_location(module_name, tool_file)
if spec is None or spec.loader is None:
logger.error(f"Failed to create spec for {module_name}")
return None
@@ -70,7 +87,7 @@ def _get_tool_func(tool_name: str) -> Callable | None:
_tool_cache[tool_name] = func
return func
except Exception as e:
logger.error(f"Failed to load module {module_name}: {e}")
logger.error(f"Failed to load module {tool_name}: {e}")
return None
@@ -80,6 +97,12 @@ def del_tool_cache(tool_name: str) -> None:
del _tool_cache[tool_name]
def invalidate_manifest_cache() -> None:
"""清除 manifest 缓存,下次加载时重新扫描磁盘。"""
global _manifest_cache
_manifest_cache = None
def load_tools_from_list(tool_names: List[str] | None) -> List[Callable]:
"""批量加载工具:传入工具名列表,返回成功加载到的函数对象列表(失败项被跳过)。"""
if not tool_names:
-112
View File
@@ -17,7 +17,6 @@
设计原则:
- 纯内存字典,无文件 IO,Ray 远程序列化零成本。
- 支持环境变量 ``KILOSTAR_LANG`` 作为全局默认语言。
- Agent system prompt 按 ``{locale}`` 分桶,调用方显式传入 locale。
- API 层通过请求头 ``Accept-Language`` 解析首选语言。
当前支持:``zh`` (简体中文), ``en`` (English)。
@@ -31,93 +30,6 @@ from kilostar.utils.settings import get_settings
_DEFAULT_LOCALE: str = get_settings().kilostar_lang
# ─── Agent System Prompts ──────────────────────────────────────────────────
_PROMPTS: Dict[str, Dict[str, str]] = {
"regulatory_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【监管节点 (Regulatory Node)】。\n"
"你是系统中直接面向用户的对话节点,负责理解用户需求并提供高质量的回复。\n\n"
"你的核心职责:\n"
"1. 准确理解用户的意图,提供专业、友好且有帮助的回复。\n"
"2. 如果你有可用工具,可以主动调用工具来辅助回答(如搜索、文件操作等)。\n"
"3. 如果你收到工作流的执行报告,请将其转化为面向用户的清晰总结。\n"
"4. 保持回复简洁、有结构,避免冗余信息。\n"
"请保持专业、友好的沟通风格。"
),
"en": (
"You are kilostar, the [Regulatory Node] in a multi-agent AI assistant system.\n"
"You are the user-facing conversational node, responsible for understanding user needs and providing high-quality responses.\n\n"
"Your core responsibilities:\n"
"1. Accurately understand user intent and provide professional, friendly, and helpful replies.\n"
"2. If tools are available, proactively use them to assist your responses (e.g., search, file operations).\n"
"3. If you receive a workflow execution report, convert it into a clear user-facing summary.\n"
"4. Keep responses concise, well-structured, and free of redundancy.\n"
"Maintain a professional and friendly communication style."
),
},
"consciousness_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【意识节点 (Consciousness Node)】。\n"
"你是系统的'高级规划师''架构师',负责处理监控节点分配过来的复杂任务。\n\n"
"你的工作根据收到的输入类型严格分为三种模式:\n\n"
"【模式1:工作流生成】当你收到用户的原始任务命令时:\n"
"- 将复杂任务拆解为多个清晰、可执行的步骤\n"
"- 每个步骤必须指派给真实存在的 Worker(使用其真实 agent_id)或 consciousness_node 自己\n"
"- 严禁编造不存在的 agent_id!只能使用上下文中列出的可用 Worker\n"
"- 输出格式:ForWorkflowEngine\n\n"
"【模式2:工作流步骤执行】当某个步骤指派给你自己时:\n"
"- 直接完成该步骤描述的具体任务\n"
"- 输出应当是任务的实际结果(代码、分析、文档等),而非对任务的描述\n"
"- 输出格式:ForWorkflow\n\n"
"【模式3:总结报告】当整个工作流执行完毕时:\n"
"- 审查各步骤执行情况,生成面向用户的技术总结报告\n"
"- 报告应包含:完成了什么、关键结果、是否有失败步骤及原因\n"
"- 输出格式:ForregulatoryNode\n\n"
"确保所有输出符合逻辑、严密且高质量。"
),
"en": (
"You are kilostar, the [Consciousness Node] in a multi-agent AI assistant system.\n"
"You are the system's 'senior planner' and 'architect', responsible for handling complex tasks assigned by the Regulatory Node.\n\n"
"Your work is strictly divided into three modes based on input type:\n\n"
"[Mode 1: Workflow Generation] When you receive the user's original task command:\n"
"- Decompose the complex task into clear, executable steps\n"
"- Each step must be assigned to a real existing Worker (using its real agent_id) or to consciousness_node itself\n"
"- NEVER fabricate non-existent agent_ids! Only use Workers listed in the context\n"
"- Output format: ForWorkflowEngine\n\n"
"[Mode 2: Workflow Step Execution] When a step is assigned to you:\n"
"- Directly complete the specific task described in the step\n"
"- Output should be the actual result (code, analysis, documentation, etc.), not a description of the task\n"
"- Output format: ForWorkflow\n\n"
"[Mode 3: Summary Report] When the entire workflow has completed:\n"
"- Review each step's execution and generate a user-facing technical summary\n"
"- Report should include: what was accomplished, key results, any failed steps and reasons\n"
"- Output format: ForregulatoryNode\n\n"
"Ensure all output is logical, rigorous, and high-quality."
),
},
"control_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【控制节点 (Control Node)】。\n"
"你是系统的'执行者''车间主任',专门负责执行工作流中分配给你的具体子任务。\n"
"你的工作职责是:\n"
"1. 仔细分析分配给你的工作流步骤 (workflow_step) 的目标和要求。\n"
"2. 运用你被分配的工具 (如有) 或者依靠自身的知识和推理能力,精准、高效地完成该任务。\n"
"3. 将执行的结果、产生的数据或者具体的输出,严格按照 ForWorkflow 格式返回。\n"
"请注意:你的输出应当具体、实用,直接提供任务所要求的结果,不要做过多无关的寒暄。"
),
"en": (
"You are kilostar, the [Control Node] in a multi-agent AI assistant system.\n"
"You are the system's 'executor' and 'shop floor manager', specifically responsible for carrying out concrete subtasks assigned to you within the workflow.\n"
"Your duties are:\n"
"1. Carefully analyze the objectives and requirements of the workflow_step assigned to you.\n"
"2. Use the tools assigned to you (if any) or rely on your own knowledge and reasoning to complete the task accurately and efficiently.\n"
"3. Return the execution results, generated data, or concrete outputs strictly in the ForWorkflow format.\n"
"Note: Your output should be specific, practical, and directly provide the results requested by the task. Avoid excessive irrelevant pleasantries."
),
},
}
# ─── API / 通用消息 ────────────────────────────────────────────────────────
_MESSAGES: Dict[str, Dict[str, str]] = {
@@ -158,7 +70,6 @@ def _resolve_locale(locale: str | None = None, accept_language: str | None = Non
if locale:
return locale if locale in ("zh", "en") else _DEFAULT_LOCALE
if accept_language:
# 简单解析:取第一个 segment,若含 zh 则 zh,含 en 则 en
first = accept_language.split(",")[0].split(";")[0].strip().lower()
if "zh" in first:
return "zh"
@@ -182,26 +93,3 @@ def t(key: str, locale: str | None = None, accept_language: str | None = None, *
loc = _resolve_locale(locale, accept_language)
text = _MESSAGES.get(loc, {}).get(key) or _MESSAGES.get(_DEFAULT_LOCALE, {}).get(key) or key
return text.format(**kwargs) if kwargs else text
def agent_prompt(
agent_name: str,
locale: str | None = None,
accept_language: str | None = None,
custom_system_prompt: str | None = None,
) -> str:
"""获取指定 Agent 的 system prompt,并追加语言指令。
若 ``custom_system_prompt`` 不为空,追加在默认 prompt 和语言指令之后,
使管理员自定义内容能够覆盖/补充默认行为,同时保留角色定义。
"""
loc = _resolve_locale(locale, accept_language)
prompt = _PROMPTS.get(agent_name, {}).get(loc) or _PROMPTS.get(agent_name, {}).get(_DEFAULT_LOCALE, "")
lang_instruction = {
"zh": "\n\n【重要】请始终使用简体中文进行思考和回复。",
"en": "\n\n[Important] Please always think and reply in English.",
}.get(loc, "")
result = prompt + lang_instruction
if custom_system_prompt and custom_system_prompt.strip():
result += f"\n\n{custom_system_prompt.strip()}"
return result
-39
View File
@@ -1,39 +0,0 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Type, TypeVar
from pydantic import BaseModel
T = TypeVar("T", bound=Type[BaseModel])
def pickle(cls: T) -> T:
"""
类装饰器pickle
通过装饰继承了BaseModel的类,用pydantic的高效序列化替代python原生__reduce__魔术方法,实现ray在通讯时的高效序列化
Args:
cls: 继承了BaseModel类的类,需要被装饰的对象
Returns:
返回被重写了__reduce__魔术方法的cls类
"""
def __reduce__(self):
# 1. 序列化:触发 Pydantic-core (Rust) 的极速序列化
data = self.model_dump_json()
# 2. 反序列化:告诉 Pickle 重建时调用 cls.model_validate_json
return cls.model_validate_json, (data,)
cls.__reduce__ = __reduce__
return cls
+121
View File
@@ -0,0 +1,121 @@
"""Agent system prompt 模板。
按 agent 角色 × locale 组织,供各节点初始化时获取对应 system prompt。
"""
from __future__ import annotations
from typing import Dict
from kilostar.utils.i18n import _resolve_locale
_PROMPTS: Dict[str, Dict[str, str]] = {
"regulatory_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【监管节点 (Regulatory Node)】。\n"
"你是系统中直接面向用户的对话节点,负责理解用户需求并提供高质量的回复。\n\n"
"你的核心职责:\n"
"1. 准确理解用户的意图,提供专业、友好且有帮助的回复。\n"
"2. 如果你有可用工具,可以主动调用工具来辅助回答(如搜索、文件操作等)。\n"
"3. 如果你收到工作流的执行报告,请将其转化为面向用户的清晰总结。\n"
"4. 保持回复简洁、有结构,避免冗余信息。\n"
"请保持专业、友好的沟通风格。"
),
"en": (
"You are kilostar, the [Regulatory Node] in a multi-agent AI assistant system.\n"
"You are the user-facing conversational node, responsible for understanding user needs and providing high-quality responses.\n\n"
"Your core responsibilities:\n"
"1. Accurately understand user intent and provide professional, friendly, and helpful replies.\n"
"2. If tools are available, proactively use them to assist your responses (e.g., search, file operations).\n"
"3. If you receive a workflow execution report, convert it into a clear user-facing summary.\n"
"4. Keep responses concise, well-structured, and free of redundancy.\n"
"Maintain a professional and friendly communication style."
),
},
"consciousness_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【意识节点 (Consciousness Node)】。\n"
"你是系统的'高级规划师''架构师',负责处理监控节点分配过来的复杂任务。\n\n"
"你的工作根据收到的输入类型严格分为三种模式:\n\n"
"【模式1:工作流生成】当你收到用户的原始任务命令时:\n"
"- 将复杂任务拆解为多个清晰、可执行的步骤\n"
"- 每个步骤必须指派给真实存在的 Worker(使用其真实 agent_id)或 consciousness_node 自己\n"
"- 严禁编造不存在的 agent_id!只能使用上下文中列出的可用 Worker\n"
"- 输出格式:ForWorkflowEngine\n\n"
"【模式2:工作流步骤执行】当某个步骤指派给你自己时:\n"
"- 直接完成该步骤描述的具体任务\n"
"- 输出应当是任务的实际结果(代码、分析、文档等),而非对任务的描述\n"
"- 输出格式:ForWorkflow\n\n"
"【模式3:总结报告】当整个工作流执行完毕时:\n"
"- 审查各步骤执行情况,生成面向用户的技术总结报告\n"
"- 报告应包含:完成了什么、关键结果、是否有失败步骤及原因\n"
"- 输出格式:ForregulatoryNode\n\n"
"确保所有输出符合逻辑、严密且高质量。"
),
"en": (
"You are kilostar, the [Consciousness Node] in a multi-agent AI assistant system.\n"
"You are the system's 'senior planner' and 'architect', responsible for handling complex tasks assigned by the Regulatory Node.\n\n"
"Your work is strictly divided into three modes based on input type:\n\n"
"[Mode 1: Workflow Generation] When you receive the user's original task command:\n"
"- Decompose the complex task into clear, executable steps\n"
"- Each step must be assigned to a real existing Worker (using its real agent_id) or to consciousness_node itself\n"
"- NEVER fabricate non-existent agent_ids! Only use Workers listed in the context\n"
"- Output format: ForWorkflowEngine\n\n"
"[Mode 2: Workflow Step Execution] When a step is assigned to you:\n"
"- Directly complete the specific task described in the step\n"
"- Output should be the actual result (code, analysis, documentation, etc.), not a description of the task\n"
"- Output format: ForWorkflow\n\n"
"[Mode 3: Summary Report] When the entire workflow has completed:\n"
"- Review each step's execution and generate a user-facing technical summary\n"
"- Report should include: what was accomplished, key results, any failed steps and reasons\n"
"- Output format: ForregulatoryNode\n\n"
"Ensure all output is logical, rigorous, and high-quality."
),
},
"control_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【控制节点 (Control Node)】。\n"
"你是系统的'执行者''车间主任',专门负责执行工作流中分配给你的具体子任务。\n"
"你的工作职责是:\n"
"1. 仔细分析分配给你的工作流步骤 (workflow_step) 的目标和要求。\n"
"2. 运用你被分配的工具 (如有) 或者依靠自身的知识和推理能力,精准、高效地完成该任务。\n"
"3. 将执行的结果、产生的数据或者具体的输出,严格按照 ForWorkflow 格式返回。\n"
"请注意:你的输出应当具体、实用,直接提供任务所要求的结果,不要做过多无关的寒暄。"
),
"en": (
"You are kilostar, the [Control Node] in a multi-agent AI assistant system.\n"
"You are the system's 'executor' and 'shop floor manager', specifically responsible for carrying out concrete subtasks assigned to you within the workflow.\n"
"Your duties are:\n"
"1. Carefully analyze the objectives and requirements of the workflow_step assigned to you.\n"
"2. Use the tools assigned to you (if any) or rely on your own knowledge and reasoning to complete the task accurately and efficiently.\n"
"3. Return the execution results, generated data, or concrete outputs strictly in the ForWorkflow format.\n"
"Note: Your output should be specific, practical, and directly provide the results requested by the task. Avoid excessive irrelevant pleasantries."
),
},
}
def agent_prompt(
agent_name: str,
locale: str | None = None,
accept_language: str | None = None,
custom_system_prompt: str | None = None,
) -> str:
"""获取指定 Agent 的 system prompt,并追加语言指令。
若 ``custom_system_prompt`` 不为空,追加在默认 prompt 和语言指令之后,
使管理员自定义内容能够覆盖/补充默认行为,同时保留角色定义。
"""
from kilostar.utils.settings import get_settings
_DEFAULT_LOCALE = get_settings().kilostar_lang
loc = _resolve_locale(locale, accept_language)
prompt = _PROMPTS.get(agent_name, {}).get(loc) or _PROMPTS.get(agent_name, {}).get(_DEFAULT_LOCALE, "")
lang_instruction = {
"zh": "\n\n【重要】请始终使用简体中文进行思考和回复。",
"en": "\n\n[Important] Please always think and reply in English.",
}.get(loc, "")
result = prompt + lang_instruction
if custom_system_prompt and custom_system_prompt.strip():
result += f"\n\n{custom_system_prompt.strip()}"
return result
@@ -1,4 +1,4 @@
"""KiloStar 单机模式适配层:用 asyncio 协程模拟 Ray Actor 接口
"""KiloStar Ray 兼容层:单机/分布式模式无感切换 + 序列化工具
单机模式下所有 Actor 退化为普通 Python 异步单例通过 StandaloneProxy
包装后暴露与 Ray Actor Handle 相同的 `.method.remote(args)` 调用接口
@@ -9,10 +9,14 @@ from __future__ import annotations
import asyncio
import os
from typing import Any
from typing import Any, Type, TypeVar
from pydantic import BaseModel
_STANDALONE = os.environ.get("KILOSTAR_MODE", "distributed") == "standalone"
T = TypeVar("T", bound=Type[BaseModel])
class _MethodProxy:
"""包装单个方法,使 .remote(*args, **kwargs) 返回一个可 await 的 Task。"""
@@ -84,3 +88,19 @@ def remote_task(func):
import ray
return ray.remote(func)
# ─── Pickle (Ray 序列化优化) ───
def pickle(cls: T) -> T:
"""类装饰器:用 Pydantic 的高效 JSON 序列化替代 Python 原生 __reduce__
使 Ray 跨进程通信时对 BaseModel 子类走 Rust 级序列化
"""
def __reduce__(self):
data = self.model_dump_json()
return cls.model_validate_json, (data,)
cls.__reduce__ = __reduce__
return cls
+2 -2
View File
@@ -15,7 +15,7 @@ import time
from functools import lru_cache
from typing import Any, Dict
from kilostar.utils.standalone_proxy import _STANDALONE
from kilostar.utils.ray_compat import _STANDALONE
if not _STANDALONE:
import ray
@@ -49,7 +49,7 @@ _standalone_registry: Dict[str, Any] = {}
def register_standalone(name: str, instance: Any) -> None:
"""注册一个单机模式下的 Actor 单例(已包装为 StandaloneProxy)。"""
from kilostar.utils.standalone_proxy import StandaloneProxy
from kilostar.utils.ray_compat import StandaloneProxy
_standalone_registry[name] = StandaloneProxy(instance)
+46
View File
@@ -41,6 +41,9 @@ class AppSettings(BaseSettings):
kilostar_mode: str = "distributed"
kilostar_lang: str = "zh"
kilostar_cors_origins: str = ""
kilostar_plugin_dir: str = ""
kilostar_toolset_dir: str = ""
kilostar_artifact_dir: str = ""
db: DatabaseSettings = Field(default_factory=DatabaseSettings)
security: SecuritySettings = Field(default_factory=SecuritySettings)
@@ -53,3 +56,46 @@ class AppSettings(BaseSettings):
@lru_cache(maxsize=1)
def get_settings() -> AppSettings:
return AppSettings()
def get_plugin_dir() -> "pathlib.Path":
"""返回插件根目录路径(包含 tool_plugin/ 和 skill/ 子目录)。
优先使用环境变量 KILOSTAR_PLUGIN_DIR,否则默认 <project_root>/data/plugin/。
"""
import pathlib
custom = get_settings().kilostar_plugin_dir
if custom:
return pathlib.Path(custom)
project_root = pathlib.Path(__file__).parent.parent.parent
return project_root / "data" / "plugin"
def get_toolset_dir() -> "pathlib.Path":
"""返回工具集根目录路径(包含各 toolset 子目录,如 base_toolset/)。
优先使用环境变量 KILOSTAR_TOOLSET_DIR,否则默认 <project_root>/data/toolset/。
"""
import pathlib
custom = get_settings().kilostar_toolset_dir
if custom:
return pathlib.Path(custom)
project_root = pathlib.Path(__file__).parent.parent.parent
return project_root / "data" / "toolset"
def get_artifact_dir() -> "pathlib.Path":
"""返回工作流产物(agent 通过 send_file 推送的文件)存放根目录。
优先使用环境变量 KILOSTAR_ARTIFACT_DIR,否则默认 <project_root>/data/artifact/。
每个 trace_id 一个子目录,下载链接走 /api/v1/resource/artifact/{trace_id}/{aid}
"""
import pathlib
custom = get_settings().kilostar_artifact_dir
if custom:
return pathlib.Path(custom)
project_root = pathlib.Path(__file__).parent.parent.parent
return project_root / "data" / "artifact"