use crate::provider::LlmProvider;
use async_trait::async_trait;
use std::sync::{Arc, OnceLock};
use super::{
InfoPiece, ResearchConfig, ResearchPhase, ResearchProgress, ResearchReport, ScoringConfig,
SuggestionType,
};
fn url_regex() -> &'static regex::Regex {
static INSTANCE: OnceLock<regex::Regex> = OnceLock::new();
INSTANCE.get_or_init(|| regex::Regex::new(r"https?://[^\s\)]+").unwrap())
}
#[async_trait]
pub trait DeepResearchEngine: Send + Sync {
async fn research(
&self,
provider: &Arc<dyn LlmProvider>,
topic: &str,
) -> Result<ResearchReport, ResearchError>;
fn progress(&self) -> ResearchProgress;
}
#[async_trait]
pub trait StrategyTrait: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
async fn search(
&self,
provider: &Arc<dyn LlmProvider>,
topic: &str,
context: &mut ResearchContext,
) -> Result<StrategyResult, ResearchError>;
fn should_continue(&self, result: &StrategyResult) -> bool;
fn max_iterations(&self) -> Option<u32>;
fn config(&self) -> &ResearchConfig;
}
#[derive(Debug)]
pub struct ResearchContext {
pub topic: String,
pub phase: ResearchPhase,
pub collected_info: Vec<super::InfoPiece>,
pub visited_urls: Vec<String>,
pub search_history: Vec<super::SearchHistory>,
pub citations: Vec<super::Citation>,
pub state: std::collections::HashMap<String, serde_json::Value>,
}
impl ResearchContext {
pub fn new(topic: &str) -> Self {
Self {
topic: topic.to_string(),
phase: ResearchPhase::Init,
collected_info: Vec::new(),
visited_urls: Vec::new(),
search_history: Vec::new(),
citations: Vec::new(),
state: std::collections::HashMap::new(),
}
}
pub fn add_info(&mut self, info: super::InfoPiece) {
if let Some(url) = &info.source_url
&& !self.visited_urls.contains(url)
{
self.visited_urls.push(url.clone());
}
self.collected_info.push(info);
}
pub fn add_citation(&mut self, citation: super::Citation) {
if !self.citations.iter().any(|c| c.url == citation.url) {
self.citations.push(citation);
}
}
pub fn has_visited(&self, url: &str) -> bool {
self.visited_urls.contains(&url.to_string())
}
pub fn visit_url(&mut self, url: String) {
if !self.visited_urls.contains(&url) {
self.visited_urls.push(url);
}
}
pub fn set_phase(&mut self, phase: ResearchPhase) {
self.phase = phase;
}
pub fn add_search_history(&mut self, query: String, result_count: usize) {
self.search_history
.push(super::SearchHistory::new(query, result_count));
}
pub fn set_state(&mut self, key: &str, value: serde_json::Value) {
self.state.insert(key.to_string(), value);
}
pub fn get_state(&self, key: &str) -> Option<&serde_json::Value> {
self.state.get(key)
}
pub fn total_content_length(&self) -> usize {
self.collected_info.iter().map(|i| i.content.len()).sum()
}
}
#[derive(Debug)]
pub struct StrategyResult {
pub is_complete: bool,
pub new_info: Vec<super::InfoPiece>,
pub discovered_urls: Vec<String>,
pub confidence: f32,
pub search_count: u32,
pub tokens_used: u32,
}
impl Default for StrategyResult {
fn default() -> Self {
Self {
is_complete: false,
new_info: Vec::new(),
discovered_urls: Vec::new(),
confidence: 0.0,
search_count: 0,
tokens_used: 0,
}
}
}
impl StrategyResult {
pub fn complete() -> Self {
Self {
is_complete: true,
..Default::default()
}
}
pub fn complete_with(mut self, confidence: f32, tokens_used: u32, search_count: u32) -> Self {
self.is_complete = true;
self.confidence = confidence;
self.tokens_used = tokens_used;
self.search_count = search_count;
self
}
pub fn with_info(mut self, info: Vec<super::InfoPiece>) -> Self {
self.new_info = info;
self
}
pub fn with_confidence(mut self, confidence: f32) -> Self {
self.confidence = confidence;
self
}
pub fn with_tokens(mut self, tokens: u32) -> Self {
self.tokens_used = tokens;
self
}
}
#[derive(Debug, thiserror::Error)]
pub enum ResearchError {
Provider {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Tool {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Timeout,
InvalidConfig(String),
Failed {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Storage {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
impl std::fmt::Display for ResearchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResearchError::Provider { message, .. } => write!(f, "Provider 错误: {message}"),
ResearchError::Tool { message, .. } => write!(f, "工具错误: {message}"),
ResearchError::Timeout => write!(f, "超时"),
ResearchError::InvalidConfig(msg) => write!(f, "无效配置: {msg}"),
ResearchError::Failed { message, .. } => write!(f, "研究失败: {message}"),
ResearchError::Storage { message, .. } => write!(f, "存储错误: {message}"),
}
}
}
impl From<crate::error::AgentError> for ResearchError {
fn from(e: crate::error::AgentError) -> Self {
ResearchError::Failed {
message: e.to_string(),
source: Some(Box::new(e)),
}
}
}
#[async_trait]
pub trait ResearchLibrary: Send + Sync {
async fn save(&self, report: &ResearchReport) -> Result<String, ResearchError>;
async fn search(&self, query: &str) -> Result<Vec<ResearchReport>, ResearchError>;
async fn get(&self, id: &str) -> Result<Option<ResearchReport>, ResearchError>;
async fn list(&self, limit: usize) -> Result<Vec<ResearchReport>, ResearchError>;
async fn delete(&self, id: &str) -> Result<(), ResearchError>;
}
pub trait CitationHandler: Send + Sync {
fn extract_citations(&self, content: &str) -> Vec<super::Citation>;
fn format_citation(&self, citation: &super::Citation) -> String;
fn format_reference_list(&self, citations: &[super::Citation]) -> String;
}
pub struct DefaultCitationHandler;
impl DefaultCitationHandler {
pub fn new() -> Self {
Self
}
}
impl Default for DefaultCitationHandler {
fn default() -> Self {
Self::new()
}
}
impl CitationHandler for DefaultCitationHandler {
fn extract_citations(&self, content: &str) -> Vec<super::Citation> {
let mut citations = Vec::new();
for cap in url_regex().find_iter(content) {
let url = cap.as_str().to_string();
if !citations.iter().any(|c: &super::Citation| c.url == url) {
citations.push(super::Citation::new(url, "".to_string(), "".to_string()));
}
}
citations
}
fn format_citation(&self, citation: &super::Citation) -> String {
citation.format_apa()
}
fn format_reference_list(&self, citations: &[super::Citation]) -> String {
if citations.is_empty() {
return "无可用引用".to_string();
}
let mut result = String::from("## 参考来源\n\n");
for (i, citation) in citations.iter().enumerate() {
result.push_str(&format!("{}. {}\n\n", i + 1, citation.format_apa()));
}
result
}
}
pub trait StrategyFactory: Send + Sync {
fn create(&self, config: &ResearchConfig) -> Box<dyn StrategyTrait>;
}
pub struct ResearchQualityAssessor {
pub config: ScoringConfig,
pub topic_keywords: Vec<String>,
}
impl ResearchQualityAssessor {
pub fn new(config: ScoringConfig, topic_keywords: Vec<String>) -> Self {
Self {
config,
topic_keywords,
}
}
pub fn with_default(topic: &str) -> Self {
Self {
config: ScoringConfig::default(),
topic_keywords: extract_keywords(topic),
}
}
pub fn assess(
&self,
info_pieces: &[InfoPiece],
citations: &[super::Citation],
search_rounds: usize,
) -> super::ResearchQualityScore {
super::ResearchQualityScore::calculate(
info_pieces,
citations,
search_rounds,
&self.topic_keywords,
)
}
pub fn suggest(&self, score: &super::ResearchQualityScore) -> super::ResearchSuggestion {
if score.is_sufficient(self.config.quality_threshold) {
return super::ResearchSuggestion::sufficient();
}
if score.details.info_count < self.config.min_info_count {
return super::ResearchSuggestion::need_more_info(
score.details.info_count,
self.config.min_info_count,
);
}
if score.details.duplicate_ratio > self.config.duplicate_threshold {
return super::ResearchSuggestion::need_new_angle(score.details.duplicate_ratio);
}
if score.details.source_diversity < self.config.min_source_diversity {
return super::ResearchSuggestion::need_more_sources(score.details.source_diversity);
}
if score.confidence < self.config.confidence_threshold {
return super::ResearchSuggestion::need_validation(score.confidence);
}
super::ResearchSuggestion::need_more_info(
score.details.info_count,
self.config.min_info_count,
)
}
pub fn should_continue(
&self,
score: &super::ResearchQualityScore,
current_round: u32,
max_rounds: u32,
) -> bool {
if current_round >= max_rounds {
return false;
}
if score.is_sufficient(self.config.quality_threshold) {
return false;
}
true
}
pub fn get_next_search_hint(
&self,
score: &super::ResearchQualityScore,
current_topic: &str,
) -> String {
let suggestion = self.suggest(score);
let mut hints = vec![current_topic.to_string()];
if !self.topic_keywords.is_empty() {
hints.extend(self.topic_keywords.iter().take(2).cloned());
}
match suggestion.suggestion_type {
SuggestionType::NeedMoreInfo => {
hints.push("详细介绍".to_string());
hints.push("详细说明".to_string());
}
SuggestionType::NeedMoreSources => {
hints.extend(suggestion.suggested_keywords);
}
SuggestionType::NeedNewAngle => {
hints.extend(suggestion.suggested_keywords);
}
SuggestionType::NeedValidation => {
hints.push("官方".to_string());
hints.push("验证".to_string());
}
SuggestionType::Sufficient => {
return "研究已完成".to_string();
}
}
hints.join(" ")
}
}
pub(crate) fn extract_keywords(topic: &str) -> Vec<String> {
topic
.split(&[' ', ',', ',', '。', '、', '?', '?'][..])
.filter(|s| !s.is_empty() && s.len() > 1)
.take(5)
.map(|s| s.to_string())
.collect()
}