pub mod grader;
pub mod graph;
pub mod rewriter;
mod error;
#[cfg(test)]
mod tests;
mod types;
pub use error::CRAGError;
pub use types::CRAGResult;
use lc_core::language_models::BaseChatModel;
use lc_core::tools::BaseTool;
use lc_rag::RetrieverTrait;
use graph::CRAGGraph;
pub struct CorrectiveRAGAgent<M: BaseChatModel, R: RetrieverTrait> {
llm: M,
retriever: R,
web_fallback: Option<Box<dyn BaseTool>>,
grade_threshold: f64,
retrieve_k: usize,
enable_hallucination_check: bool,
grader_llm: Option<M>,
max_context_tokens: Option<usize>,
}
impl<M: BaseChatModel, R: RetrieverTrait> CorrectiveRAGAgent<M, R> {
pub fn new(llm: M, retriever: R) -> Self {
Self {
llm,
retriever,
web_fallback: None,
grade_threshold: 0.6,
retrieve_k: 4,
enable_hallucination_check: true,
grader_llm: None,
max_context_tokens: None,
}
}
pub fn with_web_fallback(mut self, tool: Box<dyn BaseTool>) -> Self {
self.web_fallback = Some(tool);
self
}
pub fn with_grade_threshold(mut self, threshold: f64) -> Self {
self.grade_threshold = threshold.clamp(0.0, 1.0);
self
}
pub fn with_retrieve_k(mut self, k: usize) -> Self {
self.retrieve_k = k.max(1);
self
}
pub fn with_hallucination_check(mut self, enable: bool) -> Self {
self.enable_hallucination_check = enable;
self
}
pub fn with_grader_llm(mut self, llm: M) -> Self {
self.grader_llm = Some(llm);
self
}
pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
self.max_context_tokens = Some(tokens);
self
}
pub async fn invoke(&self, query: &str) -> Result<CRAGResult, CRAGError> {
let web_ref: Option<&dyn BaseTool> = self.web_fallback.as_ref().map(|b| b.as_ref());
let mut graph = CRAGGraph::new(&self.llm, &self.retriever, web_ref, self.grade_threshold)
.with_retrieve_k(self.retrieve_k)
.with_hallucination_check(self.enable_hallucination_check);
if let Some(ref grader) = self.grader_llm {
graph = graph.with_grader_llm(grader);
}
if let Some(tokens) = self.max_context_tokens {
graph = graph.with_max_context_tokens(tokens);
}
graph.run(query).await
}
pub async fn stream(
&self,
query: &str,
) -> Result<
std::pin::Pin<
Box<dyn futures_util::Stream<Item = crate::streaming::AgentStreamEvent> + Send>,
>,
CRAGError,
> {
use crate::streaming::AgentStreamEvent;
use graph::CRAGState;
let web_ref: Option<&dyn BaseTool> = self.web_fallback.as_ref().map(|b| b.as_ref());
let grade_threshold = self.grade_threshold;
let retrieve_k = self.retrieve_k;
let enable_hallucination_check = self.enable_hallucination_check;
let max_context_tokens = self.max_context_tokens;
let mut graph = CRAGGraph::new(&self.llm, &self.retriever, web_ref, grade_threshold)
.with_retrieve_k(retrieve_k)
.with_hallucination_check(enable_hallucination_check);
if let Some(ref grader) = self.grader_llm {
graph = graph.with_grader_llm(grader);
}
if let Some(tokens) = max_context_tokens {
graph = graph.with_max_context_tokens(tokens);
}
let mut events: Vec<AgentStreamEvent> = Vec::new();
let mut state = CRAGState::new(query);
events.push(AgentStreamEvent::PipelineStep {
step: "retrieving".to_string(),
detail: Some("Retrieving documents...".to_string()),
});
graph.retrieve(&mut state).await?;
events.push(AgentStreamEvent::PipelineStep {
step: "retrieved".to_string(),
detail: Some(format!("Retrieved {} documents", state.documents.len())),
});
events.push(AgentStreamEvent::PipelineStep {
step: "grading".to_string(),
detail: Some("Grading document relevance...".to_string()),
});
graph.grade_documents(&mut state).await?;
events.push(AgentStreamEvent::PipelineStep {
step: "graded".to_string(),
detail: Some(format!("Average grade score: {:.2}", state.avg_score)),
});
if state.avg_score < grade_threshold {
events.push(AgentStreamEvent::PipelineStep {
step: "correcting".to_string(),
detail: Some("Score below threshold, rewriting query...".to_string()),
});
graph.correct(&mut state).await?;
events.push(AgentStreamEvent::PipelineStep {
step: "corrected".to_string(),
detail: Some(format!("Query rewritten: {}", state.query_rewritten)),
});
}
events.push(AgentStreamEvent::PipelineStep {
step: "generating".to_string(),
detail: Some("Generating answer...".to_string()),
});
let filtered: Vec<lc_vector_stores::Document> = state
.documents
.iter()
.zip(state.grade_scores.iter())
.filter(|(_, &score)| score >= grade_threshold)
.map(|(doc, _)| doc.clone())
.collect();
let source_docs = if filtered.is_empty() {
Vec::new()
} else {
filtered
};
let reasoning_section = graph::format_reasoning(&state.grade_reasoning);
graph
.generate(&mut state, &source_docs, &reasoning_section)
.await?;
if enable_hallucination_check && state.answer.is_some() {
events.push(AgentStreamEvent::PipelineStep {
step: "hallucination_check".to_string(),
detail: Some("Checking answer grounding...".to_string()),
});
let _ = graph.hallucination_check(&mut state, &source_docs).await;
}
events.push(AgentStreamEvent::FinalAnswer {
content: state.answer.unwrap_or_default(),
});
Ok(Box::pin(futures_util::stream::iter(events)))
}
}