Skip to main content

graphrag_cli/
lib.rs

1//! GraphRAG CLI library entry point.
2//!
3//! Exposes [`run`] so the `graphrag` meta-crate (and tests) can invoke the
4//! full CLI without going through a subprocess.
5
6pub 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// ──────────────────────────────────────────────────────────────────────────────
24// CLI types
25// ──────────────────────────────────────────────────────────────────────────────
26
27#[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    /// Configuration file path
33    #[arg(short, long, value_name = "FILE")]
34    pub config: Option<PathBuf>,
35
36    /// Workspace name
37    #[arg(short, long)]
38    pub workspace: Option<String>,
39
40    /// Enable debug logging
41    #[arg(short, long)]
42    pub debug: bool,
43
44    /// Output format: text (default) or json (for scripting/CI)
45    #[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 a document into a workspace. No config file required.
55    ///
56    /// Example: `graphrag index ./book.txt`
57    /// Example: `graphrag index ./book.txt --workspace ./my-graph --ollama`
58    Index {
59        /// Path to document file (txt, md, pdf via plain text)
60        path: PathBuf,
61        /// Workspace directory (created if missing)
62        #[arg(long, default_value = "./graphrag-data")]
63        workspace: PathBuf,
64        /// Enable Ollama LLM extraction (requires running `ollama serve`)
65        #[arg(long)]
66        ollama: bool,
67        /// Override chunk size (default 1000)
68        #[arg(long)]
69        chunk_size: Option<usize>,
70    },
71
72    /// Ask a question against an indexed workspace. No config file required.
73    ///
74    /// Example: `graphrag ask "Who is Diotima?"`
75    Ask {
76        /// Question to ask
77        query: String,
78        /// Workspace directory created by `graphrag index`
79        #[arg(long, default_value = "./graphrag-data")]
80        workspace: PathBuf,
81        /// Enable Ollama LLM for semantic answering
82        #[arg(long)]
83        ollama: bool,
84    },
85
86    /// Start interactive TUI (default)
87    Tui,
88
89    /// Interactive setup wizard - creates graphrag.toml with guided configuration
90    Setup {
91        /// Template to use: general, legal, medical, financial, technical
92        #[arg(short, long)]
93        template: Option<String>,
94
95        /// Output path for configuration file
96        #[arg(short, long, default_value = "./graphrag.toml")]
97        output: PathBuf,
98    },
99
100    /// Validate a configuration file (TOML or JSON5)
101    Validate {
102        /// Path to the configuration file to validate
103        config_file: PathBuf,
104    },
105
106    /// Initialize GraphRAG with configuration (deprecated: prefer TUI with /config)
107    Init {
108        /// Configuration file path
109        config: PathBuf,
110    },
111
112    /// Load a document into the knowledge graph (deprecated: prefer TUI with /load)
113    Load {
114        /// Document file path
115        document: PathBuf,
116
117        /// Configuration file (required if not already initialized)
118        #[arg(short, long)]
119        config: Option<PathBuf>,
120    },
121
122    /// Execute a query (deprecated: prefer TUI)
123    Query {
124        /// Query text
125        query: String,
126
127        /// Configuration file (required if not already initialized)
128        #[arg(short, long)]
129        config: Option<PathBuf>,
130    },
131
132    /// List entities in the knowledge graph (deprecated: prefer TUI with /entities)
133    Entities {
134        /// Filter by name or type
135        filter: Option<String>,
136
137        /// Configuration file
138        #[arg(short, long)]
139        config: Option<PathBuf>,
140    },
141
142    /// Configuration file
143    Stats {
144        /// Configuration file
145        #[arg(short, long)]
146        config: Option<PathBuf>,
147    },
148
149    /// Run full E2E benchmark (Init -> Load -> Query) in memory
150    Bench {
151        /// Configuration file
152        #[arg(short, long)]
153        config: PathBuf,
154
155        /// Book text file
156        #[arg(short, long)]
157        book: PathBuf,
158
159        /// Pipe-separated list of questions e.g. "Q1?|Q2?"
160        #[arg(short, long)]
161        questions: String,
162    },
163
164    /// Workspace management commands
165    Workspace {
166        #[command(subcommand)]
167        action: WorkspaceCommands,
168    },
169}
170
171#[derive(Subcommand)]
172pub enum WorkspaceCommands {
173    /// List all workspaces
174    List,
175
176    /// Create a new workspace
177    Create { name: String },
178
179    /// Show workspace information
180    Info { id: String },
181
182    /// Delete a workspace
183    Delete { id: String },
184}
185
186// ──────────────────────────────────────────────────────────────────────────────
187// Public entry point
188// ──────────────────────────────────────────────────────────────────────────────
189
190/// Run the full GraphRAG CLI. Called by both the `graphrag-cli` binary and
191/// the `graphrag` meta-crate binary.
192pub 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
383// ──────────────────────────────────────────────────────────────────────────────
384// Internal helpers
385// ──────────────────────────────────────────────────────────────────────────────
386
387async fn load_config_from_file(path: &std::path::Path) -> Result<graphrag_core::Config> {
388    config::load_config(path).await
389}
390
391/// Build a turnkey config for the given workspace, optionally enabling Ollama.
392fn 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
920/// Restore the terminal on panic (called at the top of [`run`]).
921pub 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}