| from __future__ import annotations |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
|
|
| from hydradeck.config import ( |
| UserConfig, |
| resolve_api_key, |
| resolve_base_url, |
| resolve_model, |
| resolve_pdf_compiler, |
| resolve_template, |
| save_config, |
| ) |
| from hydradeck.core.types import RunConfig |
| from hydradeck.pipeline import run |
| from hydradeck.resources_pack import build_resources_pack |
|
|
|
|
| def _build_parser() -> argparse.ArgumentParser: |
| p = argparse.ArgumentParser(prog="hydradeck") |
| sub = p.add_subparsers(dest="cmd", required=True) |
|
|
| runp = sub.add_parser("run", help="Run Grok deep research pipeline") |
| runp.add_argument("--topic", required=True, help="Research topic") |
| runp.add_argument("--out", required=True, help="Output directory or .zip") |
| runp.add_argument("--iterations", type=int, default=3, help="Persona iteration rounds") |
| runp.add_argument("--max-sources", type=int, default=10, help="Max sources to include") |
| runp.add_argument( |
| "--min-words", |
| type=int, |
| default=12000, |
| help="Target minimum words (guidance to model; markdown is primary)", |
| ) |
| runp.add_argument("--base-url", default=None, help="API base URL") |
| runp.add_argument("--model", default=None, help="Model name") |
| runp.add_argument( |
| "--keep-stage", |
| action="store_true", |
| help="If --out is a .zip, keep the staging directory on disk", |
| ) |
| runp.add_argument( |
| "--seed-url", |
| action="append", |
| default=None, |
| help="Seed URL to include as source (can be repeated)", |
| ) |
| runp.add_argument("--llm-timeout", type=float, default=180.0, help="LLM timeout seconds") |
| runp.add_argument("--mock", action="store_true", help="Use deterministic mock (no network)") |
| runp.add_argument("--verbose", action="store_true", help="Verbose logging") |
| runp.add_argument( |
| "--heartbeat", |
| action="store_true", |
| help="Emit periodic heartbeat during long network calls", |
| ) |
| runp.add_argument( |
| "--progress", |
| action="store_true", |
| help="Show a progress bar for generation stages", |
| ) |
| runp.add_argument( |
| "--request-budget", |
| type=float, |
| default=20.0, |
| help="Per-request timeout budget (seconds)", |
| ) |
| runp.add_argument( |
| "--verbatim", |
| action="store_true", |
| help="Write model-produced artifacts verbatim (no rendering/rewriting)", |
| ) |
| runp.add_argument( |
| "--no-archive-prompts", |
| action="store_true", |
| help="Do not archive prompts/requests in the output package", |
| ) |
| runp.add_argument( |
| "--quality-gate", |
| action="store_true", |
| help="Require passing third-party score before writing outputs", |
| ) |
| runp.add_argument( |
| "--min-quality", |
| type=float, |
| default=0.85, |
| help="Minimum quality score (0-1)", |
| ) |
| runp.add_argument( |
| "--quality-attempts", |
| type=int, |
| default=3, |
| help="Max regeneration attempts to meet quality gate", |
| ) |
| runp.add_argument( |
| "--archive-snapshots", |
| action="store_true", |
| help="Fetch and archive source page snapshots into resources/snapshots", |
| ) |
| runp.add_argument( |
| "--snapshot-timeout", |
| type=float, |
| default=25.0, |
| help="Per-URL snapshot fetch timeout (seconds)", |
| ) |
| runp.add_argument( |
| "--snapshot-total-timeout", |
| type=float, |
| default=60.0, |
| help="Total time budget for all snapshots (seconds)", |
| ) |
|
|
| prep = sub.add_parser( |
| "pre", |
| help="Generate a preset pre-research package (no API key required)", |
| ) |
| prep.add_argument("--preset", required=True, help="Preset name (e.g. rynnbrain)") |
| prep.add_argument("--out", required=True, help="Output directory or .zip") |
| prep.add_argument( |
| "--keep-stage", |
| action="store_true", |
| help="Keep staging directory when output is .zip", |
| ) |
| prep.add_argument( |
| "--no-fetch", |
| action="store_true", |
| help="Do not fetch and archive web snapshots", |
| ) |
|
|
| models_p = sub.add_parser("models", help="List available models") |
| models_p.add_argument( |
| "--base-url", |
| default=None, |
| help="API base URL", |
| ) |
|
|
| auto_p = sub.add_parser( |
| "auto", |
| help="Run autonomous deep research (verbatim + prompts + snapshots)", |
| ) |
| auto_p.add_argument("--topic", required=True, help="Research topic") |
| auto_p.add_argument("--out", required=True, help="Output directory or .zip") |
| auto_p.add_argument( |
| "--base-url", |
| default=None, |
| help="API base URL", |
| ) |
| auto_p.add_argument( |
| "--model", |
| default=None, |
| help="Fallback model name", |
| ) |
| auto_p.add_argument( |
| "--iterations", |
| type=int, |
| default=3, |
| help="Persona iteration rounds", |
| ) |
| auto_p.add_argument( |
| "--max-sources", |
| type=int, |
| default=12, |
| help="Max sources to include", |
| ) |
| auto_p.add_argument( |
| "--module-sources", |
| type=int, |
| default=5, |
| help="Sources per query module", |
| ) |
| auto_p.add_argument( |
| "--query-count", |
| type=int, |
| default=8, |
| help="Number of queries to generate (high recall)", |
| ) |
| auto_p.add_argument( |
| "--max-query-modules", |
| type=int, |
| default=2, |
| help="Max query modules to expand into sources", |
| ) |
| auto_p.add_argument( |
| "--sources-attempts", |
| type=int, |
| default=3, |
| help="Max attempts to obtain sources (must be <=3)", |
| ) |
| auto_p.add_argument( |
| "--facts-max-pages", |
| type=int, |
| default=6, |
| help="Max pages to pass into facts extraction", |
| ) |
| auto_p.add_argument( |
| "--facts-max-chars", |
| type=int, |
| default=8000, |
| help="Max chars per page passed into facts extraction", |
| ) |
| auto_p.add_argument( |
| "--facts-target", |
| type=int, |
| default=30, |
| help="Approximate number of facts to extract", |
| ) |
| auto_p.add_argument( |
| "--judge-max-chars", |
| type=int, |
| default=12000, |
| help="Max chars per artifact passed into judge", |
| ) |
| auto_p.add_argument( |
| "--max-runtime", |
| type=float, |
| default=240.0, |
| help="Max total runtime seconds before aborting", |
| ) |
| auto_p.add_argument( |
| "--llm-timeout", |
| type=float, |
| default=180.0, |
| help="LLM timeout seconds", |
| ) |
| auto_p.add_argument( |
| "--snapshot-timeout", |
| type=float, |
| default=25.0, |
| help="Per-URL snapshot fetch timeout (seconds)", |
| ) |
| auto_p.add_argument("--mock", action="store_true", help="Use deterministic mock") |
| auto_p.add_argument("--verbose", action="store_true", help="Verbose logging") |
| auto_p.add_argument( |
| "--heartbeat", |
| action="store_true", |
| help="Emit periodic heartbeat during long network calls", |
| ) |
| auto_p.add_argument( |
| "--progress", |
| action="store_true", |
| help="Show a progress bar for generation stages", |
| ) |
| auto_p.add_argument( |
| "--request-budget", |
| type=float, |
| default=20.0, |
| help="Per-request timeout budget (seconds)", |
| ) |
| auto_p.add_argument( |
| "--min-quality", |
| type=float, |
| default=0.85, |
| help="Minimum quality score (0-1)", |
| ) |
| auto_p.add_argument( |
| "--quality-attempts", |
| type=int, |
| default=3, |
| help="Max regeneration attempts to meet quality gate", |
| ) |
|
|
| cfg_p = sub.add_parser("config", help="Persist local config (base_url/model/api_key)") |
| cfg_p.add_argument("--base-url", default=None, help="API base URL") |
| cfg_p.add_argument("--model", default=None, help="Default model") |
| cfg_p.add_argument("--api-key", default=None, help="API key (stored locally)") |
| cfg_p.add_argument( |
| "--pdf-compiler", |
| default=None, |
| help="PDF compiler backend: latexonline or texlive", |
| ) |
| cfg_p.add_argument( |
| "--template", |
| default=None, |
| help="Template: iclr2026 or plain", |
| ) |
|
|
| res_p = sub.add_parser("resources", help="One-click resources pack (no seed required)") |
| res_p.add_argument("--topic", required=True, help="Research topic") |
| res_p.add_argument("--out", required=True, help="Output directory or .zip") |
| res_p.add_argument( |
| "--base-url", |
| default=None, |
| help="API base URL", |
| ) |
| res_p.add_argument( |
| "--model", |
| default=None, |
| help="Model name", |
| ) |
| res_p.add_argument( |
| "--pdf-compiler", |
| default=resolve_pdf_compiler("auto"), |
| help="PDF compiler: auto|latexonline|texlive", |
| ) |
| res_p.add_argument( |
| "--template", |
| default=resolve_template("pretty"), |
| help="Template: pretty|plain", |
| ) |
| res_p.add_argument("--max-sources", type=int, default=8, help="Max sources") |
| res_p.add_argument("--module-sources", type=int, default=3, help="Sources per module") |
| res_p.add_argument("--llm-timeout", type=float, default=35.0, help="LLM timeout") |
| res_p.add_argument("--snapshot-timeout", type=float, default=10.0, help="Snapshot timeout") |
| res_p.add_argument( |
| "--snapshot-total-timeout", |
| type=float, |
| default=60.0, |
| help="Total time budget for all snapshots", |
| ) |
| res_p.add_argument("--max-runtime", type=float, default=180.0, help="Max runtime") |
| res_p.add_argument("--request-budget", type=float, default=15.0, help="Per-request budget") |
| res_p.add_argument("--keep-stage", action="store_true", help="Keep staging directory") |
| res_p.add_argument("--heartbeat", action="store_true", help="Heartbeat") |
| res_p.add_argument("--progress", action="store_true", help="Progress bar") |
|
|
| wiz_p = sub.add_parser("wizard", help="Guided research (interactive)") |
| wiz_p.add_argument("--out", required=False, default=None, help="Output directory or .zip") |
| return p |
|
|
|
|
| def _prompt(prompt: str, default: str | None = None) -> str: |
| suffix = f" [{default}]" if default else "" |
| v = input(prompt + suffix + ": ").strip() |
| if not v and default is not None: |
| return default |
| return v |
|
|
|
|
| def _prompt_int(prompt: str, default: int) -> int: |
| v = _prompt(prompt, str(default)) |
| try: |
| return int(v) |
| except Exception: |
| return default |
|
|
|
|
| def _prompt_float(prompt: str, default: float) -> float: |
| v = _prompt(prompt, str(default)) |
| try: |
| return float(v) |
| except Exception: |
| return default |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| args = _build_parser().parse_args(argv) |
| if args.cmd == "run": |
| base_url = resolve_base_url(args.base_url) |
| model = resolve_model(args.model) |
| cfg = RunConfig( |
| topic=args.topic, |
| out=Path(args.out), |
| base_url=base_url, |
| api_key=resolve_api_key(), |
| model=model, |
| iterations=max(int(args.iterations), 1), |
| max_sources=max(int(args.max_sources), 1), |
| min_total_words=max(int(args.min_words), 1000), |
| use_mock=bool(args.mock), |
| verbose=bool(args.verbose or args.heartbeat), |
| progress=bool(args.progress), |
| llm_timeout_s=float(args.llm_timeout), |
| request_budget_s=float(args.request_budget), |
| keep_stage=bool(args.keep_stage), |
| verbatim=bool(args.verbatim), |
| archive_prompts=not bool(args.no_archive_prompts), |
| archive_snapshots=bool(args.archive_snapshots), |
| snapshot_timeout_s=float(args.snapshot_timeout), |
| snapshot_total_timeout_s=float(args.snapshot_total_timeout), |
| quality_gate=bool(args.quality_gate), |
| min_quality_score=float(args.min_quality), |
| max_quality_attempts=int(args.quality_attempts), |
| seed_urls=args.seed_url, |
| ) |
| run(cfg) |
| return 0 |
| if args.cmd == "pre": |
| from hydradeck.presets.rynnbrain import generate |
|
|
| if str(args.preset).strip().lower() != "rynnbrain": |
| print(f"Unknown preset: {args.preset}", file=sys.stderr) |
| return 2 |
| generate( |
| out=Path(args.out), |
| keep_stage=bool(args.keep_stage), |
| fetch=not bool(args.no_fetch), |
| ) |
| return 0 |
|
|
| if args.cmd == "models": |
| from hydradeck.clients import GrokClient |
|
|
| client = GrokClient( |
| base_url=resolve_base_url(str(args.base_url) if args.base_url else None), |
| api_key=resolve_api_key(), |
| model="grok-4", |
| ) |
| for mid in client.list_models(): |
| print(mid) |
| return 0 |
|
|
| if args.cmd == "auto": |
| base_url = resolve_base_url(args.base_url) |
| model = resolve_model(args.model) |
| cfg = RunConfig( |
| topic=args.topic, |
| out=Path(args.out), |
| base_url=base_url, |
| api_key=resolve_api_key(), |
| model=model, |
| iterations=max(int(args.iterations), 1), |
| max_sources=max(int(args.max_sources), 1), |
| module_sources=max(int(args.module_sources), 1), |
| query_count=max(int(args.query_count), 1), |
| max_query_modules=max(int(args.max_query_modules), 1), |
| sources_attempts=min(max(int(args.sources_attempts), 1), 3), |
| facts_max_pages=max(int(args.facts_max_pages), 1), |
| facts_max_chars_per_page=max(int(args.facts_max_chars), 1000), |
| facts_target=max(int(args.facts_target), 5), |
| judge_max_chars=max(int(args.judge_max_chars), 2000), |
| max_total_runtime_s=float(args.max_runtime), |
| min_total_words=12000, |
| use_mock=bool(args.mock), |
| verbose=bool(args.verbose or args.heartbeat), |
| progress=bool(args.progress), |
| llm_timeout_s=float(args.llm_timeout), |
| keep_stage=False, |
| verbatim=True, |
| archive_prompts=True, |
| archive_snapshots=True, |
| snapshot_timeout_s=float(args.snapshot_timeout), |
| auto=True, |
| auto_queries=True, |
| auto_models=True, |
| quality_gate=True, |
| min_quality_score=float(args.min_quality), |
| max_quality_attempts=int(args.quality_attempts), |
| seed_urls=None, |
| ) |
| run(cfg) |
| return 0 |
|
|
| if args.cmd == "config": |
| uc = UserConfig( |
| base_url=str(args.base_url) if args.base_url else None, |
| api_key=str(args.api_key) if args.api_key else None, |
| model=str(args.model) if args.model else None, |
| pdf_compiler=str(args.pdf_compiler) if args.pdf_compiler else None, |
| template=str(args.template) if args.template else None, |
| ) |
| p = save_config(uc) |
| print(str(p)) |
| return 0 |
|
|
| if args.cmd == "resources": |
| base_url = resolve_base_url(args.base_url) |
| model = resolve_model(args.model) |
| cfg = RunConfig( |
| topic=args.topic, |
| out=Path(args.out), |
| base_url=base_url, |
| api_key=resolve_api_key(), |
| model=model, |
| pdf_compiler=str(args.pdf_compiler), |
| template=str(args.template), |
| max_sources=max(int(args.max_sources), 1), |
| module_sources=max(int(args.module_sources), 1), |
| use_mock=False, |
| verbose=bool(args.heartbeat), |
| progress=bool(args.progress), |
| llm_timeout_s=float(args.llm_timeout), |
| snapshot_timeout_s=float(args.snapshot_timeout), |
| max_total_runtime_s=float(args.max_runtime), |
| request_budget_s=float(args.request_budget), |
| keep_stage=bool(args.keep_stage), |
| ) |
| build_resources_pack(cfg) |
| return 0 |
|
|
| if args.cmd == "wizard": |
| topic = _prompt("Topic", "RynnBrain") |
| out = args.out or _prompt("Output path (.zip)", "hydradeck/out/pre.zip") |
| base_url = _prompt("Base URL (from config if empty)", "") |
| model = _prompt("Model (from config if empty)", "") |
| max_sources = _prompt_int("Max sources", 8) |
| module_sources = _prompt_int("Sources per module", 3) |
| llm_timeout = _prompt_float("LLM timeout (s)", 35.0) |
| snapshot_timeout = _prompt_float("Snapshot timeout (s)", 10.0) |
| max_runtime = _prompt_float("Max runtime (s)", 300.0) |
| request_budget = _prompt_float("Per-request budget (s)", 20.0) |
| pdf_compiler = _prompt("PDF compiler (auto|latexonline|texlive)", "auto") |
| template = _prompt("Template (iclr2026|plain)", "iclr2026") |
|
|
| cfg = RunConfig( |
| topic=topic, |
| out=Path(out), |
| base_url=resolve_base_url(base_url or None), |
| api_key=resolve_api_key(), |
| model=resolve_model(model or None), |
| pdf_compiler=pdf_compiler, |
| template=template, |
| max_sources=max(max_sources, 1), |
| module_sources=max(module_sources, 1), |
| use_mock=False, |
| verbose=True, |
| progress=True, |
| llm_timeout_s=llm_timeout, |
| snapshot_timeout_s=snapshot_timeout, |
| max_total_runtime_s=max_runtime, |
| request_budget_s=request_budget, |
| keep_stage=False, |
| ) |
| build_resources_pack(cfg) |
| print(out) |
| return 0 |
|
|
| print(f"Unknown command: {args.cmd}", file=sys.stderr) |
| return 2 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|