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:
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
██╗ ██╗██╗██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗
|
||||
██║ ██╔╝██║██║ ██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user