chore: 迁移服务器前同步本地修改
This commit is contained in:
@@ -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发送消息请求的模型
|
||||
|
||||
@@ -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 都兼容
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user