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
+40 -13
View File
@@ -2,7 +2,7 @@
# KiloStar # KiloStar
A distributed multi-agent collaboration system built with Python An open-source general-purpose multi-agent collaboration platform
[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/) [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/)
[![Ray](https://img.shields.io/badge/Distributed-Ray-0288d1.svg)](https://docs.ray.io/) [![Ray](https://img.shields.io/badge/Distributed-Ray-0288d1.svg)](https://docs.ray.io/)
@@ -15,31 +15,58 @@ A distributed multi-agent collaboration system built with Python
--- ---
**KiloStar** is a next-generation distributed multi-agent collaboration system powered by **Ray**. It adopts a "central oversight + edge execution" heterogeneous cluster model, leveraging large MoE models for high-level reasoning while coordinating fine-tuned lightweight models for efficient task execution. Built on **Pydantic-AI** with strong typing and a FastAPI async gateway, KiloStar delivers end-to-end automation from requirement decomposition to resource scheduling and execution. ## Overview
**KiloStar** is an open-source general-purpose multi-agent collaboration platform that provides a complete capability stack covering model integration, agent orchestration, workflow execution, and plugin extension. The system uses **Ray** for distributed execution, **Pydantic-AI** for type-safe agent development, and exposes a unified API surface through **FastAPI**.
The platform supports both cloud API models and locally fine-tuned models, ships with built-in core nodes for multi-agent collaboration (Regulatory, Consciousness, Control, Growth), and provides a **heavy plugin** mechanism that lets users reshape the platform into purpose-built agent applications.
> **Current version**: `v0.1.1-alpha` > **Current version**: `v0.1.1-alpha`
## Highlights
- **Local fine-tuned models as first-class citizens**: Built-in vLLM adapter; locally fine-tuned models are interchangeable with cloud API models at the call site, allowing different agent nodes to bind different local models.
- **Heavy plugin mechanism**: Plugins can ship their own frontend pages, tool sets, and API endpoints — turning KiloStar into specialized agent applications such as coding assistants, learning helpers, or data analysis tools.
- **Multi-agent collaboration core**: Four system node types (Regulatory / Consciousness / Control / Growth) plus dynamically spawned Worker individuals, with task decomposition, scheduling, and supervision built in.
- **standalone / distributed dual mode**: Zero-dependency single-machine startup; horizontal scaling on demand. Business code is identical across both modes.
- **Private deployment friendly**: Every component runs inside the user's own environment without mandatory third-party dependencies.
--- ---
## ✨ Key Features ## ✨ Core Capabilities
### 🧠 Heterogeneous Agent Architecture ### 🧠 Multi-Agent Collaboration
- **Multi-agent cluster**: Built-in Regulatory, Consciousness, Control, and Growth core nodes - **System node specialization**: Regulatory, Consciousness, Control, and Growth nodes each cover a distinct responsibility
- **Dynamic Worker spawning**: On-demand creation of Ordinary or Skill-type Worker Individuals - **Worker dynamic spawning**: On-demand creation of Ordinary / Skill / Special Worker individuals
- **Strongly-typed communication**: Pydantic-AI constrains LLM output to structured data, eliminating the unstructured-text black box in multi-agent flows
### 🚀 Distributed Performance ### 🚀 Distributed Execution
- **Ray-powered**: Cross-process, cross-machine Actor communication for high-concurrency workloads - **Ray Actor model**: Cross-process and cross-machine collaboration for high-concurrency workloads
- **Local-first**: Deep vLLM integration for private model deployment - **Heterogeneous resource labels**: `kilostar_node_cpu` / `core` / `gpu` route Workers to the right physical nodes
- **Standalone mode**: Zero external dependencies for single-machine startup; shares the same business code as distributed mode
### 🔄 Workflow Engine ### 🔄 Workflow Engine
- **pydantic-graph based**: Directed-graph workflow orchestration with conditional branching - **pydantic-graph driven**: Directed-graph workflow orchestration with conditional branching and loops
- **Cross-process persistence**: PostgreSQL state snapshots enabling workflow resume after interruption - **Cross-process persistence**: PostgreSQL state snapshots enable workflow resume after interruption
- **Human-in-the-Loop (HITL)**: Built-in HumanApproval node with idempotent resume semantics - **Human-in-the-Loop (HITL)**: Built-in HumanApproval node with idempotent resume semantics
### 🧩 Plugin System
- **Tool plugins**: Standard tool calls; MCP protocol support for third-party services
- **Skill (compatible with Anthropic Agent Skills spec)**: Installed and parsed via [viceroy](https://github.com/zhaoxi826/viceroy), loaded on demand at runtime
- **Heavy plugins (planned)**: Vertical application packages with dedicated UI that reshape KiloStar into specialized platforms
### 🛡️ Security ### 🛡️ Security
- **JWT authentication**: All API endpoints (including SSE streams) require Bearer Token auth - **JWT authentication**: All API endpoints (including SSE streams) require Bearer Token auth
- **Ownership enforcement**: Workflow/chat resources are user-bound; cross-user access returns 403 - **Ownership enforcement**: Workflow / chat resources are user-bound; cross-user access returns 403
- **fetch-based SSE**: Token transmitted via Authorization header, never exposed in URLs - **fetch-based SSE**: Token is transmitted via the `Authorization` header, never exposed in URLs
### 📦 Companion Subprojects
| Project | Codename | Purpose | Status |
|:--|:--|:--|:--|
| [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | Viceroy | Skill installation and cluster-wide distribution | ✅ Released |
| [kilostar-stardomain](./subprojects/stardomain) | Stardomain | Sandbox execution for Skill / plugin scripts | In progress |
| [kilostar-thought](https://github.com/zhaoxi826/thought) | Thought | Augmented memory system for agents | In progress |
--- ---
+29 -9
View File
@@ -2,7 +2,7 @@
# KiloStar (千星) # KiloStar (千星)
一款基于 Python 的分布式多 Agent 协作系统 开源通用多 Agent 协作平台
[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/) [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/)
[![Ray](https://img.shields.io/badge/Distributed-Ray-0288d1.svg)](https://docs.ray.io/) [![Ray](https://img.shields.io/badge/Distributed-Ray-0288d1.svg)](https://docs.ray.io/)
@@ -15,27 +15,46 @@
--- ---
**KiloStar** 是一款基于 **Ray** 构建的下一代分布式多 Agent 协作系统。项目采用"中心监管 + 边缘执行"的异构集群模式,通过大参数 MoE 模型进行高层逻辑推理,并协同微调后的轻量化模型高效完成具体任务。借助 **Pydantic-AI** 提供的强类型约束与 FastAPI 异步网关,KiloStar 实现了任务从需求拆解、资源调度到自动化执行的全链路闭环。 ## 简介
**KiloStar** 是一个开源的通用多 Agent 协作平台,提供从模型接入、Agent 编排、工作流执行到插件扩展的完整能力栈。系统基于 **Ray** 实现分布式执行,基于 **Pydantic-AI** 提供类型安全的 Agent 开发框架,并通过 **FastAPI** 网关对外暴露统一接口。
平台同时支持云端 API 模型与本地微调模型,内置多 Agent 协作的核心节点(监管、意识、控制、生长),并通过**重型插件**机制允许使用者把平台改造成面向具体场景的专用 Agent 应用。
> **当前版本**`v0.1.1-alpha` > **当前版本**`v0.1.1-alpha`
## 项目特色
- **本地微调小模型一等公民**:内置 vLLM 适配,支持将本地微调模型部署为系统中的 Agent 节点,与云端 API 模型在调用层面对等
- **重型插件机制**:插件可附带独立前端页面、工具组与 API 接口,将 KiloStar 改造为编程辅助、学习助手、数据分析等专用 Agent 应用
- **多 Agent 协作内核**:监管 / 意识 / 控制 / 生长四类系统节点 + 动态派生的 Worker 个体,原生支持任务拆解、调度、监督的分工模式
- **分布式与单机统一**standalone 与 distributed 双模式共享同一套代码,单机零依赖起步,集群按需横向扩展
- **私有化部署友好**:所有组件可在用户自有环境内运行,不强制依赖任何第三方服务
--- ---
## ✨ 核心特性 ## ✨ 核心能力
### 🧠 异构协作体系 ### 🧠 多 Agent 协作
- **多智能体集群**:内置监控 (Regulatory)、意识 (Consciousness)、控制 (Control)、生长 (Growth) 核心节点 - **核心节点分工**:监管 (Regulatory)、意识 (Consciousness)、控制 (Control)、生长 (Growth) 四类系统节点
- **Worker 动态派生**:根据任务需求动态拉起 Ordinary Skill 类型的 Worker Individual - **Worker 动态派生**:根据任务需求拉起 Ordinary / Skill / Special 三种 Worker 个体
- **强类型通信**:基于 Pydantic-AI 将 LLM 输出约束为结构化数据,避免多 Agent 协作中的非结构化文本黑盒
### 🚀 分布式性能保障 ### 🚀 分布式执行
- **Ray 驱动**:跨进程、跨机器的 Actor 通讯,轻松应对高并发任务流 - **Ray Actor 模型**:跨进程、跨机器协作,支持高并发任务流
- **本地化优先**:深度适配 vLLM,支持本地私有化模型部署 - **异构资源标签**`kilostar_node_cpu` / `core` / `gpu` 调度不同 Worker 到合适节点
- **standalone 模式**:单机零依赖起步,与分布式模式共享同一套业务代码
### 🔄 工作流引擎 ### 🔄 工作流引擎
- **pydantic-graph 驱动**:基于有向图的工作流编排,支持条件分支与循环 - **pydantic-graph 驱动**:基于有向图的工作流编排,支持条件分支与循环
- **跨进程持久化**:PostgreSQL 状态快照,支持 workflow 中断后恢复(resume - **跨进程持久化**:PostgreSQL 状态快照,支持 workflow 中断后恢复(resume
- **人工介入 (HITL)**:内置 HumanApproval 节点,支持审批挂起与幂等恢复 - **人工介入 (HITL)**:内置 HumanApproval 节点,支持审批挂起与幂等恢复
### 🧩 插件体系
- **工具插件**:标准 Tool 调用,支持 MCP 协议接入第三方服务
- **Skill(兼容 Anthropic Agent Skills 标准)**:通过 [viceroy](https://github.com/zhaoxi826/viceroy) 安装解析,运行时按需加载
- **重型插件(规划中)**:带独立 UI 的垂直应用包,把 KiloStar 改造成专用 Agent 平台
### 🛡️ 安全设计 ### 🛡️ 安全设计
- **JWT 鉴权**:所有 API 端点(含 SSE 事件流)均走 Bearer Token 认证 - **JWT 鉴权**:所有 API 端点(含 SSE 事件流)均走 Bearer Token 认证
- **归属校验**workflow / chat 资源严格绑定 user_id,跨用户访问返回 403 - **归属校验**workflow / chat 资源严格绑定 user_id,跨用户访问返回 403
@@ -46,6 +65,7 @@
| 项目 | 代号 | 功能 | 状态 | | 项目 | 代号 | 功能 | 状态 |
|:--|:--|:--|:--| |:--|:--|:--|:--|
| [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | 总督 | Skill 动态安装与全集群分发 | ✅ 已发布 | | [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | 总督 | Skill 动态安装与全集群分发 | ✅ 已发布 |
| [kilostar-stardomain](./subprojects/stardomain) | 星域 | Skill / 插件脚本沙箱执行 | 开发中 |
| [kilostar-thought](https://github.com/zhaoxi826/thought) | 思绪 | Agent 增强记忆系统 | 开发中 | | [kilostar-thought](https://github.com/zhaoxi826/thought) | 思绪 | Agent 增强记忆系统 | 开发中 |
--- ---
+67 -13
View File
@@ -1,16 +1,70 @@
# ROADMAP
KiloStar 各阶段的方向规划。已完成项归入 [CHANGELOG](./CHANGELOG.md)。
--- ---
## [v0.1.0Alpha] - 2026/4/28
### 未来展望:
#### 功能增加
- [ ] **完善系统插件**: 如 **RAG(检索增强生成)****沙箱** **联网搜索** ,使agent拥有更多的能力适应多样化任务需求
- [ ] **增加MCP功能**: 增加MCP,使得agent可以调用通用工具
- [ ] **完善special_individual** 使得`regulatory_node`等可以调用实现语言生成图像生成等功能
- [ ] **完善regulatory_node**: 实现`regulatory_node`对于工作流状态的访问,实现更方便的检测
- [ ] **对消息平台的对接**: 完善platform,实现对于更多消息平台的对接(如:钉钉微信等),实现在社交软件对`regulatory_node`下达命令
#### 系统优化 ## v0.1.x 系列(当前)
- [ ] **优化workflow逻辑**: 通过**graph**等设计实现更优秀的工作流调度
- [ ] **优化GSM设计**: 对于 **GSMglobal_state_machine全局状态机)** 进行重构,实现更高的并发 主线目标:补齐 v0.1.0 骨架之上的工程化与可用性短板,让平台进入"装上能用"的状态。
- [ ] **工具及skill优化**: 完善前端获取工具或skill的逻辑,实现对于skill或者tool的配置改写以及详细信息获取
- [ ] **前端优化**: 完善前端设置逻辑(如:调节语言等),以及使前端更加灵活智能 ### 已完成(截至 v0.1.1-alpha
- **多 Agent 编排能力线**:人设模板、节点调度标签、Worker 动态派生、调控节点对话模式重构
- **工具系统重构**:以 toolset 为单位组织工具,系统预置工具集自动补种,Agent 工具集多选绑定
- **MCP 完整接入**:前端 CRUD、Dockerfile Node.js、后端 API 全链路落地,可作为标准 MCP 客户端调用第三方 MCP 服务器
- **Provider UX 重做**5 种 Provider 类型、品牌图标、默认 URL、Test Connection、API key 脱敏
- **沙箱执行子项目**stardomain 落地(local + Docker 双模式)
- **基础安全**:JWT 鉴权、资源归属校验、fetch-based SSE、生产密钥强制校验
### 计划中
#### 平台体验
- **重型插件机制**:定义包格式(manifest + frontend + backend + tools + agent 配置)、挂载协议、生命周期管理;为后续生态铺路
- **Skill 工程化**:兼容 Anthropic Agent Skills 标准的同时,补充 KiloStar 自己的 SkillManifest 抽象(依赖识别、文件分类、执行模式声明)
- **Provider 模型调用参数体系**:贯通 temperature / top_p / 自定义 headers / 超时 等模型调用参数,统一前后端
- **前端 Tauri 桌面端**:把当前 Web 前端打包为 Tauri 桌面客户端,承载需要本地能力的功能
#### 模型与 Agent 能力
- **本地微调模型集成**:vLLM 适配深化,支持把本地微调小模型部署为 Agent 节点
- **special_individual 完善**embedding / TTS / 图像生成等特殊 Agent 标准化接入
- **regulatory_node 工作流可见性**:监管节点对工作流执行状态的访问与干预能力
#### 系统性优化
- **GSM 写入路径优化**:单 Actor 写串行化的瓶颈处理,方向上倾向"PG 为真相之源 + GSM 退化为热缓存"
- **workflow 引擎深化**:基于 pydantic-graph 的更复杂调度模式(嵌套子流、并行分支汇聚等)
- **可观测性**:跨节点 trace 串联、workflow 执行可视化、日志检索体验
---
## v0.2.x 系列(中期)
围绕"通用 Agent 平台"的关键缺口。
- **重型插件生态**:第一批官方重型插件示例 + 第三方插件开发文档
- **Skill 分发与缓存层**:跨节点的 Skill 同步策略,按需拉取 + 本地缓存 + 哈希去重
- **多镜像部署**:拆分 core / worker / gpu / standalone 等多镜像,按场景组合
- **消息平台对接**:完善 platform 模块,支持钉钉 / 微信 / Slack 等平台的接入
- **persona 外键化**:人设统一为外键引用,消除 system_prompt 的数据冗余
---
## v0.3.x 系列及之后(远期)
- **特殊 Agent 生态**embedding / 多模态 / 语音 / 图像生成等专项 Agent 的标准化接入
- **生长机制**growth_node 真正实现集群与子个体的自适应扩张
- **微调模型工具链**:与 unsloth / axolotl 等微调框架的集成路径,把"训练 → 部署 → 接入"做成顺滑流程
- **多用户多租户**:从单实例多用户演进到真正的多租户隔离
- **联邦化部署**:跨组织的 Agent 协作与资源借用机制
---
## 不在路线图中
KiloStar 不计划自己做:
- 任何具体垂直场景的 Agent 产品(编程助手、英语学习、数据分析等都应通过重型插件实现)
- 闭源模型的深度定制
- 自研推理引擎(继续依托 vLLM / llama.cpp 等成熟方案)
> 路线图按版本节奏组织,但实际推进顺序会根据使用反馈调整。重大方向变化会在此文档留痕。
+94 -26
View File
@@ -1,34 +1,102 @@
## kilostar项目 # KiloStar 项目介绍
#### 简介 ## 简介
**kilostar**是一款python开发,实现将小模型进行微调后整理为一个大型集群,从而实现低算力情况下高复杂度任务的实现。
系统模型分为以下部分:
- **监管节点**:负责基本交流和简单任务执行;
- **管控节点**:负责调度系统资源;
- **意识节点**:负责复杂任务的处理;
- **生长节点**:负责扩张集群和子个体;
- **特殊子个体**:与外界交互的模型,如embedding模型,tts模型等;
- **专家子个体**:携带有专业skill的agent对象;
- **基础子个体**:普通的agent对象;
---
#### 项目介绍
**kilostar** 是一款基于分布式计算平台 **Ray** 和 agent开发框架**pydantic-AI** 开发的多智能体协作平台,通过多智能体的协作和任务拆解,实现复杂任务的高质量完成。
**kilostar**使用 **python**著名的高性能后端框架 **Fastapi** 来作为整个系统对用户暴露接口的网关。在**kilostar**运行过程中,用户通过发送请求至fastapi从而包装为 `kilostarEvent`对象,并且发往`regulatory_node`,由**regulatory_node**进行简单的意图判断,如果判断用户只是简单交流比如聊天等,**regulatory_node**会直接对用户进行回复结束事件 **KiloStar(千星)** 是一个开源的通用多 Agent 协作平台,提供从模型接入、Agent 编排、工作流执行到插件扩展的完整能力栈。系统基于 [Ray](https://www.ray.io/) 实现分布式执行,基于 [Pydantic-AI](https://ai.pydantic.dev/) 提供类型安全的 Agent 开发框架,并通过 [FastAPI](https://fastapi.tiangolo.com/) 网关对外暴露统一接口
,如果判断用户想要完成复杂的任务,**regulatory_node**会选择将从`workflow_template(工作流模板)`中选择一个或者不选择,然后将event挂到`全局状态机`实现追溯方便并发往`Workflow_Running_Engine`的异步队列,被协程对象取走后,由**consciousness_node**创建为`kilostarWorkflow`对象,挂载到实例化的`WorkflowEngine`进行执行。完成任务后返回给用户。
平台同时支持云端 API 模型与本地微调模型,内置多 Agent 协作的核心系统节点,并通过**重型插件**机制允许使用者把平台改造成面向具体场景的专用 Agent 应用。
--- ---
#### 技术架构背景
- 分布式大脑:利用 Ray 框架实现 Actor 模型,将不同的智能体节点(Node)部署为独立运行的分布式 Actor,具备跨节点通信和动态调度的能力。 ## 项目特色
- 强类型通信协议:引入 PydanticAI 作为智能体开发框架,核心目的在于将大语言模型(LLM)产生的非结构化文本,通过 Pydantic 模型转化为强类型的结构化数据(JSON),确保多智能体协作时数据传输的工业级稳定性。
- 推理驱动路由:系统针对最新的**deepseek-v4**系列进行了适配,实现灵活调用 1. **本地微调小模型一等公民**:内置 vLLM 适配,本地微调模型在调用层与云端 API 模型对等,使用者可以为不同 Agent 节点绑定不同的本地模型。
2. **重型插件机制**:插件可附带独立前端页面、工具组与 API 接口,将 KiloStar 改造为编程辅助、学习助手、数据分析等专用 Agent 应用。
3. **多 Agent 协作内核**:监管 / 意识 / 控制 / 生长四类系统节点 + 动态派生的 Worker 个体,原生支持任务拆解、调度、监督的分工模式。
4. **standalone / distributed 双模式**:单机零依赖起步,集群按需横向扩展,业务代码在两种模式下完全一致。
5. **私有化部署友好**:所有组件可在用户自有环境内运行,不强制依赖任何第三方服务。
--- ---
#### 项目背景
###### 1.多智能体架构的需求
随着任务复杂度的提升,单一**Agent**一定程度上以及满足不了人们对于人工智能完成复杂任务的需求。模仿人类社会中的团队合作,kilostar以**Ray**作为底座,从而实现一种多智能体协作的设计。
###### 2.对于大语言模型输出内容约束的需求
LLM 输出的非结构化文本在多智能体交互中极易崩溃。所以,**kilostar**没有选择如**LangChain**这种老牌智能体开发框架,而是选择了新兴的**pydanticAI**这种强约束框架,使得多智能体协作避免黑盒化。
**PydanticAI**是一款基于**Pydantic**的智能体开发框架,**Pydantic**是**python**中著名的数据类型约束库,**Pydantic**官方通过**Pydantic**的强约束,实现了对于LLM的生成约束。
## 系统组成
### 节点体系
| 节点类型 | 职责 |
|:--|:--|
| **Regulatory Node** | 监管节点。承担用户对话入口、意图判断、对话模式的工具调用,以及对工作流执行结果的监督 |
| **Consciousness Node** | 意识节点。负责复杂任务的拆解、规划与工作流构建 |
| **Control Node** | 控制节点。负责工作流执行过程中的路由调度与状态监控 |
| **Growth Node** | 生长节点。负责集群与子个体的扩张 |
| **Worker Individual** | Worker 个体。包括 Ordinary(基础 Agent)、Skill(携带专业技能的 Agent)、Special(与外部世界交互的特殊 Agent,例如 embedding / TTS |
### 模块布局
```
KiloStar/
├── kilostar/
│ ├── api/ # FastAPI 路由层
│ ├── core/
│ │ ├── individual/ # 各类系统节点实现
│ │ ├── work/ # 工作流 / 对话 / 任务执行层
│ │ ├── global_state_machine/ # 全局状态机(Provider / Config 等)
│ │ ├── global_workflow_manager/ # 工作流消息队列 Actor
│ │ └── postgres_database/ # PostgreSQL DAO 层
│ ├── adapter/ # 模型适配器
│ ├── plugin/ # 工具插件
│ ├── worker_cluster/ # Worker 集群管理
│ ├── worker_individual/ # Worker 个体生命周期
│ └── utils/ # 鉴权 / Ray 句柄 / 配置等
├── frontend/ # React + Vite 前端
├── subprojects/ # Rust 子项目(viceroy / stardomain
├── alembic/ # 数据库迁移
├── tests/ # 单元 + 集成测试
└── docs/ # 设计文档
```
---
## 技术栈
| 层级 | 选型 | 说明 |
|:--|:--|:--|
| 分布式运行时 | **Ray** | Actor 模型、跨进程跨机器通信、自定义资源调度 |
| Agent 框架 | **Pydantic-AI** | LLM 输出强类型约束,避免多 Agent 协作中的非结构化文本黑盒 |
| Web 网关 | **FastAPI + Ray Serve** | 异步 HTTP 网关与 SSE 事件流 |
| 工作流引擎 | **pydantic-graph** | 有向图工作流编排,支持条件分支、循环、人工介入 |
| 状态持久化 | **PostgreSQL + Alembic** | workflow 中断恢复、Agent 配置、Provider 注册等 |
| 前端 | **React 19 + Vite + Tailwind + Zustand** | TypeScript 单页应用 |
| 子项目 | **Rust** | viceroySkill 安装)、stardomain(沙箱执行) |
---
## 运行模式
### standalone 单机模式
通过 `KILOSTAR_MODE=standalone` 启动,所有 Actor 退化为普通 Python 异步实例,无需安装 Ray,适合个人使用与开发调试。
### distributed 分布式模式
默认模式。Ray 启动后将系统节点部署为命名 ActorWorkerCluster 按 `kilostar_node_cpu` / `core` / `gpu` 自定义资源调度到对应物理节点,支持跨机器横向扩展。
两种模式共享同一套业务代码,通过 `kilostar.utils.standalone_proxy` 在 Actor Handle 层做透明适配。
---
## 安全设计
- **JWT 鉴权**:所有 API 端点(含 SSE 事件流)均走 Bearer Token 认证
- **资源归属校验**workflow / chat 等用户资源严格绑定 user_id,跨用户访问返回 403
- **fetch-based SSE**Token 走 `Authorization` header,不暴露在 URL 中
- **生产模式密钥校验**:未提供有效 SECRET_KEY 时拒绝以 production 模式启动
---
## 生态子项目
| 项目 | 代号 | 功能 | 状态 |
|:--|:--|:--|:--|
| [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | 总督 | Skill 动态安装与全集群分发 | ✅ 已发布 |
| [kilostar-stardomain](../subprojects/stardomain) | 星域 | Skill / 插件脚本沙箱执行 | 开发中 |
| [kilostar-thought](https://github.com/zhaoxi826/thought) | 思绪 | Agent 增强记忆系统 | 开发中 |
@@ -0,0 +1,82 @@
interface ProviderIconProps {
type: string;
size?: number;
className?: string;
}
export function ProviderIcon({ type, size = 18, className = '' }: ProviderIconProps) {
const props = {
width: size,
height: size,
viewBox: '0 0 24 24',
fill: 'currentColor',
className,
};
switch (type?.toLowerCase()) {
case 'openai':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/>
</svg>
);
case 'claude':
case 'anthropic':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M4.709 15.955l4.72-2.647.079-.23-.079-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.328h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312z"/>
</svg>
);
case 'deepseek':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.846-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"/>
</svg>
);
case 'gemini':
case 'google':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12"/>
</svg>
);
case 'local':
case 'ollama':
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="12" rx="2" />
<path d="M7 20h10M9 16v4M15 16v4" />
<circle cx="8" cy="10" r="1" fill="currentColor" />
<circle cx="12" cy="10" r="1" fill="currentColor" />
<circle cx="16" cy="10" r="1" fill="currentColor" />
</svg>
);
default:
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3" />
<path d="M9 9h6v6H9z" />
</svg>
);
}
}
const PROVIDER_BRAND_COLORS: Record<string, string> = {
openai: '#10a37f',
claude: '#d97757',
anthropic: '#d97757',
deepseek: '#4d6bfe',
gemini: '#4285f4',
google: '#4285f4',
local: '#8a8a8a',
ollama: '#8a8a8a',
};
export function getProviderBrandColor(type: string): string {
return PROVIDER_BRAND_COLORS[type?.toLowerCase()] || '#8a8a8a';
}
@@ -1,17 +1,51 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Plus, X, Server, Loader2, Boxes } from 'lucide-react'; import { Plus, X, Loader2, Boxes, Zap, ChevronDown, ChevronRight, Settings as SettingsIcon } from 'lucide-react';
import type { Provider } from '../../types'; import type { Provider } from '../../types';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import { ProviderIcon, getProviderBrandColor } from './ProviderIcon';
interface ProviderTypeOption {
id: string;
iconKey: string;
backendType: 'openai' | 'claude' | 'deepseek' | 'gemini';
defaultUrl: string;
descKey: string;
nameKey: string;
}
const PROVIDER_TYPES: ProviderTypeOption[] = [
{ id: 'openai', iconKey: 'openai', backendType: 'openai', defaultUrl: 'https://api.openai.com/v1', nameKey: 'agent.providerTypeOpenai', descKey: 'agent.providerTypeOpenaiDesc' },
{ id: 'openai_compat', iconKey: 'openai', backendType: 'openai', defaultUrl: '', nameKey: 'agent.providerTypeOpenaiCompat', descKey: 'agent.providerTypeOpenaiCompatDesc' },
{ id: 'anthropic', iconKey: 'claude', backendType: 'claude', defaultUrl: 'https://api.anthropic.com', nameKey: 'agent.providerTypeAnthropic', descKey: 'agent.providerTypeAnthropicDesc' },
{ id: 'gemini', iconKey: 'gemini', backendType: 'gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta', nameKey: 'agent.providerTypeGemini', descKey: 'agent.providerTypeGeminiDesc' },
{ id: 'deepseek', iconKey: 'deepseek', backendType: 'deepseek', defaultUrl: 'https://api.deepseek.com/v1', nameKey: 'agent.providerTypeDeepseek', descKey: 'agent.providerTypeDeepseekDesc' },
];
function detectTypeFromProvider(p: Provider): string {
if (p.provider_type === 'openai') {
return p.provider_url?.includes('api.openai.com') ? 'openai' : 'openai_compat';
}
if (p.provider_type === 'claude') return 'anthropic';
return p.provider_type || 'openai';
}
export function ProvidersSettings() { export function ProvidersSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
const [providers, setProviders] = useState<Provider[]>([]); const [providers, setProviders] = useState<Provider[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); const [editingProvider, setEditingProvider] = useState<string | null>(null);
const [selectedTypeId, setSelectedTypeId] = useState<string>('openai');
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '' });
const [showAdvanced, setShowAdvanced] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false);
const [testLoading, setTestLoading] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0];
const fetchProviders = async () => { const fetchProviders = async () => {
setLoading(true); setLoading(true);
@@ -19,7 +53,7 @@ export function ProvidersSettings() {
const response = await apiClient.get('/api/v1/provider/list'); const response = await apiClient.get('/api/v1/provider/list');
setProviders(Object.values(response.data.provider_list || {})); setProviders(Object.values(response.data.provider_list || {}));
} catch (error) { } catch (error) {
console.error("Failed to fetch providers", error); console.error('Failed to fetch providers', error);
setProviders([]); setProviders([]);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -28,6 +62,72 @@ export function ProvidersSettings() {
useEffect(() => { fetchProviders(); }, []); useEffect(() => { fetchProviders(); }, []);
const openAddModal = () => {
setEditingProvider(null);
setSelectedTypeId('openai');
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '' });
setError('');
setTestResult(null);
setShowAdvanced(false);
setIsModalOpen(true);
};
const openEditModal = (provider: Provider) => {
const typeId = detectTypeFromProvider(provider);
setEditingProvider(provider.provider_title);
setSelectedTypeId(typeId);
setFormData({
provider_title: provider.provider_title,
provider_url: provider.provider_url || '',
provider_apikey: '',
custom_models: '',
});
setError('');
setTestResult(null);
setShowAdvanced(false);
setIsModalOpen(true);
};
const handleSelectType = (typeId: string) => {
setSelectedTypeId(typeId);
if (!editingProvider) {
const tp = PROVIDER_TYPES.find((p) => p.id === typeId);
if (tp) setFormData((prev) => ({ ...prev, provider_url: tp.defaultUrl }));
}
setTestResult(null);
};
const buildPayload = () => {
const customModels = formData.custom_models
.split(',').map((s) => s.trim()).filter(Boolean);
const payload: any = {
provider_type: selectedType.backendType,
provider_title: formData.provider_title,
provider_url: formData.provider_url,
provider_apikey: formData.provider_apikey,
};
if (customModels.length > 0) payload.custom_models = customModels;
return payload;
};
const handleTestConnection = async () => {
if (!formData.provider_url || !formData.provider_apikey) {
setError(t('agent.providerFillUrlAndKey'));
return;
}
setTestLoading(true);
setTestResult(null);
setError('');
try {
const resp = await apiClient.post('/api/v1/provider/test', buildPayload());
setTestResult(resp.data);
} catch {
setTestResult({ success: false, error: 'Request failed' });
} finally {
setTestLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!formData.provider_title || !formData.provider_url || !formData.provider_apikey) { if (!formData.provider_title || !formData.provider_url || !formData.provider_apikey) {
@@ -37,9 +137,13 @@ export function ProvidersSettings() {
setSubmitLoading(true); setSubmitLoading(true);
setError(''); setError('');
try { try {
await apiClient.post('/api/v1/provider', formData); if (editingProvider) {
await apiClient.delete(`/api/v1/provider/${editingProvider}`);
}
await apiClient.post('/api/v1/provider', buildPayload());
await fetchProviders(); await fetchProviders();
setIsModalOpen(false); setIsModalOpen(false);
setEditingProvider(null);
} catch (err) { } catch (err) {
setError(t('agent.providerAddFailed')); setError(t('agent.providerAddFailed'));
} finally { } finally {
@@ -54,7 +158,7 @@ export function ProvidersSettings() {
<h3 className="text-lg font-bold text-text-primary">{t('agent.providerManagement')}</h3> <h3 className="text-lg font-bold text-text-primary">{t('agent.providerManagement')}</h3>
<p className="text-sm text-text-muted mt-0.5">{t('agent.providerDesc')}</p> <p className="text-sm text-text-muted mt-0.5">{t('agent.providerDesc')}</p>
</div> </div>
<button onClick={() => { setFormData({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); setError(''); setIsModalOpen(true); }} <button onClick={openAddModal}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium"> className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
<Plus size={14} /> {t('agent.addProvider')} <Plus size={14} /> {t('agent.addProvider')}
</button> </button>
@@ -76,25 +180,43 @@ export function ProvidersSettings() {
<div key={i} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover"> <div key={i} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center"> <div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center" style={{ color: getProviderBrandColor(provider.provider_type) }}>
<Box size={18} className="text-text-secondary" /> <ProviderIcon type={provider.provider_type} size={20} />
</div> </div>
<div> <div>
<h4 className="font-semibold text-sm text-text-primary">{provider.provider_title}</h4> <h4 className="font-semibold text-sm text-text-primary">{provider.provider_title}</h4>
<span className="text-[10px] text-text-muted font-mono uppercase">{provider.provider_type}</span> <span className="text-[10px] text-text-muted font-mono uppercase">{provider.provider_type}</span>
</div> </div>
</div> </div>
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}> <span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' || provider.provider_status === 'up' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
{provider.status === 'Connected' && <span className="w-1 h-1 rounded-full bg-success" />} {(provider.status === 'Connected' || provider.provider_status === 'up') && <span className="w-1 h-1 rounded-full bg-success" />}
{provider.status || t('common.unknown')} {provider.provider_status === 'up' ? 'Connected' : provider.status || t('common.unknown')}
</span> </span>
</div> </div>
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-4"> <div className="bg-bg-secondary rounded-lg px-3 py-2 mb-3">
<p className="text-[10px] text-text-muted mb-0.5">{t('agent.endpoint')}</p> <p className="text-[10px] text-text-muted mb-0.5">{t('agent.endpoint')}</p>
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || t('common.default')}</p> <p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || t('common.default')}</p>
</div> </div>
{provider.provider_models && provider.provider_models.length > 0 && (
<div className="mb-3">
<button
onClick={() => setExpandedProvider(expandedProvider === provider.provider_title ? null : provider.provider_title)}
className="flex items-center gap-1.5 text-[11px] text-text-muted hover:text-accent transition-colors"
>
{expandedProvider === provider.provider_title ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
{provider.provider_models.length} {t('agent.providerModels')}
</button>
{expandedProvider === provider.provider_title && (
<div className="mt-2 bg-bg-secondary rounded-lg p-2 max-h-32 overflow-y-auto">
{provider.provider_models.map((model: string) => (
<div key={model} className="text-[11px] font-mono text-text-secondary py-0.5 px-1.5">{model}</div>
))}
</div>
)}
</div>
)}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button> <button onClick={() => openEditModal(provider)} className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button>
<button onClick={async () => { <button onClick={async () => {
if (!confirm(t('agent.deleteProviderConfirm'))) return; if (!confirm(t('agent.deleteProviderConfirm'))) return;
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert(t('common.deleteFailed')); } try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert(t('common.deleteFailed')); }
@@ -105,46 +227,171 @@ export function ProvidersSettings() {
</div> </div>
)} )}
{/* Modal */} {/* Modal — 2 column layout */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale"> <div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-3xl overflow-hidden border border-border-primary animate-fade-in-scale flex flex-col max-h-[90vh]">
<div className="flex justify-between items-center p-5 border-b border-border-primary"> <div className="flex justify-between items-center px-5 py-4 border-b border-border-primary shrink-0">
<div className="flex items-center gap-2"> <h3 className="text-base font-bold text-text-primary">
<Server size={16} className="text-accent" /> {editingProvider ? t('agent.editProvider') : t('agent.addNewProvider')}
<h3 className="text-base font-bold text-text-primary">{t('agent.addNewProvider')}</h3> </h3>
</div> <button onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors">
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button> <X size={18} />
</button>
</div> </div>
<form onSubmit={handleSubmit} className="p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>} <form onSubmit={handleSubmit} className="flex flex-1 overflow-hidden">
<div> {/* Left: provider type list */}
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.providerType')}</label> <div className="w-56 shrink-0 border-r border-border-primary bg-bg-secondary/40 overflow-y-auto p-3 space-y-1">
<select name="provider_type" value={formData.provider_type} onChange={(e) => setFormData({...formData, provider_type: e.target.value})} <div className="text-[10px] font-bold text-text-muted uppercase tracking-wider px-2 py-1.5">
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary"> {t('agent.providerType')}
<option value="openai">OpenAI</option> </div>
<option value="deepseek">DeepSeek</option> {PROVIDER_TYPES.map((type) => {
<option value="claude">Claude</option> const active = selectedTypeId === type.id;
<option value="local">Local</option> return (
</select> <button
</div> key={type.id}
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => ( type="button"
<div key={field}> onClick={() => handleSelectType(type.id)}
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider"> disabled={!!editingProvider}
{field === 'provider_title' ? t('agent.providerTitle') : field === 'provider_url' ? t('agent.baseUrl') : t('agent.apiKey')} className={`w-full flex items-center gap-3 px-2.5 py-2.5 rounded-lg text-left transition-all ${
</label> active
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'} ? 'bg-bg-card border border-accent/40 shadow-sm'
name={field} value={(formData as any)[field]} : 'border border-transparent hover:bg-bg-card/60'
onChange={(e) => setFormData({...formData, [field]: e.target.value})} } disabled:opacity-50 disabled:cursor-not-allowed`}
placeholder={field === 'provider_title' ? t('agent.providerTitlePlaceholder') : field === 'provider_url' ? t('agent.baseUrlPlaceholder') : t('agent.apiKeyPlaceholder')} >
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono" /> <div
className="w-8 h-8 rounded-lg bg-bg-card border border-border-primary flex items-center justify-center shrink-0"
style={{ color: getProviderBrandColor(type.iconKey) }}
>
<ProviderIcon type={type.iconKey} size={18} />
</div>
<div className="flex-1 min-w-0">
<div className={`text-xs font-semibold truncate ${active ? 'text-accent' : 'text-text-primary'}`}>
{t(type.nameKey)}
</div>
<div className="text-[10px] text-text-muted truncate">{t(type.descKey)}</div>
</div>
</button>
);
})}
</div>
{/* Right: form */}
<div className="flex-1 overflow-y-auto p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
{testResult && (
<div className={`p-3 text-sm rounded-xl border ${testResult.success ? 'bg-success-bg text-success border-success/20' : 'bg-danger-bg text-danger border-danger/20'}`}>
{testResult.success
? `${t('agent.providerTestSuccess')} · ${testResult.model_count} ${t('agent.providerModels')}`
: `${t('agent.providerTestFailed')}: ${testResult.error}`}
</div>
)}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.providerTitle')}
</label>
<input
type="text"
value={formData.provider_title}
onChange={(e) => setFormData({ ...formData, provider_title: e.target.value })}
placeholder={t('agent.providerTitlePlaceholder')}
disabled={!!editingProvider}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 disabled:opacity-50"
/>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.baseUrl')}
</label>
<input
type="text"
value={formData.provider_url}
onChange={(e) => setFormData({ ...formData, provider_url: e.target.value })}
placeholder={selectedType.defaultUrl || t('agent.baseUrlPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono"
/>
{selectedType.defaultUrl && !editingProvider && (
<p className="text-[10px] text-text-muted mt-1">
{t('agent.baseUrlHint')}: <span className="font-mono">{selectedType.defaultUrl}</span>
</p>
)}
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.apiKey')}
</label>
<input
type="password"
value={formData.provider_apikey}
onChange={(e) => setFormData({ ...formData, provider_apikey: e.target.value })}
placeholder={editingProvider ? t('agent.apiKeyEditPlaceholder') : t('agent.apiKeyPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono"
/>
</div>
{/* 参数设置 — collapsible */}
<div className="border border-border-primary rounded-xl overflow-hidden">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between px-3.5 py-2.5 bg-bg-secondary/50 hover:bg-bg-secondary transition-colors"
>
<div className="flex items-center gap-2 text-xs font-semibold text-text-secondary">
<SettingsIcon size={13} />
{t('agent.providerAdvanced')}
</div>
{showAdvanced ? <ChevronDown size={14} className="text-text-muted" /> : <ChevronRight size={14} className="text-text-muted" />}
</button>
{showAdvanced && (
<div className="p-4 space-y-3 bg-bg-card">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.providerCustomModels')}
</label>
<textarea
value={formData.custom_models}
onChange={(e) => setFormData({ ...formData, custom_models: e.target.value })}
placeholder={t('agent.providerCustomModelsPlaceholder')}
rows={3}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono resize-none"
/>
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p>
</div>
</div>
)}
</div> </div>
))}
<div className="pt-2 flex justify-end gap-2">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? t('common.saving') : t('agent.addProvider')}</button>
</div> </div>
</form> </form>
{/* Footer */}
<div className="px-5 py-4 border-t border-border-primary bg-bg-secondary/30 flex items-center justify-between shrink-0">
<button
type="button"
onClick={handleTestConnection}
disabled={testLoading}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-medium text-accent bg-accent-light hover:bg-accent/20 rounded-xl transition-colors disabled:opacity-50"
>
{testLoading ? <Loader2 size={12} className="animate-spin" /> : <Zap size={12} />}
{t('agent.testConnection')}
</button>
<div className="flex gap-2">
<button type="button" onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleSubmit}
disabled={submitLoading}
className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50"
>
{submitLoading ? t('common.saving') : editingProvider ? t('common.save') : t('agent.addProvider')}
</button>
</div>
</div>
</div> </div>
</div> </div>
)} )}
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight } from 'lucide-react'; import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight, Pause, Play } from 'lucide-react';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
interface EventLog { interface EventLog {
@@ -47,6 +47,7 @@ const STATUS_STYLES: Record<string, { bg: string; text: string }> = {
export function SystemLogsView() { export function SystemLogsView() {
const { t } = useTranslation(); const { t } = useTranslation();
const [tab, setTab] = useState<'system' | 'workflow'>('system'); const [tab, setTab] = useState<'system' | 'workflow'>('system');
const [autoRefresh, setAutoRefresh] = useState(true);
// System logs state // System logs state
const [logs, setLogs] = useState<EventLog[]>([]); const [logs, setLogs] = useState<EventLog[]>([]);
@@ -63,7 +64,7 @@ export function SystemLogsView() {
const [wfLoading, setWfLoading] = useState(false); const [wfLoading, setWfLoading] = useState(false);
const [expandedStep, setExpandedStep] = useState<number | null>(null); const [expandedStep, setExpandedStep] = useState<number | null>(null);
const fetchLogs = async () => { const fetchLogs = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -78,7 +79,7 @@ export function SystemLogsView() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [traceFilter, typeFilter, levelFilter]);
const fetchWorkflows = async () => { const fetchWorkflows = async () => {
try { try {
@@ -106,7 +107,13 @@ export function SystemLogsView() {
useEffect(() => { useEffect(() => {
if (tab === 'system') fetchLogs(); if (tab === 'system') fetchLogs();
else fetchWorkflows(); else fetchWorkflows();
}, [tab]); }, [tab, fetchLogs]);
useEffect(() => {
if (tab !== 'system' || !autoRefresh) return;
const interval = setInterval(fetchLogs, 5000);
return () => clearInterval(interval);
}, [tab, autoRefresh, fetchLogs]);
useEffect(() => { useEffect(() => {
if (selectedTrace) fetchWorkflowDetail(selectedTrace); if (selectedTrace) fetchWorkflowDetail(selectedTrace);
@@ -143,6 +150,15 @@ export function SystemLogsView() {
{t('agent.workflowLogs')} {t('agent.workflowLogs')}
</button> </button>
<div className="flex-1" /> <div className="flex-1" />
{tab === 'system' && (
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={`p-2 rounded-lg transition-all ${autoRefresh ? 'text-accent bg-accent-light' : 'text-text-muted hover:text-accent hover:bg-accent-light'}`}
title={autoRefresh ? t('agent.logPauseRefresh') : t('agent.logResumeRefresh')}
>
{autoRefresh ? <Pause size={14} /> : <Play size={14} />}
</button>
)}
<button <button
onClick={() => tab === 'system' ? fetchLogs() : fetchWorkflows()} onClick={() => tab === 'system' ? fetchLogs() : fetchWorkflows()}
disabled={loading || wfLoading} disabled={loading || wfLoading}
+25 -1
View File
@@ -151,8 +151,15 @@
"addProvider": "Add Provider", "addProvider": "Add Provider",
"noProviders": "No providers configured yet", "noProviders": "No providers configured yet",
"providerFillAll": "Please fill in all fields.", "providerFillAll": "Please fill in all fields.",
"providerFillUrlAndKey": "Please fill in URL and API Key first.",
"providerAddFailed": "Failed to add provider. Please check your inputs and try again.", "providerAddFailed": "Failed to add provider. Please check your inputs and try again.",
"deleteProviderConfirm": "Delete this provider?", "deleteProviderConfirm": "Delete this provider?",
"testConnection": "Test Connection",
"providerTestSuccess": "Connection successful",
"providerTestFailed": "Connection failed",
"providerModels": "models",
"editProvider": "Edit Provider",
"apiKeyEditPlaceholder": "Enter new API key (leave blank to keep current)",
"providerType": "Type", "providerType": "Type",
"providerTitle": "Title", "providerTitle": "Title",
"baseUrl": "Base URL", "baseUrl": "Base URL",
@@ -184,6 +191,8 @@
"logFilterAllTypes": "All event types", "logFilterAllTypes": "All event types",
"logFilterAllLevels": "All levels", "logFilterAllLevels": "All levels",
"logSearch": "Search", "logSearch": "Search",
"logPauseRefresh": "Pause auto-refresh",
"logResumeRefresh": "Resume auto-refresh",
"logLevel": "Level", "logLevel": "Level",
"logType": "Type", "logType": "Type",
"logNode": "Node", "logNode": "Node",
@@ -224,7 +233,22 @@
"customPrompt": "Custom Prompt", "customPrompt": "Custom Prompt",
"customPromptPlaceholder": "Additional content appended after the default system prompt...", "customPromptPlaceholder": "Additional content appended after the default system prompt...",
"displayName": "Display Name", "displayName": "Display Name",
"persona": "Persona" "persona": "Persona",
"providerTypeOpenai": "OpenAI",
"providerTypeOpenaiDesc": "Official OpenAI API",
"providerTypeOpenaiCompat": "OpenAI-Compatible",
"providerTypeOpenaiCompatDesc": "Self-hosted or third-party compatible",
"providerTypeAnthropic": "Anthropic",
"providerTypeAnthropicDesc": "Claude family models",
"providerTypeGemini": "Gemini",
"providerTypeGeminiDesc": "Google AI models",
"providerTypeDeepseek": "DeepSeek",
"providerTypeDeepseekDesc": "DeepSeek official API",
"baseUrlHint": "Default",
"providerAdvanced": "Parameter Settings",
"providerCustomModels": "Custom Model List",
"providerCustomModelsPlaceholder": "Comma separated, e.g. gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider."
}, },
"plugin": { "plugin": {
"toolManagement": "Toolset Center", "toolManagement": "Toolset Center",
+25 -1
View File
@@ -151,8 +151,15 @@
"addProvider": "添加供应商", "addProvider": "添加供应商",
"noProviders": "暂无已配置的供应商", "noProviders": "暂无已配置的供应商",
"providerFillAll": "请填写所有字段。", "providerFillAll": "请填写所有字段。",
"providerFillUrlAndKey": "请先填写 URL 和 API 密钥。",
"providerAddFailed": "添加供应商失败,请检查输入后重试。", "providerAddFailed": "添加供应商失败,请检查输入后重试。",
"deleteProviderConfirm": "确定要删除此供应商吗?", "deleteProviderConfirm": "确定要删除此供应商吗?",
"testConnection": "测试连接",
"providerTestSuccess": "连接成功",
"providerTestFailed": "连接失败",
"providerModels": "个模型",
"editProvider": "编辑供应商",
"apiKeyEditPlaceholder": "输入新密钥(留空保持现有密钥)",
"providerType": "类型", "providerType": "类型",
"providerTitle": "名称", "providerTitle": "名称",
"baseUrl": "基础 URL", "baseUrl": "基础 URL",
@@ -184,6 +191,8 @@
"logFilterAllTypes": "所有事件类型", "logFilterAllTypes": "所有事件类型",
"logFilterAllLevels": "所有级别", "logFilterAllLevels": "所有级别",
"logSearch": "查询", "logSearch": "查询",
"logPauseRefresh": "暂停自动刷新",
"logResumeRefresh": "恢复自动刷新",
"logLevel": "级别", "logLevel": "级别",
"logType": "类型", "logType": "类型",
"logNode": "节点", "logNode": "节点",
@@ -224,7 +233,22 @@
"customPrompt": "附加人设", "customPrompt": "附加人设",
"customPromptPlaceholder": "在默认系统提示词之后追加的自定义内容...", "customPromptPlaceholder": "在默认系统提示词之后追加的自定义内容...",
"displayName": "显示名称", "displayName": "显示名称",
"persona": "人设" "persona": "人设",
"providerTypeOpenai": "OpenAI",
"providerTypeOpenaiDesc": "官方 OpenAI API",
"providerTypeOpenaiCompat": "OpenAI 兼容",
"providerTypeOpenaiCompatDesc": "自托管或第三方兼容服务",
"providerTypeAnthropic": "Anthropic",
"providerTypeAnthropicDesc": "Claude 系列模型",
"providerTypeGemini": "Gemini",
"providerTypeGeminiDesc": "Google AI 模型",
"providerTypeDeepseek": "DeepSeek",
"providerTypeDeepseekDesc": "DeepSeek 官方 API",
"baseUrlHint": "默认地址",
"providerAdvanced": "参数设置",
"providerCustomModels": "自定义模型列表",
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单"
}, },
"plugin": { "plugin": {
"toolManagement": "工具集中心", "toolManagement": "工具集中心",
+2
View File
@@ -19,6 +19,7 @@ export interface Provider {
provider_url?: string; provider_url?: string;
provider_owner?: string; provider_owner?: string;
provider_models?: string[]; provider_models?: string[];
provider_status?: string;
status?: string; status?: string;
model?: string; model?: string;
} }
@@ -84,6 +85,7 @@ export interface WorkflowStep {
action: string; action: string;
desc: string; desc: string;
status: string; status: string;
output?: string;
agent_id?: string; agent_id?: string;
} }
+17 -6
View File
@@ -163,15 +163,15 @@ async def stream_chat_message(
request: Request, request: Request,
token_data: TokenData = Depends(Accessor.get_current_user), 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
# 存用户消息
await postgres_database.add_chat_message.remote( await postgres_database.add_chat_message.remote(
chat_id=chat_id, message=request_body.message, message_owner="user" chat_id=chat_id, message=request_body.message, message_owner="user"
) )
# 构造 MessageRequest payload
payload = MessageRequest( payload = MessageRequest(
platform="client", platform="client",
user_name=token_data.user_id, user_name=token_data.user_id,
@@ -180,9 +180,21 @@ async def stream_chat_message(
) )
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node 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) stream_task = regulatory_node.stream_working.remote(payload, token_queue)
async def event_generator(): async def event_generator():
@@ -207,7 +219,6 @@ async def stream_chat_message(
full_response = "抱歉,生成回复时出错。" full_response = "抱歉,生成回复时出错。"
yield f"data: {json.dumps({'token': full_response})}\n\n" yield f"data: {json.dumps({'token': full_response})}\n\n"
# 流结束,存入数据库
if full_response: if full_response:
await postgres_database.add_chat_message.remote( await postgres_database.add_chat_message.remote(
chat_id=chat_id, chat_id=chat_id,
+58 -1
View File
@@ -27,7 +27,7 @@ provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
class ProviderRegister(BaseModel): class ProviderRegister(BaseModel):
"""``POST /provider`` 入参:注册一个模型 Provider 的最小字段集。""" """``POST /provider`` 入参:注册一个模型 Provider 的最小字段集。"""
provider_type: Literal["openai", "claude", "deepseek"] provider_type: Literal["openai", "claude", "deepseek", "gemini"]
provider_title: str provider_title: str
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str
@@ -72,6 +72,63 @@ async def get_provider_list(
return {"provider_list": masked} 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}") @provider_router.delete("/{provider_title}")
async def delete_provider( async def delete_provider(
provider_title: str, provider_title: str,
@@ -39,6 +39,7 @@ class ConsciousnessNode:
self.logger = get_logger("consciousness_node") self.logger = get_logger("consciousness_node")
self.agent: None | Agent = None self.agent: None | Agent = None
self.locale: str = "zh"
async def create_agent( async def create_agent(
self, self,
@@ -51,6 +52,7 @@ class ConsciousnessNode:
custom_system_prompt: str | None = None, custom_system_prompt: str | None = None,
) -> None: ) -> None:
system_prompt: str = agent_prompt("consciousness_node", locale=locale, custom_system_prompt=custom_system_prompt) 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] output_type = Union[ForregulatoryNode, ForWorkflow, ForWorkflowEngine]
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
@@ -74,23 +76,43 @@ class ConsciousnessNode:
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]): async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
locale = ctx.deps.locale
prompt = system_prompt + "\n\n" prompt = system_prompt + "\n\n"
prompt += (
f"=== 当前任务上下文 ===\n" if locale == "en":
f"- 当前指令 (Command): {ctx.deps.command}\n" prompt += (
f"- 原始用户命令 (Original Command): {ctx.deps.original_command}\n" f"=== Current Task Context ===\n"
) f"- Command: {ctx.deps.command}\n"
if ctx.deps.available_skills: f"- Original User Command: {ctx.deps.original_command}\n"
prompt += "\n=== 当前可用 Skill Individual ===\n" )
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!),作为可调用的工具。\n" if ctx.deps.available_skills:
for skill in ctx.deps.available_skills: prompt += "\n=== Available Skill Individuals ===\n"
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\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: else:
prompt += "\n=== 重要:当前无可用 Worker Individual ===\n" prompt += (
prompt += "系统中当前没有注册任何 Worker Individual。在生成工作流时,你有且仅有以下两种选择:\n" f"=== 当前任务上下文 ===\n"
prompt += "1. 将步骤分配给 consciousness_node 自己完成(设置 node 为 consciousness_nodeagent_id 为 null)。\n" f"- 当前指令: {ctx.deps.command}\n"
prompt += "2. 如果任务确实需要专用工具或技能才能完成,则拒绝执行并在输出中说明需要先创建对应的 Worker。\n" f"- 原始用户命令: {ctx.deps.original_command}\n"
prompt += "绝对禁止编造不存在的 agent_id!\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 return prompt
@@ -127,7 +149,6 @@ class ConsciousnessNode:
original_command=command, available_skills=available_skills original_command=command, available_skills=available_skills
) )
# 通知 SSE 正在生成图结构(pending 队列:节点端写入 → API SSE 读取,单向下行)
global_workflow_manager = ray_actor_hook( global_workflow_manager = ray_actor_hook(
"global_workflow_manager" "global_workflow_manager"
).global_workflow_manager ).global_workflow_manager
@@ -135,7 +156,6 @@ class ConsciousnessNode:
trace_id, "正在为您构建并规划工作流任务节点,请稍候..." trace_id, "正在为您构建并规划工作流任务节点,请稍候..."
) )
# 实际构建过程
result = await self.working(payload) result = await self.working(payload)
if result and isinstance(result, ForWorkflowEngine): if result and isinstance(result, ForWorkflowEngine):
@@ -197,6 +217,7 @@ class ConsciousnessNode:
original_command=payload.original_command, original_command=payload.original_command,
command="自主分析并拆解原始命令,生成严密可执行的工作流", command="自主分析并拆解原始命令,生成严密可执行的工作流",
available_skills=payload.available_skills, available_skills=payload.available_skills,
locale=self.locale,
) )
self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)") self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)")
prompt = "根据original_command制定严密的可执行workflow" prompt = "根据original_command制定严密的可执行workflow"
@@ -207,6 +228,7 @@ class ConsciousnessNode:
deps = ConsciousnessNodeDeps( deps = ConsciousnessNodeDeps(
original_command=payload.original_command, original_command=payload.original_command,
command="完成workflow step中分配给意识节点的特定任务或指导", command="完成workflow step中分配给意识节点的特定任务或指导",
locale=self.locale,
) )
self.logger.debug( self.logger.debug(
"ConsciousnessNode: 开始处理工作流节点任务 (原生重试开启)" "ConsciousnessNode: 开始处理工作流节点任务 (原生重试开启)"
@@ -221,6 +243,7 @@ class ConsciousnessNode:
deps = ConsciousnessNodeDeps( deps = ConsciousnessNodeDeps(
original_command=payload.original_command, original_command=payload.original_command,
command="对于工作流整体执行结果进行检查,并且生成一份专业的技术性总结报告", command="对于工作流整体执行结果进行检查,并且生成一份专业的技术性总结报告",
locale=self.locale,
) )
self.logger.debug( self.logger.debug(
"ConsciousnessNode: 开始生成技术总结报告 (原生重试开启)" "ConsciousnessNode: 开始生成技术总结报告 (原生重试开启)"
@@ -28,7 +28,8 @@ class ConsciousnessNodeDeps(DepsModel):
"""ConsciousnessNode 在 pydantic-ai Agent 中使用的依赖:原始指令、当前指令以及可用 Skill 列表。""" """ConsciousnessNode 在 pydantic-ai Agent 中使用的依赖:原始指令、当前指令以及可用 Skill 列表。"""
original_command: str original_command: str
command: str command: str
available_skills: Optional[List[str]] = None available_skills: Optional[List[dict]] = None
locale: str = "zh"
class ConsciousnessNodeInput(RequestModel): class ConsciousnessNodeInput(RequestModel):
"""ConsciousnessNode 各类入参的共同基类,仅用于打 schema 标签。""" """ConsciousnessNode 各类入参的共同基类,仅用于打 schema 标签。"""
@@ -30,10 +30,9 @@ from kilostar.utils.i18n import agent_prompt
@actor_class @actor_class
class RegulatoryNode: class RegulatoryNode:
"""RegulatoryNode(监管节点):用户请求的入口路由 Actor """RegulatoryNode(监管节点):用户请求的直接对话节点
负责对消息做意图识别:闲聊 → 直接回 ``ForUser``;复杂任务 → 走 负责理解用户需求并提供回复;如果收到工作流执行报告则转化为用户友好的总结。
``ForConsciousnessNode`` 移交给意识节点;工作流回执 → 转译成对用户的总结回复。
""" """
def __init__(self) -> None: def __init__(self) -> None:
@@ -100,12 +99,9 @@ class RegulatoryNode:
f"- 用户名 (User): {ctx.deps.user_name}\n" f"- 用户名 (User): {ctx.deps.user_name}\n"
f"- 当前时间 (Time): {ctx.deps.time}\n" f"- 当前时间 (Time): {ctx.deps.time}\n"
) )
# 修改 system_prompt 变量
prompt += ( prompt += (
"\n\n注意:你必须调用且只能调用一个函数(工具)来输出结果" "\n\n注意:请基于上下文信息为用户提供准确、专业的回复"
"如果你想直接回复用户,请调用 ForUser;" "如果你有可用工具,可在需要时主动调用。"
"如果你想移交给工作流,请调用 ForConsciousnessNode。"
"严禁返回纯文本,必须使用工具格式!"
) )
if ctx.deps.error_history: if ctx.deps.error_history:
prompt += ( prompt += (
@@ -130,7 +126,7 @@ class RegulatoryNode:
"规则:\n" "规则:\n"
"1. 直接、详细地回答用户问题,像一个专业且友好的助手。\n" "1. 直接、详细地回答用户问题,像一个专业且友好的助手。\n"
"2. 如果你有可用工具,可以调用工具来辅助回答(如搜索、读文件等)。\n" "2. 如果你有可用工具,可以调用工具来辅助回答(如搜索、读文件等)。\n"
"3. 不要输出内部思考过程,不要做路由判断,不要提及 ForUser/ForConsciousnessNode 等格式\n" "3. 不要输出内部思考过程,直接给出回复内容\n"
"4. 回复应当完整、有帮助,避免过于简短。\n" "4. 回复应当完整、有帮助,避免过于简短。\n"
) )
@@ -104,7 +104,7 @@ class IndividualDatabase:
individual = results.scalar_one_or_none() individual = results.scalar_one_or_none()
if not individual: if not individual:
return False return False
session.delete(individual) await session.delete(individual)
await session.commit() await session.commit()
return True return True
@@ -90,7 +90,7 @@ class ProviderDatabase:
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
provider = await session.get(ProviderModel, provider_id) provider = await session.get(ProviderModel, provider_id)
if provider is not None: if provider is not None:
session.delete(provider) await session.delete(provider)
await session.commit() await session.commit()
@database_exception @database_exception
@@ -78,7 +78,7 @@ class AuthDatabase:
user = results.scalar_one_or_none() user = results.scalar_one_or_none()
if user is None: if user is None:
raise UserNotExistError() raise UserNotExistError()
session.delete(user) await session.delete(user)
await session.commit() await session.commit()
@database_exception @database_exception
@@ -88,7 +88,7 @@ class AuthDatabase:
user = await session.get(User, user_id) user = await session.get(User, user_id)
if user is None: if user is None:
raise UserNotExistError() raise UserNotExistError()
session.delete(user) await session.delete(user)
await session.commit() await session.commit()
@database_exception @database_exception
+31 -6
View File
@@ -223,11 +223,28 @@ class Finalize(BaseNode[WorkflowGraphState, WorkflowDeps, str]):
) -> End[str]: ) -> End[str]:
ctx.state.final_status = self.status ctx.state.final_status = self.status
await ctx.deps.update_workflow_status(ctx.state.trace_id, self.status) await ctx.deps.update_workflow_status(ctx.state.trace_id, self.status)
msg = (
"工作流执行完成!" if self.status == WorkflowStatus.COMPLETED.value:
if self.status == WorkflowStatus.COMPLETED.value summary_parts = []
else "工作流执行失败。" 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) await ctx.deps.put_pending(ctx.state.trace_id, msg)
return End(self.status) return End(self.status)
@@ -295,9 +312,13 @@ async def _execute_step(
state.logs[-1][str(state.current_step_index)] = [ state.logs[-1][str(state.current_step_index)] = [
str(datetime.datetime.now()), str(datetime.datetime.now()),
"completed", "completed",
f"成功: {step_data.get('action', '')}", output_text,
] ]
await _persist_context(ctx, status=WorkflowStatus.RUNNING.value) 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 {} logic_gate = step_data.get("logic_gate") or {}
if logic_gate.get("if_pass") == "exit": if logic_gate.get("if_pass") == "exit":
@@ -314,6 +335,10 @@ async def _execute_step(
"failed", "failed",
output_text, 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 {} logic_gate = step_data.get("logic_gate") or {}
fail_target = logic_gate.get("if_fail") fail_target = logic_gate.get("if_fail")
if fail_target and "jump_to_step_" in fail_target: 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]] = { _PROMPTS: Dict[str, Dict[str, str]] = {
"regulatory_node": { "regulatory_node": {
"zh": ( "zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【监节点 (regulatory Node)】。\n" "你叫kilostar,是一个多智能体AI助手系统中的【监节点 (Regulatory Node)】。\n"
"你是系统'前台接待''大脑皮层',负责接收用户的初始请求或工作流的最终报告。\n" "你是系统中直接面向用户的对话节点,负责理解用户需求并提供高质量的回复。\n\n"
"你的核心职责是进行【意图识别与路由】。请仔细阅读用户的请求\n" "你的核心职责:\n"
"1. 如果用户只是进行简单的问候、闲聊或查询非常基础的信息,请直接生成友好的回复,使用 ForUser 格式\n" "1. 准确理解用户的意图,提供专业、友好且有帮助的回复\n"
"2. 如果用户提出的是复杂任务(如需要编写代码、多步骤规划、数据处理等),请务必将其判定为需要工作流处理的任务," "2. 如果你有可用工具,可以主动调用工具来辅助回答(如搜索、文件操作等)。\n"
" 并使用 ForConsciousnessNode 格式将其移交意识节点处理\n" "3. 如果你收到工作流的执行报告,请将其转化为面向用户的清晰总结\n"
"3. 如果你收到的是 TerminationMessage(代表工作流已完成并生成了报告),请将报告内容转化为友好的面向用户的回复,使用 ForUser 格式\n" "4. 保持回复简洁、有结构,避免冗余信息\n"
"请保持冷静、专业,并严格遵循上述路由规则" "请保持专业、友好的沟通风格"
), ),
"en": ( "en": (
"You are kilostar, the [Regulatory Node] in a multi-agent AI assistant system.\n" "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" "You are the user-facing conversational node, responsible for understanding user needs and providing high-quality responses.\n\n"
"Your core duty is [intent recognition and routing]. Please read the user's request carefully:\n" "Your core responsibilities:\n"
"1. If the user is simply greeting, chatting, or asking very basic questions, generate a friendly reply directly in the ForUser format.\n" "1. Accurately understand user intent and provide professional, friendly, and helpful replies.\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 " "2. If tools are available, proactively use them to assist your responses (e.g., search, file operations).\n"
" and hand it over to the Consciousness Node using the ForConsciousnessNode format.\n" "3. If you receive a workflow execution report, convert it into a clear user-facing summary.\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" "4. Keep responses concise, well-structured, and free of redundancy.\n"
"Please remain calm, professional, and strictly follow the routing rules above." "Maintain a professional and friendly communication style."
), ),
}, },
"consciousness_node": { "consciousness_node": {
@@ -41,7 +41,7 @@ class OrdinaryIndividual(BaseIndividual):
self.agent.retries = 3 self.agent.retries = 3
try: try:
result = await self.agent.run(f"请执行以下任务:\n{task_event}", deps=deps) 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: except Exception as e:
logger.exception(f"OrdinaryIndividual {self.agent_id} 执行失败: {e}") logger.exception(f"OrdinaryIndividual {self.agent_id} 执行失败: {e}")
raise raise
@@ -123,7 +123,7 @@ class SkillIndividual(BaseIndividual):
deps=deps, deps=deps,
tools=tools if tools else None, tools=tools if tools else None,
) )
return {"output": result.data.output} return {"output": result.output.output}
except Exception as e: except Exception as e:
logger.exception(f"SkillIndividual {self.agent_id} 执行失败: {e}") logger.exception(f"SkillIndividual {self.agent_id} 执行失败: {e}")
raise raise
@@ -41,7 +41,7 @@ class SpecialIndividual(BaseIndividual):
self.agent.retries = 3 self.agent.retries = 3
try: try:
result = await self.agent.run(f"请执行以下任务:\n{task_event}", deps=deps) 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: except Exception as e:
logger.exception(f"SpecialIndividual {self.agent_id} 执行失败: {e}") logger.exception(f"SpecialIndividual {self.agent_id} 执行失败: {e}")
raise raise
-36
View File
@@ -118,17 +118,6 @@ async def test_delete_template_not_found(app, fake_actors):
assert r.status_code == 404 assert r.status_code == 404
@pytest.mark.asyncio
async def test_delete_builtin_template_forbidden(app, fake_actors):
pg = types.SimpleNamespace(
get_template=types.SimpleNamespace(remote=AsyncMock(return_value=_tpl(is_builtin=True)))
)
fake_actors.register("postgres_database", pg)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.delete("/api/v1/agent/template/tpl1")
assert r.status_code == 403
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delete_other_users_template_forbidden(app, fake_actors): async def test_delete_other_users_template_forbidden(app, fake_actors):
pg = types.SimpleNamespace( pg = types.SimpleNamespace(
@@ -138,28 +127,3 @@ async def test_delete_other_users_template_forbidden(app, fake_actors):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c: async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.delete("/api/v1/agent/template/tpl1") r = await c.delete("/api/v1/agent/template/tpl1")
assert r.status_code == 403 assert r.status_code == 403
@pytest.mark.asyncio
async def test_create_worker_from_template(app, fake_actors):
worker = types.SimpleNamespace(agent_id="w1")
pg = types.SimpleNamespace(
get_template=types.SimpleNamespace(remote=AsyncMock(return_value=_tpl())),
add_worker_individual=types.SimpleNamespace(remote=AsyncMock(return_value=worker)),
)
fake_actors.register("postgres_database", pg)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.post("/api/v1/agent/worker/from-template/tpl1")
assert r.status_code == 200
assert r.json()["agent_id"] == "w1"
@pytest.mark.asyncio
async def test_create_worker_from_missing_template(app, fake_actors):
pg = types.SimpleNamespace(
get_template=types.SimpleNamespace(remote=AsyncMock(return_value=None))
)
fake_actors.register("postgres_database", pg)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.post("/api/v1/agent/worker/from-template/nope")
assert r.status_code == 404
+1 -1
View File
@@ -79,7 +79,7 @@ def test_rebuild_custom_toolsets(manager: GlobalToolManager):
manager.rebuild_custom_toolsets(custom_defs) manager.rebuild_custom_toolsets(custom_defs)
assert "grp1" in manager._custom_toolsets assert "grp1" in manager._custom_toolsets
sets = manager.get_toolsets_for_scope("default") sets = manager.get_toolsets_for_scope("default")
custom_ids = [getattr(ts, "id", "") for ts in sets if "custom::" in getattr(ts, "id", "")] custom_ids = [getattr(ts, "id", "") for ts in sets if "toolset::" in getattr(ts, "id", "")]
assert any("grp1" in cid for cid in custom_ids) assert any("grp1" in cid for cid in custom_ids)
manager.rebuild_custom_toolsets({}) manager.rebuild_custom_toolsets({})
assert manager._custom_toolsets == {} assert manager._custom_toolsets == {}
+1
View File
@@ -144,6 +144,7 @@ def consciousness_instance():
from kilostar.utils.logger import get_logger from kilostar.utils.logger import get_logger
obj.logger = get_logger("consciousness_node") obj.logger = get_logger("consciousness_node")
obj.agent = None obj.agent = None
obj.locale = "zh"
return obj return obj
Generated
+2
View File
@@ -2147,6 +2147,7 @@ dependencies = [
{ name = "pretor-viceroy" }, { name = "pretor-viceroy" },
{ name = "pwdlib", extra = ["argon2", "bcrypt"] }, { name = "pwdlib", extra = ["argon2", "bcrypt"] },
{ name = "pydantic-ai" }, { name = "pydantic-ai" },
{ name = "pydantic-settings" },
{ name = "pyfiglet" }, { name = "pyfiglet" },
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "python-ulid" }, { name = "python-ulid" },
@@ -2178,6 +2179,7 @@ requires-dist = [
{ name = "pretor-viceroy", specifier = ">=0.2.0" }, { name = "pretor-viceroy", specifier = ">=0.2.0" },
{ name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" }, { name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
{ name = "pydantic-ai", specifier = ">=1.73.0" }, { name = "pydantic-ai", specifier = ">=1.73.0" },
{ name = "pydantic-settings", specifier = ">=2.0" },
{ name = "pyfiglet", specifier = ">=1.0.4" }, { name = "pyfiglet", specifier = ">=1.0.4" },
{ name = "pyjwt", specifier = ">=2.12.1" }, { name = "pyjwt", specifier = ">=2.12.1" },
{ name = "python-ulid", specifier = ">=3.1.0" }, { name = "python-ulid", specifier = ">=3.1.0" },