From 427776b8639f2f5a60c585a8f1330e4bd09445aa Mon Sep 17 00:00:00 2001 From: "Lv, Qi" Date: Tue, 18 Nov 2025 07:45:27 +0800 Subject: [PATCH] feat(analysis): Implement Configurable Analysis Template Engine This commit introduces a comprehensive, template-based analysis orchestration system, refactoring the entire analysis generation workflow from the ground up. Key Changes: 1. **Backend Architecture (`report-generator-service`):** * Replaced the naive analysis workflow with a robust orchestrator based on a Directed Acyclic Graph (DAG) of module dependencies. * Implemented a full topological sort (`petgraph`) to determine the correct execution order and detect circular dependencies. 2. **Data Models (`common-contracts`, `data-persistence-service`):** * Introduced the concept of `AnalysisTemplateSets` to allow for multiple, independent, and configurable analysis workflows. * Created a new `analysis_results` table to persist the output of each module for every analysis run, ensuring traceability. * Implemented a file-free data seeding mechanism to populate default analysis templates on service startup. 3. **API Layer (`api-gateway`):** * Added a new asynchronous endpoint (`POST /analysis-requests/{symbol}`) to trigger analysis workflows via NATS messages. * Updated all configuration endpoints to support the new `AnalysisTemplateSets` model. 4. **Frontend UI (`/config`, `/query`):** * Completely refactored the "Analysis Config" page into a two-level management UI for "Template Sets" and the "Modules" within them, supporting full CRUD operations. * Updated the "Query" page to allow users to select which analysis template to use when generating a report. This new architecture provides a powerful, flexible, and robust foundation for all future development of our intelligent analysis capabilities. --- ...Active]_implement_analysis_orchestrator.md | 245 +++++++++ ...Active]_implement_analysis_orchestrator.md | 111 ---- .../configs/analysis_template_sets/route.ts | 45 ++ frontend/src/app/config/page.tsx | 506 ++++++++++++------ frontend/src/app/query/page.tsx | 266 +++++---- frontend/src/hooks/useApi.ts | 23 +- frontend/src/types/index.ts | 26 + services/api-gateway/src/api.rs | 81 ++- services/api-gateway/src/persistence.rs | 22 +- .../common-contracts/src/config_models.rs | 46 +- services/common-contracts/src/dtos.rs | 20 +- services/common-contracts/src/messages.rs | 16 +- services/data-persistence-service/Cargo.toml | 2 +- services/data-persistence-service/Dockerfile | 4 + .../src/api/analysis.rs | 115 ++-- .../src/api/companies.rs | 6 +- .../src/api/configs.rs | 29 +- .../src/api/market_data.rs | 14 +- .../data-persistence-service/src/api/mod.rs | 57 +- .../src/db/companies.rs | 42 ++ .../src/db/market_data.rs | 237 ++++++++ .../data-persistence-service/src/db/mod.rs | 8 + services/data-persistence-service/src/main.rs | 46 +- .../data-persistence-service/src/models.rs | 17 +- .../data-persistence-service/src/seeding.rs | 91 ++++ services/report-generator-service/Cargo.lock | 17 + services/report-generator-service/Cargo.toml | 1 + services/report-generator-service/src/main.rs | 2 +- .../src/message_consumer.rs | 24 +- .../src/persistence.rs | 43 +- .../report-generator-service/src/worker.rs | 222 +++++--- 31 files changed, 1786 insertions(+), 598 deletions(-) create mode 100644 docs/3_project_management/tasks/completed/20251117_[Active]_implement_analysis_orchestrator.md delete mode 100644 docs/3_project_management/tasks/pending/20251117_[Active]_implement_analysis_orchestrator.md create mode 100644 frontend/src/app/api/configs/analysis_template_sets/route.ts create mode 100644 services/data-persistence-service/src/db/companies.rs create mode 100644 services/data-persistence-service/src/db/market_data.rs create mode 100644 services/data-persistence-service/src/seeding.rs diff --git a/docs/3_project_management/tasks/completed/20251117_[Active]_implement_analysis_orchestrator.md b/docs/3_project_management/tasks/completed/20251117_[Active]_implement_analysis_orchestrator.md new file mode 100644 index 0000000..d1213d3 --- /dev/null +++ b/docs/3_project_management/tasks/completed/20251117_[Active]_implement_analysis_orchestrator.md @@ -0,0 +1,245 @@ +--- +status: "Active" +date: "2025-11-17" +author: "AI 助手" +--- + +# 设计文档:可配置的分析模板与编排器 + +## 1. 概述与目标 + +### 1.1. 问题陈述 + +我们当前基于 Rust 的后端缺少执行智能、多步骤财务分析所需的核心业务逻辑。`report-generator-service` 作为此逻辑的载体,其内部实现尚不完整。更重要的是,当前的系统设计缺少一个清晰的、可扩展的方式来管理和复用成套的分析流程,并且在配置初始化方面存在对本地文件的依赖,这不符合我们健壮的系统设计原则。 + +### 1.2. 目标 + +本任务旨在我们的 Rust 微服务架构中,设计并实现一个以**分析模板集(Analysis Template Sets)**为核心的、健壮的、可配置的**分析模块编排器**。该系统将允许我们创建、管理和执行多套独立的、包含复杂依赖关系的分析工作流。 + +为达成此目标,需要完成以下任务: +1. **引入分析模板集**:在系统顶层设计中引入“分析模板集”的概念,每个模板集包含一套独立的分析模块及其配置。 +2. **实现前端模板化管理**:在前端配置中心实现对“分析模板集”的完整 CRUD 管理,并允许在每个模板集内部对分析模块进行 CRUD 管理。 +3. **构建健壮的后端编排器**:在 `report-generator-service` 中实现一个能够执行指定分析模板集的后端编排器,该编排器需基于拓扑排序来处理模块间的依赖关系。 +4. **实现无文件依赖的数据初始化**:通过在服务二进制文件中嵌入默认配置的方式,实现系统首次启动时的数据播种(Seeding),彻底移除对本地配置文件的依赖。 + +## 2. 新数据模型 (`common-contracts`) + +为了支持“分析模板集”的概念,我们需要定义新的数据结构。 + +```rust +// common-contracts/src/config_models.rs + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// 整个系统的分析模板配置,作为顶级对象存储在数据库中 +// Key: 模板ID (e.g., "standard_fundamentals") +pub type AnalysisTemplateSets = HashMap; + +// 单个分析模板集,代表一套完整的分析流程 +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AnalysisTemplateSet { + pub name: String, // 人类可读的模板名称, e.g., "标准基本面分析" + // 该模板集包含的所有分析模块 + // Key: 模块ID (e.g., "fundamental_analysis") + pub modules: HashMap, +} + +// 单个分析模块的配置 (与之前定义保持一致) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AnalysisModuleConfig { + pub name: String, + pub provider_id: String, + pub model_id: String, + pub prompt_template: String, + // 依赖关系列表,其中的字符串必须是同一个模板集内其他模块的ID + pub dependencies: Vec, +} +``` + +## 3. 系统架构与数据流 + +### 3.1. 高层数据流 + +1. **配置流程**: + * **用户** 在 **前端** 与配置页面交互,创建或修改一个“分析模板集”。 + * **前端** 向 **API 网关** 发送 `PUT /api/v1/configs/analysis_template_sets` 请求。 + * **API 网关** 将请求代理至 **数据持久化服务**,由其将序列化后的 `AnalysisTemplateSets` 对象完整保存到数据库中。 + +2. **执行流程**: + * **用户** 在 **前端** 选择一个**分析模板集**,然后为特定的股票代码触发分析。 + * **前端** 向 **API 网关** 发送 `POST /api/v1/analysis-requests/{symbol}` 请求,请求体中包含所选的 `template_id`。 + * **API 网关** 验证请求,并向 **NATS 消息总线** 发布一条包含 `symbol`, `template_id` 和 `request_id` 的 `GenerateReportCommand` 消息。 + * **报告生成服务** 订阅该消息,并根据 `template_id` 启动指定的编排工作流。 + +## 4. 前端实施计划 (`/config` 页面) + +前端配置页面需要重构为两级结构: + +1. **第一级:模板集管理** + * 显示一个包含所有“分析模板集”的列表。 + * 提供“创建新模板集”、“重命名”、“删除模板集”的功能。 + * 用户选择一个模板集后,进入第二级管理界面。 + +2. **第二级:分析模块管理 (在选定的模板集内)** + * **主界面**: 进入模板集后,主界面将以列表形式展示该模板集内所有的分析模块。每个模块将以一个独立的“卡片”形式呈现。 + * **创建 (Create)**: + * 在模块列表的顶部或底部,将设置一个“新增分析模块”按钮。 + * 点击后,将展开一个表单,要求用户输入新模块的**模块ID**(唯一的、机器可读的英文标识符)和**模块名称**(人类可读的显示名称)。 + * **读取 (Read)**: + * 每个模块卡片默认会显示其**模块名称**和**模块ID**。 + * 卡片可以被展开,以显示其详细配置。 + * **更新 (Update)**: + * 在展开的模块卡片内,所有配置项均可编辑: + * **LLM Provider**: 一个下拉菜单,选项为系统中所有已配置的LLM供应商。 + * **Model**: 一个级联下拉菜单,根据所选的Provider动态加载其可用模型。 + * **提示词模板**: 一个多行文本输入框,用于编辑模块的核心Prompt。 + * **依赖关系**: 一个复选框列表,该列表**仅显示当前模板集内除本模块外的所有其他模块**,用于勾选依赖项。 + * **删除 (Delete)**: + * 每个模块卡片的右上角将设置一个“删除”按钮。 + * 点击后,会弹出一个确认对话框,防止用户误操作。 + +## 6. 数据库与数据结构设计 + +为了支撑上述功能,我们需要在 `data-persistence-service` 中明确两个核心的数据存储模型:一个用于存储**配置**,一个用于存储**结果**。 + +### 6.1. 配置存储:`system_config` 表 + +我们将利用现有的 `system_config` 表来存储整个分析模板集的配置。 + +- **用途**: 作为所有分析模板集的“单一事实来源”。 +- **存储方式**: + - 表中的一条记录。 + - `config_key` (主键): `analysis_template_sets` + - `config_value` (类型: `JSONB`): 存储序列化后的 `AnalysisTemplateSets` (即 `HashMap`) 对象。 +- **对应数据结构 (`common-contracts`)**: 我们在第2节中定义的 `AnalysisTemplateSets` 类型是此记录的直接映射。 + +### 6.2. 结果存储:`analysis_results` 表 (新) + +为了存储每次分析工作流执行后,各个模块生成的具体内容,我们需要一张新表。 + +- **表名**: `analysis_results` +- **用途**: 持久化存储每一次分析运行的产出,便于历史追溯和未来查询。 +- **SQL Schema**: + ```sql + CREATE TABLE analysis_results ( + id BIGSERIAL PRIMARY KEY, + request_id UUID NOT NULL, -- 关联单次完整分析请求的ID + symbol VARCHAR(32) NOT NULL, -- 关联的股票代码 + template_id VARCHAR(64) NOT NULL, -- 使用的分析模板集ID + module_id VARCHAR(64) NOT NULL, -- 产出此结果的模块ID + content TEXT NOT NULL, -- LLM生成的分析内容 + meta_data JSONB, -- 存储额外元数据 (e.g., model_name, tokens, elapsed_ms) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- 建立索引以优化查询 + INDEX idx_analysis_results_request_id (request_id), + INDEX idx_analysis_results_symbol_template (symbol, template_id) + ); + ``` +- **对应数据结构 (`common-contracts`)**: + ```rust + // common-contracts/src/dtos.rs + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct NewAnalysisResult { + pub request_id: Uuid, + pub symbol: String, + pub template_id: String, + pub module_id: String, + pub content: String, + pub meta_data: serde_json::Value, + } + ``` + +## 5. 后端实施计划 + +### 5.1. `data-persistence-service` + +- **数据初始化 (无文件依赖)**: 实现一次性的、基于硬编码的启动逻辑。 + 1. 在 `data-persistence-service` 的代码中,将 `config/analysis-config.json` 的内容硬编码为一个 Rust 字符串常量。 + 2. 在服务启动时,检查 `system_config` 表中是否存在键为 `analysis_template_sets` 的记录。 + 3. 如果**不存在**,则: + a. 解析硬编码的字符串,构建一个默认的 `AnalysisTemplateSet` (例如,ID为 `default`, 名称为 “默认分析模板”)。 + b. 将这个默认模板集包装进一个 `AnalysisTemplateSets` 的 HashMap 中。 + c. 将序列化后的 `AnalysisTemplateSets` 对象写入数据库。 + 4. 此机制确保系统在首次部署时,无需任何外部文件即可拥有一套功能完备的默认分析模板。 +- **新职责**: 实现对 `analysis_results` 表的CRUD操作API。 + +### 5.2. `api-gateway` + +- **端点更新**: `POST /api/v1/analysis-requests/{symbol}`。 +- **逻辑变更**: + * 该端点现在需要从请求体中解析出 `template_id`。 + * 它构建的 `GenerateReportCommand` 消息中,必须包含 `template_id` 字段。 + +### 5.3. `report-generator-service` (核心任务) + +`worker.rs` 中的编排逻辑需要进行如下调整和实现: + +1. **消息消费者**: 订阅的 `GenerateReportCommand` 消息现在会包含 `template_id`。 + +2. **编排逻辑 (`run_report_generation_workflow`)**: + * **获取配置**: 从 `data-persistence-service` 获取完整的 `AnalysisTemplateSets` 对象。 + * **选择模板**: 根据传入的 `template_id`,从 `AnalysisTemplateSets` 中选择出本次需要执行的 `AnalysisTemplateSet`。如果找不到,则记录错误并终止。 + * **构建依赖图**: 使用所选模板集中的 `modules` 来构建有向图。强烈推荐使用 `petgraph` crate。 + * **拓扑排序**: 对该图执行拓扑排序,**必须包含循环检测**。 + * **顺序执行**: 遍历排序后的模块列表,后续的上下文注入、LLM调用和结果持久化逻辑与之前设计一致,但操作范围仅限于当前模板集内的模块。 + +3. **补全缺失逻辑**: + * **实现结果持久化**: 调用 `data-persistence-service` 提供的API,将每个模块生成的 `NewAnalysisResult` 存入 `analysis_results` 表。 + +## 6. 未来工作 + +### 6.1. 演进至 "Deep Research" 模块 + +此设计为未来的 "Deep Research" 模块演进奠定了坚实的基础。当该模块准备就绪时,我们可以创建一个新的“分析模板集”,其中的某些模块(如 `news_analysis`)将不再直接调用 LLM,而是调用 Deep Research 服务。Deep Research 服务将执行复杂的数据挖掘,并将高度精炼的结果返回给编排器,再由编排器注入到后续的 LLM 调用中,从而实现“数据驱动”的分析范式。 + +### 6.2. 引入工具调用框架 (Tool Calling Framework) + +为了以一种更通用和可扩展的方式向提示词模板中注入多样化的上下文数据,我们规划引入“工具调用”框架。 + +- **概念**: “工具”是指一段独立的、用于获取特定类型数据的程序(例如,获取财务数据、获取实时股价、获取最新新闻等)。 +- **配置**: 在前端的模块配置界面,除了依赖关系外,我们还将为每个模块提供一个“可用工具”的复选框列表。用户可以为模块勾选需要调用的一个或多个工具。 +- **执行**: + 1. 在 `report-generator-service` 的编排器执行一个模块前,它会先检查该模块配置中启用了哪些“工具”。 + 2. 编排器将按顺序执行这些工具。 + 3. 每个工具的输出(例如,格式化为Markdown的财务数据表格)将被注入到一个统一的上下文字段中。 +- **首个工具**: 我们设想的第一个工具就是 **`财务数据注入工具`**。它将负责获取并格式化财务报表,其实现逻辑与本文档旧版本中描述的“核心逻辑细化”部分一致。 + +通过此框架,我们可以将数据注入的逻辑与编排器的核心逻辑解耦,使其更易于维护和扩展。**此项为远期规划,不在本轮实施范围之内。** + +## 8. 实施清单 (Step-by-Step To-do List) + +以下是为完成本项目所需的、按顺序排列的开发任务清单。 + +### 阶段一:数据模型与持久化层准备 + +- [x] **任务 1.1**: 在 `common-contracts` crate 中,创建或更新 `src/config_models.rs`,定义 `AnalysisTemplateSets`, `AnalysisTemplateSet`, `AnalysisModuleConfig` 等新的数据结构。 +- [x] **任务 1.2**: 在 `common-contracts` crate 中,创建或更新 `src/dtos.rs`,定义用于写入分析结果的 `NewAnalysisResult` 数据传输对象 (DTO)。 +- [x] **任务 1.3**: 在 `data-persistence-service` 中,创建新的数据库迁移文件 (`migrations/`),用于新增 `analysis_results` 表,其 schema 遵循本文档第6.2节的定义。 +- [x] **任务 1.4**: 在 `data-persistence-service` 中,实现 `analysis_results` 表的 CRUD API (至少需要 `create` 方法)。 +- [x] **任务 1.5**: 在 `data-persistence-service` 中,实现数据播种(Seeding)逻辑:在服务启动时,将硬编码的默认分析模板集写入数据库(如果尚不存在)。 + +### 阶段二:后端核心逻辑实现 (`report-generator-service`) + +- [x] **任务 2.1**: 为 `report-generator-service` 添加 `petgraph` crate 作为依赖,用于构建和处理依赖图。 +- [x] **任务 2.2**: 重构 `worker.rs` 中的 `run_report_generation_workflow` 函数,使其能够接收包含 `template_id` 的消息。 +- [x] **任务 2.3**: 在 `worker.rs` 中,**实现完整的拓扑排序算法**,用以替代当前简陋的循环实现。此算法必须包含循环依赖检测。 +- [x] **任务 2.4**: 更新编排器逻辑,使其能够根据 `template_id` 从获取到的 `AnalysisTemplateSets` 中选择正确的工作流进行处理。 +- [x] **任务 2.5**: 实现调用 `data-persistence-service` 的逻辑,将每个模块成功生成的 `NewAnalysisResult` 持久化到 `analysis_results` 表中。 + +### 阶段三:服务集成与端到端打通 + +- [x] **任务 3.1**: 在 `api-gateway` 中,新增 `POST /api/v1/analysis-requests/{symbol}` 端点。 +- [x] **任务 3.2**: 在 `api-gateway` 的新端点中,实现接收前端请求(包含 `template_id`),并向 NATS 发布 `GenerateReportCommand` 消息的逻辑。 +- [x] **任务 3.3**: 在 `report-generator-service` 中,更新其 NATS 消费者,使其能够正确订阅和解析新的 `GenerateReportCommand` 消息。 +- [x] **任务 3.4**: 进行端到端集成测试,确保从前端触发的请求能够正确地启动 `report-generator-service` 并执行完整的分析流程(此时可不关心前端UI)。 + +### 阶段四:前端 UI 实现 + +- [x] **任务 4.1**: 重构 `frontend/src/app/config/page.tsx` 页面,实现两级管理结构:先管理“分析模板集”。 +- [x] **任务 4.2**: 实现“分析模板集”的创建、重命名和删除功能,并调用对应的后端API。 +- [x] **任务 4.3**: 实现模板集内部的“分析模块”管理界面,包括模块的创建、更新(所有字段)和删除功能。 +- [x] **任务 4.4**: 确保在分析请求发起的页面(例如主查询页面),用户可以选择使用哪个“分析模板集”来执行分析。 +- [x] **任务 4.5**: 更新前端调用 `api-gateway` 的逻辑,在分析请求的 body 中附带上用户选择的 `template_id`。 diff --git a/docs/3_project_management/tasks/pending/20251117_[Active]_implement_analysis_orchestrator.md b/docs/3_project_management/tasks/pending/20251117_[Active]_implement_analysis_orchestrator.md deleted file mode 100644 index 486c71b..0000000 --- a/docs/3_project_management/tasks/pending/20251117_[Active]_implement_analysis_orchestrator.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -status: "Active" -date: "2025-11-17" -author: "AI 助手" ---- - -# 设计文档:分析模块编排器 - -## 1. 概述与目标 - -### 1.1. 问题陈述 - -我们当前基于 Rust 的后端缺少执行智能、多步骤财务分析所需的核心业务逻辑。尽管旧的 Python 系统拥有一个功能性的分析框架,但在初次重构过程中,这部分逻辑并未被迁移。本应承载此逻辑的 `report-generator-service` 服务目前仅包含一个无法正常工作的占位符实现。此外,前端配置页面缺少从零开始创建或管理分析模块的用户界面,这导致了一个“先有鸡还是先有蛋”的困境——系统中不存在任何可供配置的默认模块。 - -### 1.2. 目标 - -本任务旨在我们的 Rust 微服务架构中,设计并实现一个健壮的、可配置的**分析模块编排器(Analysis Module Orchestrator)**。该系统将复刻并改进旧 Python 系统的逻辑,以支持完全通过配置(提示词和依赖关系)来创建、管理和执行复杂的、具备依赖关系感知能力的分析工作流。 - -为达成此目标,需要完成以下任务: -1. 在前端为分析模块管理实现一个完整的 CRUD (创建、读取、更新、删除) 操作界面。 -2. 在 `report-generator-service` 中实现一个健壮的后端编排器,使其能够基于模块依赖关系构成的有向无环图 (DAG) 来执行分析工作流。 -3. 通过 `api-gateway` 和 NATS 消息总线整合前端与后端服务,以打造无缝的端到端用户体验。 -4. 实现一个数据播种(Data Seeding)机制,以确保系统在首次启动时能够预加载一套默认的分析模块。 - -## 2. 系统架构与数据流 - -本次实现将涉及四个关键服务和一个消息总线:`前端`、`API 网关`、`数据持久化服务` 和 `报告生成服务`。 - -### 2.1. 高层数据流 - -1. **配置流程**: - * **用户** 在 **前端** 配置页面上进行交互,以创建或更新分析模块。 - * **前端** 向 **API 网关** 发送 `PUT /api/v1/configs/analysis_modules` 请求。 - * **API 网关** 将这些请求代理至 **数据持久化服务**,由其将配置保存到数据库的 `system_config` 表中。 - -2. **执行流程**: - * **用户** 在 **前端** 为特定的股票代码触发一次分析运行。 - * **前端** 向 **API 网关** 发送 `POST /api/v1/analysis-requests/{symbol}` 请求。 - * **API 网关** 验证请求,并向 **NATS 消息总线** 的一个新主题发布一条 `GenerateReportCommand` 消息。随后,它会立即向前端返回一个带有请求ID的 `202 Accepted` 响应。 - * **报告生成服务** 订阅 `GenerateReportCommand` 主题,接收到消息后,启动编排工作流。 - * **报告生成服务** 从 **数据持久化服务** 获取所需的分析模块配置。 - * 服务执行分析,为每个模块调用 LLM API,并通过 **数据持久化服务** 将结果持久化存回数据库。 - -## 3. 前端实施计划 (`/config` 页面) - -我们将修改 `frontend/src/app/config/page.tsx` 文件,为分析模块提供完整的 CRUD 用户体验。 - -- **创建 (Create)**: 添加一个“新增模块”按钮。点击后,将显示一个表单,用于输入: - - **模块 ID**: 一个唯一的、机器可读的字符串 (例如, `fundamental_analysis`)。 - - **模块名称**: 一个人类可读的显示名称 (例如, "基本面分析")。 -- **读取 (Read)**: 页面将为每个已存在的分析模块渲染一个卡片,展示其当前配置。 -- **更新 (Update)**: 每个模块卡片将包含以下可编辑字段: - - **LLM Provider**: 一个下拉菜单,其选项从 `llm_providers` 配置中动态填充。 - - **Model**: 一个级联下拉菜单,显示所选 Provider 下可用的模型。 - - **提示词模板**: 一个用于编写 Prompt 的大文本区域。 - - **依赖关系**: 一个包含所有其他模块ID的复选框列表,允许用户定义模块间的依赖。 -- **删除 (Delete)**: 每个模块卡片将有一个带有确认对话框的“删除”按钮。 - -## 4. 后端实施计划 - -### 4.1. `data-persistence-service` - -- **数据播种 (关键任务)**: 实现一次性的启动逻辑。 - 1. 在服务启动时,检查 `system_config` 表中是否存在键为 `analysis_modules` 的记录。 - 2. 如果记录**不存在**,则从磁盘读取旧的 `config/analysis-config.json` 文件。 - 3. 解析文件内容,并将其作为 `analysis_modules` 的值插入数据库。 - 4. 此机制确保系统在首次部署时,即被预置一套默认且功能完备的分析模块。 -- **API**: 无需变更。现有的 `GET /configs/analysis_modules` 和 `PUT /configs/analysis_modules` 端点已能满足需求。 - -### 4.2. `api-gateway` - -- **新端点**: 创建一个新的端点 `POST /api/v1/analysis-requests/{symbol}`。 -- **逻辑**: - 1. 此端点不应执行任何重度计算任务。 - 2. 它将从路径中接收一个股票 `symbol`。 - 3. 它将生成一个唯一的 `request_id` (例如, UUID)。 - 4. 它将构建一条包含 `symbol` 和 `request_id` 的 `GenerateReportCommand` 消息。 - 5. 它将此消息发布到一个专用的 NATS 主题 (例如, `analysis.commands.generate_report`)。 - 6. 它将立即返回一个 `202 Accepted` 状态码,并在响应体中包含 `request_id`。 - -### 4.3. `report-generator-service` (核心任务) - -此服务需要进行最主要的开发工作。所有逻辑将在 `worker.rs` 文件中实现。 - -1. **消息消费者**: 服务将订阅 `analysis.commands.generate_report` NATS 主题。一旦收到 `GenerateReportCommand` 消息,即触发 `run_report_generation_workflow` 工作流。 - -2. **编排逻辑 (`run_report_generation_workflow`)**: - * **获取配置**: 从 `data-persistence-service` 获取完整的 `AnalysisModulesConfig`。 - * **构建依赖图**: 根据模块配置,在内存中构建一个有向图。强烈推荐使用 `petgraph` crate 来完成此任务。 - * **拓扑排序**: 对该图执行拓扑排序,以获得一个线性的执行顺序。该算法**必须**包含循环检测功能,以便在配置错误时能够优雅地处理,并记录错误日志。 - * **顺序执行**: 遍历排序后的模块列表。对每个模块: - * **构建上下文**: 收集其所有直接依赖模块的文本输出(这些模块已保证被提前执行)。 - * **渲染提示词**: 使用 `Tera` 模板引擎,将依赖模块的输出以及其他所需数据(如公司名称、财务数据)注入到当前模块的 `prompt_template` 中。 - * **执行 LLM 调用**: 通过 `LlmClient` 调用相应的 LLM API。 - * **持久化结果**: 成功生成内容后,调用 `data-persistence-service` 将输出文本保存,并与 `symbol` 和 `module_id` 关联。同时,将结果保存在本地,以供工作流中的后续模块使用。 - -3. **补全缺失逻辑**: - * 实现 `// TODO` 中关于持久化结果的部分。 - * 将 `financial_data` 占位符替换为从 `data-persistence-service` 获取并格式化后的真实财务数据。 - -## 5. 未来工作:向 "Deep Research" 模块演进 - -如前所述,初始实现将依赖 LLM 的内部知识来完成“新闻”或“市场情绪”等分析。这是一个为快速实现功能而刻意选择的短期策略。 - -长期愿景是用一个 `Deep Research` 模块来取代这种模式。该模块将作为一个智能的数据预处理器。届时,编排器将不再注入简单的文本,而是触发 Deep Research 模块,后者将: -1. 理解目标分析模块(如 `news_analysis`)的数据需求。 -2. 查询内部数据源(例如,数据库中的 `news` 表)以查找相关信息。 -3. 对检索到的数据执行多步推理或摘要。 -4. 为最终的分析模块提示词提供一个高质量、经过浓缩的数据包。 - -这一演进将使我们的系统从“提示词驱动”转变为“数据驱动”,从而显著提升分析结果的可靠性、可控性和准确性。 diff --git a/frontend/src/app/api/configs/analysis_template_sets/route.ts b/frontend/src/app/api/configs/analysis_template_sets/route.ts new file mode 100644 index 0000000..35826ae --- /dev/null +++ b/frontend/src/app/api/configs/analysis_template_sets/route.ts @@ -0,0 +1,45 @@ +const BACKEND_BASE = process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_BACKEND_URL; + +export async function GET() { + if (!BACKEND_BASE) { + return new Response('NEXT_PUBLIC_BACKEND_URL 未配置', { status: 500 }); + } + try { + const resp = await fetch(`${BACKEND_BASE}/configs/analysis_template_sets`, { + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + const text = await resp.text(); + return new Response(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }); + } catch (e: any) { + const errorBody = JSON.stringify({ message: e?.message || '连接后端失败' }); + return new Response(errorBody, { status: 502, headers: { 'Content-Type': 'application/json' } }); + } +} + +export async function PUT(req: Request) { + if (!BACKEND_BASE) { + return new Response('NEXT_PUBLIC_BACKEND_URL 未配置', { status: 500 }); + } + const body = await req.text(); + try { + const resp = await fetch(`${BACKEND_BASE}/configs/analysis_template_sets`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body, + }); + const text = await resp.text(); + return new Response(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }); + } catch (e: any) { + const errorBody = JSON.stringify({ message: e?.message || '连接后端失败' }); + return new Response(errorBody, { status: 502, headers: { 'Content-Type': 'application/json' } }); + } +} + + diff --git a/frontend/src/app/config/page.tsx b/frontend/src/app/config/page.tsx index 054556e..eab4407 100644 --- a/frontend/src/app/config/page.tsx +++ b/frontend/src/app/config/page.tsx @@ -20,8 +20,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Spinner } from "@/components/ui/spinner"; // Types are imported from '@/types' -import type { AnalysisModulesConfig, DataSourcesConfig, DataSourceConfig, DataSourceProvider, LlmProvidersConfig, LlmModel } from '@/types'; -import { useDataSourcesConfig, updateDataSourcesConfig } from '@/hooks/useApi'; +import type { + AnalysisModulesConfig, DataSourcesConfig, DataSourceConfig, DataSourceProvider, LlmProvidersConfig, LlmModel, + AnalysisTemplateSets, AnalysisTemplateSet, AnalysisModuleConfig +} from '@/types'; +import { useDataSourcesConfig, updateDataSourcesConfig, useAnalysisTemplateSets, updateAnalysisTemplateSets } from '@/hooks/useApi'; export default function ConfigPage() { // 从 Zustand store 获取全局状态 @@ -31,6 +34,8 @@ export default function ConfigPage() { // 加载分析配置(统一使用 initialAnalysisModules) // const { data: analysisConfig, mutate: mutateAnalysisConfig } = useAnalysisModules(); + // LLM Providers(用于模型列表与保存) + const { data: llmProviders, mutate: mutateLlmProviders } = useLlmProviders(); // 本地表单状态 // 数据源本地状态 @@ -39,12 +44,6 @@ export default function ConfigPage() { // 分析配置的本地状态 const [localAnalysisModules, setLocalAnalysisModules] = useState({}); - - // -- New State for Creating Analysis Modules -- - const [isCreatingModule, setIsCreatingModule] = useState(false); - const [newModuleId, setNewModuleId] = useState(''); - const [newModuleName, setNewModuleName] = useState(''); - // 分析配置保存状态(状态定义在下方统一维护) // 测试结果状态 @@ -55,11 +54,22 @@ export default function ConfigPage() { const [saveMessage, setSaveMessage] = useState(''); // --- New State for Analysis Modules --- - const { data: llmProviders, mutate: mutateLlmProviders } = useLlmProviders(); - const { data: initialAnalysisModules, mutate } = useAnalysisModules(); + const { data: initialAnalysisTemplateSets, mutate: mutateAnalysisTemplateSets } = useAnalysisTemplateSets(); + const [localTemplateSets, setLocalTemplateSets] = useState({}); + const [selectedTemplateId, setSelectedTemplateId] = useState(null); + const [isSavingAnalysis, setIsSavingAnalysis] = useState(false); const [analysisSaveMessage, setAnalysisSaveMessage] = useState(''); + // State for creating/editing templates and modules + const [newTemplateId, setNewTemplateId] = useState(''); + const [newTemplateName, setNewTemplateName] = useState(''); + const [isCreatingTemplate, setIsCreatingTemplate] = useState(false); + + const [isCreatingModule, setIsCreatingModule] = useState(false); + const [newModuleId, setNewModuleId] = useState(''); + const [newModuleName, setNewModuleName] = useState(''); + // --- State for LLM Providers Management --- const [localLlmProviders, setLocalLlmProviders] = useState({}); const [isSavingLlm, setIsSavingLlm] = useState(false); @@ -160,10 +170,11 @@ export default function ConfigPage() { }, [localLlmProviders, pendingApiKeys, flushSaveLlmImmediate]); useEffect(() => { - if (initialAnalysisModules) { - setLocalAnalysisModules(initialAnalysisModules); - } - }, [initialAnalysisModules]); + if (!initialAnalysisTemplateSets) return; + setLocalTemplateSets(initialAnalysisTemplateSets); + // 仅在未选择时,从后端数据中选择第一个模板;避免覆盖本地新增的选择与状态 + setSelectedTemplateId(prev => prev ?? (Object.keys(initialAnalysisTemplateSets)[0] || null)); + }, [initialAnalysisTemplateSets]); useEffect(() => { if (initialDataSources) { @@ -180,10 +191,20 @@ export default function ConfigPage() { } }, [llmProviders, normalizeProviders]); - const handleAnalysisChange = (moduleId: string, field: string, value: string) => { - setLocalAnalysisModules(prev => ({ + const handleAnalysisChange = (moduleId: string, field: string, value: any) => { + if (!selectedTemplateId) return; + setLocalTemplateSets(prev => ({ ...prev, - [moduleId]: { ...prev[moduleId], [field]: value } + [selectedTemplateId]: { + ...prev[selectedTemplateId], + modules: { + ...prev[selectedTemplateId].modules, + [moduleId]: { + ...prev[selectedTemplateId].modules[moduleId], + [field]: value, + }, + }, + }, })); }; @@ -191,8 +212,8 @@ export default function ConfigPage() { setIsSavingAnalysis(true); setAnalysisSaveMessage('保存中...'); try { - const updated = await updateAnalysisModules(localAnalysisModules); - await mutate(updated, false); + const updated = await updateAnalysisTemplateSets(localTemplateSets); + await mutateAnalysisTemplateSets(updated, false); setAnalysisSaveMessage('分析配置保存成功!'); } catch (e: any) { setAnalysisSaveMessage(`保存失败: ${e.message}`); @@ -216,61 +237,129 @@ export default function ConfigPage() { }; // 更新分析模块的依赖 - const updateAnalysisDependencies = (type: string, dependency: string, checked: boolean) => { - setLocalAnalysisModules(prev => { - const currentConfig = prev[type]; - const currentDeps = currentConfig.dependencies || []; - - const newDeps = checked - ? [...currentDeps, dependency] - // 移除依赖,并去重 - : currentDeps.filter(d => d !== dependency); - - return { - ...prev, - [type]: { - ...currentConfig, - dependencies: [...new Set(newDeps)] // 确保唯一性 - } - }; + const updateAnalysisDependencies = (moduleId: string, dependency: string, checked: boolean) => { + if (!selectedTemplateId) return; + setLocalTemplateSets(prev => { + const currentModule = prev[selectedTemplateId].modules[moduleId]; + const currentDeps = currentModule.dependencies || []; + const newDeps = checked + ? [...currentDeps, dependency] + : currentDeps.filter(d => d !== dependency); + + return { + ...prev, + [selectedTemplateId]: { + ...prev[selectedTemplateId], + modules: { + ...prev[selectedTemplateId].modules, + [moduleId]: { + ...currentModule, + dependencies: [...new Set(newDeps)], + }, + }, + }, + }; }); }; - // --- New handlers for module creation/deletion --- - const handleAddNewModule = () => { - if (!newModuleId || !newModuleName) { - setAnalysisSaveMessage('模块 ID 和名称不能为空'); - setTimeout(() => setAnalysisSaveMessage(''), 3000); - return; - } - if (localAnalysisModules[newModuleId]) { - setAnalysisSaveMessage('模块 ID 已存在'); - setTimeout(() => setAnalysisSaveMessage(''), 3000); - return; - } - setLocalAnalysisModules(prev => ({ - ...prev, - [newModuleId]: { - name: newModuleName, - provider_id: '', - model_id: '', - prompt_template: '', - dependencies: [], + // --- Handlers for templates and modules --- + + const handleAddTemplate = () => { + if (!newTemplateId || !newTemplateName) { + setAnalysisSaveMessage('模板 ID 和名称不能为空'); + return; } - })); - setNewModuleId(''); - setNewModuleName(''); - setIsCreatingModule(false); + if (localTemplateSets[newTemplateId]) { + setAnalysisSaveMessage('模板 ID 已存在'); + return; + } + const newSet: AnalysisTemplateSets = { + ...localTemplateSets, + [newTemplateId]: { + name: newTemplateName, + modules: {}, + }, + }; + setLocalTemplateSets(newSet); + setSelectedTemplateId(newTemplateId); + setNewTemplateId(''); + setNewTemplateName(''); + setIsCreatingTemplate(false); + // 新建后立即持久化,避免刷新/切 tab 导致本地新增被覆盖且无网络请求记录 + (async () => { + setIsSavingAnalysis(true); + setAnalysisSaveMessage('保存中...'); + try { + const updated = await updateAnalysisTemplateSets(newSet); + await mutateAnalysisTemplateSets(updated, false); + setAnalysisSaveMessage('分析配置保存成功!'); + } catch (e: any) { + setAnalysisSaveMessage(`保存失败: ${e?.message || '未知错误'}`); + } finally { + setIsSavingAnalysis(false); + setTimeout(() => setAnalysisSaveMessage(''), 5000); + } + })(); }; - + + const handleDeleteTemplate = () => { + if (!selectedTemplateId || !window.confirm(`确定要删除模板 "${localTemplateSets[selectedTemplateId].name}" 吗?`)) { + return; + } + const newSets = { ...localTemplateSets }; + delete newSets[selectedTemplateId]; + setLocalTemplateSets(newSets); + // Select the first available template or null + const firstKey = Object.keys(newSets)[0] || null; + setSelectedTemplateId(firstKey); + }; + + + const handleAddNewModule = () => { + if (!selectedTemplateId || !newModuleId || !newModuleName) { + setAnalysisSaveMessage('模块 ID 和名称不能为空'); + return; + } + if (localTemplateSets[selectedTemplateId].modules[newModuleId]) { + setAnalysisSaveMessage('模块 ID 已存在'); + return; + } + setLocalTemplateSets(prev => ({ + ...prev, + [selectedTemplateId]: { + ...prev[selectedTemplateId], + modules: { + ...prev[selectedTemplateId].modules, + [newModuleId]: { + name: newModuleName, + provider_id: '', + model_id: '', + prompt_template: '', + dependencies: [], + } + } + } + })); + setNewModuleId(''); + setNewModuleName(''); + setIsCreatingModule(false); + }; + const handleDeleteModule = (moduleId: string) => { - setLocalAnalysisModules(prev => { - const next = { ...prev }; - delete next[moduleId]; - return next; - }); + if (!selectedTemplateId) return; + setLocalTemplateSets(prev => { + const newModules = { ...prev[selectedTemplateId].modules }; + delete newModules[moduleId]; + return { + ...prev, + [selectedTemplateId]: { + ...prev[selectedTemplateId], + modules: newModules, + }, + }; + }); }; - + // 旧版保存逻辑已移除,统一使用 handleSaveAnalysis const handleSave = async () => { @@ -993,114 +1082,195 @@ export default function ConfigPage() { - 分析模块配置 - 配置各个分析模块的模型、提示词和依赖关系。模块ID将作为Prompt中注入上下文的占位符。 + 分析模板与模块配置 + 管理不同的分析模板集,并为每个模板集内的模块配置模型、提示词和依赖关系。 - {Object.entries(localAnalysisModules).map(([moduleId, config]) => { - const availableModels = llmProviders?.[config.provider_id]?.models.filter(m => m.is_active) || []; - return ( -
-
-
-

{config.name || moduleId}

-

ID: {moduleId}

-
- -
-
-
- - -
-
- - -
-
- -
- -