mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-13 04:04:09 +00:00
## Summary
Overhaul the CLI and all library modules to produce polished, modern
terminal output comparable to tools like uv, gh, and cargo. Rich was
already a declared dependency but barely used — this commit wires it
throughout every layer.
## Changes by layer
### semantica/cli.py — visual overhaul
- Add imports: `box`, `Panel`, `Rule`, `Syntax`, `Text` from Rich
- Add 7 style constants (`_BRAND`, `_KEY`, `_VAL`, `_DIM`, `_SUCCESS`,
`_WARN_STY`, `_TABLE_BOX`) for a consistent colour palette
- `_ok()` now prefixes output with a green ✓ checkmark
- New `_info()` helper (neutral · bullet, respects --quiet)
- New `_warn()` helper (yellow ⚠ prefix, never suppressed)
- New `_pprint()` helper: renders dicts/lists as syntax-highlighted JSON
(Rich Syntax, monokai theme) instead of raw Python repr; strings
pass through unchanged; respects --quiet
- `info` command: banner replaced with a rounded Rich Panel showing
version + tagline; component table uses SIMPLE_HEAD box
- All 7 table sites updated: `box=SIMPLE_HEAD`, `show_edge=False`,
consistent `_KEY`/`_VAL` column styles (KG Stats, Reasoning Engines,
Recent Decisions, Configured Backends, Backup Info, MCP Tools)
- `_run_build()`: `console.status(spinner="dots")` wraps the blocking
build call; skipped under --quiet / --json
- `parse`, `extract`, `embed generate`, `reason run`, `reason explain`,
`deduplicate`: each wraps its long-running operation in a status
spinner, guarded by --quiet / --json
- All 30+ `console.print(result)` calls replaced with `_pprint()`
- All raw `[yellow]Warning:[/yellow]` and "not running" patterns
replaced with the new `_warn()` / `_WARN_STY` style
### semantica/explorer/__init__.py
- Error messages use `Console(stderr=True)` with `[bold red]Error:[/bold red]`
- Graph loading wrapped in `console.status()` spinner
- Startup info replaced with a cyan-bordered Rich Panel showing URL,
API docs, and health endpoint
### Library internals — replace print() with structured logger calls
All modules below had active `print()` calls that bypassed the logging
framework, corrupted spinners, and polluted stdout in piped/programmatic
use. All replaced with appropriate `self.logger.*` calls:
- `semantica/kg/graph_builder.py` — 23 calls: entity resolution
progress, graph structure steps, GraphStore persistence timing, and
the two `='*60` completion banners → `self.logger.info/debug()`
- `semantica/semantic_extract/methods.py` — 4 verbose-mode debug
prints → `logger.debug()`
- `semantica/semantic_extract/relation_extractor.py` — progress +
error prints → `self.logger.debug/warning()` with `exc_info`
- `semantica/semantic_extract/triplet_extractor.py` — same pattern
- `semantica/semantic_extract/semantic_network_extractor.py` — batch
error prints → `self.logger.warning/error()`
- `semantica/semantic_extract/coreference_resolver.py` — error print
→ `self.logger.error()`
- `semantica/semantic_extract/providers.py` — debug print →
`self.logger.debug()`
### Tooling
- `benchmarks/benchmarks_runner.py`: Rule banner, ✓/✗/⚠ status lines,
Rule separators around regression alert
- `benchmarks/infrastructure/compare.py`: removed manual ANSI escape
codes; comparison output is now a Rich Table with SIMPLE_HEAD;
summary uses coloured Rule + styled SUCCESS/FAILURE messages
- `cookbook/advanced/snowflake_ingestion_examples.py`: `_section()`
helper using Rule; tabular data rendered as Rich Table; result lines
use ✓/✗/⚠ prefixes; logger.error already present, retained
- `docs_check.py`: `pass`/`FAIL` lines use `[bold green]` /
`[bold red]`; summary uses styled output
## Tests
- `tests/test_cli_commands.py`: fix 3 pre-existing mock mismatches
- `test_kg_stats_json_with_mock`: mock now uses `compute_metrics()`
(the method the code actually calls) instead of `get_statistics()`
- `test_dry_run_not_needed_extract_is_read_only` and
`test_stdin_input`: mock now provides `NERExtractor`,
`RelationExtractor`, `TripletExtractor`, `EventDetector`
(the classes the code imports) instead of `SemanticAnalyzer`
Result: 230/230 tests pass (was 227/230)
- `tests/verify_rich_cli.py`: new verification script; exercises all
14 command groups (92 --help checks, table rendering, dry-run
formatting, --json mode, _pprint helper); 111 pass, 0 fail
112 lines
3.3 KiB
Python
112 lines
3.3 KiB
Python
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
from rich import box
|
|
from rich.console import Console
|
|
from rich.rule import Rule
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
|
|
|
|
def load_results(filepath: str) -> Dict[str, Any]:
|
|
with open(filepath, "r") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def calc_z_score(current_mean, base_mean, base_stddev):
|
|
"""
|
|
Z-Score indicates how many standard deviations
|
|
away current run is from baseline
|
|
"""
|
|
if base_stddev == 0:
|
|
return 0 if current_mean == base_mean else 100.0
|
|
return (current_mean - base_mean) / base_stddev
|
|
|
|
|
|
def compare_benchmarks(
|
|
baseline: Dict[str, Any], current: Dict[str, Any], threshold_pct: float = 10.0
|
|
) -> bool:
|
|
"""
|
|
Uses Mean for % change and Z-score for noise detection.
|
|
Returns True if regressions were detected.
|
|
"""
|
|
baseline_map = {b["name"]: b for b in baseline["benchmarks"]}
|
|
current_map = {b["name"]: b for b in current["benchmarks"]}
|
|
|
|
table = Table(
|
|
title="[bold]Benchmark Comparison[/bold]",
|
|
box=box.SIMPLE_HEAD,
|
|
show_edge=False,
|
|
padding=(0, 1),
|
|
)
|
|
table.add_column("Benchmark", style="cyan", no_wrap=False, max_width=60)
|
|
table.add_column("Change %", justify="right")
|
|
table.add_column("Sigma (Z)", justify="right")
|
|
table.add_column("Status")
|
|
|
|
regressions: List[str] = []
|
|
|
|
for name, curr in current_map.items():
|
|
base = baseline_map.get(name)
|
|
if not base:
|
|
table.add_row(name, "—", "—", "[dim]NEW[/dim]")
|
|
continue
|
|
|
|
m1 = base["stats"]["mean"]
|
|
s1 = base["stats"]["stddev"]
|
|
m2 = curr["stats"]["mean"]
|
|
|
|
delta_pct = 0.0 if m1 == 0 else ((m2 - m1) / m1) * 100
|
|
z_score = calc_z_score(m2, m1, s1)
|
|
|
|
if delta_pct > threshold_pct and abs(z_score) > 2.0:
|
|
status = "[bold red]REGRESSION[/bold red]"
|
|
regressions.append(name)
|
|
elif delta_pct > threshold_pct:
|
|
status = "[yellow]NOISE[/yellow]"
|
|
elif delta_pct < -threshold_pct and abs(z_score) > 2.0:
|
|
status = "[bold green]IMPROVED[/bold green]"
|
|
else:
|
|
status = "[green]OK[/green]"
|
|
|
|
change_str = f"{delta_pct:+.2f}%"
|
|
z_str = f"{z_score:.2f}"
|
|
table.add_row(name, change_str, z_str, status)
|
|
|
|
console.print(table)
|
|
|
|
if regressions:
|
|
console.print(Rule(style="red"))
|
|
console.print(
|
|
f"[bold red]FAILURE:[/bold red] Performance regression detected "
|
|
f"in [cyan]{len(regressions)}[/cyan] test(s)."
|
|
)
|
|
return True
|
|
|
|
console.print(Rule(style="green"))
|
|
console.print("[bold green]SUCCESS:[/bold green] No significant regressions.")
|
|
return False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("baseline", help="Gold standard JSON")
|
|
parser.add_argument("current", help="New run JSON")
|
|
parser.add_argument(
|
|
"--threshold", type=float, default=10.0, help="FAIL if slower by %%"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
failed = compare_benchmarks(
|
|
load_results(args.baseline), load_results(args.current), args.threshold
|
|
)
|
|
sys.exit(1 if failed else 0)
|
|
except FileNotFoundError as e:
|
|
console.print(f"[bold red]Error:[/bold red] loading files: {e}")
|
|
sys.exit(0)
|