fix: 修复 5 项确定 bug + Provider UX 重做 + 文档统一

Bug fixes:
- fix(dao): AsyncSession.delete 补齐漏掉的 await(provider/user/individual 共 4 处)
- fix(worker): result.data.output → result.output.output(pydantic-ai 1.x API 适配)
- fix(api): 删除 create_worker_from_template 死端点(ORM 字段不匹配必崩)
- fix(api): /provider/test 按 provider_type 分支适配 Anthropic/Gemini/OpenAI 三种协议
- fix(chat): SSE 流式聊天在 distributed 模式 fallback 到非流式,避免 asyncio.Queue 序列化崩溃

Features (previously unstaged):
- feat(provider): Provider 管理页重做(品牌图标、5 种类型、Test Connection、编辑模式)
- feat(provider): 新增 Gemini provider_type 支持
- feat(workflow): Finalize 节点输出 blackboard 摘要 + 失败原因;步骤完成/失败实时推送 SSE
- feat(i18n): regulatory_node 提示词从路由模式改为直接对话模式(中英双语)
- feat(consciousness): dynamic_prompt 支持 locale 国际化
- feat(logs): SystemLogsView 自动刷新 + 暂停按钮

Docs:
- docs: README/README-EN 统一为"开源通用多 Agent 协作平台"口径
- docs: ROADMAP 按 v0.1.x / v0.2.x / v0.3.x 重组
- docs: project.md 重写为结构化项目介绍

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 08:49:38 +00:00
parent c0fcbe2849
commit 9b73ae4db4
27 changed files with 858 additions and 214 deletions
+17 -6
View File
@@ -163,15 +163,15 @@ async def stream_chat_message(
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""SSE 流式聊天端点:通过 regulatory_node agent 流式输出,支持工具调用"""
"""SSE 流式聊天端点:standalone 模式下逐 token 流式输出distributed 模式 fallback 到整段回复"""
from kilostar.utils.standalone_proxy import _STANDALONE
postgres_database = ray_actor_hook("postgres_database").postgres_database
# 存用户消息
await postgres_database.add_chat_message.remote(
chat_id=chat_id, message=request_body.message, message_owner="user"
)
# 构造 MessageRequest payload
payload = MessageRequest(
platform="client",
user_name=token_data.user_id,
@@ -180,9 +180,21 @@ async def stream_chat_message(
)
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
token_queue = asyncio.Queue()
# stream_working.remote() returns an asyncio.Task in standalone mode
if not _STANDALONE:
async def fallback_generator():
resp = await regulatory_node.working.remote(payload)
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")
token_queue = asyncio.Queue()
stream_task = regulatory_node.stream_working.remote(payload, token_queue)
async def event_generator():
@@ -207,7 +219,6 @@ async def stream_chat_message(
full_response = "抱歉,生成回复时出错。"
yield f"data: {json.dumps({'token': full_response})}\n\n"
# 流结束,存入数据库
if full_response:
await postgres_database.add_chat_message.remote(
chat_id=chat_id,
+58 -1
View File
@@ -27,7 +27,7 @@ provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
class ProviderRegister(BaseModel):
"""``POST /provider`` 入参:注册一个模型 Provider 的最小字段集。"""
provider_type: Literal["openai", "claude", "deepseek"]
provider_type: Literal["openai", "claude", "deepseek", "gemini"]
provider_title: str
provider_url: str
provider_apikey: str
@@ -72,6 +72,63 @@ async def get_provider_list(
return {"provider_list": masked}
@provider_router.post("/test")
async def test_provider_connection(
provider_register: ProviderRegister,
_: TokenData = Depends(Accessor.get_current_user),
) -> Dict[str, Any]:
"""测试 Provider 连接:按 provider_type 选择对应协议拉取模型列表。"""
import httpx
ptype = provider_register.provider_type
url = provider_register.provider_url
apikey = provider_register.provider_apikey
try:
async with httpx.AsyncClient(timeout=10.0) as client:
if ptype == "claude":
endpoint = f"{url}/v1/models"
headers = {
"x-api-key": apikey,
"anthropic-version": "2023-06-01",
}
response = await client.get(endpoint, headers=headers)
if response.status_code == 200:
data = response.json()
models = [m["id"] for m in data.get("data", [])]
return {"success": True, "models": sorted(models), "model_count": len(models)}
return {"success": False, "error": f"HTTP {response.status_code}", "models": []}
elif ptype == "gemini":
endpoint = f"{url}/models"
params = {"key": apikey}
response = await client.get(endpoint, params=params)
if response.status_code == 200:
data = response.json()
models = [m.get("name", "").removeprefix("models/") for m in data.get("models", [])]
return {"success": True, "models": sorted(models), "model_count": len(models)}
return {"success": False, "error": f"HTTP {response.status_code}", "models": []}
else:
if "/v1" not in url:
endpoint = f"{url}/v1/models"
else:
endpoint = f"{url}/models"
headers = {
"Authorization": f"Bearer {apikey}",
"Content-Type": "application/json",
}
response = await client.get(endpoint, headers=headers)
if response.status_code == 200:
data = response.json()
models = [m["id"] for m in data.get("data", [])]
return {"success": True, "models": sorted(models), "model_count": len(models)}
return {"success": False, "error": f"HTTP {response.status_code}", "models": []}
except Exception as e:
return {"success": False, "error": str(e), "models": []}
@provider_router.delete("/{provider_title}")
async def delete_provider(
provider_title: str,
@@ -39,6 +39,7 @@ class ConsciousnessNode:
self.logger = get_logger("consciousness_node")
self.agent: None | Agent = None
self.locale: str = "zh"
async def create_agent(
self,
@@ -51,6 +52,7 @@ class ConsciousnessNode:
custom_system_prompt: str | None = None,
) -> None:
system_prompt: str = agent_prompt("consciousness_node", locale=locale, custom_system_prompt=custom_system_prompt)
self.locale = locale or "zh"
output_type = Union[ForregulatoryNode, ForWorkflow, ForWorkflowEngine]
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
@@ -74,23 +76,43 @@ class ConsciousnessNode:
@self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
locale = ctx.deps.locale
prompt = system_prompt + "\n\n"
prompt += (
f"=== 当前任务上下文 ===\n"
f"- 当前指令 (Command): {ctx.deps.command}\n"
f"- 原始用户命令 (Original Command): {ctx.deps.original_command}\n"
)
if ctx.deps.available_skills:
prompt += "\n=== 当前可用 Skill Individual ===\n"
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!),作为可调用的工具。\n"
for skill in ctx.deps.available_skills:
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\n"
if locale == "en":
prompt += (
f"=== Current Task Context ===\n"
f"- Command: {ctx.deps.command}\n"
f"- Original User Command: {ctx.deps.original_command}\n"
)
if ctx.deps.available_skills:
prompt += "\n=== Available Skill Individuals ===\n"
prompt += "You may assign the following Skill Individuals to workflow steps (set node to skill_individual, and set agent_id to the real agent_id below — never use the name!).\n"
for skill in ctx.deps.available_skills:
prompt += f"- agent_id: {skill.get('agent_id')}\n Name: {skill['name']}\n Description: {skill['description']}\n"
else:
prompt += "\n=== IMPORTANT: No Worker Individuals Available ===\n"
prompt += "No Worker Individuals are registered. When generating a workflow, you have exactly two options:\n"
prompt += "1. Assign the step to consciousness_node itself (set node to consciousness_node, agent_id to null).\n"
prompt += "2. If the task truly requires specialized tools, refuse and explain that a Worker must be created first.\n"
prompt += "NEVER fabricate non-existent agent_ids!\n"
else:
prompt += "\n=== 重要:当前无可用 Worker Individual ===\n"
prompt += "系统中当前没有注册任何 Worker Individual。在生成工作流时,你有且仅有以下两种选择:\n"
prompt += "1. 将步骤分配给 consciousness_node 自己完成(设置 node 为 consciousness_nodeagent_id 为 null)。\n"
prompt += "2. 如果任务确实需要专用工具或技能才能完成,则拒绝执行并在输出中说明需要先创建对应的 Worker。\n"
prompt += "绝对禁止编造不存在的 agent_id!\n"
prompt += (
f"=== 当前任务上下文 ===\n"
f"- 当前指令: {ctx.deps.command}\n"
f"- 原始用户命令: {ctx.deps.original_command}\n"
)
if ctx.deps.available_skills:
prompt += "\n=== 当前可用 Skill Individual ===\n"
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!)。\n"
for skill in ctx.deps.available_skills:
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\n"
else:
prompt += "\n=== 重要:当前无可用 Worker Individual ===\n"
prompt += "系统中当前没有注册任何 Worker Individual。在生成工作流时,你有且仅有以下两种选择:\n"
prompt += "1. 将步骤分配给 consciousness_node 自己完成(设置 node 为 consciousness_nodeagent_id 为 null)。\n"
prompt += "2. 如果任务确实需要专用工具或技能才能完成,则拒绝执行并在输出中说明需要先创建对应的 Worker。\n"
prompt += "绝对禁止编造不存在的 agent_id!\n"
return prompt
@@ -127,7 +149,6 @@ class ConsciousnessNode:
original_command=command, available_skills=available_skills
)
# 通知 SSE 正在生成图结构(pending 队列:节点端写入 → API SSE 读取,单向下行)
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
@@ -135,7 +156,6 @@ class ConsciousnessNode:
trace_id, "正在为您构建并规划工作流任务节点,请稍候..."
)
# 实际构建过程
result = await self.working(payload)
if result and isinstance(result, ForWorkflowEngine):
@@ -197,6 +217,7 @@ class ConsciousnessNode:
original_command=payload.original_command,
command="自主分析并拆解原始命令,生成严密可执行的工作流",
available_skills=payload.available_skills,
locale=self.locale,
)
self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)")
prompt = "根据original_command制定严密的可执行workflow"
@@ -207,6 +228,7 @@ class ConsciousnessNode:
deps = ConsciousnessNodeDeps(
original_command=payload.original_command,
command="完成workflow step中分配给意识节点的特定任务或指导",
locale=self.locale,
)
self.logger.debug(
"ConsciousnessNode: 开始处理工作流节点任务 (原生重试开启)"
@@ -221,6 +243,7 @@ class ConsciousnessNode:
deps = ConsciousnessNodeDeps(
original_command=payload.original_command,
command="对于工作流整体执行结果进行检查,并且生成一份专业的技术性总结报告",
locale=self.locale,
)
self.logger.debug(
"ConsciousnessNode: 开始生成技术总结报告 (原生重试开启)"
@@ -28,7 +28,8 @@ class ConsciousnessNodeDeps(DepsModel):
"""ConsciousnessNode 在 pydantic-ai Agent 中使用的依赖:原始指令、当前指令以及可用 Skill 列表。"""
original_command: str
command: str
available_skills: Optional[List[str]] = None
available_skills: Optional[List[dict]] = None
locale: str = "zh"
class ConsciousnessNodeInput(RequestModel):
"""ConsciousnessNode 各类入参的共同基类,仅用于打 schema 标签。"""
@@ -30,10 +30,9 @@ from kilostar.utils.i18n import agent_prompt
@actor_class
class RegulatoryNode:
"""RegulatoryNode(监管节点):用户请求的入口路由 Actor
"""RegulatoryNode(监管节点):用户请求的直接对话节点
负责对消息做意图识别:闲聊 → 直接回 ``ForUser``;复杂任务 → 走
``ForConsciousnessNode`` 移交给意识节点;工作流回执 → 转译成对用户的总结回复。
负责理解用户需求并提供回复;如果收到工作流执行报告则转化为用户友好的总结。
"""
def __init__(self) -> None:
@@ -100,12 +99,9 @@ class RegulatoryNode:
f"- 用户名 (User): {ctx.deps.user_name}\n"
f"- 当前时间 (Time): {ctx.deps.time}\n"
)
# 修改 system_prompt 变量
prompt += (
"\n\n注意:你必须调用且只能调用一个函数(工具)来输出结果"
"如果你想直接回复用户,请调用 ForUser;"
"如果你想移交给工作流,请调用 ForConsciousnessNode。"
"严禁返回纯文本,必须使用工具格式!"
"\n\n注意:请基于上下文信息为用户提供准确、专业的回复"
"如果你有可用工具,可在需要时主动调用。"
)
if ctx.deps.error_history:
prompt += (
@@ -130,7 +126,7 @@ class RegulatoryNode:
"规则:\n"
"1. 直接、详细地回答用户问题,像一个专业且友好的助手。\n"
"2. 如果你有可用工具,可以调用工具来辅助回答(如搜索、读文件等)。\n"
"3. 不要输出内部思考过程,不要做路由判断,不要提及 ForUser/ForConsciousnessNode 等格式\n"
"3. 不要输出内部思考过程,直接给出回复内容\n"
"4. 回复应当完整、有帮助,避免过于简短。\n"
)
@@ -104,7 +104,7 @@ class IndividualDatabase:
individual = results.scalar_one_or_none()
if not individual:
return False
session.delete(individual)
await session.delete(individual)
await session.commit()
return True
@@ -90,7 +90,7 @@ class ProviderDatabase:
async with self.async_session_maker() as session:
provider = await session.get(ProviderModel, provider_id)
if provider is not None:
session.delete(provider)
await session.delete(provider)
await session.commit()
@database_exception
@@ -78,7 +78,7 @@ class AuthDatabase:
user = results.scalar_one_or_none()
if user is None:
raise UserNotExistError()
session.delete(user)
await session.delete(user)
await session.commit()
@database_exception
@@ -88,7 +88,7 @@ class AuthDatabase:
user = await session.get(User, user_id)
if user is None:
raise UserNotExistError()
session.delete(user)
await session.delete(user)
await session.commit()
@database_exception
+31 -6
View File
@@ -223,11 +223,28 @@ class Finalize(BaseNode[WorkflowGraphState, WorkflowDeps, str]):
) -> End[str]:
ctx.state.final_status = self.status
await ctx.deps.update_workflow_status(ctx.state.trace_id, self.status)
msg = (
"工作流执行完成!"
if self.status == WorkflowStatus.COMPLETED.value
else "工作流执行失败。"
)
if self.status == WorkflowStatus.COMPLETED.value:
summary_parts = []
for key, val in ctx.state.blackboard.items():
text = str(val)[:200]
summary_parts.append(f"{key}: {text}")
summary = "\n".join(summary_parts) if summary_parts else ""
msg = f"工作流执行完成!\n{summary}" if summary else "工作流执行完成!"
else:
failed_logs = [
entry for entry in ctx.state.logs
if any(
isinstance(v, (list, tuple)) and len(v) >= 2 and v[1] == "failed"
for v in (entry.values() if isinstance(entry, dict) else [])
)
]
msg = "工作流执行失败。"
if failed_logs:
last = list(failed_logs[-1].values())[0]
if isinstance(last, (list, tuple)) and len(last) >= 3:
msg += f"\n失败原因: {last[2][:300]}"
await ctx.deps.put_pending(ctx.state.trace_id, msg)
return End(self.status)
@@ -295,9 +312,13 @@ async def _execute_step(
state.logs[-1][str(state.current_step_index)] = [
str(datetime.datetime.now()),
"completed",
f"成功: {step_data.get('action', '')}",
output_text,
]
await _persist_context(ctx, status=WorkflowStatus.RUNNING.value)
await ctx.deps.put_pending(
state.trace_id,
f"✅ 步骤 {state.current_step_index + 1} ({step_data.get('name', '')}) 完成:\n{output_text[:500]}",
)
logic_gate = step_data.get("logic_gate") or {}
if logic_gate.get("if_pass") == "exit":
@@ -314,6 +335,10 @@ async def _execute_step(
"failed",
output_text,
]
await ctx.deps.put_pending(
state.trace_id,
f"❌ 步骤 {state.current_step_index + 1} ({step_data.get('name', '')}) 失败:\n{output_text[:300]}",
)
logic_gate = step_data.get("logic_gate") or {}
fail_target = logic_gate.get("if_fail")
if fail_target and "jump_to_step_" in fail_target:
+15 -15
View File
@@ -36,24 +36,24 @@ _DEFAULT_LOCALE: str = get_settings().kilostar_lang
_PROMPTS: Dict[str, Dict[str, str]] = {
"regulatory_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【监节点 (regulatory Node)】。\n"
"你是系统'前台接待''大脑皮层',负责接收用户的初始请求或工作流的最终报告。\n"
"你的核心职责是进行【意图识别与路由】。请仔细阅读用户的请求\n"
"1. 如果用户只是进行简单的问候、闲聊或查询非常基础的信息,请直接生成友好的回复,使用 ForUser 格式\n"
"2. 如果用户提出的是复杂任务(如需要编写代码、多步骤规划、数据处理等),请务必将其判定为需要工作流处理的任务,"
" 并使用 ForConsciousnessNode 格式将其移交意识节点处理\n"
"3. 如果你收到的是 TerminationMessage(代表工作流已完成并生成了报告),请将报告内容转化为友好的面向用户的回复,使用 ForUser 格式\n"
"请保持冷静、专业,并严格遵循上述路由规则"
"你叫kilostar,是一个多智能体AI助手系统中的【监节点 (Regulatory Node)】。\n"
"你是系统中直接面向用户的对话节点,负责理解用户需求并提供高质量的回复。\n\n"
"你的核心职责:\n"
"1. 准确理解用户的意图,提供专业、友好且有帮助的回复\n"
"2. 如果你有可用工具,可以主动调用工具来辅助回答(如搜索、文件操作等)。\n"
"3. 如果你收到工作流的执行报告,请将其转化为面向用户的清晰总结\n"
"4. 保持回复简洁、有结构,避免冗余信息\n"
"请保持专业、友好的沟通风格"
),
"en": (
"You are kilostar, the [Regulatory Node] in a multi-agent AI assistant system.\n"
"You are the system's 'front desk' and 'cerebral cortex', responsible for receiving user requests and final workflow reports.\n"
"Your core duty is [intent recognition and routing]. Please read the user's request carefully:\n"
"1. If the user is simply greeting, chatting, or asking very basic questions, generate a friendly reply directly in the ForUser format.\n"
"2. If the user presents a complex task (e.g., writing code, multi-step planning, data processing), you must classify it as a workflow-requiring task "
" and hand it over to the Consciousness Node using the ForConsciousnessNode format.\n"
"3. If you receive a TerminationMessage (indicating the workflow is complete and a report has been generated), convert the report into a user-friendly reply in the ForUser format.\n"
"Please remain calm, professional, and strictly follow the routing rules above."
"You are the user-facing conversational node, responsible for understanding user needs and providing high-quality responses.\n\n"
"Your core responsibilities:\n"
"1. Accurately understand user intent and provide professional, friendly, and helpful replies.\n"
"2. If tools are available, proactively use them to assist your responses (e.g., search, file operations).\n"
"3. If you receive a workflow execution report, convert it into a clear user-facing summary.\n"
"4. Keep responses concise, well-structured, and free of redundancy.\n"
"Maintain a professional and friendly communication style."
),
},
"consciousness_node": {
@@ -41,7 +41,7 @@ class OrdinaryIndividual(BaseIndividual):
self.agent.retries = 3
try:
result = await self.agent.run(f"请执行以下任务:\n{task_event}", deps=deps)
return {"output": result.data.output}
return {"output": result.output.output}
except Exception as e:
logger.exception(f"OrdinaryIndividual {self.agent_id} 执行失败: {e}")
raise
@@ -123,7 +123,7 @@ class SkillIndividual(BaseIndividual):
deps=deps,
tools=tools if tools else None,
)
return {"output": result.data.output}
return {"output": result.output.output}
except Exception as e:
logger.exception(f"SkillIndividual {self.agent_id} 执行失败: {e}")
raise
@@ -41,7 +41,7 @@ class SpecialIndividual(BaseIndividual):
self.agent.retries = 3
try:
result = await self.agent.run(f"请执行以下任务:\n{task_event}", deps=deps)
return {"output": result.data.output}
return {"output": result.output.output}
except Exception as e:
logger.exception(f"SpecialIndividual {self.agent_id} 执行失败: {e}")
raise