1use color_eyre::eyre::{eyre, Result};
6use graphrag_core::{persistence::WorkspaceManager, Config, Entity, GraphRAG};
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9use std::sync::Arc;
10use tokio::sync::Mutex;
11
12#[derive(Debug, Clone, Default)]
14pub struct GraphStats {
15 pub entities: usize,
16 pub relationships: usize,
17 pub documents: usize,
18 pub chunks: usize,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SourceReference {
24 pub id: String,
25 pub excerpt: String,
26 pub relevance_score: f32,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ReasoningStep {
32 pub step_number: u8,
33 pub description: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct QueryExplainedResult {
39 pub answer: String,
40 pub confidence: f32,
41 pub sources: Vec<SourceReference>,
42 pub reasoning_steps: Vec<ReasoningStep>,
43}
44
45#[derive(Clone)]
47pub struct GraphRAGHandler {
48 graphrag: Arc<Mutex<Option<GraphRAG>>>,
49}
50
51impl GraphRAGHandler {
52 pub fn new() -> Self {
54 Self {
55 graphrag: Arc::new(Mutex::new(None)),
56 }
57 }
58
59 pub async fn is_initialized(&self) -> bool {
61 let guard = self.graphrag.lock().await;
62 guard.is_some()
63 }
64
65 pub async fn initialize(&self, config: Config) -> Result<()> {
67 tracing::info!("Initializing GraphRAG with config");
68
69 let mut config = config;
70 config.suppress_progress_bars = true;
73
74 let mut graphrag = GraphRAG::new(config)?;
75 graphrag.initialize()?;
76
77 let mut guard = self.graphrag.lock().await;
78 *guard = Some(graphrag);
79
80 tracing::info!("GraphRAG initialized successfully");
81 Ok(())
82 }
83
84 async fn check_ollama_models(&self) -> Result<()> {
91 let (needs_ollama, host, port, embedding_backend, embedding_model, chat_model) = {
93 let guard = self.graphrag.lock().await;
94 match guard.as_ref() {
95 Some(g) => {
96 let c = g.config();
97 (
98 c.ollama.enabled,
99 c.ollama.host.clone(),
100 c.ollama.port,
101 c.embeddings.backend.clone(),
102 c.ollama.embedding_model.clone(),
103 c.ollama.chat_model.clone(),
104 )
105 },
106 None => return Ok(()), }
108 };
109
110 let mut required: Vec<String> = Vec::new();
112 if embedding_backend == "ollama" {
113 required.push(embedding_model);
114 }
115 if needs_ollama {
116 required.push(chat_model);
117 }
118 required.sort();
120 required.dedup();
121
122 if required.is_empty() {
123 return Ok(()); }
125
126 let url = format!("{}:{}/api/tags", host, port);
128 let agent = ureq::AgentBuilder::new()
129 .timeout(std::time::Duration::from_secs(3))
130 .build();
131
132 let available: Vec<String> = match agent.get(&url).call() {
133 Ok(resp) => {
134 let body: serde_json::Value = resp
135 .into_json()
136 .unwrap_or_else(|_| serde_json::json!({"models": []}));
137 body["models"]
138 .as_array()
139 .map(|arr| {
140 arr.iter()
141 .filter_map(|m| m["name"].as_str().map(|s| s.to_string()))
142 .collect()
143 })
144 .unwrap_or_default()
145 },
146 Err(e) => {
147 return Err(eyre!(
148 "Cannot reach Ollama at {} — is it running?\nError: {}",
149 url,
150 e
151 ));
152 },
153 };
154
155 let missing: Vec<&String> = required
157 .iter()
158 .filter(|req| {
159 !available.iter().any(|avail| {
160 avail == req.as_str()
161 || avail.starts_with(&format!("{}:", req))
162 || req.ends_with(":latest") && avail == req.trim_end_matches(":latest")
163 })
164 })
165 .collect();
166
167 if !missing.is_empty() {
168 let pull_cmds: Vec<String> = missing
169 .iter()
170 .map(|m| format!("ollama pull {}", m))
171 .collect();
172 return Err(eyre!(
173 "Missing Ollama models: {}\n\nPull them first:\n {}",
174 missing
175 .iter()
176 .map(|s| s.as_str())
177 .collect::<Vec<_>>()
178 .join(", "),
179 pull_cmds.join("\n ")
180 ));
181 }
182
183 Ok(())
184 }
185
186 pub async fn load_document_with_options(&self, path: &Path, rebuild: bool) -> Result<String> {
192 self.check_ollama_models().await?;
194
195 tracing::info!("Loading document: {:?} (rebuild: {})", path, rebuild);
196
197 let content = tokio::fs::read_to_string(path)
199 .await
200 .map_err(|e| eyre!("Failed to read file: {}", e))?;
201
202 let filename = path
203 .file_name()
204 .and_then(|n| n.to_str())
205 .unwrap_or("unknown")
206 .to_string();
207
208 let mut guard = self.graphrag.lock().await;
210 if let Some(ref mut graphrag) = *guard {
211 if rebuild {
213 tracing::info!("Clearing existing graph and documents for rebuild");
214 graphrag.initialize()?;
216 }
217
218 graphrag.add_document_from_text(&content)?;
220
221 graphrag.build_graph().await?;
223
224 let message = if rebuild {
225 format!(
226 "Document '{}' loaded successfully (complete rebuild from scratch)",
227 filename
228 )
229 } else {
230 format!("Document '{}' loaded successfully", filename)
231 };
232
233 Ok(message)
234 } else {
235 Err(eyre!("GraphRAG not initialized"))
236 }
237 }
238
239 pub async fn clear_graph(&self) -> Result<String> {
241 tracing::info!("Clearing knowledge graph");
242
243 let mut guard = self.graphrag.lock().await;
244 if let Some(ref mut graphrag) = *guard {
245 graphrag.clear_graph()?;
246 Ok("Knowledge graph cleared successfully. Entities and relationships removed, documents preserved.".to_string())
247 } else {
248 Err(eyre!("GraphRAG not initialized"))
249 }
250 }
251
252 pub async fn rebuild_graph(&self) -> Result<String> {
257 tracing::info!("Rebuilding knowledge graph from existing documents");
258
259 let mut guard = self.graphrag.lock().await;
260 if let Some(ref mut graphrag) = *guard {
261 graphrag.clear_graph()?;
263
264 if !graphrag.has_documents() {
266 return Err(eyre!(
267 "No documents loaded. Use /load <file> to load a document first."
268 ));
269 }
270
271 graphrag.build_graph().await?;
273
274 let stats = graphrag
275 .knowledge_graph()
276 .map(|kg| (kg.entities().count(), kg.relationships().count()))
277 .unwrap_or((0, 0));
278
279 Ok(format!(
280 "Knowledge graph rebuilt successfully. Extracted {} entities and {} relationships.",
281 stats.0, stats.1
282 ))
283 } else {
284 Err(eyre!("GraphRAG not initialized"))
285 }
286 }
287
288 pub async fn query_with_raw(&self, query_text: &str) -> Result<(String, Vec<String>)> {
292 tracing::info!("Executing query with raw results: {}", query_text);
293
294 let mut guard = self.graphrag.lock().await;
295 if let Some(ref mut graphrag) = *guard {
296 let raw_results = graphrag.query_internal(query_text).await?;
298
299 let answer = graphrag.ask(query_text).await?;
301
302 Ok((answer, raw_results))
303 } else {
304 Err(eyre!(
305 "GraphRAG not initialized. Use /config to load a configuration first."
306 ))
307 }
308 }
309
310 pub async fn query_explained(&self, query_text: &str) -> Result<QueryExplainedResult> {
318 tracing::info!("Executing explained query: {}", query_text);
319
320 let mut guard = self.graphrag.lock().await;
321 if let Some(ref mut graphrag) = *guard {
322 let explained = graphrag.ask_explained(query_text).await?;
323
324 Ok(QueryExplainedResult {
325 answer: explained.answer,
326 confidence: explained.confidence,
327 sources: explained
328 .sources
329 .into_iter()
330 .map(|s| SourceReference {
331 id: s.id,
332 excerpt: s.excerpt,
333 relevance_score: s.relevance_score,
334 })
335 .collect(),
336 reasoning_steps: explained
337 .reasoning_steps
338 .into_iter()
339 .map(|s| ReasoningStep {
340 step_number: s.step_number,
341 description: s.description,
342 })
343 .collect(),
344 })
345 } else {
346 Err(eyre!(
347 "GraphRAG not initialized. Use /config to load a configuration first."
348 ))
349 }
350 }
351
352 pub async fn query_with_reasoning(&self, query_text: &str) -> Result<String> {
357 tracing::info!("Executing query with reasoning: {}", query_text);
358
359 let mut guard = self.graphrag.lock().await;
360 if let Some(ref mut graphrag) = *guard {
361 let answer = graphrag.ask_with_reasoning(query_text).await?;
362 Ok(answer)
363 } else {
364 Err(eyre!(
365 "GraphRAG not initialized. Use /config to load a configuration first."
366 ))
367 }
368 }
369
370 #[allow(dead_code)]
372 pub async fn has_documents(&self) -> bool {
373 let guard = self.graphrag.lock().await;
374 guard.as_ref().map_or(false, |g| g.has_documents())
375 }
376
377 #[allow(dead_code)]
379 pub async fn has_graph(&self) -> bool {
380 let guard = self.graphrag.lock().await;
381 guard.as_ref().map_or(false, |g| g.has_graph())
382 }
383
384 pub async fn get_stats(&self) -> Option<GraphStats> {
386 let guard = self.graphrag.lock().await;
387 guard.as_ref().and_then(|g| {
388 g.knowledge_graph().map(|kg| GraphStats {
389 entities: kg.entities().count(),
390 relationships: kg.relationships().count(),
391 documents: kg.documents().count(),
392 chunks: kg.chunks().count(),
393 })
394 })
395 }
396
397 pub async fn get_entities(&self, filter: Option<&str>) -> Result<Vec<Entity>> {
399 let guard = self.graphrag.lock().await;
400 if let Some(ref graphrag) = *guard {
401 if let Some(kg) = graphrag.knowledge_graph() {
402 let entities: Vec<Entity> = match filter {
403 Some(f) => kg
404 .entities()
405 .filter(|e| {
406 e.name.to_lowercase().contains(&f.to_lowercase())
407 || e.entity_type.to_lowercase().contains(&f.to_lowercase())
408 })
409 .cloned()
410 .collect(),
411 None => kg.entities().cloned().collect(),
412 };
413 Ok(entities)
414 } else {
415 Err(eyre!("Knowledge graph not built yet"))
416 }
417 } else {
418 Err(eyre!("GraphRAG not initialized"))
419 }
420 }
421
422 #[allow(dead_code)]
424 pub async fn has_knowledge_graph(&self) -> bool {
425 let guard = self.graphrag.lock().await;
426 if let Some(ref graphrag) = *guard {
427 graphrag.knowledge_graph().is_some()
428 } else {
429 false
430 }
431 }
432
433 pub async fn list_workspaces(&self, workspace_dir: &str) -> Result<String> {
437 let workspace_manager = WorkspaceManager::new(workspace_dir)?;
438 let workspaces = workspace_manager.list_workspaces()?;
439
440 if workspaces.is_empty() {
441 return Ok(
442 "No workspaces found. Use /workspace save <name> to create one.".to_string(),
443 );
444 }
445
446 let mut output = format!("📁 Available Workspaces ({} total):\n\n", workspaces.len());
447
448 for (i, ws) in workspaces.iter().enumerate() {
449 output.push_str(&format!(
450 "{}. {} ({:.2} KB)\n",
451 i + 1,
452 ws.name,
453 ws.size_bytes as f64 / 1024.0
454 ));
455 output.push_str(&format!(
456 " Entities: {}, Relationships: {}, Documents: {}, Chunks: {}\n",
457 ws.metadata.entity_count,
458 ws.metadata.relationship_count,
459 ws.metadata.document_count,
460 ws.metadata.chunk_count
461 ));
462 output.push_str(&format!(
463 " Created: {}\n",
464 ws.metadata.created_at.format("%Y-%m-%d %H:%M:%S")
465 ));
466 if let Some(desc) = &ws.metadata.description {
467 output.push_str(&format!(" Description: {}\n", desc));
468 }
469 output.push('\n');
470 }
471
472 Ok(output)
473 }
474
475 pub async fn save_workspace(&self, workspace_dir: &str, name: &str) -> Result<String> {
477 let guard = self.graphrag.lock().await;
478 if let Some(ref graphrag) = *guard {
479 if let Some(kg) = graphrag.knowledge_graph() {
480 let workspace_manager = WorkspaceManager::new(workspace_dir)?;
481 workspace_manager.save_graph(kg, name)?;
482
483 let stats = (
484 kg.entities().count(),
485 kg.relationships().count(),
486 kg.documents().count(),
487 kg.chunks().count(),
488 );
489
490 Ok(format!(
491 "✅ Workspace '{}' saved successfully!\n\n\
492 Saved: {} entities, {} relationships, {} documents, {} chunks",
493 name, stats.0, stats.1, stats.2, stats.3
494 ))
495 } else {
496 Err(eyre!(
497 "No knowledge graph to save. Build a graph first with /load <file>"
498 ))
499 }
500 } else {
501 Err(eyre!("GraphRAG not initialized"))
502 }
503 }
504
505 pub async fn load_workspace(&self, workspace_dir: &str, name: &str) -> Result<String> {
507 let workspace_manager = WorkspaceManager::new(workspace_dir)?;
508 let loaded_kg = workspace_manager.load_graph(name)?;
509
510 let stats = (
511 loaded_kg.entities().count(),
512 loaded_kg.relationships().count(),
513 loaded_kg.documents().count(),
514 loaded_kg.chunks().count(),
515 );
516
517 let mut guard = self.graphrag.lock().await;
519 if let Some(ref mut graphrag) = *guard {
520 if let Some(kg_mut) = graphrag.knowledge_graph_mut() {
522 *kg_mut = loaded_kg;
523 } else {
524 return Err(eyre!("Knowledge graph not initialized. Use /config first."));
525 }
526
527 Ok(format!(
528 "✅ Workspace '{}' loaded successfully!\n\n\
529 Loaded: {} entities, {} relationships, {} documents, {} chunks",
530 name, stats.0, stats.1, stats.2, stats.3
531 ))
532 } else {
533 Err(eyre!(
534 "GraphRAG not initialized. Use /config to load configuration first."
535 ))
536 }
537 }
538
539 pub async fn delete_workspace(&self, workspace_dir: &str, name: &str) -> Result<String> {
541 let workspace_manager = WorkspaceManager::new(workspace_dir)?;
542
543 workspace_manager.delete_workspace(name)?;
545
546 Ok(format!("✅ Workspace '{}' deleted successfully.", name))
547 }
548}
549
550impl Default for GraphRAGHandler {
551 fn default() -> Self {
552 Self::new()
553 }
554}