1pub mod action;
7pub mod app;
8pub mod commands;
9pub mod config;
10pub mod handlers;
11pub mod mode;
12pub mod query_history;
13pub mod theme;
14pub mod tui;
15pub mod ui;
16pub mod workspace;
17
18use app::App;
19use clap::{Parser, Subcommand};
20use color_eyre::eyre::Result;
21use std::path::PathBuf;
22
23#[derive(Parser)]
28#[command(name = "graphrag")]
29#[command(version, about = "Modern Terminal UI for GraphRAG operations", long_about = None)]
30#[command(author = "GraphRAG Contributors")]
31pub struct Cli {
32 #[arg(short, long, value_name = "FILE")]
34 pub config: Option<PathBuf>,
35
36 #[arg(short, long)]
38 pub workspace: Option<String>,
39
40 #[arg(short, long)]
42 pub debug: bool,
43
44 #[arg(long, default_value = "text", value_parser = ["text", "json"])]
46 pub format: String,
47
48 #[command(subcommand)]
49 pub command: Option<Commands>,
50}
51
52#[derive(Subcommand)]
53pub enum Commands {
54 Index {
59 path: PathBuf,
61 #[arg(long, default_value = "./graphrag-data")]
63 workspace: PathBuf,
64 #[arg(long)]
66 ollama: bool,
67 #[arg(long)]
69 chunk_size: Option<usize>,
70 },
71
72 Ask {
76 query: String,
78 #[arg(long, default_value = "./graphrag-data")]
80 workspace: PathBuf,
81 #[arg(long)]
83 ollama: bool,
84 },
85
86 Tui,
88
89 Setup {
91 #[arg(short, long)]
93 template: Option<String>,
94
95 #[arg(short, long, default_value = "./graphrag.toml")]
97 output: PathBuf,
98 },
99
100 Validate {
102 config_file: PathBuf,
104 },
105
106 Init {
108 config: PathBuf,
110 },
111
112 Load {
114 document: PathBuf,
116
117 #[arg(short, long)]
119 config: Option<PathBuf>,
120 },
121
122 Query {
124 query: String,
126
127 #[arg(short, long)]
129 config: Option<PathBuf>,
130 },
131
132 Entities {
134 filter: Option<String>,
136
137 #[arg(short, long)]
139 config: Option<PathBuf>,
140 },
141
142 Stats {
144 #[arg(short, long)]
146 config: Option<PathBuf>,
147 },
148
149 Bench {
151 #[arg(short, long)]
153 config: PathBuf,
154
155 #[arg(short, long)]
157 book: PathBuf,
158
159 #[arg(short, long)]
161 questions: String,
162 },
163
164 Workspace {
166 #[command(subcommand)]
167 action: WorkspaceCommands,
168 },
169}
170
171#[derive(Subcommand)]
172pub enum WorkspaceCommands {
173 List,
175
176 Create { name: String },
178
179 Info { id: String },
181
182 Delete { id: String },
184}
185
186pub async fn run() -> Result<()> {
193 install_panic_hook();
194
195 let cli = Cli::parse();
196
197 color_eyre::install()?;
198
199 match cli.command {
200 Some(Commands::Tui) | None => {
201 run_tui(cli.config, cli.workspace).await?;
202 },
203 Some(Commands::Index {
204 path,
205 workspace,
206 ollama,
207 chunk_size,
208 }) => {
209 setup_logging(cli.debug)?;
210 run_index(&path, &workspace, ollama, chunk_size, &cli.format).await?;
211 },
212 Some(Commands::Ask {
213 query,
214 workspace,
215 ollama,
216 }) => {
217 setup_logging(cli.debug)?;
218 run_ask(&query, &workspace, ollama, &cli.format).await?;
219 },
220 Some(Commands::Setup { template, output }) => {
221 run_setup_wizard(template, output).await?;
222 },
223 Some(Commands::Validate { config_file }) => {
224 setup_logging(cli.debug)?;
225 run_validate(&config_file, &cli.format)?;
226 },
227 Some(Commands::Init { config }) => {
228 setup_logging(cli.debug)?;
229 eprintln!(
230 "⚠️ `init` is deprecated. Prefer: graphrag tui --config {}",
231 config.display()
232 );
233
234 let handler = handlers::graphrag::GraphRAGHandler::new();
235 let cfg = load_config_from_file(&config).await?;
236 handler.initialize(cfg).await?;
237
238 if cli.format == "json" {
239 println!(
240 "{}",
241 serde_json::json!({"status": "initialized", "config": config.display().to_string()})
242 );
243 } else {
244 println!("✅ GraphRAG initialized with config: {}", config.display());
245 }
246 },
247 Some(Commands::Load { document, config }) => {
248 setup_logging(cli.debug)?;
249 eprintln!(
250 "⚠️ `load` is deprecated. Prefer: graphrag tui, then /load {}",
251 document.display()
252 );
253
254 let handler = handlers::graphrag::GraphRAGHandler::new();
255 let config_path = config.unwrap_or_else(|| PathBuf::from("./graphrag.toml"));
256 let cfg = load_config_from_file(&config_path).await?;
257 handler.initialize(cfg).await?;
258 let result = handler.load_document_with_options(&document, false).await?;
259
260 if cli.format == "json" {
261 println!(
262 "{}",
263 serde_json::json!({"status": "loaded", "document": document.display().to_string(), "details": result})
264 );
265 } else {
266 println!("✅ {}", result);
267 }
268 },
269 Some(Commands::Query { query, config }) => {
270 setup_logging(cli.debug)?;
271 eprintln!(
272 "⚠️ `query` is deprecated. Prefer: graphrag tui, then /query {}",
273 query
274 );
275
276 let handler = handlers::graphrag::GraphRAGHandler::new();
277 let config_path = config.unwrap_or_else(|| PathBuf::from("./graphrag.toml"));
278 let cfg = load_config_from_file(&config_path).await?;
279 handler.initialize(cfg).await?;
280
281 let (answer, raw_results) = handler.query_with_raw(&query).await?;
282
283 if cli.format == "json" {
284 println!(
285 "{}",
286 serde_json::json!({"query": query, "answer": answer, "sources": raw_results})
287 );
288 } else {
289 println!("📝 Query: {}\n", query);
290 println!("💡 Answer:\n{}\n", answer);
291 if !raw_results.is_empty() {
292 println!("📚 Sources:");
293 for (i, src) in raw_results.iter().enumerate() {
294 println!(" {}. {}", i + 1, src);
295 }
296 }
297 }
298 },
299 Some(Commands::Entities { filter, config }) => {
300 setup_logging(cli.debug)?;
301 eprintln!("⚠️ `entities` is deprecated. Prefer: graphrag tui, then /entities");
302
303 let handler = handlers::graphrag::GraphRAGHandler::new();
304 let config_path = config.unwrap_or_else(|| PathBuf::from("./graphrag.toml"));
305 let cfg = load_config_from_file(&config_path).await?;
306 handler.initialize(cfg).await?;
307 let entities = handler.get_entities(filter.as_deref()).await?;
308
309 if cli.format == "json" {
310 let json_entities: Vec<serde_json::Value> = entities
311 .iter()
312 .map(|e| serde_json::json!({"name": e.name, "type": e.entity_type}))
313 .collect();
314 println!(
315 "{}",
316 serde_json::json!({"entities": json_entities, "count": entities.len()})
317 );
318 } else {
319 println!("📊 Entities ({} found):\n", entities.len());
320 for entity in &entities {
321 println!(" • {} [{}]", entity.name, entity.entity_type);
322 }
323 }
324 },
325 Some(Commands::Stats { config }) => {
326 setup_logging(cli.debug)?;
327 eprintln!("⚠️ `stats` is deprecated. Prefer: graphrag tui, then /stats");
328
329 let handler = handlers::graphrag::GraphRAGHandler::new();
330 let config_path = config.unwrap_or_else(|| PathBuf::from("./graphrag.toml"));
331 let cfg = load_config_from_file(&config_path).await?;
332 handler.initialize(cfg).await?;
333
334 if let Some(stats) = handler.get_stats().await {
335 if cli.format == "json" {
336 println!(
337 "{}",
338 serde_json::json!({
339 "entities": stats.entities,
340 "relationships": stats.relationships,
341 "documents": stats.documents,
342 "chunks": stats.chunks,
343 })
344 );
345 } else {
346 println!("📊 Knowledge Graph Statistics:");
347 println!(" Entities: {}", stats.entities);
348 println!(" Relationships: {}", stats.relationships);
349 println!(" Documents: {}", stats.documents);
350 println!(" Chunks: {}", stats.chunks);
351 }
352 } else if cli.format == "json" {
353 println!(
354 "{}",
355 serde_json::json!({"error": "No knowledge graph built yet"})
356 );
357 } else {
358 println!("⚠️ No knowledge graph built yet. Load documents first.");
359 }
360 },
361 Some(Commands::Bench {
362 config,
363 book,
364 questions,
365 }) => {
366 if !cli.debug {
367 std::env::set_var("RUST_LOG", "error");
368 }
369 setup_logging(cli.debug)?;
370
371 let q_vec: Vec<String> = questions.split('|').map(|s| s.to_string()).collect();
372 handlers::bench::run_benchmark(&config, &book, q_vec).await?;
373 },
374 Some(Commands::Workspace { action }) => {
375 setup_logging(cli.debug)?;
376 handle_workspace_commands(action).await?;
377 },
378 }
379
380 Ok(())
381}
382
383async fn load_config_from_file(path: &std::path::Path) -> Result<graphrag_core::Config> {
388 config::load_config(path).await
389}
390
391fn turnkey_config(
393 workspace: &std::path::Path,
394 ollama: bool,
395 chunk_size: Option<usize>,
396) -> graphrag_core::Config {
397 let mut cfg = graphrag_core::Config::quick(workspace);
398 if ollama {
399 cfg = cfg.with_ollama();
400 }
401 if let Some(n) = chunk_size {
402 cfg = cfg.with_chunk_size(n);
403 }
404 cfg
405}
406
407async fn run_index(
408 path: &std::path::Path,
409 workspace: &std::path::Path,
410 ollama: bool,
411 chunk_size: Option<usize>,
412 format: &str,
413) -> Result<()> {
414 if !path.exists() {
415 return Err(color_eyre::eyre::eyre!(
416 "Document not found: {}",
417 path.display()
418 ));
419 }
420 let cfg = turnkey_config(workspace, ollama, chunk_size);
421 let handler = handlers::graphrag::GraphRAGHandler::new();
422 handler.initialize(cfg).await?;
423 let summary = handler.load_document_with_options(path, true).await?;
424
425 if format == "json" {
426 println!(
427 "{}",
428 serde_json::json!({
429 "status": "indexed",
430 "document": path.display().to_string(),
431 "workspace": workspace.display().to_string(),
432 "details": summary,
433 })
434 );
435 } else {
436 println!(
437 "✅ Indexed `{}` into `{}`",
438 path.display(),
439 workspace.display()
440 );
441 println!(" {}", summary);
442 println!("\nAsk a question:");
443 println!(
444 " graphrag ask \"your question\" --workspace {}",
445 workspace.display()
446 );
447 }
448 Ok(())
449}
450
451async fn run_ask(
452 query: &str,
453 workspace: &std::path::Path,
454 ollama: bool,
455 format: &str,
456) -> Result<()> {
457 if !workspace.exists() {
458 return Err(color_eyre::eyre::eyre!(
459 "Workspace not found: {}. Run `graphrag index <file>` first.",
460 workspace.display()
461 ));
462 }
463 let cfg = turnkey_config(workspace, ollama, None);
464 let handler = handlers::graphrag::GraphRAGHandler::new();
465 handler.initialize(cfg).await?;
466 let (answer, sources) = handler.query_with_raw(query).await?;
467
468 if format == "json" {
469 println!(
470 "{}",
471 serde_json::json!({"query": query, "answer": answer, "sources": sources})
472 );
473 } else {
474 println!("📝 {}\n", query);
475 println!("💡 {}\n", answer);
476 if !sources.is_empty() {
477 println!("📚 Sources:");
478 for (i, src) in sources.iter().enumerate() {
479 println!(" {}. {}", i + 1, src);
480 }
481 }
482 }
483 Ok(())
484}
485
486fn run_validate(config_file: &std::path::Path, format: &str) -> Result<()> {
487 use graphrag_core::config::json5_loader::{detect_config_format, ConfigFormat};
488 use graphrag_core::config::setconfig::SetConfig;
489
490 if !config_file.exists() {
491 if format == "json" {
492 println!(
493 "{}",
494 serde_json::json!({"valid": false, "error": format!("File not found: {}", config_file.display())})
495 );
496 } else {
497 println!("❌ File not found: {}", config_file.display());
498 }
499 return Ok(());
500 }
501
502 let fmt = match detect_config_format(config_file) {
503 Some(f) => f,
504 None => {
505 if format == "json" {
506 println!(
507 "{}",
508 serde_json::json!({"valid": false, "error": "Unsupported file format. Use .toml, .json, or .json5"})
509 );
510 } else {
511 println!("❌ Unsupported file format. Use .toml, .json, or .json5");
512 }
513 return Ok(());
514 },
515 };
516
517 let content = std::fs::read_to_string(config_file)
518 .map_err(|e| color_eyre::eyre::eyre!("Cannot read file: {}", e))?;
519
520 let result: std::result::Result<SetConfig, String> = match fmt {
521 ConfigFormat::Toml => toml::from_str(&content).map_err(|e| format!("{}", e)),
522 ConfigFormat::Json => serde_json::from_str(&content).map_err(|e| format!("{}", e)),
523 ConfigFormat::Json5 => {
524 #[cfg(feature = "json5-support")]
525 {
526 json5::from_str(&content).map_err(|e| format!("{}", e))
527 }
528 #[cfg(not(feature = "json5-support"))]
529 {
530 Err("JSON5 support not enabled".to_string())
531 }
532 },
533 ConfigFormat::Yaml => Err("YAML support not enabled".to_string()),
534 };
535
536 match result {
537 Ok(set_config) => {
538 let config = set_config.to_graphrag_config();
539 if format == "json" {
540 println!(
541 "{}",
542 serde_json::json!({
543 "valid": true,
544 "format": format!("{:?}", fmt),
545 "approach": set_config.mode.approach,
546 "ollama_enabled": config.ollama.enabled,
547 "chunk_size": config.chunk_size,
548 })
549 );
550 } else {
551 println!("✅ Configuration is valid!");
552 println!(" Format: {:?}", fmt);
553 println!(" Approach: {}", set_config.mode.approach);
554 println!(
555 " Ollama: {}",
556 if config.ollama.enabled {
557 "enabled"
558 } else {
559 "disabled"
560 }
561 );
562 println!(" Chunk size: {}", config.chunk_size);
563 }
564 },
565 Err(err) => {
566 if format == "json" {
567 println!("{}", serde_json::json!({"valid": false, "error": err}));
568 } else {
569 println!("❌ Invalid configuration:\n {}", err);
570 }
571 },
572 }
573
574 Ok(())
575}
576
577async fn run_tui(config_path: Option<PathBuf>, workspace: Option<String>) -> Result<()> {
578 setup_tui_logging()?;
579 let mut app = App::new(config_path, workspace)?;
580 app.run().await?;
581 Ok(())
582}
583
584async fn handle_workspace_commands(action: WorkspaceCommands) -> Result<()> {
585 let workspace_manager = workspace::WorkspaceManager::new()?;
586
587 match action {
588 WorkspaceCommands::List => {
589 let workspaces = workspace_manager.list_workspaces().await?;
590
591 if workspaces.is_empty() {
592 println!("No workspaces found.");
593 println!("\nCreate a workspace with: graphrag workspace create <name>");
594 } else {
595 println!("Available workspaces:\n");
596 for ws in workspaces {
597 println!(" 📁 {} ({})", ws.name, ws.id);
598 println!(
599 " Created: {}",
600 ws.created_at.format("%Y-%m-%d %H:%M:%S")
601 );
602 println!(
603 " Last accessed: {}",
604 ws.last_accessed.format("%Y-%m-%d %H:%M:%S")
605 );
606 if let Some(ref cfg) = ws.config_path {
607 println!(" Config: {}", cfg.display());
608 }
609 println!();
610 }
611 }
612 },
613 WorkspaceCommands::Create { name } => {
614 let workspace = workspace_manager.create_workspace(name.clone()).await?;
615 println!("✅ Workspace created successfully!");
616 println!(" Name: {}", workspace.name);
617 println!(" ID: {}", workspace.id);
618 println!("\nUse it with: graphrag tui --workspace {}", workspace.id);
619 },
620 WorkspaceCommands::Info { id } => match workspace_manager.load_metadata(&id).await {
621 Ok(workspace) => {
622 println!("Workspace Information:\n");
623 println!(" Name: {}", workspace.name);
624 println!(" ID: {}", workspace.id);
625 println!(
626 " Created: {}",
627 workspace.created_at.format("%Y-%m-%d %H:%M:%S")
628 );
629 println!(
630 " Last accessed: {}",
631 workspace.last_accessed.format("%Y-%m-%d %H:%M:%S")
632 );
633 if let Some(ref cfg) = workspace.config_path {
634 println!(" Config: {}", cfg.display());
635 }
636
637 let history_path = workspace_manager.query_history_path(&id);
638 if history_path.exists() {
639 if let Ok(history) = query_history::QueryHistory::load(&history_path).await {
640 println!("\n Total queries: {}", history.total_queries());
641 }
642 }
643 },
644 Err(e) => {
645 eprintln!("❌ Error loading workspace: {}", e);
646 eprintln!("\nList available workspaces with: graphrag workspace list");
647 },
648 },
649 WorkspaceCommands::Delete { id } => {
650 workspace_manager.delete_workspace(&id).await?;
651 println!("✅ Workspace deleted: {}", id);
652 },
653 }
654
655 Ok(())
656}
657
658async fn run_setup_wizard(template: Option<String>, output: PathBuf) -> Result<()> {
659 use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
660 use std::fs;
661
662 let theme = ColorfulTheme::default();
663
664 println!(
665 "\n╔════════════════════════════════════════════════════════════╗\n\
666 ║ GraphRAG Configuration Setup Wizard ║\n\
667 ╚════════════════════════════════════════════════════════════╝"
668 );
669 println!();
670
671 let use_case = if let Some(ref t) = template {
672 t.clone()
673 } else {
674 let options = vec![
675 "General purpose - Mixed documents, articles (Recommended)",
676 "Legal documents - Contracts, agreements, regulations",
677 "Medical documents - Clinical notes, patient records",
678 "Financial documents - Reports, SEC filings, analysis",
679 "Technical documentation - API docs, code documentation",
680 ];
681
682 let selection = Select::with_theme(&theme)
683 .with_prompt("Select your use case")
684 .items(&options)
685 .default(0)
686 .interact()?;
687
688 match selection {
689 0 => "general",
690 1 => "legal",
691 2 => "medical",
692 3 => "financial",
693 4 => "technical",
694 _ => "general",
695 }
696 .to_string()
697 };
698
699 println!("\n Selected template: {}\n", use_case);
700
701 let llm_options = vec![
702 "Local Ollama (Recommended - free, private, runs locally)",
703 "No LLM (Pattern-based extraction only, faster but less accurate)",
704 ];
705
706 let llm_selection = Select::with_theme(&theme)
707 .with_prompt("Select LLM provider")
708 .items(&llm_options)
709 .default(0)
710 .interact()?;
711
712 let ollama_enabled = llm_selection == 0;
713
714 let mut ollama_host = "localhost".to_string();
715 let mut ollama_port: u16 = 11434;
716 let mut chat_model = "llama3.2:3b".to_string();
717
718 if ollama_enabled {
719 println!("\n Ollama Configuration:");
720
721 ollama_host = Input::with_theme(&theme)
722 .with_prompt(" Ollama host")
723 .default("localhost".to_string())
724 .interact_text()?;
725
726 let port_str: String = Input::with_theme(&theme)
727 .with_prompt(" Ollama port")
728 .default("11434".to_string())
729 .interact_text()?;
730
731 ollama_port = port_str.parse().unwrap_or(11434);
732
733 chat_model = Input::with_theme(&theme)
734 .with_prompt(" Chat model")
735 .default("llama3.2:3b".to_string())
736 .interact_text()?;
737 }
738
739 let output_dir: String = Input::with_theme(&theme)
740 .with_prompt("Output directory for graph data")
741 .default("./graphrag-output".to_string())
742 .interact_text()?;
743
744 println!("\n Generating configuration...\n");
745
746 let config_content = generate_config(
747 &use_case,
748 ollama_enabled,
749 &ollama_host,
750 ollama_port,
751 &chat_model,
752 &output_dir,
753 );
754
755 if output.exists() {
756 let overwrite = Confirm::with_theme(&theme)
757 .with_prompt(format!(
758 "File {} already exists. Overwrite?",
759 output.display()
760 ))
761 .default(false)
762 .interact()?;
763
764 if !overwrite {
765 println!("\n Setup cancelled.");
766 return Ok(());
767 }
768 }
769
770 fs::write(&output, config_content)?;
771
772 println!(" ✅ Configuration saved to: {}\n", output.display());
773 println!("╔════════════════════════════════════════════════════════════╗");
774 println!("║ Next Steps ║");
775 println!("╠════════════════════════════════════════════════════════════╣");
776 println!("║ 1. Start the TUI: ║");
777 println!(
778 "║ graphrag tui --config {} ║",
779 output.display()
780 );
781 println!("║ ║");
782 println!("║ 2. Load a document in the TUI: ║");
783 println!("║ /load path/to/your/document.txt ║");
784 println!("║ ║");
785 println!("║ 3. Query your knowledge graph: ║");
786 println!("║ Type your question and press Enter ║");
787 println!("╚════════════════════════════════════════════════════════════╝");
788
789 if ollama_enabled {
790 println!(
791 "\n 💡 Tip: Make sure Ollama is running at {}:{}",
792 ollama_host, ollama_port
793 );
794 println!(" Start it with: ollama serve");
795 println!(" Pull model with: ollama pull {}", chat_model);
796 }
797
798 Ok(())
799}
800
801fn generate_config(
802 use_case: &str,
803 ollama_enabled: bool,
804 ollama_host: &str,
805 ollama_port: u16,
806 chat_model: &str,
807 output_dir: &str,
808) -> String {
809 let entity_types = match use_case {
810 "legal" => {
811 r#"["PARTY", "PERSON", "ORGANIZATION", "DATE", "MONETARY_VALUE", "JURISDICTION", "CLAUSE_TYPE", "OBLIGATION"]"#
812 },
813 "medical" => {
814 r#"["PATIENT", "DIAGNOSIS", "MEDICATION", "PROCEDURE", "SYMPTOM", "LAB_VALUE", "PROVIDER", "DATE"]"#
815 },
816 "financial" => {
817 r#"["COMPANY", "TICKER", "PERSON", "MONETARY_VALUE", "PERCENTAGE", "DATE", "METRIC", "INDUSTRY"]"#
818 },
819 "technical" => {
820 r#"["FUNCTION", "CLASS", "MODULE", "API_ENDPOINT", "PARAMETER", "VERSION", "DEPENDENCY"]"#
821 },
822 _ => r#"["PERSON", "ORGANIZATION", "LOCATION", "DATE", "EVENT"]"#,
823 };
824
825 let approach = match use_case {
826 "legal" | "medical" => "semantic",
827 "technical" => "algorithmic",
828 _ => "hybrid",
829 };
830
831 let chunk_size = match use_case {
832 "legal" => 500,
833 "medical" => 750,
834 "technical" => 600,
835 "financial" => 1200,
836 _ => 1000,
837 };
838
839 let use_gleaning = ollama_enabled && matches!(use_case, "legal" | "medical" | "financial");
840
841 format!(
842 r#"# GraphRAG Configuration
843# Generated by: graphrag setup
844# Template: {use_case}
845# ===================================================
846
847output_dir = "{output_dir}"
848approach = "{approach}"
849
850# Text chunking settings
851chunk_size = {chunk_size}
852chunk_overlap = {overlap}
853
854# Retrieval settings
855top_k_results = 10
856similarity_threshold = 0.7
857
858[embeddings]
859backend = "{embedding_backend}"
860dimension = 384
861fallback_to_hash = true
862batch_size = 32
863
864[entities]
865min_confidence = 0.7
866entity_types = {entity_types}
867use_gleaning = {use_gleaning}
868max_gleaning_rounds = 3
869
870[graph]
871max_connections = 10
872similarity_threshold = 0.8
873extract_relationships = true
874relationship_confidence_threshold = 0.5
875
876[graph.traversal]
877max_depth = 3
878max_paths = 10
879use_edge_weights = true
880min_relationship_strength = 0.3
881
882[retrieval]
883top_k = 10
884search_algorithm = "cosine"
885
886[parallel]
887enabled = true
888num_threads = 0
889min_batch_size = 10
890
891[ollama]
892enabled = {ollama_enabled}
893host = "{ollama_host}"
894port = {ollama_port}
895chat_model = "{chat_model}"
896embedding_model = "nomic-embed-text"
897timeout_seconds = 30
898enable_caching = true
899
900[auto_save]
901enabled = false
902interval_seconds = 300
903max_versions = 5
904"#,
905 use_case = use_case,
906 output_dir = output_dir,
907 approach = approach,
908 chunk_size = chunk_size,
909 overlap = chunk_size / 5,
910 embedding_backend = if ollama_enabled { "ollama" } else { "hash" },
911 entity_types = entity_types,
912 use_gleaning = use_gleaning,
913 ollama_enabled = ollama_enabled,
914 ollama_host = ollama_host,
915 ollama_port = ollama_port,
916 chat_model = chat_model,
917 )
918}
919
920pub fn install_panic_hook() {
922 let original_hook = std::panic::take_hook();
923 std::panic::set_hook(Box::new(move |panic_info| {
924 let _ = crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen);
925 let _ = crossterm::terminal::disable_raw_mode();
926 original_hook(panic_info);
927 }));
928}
929
930fn setup_logging(debug: bool) -> Result<()> {
931 use tracing_subscriber::EnvFilter;
932
933 let filter = if debug {
934 EnvFilter::new("graphrag_cli=debug,graphrag_core=debug")
935 } else {
936 EnvFilter::new("graphrag_cli=info,graphrag_core=info")
937 };
938
939 tracing_subscriber::fmt()
940 .with_env_filter(filter)
941 .with_writer(std::io::stderr)
942 .with_target(false)
943 .with_file(true)
944 .with_line_number(true)
945 .init();
946
947 Ok(())
948}
949
950fn setup_tui_logging() -> Result<()> {
951 use std::fs::OpenOptions;
952 use std::sync::Arc;
953 use tracing_subscriber::EnvFilter;
954
955 let log_dir = dirs::data_local_dir()
956 .unwrap_or_else(|| PathBuf::from("."))
957 .join("graphrag-cli")
958 .join("logs");
959
960 std::fs::create_dir_all(&log_dir)?;
961
962 let log_file = log_dir.join("graphrag-cli.log");
963 let file = OpenOptions::new()
964 .create(true)
965 .append(true)
966 .open(log_file)?;
967
968 let filter = EnvFilter::new("graphrag_cli=warn,graphrag_core=warn");
969
970 tracing_subscriber::fmt()
971 .with_env_filter(filter)
972 .with_writer(Arc::new(file))
973 .with_target(false)
974 .with_file(false)
975 .with_line_number(false)
976 .with_ansi(false)
977 .init();
978
979 Ok(())
980}