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:
+40
-13
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# KiloStar
|
# KiloStar
|
||||||
|
|
||||||
A distributed multi-agent collaboration system built with Python
|
An open-source general-purpose multi-agent collaboration platform
|
||||||
|
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://docs.ray.io/)
|
[](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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# KiloStar (千星)
|
# KiloStar (千星)
|
||||||
|
|
||||||
一款基于 Python 的分布式多 Agent 协作系统
|
开源通用多 Agent 协作平台
|
||||||
|
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://docs.ray.io/)
|
[](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
@@ -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设计**: 对于 **GSM(global_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
@@ -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** | viceroy(Skill 安装)、stardomain(沙箱执行) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 运行模式
|
||||||
|
|
||||||
|
### standalone 单机模式
|
||||||
|
|
||||||
|
通过 `KILOSTAR_MODE=standalone` 启动,所有 Actor 退化为普通 Python 异步实例,无需安装 Ray,适合个人使用与开发调试。
|
||||||
|
|
||||||
|
### distributed 分布式模式
|
||||||
|
|
||||||
|
默认模式。Ray 启动后将系统节点部署为命名 Actor,WorkerCluster 按 `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>
|
||||||
|
<button onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors">
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button>
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 overflow-hidden">
|
||||||
|
{/* Left: provider type list */}
|
||||||
|
<div className="w-56 shrink-0 border-r border-border-primary bg-bg-secondary/40 overflow-y-auto p-3 space-y-1">
|
||||||
|
<div className="text-[10px] font-bold text-text-muted uppercase tracking-wider px-2 py-1.5">
|
||||||
|
{t('agent.providerType')}
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit} className="p-5 space-y-4">
|
{PROVIDER_TYPES.map((type) => {
|
||||||
|
const active = selectedTypeId === type.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectType(type.id)}
|
||||||
|
disabled={!!editingProvider}
|
||||||
|
className={`w-full flex items-center gap-3 px-2.5 py-2.5 rounded-lg text-left transition-all ${
|
||||||
|
active
|
||||||
|
? 'bg-bg-card border border-accent/40 shadow-sm'
|
||||||
|
: 'border border-transparent hover:bg-bg-card/60'
|
||||||
|
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||||
|
>
|
||||||
|
<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>}
|
{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>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.providerType')}</label>
|
|
||||||
<select name="provider_type" value={formData.provider_type} onChange={(e) => setFormData({...formData, provider_type: e.target.value})}
|
|
||||||
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">
|
|
||||||
<option value="openai">OpenAI</option>
|
|
||||||
<option value="deepseek">DeepSeek</option>
|
|
||||||
<option value="claude">Claude</option>
|
|
||||||
<option value="local">Local</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => (
|
|
||||||
<div key={field}>
|
|
||||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||||
{field === 'provider_title' ? t('agent.providerTitle') : field === 'provider_url' ? t('agent.baseUrl') : t('agent.apiKey')}
|
{t('agent.providerTitle')}
|
||||||
</label>
|
</label>
|
||||||
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'}
|
<input
|
||||||
name={field} value={(formData as any)[field]}
|
type="text"
|
||||||
onChange={(e) => setFormData({...formData, [field]: e.target.value})}
|
value={formData.provider_title}
|
||||||
placeholder={field === 'provider_title' ? t('agent.providerTitlePlaceholder') : field === 'provider_url' ? t('agent.baseUrlPlaceholder') : t('agent.apiKeyPlaceholder')}
|
onChange={(e) => setFormData({ ...formData, provider_title: e.target.value })}
|
||||||
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" />
|
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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "工具集中心",
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,15 +76,35 @@ 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"
|
||||||
|
|
||||||
|
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 += (
|
prompt += (
|
||||||
f"=== 当前任务上下文 ===\n"
|
f"=== 当前任务上下文 ===\n"
|
||||||
f"- 当前指令 (Command): {ctx.deps.command}\n"
|
f"- 当前指令: {ctx.deps.command}\n"
|
||||||
f"- 原始用户命令 (Original Command): {ctx.deps.original_command}\n"
|
f"- 原始用户命令: {ctx.deps.original_command}\n"
|
||||||
)
|
)
|
||||||
if ctx.deps.available_skills:
|
if ctx.deps.available_skills:
|
||||||
prompt += "\n=== 当前可用 Skill Individual ===\n"
|
prompt += "\n=== 当前可用 Skill Individual ===\n"
|
||||||
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!),作为可调用的工具。\n"
|
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!)。\n"
|
||||||
for skill in ctx.deps.available_skills:
|
for skill in ctx.deps.available_skills:
|
||||||
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\n"
|
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\n"
|
||||||
else:
|
else:
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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 == {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user