chore: 迁移服务器前同步本地修改

This commit is contained in:
2026-08-05 21:26:13 +08:00
parent aa47a19e98
commit 1a914a2517
73 changed files with 908 additions and 1961 deletions
@@ -24,8 +24,9 @@ from pydantic_ai.providers.deepseek import DeepSeekProvider
from pydantic_ai.providers.google import GoogleProvider
from pydantic_ai.toolsets import AbstractToolset
from pydantic import BaseModel
from kilostar.core.global_state_machine.model_provider import Provider
from kilostar.utils.agent_model import ResponseModel, DepsModel
from kilostar.utils.error import ModelNotExistError
@@ -65,9 +66,9 @@ class AgentFactory:
self,
provider: Provider,
model_id: str,
output_type: ResponseModel,
output_type: type[BaseModel],
system_prompt: str,
deps_type: DepsModel,
deps_type: type[BaseModel],
agent_name: str,
tools: list = None,
toolsets: Sequence[AbstractToolset[Any]] = None,
+1 -14
View File
@@ -13,19 +13,14 @@
# limitations under the License.
import os
from typing import Dict
from fastapi import FastAPI, WebSocket, Request
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from kilostar.utils.ray_compat import _STANDALONE
from kilostar.utils.settings import get_settings
if not _STANDALONE:
from ray import serve
from .agent import agent_router
from .auth import auth_router
from .system import system_router, system_api_router
@@ -221,11 +216,3 @@ else:
)
if not _STANDALONE:
@serve.deployment
@serve.ingress(app)
class KiloStarGateway:
gateway: Dict[str, WebSocket]
def __init__(self):
self.gateway = {}
+42 -42
View File
@@ -14,7 +14,7 @@
from typing import Union
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel, field_validator
from kilostar.utils.access import Accessor, TokenData, RoleChecker
@@ -52,8 +52,8 @@ async def get_system_nodes(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""返回两大系统节点(regulatory/consciousness)当前的持久化配置。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
configs = await postgres_database.get_all_system_node_configs.remote()
postgres_database = get_actor("postgres_database")
configs = await postgres_database.get_all_system_node_configs()
return {"system_nodes": configs}
@@ -64,8 +64,8 @@ async def load_agent(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""加载/重载某个系统节点的 Agent:先持久化配置,再调用对应节点 Actor 的 ``create_agent``。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
postgres_database = ray_actor_hook("postgres_database").postgres_database
global_state_machine = get_actor("global_state_machine")
postgres_database = get_actor("postgres_database")
accept_lang = request.headers.get("accept-language", "")
if isinstance(agent_register, AgentLocalRegister):
@@ -73,7 +73,7 @@ async def load_agent(
elif isinstance(agent_register, AgentRegister):
try:
await postgres_database.upsert_system_node_config.remote(
await postgres_database.upsert_system_node_config(
agent_register.individual_name,
agent_register.provider_title,
agent_register.model_id,
@@ -90,14 +90,14 @@ async def load_agent(
# Resolve persona system_prompt from DB
persona_prompt = None
if agent_register.persona_id:
tpl = await postgres_database.get_template.remote(agent_register.persona_id)
tpl = await postgres_database.get_template(agent_register.persona_id)
if tpl:
persona_prompt = tpl.system_prompt
match scope:
case "regulatory_node":
node = ray_actor_hook("regulatory_node").regulatory_node
await node.create_agent.remote(
node = get_actor("regulatory_node")
await node.create_agent(
global_state_machine,
agent_register.provider_title,
agent_register.model_id,
@@ -107,8 +107,8 @@ async def load_agent(
persona_prompt,
)
case "consciousness_node":
node = ray_actor_hook("consciousness_node").consciousness_node
await node.create_agent.remote(
node = get_actor("consciousness_node")
await node.create_agent(
global_state_machine,
agent_register.provider_title,
agent_register.model_id,
@@ -181,10 +181,10 @@ async def create_worker_individual(
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""创建一个 Worker Agent,``owner_id`` 自动绑定为当前登录用户。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
data_dict = worker_data.model_dump()
data_dict["owner_id"] = token_data.user_id
worker = await postgres_database.add_worker_individual.remote(**data_dict)
worker = await postgres_database.add_worker_individual(**data_dict)
return {"message": "success", "agent_id": worker.agent_id}
@@ -197,11 +197,11 @@ async def get_worker_individual_list(
plugin_owned slot 是插件登记的"占位 agent",所有用户共享同一份配置,
在前端展示时会用徽标标记,并允许任何登录用户装配 provider/model。
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
workers = await postgres_database.get_worker_individual_list.remote(
postgres_database = get_actor("postgres_database")
workers = await postgres_database.get_worker_individual_list(
owner_id=token_data.user_id
) or []
all_workers = await postgres_database.get_all_worker_individual.remote() or []
all_workers = await postgres_database.get_all_worker_individual() or []
seen_ids = {w.agent_id for w in workers}
plugin_slots = [
w for w in all_workers
@@ -215,8 +215,8 @@ async def get_worker_individual(
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
):
"""按 ``agent_id`` 查询 Worker Agent;非本人的 Agent 返回 403(plugin_owned slot 例外)。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
postgres_database = get_actor("postgres_database")
worker = await postgres_database.get_worker_individual(agent_id=agent_id)
if not worker:
raise HTTPException(status_code=404, detail="Agent not found")
if not getattr(worker, "plugin_owned", None) and worker.owner_id != token_data.user_id:
@@ -233,8 +233,8 @@ async def update_worker_individual(
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""局部更新 Worker Agent 配置;同时把状态机里的旧实例移除等待懒加载。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
postgres_database = get_actor("postgres_database")
worker = await postgres_database.get_worker_individual(agent_id=agent_id)
if not worker:
raise HTTPException(status_code=404, detail="Agent not found")
# plugin_owned slot:任何登录用户都能装配 provider/model;普通 worker 仅 owner 可改
@@ -244,21 +244,21 @@ async def update_worker_individual(
)
update_data = worker_data.model_dump(exclude_unset=True)
updated_worker = await postgres_database.update_worker_individual.remote(
updated_worker = await postgres_database.update_worker_individual(
agent_id=agent_id, **update_data
)
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
global_state_machine = get_actor("global_state_machine")
try:
await global_state_machine.remove_individual.remote(agent_id)
await global_state_machine.remove_individual(agent_id)
except Exception:
pass
# plugin_owned 时顺带触发对应插件的 reload,让新 provider/model 立刻生效
if getattr(worker, "plugin_owned", None):
try:
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
await pm.reload.remote(worker.plugin_owned)
pm = get_actor("global_plugin_manager")
await pm.reload(worker.plugin_owned)
except Exception:
pass
@@ -270,8 +270,8 @@ async def reload_worker_individual(
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
):
"""强制把 Worker 从内存池中卸载,下次调用时按最新配置重新加载。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
postgres_database = get_actor("postgres_database")
worker = await postgres_database.get_worker_individual(agent_id=agent_id)
if not worker:
raise HTTPException(status_code=404, detail="Agent not found")
if worker.owner_id != token_data.user_id:
@@ -279,8 +279,8 @@ async def reload_worker_individual(
status_code=403, detail="Forbidden: You do not own this agent"
)
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.remove_individual.remote(agent_id)
global_state_machine = get_actor("global_state_machine")
await global_state_machine.remove_individual(agent_id)
return {"message": "Worker will be reloaded on next use"}
@@ -290,15 +290,15 @@ async def delete_worker_individual(
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
):
"""删除 Worker Agent;非本人 Agent 返回 403。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
postgres_database = get_actor("postgres_database")
worker = await postgres_database.get_worker_individual(agent_id=agent_id)
if not worker:
raise HTTPException(status_code=404, detail="Agent not found")
if worker.owner_id != token_data.user_id:
raise HTTPException(
status_code=403, detail="Forbidden: You do not own this agent"
)
await postgres_database.delete_worker_individual.remote(agent_id=agent_id)
await postgres_database.delete_worker_individual(agent_id=agent_id)
return {"message": "success"}
@@ -318,8 +318,8 @@ class PersonaTemplateUpdate(BaseModel):
async def list_templates(
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
templates = await postgres_database.list_templates.remote(
postgres_database = get_actor("postgres_database")
templates = await postgres_database.list_templates(
owner_id=token_data.user_id
)
return {"templates": templates}
@@ -330,8 +330,8 @@ async def create_template(
data: PersonaTemplateCreate,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
tpl = await postgres_database.add_template.remote(
postgres_database = get_actor("postgres_database")
tpl = await postgres_database.add_template(
name=data.name,
system_prompt=data.system_prompt,
owner_id=token_data.user_id,
@@ -345,13 +345,13 @@ async def update_template(
data: PersonaTemplateUpdate,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
tpl = await postgres_database.get_template.remote(template_id)
postgres_database = get_actor("postgres_database")
tpl = await postgres_database.get_template(template_id)
if not tpl:
raise HTTPException(status_code=404, detail="Template not found")
if tpl.owner_id != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
updated = await postgres_database.update_template.remote(
updated = await postgres_database.update_template(
template_id, **data.model_dump(exclude_unset=True)
)
return {"message": "success", "template": updated}
@@ -362,11 +362,11 @@ async def delete_template(
template_id: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
tpl = await postgres_database.get_template.remote(template_id)
postgres_database = get_actor("postgres_database")
tpl = await postgres_database.get_template(template_id)
if not tpl:
raise HTTPException(status_code=404, detail="Template not found")
if tpl.owner_id != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
await postgres_database.delete_template.remote(template_id)
await postgres_database.delete_template(template_id)
return {"message": "success"}
+11 -11
View File
@@ -17,7 +17,7 @@ from fastapi import Depends
from pydantic import BaseModel
from kilostar.utils.access import Accessor, TokenData, RoleChecker
from fastapi.concurrency import run_in_threadpool
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from kilostar.core.postgres_database.model import UserAuthority
from kilostar.utils.error import UserNotExistError
from kilostar.utils.rate_limit import register_limiter, login_limiter
@@ -36,7 +36,7 @@ class UserRegister(BaseModel):
async def create_user(user_register: UserRegister, request: Request):
"""注册新用户:异步线程池里做 argon2 哈希,再交由 PostgresDatabase Actor 落库。"""
register_limiter.check(request)
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
try:
hashed_password = await run_in_threadpool(
Accessor.hash_password, user_register.password
@@ -47,7 +47,7 @@ async def create_user(user_register: UserRegister, request: Request):
status_code=400,
content={"code": "password_invalid", "message": str(e)},
)
user = await postgres_database.add_user.remote(
user = await postgres_database.add_user(
user_register.user_name, hashed_password
)
return {"message": "success", "user_id": user.user_id}
@@ -64,8 +64,8 @@ class UserLogin(BaseModel):
async def login_user(user_login: UserLogin, request: Request):
"""用户登录:查询用户后在线程池中校验口令,校验成功则签发 JWT。"""
login_limiter.check(request)
postgres_database = ray_actor_hook("postgres_database").postgres_database
user = await postgres_database.login_user.remote(user_login.user_name)
postgres_database = get_actor("postgres_database")
user = await postgres_database.login_user(user_login.user_name)
if not user:
raise UserNotExistError()
tokens = await run_in_threadpool(
@@ -113,8 +113,8 @@ async def change_authority(
"""
Update a user's authority level. Only accessible by SUPER_ADMINISTRATOR.
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
user = await postgres_database.change_user_authority.remote(
postgres_database = get_actor("postgres_database")
user = await postgres_database.change_user_authority(
user_id=request.user_id, new_authority=request.new_authority
)
return {
@@ -133,8 +133,8 @@ async def get_user_list(
"""
Get a list of all users. Only accessible by SUPER_ADMINISTRATOR.
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
users = await postgres_database.get_all_users.remote()
postgres_database = get_actor("postgres_database")
users = await postgres_database.get_all_users()
return {
"users": [
{"user_id": u.user_id, "user_name": u.user_name, "role": u.user_authority}
@@ -153,6 +153,6 @@ async def delete_user(
"""
Delete a user. Only accessible by SUPER_ADMINISTRATOR.
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
await postgres_database.delete_user_by_id.remote(user_id=user_id)
postgres_database = get_actor("postgres_database")
await postgres_database.delete_user_by_id(user_id=user_id)
return {"message": "success"}
+27 -40
View File
@@ -17,7 +17,7 @@ import asyncio
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from kilostar.utils.access import Accessor, TokenData
from kilostar.core.individual.regulatory_node.template import (
MessageRequest,
@@ -56,8 +56,8 @@ def _build_message_history(rows) -> list:
async def _load_message_history(chat_id: str) -> list:
postgres_database = ray_actor_hook("postgres_database").postgres_database
rows = await postgres_database.list_chat_messages.remote(chat_id=chat_id)
postgres_database = get_actor("postgres_database")
rows = await postgres_database.list_chat_messages(chat_id=chat_id)
return _build_message_history(rows or [])
@@ -76,14 +76,14 @@ async def _ask_regulatory(
*, user_id: str, chat_id: str, message: str, message_history: list | None = None
) -> str | None:
"""统一封装 chat 入口对 RegulatoryNode 的调用。"""
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
regulatory_node = get_actor("regulatory_node")
payload = MessageRequest(
platform="client",
user_name=user_id,
platform_id=chat_id,
message=message,
)
resp: MessageResponse | None = await regulatory_node.working.remote(
resp: MessageResponse | None = await regulatory_node.working(
payload, message_history
)
return _extract_reply(resp)
@@ -103,13 +103,13 @@ async def create_chat_session(
request: CreateChatRequest,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
chat = await postgres_database.create_chat_session.remote(
postgres_database = get_actor("postgres_database")
chat = await postgres_database.create_chat_session(
user_id=token_data.user_id, title=request.title
)
# 存入用户消息
await postgres_database.add_chat_message.remote(
await postgres_database.add_chat_message(
chat_id=chat.chat_id, message=request.initial_message, message_owner="user"
)
@@ -122,7 +122,7 @@ async def create_chat_session(
# 存入回复消息
if response_msg:
await postgres_database.add_chat_message.remote(
await postgres_database.add_chat_message(
chat_id=chat.chat_id, message=response_msg, message_owner="regulatory_node"
)
@@ -133,8 +133,8 @@ async def create_chat_session(
async def list_chat_sessions(
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
sessions = await postgres_database.list_chat_sessions.remote(
postgres_database = get_actor("postgres_database")
sessions = await postgres_database.list_chat_sessions(
user_id=token_data.user_id
)
return {"sessions": sessions}
@@ -144,8 +144,8 @@ async def list_chat_sessions(
async def get_chat_history(
chat_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
messages = await postgres_database.list_chat_messages.remote(chat_id=chat_id)
postgres_database = get_actor("postgres_database")
messages = await postgres_database.list_chat_messages(chat_id=chat_id)
return {"messages": messages}
@@ -155,10 +155,10 @@ async def send_chat_message(
request: SendMessageRequest,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
# 先取历史(不含当前输入),再写入用户消息,避免历史里出现重复
message_history = await _load_message_history(chat_id)
await postgres_database.add_chat_message.remote(
await postgres_database.add_chat_message(
chat_id=chat_id, message=request.message, message_owner="user"
)
@@ -172,7 +172,7 @@ async def send_chat_message(
# 存回复
if response_msg:
await postgres_database.add_chat_message.remote(
await postgres_database.add_chat_message(
chat_id=chat_id, message=response_msg, message_owner="regulatory_node"
)
@@ -184,13 +184,13 @@ async def delete_chat_session(
chat_id: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
session = await postgres_database.get_chat_session.remote(chat_id=chat_id)
postgres_database = get_actor("postgres_database")
session = await postgres_database.get_chat_session(chat_id=chat_id)
if not session:
raise HTTPException(status_code=404, detail="Chat session not found")
if session.user_id != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
await postgres_database.delete_chat_session.remote(chat_id=chat_id)
await postgres_database.delete_chat_session(chat_id=chat_id)
return {"message": "success"}
@@ -201,14 +201,12 @@ async def stream_chat_message(
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""SSE 流式聊天端点:standalone 模式下逐 token 流式输出;distributed 模式 fallback 到整段回复。"""
from kilostar.utils.ray_compat import _STANDALONE
postgres_database = ray_actor_hook("postgres_database").postgres_database
"""SSE 流式聊天端点:逐 token 流式输出。"""
postgres_database = get_actor("postgres_database")
message_history = await _load_message_history(chat_id)
await postgres_database.add_chat_message.remote(
await postgres_database.add_chat_message(
chat_id=chat_id, message=request_body.message, message_owner="user"
)
@@ -219,23 +217,12 @@ async def stream_chat_message(
message=request_body.message,
)
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
if not _STANDALONE:
async def fallback_generator():
resp = await regulatory_node.working.remote(payload, message_history)
full_response = resp.reply_message if resp else ""
if full_response:
await postgres_database.add_chat_message.remote(
chat_id=chat_id, message=full_response, message_owner="regulatory_node"
)
yield f"data: {json.dumps({'token': full_response})}\n\n"
yield f"data: {json.dumps({'done': True, 'full_message': full_response})}\n\n"
return StreamingResponse(fallback_generator(), media_type="text/event-stream")
regulatory_node = get_actor("regulatory_node")
token_queue = asyncio.Queue()
stream_task = regulatory_node.stream_working.remote(payload, token_queue, message_history)
stream_task = asyncio.create_task(
regulatory_node.stream_working(payload, token_queue, message_history)
)
async def event_generator():
full_response = ""
@@ -260,7 +247,7 @@ async def stream_chat_message(
yield f"data: {json.dumps({'token': full_response})}\n\n"
if full_response:
await postgres_database.add_chat_message.remote(
await postgres_database.add_chat_message(
chat_id=chat_id,
message=full_response,
message_owner="regulatory_node",
+3 -3
View File
@@ -15,7 +15,7 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from kilostar.utils.access import Accessor, TokenData
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from kilostar.core.individual.regulatory_node.template import (
MessageRequest,
MessageResponse,
@@ -42,14 +42,14 @@ async def create_message(
"""把前端消息转交给 RegulatoryNode 处理,并把回复透传给前端。"""
logger.info("收到消息,来源:客户端")
logger.debug(f"消息内容:{message.message}")
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
regulatory_node = get_actor("regulatory_node")
msg_request = MessageRequest(
platform="client",
user_name=token_data.username,
platform_id=token_data.user_id,
message=message.message,
)
result = await regulatory_node.working.remote(msg_request)
result = await regulatory_node.working(msg_request)
if isinstance(result, MessageResponse):
return {"message": result.reply_message}
if isinstance(result, str):
+3 -3
View File
@@ -39,7 +39,7 @@ from kilostar.core.individual.regulatory_node.template import (
MessageResponse,
)
from kilostar.utils.logger import get_logger
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
logger = get_logger("onebot")
@@ -129,8 +129,8 @@ async def _dispatch_event(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
)
try:
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
result = await regulatory_node.working.remote(msg_request)
regulatory_node = get_actor("regulatory_node")
result = await regulatory_node.working(msg_request)
except Exception as e:
logger.exception(f"[OneBot] RegulatoryNode 调用失败: {e}")
return None
+21 -23
View File
@@ -8,7 +8,7 @@ from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from kilostar.utils.access import Accessor, TokenData
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from kilostar.utils.settings import get_plugin_dir
plugin_router = APIRouter(prefix="/api/v1/plugin", tags=["plugin"])
@@ -25,15 +25,16 @@ async def submit_task(
req: SubmitRequest,
token_data: TokenData = Depends(Accessor.get_current_user),
):
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
plugins = await pm.list_plugins.remote()
if req.org_name not in plugins:
pm = get_actor("global_plugin_manager")
plugins = await pm.list_plugins()
# list_plugins 返回 [{"name": ..., ...}],按 name 判断插件是否已加载
if req.org_name not in {p["name"] for p in plugins}:
raise HTTPException(404, f"Plugin '{req.org_name}' not found")
org = ray_actor_hook(f"org_{req.org_name}").get(f"org_{req.org_name}")
org = get_actor(f"org_{req.org_name}")
ctx = req.context or {}
ctx["user"] = token_data.username
task_id = await org.submit.remote(req.task_description, ctx)
task_id = await org.submit(req.task_description, ctx)
return {"task_id": task_id}
@@ -42,8 +43,8 @@ async def get_task_status(
task_id: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
db = ray_actor_hook("postgres_database").postgres_database
task = await db.get_org_task.remote(task_id)
db = get_actor("postgres_database")
task = await db.get_org_task(task_id)
if not task:
raise HTTPException(404, "Task not found")
return task
@@ -54,8 +55,8 @@ async def get_task_events(
task_id: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
db = ray_actor_hook("postgres_database").postgres_database
events = await db.query_org_events.remote(task_id)
db = get_actor("postgres_database")
events = await db.query_org_events(task_id)
return {"events": events}
@@ -64,19 +65,16 @@ async def stream_task(
task_id: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
import asyncio
org_name = None
db = ray_actor_hook("postgres_database").postgres_database
task = await db.get_org_task.remote(task_id)
db = get_actor("postgres_database")
task = await db.get_org_task(task_id)
if not task:
raise HTTPException(404, "Task not found")
org_name = task["org_name"]
org = ray_actor_hook(f"org_{org_name}").get(f"org_{org_name}")
org = get_actor(f"org_{org_name}")
async def _generate():
async for event in await org.stream.remote(task_id):
async for event in await org.stream(task_id):
yield f"data: {event}\n\n"
return StreamingResponse(_generate(), media_type="text/event-stream")
@@ -86,8 +84,8 @@ async def stream_task(
async def list_plugins(
token_data: TokenData = Depends(Accessor.get_current_user),
):
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
plugins = await pm.list_plugins.remote()
pm = get_actor("global_plugin_manager")
plugins = await pm.list_plugins()
return {"plugins": plugins}
@@ -96,8 +94,8 @@ async def install_plugin(
name: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
await pm.install.remote(name)
pm = get_actor("global_plugin_manager")
await pm.install(name)
return {"status": "ok", "name": name}
@@ -106,8 +104,8 @@ async def reload_plugin(
name: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
await pm.reload.remote(name)
pm = get_actor("global_plugin_manager")
await pm.reload(name)
return {"status": "ok", "name": name}
+7 -7
View File
@@ -18,7 +18,7 @@ from typing import Any, Dict, Literal, Optional
from kilostar.utils.access import TokenData, Accessor, RoleChecker
from kilostar.core.postgres_database.model import UserAuthority
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
@@ -39,8 +39,8 @@ async def create_provider(
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
) -> None:
"""注册一个 Provider;owner 为当前登录用户的 ``user_id``。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.add_provider_wrap.remote(
global_state_machine = get_actor("global_state_machine")
await global_state_machine.add_provider_wrap(
provider_type=provider_register.provider_type,
provider_title=provider_register.provider_title,
provider_url=provider_register.provider_url,
@@ -61,10 +61,10 @@ async def get_provider_list(
_: TokenData = Depends(Accessor.get_current_user),
) -> Dict[str, Any]:
"""返回当前所有已注册的 Provider,前端用以展示模型清单。apikey 脱敏。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
global_state_machine = get_actor("global_state_machine")
provider_list: Dict[
str, Provider
] = await global_state_machine.get_provider_list.remote()
] = await global_state_machine.get_provider_list()
masked = {}
for title, p in provider_list.items():
d = p.model_dump() if hasattr(p, "model_dump") else dict(p)
@@ -138,6 +138,6 @@ async def delete_provider(
),
) -> dict:
"""删除指定 ``provider_title`` 的 Provider;仅超管可调用。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.delete_provider.remote(provider_title=provider_title)
global_state_machine = get_actor("global_state_machine")
await global_state_machine.delete_provider(provider_title=provider_title)
return {"message": "success"}
+42 -42
View File
@@ -15,7 +15,7 @@
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
import viceroy
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from kilostar.utils.access import TokenData, RoleChecker, Accessor
@@ -50,7 +50,7 @@ async def install_skill(
skill: Skill, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))
):
"""通过 viceroy 把 skill 仓库克隆到 ``data/plugin/skill``,并在状态机中登记。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
global_state_machine = get_actor("global_state_machine")
import os
from kilostar.utils.settings import get_plugin_dir
@@ -63,7 +63,7 @@ async def install_skill(
skill_name = skill.path.split("/")[-1]
else:
skill_name = skill.repo_url.split("/")[-1]
await global_state_machine.add_skill.remote(skill_name)
await global_state_machine.add_skill(skill_name)
return {"message": "创建成功"}
@@ -72,8 +72,8 @@ async def get_skills(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""返回当前状态机中已登记的所有 skill 名称列表。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
skills = await global_state_machine.get_skill_list.remote()
global_state_machine = get_actor("global_state_machine")
skills = await global_state_machine.get_skill_list()
return {"skills": skills}
@@ -85,8 +85,8 @@ async def delete_skill(
),
):
"""从状态机中移除 skill 注册项;不会删除磁盘上的代码文件。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.remove_skill.remote(skill_name)
global_state_machine = get_actor("global_state_machine")
await global_state_machine.remove_skill(skill_name)
return {"message": "success"}
@@ -98,12 +98,12 @@ async def add_mcp_server(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""注册一个 MCP 服务器到全局状态机。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
global_state_machine = get_actor("global_state_machine")
import uuid
server_id = str(uuid.uuid4())[:8]
cfg_dict = config.model_dump(exclude_none=True)
await global_state_machine.add_mcp_server.remote(server_id, cfg_dict)
await global_state_machine.add_mcp_server(server_id, cfg_dict)
return {"server_id": server_id, "message": "MCP server registered"}
@@ -112,8 +112,8 @@ async def list_mcp_servers(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""返回已注册的全部 MCP 服务器配置;env 中的敏感字段脱敏。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
servers = await global_state_machine.list_mcp_servers.remote()
global_state_machine = get_actor("global_state_machine")
servers = await global_state_machine.list_mcp_servers()
for s in servers:
if "env" in s and isinstance(s["env"], dict):
s["env"] = _mask_config(s["env"])
@@ -126,8 +126,8 @@ async def delete_mcp_server(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""从状态机中移除一个 MCP 服务器配置。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
ok = await global_state_machine.delete_mcp_server.remote(server_id)
global_state_machine = get_actor("global_state_machine")
ok = await global_state_machine.delete_mcp_server(server_id)
if not ok:
raise HTTPException(status_code=404, detail="MCP server not found")
return {"message": "success"}
@@ -152,8 +152,8 @@ async def download_artifact(
if not artifact_id.isalnum() or len(artifact_id) > 32:
raise HTTPException(status_code=400, detail="invalid artifact id")
postgres_database = ray_actor_hook("postgres_database").postgres_database
wf = await postgres_database.get_workflow.remote(trace_id)
postgres_database = get_actor("postgres_database")
wf = await postgres_database.get_workflow(trace_id)
if not wf:
raise HTTPException(status_code=404, detail="Workflow not found")
if getattr(wf, "user_id", None) != token_data.user_id:
@@ -187,8 +187,8 @@ async def list_toolset_packages(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""列出所有磁盘上的工具包(``data/toolset/<name>/`` 单元)。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
packages = await global_state_machine.list_toolset_packages.remote()
global_state_machine = get_actor("global_state_machine")
packages = await global_state_machine.list_toolset_packages()
return {"packages": packages}
@@ -198,8 +198,8 @@ async def get_toolset_package_readme(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""返回指定工具包的 README.md 内容(markdown 文本)。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
content = await global_state_machine.get_toolset_package_readme.remote(name)
global_state_machine = get_actor("global_state_machine")
content = await global_state_machine.get_toolset_package_readme(name)
if content is None:
raise HTTPException(status_code=404, detail="README not found")
return {"name": name, "content": content}
@@ -216,9 +216,9 @@ async def get_tools(
其中 ``mcp_servers`` 会现场尝试连接每个已注册的 MCP 服务器并列出它们暴露的
工具名,便于前端展示;任意一台 MCP server 不可达不影响其他工具的返回。
"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
tool_mapper = await global_state_machine.get_tool_mapper.remote()
categories = await global_state_machine.get_tool_categories.remote()
global_state_machine = get_actor("global_state_machine")
tool_mapper = await global_state_machine.get_tool_mapper()
categories = await global_state_machine.get_tool_categories()
all_tool_names = set()
for scope_tools in tool_mapper.values():
@@ -266,8 +266,8 @@ async def list_tool_configs(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""列出所有工具运行期配置;敏感字段会被脱敏。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
raw = await global_state_machine.list_tool_configs.remote()
global_state_machine = get_actor("global_state_machine")
raw = await global_state_machine.list_tool_configs()
return {
"configs": {name: _mask_config(cfg) for name, cfg in raw.items()},
}
@@ -279,8 +279,8 @@ async def get_tool_config(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""按工具名取出脱敏后的配置。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
raw = await global_state_machine.get_tool_config.remote(tool_name)
global_state_machine = get_actor("global_state_machine")
raw = await global_state_machine.get_tool_config(tool_name)
return {"tool_name": tool_name, "config": _mask_config(raw)}
@@ -291,8 +291,8 @@ async def set_tool_config(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""写入/覆盖某工具的运行期配置(如 ``tavily_search`` 的 ``api_key``)。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.set_tool_config.remote(tool_name, body.config)
global_state_machine = get_actor("global_state_machine")
await global_state_machine.set_tool_config(tool_name, body.config)
return {"message": "success"}
@@ -302,8 +302,8 @@ async def delete_tool_config(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""删除某工具的运行期配置。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
ok = await global_state_machine.delete_tool_config.remote(tool_name)
global_state_machine = get_actor("global_state_machine")
ok = await global_state_machine.delete_tool_config(tool_name)
if not ok:
raise HTTPException(status_code=404, detail="Tool config not found")
return {"message": "success"}
@@ -343,12 +343,12 @@ async def create_custom_toolset(
body: CustomToolsetCreate,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
global_state_machine = get_actor("global_state_machine")
import uuid
toolset_id = str(uuid.uuid4())[:8]
try:
saved = await global_state_machine.add_custom_toolset.remote(
saved = await global_state_machine.add_custom_toolset(
toolset_id=toolset_id,
name=body.name,
tools=body.tools,
@@ -368,8 +368,8 @@ async def list_custom_toolsets(
"""列出工具组:支持按 category 过滤。USER 只能看到自己的+系统的;ADMIN 看全部。"""
from kilostar.utils.access import get_authority
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
toolsets = await global_state_machine.list_custom_toolsets.remote()
global_state_machine = get_actor("global_state_machine")
toolsets = await global_state_machine.list_custom_toolsets()
authority = await get_authority(token_data.user_id)
if authority < UserAuthority.ADMINISTRATOR:
toolsets = [
@@ -386,8 +386,8 @@ async def get_custom_toolset(
toolset_id: str,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
ts = await global_state_machine.get_custom_toolset.remote(toolset_id)
global_state_machine = get_actor("global_state_machine")
ts = await global_state_machine.get_custom_toolset(toolset_id)
if not ts:
raise HTTPException(status_code=404, detail="Custom toolset not found")
await _assert_toolset_owner_or_admin(ts, token_data)
@@ -400,8 +400,8 @@ async def update_custom_toolset(
body: CustomToolsetUpdate,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
existing = await global_state_machine.get_custom_toolset.remote(toolset_id)
global_state_machine = get_actor("global_state_machine")
existing = await global_state_machine.get_custom_toolset(toolset_id)
if not existing:
raise HTTPException(status_code=404, detail="Custom toolset not found")
if existing.get("is_system"):
@@ -411,7 +411,7 @@ async def update_custom_toolset(
tools = body.tools if body.tools is not None else existing["tools"]
description = body.description if body.description is not None else existing.get("description")
try:
saved = await global_state_machine.add_custom_toolset.remote(
saved = await global_state_machine.add_custom_toolset(
toolset_id=toolset_id,
name=name,
tools=tools,
@@ -429,14 +429,14 @@ async def delete_custom_toolset(
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""删除工具组:系统预置不可删;USER 只能删自己的;ADMIN 及以上可删任意用户的。"""
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
existing = await global_state_machine.get_custom_toolset.remote(toolset_id)
global_state_machine = get_actor("global_state_machine")
existing = await global_state_machine.get_custom_toolset(toolset_id)
if not existing:
raise HTTPException(status_code=404, detail="Custom toolset not found")
if existing.get("is_system"):
raise HTTPException(status_code=403, detail="系统预置工具集不可删除")
await _assert_toolset_owner_or_admin(existing, token_data)
ok = await global_state_machine.delete_custom_toolset.remote(toolset_id)
ok = await global_state_machine.delete_custom_toolset(toolset_id)
if not ok:
raise HTTPException(status_code=404, detail="Custom toolset not found")
return {"message": "success"}
+7 -7
View File
@@ -24,7 +24,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
from kilostar.utils.access import Accessor, TokenData, RoleChecker
from kilostar.core.postgres_database.model import UserAuthority
from kilostar.utils.config_loader import (
@@ -49,15 +49,15 @@ async def readiness():
checks = {"postgres": False, "global_state_machine": False}
try:
postgres_database = ray_actor_hook("postgres_database").postgres_database
await postgres_database.ping.remote()
postgres_database = get_actor("postgres_database")
await postgres_database.ping()
checks["postgres"] = True
except Exception:
pass
try:
gsm = ray_actor_hook("global_state_machine").global_state_machine
await gsm.get_skill_list.remote()
gsm = get_actor("global_state_machine")
await gsm.get_skill_list()
checks["global_state_machine"] = True
except Exception:
pass
@@ -95,8 +95,8 @@ async def query_system_logs(
offset: int = 0,
_: TokenData = Depends(Accessor.get_current_user),
):
pg = ray_actor_hook("postgres_database").postgres_database
logs = await pg.query_event_logs.remote(
pg = get_actor("postgres_database")
logs = await pg.query_event_logs(
trace_id=trace_id,
event_type=event_type,
level=level,
+5 -5
View File
@@ -17,7 +17,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from kilostar.utils.access import Accessor, TokenData
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
task_router = APIRouter(prefix="/api/v1/task", tags=["task"])
@@ -30,8 +30,8 @@ async def list_tasks(
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""列出当前用户的所有短任务,按时间倒序。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
tasks = await postgres_database.list_tasks_by_user.remote(
postgres_database = get_actor("postgres_database")
tasks = await postgres_database.list_tasks_by_user(
user_id=token_data.user_id,
status=status,
limit=limit,
@@ -46,8 +46,8 @@ async def get_task(
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""按 task_id 读取一条 task 详情。仅 owner 可访问。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
task = await postgres_database.get_task.remote(task_id)
postgres_database = get_actor("postgres_database")
task = await postgres_database.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="task not found")
if task.get("user_id") != token_data.user_id:
+33 -38
View File
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor, spawn_background
from fastapi import APIRouter, Request, HTTPException, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
@@ -34,22 +34,23 @@ async def create_workflow(
request: CreateWorkflowRequest,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
trace_id = str(ULID())
await postgres_database.create_workflow.remote(
await postgres_database.create_workflow(
trace_id=trace_id,
user_id=token_data.user_id,
title=request.title,
command=request.command,
)
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.create_trace.remote(trace_id)
global_workflow_manager = get_actor("global_workflow_manager")
await global_workflow_manager.create_trace(trace_id)
consciousness_node = ray_actor_hook("consciousness_node").consciousness_node
consciousness_node.start_workflow_design.remote(trace_id, request.command)
consciousness_node = get_actor("consciousness_node")
# fire-and-forget:设计过程可能跑很久,不能阻塞 HTTP 响应
spawn_background(
consciousness_node.start_workflow_design(trace_id, request.command)
)
return {"trace_id": trace_id, "status": "creating"}
@@ -57,8 +58,8 @@ async def create_workflow(
@workflow_router.get("/list")
async def get_workflow_list(
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
postgres_database = ray_actor_hook("postgres_database").postgres_database
workflows = await postgres_database.list_workflows.remote(
postgres_database = get_actor("postgres_database")
workflows = await postgres_database.list_workflows(
user_id=token_data.user_id
)
return {"workflows": workflows}
@@ -75,23 +76,21 @@ async def get_workflow_sse(
鉴权走标准 ``Authorization: Bearer`` 头(前端用 fetch-based SSE,
token 不进 URL)。校验该 trace_id 属于当前用户。
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
wf = await postgres_database.get_workflow.remote(trace_id)
postgres_database = get_actor("postgres_database")
wf = await postgres_database.get_workflow(trace_id)
if not wf:
raise HTTPException(status_code=404, detail="Workflow not found")
if getattr(wf, "user_id", None) != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
global_workflow_manager = get_actor("global_workflow_manager")
async def event_generator():
try:
while True:
if await request.is_disconnected():
break
message = await global_workflow_manager.get_pending.remote(trace_id)
message = await global_workflow_manager.get_pending(trace_id)
if message:
yield f"data: {message}\n\n"
else:
@@ -108,8 +107,8 @@ async def post_workflow_reply(
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
wf = await postgres_database.get_workflow.remote(trace_id)
postgres_database = get_actor("postgres_database")
wf = await postgres_database.get_workflow(trace_id)
if not wf:
raise HTTPException(status_code=404, detail="Workflow not found")
if getattr(wf, "user_id", None) != token_data.user_id:
@@ -117,10 +116,8 @@ async def post_workflow_reply(
data = await request.json()
reply_msg = data.get("message", "")
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.put_received.remote(trace_id, reply_msg)
global_workflow_manager = get_actor("global_workflow_manager")
await global_workflow_manager.put_received(trace_id, reply_msg)
return {"status": "ok"}
@@ -128,14 +125,14 @@ async def post_workflow_reply(
async def get_workflow_detail(
trace_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
wf = await postgres_database.get_workflow.remote(trace_id)
postgres_database = get_actor("postgres_database")
wf = await postgres_database.get_workflow(trace_id)
if not wf:
raise HTTPException(status_code=404, detail="Workflow not found")
if getattr(wf, "user_id", None) != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
context = await postgres_database.get_workflow_context.remote(trace_id)
context = await postgres_database.get_workflow_context(trace_id)
work_link = (
context.work_link if context and hasattr(context, "work_link") else []
@@ -209,32 +206,30 @@ async def resume_workflow(
):
"""从 ``workflow_graph_state`` 持久化恢复一个被中断/挂起的工作流。
新 fire 一个 ray task,task 入口的 ``hydrate`` 检查会自动走 resume 路径
新 fire 一个后台任务,入口的 ``hydrate`` 检查会自动走 resume 路径
把剩余节点跑完。
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
wf = await postgres_database.get_workflow.remote(trace_id)
postgres_database = get_actor("postgres_database")
wf = await postgres_database.get_workflow(trace_id)
if not wf:
raise HTTPException(status_code=404, detail="Workflow not found")
if getattr(wf, "user_id", None) != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
record = await postgres_database.get_workflow_graph_state.remote(trace_id)
record = await postgres_database.get_workflow_graph_state(trace_id)
if record is None:
raise HTTPException(
status_code=409, detail="该工作流没有可恢复的图持久化记录"
)
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.create_trace.remote(trace_id)
global_workflow_manager = get_actor("global_workflow_manager")
await global_workflow_manager.create_trace(trace_id)
from kilostar.core.work.workflow.workflow_engine import run_workflow_task
# resume_only=True:task 入口 hydrate 失败会 fail-fast,绝不 fall through
# 到"全新模式空跑"。workflow_data 在 resume 路径上不会被使用,传空 dict 占位。
run_workflow_task.remote({}, trace_id, resume_only=True)
spawn_background(run_workflow_task({}, trace_id, resume_only=True))
return {"trace_id": trace_id, "status": "resuming"}
@@ -251,15 +246,15 @@ async def get_workflow_graph_mermaid(
"""
from kilostar.core.work.workflow.workflow_engine import workflow_graph
postgres_database = ray_actor_hook("postgres_database").postgres_database
wf = await postgres_database.get_workflow.remote(trace_id)
postgres_database = get_actor("postgres_database")
wf = await postgres_database.get_workflow(trace_id)
if not wf:
raise HTTPException(status_code=404, detail="Workflow not found")
if getattr(wf, "user_id", None) != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
visited: list[str] = []
record = await postgres_database.get_workflow_graph_state.remote(trace_id)
record = await postgres_database.get_workflow_graph_state(trace_id)
if record is not None:
history = getattr(record, "history", None) or []
# history 里每条 NodeSnapshot.id 形如 "ClassName:hash",截前缀作为 NodeIdent
@@ -13,10 +13,6 @@
# limitations under the License.
from typing import Any, Dict, List, Optional, Tuple
from kilostar.utils.ray_compat import actor_class, _STANDALONE
if not _STANDALONE:
import ray
from kilostar.core.global_state_machine.individual_manager import (
GlobalIndividualManager,
@@ -28,7 +24,6 @@ from kilostar.core.global_state_machine.gsm_snapshot import GSMSnapshot
from kilostar.core.postgres_database import PostgresDatabase
@actor_class
class GlobalStateMachine:
"""全局状态机 Actor,统一持有 Provider/Tool/Skill/Individual/MCP/CustomToolset 注册表。
@@ -62,13 +57,13 @@ class GlobalStateMachine:
self.postgres_database
)
# MCP servers
rows = await self.postgres_database.list_mcp_servers_db.remote()
rows = await self.postgres_database.list_mcp_servers_db()
self._mcp_servers = {row["server_id"]: row for row in rows}
# Tool configs
cfg_rows = await self.postgres_database.list_tool_configs_db.remote()
cfg_rows = await self.postgres_database.list_tool_configs_db()
self._tool_configs = {row["tool_name"]: row["config"] for row in cfg_rows}
# Custom toolsets(含系统预置)
ts_rows = await self.postgres_database.list_custom_toolsets.remote()
ts_rows = await self.postgres_database.list_custom_toolsets()
self._custom_toolsets = {row["toolset_id"]: row for row in ts_rows}
# 补种系统预置工具集(首次启动或 DB 被 create_all 重建后)
await self._seed_system_toolsets()
@@ -96,12 +91,12 @@ class GlobalStateMachine:
continue
if tid in wanted_ids:
continue
await self.postgres_database.delete_custom_toolset.remote(tid)
await self.postgres_database.delete_custom_toolset(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(
saved = await self.postgres_database.upsert_custom_toolset(
toolset_id=tid,
name=pkg.get("display_name") or name,
tools=list(pkg.get("tools", [])),
@@ -155,18 +150,10 @@ class GlobalStateMachine:
def _publish_snapshot(self) -> None:
"""版本号 +1 并发布当前状态快照。"""
self._config_version += 1
snapshot = self._build_snapshot()
if _STANDALONE:
self._current_ref = snapshot
else:
self._current_ref = ray.put(snapshot)
self._current_ref = self._build_snapshot()
async def current_config_ref(self) -> Tuple[int, Any]:
"""返回 ``(version, ObjectRef 或 snapshot)``。
分布式模式返回 ObjectRef,调用方用 ``ray.get`` 自取;
单机模式直接返回 snapshot 对象。
"""
"""返回 ``(version, snapshot)``。进程内直接持有 snapshot 对象。"""
if self._current_ref is None:
self._publish_snapshot()
return self._config_version, self._current_ref
@@ -199,11 +186,11 @@ class GlobalStateMachine:
self._publish_snapshot()
return result
def get_provider_list(self):
async def get_provider_list(self):
"""返回内存中已登记的全部 Provider。"""
return self._global_provider_manager.get_provider_list()
def get_provider(self, provider_title):
async def get_provider(self, provider_title):
"""按 provider_title 取出单个 Provider 实例。"""
return self._global_provider_manager.get_provider(provider_title)
@@ -217,17 +204,17 @@ class GlobalStateMachine:
# ─── Tool / Toolset ────────────────────────────────────────
def get_tool_mapper(self):
async def get_tool_mapper(self):
"""返回 agent_name -> {tool_name: callable} 的全量映射。"""
return self._global_tool_manager.tool_mapper
def get_tool_list(self, agent_name: str):
async def get_tool_list(self, agent_name: str):
"""返回某个 agent 可用的工具集(其专属工具与 default 工具的并集)。"""
tools = self._global_tool_manager.tool_mapper.get(agent_name, {})
default_tools = self._global_tool_manager.tool_mapper.get("default", {})
return {**default_tools, **tools}
def get_tool_categories(self):
async def get_tool_categories(self):
"""返回工具按分类聚合的完整信息。"""
return {
"system": self._global_tool_manager.get_system_tools(),
@@ -241,22 +228,22 @@ class GlobalStateMachine:
"all": self._global_tool_manager.get_all_tools(),
}
def get_toolsets_for_scope(self, scope: str) -> List[Any]:
async def get_toolsets_for_scope(self, scope: str) -> List[Any]:
"""返回某个 scope 下的"系统 + 自定义工具组"toolset 列表(不含 MCP)。"""
return self._global_tool_manager.get_toolsets_for_scope(scope)
def get_retrieval_toolsets_for_scope(self, scope: str) -> List[Any]:
async def get_retrieval_toolsets_for_scope(self, scope: str) -> List[Any]:
"""仅返回 retrieval 工具集(system_node 专用,不包含 generation 工具)。"""
return self._global_tool_manager.get_retrieval_toolsets_for_scope(scope)
def list_toolset_packages(self) -> List[Dict[str, Any]]:
async 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]:
async 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"):
@@ -271,26 +258,26 @@ class GlobalStateMachine:
async def add_mcp_server(self, server_id: str, config: Dict[str, Any]) -> bool:
"""注册一个 MCP 服务器配置(写库 → 写内存)。"""
saved = await self.postgres_database.upsert_mcp_server.remote(server_id, config)
saved = await self.postgres_database.upsert_mcp_server(server_id, config)
self._mcp_servers[server_id] = saved
self._publish_snapshot()
return True
def get_mcp_server(self, server_id: str) -> Optional[Dict[str, Any]]:
async def get_mcp_server(self, server_id: str) -> Optional[Dict[str, Any]]:
return self._mcp_servers.get(server_id)
def list_mcp_servers(self) -> List[Dict[str, Any]]:
async def list_mcp_servers(self) -> List[Dict[str, Any]]:
return [
{"server_id": sid, **cfg} for sid, cfg in self._mcp_servers.items()
]
async def delete_mcp_server(self, server_id: str) -> bool:
ok = await self.postgres_database.delete_mcp_server_db.remote(server_id)
ok = await self.postgres_database.delete_mcp_server_db(server_id)
self._mcp_servers.pop(server_id, None)
self._publish_snapshot()
return bool(ok)
def get_mcp_server_configs(self) -> Dict[str, Dict[str, Any]]:
async def get_mcp_server_configs(self) -> Dict[str, Dict[str, Any]]:
"""返回原始 MCP 服务器配置字典(供节点创建 toolsets 时使用)。"""
return dict(self._mcp_servers)
@@ -298,23 +285,23 @@ class GlobalStateMachine:
async def set_tool_config(self, tool_name: str, config: Dict[str, Any]) -> bool:
"""整体覆盖某工具的运行期配置(敏感字段在 DAO 内自动加密)。"""
saved = await self.postgres_database.upsert_tool_config.remote(
saved = await self.postgres_database.upsert_tool_config(
tool_name, config
)
self._tool_configs[tool_name] = saved["config"]
self._publish_snapshot()
return True
def get_tool_config(self, tool_name: str) -> Dict[str, Any]:
async def get_tool_config(self, tool_name: str) -> Dict[str, Any]:
"""按工具名取出配置;不存在则返回空字典。"""
return dict(self._tool_configs.get(tool_name, {}))
def list_tool_configs(self) -> Dict[str, Dict[str, Any]]:
async def list_tool_configs(self) -> Dict[str, Dict[str, Any]]:
"""返回全部已配置工具的配置(包含敏感字段,调用方需自行脱敏)。"""
return dict(self._tool_configs)
async def delete_tool_config(self, tool_name: str) -> bool:
ok = await self.postgres_database.delete_tool_config_db.remote(tool_name)
ok = await self.postgres_database.delete_tool_config_db(tool_name)
self._tool_configs.pop(tool_name, None)
self._publish_snapshot()
return bool(ok)
@@ -345,7 +332,7 @@ class GlobalStateMachine:
raise ValueError(
f"用户工具集只允许包含第三方工具,以下不合法:{invalid}"
)
saved = await self.postgres_database.upsert_custom_toolset.remote(
saved = await self.postgres_database.upsert_custom_toolset(
toolset_id=toolset_id,
name=name,
tools=list(tools),
@@ -359,14 +346,14 @@ class GlobalStateMachine:
self._publish_snapshot()
return saved
def list_custom_toolsets(self) -> List[Dict[str, Any]]:
async def list_custom_toolsets(self) -> List[Dict[str, Any]]:
return list(self._custom_toolsets.values())
def get_custom_toolset(self, toolset_id: str) -> Optional[Dict[str, Any]]:
async def get_custom_toolset(self, toolset_id: str) -> Optional[Dict[str, Any]]:
return self._custom_toolsets.get(toolset_id)
async def delete_custom_toolset(self, toolset_id: str) -> bool:
ok = await self.postgres_database.delete_custom_toolset.remote(toolset_id)
ok = await self.postgres_database.delete_custom_toolset(toolset_id)
self._custom_toolsets.pop(toolset_id, None)
self._global_tool_manager.rebuild_custom_toolsets(self._custom_toolsets)
self._publish_snapshot()
@@ -374,33 +361,33 @@ class GlobalStateMachine:
# ─── Skill ────────────────────────────────────────────────
def add_skill(self, skill_name: str):
async def add_skill(self, skill_name: str):
result = self._global_skill_manager.add_skill(skill_name)
self._publish_snapshot()
return result
def get_skill_list(self):
async def get_skill_list(self):
return self._global_skill_manager.get_skill_list()
def remove_skill(self, skill_name: str):
async def remove_skill(self, skill_name: str):
result = self._global_skill_manager.remove_skill(skill_name)
self._publish_snapshot()
return result
# ─── Individual ───────────────────────────────────────────
def add_individual(self, agent_id: str, config):
async def add_individual(self, agent_id: str, config):
result = self._global_individual_manager.add_individual(agent_id, config)
self._publish_snapshot()
return result
def get_individual(self, agent_id: str):
async def get_individual(self, agent_id: str):
return self._global_individual_manager.get_individual(agent_id)
def remove_individual(self, agent_id: str):
async def remove_individual(self, agent_id: str):
result = self._global_individual_manager.remove_individual(agent_id)
self._publish_snapshot()
return result
def list_individuals(self):
async def list_individuals(self):
return self._global_individual_manager.list_individuals()
@@ -14,14 +14,14 @@
"""GSM 快照对象与客户端拉取工具。
设计动机:把 GSM actor 内存里的"读路径配置"打包成可放进 Ray Object Store
的不可变快照。读端不再走 actor RPC,而是 ``fetch_snapshot()`` 一次拿到全量
当前配置,亚毫秒级共享内存读,绕开单 actor 的吞吐瓶颈。
设计动机:把 GSM 内存里的"读路径配置"打包成一个不可变快照对象。读端不逐字段
走 GSM 方法,而是 ``fetch_snapshot()`` 一次拿到全量当前配置,进程内直接引用,
配合版本号做进程内缓存失效。
GSM 仍然是 source of truth + 写入串行化器,但读路径解耦:
- 写入 → GSM 内存更新 → ``ray.put(snapshot)`` 拿到新 ObjectRef → 版本号 +1
- 读取 → ``current_config_ref()`` 拿 (version, ref) → ``ray.get(ref)`` 直读
- 写入 → GSM 内存更新 → 重建 snapshot 对象 → 版本号 +1
- 读取 → ``current_version()`` 校验缓存新鲜度 → ``current_config_ref()`` 拿快照
旧的 ``get_provider / get_individual / ...`` 接口保留不动,是低频路径的兜底;
新代码(特别是 skill task 这种高并发热路径)应优先走 ``fetch_snapshot``。
@@ -29,15 +29,9 @@ GSM 仍然是 source of truth + 写入串行化器,但读路径解耦:
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Tuple
from kilostar.utils.ray_compat import _STANDALONE
if not _STANDALONE:
import ray
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
from kilostar.utils.logger import get_logger
@@ -73,7 +67,6 @@ class GSMSnapshot:
_local_cache: Dict[str, Any] = {"version": -1, "snapshot": None}
_cache_lock = asyncio.Lock()
async def fetch_snapshot(
@@ -83,54 +76,34 @@ async def fetch_snapshot(
) -> GSMSnapshot:
"""拉取当前 GSM 快照。
优先走"版本号检查 + ObjectRef 直读"路径:
单进程 asyncio 模型下,写入路径原子发布快照对象,读端不需要加锁:
1. 调 ``gsm.current_version.remote()`` 看本地缓存是否还新(一次轻量 RPC)
2. 若本地缓存版本号一致,直接返回缓存(亚毫秒,零网络)
3. 否则调 ``gsm.current_config_ref.remote()`` 拿 ref,``ray.get`` 解出
4. 更新本地缓存
Args:
use_cache: 默认开启进程内 LRU 缓存(实际是单槽位,持有当前版本);
测试或诊断场景可关掉强制重拉。
gsm_actor: 可选传入 GSM actor handle;省略时通过 ``ray_actor_hook`` 获取。
Note:
本函数在 task / actor 进程内多次调用是廉价的;建议每次需要 config 时
现取,不要把 snapshot 长期持有跨任务边界(避免 ObjectRef 阻碍回收)。
1. 先看本地缓存版本号是否与 ``current_version()`` 一致;
2. 一致则直接返回缓存对象(少一次全量数据传递);
3. 不一致则调 ``current_config_ref()`` 拿最新快照并更新缓存。
"""
if gsm_actor is None:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_gsm
gsm_actor = ray_actor_hook("global_state_machine").global_state_machine
gsm_actor = get_gsm()
if use_cache:
async with _cache_lock:
try:
latest_version = await gsm_actor.current_version.remote()
except Exception:
latest_version = None
try:
latest_version = await gsm_actor.current_version()
except Exception:
latest_version = None
if (
latest_version is not None
and _local_cache.get("version") == latest_version
and _local_cache.get("snapshot") is not None
):
return _local_cache["snapshot"]
if (
latest_version is not None
and _local_cache["version"] == latest_version
and _local_cache["snapshot"] is not None
):
return _local_cache["snapshot"]
version, ref_or_snapshot = await gsm_actor.current_config_ref.remote()
if _STANDALONE:
snapshot = ref_or_snapshot
else:
snapshot = ray.get(ref_or_snapshot)
_local_cache["version"] = version
_local_cache["snapshot"] = snapshot
return snapshot
version, ref_or_snapshot = await gsm_actor.current_config_ref.remote()
if _STANDALONE:
return ref_or_snapshot
return ray.get(ref_or_snapshot)
version, snapshot = await gsm_actor.current_config_ref()
_local_cache["version"] = version
_local_cache["snapshot"] = snapshot
return snapshot
def reset_local_cache() -> None:
@@ -32,7 +32,7 @@ class GlobalIndividualManager:
"""
try:
try:
individuals = await postgres.get_all_worker_individual.remote()
individuals = await postgres.get_all_worker_individual()
for ind in individuals:
agent_id = getattr(ind, "agent_id", None)
if agent_id:
@@ -46,7 +46,7 @@ class ProviderManager:
async def init_provider_register(self, postgres) -> None:
"""从 Postgres 读取已存的 Provider 列表,按 provider_title 装入内存注册表。"""
providers = await postgres.get_provider.remote()
providers = await postgres.get_provider()
for provider in providers:
self.provider_register[provider.provider_title] = provider
@@ -90,7 +90,7 @@ class ProviderManager:
provider: Provider = await provider_class.create_provider(provider_args)
provider.provider_owner = provider_owner
self.provider_register[provider_title] = provider
await postgres_database.add_provider_db.remote(
await postgres_database.add_provider_db(
provider_id=str(ulid.ULID()),
provider_title=provider.provider_title,
provider_url=provider.provider_url,
@@ -125,7 +125,7 @@ class ProviderManager:
async def delete_provider(self, provider_title: str, postgres_database) -> None:
"""从内存注册表 + Postgres 中一并删除指定 Provider;不存在时静默返回。"""
if provider_title in self.provider_register:
await postgres_database.delete_provider_by_title.remote(
await postgres_database.delete_provider_by_title(
provider_title=provider_title
)
del self.provider_register[provider_title]
@@ -1,7 +1,5 @@
import asyncio
from typing import Dict
from kilostar.utils.ray_compat import actor_class
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.logger import get_logger
@@ -11,7 +9,6 @@ class TraceQueues:
self.receive: asyncio.Queue[str] = asyncio.Queue()
@actor_class
class GlobalWorkflowManager:
def __init__(self):
self._traces: Dict[str, TraceQueues] = {}
@@ -14,7 +14,6 @@
from typing import Union, overload
from kilostar.utils.ray_compat import actor_class
from kilostar.core.individual.consciousness_node.template import (
ConsciousnessNodeDeps,
ForregulatoryNode,
@@ -28,11 +27,9 @@ from pydantic_ai import Agent, RunContext
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
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.prompts import agent_prompt
@actor_class
class ConsciousnessNode:
def __init__(self) -> None:
from kilostar.utils.logger import get_logger
@@ -134,8 +131,10 @@ class ConsciousnessNode:
f"ConsciousnessNode: 开始为 trace_id {trace_id} 设计工作流。原始命令:{command}"
)
# 获取可用技能 (示例)
postgres_database = ray_actor_hook("postgres_database").postgres_database
skills_entities = await postgres_database.get_all_worker_individual.remote()
from kilostar.utils.actor import get_postgres
postgres_database = get_postgres()
skills_entities = await postgres_database.get_all_worker_individual()
available_skills = []
if skills_entities:
for skill in skills_entities:
@@ -151,10 +150,10 @@ class ConsciousnessNode:
original_command=command, available_skills=available_skills
)
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.put_pending.remote(
from kilostar.utils.actor import get_gwm
global_workflow_manager = get_gwm()
await global_workflow_manager.put_pending(
trace_id, "正在为您构建并规划工作流任务节点,请稍候..."
)
@@ -164,20 +163,21 @@ class ConsciousnessNode:
workflow = result.workflow
workflow.trace_id = trace_id
await global_workflow_manager.put_pending.remote(
await global_workflow_manager.put_pending(
trace_id, "工作流构建完成,即将开始执行!"
)
# 直接以 ray task 形式 fire workflow,不再经过 WorkflowRunningEngine 这层中转:
# workflow 是一次性、有头有尾的执行,task 语义比常驻 actor 更贴。
# 以后台任务形式 fire workflow:workflow 是一次性、有头有尾的
# 执行,跑完即结束;持久化交给 PostgresStatePersistence 保证可恢复。
from kilostar.utils.actor import spawn_background
from kilostar.core.work.workflow.workflow_engine import run_workflow_task
run_workflow_task.remote(workflow.model_dump(), trace_id)
spawn_background(run_workflow_task(workflow.model_dump(), trace_id))
else:
await global_workflow_manager.put_pending.remote(
await global_workflow_manager.put_pending(
trace_id, "很抱歉,工作流生成失败。"
)
await postgres_database.update_workflow_status.remote(trace_id, "failed")
await postgres_database.update_workflow_status(trace_id, "failed")
async def working(
self,
@@ -14,24 +14,23 @@
from kilostar.core.work.workflow.workflow import KiloStarWorkflow, WorkflowStep
from kilostar.utils.agent_model import ResponseModel, DepsModel, RequestModel
from pydantic import Field
from pydantic import BaseModel, Field
from typing import Optional, List
# 意识节点回复类
class ConsciousnessNodeResponse(ResponseModel):
class ConsciousnessNodeResponse(BaseModel):
"""Consciousness response model,是意识节点所有回复类型的父类"""
pass
class ConsciousnessNodeDeps(DepsModel):
class ConsciousnessNodeDeps(BaseModel):
"""ConsciousnessNode 在 pydantic-ai Agent 中使用的依赖:原始指令、当前指令以及可用 Skill 列表。"""
original_command: str
command: str
available_skills: Optional[List[dict]] = None
locale: str = "zh"
class ConsciousnessNodeInput(RequestModel):
class ConsciousnessNodeInput(BaseModel):
"""ConsciousnessNode 各类入参的共同基类,仅用于打 schema 标签。"""
pass
@@ -1,17 +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 .control_node import ControlNode
__all__ = ["ControlNode"]
@@ -1,135 +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 pydantic_ai import Agent, RunContext
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
from kilostar.core.individual.control_node.template import (
ForWorkflow,
ForWorkflowInput,
ControlNodeDeps,
)
from kilostar.utils.prompts import agent_prompt
@actor_class
class ControlNode:
"""ControlNode(控制节点):**已废弃**——名字保留给未来的远程探针/系统控制节点。
历史:早期设计里它是工作流的"单步执行 actor",但 workflow_engine 的 Dispatch
最终只识别 ``consciousness_node`` 和 ``skill_individual``,本类从未真正被调用过。
保留目录与类壳子,避免改名带来的 git 历史断层;**不要新增对它的依赖**。
待远程探针/监控流子项目启动时,本目录将被重写为远程机器控制节点。
"""
def __init__(self):
from kilostar.utils.logger import get_logger
self.logger = get_logger("control_node")
self.agent: Agent | None = None
self._model_settings: dict = {}
async def create_agent(
self,
global_state_machine: GlobalStateMachine,
provider_title: str,
model_id: str,
tools_list: list[str] = None,
toolsets=None,
locale: str | None = None,
custom_system_prompt: str | None = None,
) -> None:
"""
create_agent方法,将agent对象装配到Control的属性内
该方法通过provider_title从global_state_machine中获取provider对象,然后从provider对象中取出供应商形象,装配为pydantic_ai的
Agent实例,
并挂载到self.agent属性
Args:
global_state_machine: 全局状态机
provider_title: 供应商名
model_id: 模型id
locale: 语言代码(zh/en),控制system prompt语言
custom_system_prompt: 管理员自定义追加提示词(可选)
Returns:
无返回
"""
system_prompt: str = agent_prompt("control_node", locale=locale, custom_system_prompt=custom_system_prompt)
output_type = ForWorkflow
from kilostar.utils.get_tool import load_tools_from_list
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
snapshot = await fetch_snapshot(gsm_actor=global_state_machine)
provider: Provider = snapshot.providers.get(provider_title)
if provider is None:
from kilostar.utils.i18n import t
raise ValueError(t("provider_not_registered", locale=locale, provider_title=provider_title))
agent_factory = AgentFactory()
callables = load_tools_from_list(tools_list)
self.agent = agent_factory.create_agent(
provider=provider,
model_id=model_id,
output_type=output_type,
system_prompt=system_prompt,
deps_type=ControlNodeDeps,
agent_name="control_node",
tools=callables,
toolsets=toolsets,
)
self._model_settings = AgentFactory.resolve_model_settings(provider, model_id)
@self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]):
"""运行期动态拼接 system prompt:把当前 workflow_step 的关键字段塞进去。"""
prompt = system_prompt + "\n\n"
prompt += (
f"=== 当前任务步骤上下文 ===\n"
f"- 步骤名称 (Name): {ctx.deps.workflow_step.name}\n"
f"- 步骤目标/描述 (Description): {ctx.deps.workflow_step.desc}\n"
f"- 前置输入(input): {ctx.deps.workflow_step.inputs}\n"
)
return prompt
async def working(self, payload: ForWorkflowInput) -> str:
"""对外入口:执行一次步骤,吞掉异常并返回 ``None`` 以避免拖垮上游 Workflow。"""
try:
result: ForWorkflow = await self._run(payload)
return result
except Exception:
self.logger.exception("ControlNode在执行working时发生严重错误")
return None
async def _run(self, payload: ForWorkflowInput) -> ForWorkflow:
"""实际执行步骤:组装 ``ControlNodeDeps``、调用 Agent,最终把 ``ForWorkflow`` 输出取出。"""
try:
self.agent.retries = 3
deps = ControlNodeDeps(workflow_step=payload.workflow_step)
self.logger.debug(
f"ControlNode: 开始执行工作流节点 [{payload.workflow_step.name}] (原生重试开启)"
)
result = await self.agent.run(
f"请根据提供的 workflow_step 上下文,执行此步骤并输出结果。\n详细指令或附加数据:{payload.workflow_step.model_dump_json()}",
deps=deps,
model_settings=self._model_settings or None,
)
return result.output
except Exception as e:
self.logger.exception(
f"ControlNode 在执行步骤 [{payload.workflow_step.name}] 时最终失败: {str(e)}"
)
raise RuntimeError(f"ControlNode 执行步骤失败: {str(e)}") from e
@@ -1,51 +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 pydantic import Field
from kilostar.core.work.workflow.workflow import WorkflowStep
from kilostar.utils.agent_model import ResponseModel, RequestModel, DepsModel
class ControlNodeResponse(ResponseModel):
"""控制节点回复的基类。"""
pass
class ControlNodeInput(RequestModel):
"""控制节点输入的基类,承载一次调度所需的入参。"""
pass
class ControlNodeDeps(DepsModel):
"""控制节点运行期依赖,注入到 pydantic-ai Agent 的 RunContext。"""
workflow_step: WorkflowStep
# In the future, this can be dynamically populated with tools specific to the current task execution
class ForWorkflow(ControlNodeResponse):
"""控制节点执行单个工作流步骤的输出模型。"""
output: str = Field(
..., description="控制节点执行特定工作流步骤的结果。包含执行细节和输出数据。"
)
class ForWorkflowInput(ControlNodeInput):
"""控制节点针对工作流步骤的输入模型。"""
workflow_step: WorkflowStep
@@ -1,14 +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.
@@ -1,14 +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.
@@ -15,7 +15,6 @@
import asyncio
import datetime
from typing import Union
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
@@ -28,7 +27,6 @@ from pydantic_ai import RunContext, Agent
from kilostar.utils.prompts import agent_prompt
@actor_class
class RegulatoryNode:
"""RegulatoryNode(监管节点):用户请求的直接对话节点。
@@ -234,12 +232,12 @@ class RegulatoryNode:
return
try:
import uuid
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
task_id = uuid.uuid4().hex
chat_id = payload.platform_id if payload.platform == "client" else None
await postgres_database.create_task.remote(
await postgres_database.create_task(
task_id=task_id,
user_id=payload.user_name,
command=payload.message,
@@ -13,12 +13,10 @@
# limitations under the License.
from typing import Literal, Optional
from pydantic import Field
from kilostar.utils.agent_model import ResponseModel, DepsModel, RequestModel
from pydantic import BaseModel, Field
class RegulatoryNodeResponse(ResponseModel):
class RegulatoryNodeResponse(BaseModel):
"""
RegulatoryNodeResponse类
一切regulatory_node回复的父类
@@ -26,14 +24,14 @@ class RegulatoryNodeResponse(ResponseModel):
pass
class RegulatoryNodeRequest(RequestModel):
class RegulatoryNodeRequest(BaseModel):
"""
RegulatoryNodeRequest类
向regulatory请求的父类
"""
pass
class RegulatoryNodeDeps(DepsModel):
class RegulatoryNodeDeps(BaseModel):
"""
RegulatoryNodeDeps类
regulatory_node的依赖模型
@@ -44,7 +42,7 @@ class RegulatoryNodeDeps(DepsModel):
retry_count: int = 0
error_history: str = ""
class MessageRequest(RequestModel):
class MessageRequest(BaseModel):
"""
MessageRequest类
任何消息渠道向regulatory_node发送消息请求的模型
+1 -3
View File
@@ -15,7 +15,6 @@
import os
import asyncio
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
@@ -62,9 +61,8 @@ from .module.org_task import OrgTaskDatabase
from .module.task import TaskDatabase
@actor_class
class PostgresDatabase:
"""以 Ray Actor 形式暴露的统一数据库门面。
"""经 actor 运行时(``get_actor("postgres_database")``)暴露的统一数据库门面。
内部组合了 Auth / Provider / Individual / SystemNode / Workflow / ChatHistory
六个子库,所有方法在调用前都会等待 ``ready_event``,确保 ``init_db`` 完成后
@@ -169,16 +169,16 @@ def _from_json_bytes(blob: bytes) -> Any:
def build_postgres_persistence(trace_id: str) -> PostgresStatePersistence:
"""生产环境构造 PostgresStatePersistence:从 ray_actor_hook 取 postgres handle。"""
from kilostar.utils.ray_hook import ray_actor_hook
"""生产环境构造 PostgresStatePersistence:取进程内 postgres 服务。"""
from kilostar.utils.actor import get_postgres
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_postgres()
async def _write(tid: str, history: Any) -> None:
await postgres_database.upsert_workflow_graph_state.remote(tid, history)
await postgres_database.upsert_workflow_graph_state(tid, history)
async def _read(tid: str) -> Optional[Any]:
record = await postgres_database.get_workflow_graph_state.remote(tid)
record = await postgres_database.get_workflow_graph_state(tid)
if record is None:
return None
# ORM 模型 / dict / list 都兼容
+43 -51
View File
@@ -36,7 +36,6 @@ import datetime
from dataclasses import dataclass
from typing import Any, Awaitable, Callable, Dict, List, Optional
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
@@ -74,7 +73,7 @@ StepExecutor = Callable[
class WorkflowDeps:
"""节点运行期依赖:所有外部 IO 都从这里走,便于测试 mock。
每个字段都是一个 awaitable,签名贴近原 ``.remote()`` 调用。生产路径由
每个字段都是一个 awaitable,签名贴近底层 actor 方法。生产路径由
``build_default_deps`` 现场组装真实 actor handle 包装;单测可以传任意
``AsyncMock``。
@@ -419,8 +418,8 @@ async def _default_skill_executor(
async def _default_consciousness_executor(
step_data: Dict[str, Any], state: WorkflowGraphState
) -> tuple[str, bool]:
"""生产环境的 consciousness 派发器:远程调用 ConsciousnessNode.working。"""
from kilostar.utils.ray_hook import ray_actor_hook
"""生产环境的 consciousness 派发器:调用 ConsciousnessNode.working。"""
from kilostar.utils.actor import get_consciousness
from kilostar.core.individual.consciousness_node.template import (
ForWorkflow,
ForWorkflowInput,
@@ -428,12 +427,12 @@ async def _default_consciousness_executor(
)
from kilostar.core.work.workflow.workflow import WorkflowStep
consciousness_node = ray_actor_hook("consciousness_node").consciousness_node
consciousness_node = get_consciousness()
payload = ForWorkflowInput(
workflow_step=WorkflowStep.model_validate(step_data),
original_command=state.original_command,
)
result = await consciousness_node.working.remote(payload)
result = await consciousness_node.working(payload)
if isinstance(result, ForWorkflow):
return result.output, True
if isinstance(result, ForregulatoryNode):
@@ -444,32 +443,30 @@ async def _default_consciousness_executor(
def build_default_deps() -> WorkflowDeps:
"""生产环境构造 ``WorkflowDeps``:把 ray actor handle 包装成 awaitable。
"""生产环境构造 ``WorkflowDeps``:把进程内服务包装成 awaitable。
抽出来是为了让 ``run_workflow_task`` 入口和测试入口共享同一套包装逻辑。
"""
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_postgres, get_gwm
postgres_database = ray_actor_hook("postgres_database").postgres_database
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
postgres_database = get_postgres()
global_workflow_manager = get_gwm()
async def _upsert_workflow_context(trace_id: str, **kwargs: Any) -> Any:
return await postgres_database.upsert_workflow_context.remote(
return await postgres_database.upsert_workflow_context(
trace_id, **kwargs
)
async def _update_workflow_status(trace_id: str, status: str) -> Any:
return await postgres_database.update_workflow_status.remote(
return await postgres_database.update_workflow_status(
trace_id, status
)
async def _put_pending(trace_id: str, message: str) -> Any:
return await global_workflow_manager.put_pending.remote(trace_id, message)
return await global_workflow_manager.put_pending(trace_id, message)
async def _get_received(trace_id: str) -> str:
return await global_workflow_manager.get_received.remote(trace_id)
return await global_workflow_manager.get_received(trace_id)
return WorkflowDeps(
upsert_workflow_context=_upsert_workflow_context,
@@ -547,11 +544,13 @@ async def resume_workflow_graph(
return final_output
@remote_task
def run_workflow_task(
async def run_workflow_task(
workflow_data: dict, trace_id: str, resume_only: bool = False
):
"""workflow 的 ray task 入口:一次性执行,跑完即销毁。
) -> None:
"""workflow 的后台任务入口:一次性执行,跑完即结束。
调用方用 ``asyncio.create_task(run_workflow_task(...))`` 以 fire-and-forget
方式在当前事件循环里后台执行。
生产路径下持久化交给 ``PostgresStatePersistence`` —— 即便进程崩溃,再 fire
一次相同 ``trace_id`` 的任务(或调 ``/workflow/{trace_id}/resume``)即可
@@ -564,8 +563,7 @@ def run_workflow_task(
否则会拿着空 ``workflow_data`` 空跑一个 ``work_link=[]`` 的 workflow 并误判
为 COMPLETED(静默 bug)。
ray task 是新进程,contextvars 不会从 caller 传过来,所以入口先 bind 一次
``trace_id``,让节点内的日志自动带上它。
入口先 bind 一次 ``trace_id``,让节点内的日志自动带上它。
"""
from kilostar.utils.request_context import trace_id_scope
from kilostar.core.work.workflow.graph_persistence import (
@@ -575,35 +573,29 @@ def run_workflow_task(
_logger = get_logger("workflow_task")
async def _entry() -> None:
with trace_id_scope(trace_id):
persistence = build_postgres_persistence(trace_id)
persistence.set_graph_types(workflow_graph)
with trace_id_scope(trace_id):
persistence = build_postgres_persistence(trace_id)
persistence.set_graph_types(workflow_graph)
recovered = False
try:
recovered = await persistence.hydrate()
except Exception as e:
if resume_only:
_logger.error(f"resume 失败:无法 hydrate 图持久化记录: {e}")
raise
recovered = False
try:
recovered = await persistence.hydrate()
except Exception as e:
if resume_only:
_logger.error(f"resume 失败:无法 hydrate 图持久化记录: {e}")
raise
recovered = False
if resume_only and not recovered:
msg = (
f"resume 失败:trace {trace_id} 没有可恢复的图持久化记录,"
"拒绝以全新模式空跑"
)
_logger.error(msg)
raise RuntimeError(msg)
if resume_only and not recovered:
msg = (
f"resume 失败:trace {trace_id} 没有可恢复的图持久化记录,"
"拒绝以全新模式空跑"
)
_logger.error(msg)
raise RuntimeError(msg)
if recovered:
await resume_workflow_graph(trace_id, persistence=persistence)
else:
await run_workflow_graph(
workflow_data, trace_id, persistence=persistence
)
if _STANDALONE:
return _entry()
else:
asyncio.run(_entry())
if recovered:
await resume_workflow_graph(trace_id, persistence=persistence)
else:
await run_workflow_graph(
workflow_data, trace_id, persistence=persistence
)
+10 -10
View File
@@ -1,7 +1,7 @@
"""BaseOrganization:重型插件基类。
设计要点:
- 单机模式 = 普通 Python 对象,分布式 = ray actor(``@actor_class`` 装饰子类)
- 进程内普通 Python 对象,经 ``register_actor`` 登记后由 ``get_actor`` 寻址
- 内置 ``asyncio.Queue`` 输入队列 + 任务表
- 对外两条通道:``dispatch`` (阻塞) / ``submit`` (射后不管),底层都汇集到 ``_run_task``
- 子类只需覆写 ``setup`` / ``react`` 两个钩子;零代码插件由 ``agents.json`` 声明驱动
@@ -335,10 +335,10 @@ class BaseOrganization:
async def _persist_task(self, ts: TaskState) -> None:
"""把任务状态写到 PG。失败不阻塞执行。"""
try:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
pg = ray_actor_hook("postgres_database").postgres_database
await pg.upsert_org_task.remote(
pg = get_actor("postgres_database")
await pg.upsert_org_task(
task_id=ts.task_id,
org_name=ts.org_name,
trace_id=ts.trace_id,
@@ -354,10 +354,10 @@ class BaseOrganization:
async def _persist_event(self, ts: TaskState, ev: OrgEvent) -> None:
try:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
pg = ray_actor_hook("postgres_database").postgres_database
await pg.append_org_task_event.remote(
pg = get_actor("postgres_database")
await pg.append_org_task_event(
task_id=ts.task_id, event=ev.to_dict()
)
except Exception:
@@ -510,10 +510,10 @@ class BaseOrganization:
if adef.model and adef.model.provider_title and adef.model.model_id:
return adef.model.provider_title, adef.model.model_id
try:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
pg = ray_actor_hook("postgres_database").postgres_database
row = await pg.find_plugin_slot.remote(self.name, adef.name)
pg = get_actor("postgres_database")
row = await pg.find_plugin_slot(self.name, adef.name)
if row is None:
return "", ""
return getattr(row, "provider_title", "") or "", getattr(row, "model_id", "") or ""
+19 -22
View File
@@ -19,18 +19,16 @@ from kilostar.plugin_runtime.loader import (
from kilostar.plugin_runtime.manifest import OrgManifest
from kilostar.plugin_runtime.tool_bridge import make_dispatch_tool
from kilostar.utils.logger import get_logger
from kilostar.utils.ray_compat import _STANDALONE, actor_class
from kilostar.utils.ray_hook import register_standalone
from kilostar.utils.actor import register_actor
from kilostar.utils.settings import get_plugin_data_dir, get_plugin_dir
logger = get_logger("plugin_manager")
@actor_class
class GlobalPluginManager:
"""单机模式下是对象,分布式下是 ray actor。
"""插件运行时管理器(进程内单例)。
每个 loaded 组织保存其 manifest 和 actor handle(standalone=proxy,dist=ray handle)。
每个 loaded 组织保存其 manifest 和实例,并注册到进程内 actor 运行时。
"""
def __init__(self):
@@ -64,13 +62,18 @@ class GlobalPluginManager:
org_info = self._orgs.pop(name, None)
if org_info is None:
return {"name": name, "status": "not_found"}
# shutdown actor
# shutdown 实例并从 actor 运行时注销
try:
handle = org_info.get("handle")
if handle is not None:
await handle.shutdown.remote()
await handle.shutdown()
except Exception as e:
logger.warning(f"shutdown org_{name} failed: {e}")
actor_name = org_info.get("actor_name")
if actor_name:
from kilostar.utils.actor import unregister_actor
unregister_actor(actor_name)
# 移除 dispatch tool
self._dispatch_tools.pop(f"dispatch_to_{name}", None)
logger.info(f"uninstalled plugin: {name}")
@@ -136,15 +139,9 @@ class GlobalPluginManager:
await instance.setup()
# 注册到 ray_actor_hook 命名空间
# 注册到进程内 actor 运行时,业务侧通过 get_actor(actor_name) 取用
actor_name = manifest.actor_name
if _STANDALONE:
register_standalone(actor_name, instance)
else:
# 分布式模式下,这里需要把 instance 包装成 ray actor
# 第一版走 standalone 逻辑(两种模式统一 register 到本进程)
# 真正分布式隔离等后续做
register_standalone(actor_name, instance)
register_actor(actor_name, instance)
# 生成 dispatch tool
tool = make_dispatch_tool(name, manifest.display_name, manifest.description)
@@ -166,15 +163,15 @@ class GlobalPluginManager:
DB 不可用时静默跳过(standalone 启动早期 / 单测场景)。
"""
try:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
pg = ray_actor_hook("postgres_database").postgres_database
pg = get_actor("postgres_database")
for adef in agents_dict.get("agents", []):
slot_name = adef.get("name")
if not slot_name:
continue
description = adef.get("role") or adef.get("system_prompt") or slot_name
await pg.upsert_plugin_slot.remote(
await pg.upsert_plugin_slot(
plugin_name=name,
slot_name=slot_name,
description=description,
@@ -185,10 +182,10 @@ class GlobalPluginManager:
async def cleanup_orphan_plugin_slots(self) -> None:
"""启动期兜底:DB 中存在但目录已不在的 plugin_owned slot 全部清掉。"""
try:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
pg = ray_actor_hook("postgres_database").postgres_database
recorded: List[str] = await pg.list_plugin_owned_names.remote() or []
pg = get_actor("postgres_database")
recorded: List[str] = await pg.list_plugin_owned_names() or []
except Exception as e:
logger.debug(f"cleanup_orphan_plugin_slots skipped: {e}")
return
@@ -198,7 +195,7 @@ class GlobalPluginManager:
for plugin_name in recorded:
if plugin_name not in present:
try:
n = await pg.delete_plugin_slots.remote(plugin_name)
n = await pg.delete_plugin_slots(plugin_name)
logger.info(f"cleaned {n} orphan slots for missing plugin {plugin_name!r}")
except Exception as e:
logger.warning(f"failed to clean orphan slots for {plugin_name}: {e}")
+4 -18
View File
@@ -6,7 +6,7 @@ RegulatoryNode/ConsciousnessNode 通过这个工具向部门派单,等待部
from __future__ import annotations
from typing import Callable, Dict
from typing import Callable
def make_dispatch_tool(org_name: str, display_name: str, description: str) -> Callable:
@@ -19,12 +19,10 @@ def make_dispatch_tool(org_name: str, display_name: str, description: str) -> Ca
desc_text = description or f"把任务派给{display_name or org_name}部门,由部门内部多 agent 协作完成。"
async def _impl(task_description: str) -> str:
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
actor_name = f"org_{org_name}"
actor = ray_actor_hook(actor_name)
target = getattr(actor, actor_name)
result = await target.dispatch.remote(task_description, {})
target = get_actor(f"org_{org_name}")
result = await target.dispatch(task_description, {})
if result.get("status") == "completed":
return str(result.get("result") or "")
return f"[{org_name} 任务失败] {result.get('error') or 'unknown'}"
@@ -38,15 +36,3 @@ def make_dispatch_tool(org_name: str, display_name: str, description: str) -> Ca
" 部门交付的结果文本,失败时返回错误说明。\n"
)
return _impl
def collect_dispatch_tools(org_specs: Dict[str, Dict[str, str]]) -> Dict[str, Callable]:
"""根据 ``{org_name: {"display_name": ..., "description": ...}}`` 批量生成。"""
return {
f"dispatch_to_{name}": make_dispatch_tool(
name,
spec.get("display_name", ""),
spec.get("description", ""),
)
for name, spec in org_specs.items()
}
+3 -3
View File
@@ -191,11 +191,11 @@ 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
from kilostar.utils.actor import get_actor
postgres_database = ray_actor_hook("postgres_database").postgres_database
postgres_database = get_actor("postgres_database")
try:
user_authority = await postgres_database.get_user_authority.remote(
user_authority = await postgres_database.get_user_authority(
user_id=user_id
)
return user_authority
+180
View File
@@ -0,0 +1,180 @@
# 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.
"""进程内 actor 运行时(位置透明的组件寻址层)。
设计目标:**一处编写,处处运行**。业务代码永远通过 ``get_actor(name)`` 拿到一个
``ActorHandle``,再 ``await handle.method(args)`` 调用,**不关心对端在哪**。
今天所有核心组件(PostgresDatabase / GlobalStateMachine / ...)都是本进程内的
普通异步对象,``get_actor`` 返回 ``LocalActorHandle``,直接转发到本地实例。
未来若要把某一层(如跑 vLLM 的重型 worker)拆到独立进程/机器上,只需新增一个
``RemoteActorHandle``(gRPC / 子进程 / 消息队列后端)并在注册时选择它——业务侧的
调用代码一行都不用改。这层接缝就是当初 Ray ``.remote()`` 提供的价值,这里用零依赖
的方式把它保留下来。
"""
from __future__ import annotations
import asyncio
from typing import Any, Awaitable, Dict, Optional, Set
_registry: Dict[str, Any] = {}
_handle_cache: Dict[str, "ActorHandle"] = {}
# 已 spawn 的后台任务:持强引用,防止事件循环只持弱引用导致任务被 GC 掉
_background_tasks: Set["asyncio.Task[Any]"] = set()
class _BoundMethod:
"""包装目标对象的一个方法,使调用统一返回 awaitable。
- 目标方法是 ``async def`` → 直接返回其协程
- 目标方法是普通函数 → 把返回值包成一个已完成的 awaitable
这样调用方永远可以 ``await handle.method(...)``,无需关心同步/异步、本地/远程。
"""
__slots__ = ("_fn",)
def __init__(self, fn: Any) -> None:
self._fn = fn
def __call__(self, *args: Any, **kwargs: Any) -> Any:
result = self._fn(*args, **kwargs)
if asyncio.iscoroutine(result):
return result
async def _wrap() -> Any:
return result
return _wrap()
class ActorHandle:
"""actor 句柄基类:定义位置透明的调用接口。
未来的 ``RemoteActorHandle`` 继承本类,改写方法解析为跨进程 RPC 即可。
"""
class LocalActorHandle(ActorHandle):
"""本进程句柄:方法调用直接转发到已注册的实例。"""
__slots__ = ("_impl",)
def __init__(self, impl: Any) -> None:
object.__setattr__(self, "_impl", impl)
def __getattr__(self, name: str) -> Any:
attr = getattr(object.__getattribute__(self, "_impl"), name)
if callable(attr):
return _BoundMethod(attr)
return attr
def register_actor(name: str, instance: Any) -> None:
"""注册一个 actor 实例。重复注册会覆盖(支持插件热重载)。"""
_registry[name] = instance
_handle_cache.pop(name, None)
def unregister_actor(name: str) -> None:
"""注销一个 actor(插件卸载用);不存在时静默。"""
_registry.pop(name, None)
_handle_cache.pop(name, None)
def get_actor(name: str) -> ActorHandle:
"""按名字取 actor 句柄;未注册时抛 KeyError。
动态名(如 ``f"org_{plugin}"``)也走这个统一入口。
"""
if name not in _registry:
raise KeyError(f"actor {name!r} 未注册")
handle = _handle_cache.get(name)
if handle is None:
handle = LocalActorHandle(_registry[name])
_handle_cache[name] = handle
return handle
def try_get_actor(name: str) -> Optional[ActorHandle]:
"""按名字取 actor 句柄;未注册时返回 None(可选依赖用)。"""
try:
return get_actor(name)
except KeyError:
return None
def clear_actors() -> None:
"""清空注册表(测试用)。"""
_registry.clear()
_handle_cache.clear()
_background_tasks.clear()
def spawn_background(coro: "Awaitable[Any]") -> "asyncio.Task[Any]":
"""在当前事件循环中后台调度一个协程,并维持强引用防止被 GC。
等价于旧 Ray `.remote()` 的 fire-and-forget 语义:
- 协程被调度执行;
- 异常不会静默消失,而是记录到日志;
- 调用方无需 await,也无需保存返回值。
"""
from kilostar.utils.logger import get_logger
task: asyncio.Task[Any] = asyncio.ensure_future(coro)
_background_tasks.add(task)
def _on_done(t: "asyncio.Task[Any]") -> None:
_background_tasks.discard(t)
if not t.cancelled() and t.exception() is not None:
get_logger("actor").exception(
f"后台任务 {t.get_name()!r} 异常退出", exc_info=t.exception()
)
task.add_done_callback(_on_done)
return task
# ─── 类型化 getter(核心单例,语义糖)──────────────────────────
def get_postgres() -> ActorHandle:
return get_actor("postgres_database")
def get_gsm() -> ActorHandle:
return get_actor("global_state_machine")
def get_gwm() -> ActorHandle:
return get_actor("global_workflow_manager")
def get_regulatory() -> ActorHandle:
return get_actor("regulatory_node")
def get_consciousness() -> ActorHandle:
return get_actor("consciousness_node")
def get_worker_cluster() -> ActorHandle:
return get_actor("worker_cluster")
def get_plugin_manager() -> ActorHandle:
return get_actor("global_plugin_manager")
-40
View File
@@ -1,40 +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 pydantic import BaseModel
class ResponseModel(BaseModel):
"""
ResponseModel类
继承自pydantic的BaseModel类,是一切回复模型的父类
"""
pass
class DepsModel(BaseModel):
"""
DepsModel类
继承自pydantic的BaseModel类,是agent运行时依赖模型的父类
"""
pass
class RequestModel(BaseModel):
"""
RequestModel类
继承自pydantic的BaseModel类,是一切请求模型的父类
"""
pass
-117
View File
@@ -1,117 +0,0 @@
import importlib.util
import json
import os
import sys
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:
"""按名字从 toolset 中加载工具函数。
根据 manifest 找到工具所在的 toolset 和文件,动态加载模块并取出同名函数。
"""
func = _tool_cache.get(tool_name)
if func:
return func
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
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"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
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.error(
f"Tool function '{tool_name}' not found or not callable in {module_name}"
)
return None
_tool_cache[tool_name] = func
return func
except Exception as e:
logger.error(f"Failed to load module {tool_name}: {e}")
return None
def del_tool_cache(tool_name: str) -> None:
"""从内存缓存中移除某个工具,下次调用 ``load_tools_from_list`` 会重新从磁盘加载。"""
if tool_name in _tool_cache:
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:
return []
tool_list = []
for tool_name in tool_names:
tool_func = _get_tool_func(tool_name)
if tool_func:
tool_list.append(tool_func)
return tool_list
+5 -5
View File
@@ -17,7 +17,7 @@
from typing import Dict, List, Any, Optional, Sequence
from kilostar.utils.logger import get_logger
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.actor import get_actor
logger = get_logger("mcp_helper")
@@ -132,8 +132,8 @@ async def get_all_tools_and_toolsets_for_scope(
# 合入重型插件的 dispatch tools
try:
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
dispatch = await pm.get_dispatch_tools.remote()
pm = get_actor("global_plugin_manager")
dispatch = await pm.get_dispatch_tools()
if dispatch:
tools.extend(dispatch.values())
except Exception as e:
@@ -154,8 +154,8 @@ async def get_retrieval_toolsets_for_scope(scope: str) -> List[Any]:
"""仅返回 retrieval 工具集(system_node 专用)。不含 generation 和 MCP 工具。"""
toolsets: List[Any] = []
try:
gsm = ray_actor_hook("global_state_machine").global_state_machine
retrieval = await gsm.get_retrieval_toolsets_for_scope.remote(scope)
gsm = get_actor("global_state_machine")
retrieval = await gsm.get_retrieval_toolsets_for_scope(scope)
if retrieval:
toolsets.extend(retrieval)
except Exception as e:
-106
View File
@@ -1,106 +0,0 @@
"""KiloStar Ray 兼容层:单机/分布式模式无感切换 + 序列化工具。
单机模式下,所有 Actor 退化为普通 Python 异步单例,通过 StandaloneProxy
包装后暴露与 Ray Actor Handle 相同的 `.method.remote(args)` 调用接口,
使上层代码在两种模式间无感切换。
"""
from __future__ import annotations
import asyncio
import os
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。"""
__slots__ = ("_method",)
def __init__(self, method: Any):
self._method = method
def remote(self, *args: Any, **kwargs: Any) -> asyncio.Task:
async def _invoke():
result = self._method(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return asyncio.ensure_future(_invoke())
class StandaloneProxy:
"""包装一个普通 Python 实例,模拟 Ray Actor Handle 的属性访问接口。
用法:proxy.some_method.remote(x, y) → 等效于 await instance.some_method(x, y)
"""
__slots__ = ("_instance",)
def __init__(self, instance: Any):
object.__setattr__(self, "_instance", instance)
def __getattr__(self, name: str) -> _MethodProxy:
attr = getattr(object.__getattribute__(self, "_instance"), name)
if callable(attr):
return _MethodProxy(attr)
return attr
# ─── 条件装饰器 ───
def actor_class(cls):
"""条件装饰器:分布式模式 → @ray.remote,单机模式 → 原样返回类。"""
if _STANDALONE:
return cls
import ray
return ray.remote(cls)
def remote_task(func):
"""条件装饰器:分布式 → @ray.remote(func),单机 → .remote() 转为 asyncio task。
单机模式下返回一个 stub 对象,其 .remote() 方法把函数以协程方式调度到
当前事件循环(workflow task 需要用 await 版本的 _entry,由调用方处理)。
"""
if _STANDALONE:
class _TaskProxy:
@staticmethod
def remote(*args, **kwargs):
async def _run():
result = func(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return asyncio.ensure_future(_run())
return _TaskProxy()
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
-142
View File
@@ -1,142 +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.
import time
from functools import lru_cache
from typing import Any, Dict
from kilostar.utils.ray_compat import _STANDALONE
if not _STANDALONE:
import ray
class ActorList:
"""属性式访问的简易容器,用 ``a.actor_name`` 取代 ``d["actor_name"]``。"""
def __init__(self):
super().__setattr__("dict", {})
def __setattr__(self, key, value):
self.dict[key] = value
def __getattr__(self, key):
if key in self.dict:
return self.dict[key]
raise AttributeError(f"ActorList 对象没有属性 '{key}'")
def __delattr__(self, key):
if key in self.dict:
del self.dict[key]
else:
raise AttributeError(f"ActorList对象没有属性 '{key}'")
# ─── Standalone Registry ───
_standalone_registry: Dict[str, Any] = {}
def register_standalone(name: str, instance: Any) -> None:
"""注册一个单机模式下的 Actor 单例(已包装为 StandaloneProxy)。"""
from kilostar.utils.ray_compat import StandaloneProxy
_standalone_registry[name] = StandaloneProxy(instance)
# ─── Distributed Mode Helpers ───
if not _STANDALONE:
@lru_cache(maxsize=128)
def _get_cached_actor_handle(actor_name: str):
"""缓存接口"""
return ray.get_actor(actor_name, namespace="kilostar")
def clear_actor_cache():
"""清理接口"""
_get_cached_actor_handle.cache_clear()
def wait_for_actor(
actor_name: str, *, timeout: float = 10.0, interval: float = 0.5
):
"""阻塞等待某个 actor 就绪,返回其句柄。"""
deadline = time.monotonic() + max(timeout, 0.0)
last_err: Exception | None = None
while True:
try:
return _get_cached_actor_handle(actor_name)
except Exception as e:
last_err = e
if time.monotonic() >= deadline:
raise TimeoutError(
f"等待 actor {actor_name!r} 就绪超时({timeout}s):{last_err}"
) from last_err
time.sleep(interval)
else:
def _get_cached_actor_handle(actor_name: str):
raise RuntimeError("单机模式下不应调用 _get_cached_actor_handle")
def clear_actor_cache():
pass
def wait_for_actor(actor_name: str, **kwargs):
raise RuntimeError("单机模式下不应调用 wait_for_actor")
# ─── 统一入口 ───
def ray_actor_hook(*actor_names: str, timeout: float = 0.0, interval: float = 0.5):
"""按名字批量取出 Actor 句柄,组装成一个 ActorList 返回。
单机模式从 _standalone_registry 取,分布式模式走 ray.get_actor。
"""
actor_list = ActorList()
if _STANDALONE:
for name in actor_names:
if name not in _standalone_registry:
raise ValueError(
f"Standalone registry: actor {name!r} not registered"
)
setattr(actor_list, name, _standalone_registry[name])
return actor_list
for actor_name in actor_names:
if timeout > 0:
handle = wait_for_actor(
actor_name, timeout=timeout, interval=interval
)
else:
handle = _get_cached_actor_handle(actor_name)
setattr(actor_list, actor_name, handle)
return actor_list
def get_worker_cluster(affinity: str = "cpu"):
"""按 node_affinity 标签取对应的 WorkerCluster actor 句柄。
单机模式统一返回唯一的 worker_cluster 实例。
分布式模式按 affinity 路由到 worker_cluster_cpu / _core / _gpu。
未知标签降级到 cpu。
"""
if _STANDALONE:
return _standalone_registry.get("worker_cluster")
_valid = {"cpu", "core", "gpu"}
node_type = affinity if affinity in _valid else "cpu"
return _get_cached_actor_handle(f"worker_cluster_{node_type}")
+4 -26
View File
@@ -27,8 +27,8 @@
1. ``contextvars`` 在 ``asyncio`` 协程间天然继承,不会跨协程串味;
2. ``loguru`` 的 ``patcher`` 钩子可以把它变成日志切面,业务代码不需要在每条
``logger.info`` 上手动 ``.bind(trace_id=...)``;
3. Ray 跨进程调用时 contextvars 不会自动传播 —— 这是有意为之,避免不同 actor
间的上下文意外串联。跨 actor 边界要走显式参数,由接收方再 ``bind_*`` 一次。
3. 跨 asyncio.create_task 边界时也会自动继承当前 context,符合单进程 actor 模型
的语义 —— 一次任务链路的 ID 与其入口请求一致,不必显式透传。
"""
from __future__ import annotations
@@ -36,7 +36,7 @@ from __future__ import annotations
import uuid
from contextlib import contextmanager
from contextvars import ContextVar, Token
from typing import Iterator, Optional
from typing import Iterator
_request_id_var: ContextVar[str] = ContextVar("kilostar_request_id", default="")
@@ -101,30 +101,8 @@ def new_request_id(prefix: str = "req") -> str:
def snapshot() -> dict[str, str]:
"""返回当前上下文 ID 的快照,便于跨 actor/task 边界显式透传。"""
"""返回当前上下文 ID 的快照,便于日志/追踪或结构化写库。"""
return {
"request_id": _request_id_var.get(),
"trace_id": _trace_id_var.get(),
}
@contextmanager
def apply_snapshot(snap: Optional[dict[str, str]]) -> Iterator[None]:
"""把外部传来的 snapshot 在当前 context 内生效一次(用于跨 Ray actor 调用时)。"""
if not snap:
yield
return
tokens: list[Token] = []
if snap.get("request_id"):
tokens.append(_request_id_var.set(snap["request_id"]))
if snap.get("trace_id"):
tokens.append(_trace_id_var.set(snap["trace_id"]))
try:
yield
finally:
for tok in reversed(tokens):
try:
tok.var.reset(tok)
except (ValueError, LookupError):
# token 可能因协程切换失效,宽容处理
pass
-1
View File
@@ -38,7 +38,6 @@ class OnebotSettings(BaseSettings):
class AppSettings(BaseSettings):
kilostar_mode: str = "distributed"
kilostar_lang: str = "zh"
kilostar_cors_origins: str = ""
kilostar_plugin_dir: str = ""
+11 -24
View File
@@ -15,13 +15,7 @@
import time
import asyncio
from collections import OrderedDict
from kilostar.utils.ray_compat import actor_class, _STANDALONE
from kilostar.utils.ray_hook import ray_actor_hook
if _STANDALONE:
from asyncio import Queue
else:
from ray.util.queue import Queue
from asyncio import Queue
from kilostar.worker_individual.base_individual import BaseIndividual
from kilostar.worker_individual.skill_individual import SkillIndividual
from kilostar.worker_individual.ordinary_individual import OrdinaryIndividual
@@ -31,15 +25,14 @@ from kilostar.worker_individual.special_individual import SpecialIndividual
from kilostar.utils.logger import get_logger
@actor_class
class WorkerCluster:
"""
工作集群 Actor:管理和调度所有的 worker_individual
设计理念:按需加载,内存 LRU 淘汰,避免 Actor 爆炸
工作集群:管理和调度所有的 worker_individual
设计理念:按需加载,内存 LRU 淘汰,避免实例爆炸
分布式模式下每种 node_type 对应一个独立实例,Ray 根据自定义资源
``kilostar_node_cpu`` / ``kilostar_node_core`` / ``kilostar_node_gpu``
将 Actor 调度到声明了对应资源的节点上。
``submit_task`` 是对外的稳定执行边界——未来若要把重型 worker(如 vLLM 本地
推理)拆到独立进程/机器上,只需在这个边界后面换一个 executor 实现,上层不感知。
``node_type`` 字段暂时保留,供未来按算力亲和性路由使用。
"""
def __init__(self, max_capacity: int = 200, num_runners: int = 10, node_type: str = "cpu"):
@@ -70,11 +63,8 @@ class WorkerCluster:
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
global_state_machine = ray_actor_hook(
"global_state_machine"
).global_state_machine
# 走快照读,避开 GSM actor RPC:高频唤醒路径不再是单 actor 瓶颈
snapshot = await fetch_snapshot(gsm_actor=global_state_machine)
# 走快照读:高频唤醒路径直接读进程内缓存快照
snapshot = await fetch_snapshot()
agent_config = snapshot.individuals.get(agent_id)
if not agent_config:
@@ -104,7 +94,7 @@ class WorkerCluster:
if self.task_queue is None:
await asyncio.sleep(0.1)
continue
task = await self.task_queue.get() if _STANDALONE else await self.task_queue.get_async()
task = await self.task_queue.get()
task_id = task.get("task_id")
agent_id = task.get("agent_id")
task_event = task.get("task_event")
@@ -150,10 +140,7 @@ class WorkerCluster:
self.results_futures[task_id] = future
task = {"task_id": task_id, "agent_id": agent_id, "task_event": task_event}
if _STANDALONE:
await self.task_queue.put(task)
else:
await self.task_queue.put_async(task)
await self.task_queue.put(task)
self.logger.debug(f"[WorkerCluster] 任务 {task_id} 已加入队列。")
try:
@@ -168,5 +155,5 @@ class WorkerCluster:
"active_worker_count": len(self._active_workers),
"max_capacity": self.max_capacity,
"cached_agent_ids": list(self._active_workers.keys()),
"queue_size": self.task_queue.qsize() if _STANDALONE else self.task_queue.size(),
"queue_size": self.task_queue.qsize() if self.task_queue else 0,
}
+6 -11
View File
@@ -13,30 +13,28 @@
# limitations under the License.
from pydantic_ai import Agent, RunContext
from pydantic import Field
from pydantic import BaseModel, Field
from kilostar.adapter.model_adapter.agent_factory import AgentFactory
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
from kilostar.utils.agent_model import ResponseModel, RequestModel, DepsModel
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.logger import get_logger
logger = get_logger("worker_individual")
class WorkerIndividualResponse(ResponseModel):
class WorkerIndividualResponse(BaseModel):
"""Worker Individual 的输出模型,承载一次任务执行后的结果文本。"""
output: str = Field(..., description="Worker执行任务的输出结果")
class WorkerIndividualDeps(DepsModel):
class WorkerIndividualDeps(BaseModel):
"""Worker Individual 的运行期依赖,注入到 pydantic-ai Agent 的 RunContext。"""
task_event: dict
class WorkerIndividualInput(RequestModel):
class WorkerIndividualInput(BaseModel):
"""Worker Individual 的输入模型,承载一次任务事件的入参。"""
task_event: dict
@@ -67,17 +65,14 @@ class BaseIndividual:
from kilostar.utils.mcp_helper import get_all_tools_and_toolsets_for_scope
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
global_state_machine = ray_actor_hook(
"global_state_machine"
).global_state_machine
provider_title = self.agent_config.get(
"provider_title", "openai"
) # default fallback
model_id = self.agent_config.get("model_id", "gpt-4o") # default fallback
toolset_ids = self.agent_config.get("tools", None)
# 直读快照,避开 actor RPC 单线程串行
snapshot = await fetch_snapshot(gsm_actor=global_state_machine)
# 直读进程内缓存快照
snapshot = await fetch_snapshot()
provider: Provider = snapshot.providers.get(provider_title)
if provider is None:
raise ValueError(f"Provider {provider_title!r} 未注册")