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:
@@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from kilostar.utils.standalone_proxy import actor_class, _STANDALONE
|
||||
from kilostar.utils.ray_compat import actor_class, _STANDALONE
|
||||
|
||||
if not _STANDALONE:
|
||||
import ray
|
||||
@@ -77,47 +77,40 @@ class GlobalStateMachine:
|
||||
# 启动期一次性发布 v1 快照,让等待中的读端立刻可用
|
||||
self._publish_snapshot()
|
||||
|
||||
_SYSTEM_TOOLSETS = [
|
||||
{
|
||||
"toolset_id": "system_basic",
|
||||
"name": "系统基础工具集",
|
||||
"description": "文件读写、搜索、代码执行等基础能力",
|
||||
"tools": ["file_reader", "write_file", "edit_file", "search_file", "python_executor", "shell_executor"],
|
||||
"is_system": True,
|
||||
"category": "system_basic",
|
||||
},
|
||||
{
|
||||
"toolset_id": "system_chat",
|
||||
"name": "系统对话工具集",
|
||||
"description": "对话场景专用工具(发送文件等)",
|
||||
"tools": ["send_file"],
|
||||
"is_system": True,
|
||||
"category": "system_chat",
|
||||
},
|
||||
{
|
||||
"toolset_id": "system_workflow",
|
||||
"name": "系统工作流工具集",
|
||||
"description": "工作流场景专用工具(审批、发送文件等)",
|
||||
"tools": ["approval", "send_file"],
|
||||
"is_system": True,
|
||||
"category": "system_workflow",
|
||||
},
|
||||
]
|
||||
|
||||
async def _seed_system_toolsets(self):
|
||||
"""若 DB 中缺少系统预置工具集则自动补种。"""
|
||||
for seed in self._SYSTEM_TOOLSETS:
|
||||
if seed["toolset_id"] not in self._custom_toolsets:
|
||||
await self.postgres_database.upsert_custom_toolset.remote(
|
||||
toolset_id=seed["toolset_id"],
|
||||
name=seed["name"],
|
||||
tools=seed["tools"],
|
||||
description=seed["description"],
|
||||
owner_id=None,
|
||||
is_system=True,
|
||||
category=seed["category"],
|
||||
)
|
||||
self._custom_toolsets[seed["toolset_id"]] = seed
|
||||
"""把磁盘上每个 toolset 包同步成一个 system custom_toolset 记录。
|
||||
|
||||
toolset 包就是插件单元——目录结构 ``data/toolset/<name>/`` 即代表一个工具集。
|
||||
启动时把每个包"投影"成一条 ``is_system=True`` 的 custom_toolset,
|
||||
前端工具插件界面看到的卡片就是这些包;将来安装第三方插件 = 把目录扔进去。
|
||||
|
||||
旧版本写死过 ``system_basic`` / ``system_chat`` / ``system_workflow`` 这种
|
||||
逻辑分组,这里会一并清理掉,避免遗留脏数据。
|
||||
"""
|
||||
packages = self._global_tool_manager.toolset_packages
|
||||
wanted_ids = {f"system::{name}" for name in packages.keys()}
|
||||
|
||||
# 清理 stale 系统 toolset(包括旧版硬编码的 system_basic/system_chat/...)
|
||||
for tid, ts in list(self._custom_toolsets.items()):
|
||||
if not ts.get("is_system"):
|
||||
continue
|
||||
if tid in wanted_ids:
|
||||
continue
|
||||
await self.postgres_database.delete_custom_toolset.remote(tid)
|
||||
self._custom_toolsets.pop(tid, None)
|
||||
|
||||
for name, pkg in packages.items():
|
||||
tid = f"system::{name}"
|
||||
saved = await self.postgres_database.upsert_custom_toolset.remote(
|
||||
toolset_id=tid,
|
||||
name=pkg.get("display_name") or name,
|
||||
tools=list(pkg.get("tools", [])),
|
||||
description=pkg.get("description") or None,
|
||||
owner_id=None,
|
||||
is_system=True,
|
||||
category=name,
|
||||
)
|
||||
self._custom_toolsets[tid] = saved
|
||||
|
||||
# ─── Snapshot 发布(Object Store 读路径) ────────────────────
|
||||
|
||||
@@ -254,6 +247,24 @@ class GlobalStateMachine:
|
||||
"""仅返回 retrieval 工具集(system_node 专用,不包含 generation 工具)。"""
|
||||
return self._global_tool_manager.get_retrieval_toolsets_for_scope(scope)
|
||||
|
||||
def list_toolset_packages(self) -> List[Dict[str, Any]]:
|
||||
"""列出所有磁盘工具包(前端"工具插件"页面卡片即由此渲染)。"""
|
||||
return [
|
||||
{k: v for k, v in pkg.items() if k != "readme_path"}
|
||||
for pkg in self._global_tool_manager.toolset_packages.values()
|
||||
]
|
||||
|
||||
def get_toolset_package_readme(self, name: str) -> Optional[str]:
|
||||
"""读取指定工具包的 README.md 内容;不存在返回 None。"""
|
||||
pkg = self._global_tool_manager.toolset_packages.get(name)
|
||||
if not pkg or not pkg.get("readme_path"):
|
||||
return None
|
||||
try:
|
||||
with open(pkg["readme_path"], "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ─── MCP Server Registry ───────────────────────────────────
|
||||
|
||||
async def add_mcp_server(self, server_id: str, config: Dict[str, Any]) -> bool:
|
||||
|
||||
@@ -33,7 +33,7 @@ import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from kilostar.utils.standalone_proxy import _STANDALONE
|
||||
from kilostar.utils.ray_compat import _STANDALONE
|
||||
|
||||
if not _STANDALONE:
|
||||
import ray
|
||||
@@ -63,7 +63,7 @@ class GSMSnapshot:
|
||||
tool_metadata: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
tool_funcs: Dict[str, Callable[..., Any]] = field(default_factory=dict)
|
||||
third_party_funcs: Dict[str, Callable[..., Any]] = field(default_factory=dict)
|
||||
tool_mapper: Dict[str, Dict[str, type]] = field(default_factory=dict)
|
||||
tool_mapper: Dict[str, Dict[str, Callable[..., Any]]] = field(default_factory=dict)
|
||||
# ``{scope: [tool_name, ...]}``:系统工具按 scope 维护的工具名清单。
|
||||
# 客户端按名字 + ``tool_funcs`` 在自己进程里重建 FunctionToolset,
|
||||
# 避开把不可序列化/版本耦合的 toolset 实例塞进快照的坑。
|
||||
|
||||
@@ -17,9 +17,11 @@ from collections import defaultdict
|
||||
import pathlib
|
||||
import json
|
||||
|
||||
from kilostar.utils.settings import get_plugin_dir
|
||||
|
||||
|
||||
class GlobalSkillManager:
|
||||
"""Skill 注册表:从 ``kilostar/plugin/skill/<name>/skill.json`` 启动期一次性扫描加载。"""
|
||||
"""Skill 注册表:从 ``data/plugin/skill/<name>/skill.json`` 启动期一次性扫描加载。"""
|
||||
|
||||
skill_mapper = Dict[str, Tuple[str]]
|
||||
"""skill的存储表"""
|
||||
@@ -27,23 +29,16 @@ class GlobalSkillManager:
|
||||
def __init__(self):
|
||||
self.skill_mapper = defaultdict(tuple)
|
||||
|
||||
import os
|
||||
|
||||
skill_plugin_dir = pathlib.Path(
|
||||
os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "plugin", "skill")
|
||||
)
|
||||
)
|
||||
skill_plugin_dir = get_plugin_dir() / "skill"
|
||||
if not skill_plugin_dir.exists() or not skill_plugin_dir.is_dir():
|
||||
return
|
||||
for item in skill_plugin_dir.iterdir():
|
||||
if item.is_dir() and not item.name.startswith((".", "__")):
|
||||
json_path = item / "skill.json" # 拼接文件路径
|
||||
json_path = item / "skill.json"
|
||||
if json_path.exists():
|
||||
try:
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
skill = json.load(f)
|
||||
# 提取并映射
|
||||
name = skill.get("name")
|
||||
if name:
|
||||
self.skill_mapper[name] = (
|
||||
@@ -55,13 +50,7 @@ class GlobalSkillManager:
|
||||
|
||||
def add_skill(self, skill_name: str) -> None:
|
||||
"""Add a skill to the manager by reading its skill.json from the path"""
|
||||
import os
|
||||
|
||||
skill_plugin_dir = pathlib.Path(
|
||||
os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "plugin", "skill")
|
||||
)
|
||||
)
|
||||
skill_plugin_dir = get_plugin_dir() / "skill"
|
||||
item = skill_plugin_dir / skill_name
|
||||
if item.is_dir() and not item.name.startswith((".", "__")):
|
||||
json_path = item / "skill.json"
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
import pathlib
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from typing import Any, Callable, Dict, List, Type
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
from kilostar.plugin.tool_plugin.base_tool import BaseToolData
|
||||
from kilostar.utils.settings import get_toolset_dir
|
||||
from kilostar.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("tool_manager")
|
||||
|
||||
_SYSTEM_BUCKET = "system"
|
||||
|
||||
def _bootstrap_toolset_modules():
|
||||
"""在 sys.modules 中注册 toolset 的虚拟包层级,使 toolset 内部的相对 import 能正常工作。"""
|
||||
toolset_dir = get_toolset_dir()
|
||||
|
||||
for pkg_name, pkg_path in [
|
||||
("data", toolset_dir.parent),
|
||||
("data.toolset", toolset_dir),
|
||||
]:
|
||||
if pkg_name not in sys.modules:
|
||||
mod = types.ModuleType(pkg_name)
|
||||
mod.__path__ = [str(pkg_path)]
|
||||
mod.__package__ = pkg_name
|
||||
sys.modules[pkg_name] = mod
|
||||
|
||||
|
||||
class GlobalToolManager:
|
||||
"""工具注册表:扫描 ``kilostar/plugin/tool_plugin/`` 下所有 BaseToolData 子类,
|
||||
按 ``action_scope`` 打包成 ``FunctionToolset``。
|
||||
"""工具注册表:扫描 ``data/toolset/`` 下所有 toolset 的 manifest.json,
|
||||
按 ``action_scope`` / ``is_system`` / ``category`` 分桶。
|
||||
|
||||
三类 toolset:
|
||||
- **system**:``is_system=True`` 的工具,按 scope 分组
|
||||
- **custom**:用户自定义工具组(由 ``rebuild_custom_toolsets`` 动态构建)
|
||||
- **mcp**:由 ``mcp_helper`` 独立管理,不经过本类
|
||||
|
||||
``category="mcp"`` 的工具不会被本类管理。
|
||||
"""
|
||||
|
||||
tool_metadata: Dict[str, Dict[str, Any]]
|
||||
@@ -30,7 +42,7 @@ class GlobalToolManager:
|
||||
_custom_toolsets: Dict[str, Any]
|
||||
_third_party_funcs: Dict[str, Callable]
|
||||
_all_funcs: Dict[str, Callable]
|
||||
tool_mapper: Dict[str, Dict[str, Type[BaseToolData]]]
|
||||
toolset_packages: Dict[str, Dict[str, Any]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tool_metadata = {}
|
||||
@@ -41,75 +53,126 @@ class GlobalToolManager:
|
||||
self._custom_toolsets = {}
|
||||
self._third_party_funcs = {}
|
||||
self._all_funcs = {}
|
||||
self.tool_mapper = defaultdict(dict)
|
||||
self.toolset_packages = {}
|
||||
|
||||
tool_plugin_dir = (
|
||||
pathlib.Path(__file__).parent.parent.parent / "plugin" / "tool_plugin"
|
||||
)
|
||||
if not tool_plugin_dir.exists() or not tool_plugin_dir.is_dir():
|
||||
_bootstrap_toolset_modules()
|
||||
|
||||
toolset_root = get_toolset_dir()
|
||||
if not toolset_root.exists() or not toolset_root.is_dir():
|
||||
return
|
||||
|
||||
for item in tool_plugin_dir.iterdir():
|
||||
if not (item.is_dir() and not item.name.startswith("__")):
|
||||
for toolset_dir in toolset_root.iterdir():
|
||||
if not toolset_dir.is_dir() or toolset_dir.name.startswith("__"):
|
||||
continue
|
||||
plugin_name = item.name
|
||||
module_name = f"kilostar.plugin.tool_plugin.{plugin_name}"
|
||||
manifest_path = toolset_dir / "manifest.json"
|
||||
if not manifest_path.exists():
|
||||
continue
|
||||
self._load_toolset(toolset_dir, manifest_path)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to import tool plugin {plugin_name}: {e}")
|
||||
self._build_system_toolsets()
|
||||
self._build_retrieval_toolsets()
|
||||
|
||||
def _load_toolset(self, toolset_dir, manifest_path) -> None:
|
||||
"""从一个 toolset 目录加载 manifest 并注册所有工具。"""
|
||||
try:
|
||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read manifest {manifest_path}: {e}")
|
||||
return
|
||||
|
||||
toolset_name = toolset_dir.name
|
||||
|
||||
# 注册 toolset 的虚拟包
|
||||
pkg_name = f"data.toolset.{toolset_name}"
|
||||
if pkg_name not in sys.modules:
|
||||
mod = types.ModuleType(pkg_name)
|
||||
mod.__path__ = [str(toolset_dir)]
|
||||
mod.__package__ = pkg_name
|
||||
sys.modules[pkg_name] = mod
|
||||
|
||||
registered_tools: List[str] = []
|
||||
|
||||
for tool_def in manifest.get("tools", []):
|
||||
tool_name = tool_def.get("name")
|
||||
tool_file = tool_def.get("file", f"{tool_name}.py")
|
||||
if not tool_name:
|
||||
continue
|
||||
|
||||
tool_data_cls = self._find_tool_data_class(module)
|
||||
if tool_data_cls is None:
|
||||
file_path = toolset_dir / tool_file
|
||||
if not file_path.exists():
|
||||
logger.warning(f"Tool file not found: {file_path}")
|
||||
continue
|
||||
|
||||
tool_func = getattr(module, plugin_name, None)
|
||||
if not callable(tool_func):
|
||||
logger.warning(
|
||||
f"Tool plugin '{plugin_name}' has no callable named "
|
||||
f"'{plugin_name}' in its module; skipped."
|
||||
)
|
||||
tool_func = self._load_tool_func(toolset_name, tool_name, file_path)
|
||||
if tool_func is None:
|
||||
continue
|
||||
|
||||
action_scopes = (
|
||||
tool_data_cls.model_fields.get("action_scope").default or []
|
||||
)
|
||||
is_system = bool(tool_data_cls.model_fields.get("is_system").default)
|
||||
category_field = tool_data_cls.model_fields.get("category")
|
||||
category = (category_field.default if category_field else "other") or "other"
|
||||
toolset_field = tool_data_cls.model_fields.get("toolset")
|
||||
toolset_name = (toolset_field.default if toolset_field else "other") or "other"
|
||||
is_system = tool_def.get("is_system", True)
|
||||
action_scopes = tool_def.get("action_scope", [])
|
||||
category = tool_def.get("category", "other")
|
||||
config_args = tool_def.get("config_args", {})
|
||||
toolset_field = tool_def.get("toolset", "other")
|
||||
|
||||
self.tool_metadata[plugin_name] = {
|
||||
"name": plugin_name,
|
||||
self.tool_metadata[tool_name] = {
|
||||
"name": tool_name,
|
||||
"is_system": is_system,
|
||||
"category": category,
|
||||
"toolset": toolset_name,
|
||||
"toolset": toolset_field,
|
||||
"action_scope": list(action_scopes),
|
||||
"config_args": config_args,
|
||||
"source_toolset": toolset_name,
|
||||
}
|
||||
registered_tools.append(tool_name)
|
||||
|
||||
if category == "mcp":
|
||||
continue
|
||||
|
||||
self._all_funcs[plugin_name] = tool_func
|
||||
self._all_funcs[tool_name] = tool_func
|
||||
|
||||
scopes = [s for s in action_scopes if s] or ["default"]
|
||||
|
||||
if is_system:
|
||||
for scope in scopes:
|
||||
self._tool_funcs[scope][plugin_name] = tool_func
|
||||
self.tool_mapper[scope][plugin_name] = tool_data_cls
|
||||
if toolset_name == "retrieval":
|
||||
self._retrieval_tool_funcs[scope][plugin_name] = tool_func
|
||||
self._tool_funcs[scope][tool_name] = tool_func
|
||||
if toolset_field == "retrieval":
|
||||
self._retrieval_tool_funcs[scope][tool_name] = tool_func
|
||||
else:
|
||||
self._third_party_funcs[plugin_name] = tool_func
|
||||
for scope in scopes:
|
||||
self.tool_mapper[scope][plugin_name] = tool_data_cls
|
||||
self._third_party_funcs[tool_name] = tool_func
|
||||
|
||||
self._build_system_toolsets()
|
||||
self._build_retrieval_toolsets()
|
||||
readme_path = toolset_dir / "README.md"
|
||||
self.toolset_packages[toolset_name] = {
|
||||
"name": toolset_name,
|
||||
"display_name": manifest.get("name", toolset_name),
|
||||
"version": manifest.get("version", ""),
|
||||
"description": manifest.get("description", ""),
|
||||
"tools": registered_tools,
|
||||
"has_readme": readme_path.exists(),
|
||||
"readme_path": str(readme_path) if readme_path.exists() else None,
|
||||
}
|
||||
|
||||
def _load_tool_func(self, toolset_name: str, tool_name: str, file_path) -> Callable | None:
|
||||
"""从文件加载工具函数。"""
|
||||
module_name = f"data.toolset.{toolset_name}.{tool_name}"
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(module_name, str(file_path))
|
||||
if spec is None or spec.loader is None:
|
||||
logger.warning(f"Failed to create spec for {module_name}")
|
||||
return None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
func = getattr(module, tool_name, None)
|
||||
if not callable(func):
|
||||
logger.warning(
|
||||
f"Tool '{tool_name}' has no callable named '{tool_name}' in {file_path}"
|
||||
)
|
||||
return None
|
||||
return func
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to import tool {tool_name}: {e}")
|
||||
return None
|
||||
|
||||
def _build_system_toolsets(self) -> None:
|
||||
FunctionToolset = self._import_function_toolset()
|
||||
@@ -142,11 +205,7 @@ class GlobalToolManager:
|
||||
logger.error(f"Failed to build retrieval toolset {scope}: {e}")
|
||||
|
||||
def rebuild_custom_toolsets(self, custom_defs: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""根据 DB 中的 toolset 定义重建 FunctionToolset。
|
||||
|
||||
系统 toolset(is_system=True)允许包含 system 工具,用户 toolset 只取得到 callable
|
||||
的工具(理论上业务层已校验只包含第三方工具)。
|
||||
"""
|
||||
"""根据 DB 中的 toolset 定义重建 FunctionToolset。"""
|
||||
FunctionToolset = self._import_function_toolset()
|
||||
if FunctionToolset is None:
|
||||
self._custom_toolsets = {}
|
||||
@@ -178,15 +237,13 @@ class GlobalToolManager:
|
||||
logger.warning("pydantic_ai.toolsets unavailable")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_tool_data_class(module) -> Type[BaseToolData] | None:
|
||||
for _, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if issubclass(obj, BaseToolData) and obj is not BaseToolData:
|
||||
return obj
|
||||
return None
|
||||
|
||||
# ─── Toolset accessors ───
|
||||
|
||||
@property
|
||||
def tool_mapper(self) -> Dict[str, Dict[str, Callable]]:
|
||||
"""scope → {tool_name: callable},兼容 GSM 快照构建。"""
|
||||
return dict(self._tool_funcs)
|
||||
|
||||
def get_system_toolset(self, scope: str) -> Any | None:
|
||||
return self._system_toolsets.get(scope)
|
||||
|
||||
@@ -230,7 +287,6 @@ class GlobalToolManager:
|
||||
def get_all_tools(self) -> List[Dict[str, Any]]:
|
||||
return list(self.tool_metadata.values())
|
||||
|
||||
# 兼容旧接口
|
||||
def get_non_system_tools(self) -> List[Dict[str, Any]]:
|
||||
return self.get_third_party_tools()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
from kilostar.utils.standalone_proxy import actor_class
|
||||
from kilostar.utils.ray_compat import actor_class
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
from kilostar.utils.logger import get_logger
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
from typing import Union, overload
|
||||
from kilostar.utils.standalone_proxy import actor_class
|
||||
from kilostar.utils.ray_compat import actor_class
|
||||
from kilostar.core.individual.consciousness_node.template import (
|
||||
ConsciousnessNodeDeps,
|
||||
ForregulatoryNode,
|
||||
@@ -29,7 +29,7 @@ from kilostar.core.global_state_machine.global_state_machine import GlobalStateM
|
||||
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
|
||||
from kilostar.adapter.model_adapter.agent_factory import AgentFactory
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
from kilostar.utils.i18n import agent_prompt
|
||||
from kilostar.utils.prompts import agent_prompt
|
||||
|
||||
|
||||
@actor_class
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from kilostar.utils.standalone_proxy import actor_class
|
||||
from kilostar.utils.ray_compat import actor_class
|
||||
from kilostar.core.global_state_machine.global_state_machine import GlobalStateMachine
|
||||
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
|
||||
from kilostar.adapter.model_adapter.agent_factory import AgentFactory
|
||||
@@ -22,7 +22,7 @@ from kilostar.core.individual.control_node.template import (
|
||||
ForWorkflowInput,
|
||||
ControlNodeDeps,
|
||||
)
|
||||
from kilostar.utils.i18n import agent_prompt
|
||||
from kilostar.utils.prompts import agent_prompt
|
||||
|
||||
|
||||
@actor_class
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import Union
|
||||
from kilostar.utils.standalone_proxy import actor_class
|
||||
from kilostar.utils.ray_compat import actor_class
|
||||
from kilostar.adapter.model_adapter.agent_factory import AgentFactory
|
||||
from kilostar.core.global_state_machine.global_state_machine import GlobalStateMachine
|
||||
from kilostar.core.global_state_machine.model_provider import Provider
|
||||
@@ -25,7 +25,7 @@ from kilostar.core.individual.regulatory_node.template import (
|
||||
MessageResponse
|
||||
)
|
||||
from pydantic_ai import RunContext, Agent
|
||||
from kilostar.utils.i18n import agent_prompt
|
||||
from kilostar.utils.prompts import agent_prompt
|
||||
|
||||
|
||||
@actor_class
|
||||
@@ -111,15 +111,20 @@ class RegulatoryNode:
|
||||
)
|
||||
return prompt
|
||||
|
||||
async def working(self, payload: MessageRequest) -> Union[MessageResponse, None]:
|
||||
async def working(
|
||||
self,
|
||||
payload: MessageRequest,
|
||||
message_history: list | None = None,
|
||||
) -> Union[MessageResponse, None]:
|
||||
"""working方法,是节点唯一的调用方法,对_run函数的结果进行判断并返回最终回复
|
||||
Args:
|
||||
payload: 消息载荷,包含所有信息
|
||||
message_history: pydantic-ai ``ModelMessage`` 列表,传入历史让多轮对话连贯
|
||||
|
||||
Returns:
|
||||
MessageResponse 或 None,监控节点对用户的结构化回复
|
||||
"""
|
||||
return await self._run(payload)
|
||||
return await self._run(payload, message_history=message_history)
|
||||
|
||||
_CHAT_INSTRUCTIONS = (
|
||||
"你是 kilostar 智能助手。你现在处于【直接对话模式】,请直接回答用户的问题。\n"
|
||||
@@ -130,7 +135,12 @@ class RegulatoryNode:
|
||||
"4. 回复应当完整、有帮助,避免过于简短。\n"
|
||||
)
|
||||
|
||||
async def stream_working(self, payload: MessageRequest, token_queue: "asyncio.Queue") -> None:
|
||||
async def stream_working(
|
||||
self,
|
||||
payload: MessageRequest,
|
||||
token_queue: "asyncio.Queue",
|
||||
message_history: list | None = None,
|
||||
) -> None:
|
||||
"""流式对话:完整执行 agent graph(含工具调用),逐 token 推送文本到 queue。
|
||||
|
||||
使用 event_stream_handler 回调拿到每个 text delta,保证工具调用后
|
||||
@@ -167,6 +177,7 @@ class RegulatoryNode:
|
||||
output_type=str,
|
||||
instructions=self._CHAT_INSTRUCTIONS,
|
||||
event_stream_handler=_stream_handler,
|
||||
message_history=message_history,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception(f"RegulatoryNode.stream_working failed: {e}")
|
||||
@@ -175,7 +186,9 @@ class RegulatoryNode:
|
||||
await token_queue.put(None)
|
||||
|
||||
async def _run(
|
||||
self, payload: MessageRequest
|
||||
self,
|
||||
payload: MessageRequest,
|
||||
message_history: list | None = None,
|
||||
) -> Union[MessageResponse, None]:
|
||||
platform = payload.platform
|
||||
user_name = payload.user_name
|
||||
@@ -187,8 +200,11 @@ class RegulatoryNode:
|
||||
user_name=user_name,
|
||||
time=time_str
|
||||
)
|
||||
agent_response = await self.agent.run(user_prompt=message,
|
||||
deps=deps,)
|
||||
agent_response = await self.agent.run(
|
||||
user_prompt=message,
|
||||
deps=deps,
|
||||
message_history=message_history,
|
||||
)
|
||||
response: MessageResponse = agent_response.output
|
||||
response.platform = platform
|
||||
response.platform_id = payload.platform_id
|
||||
|
||||
@@ -35,6 +35,8 @@ from kilostar.core.postgres_database.model.tool_config import ToolConfigModel
|
||||
from kilostar.core.postgres_database.model.custom_toolset import CustomToolsetModel
|
||||
from kilostar.core.postgres_database.model.system_event_log import SystemEventLog
|
||||
from kilostar.core.postgres_database.model.persona_template import PersonaTemplate
|
||||
from kilostar.core.postgres_database.model.org_task import OrgTask
|
||||
from kilostar.core.postgres_database.model.org_task_event import OrgTaskEvent
|
||||
|
||||
# 兼容旧代码的别名
|
||||
Provider = ProviderModel
|
||||
@@ -65,5 +67,7 @@ __all__ = [
|
||||
"CustomToolsetModel",
|
||||
"SystemEventLog",
|
||||
"PersonaTemplate",
|
||||
"OrgTask",
|
||||
"OrgTaskEvent",
|
||||
"AgentType",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from sqlalchemy import String, DateTime, Integer, func, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from .base import BaseDataModel
|
||||
|
||||
|
||||
class OrgTask(BaseDataModel):
|
||||
__tablename__ = "org_task"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(64), unique=True, index=True, comment="外部任务 ID(UUID)"
|
||||
)
|
||||
org_name: Mapped[str] = mapped_column(
|
||||
String(128), index=True, comment="所属组织/插件名"
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), index=True, default="pending",
|
||||
comment="pending/running/done/error"
|
||||
)
|
||||
description: Mapped[str] = mapped_column(
|
||||
Text, comment="任务描述"
|
||||
)
|
||||
result: Mapped[str | None] = mapped_column(
|
||||
Text, nullable=True, comment="最终结果"
|
||||
)
|
||||
context: Mapped[dict | None] = mapped_column(
|
||||
JSONB, nullable=True, comment="调用上下文"
|
||||
)
|
||||
created_at: Mapped[str] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), index=True
|
||||
)
|
||||
updated_at: Mapped[str] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(),
|
||||
onupdate=func.now()
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
from sqlalchemy import String, DateTime, Integer, func, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from .base import BaseDataModel
|
||||
|
||||
|
||||
class OrgTaskEvent(BaseDataModel):
|
||||
__tablename__ = "org_task_event"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(64), index=True, comment="关联的 org_task.task_id"
|
||||
)
|
||||
event_type: Mapped[str] = mapped_column(
|
||||
String(30), index=True,
|
||||
comment="log/step/artifact/approval_request/done/error"
|
||||
)
|
||||
payload: Mapped[dict | None] = mapped_column(
|
||||
JSONB, nullable=True, comment="事件负载"
|
||||
)
|
||||
created_at: Mapped[str] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), index=True
|
||||
)
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select, desc, update
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
|
||||
|
||||
from kilostar.core.postgres_database.model.org_task import OrgTask
|
||||
from kilostar.core.postgres_database.model.org_task_event import OrgTaskEvent
|
||||
from kilostar.core.postgres_database.database_exception import database_exception
|
||||
|
||||
|
||||
class OrgTaskDatabase:
|
||||
def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def create_task(
|
||||
self,
|
||||
task_id: str,
|
||||
org_name: str,
|
||||
description: str,
|
||||
context: Optional[dict] = None,
|
||||
) -> None:
|
||||
async with self.async_session_maker() as session:
|
||||
task = OrgTask(
|
||||
task_id=task_id,
|
||||
org_name=org_name,
|
||||
description=description,
|
||||
context=context,
|
||||
status="pending",
|
||||
)
|
||||
session.add(task)
|
||||
await session.commit()
|
||||
|
||||
@database_exception
|
||||
async def update_status(
|
||||
self, task_id: str, status: str, result: Optional[str] = None
|
||||
) -> None:
|
||||
async with self.async_session_maker() as session:
|
||||
stmt = (
|
||||
update(OrgTask)
|
||||
.where(OrgTask.task_id == task_id)
|
||||
.values(status=status, result=result)
|
||||
)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
@database_exception
|
||||
async def get_task(self, task_id: str) -> Optional[dict]:
|
||||
async with self.async_session_maker() as session:
|
||||
stmt = select(OrgTask).where(OrgTask.task_id == task_id)
|
||||
row = (await session.execute(stmt)).scalar_one_or_none()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"task_id": row.task_id,
|
||||
"org_name": row.org_name,
|
||||
"status": row.status,
|
||||
"description": row.description,
|
||||
"result": row.result,
|
||||
"context": row.context,
|
||||
"created_at": str(row.created_at) if row.created_at else None,
|
||||
"updated_at": str(row.updated_at) if row.updated_at else None,
|
||||
}
|
||||
|
||||
@database_exception
|
||||
async def list_tasks(
|
||||
self, org_name: Optional[str] = None, limit: int = 50, offset: int = 0
|
||||
) -> List[dict]:
|
||||
async with self.async_session_maker() as session:
|
||||
stmt = select(OrgTask).order_by(desc(OrgTask.created_at))
|
||||
if org_name:
|
||||
stmt = stmt.where(OrgTask.org_name == org_name)
|
||||
stmt = stmt.offset(offset).limit(limit)
|
||||
rows = (await session.execute(stmt)).scalars().all()
|
||||
return [
|
||||
{
|
||||
"task_id": r.task_id,
|
||||
"org_name": r.org_name,
|
||||
"status": r.status,
|
||||
"description": r.description,
|
||||
"created_at": str(r.created_at) if r.created_at else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@database_exception
|
||||
async def insert_event(
|
||||
self, task_id: str, event_type: str, payload: Optional[dict] = None
|
||||
) -> None:
|
||||
async with self.async_session_maker() as session:
|
||||
evt = OrgTaskEvent(
|
||||
task_id=task_id, event_type=event_type, payload=payload
|
||||
)
|
||||
session.add(evt)
|
||||
await session.commit()
|
||||
|
||||
@database_exception
|
||||
async def query_events(
|
||||
self, task_id: str, limit: int = 200
|
||||
) -> List[dict]:
|
||||
async with self.async_session_maker() as session:
|
||||
stmt = (
|
||||
select(OrgTaskEvent)
|
||||
.where(OrgTaskEvent.task_id == task_id)
|
||||
.order_by(OrgTaskEvent.created_at)
|
||||
.limit(limit)
|
||||
)
|
||||
rows = (await session.execute(stmt)).scalars().all()
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"task_id": r.task_id,
|
||||
"event_type": r.event_type,
|
||||
"payload": r.payload,
|
||||
"created_at": str(r.created_at) if r.created_at else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
@@ -15,7 +15,7 @@
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from kilostar.utils.standalone_proxy import actor_class
|
||||
from kilostar.utils.ray_compat import actor_class
|
||||
from kilostar.utils.settings import get_settings
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -44,6 +44,8 @@ from kilostar.core.postgres_database.model.tool_config import ToolConfigModel
|
||||
from kilostar.core.postgres_database.model.custom_toolset import CustomToolsetModel
|
||||
from kilostar.core.postgres_database.model.system_event_log import SystemEventLog
|
||||
from kilostar.core.postgres_database.model.persona_template import PersonaTemplate
|
||||
from kilostar.core.postgres_database.model.org_task import OrgTask
|
||||
from kilostar.core.postgres_database.model.org_task_event import OrgTaskEvent
|
||||
|
||||
from .module.individual import IndividualDatabase
|
||||
from .module.user import AuthDatabase
|
||||
@@ -56,6 +58,7 @@ from .module.tool_config import ToolConfigDatabase
|
||||
from .module.custom_toolset import CustomToolsetDatabase
|
||||
from .module.system_event_log import SystemEventLogDatabase
|
||||
from .module.persona_template import PersonaTemplateDatabase
|
||||
from .module.org_task import OrgTaskDatabase
|
||||
|
||||
|
||||
@actor_class
|
||||
@@ -89,6 +92,7 @@ class PostgresDatabase:
|
||||
self._custom_toolset_database = CustomToolsetDatabase(self.async_session_maker)
|
||||
self._system_event_log_database = SystemEventLogDatabase(self.async_session_maker)
|
||||
self._persona_template_database = PersonaTemplateDatabase(self.async_session_maker)
|
||||
self._org_task_database = OrgTaskDatabase(self.async_session_maker)
|
||||
|
||||
self.ready_event = asyncio.Event()
|
||||
|
||||
@@ -458,3 +462,28 @@ class PostgresDatabase:
|
||||
async def delete_template(self, template_id: str):
|
||||
await self.ready_event.wait()
|
||||
return await self._persona_template_database.delete_template(template_id)
|
||||
|
||||
# Org Task Database Methods
|
||||
async def create_org_task(self, task_id: str, org_name: str, description: str, context=None):
|
||||
await self.ready_event.wait()
|
||||
return await self._org_task_database.create_task(task_id, org_name, description, context)
|
||||
|
||||
async def update_org_task_status(self, task_id: str, status: str, result=None):
|
||||
await self.ready_event.wait()
|
||||
return await self._org_task_database.update_status(task_id, status, result)
|
||||
|
||||
async def get_org_task(self, task_id: str):
|
||||
await self.ready_event.wait()
|
||||
return await self._org_task_database.get_task(task_id)
|
||||
|
||||
async def list_org_tasks(self, org_name=None, limit=50, offset=0):
|
||||
await self.ready_event.wait()
|
||||
return await self._org_task_database.list_tasks(org_name, limit, offset)
|
||||
|
||||
async def insert_org_event(self, task_id: str, event_type: str, payload=None):
|
||||
await self.ready_event.wait()
|
||||
return await self._org_task_database.insert_event(task_id, event_type, payload)
|
||||
|
||||
async def query_org_events(self, task_id: str, limit=200):
|
||||
await self.ready_event.wait()
|
||||
return await self._org_task_database.query_events(task_id, limit)
|
||||
|
||||
@@ -36,7 +36,7 @@ import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
||||
|
||||
from kilostar.utils.standalone_proxy import remote_task, _STANDALONE
|
||||
from kilostar.utils.ray_compat import remote_task, _STANDALONE
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_graph import BaseNode, End, Graph, GraphRunContext
|
||||
from pydantic_graph.persistence import BaseStatePersistence
|
||||
|
||||
Reference in New Issue
Block a user