HydraDeck / hydradeck /cli.py
OpeneR
HydraDeck open-source clean snapshot
778278c
Raw
History Blame Contribute Delete
17.7 kB
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())