#[derive(clap::Args)]
#[command(
about = "Deep parallel multi-hop GraphRAG research via query decomposition",
after_long_help = "CONTRACT:\n \
stdout = pretty JSON envelope only (machine-readable).\n \
stderr = tracing / progress / diagnostics only.\n \
Never redirect with `&>` or `2>&1` into the same file as stdout — that\n \
contaminates the JSON and breaks jaq/jq. Prefer:\n \
sqlite-graphrag deep-research \"q\" > out.json 2>/dev/null\n \
or --output out.json (atomic write via atomwrite algorithm).\n\n\
EXAMPLES:\n \
# Basic deep research (single-token queries auto-expand into aspects)\n \
sqlite-graphrag deep-research \"alice\"\n\n \
# With custom parameters\n \
sqlite-graphrag deep-research \"auth\" --k 20 --max-hops 3 --max-sub-queries 7\n\n \
# Include full memory bodies in output\n \
sqlite-graphrag deep-research \"auth\" --with-bodies\n\n \
# Manual sub-queries (one query per line)\n \
sqlite-graphrag deep-research \"alice\" --sub-query-strategy manual \\\n \
--sub-queries-file aspects.txt\n\n \
# Atomic JSON file (crash-safe; preferred for large --with-bodies runs)\n \
sqlite-graphrag deep-research \"auth\" --output /tmp/dr.json\n\n \
# Tune RRF and graph scoring\n \
sqlite-graphrag deep-research \"auth and deployment\" --rrf-k 60 --graph-decay 0.7"
)]
pub struct DeepResearchArgs {
#[arg(
value_name = "QUERY",
allow_hyphen_values = true,
help = "Research query to decompose and search"
)]
pub query: String,
#[arg(
long,
short,
aliases = ["limit", "top-k"],
default_value_t = 20,
value_parser = crate::parsers::parse_k_range,
help = "Results per sub-query (Recall@20 captures 95%+ relevant hits)"
)]
pub k: usize,
#[arg(
long,
default_value_t = 7,
value_parser = crate::parsers::parse_sub_queries_range,
help = "Maximum sub-queries (covers complex multi-hop queries)"
)]
pub max_sub_queries: usize,
#[arg(
long,
default_value_t = 3,
value_parser = crate::parsers::parse_hops_range_usize,
help = "Multi-hop graph traversal depth (sweet spot: 2-3 hops)"
)]
pub max_hops: usize,
#[arg(
long,
default_value_t = 0.3,
help = "Minimum edge weight for graph traversal"
)]
pub min_weight: f64,
#[arg(long, help = "Maximum concurrent sub-queries (default: min(cpus, 8))")]
pub max_concurrency: Option<usize>,
#[arg(long, default_value_t = 30, help = "Timeout per sub-query in seconds")]
pub timeout: u64,
#[arg(
long,
default_value_t = false,
help = "Include full memory bodies in results"
)]
pub with_bodies: bool,
#[arg(
long,
default_value_t = 50,
value_parser = crate::parsers::parse_k_range,
help = "Maximum results after deduplication"
)]
pub max_results: usize,
#[arg(
long,
default_value_t = 60.0,
help = "RRF k parameter (higher = less weight on top ranks)"
)]
pub rrf_k: f64,
#[arg(
long,
default_value_t = 0.7,
help = "Graph score decay factor per hop (0.0-1.0)"
)]
pub graph_decay: f64,
#[arg(
long,
default_value_t = 0.05,
help = "Minimum score threshold for graph-expanded results"
)]
pub graph_min_score: f64,
#[arg(
long,
help = "Limit neighbours per entity per hop for graph traversal (default: unlimited)"
)]
pub max_neighbors_per_hop: Option<usize>,
#[arg(long, help = "Namespace (flag / XDG namespace.default / global)")]
pub namespace: Option<String>,
#[arg(long, default_value = "none", value_parser = ["none"], hide = true)]
pub mode: String,
#[arg(
long,
value_name = "USD",
help = "Max LLM cost in USD (inert: no LLM research mode is available)"
)]
pub max_cost_usd: Option<f64>,
#[arg(long, hide = true)]
pub json: bool,
#[arg(long)]
pub db: Option<String>,
#[arg(
long,
default_value = "heuristic",
value_parser = ["heuristic", "manual"],
help = "Sub-query strategy: heuristic (default) or manual"
)]
pub sub_query_strategy: String,
#[arg(
long,
value_name = "PATH",
help = "File with one sub-query per line (manual strategy)"
)]
pub sub_queries_file: Option<std::path::PathBuf>,
#[arg(
short = 'o',
long,
value_name = "PATH",
help = "Atomic JSON output path (atomwrite algorithm; short -o)"
)]
pub output: Option<std::path::PathBuf>,
}