use crate::core::language_models::BaseChatModel;
use crate::core::tools::BaseTool;
use crate::retrieval::RetrieverTrait;
use crate::schema::Message;
use crate::vector_stores::Document;
use super::grader::DocumentGrader;
use super::rewriter::QueryRewriter;
use super::{CRAGError, CRAGResult};
#[derive(Debug, Clone)]
pub struct CRAGState {
pub query: String,
pub current_query: String,
pub documents: Vec<Document>,
pub grade_scores: Vec<f64>,
pub avg_score: f64,
pub query_rewritten: bool,
pub web_results: Option<String>,
pub answer: Option<String>,
pub grounded: bool,
}
impl CRAGState {
pub fn new(query: impl Into<String>) -> Self {
let q = query.into();
Self {
current_query: q.clone(),
query: q,
documents: Vec::new(),
grade_scores: Vec::new(),
avg_score: 0.0,
query_rewritten: false,
web_results: None,
answer: None,
grounded: true,
}
}
}
pub struct CRAGGraph<'a, M: BaseChatModel, R: RetrieverTrait> {
llm: &'a M,
retriever: &'a R,
web_fallback: Option<&'a dyn BaseTool>,
grade_threshold: f64,
retrieve_k: usize,
enable_hallucination_check: bool,
}
impl<'a, M: BaseChatModel, R: RetrieverTrait> CRAGGraph<'a, M, R> {
pub fn new(
llm: &'a M,
retriever: &'a R,
web_fallback: Option<&'a dyn BaseTool>,
grade_threshold: f64,
) -> Self {
Self {
llm,
retriever,
web_fallback,
grade_threshold,
retrieve_k: 4,
enable_hallucination_check: true,
}
}
pub fn with_retrieve_k(mut self, k: usize) -> Self {
self.retrieve_k = k;
self
}
pub fn with_hallucination_check(mut self, enable: bool) -> Self {
self.enable_hallucination_check = enable;
self
}
pub async fn run(&self, query: &str) -> Result<CRAGResult, CRAGError> {
let mut state = CRAGState::new(query);
self.retrieve(&mut state).await?;
self.grade_documents(&mut state).await?;
if state.avg_score < self.grade_threshold {
self.correct(&mut state).await?;
}
let filtered: Vec<Document> = state
.documents
.iter()
.zip(state.grade_scores.iter())
.filter(|(_, &score)| score >= self.grade_threshold)
.map(|(doc, _)| doc.clone())
.collect();
let source_docs = if filtered.is_empty() {
Vec::new()
} else {
filtered
};
self.generate(&mut state, &source_docs).await?;
if self.enable_hallucination_check && state.answer.is_some() {
self.hallucination_check(&mut state, &source_docs).await?;
}
Ok(CRAGResult {
answer: state.answer.unwrap_or_default(),
grounded: state.grounded,
sources: source_docs,
grade_scores: state.grade_scores,
})
}
async fn retrieve(&self, state: &mut CRAGState) -> Result<(), CRAGError> {
let docs = self
.retriever
.retrieve(&state.current_query, self.retrieve_k)
.await
.map_err(CRAGError::RetrievalError)?;
if docs.is_empty() {
return Err(CRAGError::NoDocumentsRetrieved);
}
state.documents = docs;
Ok(())
}
async fn grade_documents(&self, state: &mut CRAGState) -> Result<(), CRAGError> {
let grader = DocumentGrader::new(self.llm);
let results = grader
.grade_all(&state.current_query, &state.documents)
.await
.map_err(CRAGError::GradingError)?;
let scores: Vec<f64> = results.iter().map(|r| r.score).collect();
let avg = if scores.is_empty() {
0.0
} else {
scores.iter().sum::<f64>() / scores.len() as f64
};
state.grade_scores = scores;
state.avg_score = avg;
Ok(())
}
async fn correct(&self, state: &mut CRAGState) -> Result<(), CRAGError> {
let rewriter = QueryRewriter::new(self.llm);
let rewritten = rewriter
.rewrite(&state.query)
.await
.map_err(CRAGError::RewritingError)?;
state.current_query = rewritten;
state.query_rewritten = true;
if let Some(tool) = self.web_fallback {
let web_result = tool
.run(state.current_query.clone())
.await
.map_err(CRAGError::WebSearchError)?;
state.web_results = Some(web_result);
}
self.retrieve(state).await?;
self.grade_documents(state).await?;
Ok(())
}
async fn generate(
&self,
state: &mut CRAGState,
source_docs: &[Document],
) -> Result<(), CRAGError> {
let context = build_context(source_docs, state.web_results.as_deref());
let user_message = build_generate_prompt(&context, &state.query);
let messages = vec![Message::human(&user_message)];
let result = self
.llm
.chat_with_system(GENERATE_SYSTEM_PROMPT.to_string(), messages)
.await
.map_err(|e| CRAGError::GenerationError(e.to_string()))?;
state.answer = Some(result.content);
Ok(())
}
async fn hallucination_check(
&self,
state: &mut CRAGState,
source_docs: &[Document],
) -> Result<(), CRAGError> {
let answer = match &state.answer {
Some(a) => a.clone(),
None => return Ok(()),
};
let context = build_context(source_docs, state.web_results.as_deref());
let user_message = build_hallucination_check_prompt(&context, &answer);
let messages = vec![Message::human(&user_message)];
let result = self
.llm
.chat_with_system(HALLUCINATION_SYSTEM_PROMPT.to_string(), messages)
.await
.map_err(|e| CRAGError::HallucinationCheckError(e.to_string()))?;
let response_lower = result.content.to_lowercase();
let words: Vec<&str> = response_lower.split_whitespace().collect();
let not_grounded = words.windows(2).any(|w| w == ["not", "grounded"])
|| words.windows(2).any(|w| w == ["not", "supported"])
|| words.contains(&"unsupported");
if not_grounded {
state.grounded = false;
} else if words.contains(&"grounded")
|| words.contains(&"yes")
|| words.contains(&"supported")
{
state.grounded = true;
} else {
state.grounded = false;
}
Ok(())
}
}
fn build_context(docs: &[Document], web_results: Option<&str>) -> String {
let mut parts = Vec::new();
for (i, doc) in docs.iter().enumerate() {
parts.push(format!("[Document {}]: {}", i + 1, doc.content));
}
if let Some(web) = web_results {
parts.push(format!("[Web Search Results]: {}", web));
}
parts.join("\n\n")
}
fn build_generate_prompt(context: &str, query: &str) -> String {
GENERATE_USER_PROMPT
.replace("{context}", context)
.replace("{query}", query)
}
fn build_hallucination_check_prompt(context: &str, answer: &str) -> String {
HALLUCINATION_CHECK_PROMPT
.replace("{context}", context)
.replace("{answer}", answer)
}
const GENERATE_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that answers questions based on the provided context. Follow these rules:
1. Answer the question using ONLY the information provided in the context.
2. If the context does not contain enough information to fully answer the question, say so.
3. Do not make up or infer information that is not directly supported by the context.
4. Be concise and direct in your answer.
5. Cite the source documents when possible."#;
const GENERATE_USER_PROMPT: &str = r#"Context:
{context}
Question: {query}
Answer:"#;
const HALLUCINATION_SYSTEM_PROMPT: &str =
"You are a fact-checking assistant. Your job is to verify whether an answer is grounded in the provided context.";
const HALLUCINATION_CHECK_PROMPT: &str = r#"Context:
{context}
Answer to verify:
{answer}
Is the above answer fully grounded in and supported by the provided context? Respond with either:
- "grounded" if the answer is fully supported by the context
- "not grounded" if the answer contains information not found in or contradicted by the context
Your assessment:"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crag_state_new() {
let state = CRAGState::new("What is Rust?");
assert_eq!(state.query, "What is Rust?");
assert_eq!(state.current_query, "What is Rust?");
assert!(!state.query_rewritten);
assert!(state.documents.is_empty());
assert!(state.answer.is_none());
assert!(state.grounded);
}
#[test]
fn test_build_context_with_documents() {
let docs = vec![
Document::new("Rust is a systems programming language."),
Document::new("Rust emphasizes safety and performance."),
];
let context = build_context(&docs, None);
assert!(context.contains("[Document 1]"));
assert!(context.contains("[Document 2]"));
assert!(context.contains("systems programming"));
}
#[test]
fn test_build_context_with_web_results() {
let docs = vec![Document::new("Some doc content")];
let context = build_context(&docs, Some("Web search result here"));
assert!(context.contains("[Web Search Results]"));
assert!(context.contains("Web search result here"));
}
#[test]
fn test_build_context_empty() {
let context = build_context(&[], None);
assert!(context.is_empty());
}
#[test]
fn test_build_generate_prompt() {
let prompt = build_generate_prompt("some context", "What is Rust?");
assert!(prompt.contains("some context"));
assert!(prompt.contains("What is Rust?"));
}
#[test]
fn test_build_hallucination_check_prompt() {
let prompt = build_hallucination_check_prompt("the context", "the answer");
assert!(prompt.contains("the context"));
assert!(prompt.contains("the answer"));
assert!(prompt.contains("grounded"));
}
}