Compare commits
No commits in common. "bca7294ec8e26e9781f813224866fe8aa1a25b32" and "b2b7026eb21cd3d6a3dd7f7d0e1574160cd770fe" have entirely different histories.
bca7294ec8
...
b2b7026eb2
2 changed files with 1 additions and 137 deletions
80
cli/main.py
80
cli/main.py
|
|
@ -6,9 +6,7 @@ ResearchResult contracts to the terminal.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
@ -22,9 +20,6 @@ from rich.text import Text
|
||||||
from researchers.web.models import ResearchResult
|
from researchers.web.models import ResearchResult
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TRACE_DIR = "~/.marchwarden/traces"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# MCP client
|
# MCP client
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -202,80 +197,5 @@ def ask(
|
||||||
render_result(result, console)
|
render_result(result, console)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_trace_path(trace_id: str, trace_dir: Optional[str]) -> Path:
|
|
||||||
"""Resolve the JSONL path for a trace_id."""
|
|
||||||
base = Path(os.path.expanduser(trace_dir or DEFAULT_TRACE_DIR))
|
|
||||||
return base / f"{trace_id}.jsonl"
|
|
||||||
|
|
||||||
|
|
||||||
def render_trace(entries: list[dict], trace_id: str, console: Console) -> None:
|
|
||||||
"""Pretty-print a list of trace entries."""
|
|
||||||
console.print(
|
|
||||||
Panel(
|
|
||||||
f"[bold]trace_id:[/bold] {trace_id}\n[bold]steps:[/bold] {len(entries)}",
|
|
||||||
title="[cyan]Replay[/cyan]",
|
|
||||||
border_style="cyan",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not entries:
|
|
||||||
console.print("[dim]Trace file is empty.[/dim]")
|
|
||||||
return
|
|
||||||
|
|
||||||
table = Table(show_lines=True, expand=True)
|
|
||||||
table.add_column("#", style="dim", width=4)
|
|
||||||
table.add_column("Action", style="magenta")
|
|
||||||
table.add_column("Decision", overflow="fold")
|
|
||||||
table.add_column("Details", overflow="fold")
|
|
||||||
table.add_column("Hash", style="dim", overflow="fold")
|
|
||||||
|
|
||||||
reserved = {"step", "action", "decision", "timestamp", "content_hash"}
|
|
||||||
for e in entries:
|
|
||||||
step = str(e.get("step", "?"))
|
|
||||||
action = str(e.get("action", ""))
|
|
||||||
decision = str(e.get("decision", ""))
|
|
||||||
content_hash = str(e.get("content_hash", "") or "")
|
|
||||||
extras = {k: v for k, v in e.items() if k not in reserved}
|
|
||||||
details = "\n".join(f"{k}: {v}" for k, v in extras.items())
|
|
||||||
table.add_row(step, action, decision, details, content_hash)
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("trace_id")
|
|
||||||
@click.option(
|
|
||||||
"--trace-dir",
|
|
||||||
default=None,
|
|
||||||
help=f"Trace directory (default: {DEFAULT_TRACE_DIR}).",
|
|
||||||
)
|
|
||||||
def replay(trace_id: str, trace_dir: Optional[str]) -> None:
|
|
||||||
"""Replay a prior research run by TRACE_ID."""
|
|
||||||
console = Console()
|
|
||||||
path = _resolve_trace_path(trace_id, trace_dir)
|
|
||||||
if not path.exists():
|
|
||||||
console.print(
|
|
||||||
f"[bold red]Error:[/bold red] no trace file found for "
|
|
||||||
f"trace_id [bold]{trace_id}[/bold] at {path}"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
entries: list[dict] = []
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
for lineno, line in enumerate(f, 1):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
entries.append(json.loads(line))
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
console.print(
|
|
||||||
f"[bold red]Error:[/bold red] invalid JSON on line {lineno}: {e}"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
render_trace(entries, trace_id, console)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from unittest.mock import patch
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from cli.main import cli, render_result, render_trace
|
from cli.main import cli, render_result
|
||||||
from researchers.web.models import (
|
from researchers.web.models import (
|
||||||
Citation,
|
Citation,
|
||||||
ConfidenceFactors,
|
ConfidenceFactors,
|
||||||
|
|
@ -130,59 +130,3 @@ class TestAskCommand:
|
||||||
|
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
assert "mcp went sideways" in result.output
|
assert "mcp went sideways" in result.output
|
||||||
|
|
||||||
|
|
||||||
class TestReplayCommand:
|
|
||||||
def _write_trace(self, tmp_path, trace_id="trace-xyz"):
|
|
||||||
path = tmp_path / f"{trace_id}.jsonl"
|
|
||||||
path.write_text(
|
|
||||||
'{"step": 1, "action": "search", "decision": "initial query", '
|
|
||||||
'"timestamp": "2026-04-08T00:00:00Z", "query": "utah crops"}\n'
|
|
||||||
'{"step": 2, "action": "fetch_url", "decision": "promising source", '
|
|
||||||
'"timestamp": "2026-04-08T00:00:01Z", "url": "https://example.com", '
|
|
||||||
'"content_hash": "sha256:deadbeef"}\n'
|
|
||||||
'{"step": 3, "action": "synthesize", "decision": "have enough", '
|
|
||||||
'"timestamp": "2026-04-08T00:00:02Z"}\n'
|
|
||||||
)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def test_replay_renders_trace(self, tmp_path):
|
|
||||||
runner = CliRunner()
|
|
||||||
self._write_trace(tmp_path)
|
|
||||||
result = runner.invoke(
|
|
||||||
cli,
|
|
||||||
["replay", "trace-xyz", "--trace-dir", str(tmp_path)],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
assert "trace-xyz" in result.output
|
|
||||||
assert "search" in result.output
|
|
||||||
assert "fetch_url" in result.output
|
|
||||||
assert "synthesize" in result.output
|
|
||||||
assert "sha256:deadbeef" in result.output
|
|
||||||
assert "utah crops" in result.output
|
|
||||||
|
|
||||||
def test_replay_unknown_trace_id(self, tmp_path):
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(
|
|
||||||
cli,
|
|
||||||
["replay", "missing-id", "--trace-dir", str(tmp_path)],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 1
|
|
||||||
assert "no trace file found" in result.output
|
|
||||||
|
|
||||||
def test_replay_invalid_json(self, tmp_path):
|
|
||||||
runner = CliRunner()
|
|
||||||
(tmp_path / "broken.jsonl").write_text("{not json}\n")
|
|
||||||
result = runner.invoke(
|
|
||||||
cli,
|
|
||||||
["replay", "broken", "--trace-dir", str(tmp_path)],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 1
|
|
||||||
assert "invalid JSON" in result.output
|
|
||||||
|
|
||||||
def test_render_trace_empty(self):
|
|
||||||
console = Console(record=True, width=120)
|
|
||||||
render_trace([], "empty-trace", console)
|
|
||||||
out = console.export_text()
|
|
||||||
assert "empty-trace" in out
|
|
||||||
assert "empty" in out.lower()
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue