chore: 迁移服务器前同步本地修改
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
Reference in New Issue
Block a user