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.
158 lines
4.5 KiB
Rust
158 lines
4.5 KiB
Rust
//!
|
|
//! 数据持久化客户端
|
|
//!
|
|
//! 提供一个类型化的接口,用于与 `data-persistence-service` 进行通信。
|
|
//!
|
|
|
|
use crate::error::Result;
|
|
use common_contracts::{
|
|
config_models::{AnalysisTemplateSets, LlmProvidersConfig},
|
|
dtos::{
|
|
CompanyProfileDto, NewAnalysisResult, RealtimeQuoteDto, TimeSeriesFinancialBatchDto,
|
|
TimeSeriesFinancialDto,
|
|
},
|
|
};
|
|
use tracing::info;
|
|
|
|
#[derive(Clone)]
|
|
pub struct PersistenceClient {
|
|
client: reqwest::Client,
|
|
base_url: String,
|
|
}
|
|
|
|
impl PersistenceClient {
|
|
pub fn new(base_url: String) -> Self {
|
|
Self {
|
|
client: reqwest::Client::new(),
|
|
base_url,
|
|
}
|
|
}
|
|
|
|
pub async fn get_company_profile(&self, symbol: &str) -> Result<CompanyProfileDto> {
|
|
let url = format!("{}/companies/{}", self.base_url, symbol);
|
|
info!("Fetching company profile for {} from {}", symbol, url);
|
|
let dto = self
|
|
.client
|
|
.get(&url)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json::<CompanyProfileDto>()
|
|
.await?;
|
|
Ok(dto)
|
|
}
|
|
|
|
pub async fn get_financial_statements(
|
|
&self,
|
|
symbol: &str,
|
|
) -> Result<Vec<TimeSeriesFinancialDto>> {
|
|
let url = format!("{}/market-data/financial-statements/{}", self.base_url, symbol);
|
|
info!("Fetching financials for {} from {}", symbol, url);
|
|
let dtos = self
|
|
.client
|
|
.get(&url)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json::<Vec<TimeSeriesFinancialDto>>()
|
|
.await?;
|
|
Ok(dtos)
|
|
}
|
|
|
|
// --- Config Fetching Methods ---
|
|
|
|
pub async fn get_llm_providers_config(&self) -> Result<LlmProvidersConfig> {
|
|
let url = format!("{}/configs/llm_providers", self.base_url);
|
|
info!("Fetching LLM providers config from {}", url);
|
|
let config = self
|
|
.client
|
|
.get(&url)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json::<LlmProvidersConfig>()
|
|
.await?;
|
|
Ok(config)
|
|
}
|
|
|
|
pub async fn get_analysis_template_sets(&self) -> Result<AnalysisTemplateSets> {
|
|
let url = format!("{}/configs/analysis_template_sets", self.base_url);
|
|
info!("Fetching analysis template sets from {}", url);
|
|
let config = self
|
|
.client
|
|
.get(&url)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json::<AnalysisTemplateSets>()
|
|
.await?;
|
|
Ok(config)
|
|
}
|
|
|
|
// --- Data Writing Methods ---
|
|
|
|
pub async fn create_analysis_result(&self, result: NewAnalysisResult) -> Result<()> {
|
|
let url = format!("{}/analysis-results", self.base_url);
|
|
info!(
|
|
"Persisting analysis result for symbol '{}', module '{}' to {}",
|
|
result.symbol, result.module_id, url
|
|
);
|
|
self.client
|
|
.post(&url)
|
|
.json(&result)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn upsert_company_profile(&self, profile: CompanyProfileDto) -> Result<()> {
|
|
let url = format!("{}/companies", self.base_url);
|
|
info!("Upserting company profile for {} to {}", profile.symbol, url);
|
|
self.client
|
|
.put(&url)
|
|
.json(&profile)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn upsert_realtime_quote(&self, quote: RealtimeQuoteDto) -> Result<()> {
|
|
let url = format!("{}/market-data/quotes", self.base_url);
|
|
info!("Upserting realtime quote for {} to {}", quote.symbol, url);
|
|
self.client
|
|
.post(&url)
|
|
.json("e)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn batch_insert_financials(&self, dtos: Vec<TimeSeriesFinancialDto>) -> Result<()> {
|
|
if dtos.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let url = format!("{}/market-data/financials/batch", self.base_url);
|
|
let symbol = dtos[0].symbol.clone();
|
|
info!(
|
|
"Batch inserting {} financial statements for {} to {}",
|
|
dtos.len(),
|
|
symbol,
|
|
url
|
|
);
|
|
|
|
let batch = TimeSeriesFinancialBatchDto { records: dtos };
|
|
|
|
self.client
|
|
.post(&url)
|
|
.json(&batch)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|