#![allow(unused_imports)]
use crate::config::Config;
use crate::core::{
ChunkId, Document, DocumentId, Entity, EntityId, GraphRAGError, KnowledgeGraph, Relationship,
Result, TextChunk,
};
use crate::{critic, ollama, persistence, query, retrieval};
#[cfg(feature = "parallel-processing")]
#[allow(unused_imports)]
use crate::parallel;
use super::GraphRAG;
impl GraphRAG {
#[cfg(feature = "async")]
pub async fn ask_with_reasoning(&mut self, query: &str) -> Result<String> {
if self.query_planner.is_none() {
return self.ask(query).await;
}
self.ensure_initialized()?;
if self.has_documents() && !self.has_graph() {
self.build_graph().await?;
}
let planner = self.query_planner.as_ref().expect("checked above");
tracing::info!("Decomposing query: {}", query);
let sub_queries = match planner.decompose(query).await {
Ok(sq) => sq,
Err(e) => {
tracing::warn!(
"Query decomposition failed, falling back to standard query: {}",
e
);
vec![query.to_string()]
},
};
tracing::info!("Sub-queries: {:?}", sub_queries);
let mut all_results = Vec::new();
for sub_query in sub_queries {
match self.query_internal_with_results(&sub_query).await {
Ok(results) => all_results.extend(results),
Err(e) => tracing::warn!("Failed to execute sub-query '{}': {}", sub_query, e),
}
}
if all_results.is_empty() {
return Ok("No relevant information found for the decomposed queries.".to_string());
}
all_results.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut unique_results = Vec::new();
let mut seen_ids = std::collections::HashSet::new();
for result in all_results {
if !seen_ids.contains(&result.id) {
seen_ids.insert(result.id.clone());
unique_results.push(result);
}
}
if self.config.ollama.enabled {
let mut current_answer = self
.generate_semantic_answer_from_results(query, &unique_results)
.await?;
if let Some(critic) = &self.critic {
let mut attempts = 0;
let max_retries = 3;
while attempts < max_retries {
let context_strings: Vec<String> =
unique_results.iter().map(|r| r.content.clone()).collect();
let evaluation = match critic
.evaluate(query, &context_strings, ¤t_answer)
.await
{
Ok(eval) => eval,
Err(e) => {
tracing::warn!("Critic evaluation failed: {}", e);
break;
},
};
tracing::info!(
"Critic Evaluation (Attempt {}): Score={:.2}, Grounded={}, Feedback='{}'",
attempts + 1,
evaluation.score,
evaluation.grounded,
evaluation.feedback
);
if evaluation.score >= 0.7 && evaluation.grounded {
tracing::info!("Answer accepted by critic.");
break;
}
tracing::warn!("Answer rejected by critic. Refining...");
current_answer = critic
.refine(query, ¤t_answer, &evaluation.feedback)
.await?;
attempts += 1;
}
}
return Ok(current_answer);
}
let formatted: Vec<String> = unique_results
.into_iter()
.take(10)
.map(|r| format!("{} (score: {:.2})", r.content, r.score))
.collect();
Ok(formatted.join("\n"))
}
#[cfg(feature = "async")]
pub async fn ask(&mut self, query: &str) -> Result<String> {
self.ensure_initialized()?;
if self.has_documents() && !self.has_graph() {
self.build_graph().await?;
}
let search_results = self.query_internal_with_results(query).await?;
if self.config.ollama.enabled {
return self
.generate_semantic_answer_from_results(query, &search_results)
.await;
}
let formatted: Vec<String> = search_results
.into_iter()
.map(|r| format!("{} (score: {:.2})", r.content, r.score))
.collect();
Ok(formatted.join("\n"))
}
#[cfg(not(feature = "async"))]
pub fn ask(&mut self, query: &str) -> Result<String> {
self.ensure_initialized()?;
if self.has_documents() && !self.has_graph() {
self.build_graph()?;
}
let retrieval = self
.retrieval_system
.as_ref()
.ok_or_else(|| GraphRAGError::Config {
message: "Retrieval system not initialized".to_string(),
})?;
let results = retrieval.query(query)?;
Ok(results.join("\n"))
}
#[cfg(feature = "async")]
pub async fn ask_explained(&mut self, query: &str) -> Result<retrieval::ExplainedAnswer> {
self.ensure_initialized()?;
if self.has_documents() && !self.has_graph() {
self.build_graph().await?;
}
let search_results = self.query_internal_with_results(query).await?;
let answer = if self.config.ollama.enabled {
self.generate_semantic_answer_from_results(query, &search_results)
.await?
} else {
search_results
.iter()
.take(3)
.map(|r| r.content.clone())
.collect::<Vec<_>>()
.join(" ")
};
let explained = retrieval::ExplainedAnswer::from_results(answer, &search_results, query);
Ok(explained)
}
pub async fn query_internal(&mut self, query: &str) -> Result<Vec<String>> {
let retrieval = self
.retrieval_system
.as_mut()
.ok_or_else(|| GraphRAGError::Config {
message: "Retrieval system not initialized".to_string(),
})?;
let graph = self
.knowledge_graph
.as_mut()
.ok_or_else(|| GraphRAGError::Config {
message: "Knowledge graph not initialized".to_string(),
})?;
retrieval.add_embeddings_to_graph(graph).await?;
let search_results = retrieval.hybrid_query(query, graph).await?;
let result_strings: Vec<String> = search_results
.into_iter()
.map(|r| format!("{} (score: {:.2})", r.content, r.score))
.collect();
Ok(result_strings)
}
#[cfg(feature = "async")]
async fn query_internal_with_results(
&mut self,
query: &str,
) -> Result<Vec<retrieval::SearchResult>> {
let retrieval = self
.retrieval_system
.as_mut()
.ok_or_else(|| GraphRAGError::Config {
message: "Retrieval system not initialized".to_string(),
})?;
let graph = self
.knowledge_graph
.as_mut()
.ok_or_else(|| GraphRAGError::Config {
message: "Knowledge graph not initialized".to_string(),
})?;
retrieval.add_embeddings_to_graph(graph).await?;
retrieval.hybrid_query(query, graph).await
}
#[cfg(feature = "async")]
async fn generate_semantic_answer_from_results(
&self,
query: &str,
search_results: &[retrieval::SearchResult],
) -> Result<String> {
use crate::ollama::OllamaClient;
let graph = self
.knowledge_graph
.as_ref()
.ok_or_else(|| GraphRAGError::Config {
message: "Knowledge graph not initialized".to_string(),
})?;
let mut context_parts = Vec::new();
let mut seen_chunk_ids = std::collections::HashSet::new();
for result in search_results.iter() {
if result.result_type == retrieval::ResultType::Entity
&& !result.source_chunks.is_empty()
{
let entity_label = result
.content
.split(" (score:")
.next()
.unwrap_or(&result.content);
for chunk_id_str in &result.source_chunks {
if seen_chunk_ids.contains(chunk_id_str) {
continue;
}
let chunk_id = ChunkId::new(chunk_id_str.clone());
if let Some(chunk) = graph.chunks().find(|c| c.id == chunk_id) {
seen_chunk_ids.insert(chunk_id_str.clone());
context_parts.push((
result.score,
format!(
"[Entity: {} | Relevance: {:.2}]\n{}",
entity_label, result.score, chunk.content
),
));
}
}
}
else if result.result_type == retrieval::ResultType::Chunk {
if !seen_chunk_ids.contains(&result.id) {
seen_chunk_ids.insert(result.id.clone());
context_parts.push((
result.score,
format!(
"[Chunk | Relevance: {:.2}]\n{}",
result.score, result.content
),
));
}
}
else {
context_parts.push((
result.score,
format!(
"[{:?} | Relevance: {:.2}]\n{}",
result.result_type, result.score, result.content
),
));
}
}
context_parts.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let context = context_parts
.into_iter()
.map(|(_, text)| text)
.collect::<Vec<_>>()
.join("\n\n---\n\n");
if context.trim().is_empty() {
return Ok("No relevant information found in the knowledge graph.".to_string());
}
let client = OllamaClient::new(self.config.ollama.clone());
let prompt = format!(
"You are a knowledgeable assistant specialized in answering questions based on a knowledge graph.\n\n\
IMPORTANT INSTRUCTIONS:\n\
- Answer ONLY using information from the provided context below\n\
- Synthesize information from ALL context sections to give a comprehensive answer\n\
- Provide direct, conversational, and natural responses\n\
- Do NOT show your reasoning process or use <think> tags\n\
- If the context lacks sufficient information, clearly state: \"I don't have enough information to answer this question.\"\n\
- Aim for a complete answer (3-6 sentences) that covers different aspects found across the context\n\
- Use a natural, helpful tone as if speaking to a person\n\n\
CONTEXT:\n\
{}\n\n\
QUESTION: {}\n\n\
ANSWER (direct response only, no reasoning):",
context, query
);
let max_answer_tokens: u32 = 800;
let prompt_tokens = (prompt.len() / 4) as u32;
let total = prompt_tokens + max_answer_tokens;
let with_margin = (total as f32 * 1.20) as u32;
let num_ctx = (((with_margin + 1023) / 1024) * 1024).clamp(4096, 131_072);
let params = crate::ollama::OllamaGenerationParams {
num_predict: Some(max_answer_tokens),
temperature: self.config.ollama.temperature,
num_ctx: Some(num_ctx),
keep_alive: self.config.ollama.keep_alive.clone(),
..Default::default()
};
match client.generate_with_params(&prompt, params).await {
Ok(answer) => {
let cleaned_answer = Self::remove_thinking_tags(&answer);
Ok(cleaned_answer.trim().to_string())
},
Err(e) => {
#[cfg(feature = "tracing")]
tracing::warn!(
"LLM generation failed: {}. Falling back to search results.",
e
);
Ok(format!(
"Relevant information from knowledge graph:\n\n{}",
context
))
},
}
}
#[cfg(feature = "async")]
fn remove_thinking_tags(text: &str) -> String {
let mut result = text.to_string();
while let Some(start) = result.find("<think>") {
if let Some(end) = result[start..].find("</think>") {
let end_pos = start + end + "</think>".len();
result.replace_range(start..end_pos, "");
} else {
result.replace_range(start..start + "<think>".len(), "");
break;
}
}
result.trim().to_string()
}
#[cfg(all(feature = "pagerank", feature = "async"))]
pub async fn ask_with_pagerank(
&mut self,
query: &str,
) -> Result<Vec<retrieval::pagerank_retrieval::ScoredResult>> {
use crate::retrieval::pagerank_retrieval::PageRankRetrievalSystem;
self.ensure_initialized()?;
if self.has_documents() && !self.has_graph() {
self.build_graph().await?;
}
let graph = self
.knowledge_graph
.as_ref()
.ok_or_else(|| GraphRAGError::Config {
message: "Knowledge graph not initialized".to_string(),
})?;
let pagerank_system = PageRankRetrievalSystem::new(10);
pagerank_system.search_with_pagerank(query, graph, Some(5))
}
#[cfg(all(feature = "pagerank", not(feature = "async")))]
pub fn ask_with_pagerank(
&mut self,
query: &str,
) -> Result<Vec<retrieval::pagerank_retrieval::ScoredResult>> {
use crate::retrieval::pagerank_retrieval::PageRankRetrievalSystem;
self.ensure_initialized()?;
if self.has_documents() && !self.has_graph() {
self.build_graph()?;
}
let graph = self
.knowledge_graph
.as_ref()
.ok_or_else(|| GraphRAGError::Config {
message: "Knowledge graph not initialized".to_string(),
})?;
let pagerank_system = PageRankRetrievalSystem::new(10);
pagerank_system.search_with_pagerank(query, graph, Some(5))
}
}