Spaces:
Paused
Paused
| """Auralynq command-line interface. | |
| Commands lazily import their subsystems so ``import auralynq.cli`` stays cheap and | |
| the CLI works even when optional extras are absent (they degrade to fallbacks). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import typer | |
| from rich.console import Console | |
| from rich.panel import Panel | |
| from rich.table import Table | |
| from auralynq import __version__ | |
| from auralynq.config import get_settings | |
| app = typer.Typer( | |
| name="auralynq", | |
| help="Auralynq β Talk to Your Data. Local-first agentic voice RAG with PathRAG.", | |
| no_args_is_help=True, | |
| add_completion=False, | |
| ) | |
| config_app = typer.Typer(name="config", help="Manage configuration.", no_args_is_help=True) | |
| app.add_typer(config_app) | |
| console = Console() | |
| def _bootstrap(): | |
| from auralynq.telemetry import configure_logging | |
| s = get_settings() | |
| configure_logging(level=s.log_level, json=s.log_json) | |
| s.ensure_dirs() | |
| return s | |
| def _main( | |
| ctx: typer.Context, | |
| version: bool = typer.Option(False, "--version", help="Show version and exit."), | |
| ): | |
| if version: | |
| console.print(f"auralynq {__version__}") | |
| raise typer.Exit() | |
| if ctx.invoked_subcommand is None: | |
| console.print(ctx.get_help()) | |
| raise typer.Exit() | |
| def ingest( | |
| input: Path = typer.Argument(..., help="File or directory to ingest."), | |
| recursive: bool = typer.Option(True, help="Recurse into directories."), | |
| ) -> None: | |
| """Ingest documents/audio into chunks with spans and timestamps. | |
| Example: auralynq ingest data/corpus --recursive | |
| """ | |
| _bootstrap() | |
| from auralynq.ingest.pipeline import ingest_path | |
| result = ingest_path(input, recursive=recursive) | |
| console.print( | |
| f"[green]β[/] ingested {result.n_documents} documents β " | |
| f"{result.n_chunks} chunks ({result.n_skipped} unchanged/skipped)" | |
| ) | |
| def index( | |
| input: Path = typer.Option(Path("data/corpus"), help="Corpus directory."), | |
| rebuild: bool = typer.Option(False, help="Drop and rebuild the index."), | |
| ) -> None: | |
| """Build the vector index and knowledge graph. | |
| Example: auralynq index --input data/corpus | |
| """ | |
| _bootstrap() | |
| from auralynq.pipeline import build_index | |
| stats = build_index(input, rebuild=rebuild) | |
| table = Table(title="Index built") | |
| table.add_column("metric") | |
| table.add_column("value", justify="right") | |
| for k, v in stats.items(): | |
| table.add_row(k, str(v)) | |
| console.print(table) | |
| def ask( | |
| question: str = typer.Argument(..., help="Your question."), | |
| final_k: int | None = typer.Option(None, help="Number of context chunks."), | |
| show_trace: bool = typer.Option(False, "--trace", help="Print the agent trajectory."), | |
| json_out: bool = typer.Option(False, "--json", help="Emit JSON."), | |
| ) -> None: | |
| """Ask a question and get a grounded, cited answer. | |
| Example: auralynq ask "How does PathRAG prune paths?" --trace | |
| """ | |
| _bootstrap() | |
| from auralynq.agent.runner import answer_question | |
| result = answer_question(question, final_k=final_k) | |
| if json_out: | |
| console.print_json(json.dumps(result.to_dict())) | |
| return | |
| console.print(Panel(result.answer, title="Answer", border_style="cyan")) | |
| if result.citations: | |
| table = Table(title="Citations") | |
| table.add_column("#") | |
| table.add_column("source") | |
| table.add_column("span/timestamp") | |
| for i, c in enumerate(result.citations, 1): | |
| table.add_row(str(i), c.get("source", "?"), c.get("locator", "")) | |
| console.print(table) | |
| if show_trace: | |
| for span in result.trace: | |
| console.print(f" [dim]{span['name']}[/] {span['duration_ms']}ms") | |
| def talk( | |
| audio: Path | None = typer.Option(None, help="Audio file (skip live mic)."), | |
| speak: bool = typer.Option(True, help="Speak the answer via TTS."), | |
| ) -> None: | |
| """Voice query: ASR β agent β grounded answer β optional TTS. | |
| Example: auralynq talk --audio question.wav | |
| """ | |
| _bootstrap() | |
| from auralynq.voice.loop import run_voice_turn | |
| result = run_voice_turn(audio_path=audio, speak=speak) | |
| console.print(Panel(result.transcript, title="Heard", border_style="magenta")) | |
| console.print(Panel(result.answer, title="Answer", border_style="cyan")) | |
| if result.audio_out_path: | |
| console.print(f"[green]β[/] spoken answer β {result.audio_out_path}") | |
| def eval( | |
| report: bool = typer.Option(False, "--report", help="Write a full report to reports/."), | |
| smoke: bool = typer.Option(False, "--smoke", help="Tiny smoke eval for CI."), | |
| gate: bool = typer.Option(False, "--gate", help="Exit non-zero if the trust gate fails (CI)."), | |
| judge: bool = typer.Option( | |
| False, | |
| "--judge", | |
| help="Use the configured LLM as an NLI judge for citation/faithfulness/correctness.", | |
| ), | |
| ) -> None: | |
| """Run the evaluation harness β retrieval metrics, faithfulness, citation | |
| attribution, confidence calibration (ECE) + a pass/fail regression gate. | |
| Example: auralynq eval --report --gate --judge | |
| """ | |
| _bootstrap() | |
| from auralynq.eval.report import run_eval | |
| out = run_eval(smoke=smoke, write_report=report, judge=judge) | |
| console.print_json(json.dumps(out)) | |
| g = out.get("gate", {}) | |
| if gate: | |
| if g.get("passed"): | |
| console.print("[green]β eval gate PASSED[/]") | |
| else: | |
| fails = ", ".join( | |
| f"{f['metric']}={f['value']} (need {f['op']} {f['threshold']})" | |
| for f in g.get("failures", []) | |
| ) | |
| console.print(f"[red]β eval gate FAILED:[/] {fails}") | |
| raise typer.Exit(code=1) | |
| def bench( | |
| report: bool = typer.Option(False, "--report", help="Write benchmark report to reports/."), | |
| ) -> None: | |
| """Benchmark Qdrant recall/latency/memory across quantization modes. | |
| Example: auralynq bench --report | |
| """ | |
| _bootstrap() | |
| from auralynq.eval.bench import run_bench | |
| out = run_bench(write_report=report) | |
| console.print_json(json.dumps(out)) | |
| def data( | |
| sample: bool = typer.Option(True, help="Download small sample subsets."), | |
| full: bool = typer.Option(False, help="Download full datasets."), | |
| ) -> None: | |
| """Download evaluation datasets (text + voice). Never requires paid keys. | |
| Example: auralynq data --sample | |
| """ | |
| _bootstrap() | |
| from scripts.download_data import download | |
| download(sample=sample and not full, full=full) | |
| def serve( | |
| host: str | None = typer.Option(None), | |
| port: int | None = typer.Option(None), | |
| reload: bool = typer.Option(False), | |
| ) -> None: | |
| """Start the FastAPI backend (SSE chat + WebSocket voice). | |
| Example: auralynq serve --port 8000 | |
| """ | |
| s = _bootstrap() | |
| import uvicorn | |
| uvicorn.run( | |
| "auralynq.serving.app:app", | |
| host=host or s.serve.host, | |
| port=port or s.serve.port, | |
| reload=reload, | |
| ) | |
| def mcp() -> None: | |
| """Start the auralynq-mcp server over stdio.""" | |
| _bootstrap() | |
| from auralynq.mcp_server.server import main as mcp_main | |
| mcp_main() | |
| def info() -> None: | |
| """Show resolved providers and configuration (what's actually running).""" | |
| _bootstrap() | |
| from auralynq.providers import describe_providers | |
| table = Table(title="Auralynq β resolved providers") | |
| table.add_column("subsystem") | |
| table.add_column("provider") | |
| table.add_column("status") | |
| for row in describe_providers(): | |
| table.add_row(row["subsystem"], row["provider"], row["status"]) | |
| console.print(table) | |
| # ββ config subcommands ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def config_show() -> None: | |
| """Show active configuration (resolved values, config file path, provider chain).""" | |
| from auralynq.config import find_config_file | |
| s = get_settings() | |
| cfg_path = find_config_file() | |
| table = Table(title="Auralynq β active configuration") | |
| table.add_column("setting") | |
| table.add_column("value") | |
| table.add_row("config file", str(cfg_path) if cfg_path else "[dim]none (using defaults)[/]") | |
| table.add_row("llm.provider", s.llm.provider) | |
| table.add_row("llm.model", s.llm.model) | |
| table.add_row("llm.base_url", s.llm.base_url) | |
| table.add_row("embedding.provider", s.embedding.provider) | |
| table.add_row("embedding.ollama_model", s.embedding.ollama_model) | |
| table.add_row("vector.backend", s.vector.backend) | |
| table.add_row("vector.chroma_persist_dir", str(s.vector.chroma_persist_dir)) | |
| table.add_row("pdf_source_dir", str(s.pdf_source_dir)) | |
| table.add_row("air_gapped", str(s.air_gapped)) | |
| table.add_row("openai_api_key", "[green]set[/]" if s.openai_api_key else "[dim]not set[/]") | |
| table.add_row( | |
| "anthropic_api_key", "[green]set[/]" if s.anthropic_api_key else "[dim]not set[/]" | |
| ) | |
| console.print(table) | |
| def config_init( | |
| output: Path = typer.Option(Path("config.yaml"), help="Where to write the config file."), | |
| force: bool = typer.Option(False, "--force", help="Overwrite if file already exists."), | |
| ) -> None: | |
| """Write a config.yaml template to the current directory.""" | |
| import shutil | |
| from pathlib import Path as P | |
| template = P(__file__).parent.parent / "config.yaml.example" | |
| if not template.exists(): | |
| console.print("[red]config.yaml.example not found β reinstall the package.[/]") | |
| raise typer.Exit(1) | |
| if output.exists() and not force: | |
| console.print(f"[yellow]{output} already exists.[/] Use --force to overwrite.") | |
| raise typer.Exit(1) | |
| shutil.copy(template, output) | |
| console.print(f"[green]β[/] config template written to [bold]{output}[/]") | |
| console.print("Edit it and set [bold]AURALYNQ_CONFIG=config.yaml[/] (or place it in CWD).") | |
| if __name__ == "__main__": | |
| app() | |