Skip to main content

sqlite_graphrag/commands/deep_research/
args.rs

1//! CLI surface of the `deep-research` subcommand.
2
3/// Arguments for the `deep-research` subcommand.
4#[derive(clap::Args)]
5#[command(
6    about = "Deep parallel multi-hop GraphRAG research via query decomposition",
7    after_long_help = "CONTRACT:\n  \
8        stdout = pretty JSON envelope only (machine-readable).\n  \
9        stderr = tracing / progress / diagnostics only.\n  \
10        Never redirect with `&>` or `2>&1` into the same file as stdout — that\n  \
11        contaminates the JSON and breaks jaq/jq. Prefer:\n  \
12        sqlite-graphrag deep-research \"q\" > out.json 2>/dev/null\n  \
13        or --output out.json (atomic write via atomwrite algorithm).\n\n\
14EXAMPLES:\n  \
15        # Basic deep research (single-token queries auto-expand into aspects)\n  \
16        sqlite-graphrag deep-research \"alice\"\n\n  \
17        # With custom parameters\n  \
18        sqlite-graphrag deep-research \"auth\" --k 20 --max-hops 3 --max-sub-queries 7\n\n  \
19        # Include full memory bodies in output\n  \
20        sqlite-graphrag deep-research \"auth\" --with-bodies\n\n  \
21        # Manual sub-queries (one query per line)\n  \
22        sqlite-graphrag deep-research \"alice\" --sub-query-strategy manual \\\n  \
23          --sub-queries-file aspects.txt\n\n  \
24        # Atomic JSON file (crash-safe; preferred for large --with-bodies runs)\n  \
25        sqlite-graphrag deep-research \"auth\" --output /tmp/dr.json\n\n  \
26        # Tune RRF and graph scoring\n  \
27        sqlite-graphrag deep-research \"auth and deployment\" --rrf-k 60 --graph-decay 0.7"
28)]
29pub struct DeepResearchArgs {
30    /// Research query to decompose and search.
31    #[arg(
32        value_name = "QUERY",
33        allow_hyphen_values = true,
34        help = "Research query to decompose and search"
35    )]
36    pub query: String,
37    /// Results per sub-query (Recall@20 captures 95%+ relevant hits).
38    #[arg(
39        long,
40        short,
41        aliases = ["limit", "top-k"],
42        default_value_t = 20,
43        value_parser = crate::parsers::parse_k_range,
44        help = "Results per sub-query (Recall@20 captures 95%+ relevant hits)"
45    )]
46    pub k: usize,
47    /// Maximum sub-queries from decomposition (covers complex multi-hop queries).
48    #[arg(
49        long,
50        default_value_t = 7,
51        value_parser = crate::parsers::parse_sub_queries_range,
52        help = "Maximum sub-queries (covers complex multi-hop queries)"
53    )]
54    pub max_sub_queries: usize,
55    /// Multi-hop graph traversal depth (sweet spot: 2-3 hops).
56    #[arg(
57        long,
58        default_value_t = 3,
59        value_parser = crate::parsers::parse_hops_range_usize,
60        help = "Multi-hop graph traversal depth (sweet spot: 2-3 hops)"
61    )]
62    pub max_hops: usize,
63    /// Minimum edge weight for graph traversal.
64    #[arg(
65        long,
66        default_value_t = 0.3,
67        help = "Minimum edge weight for graph traversal"
68    )]
69    pub min_weight: f64,
70    /// Maximum concurrent sub-queries (default: min(cpus, 8)).
71    #[arg(long, help = "Maximum concurrent sub-queries (default: min(cpus, 8))")]
72    pub max_concurrency: Option<usize>,
73    /// Timeout per sub-query in seconds.
74    #[arg(long, default_value_t = 30, help = "Timeout per sub-query in seconds")]
75    pub timeout: u64,
76    /// Include full memory bodies in results.
77    #[arg(
78        long,
79        default_value_t = false,
80        help = "Include full memory bodies in results"
81    )]
82    pub with_bodies: bool,
83    /// Maximum results after deduplication.
84    #[arg(
85        long,
86        default_value_t = 50,
87        value_parser = crate::parsers::parse_k_range,
88        help = "Maximum results after deduplication"
89    )]
90    pub max_results: usize,
91    /// RRF k parameter controlling score smoothing (higher = less weight on top ranks).
92    #[arg(
93        long,
94        default_value_t = 60.0,
95        help = "RRF k parameter (higher = less weight on top ranks)"
96    )]
97    pub rrf_k: f64,
98    /// Decay factor applied to graph scores per hop (score = seed_score * decay^hop).
99    #[arg(
100        long,
101        default_value_t = 0.7,
102        help = "Graph score decay factor per hop (0.0-1.0)"
103    )]
104    pub graph_decay: f64,
105    /// Minimum score threshold for graph-expanded results (filters noise).
106    #[arg(
107        long,
108        default_value_t = 0.05,
109        help = "Minimum score threshold for graph-expanded results"
110    )]
111    pub graph_min_score: f64,
112    /// Limit top-k neighbours followed per entity per hop (None = unlimited).
113    #[arg(
114        long,
115        help = "Limit neighbours per entity per hop for graph traversal (default: unlimited)"
116    )]
117    pub max_neighbors_per_hop: Option<usize>,
118    /// Namespace (flag / XDG namespace.default / global).
119    #[arg(long, help = "Namespace (flag / XDG namespace.default / global)")]
120    pub namespace: Option<String>,
121    /// Research mode. `none` (local heuristic) is the only accepted value.
122    ///
123    /// The doc used to offer `claude-code` and `codex` while `value_parser`
124    /// accepted neither, so the help advertised two values clap rejected on
125    /// sight. v1.2.0 removed both backends; the flag survives, hidden, so
126    /// existing `--mode none` invocations keep parsing.
127    #[arg(long, default_value = "none", value_parser = ["none"], hide = true)]
128    pub mode: String,
129    /// Maximum LLM cost in USD. Inert while `--mode` accepts only `none`.
130    ///
131    /// Kept as an accepted flag so scripted invocations that pass it do not
132    /// break, and reported as inert by the handler rather than silently ignored.
133    #[arg(
134        long,
135        value_name = "USD",
136        help = "Max LLM cost in USD (inert: no LLM research mode is available)"
137    )]
138    pub max_cost_usd: Option<f64>,
139    /// JSON output (always on, kept for consistency).
140    #[arg(long, hide = true)]
141    pub json: bool,
142    /// Database path.
143    #[arg(long)]
144    pub db: Option<String>,
145    /// Sub-query strategy: `heuristic` (default, syntactic + single-token aspects)
146    /// or `manual` (requires `--sub-queries-file`).
147    #[arg(
148        long,
149        default_value = "heuristic",
150        value_parser = ["heuristic", "manual"],
151        help = "Sub-query strategy: heuristic (default) or manual"
152    )]
153    pub sub_query_strategy: String,
154    /// Path to a UTF-8 text file with one sub-query per line (required when
155    /// `--sub-query-strategy manual`). Empty lines and `#` comments are ignored.
156    #[arg(
157        long,
158        value_name = "PATH",
159        help = "File with one sub-query per line (manual strategy)"
160    )]
161    pub sub_queries_file: Option<std::path::PathBuf>,
162    /// Write the JSON envelope atomically to this path (tempfile→fsync→rename).
163    /// When set, stdout receives a short confirmation JSON
164    /// `{ "written": "<path>", "bytes": N, "blake3": "..." }` instead of the full
165    /// envelope — preventing shell redirect truncation of multi-MB payloads.
166    #[arg(
167        short = 'o',
168        long,
169        value_name = "PATH",
170        help = "Atomic JSON output path (atomwrite algorithm; short -o)"
171    )]
172    pub output: Option<std::path::PathBuf>,
173}