Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working. Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
4.8 KiB
Python
135 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
"""P0-5 smoke: deepseek-flash defaults + simple_project offline checks.
|
||
|
||
Usage:
|
||
python3 scripts/smoke_simple_project.py # offline (no API)
|
||
python3 scripts/smoke_simple_project.py --live # needs DEEPSEEK_API_KEY
|
||
|
||
Offline asserts: model defaults, vision gate, graph+eval golden, shortest_path.
|
||
Live (optional): PDF ingest attaches page images under vision.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
SIMPLE = ROOT / "simple_project"
|
||
BASELINE = SIMPLE / "smoke_baseline.json"
|
||
|
||
|
||
def _check_model_defaults() -> list[str]:
|
||
from backend.config import settings
|
||
|
||
errs: list[str] = []
|
||
if settings.deepseek_model != "deepseek-flash":
|
||
errs.append(f"deepseek_model={settings.deepseek_model!r}, want deepseek-flash")
|
||
# Mirror deepseek_provider._is_vision_model without importing openai.
|
||
name = settings.deepseek_model.strip().lower()
|
||
vision_ok = (
|
||
name in {"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"}
|
||
or "vision" in name
|
||
or name.startswith("deepseek-flash")
|
||
)
|
||
if not vision_ok:
|
||
errs.append("deepseek-flash not treated as vision model")
|
||
effort = (settings.deepseek_reasoning_effort or "").lower()
|
||
if effort not in {"low", "high", "max"}:
|
||
errs.append(f"deepseek_reasoning_effort={effort!r}, want low|high|max")
|
||
return errs
|
||
|
||
|
||
def _check_simple_project_offline() -> list[str]:
|
||
from backend.periscopex.models import DesignGraph
|
||
from backend.periscopex.validation_tools import shortest_path
|
||
|
||
errs: list[str] = []
|
||
graph_path = SIMPLE / "design_graph.json"
|
||
if not graph_path.is_file():
|
||
return ["simple_project/design_graph.json missing"]
|
||
g = DesignGraph.model_validate_json(graph_path.read_text(encoding="utf-8"))
|
||
|
||
golden_path = SIMPLE / "eval_golden.json"
|
||
if not golden_path.is_file():
|
||
return ["simple_project/eval_golden.json missing"]
|
||
golden_doc = json.loads(golden_path.read_text(encoding="utf-8"))
|
||
for ref in golden_doc.get("required_refs") or []:
|
||
if ref not in g.components:
|
||
errs.append(f"missing ref {ref}")
|
||
min_c = golden_doc.get("min_components")
|
||
if min_c and len(g.components) < int(min_c):
|
||
errs.append(f"components {len(g.components)} < {min_c}")
|
||
min_n = golden_doc.get("min_nets")
|
||
if min_n and len(g.nets) < int(min_n):
|
||
errs.append(f"nets {len(g.nets)} < {min_n}")
|
||
|
||
if "U3" in g.components and "X1" in g.components:
|
||
pin = next(iter(g.components["U3"].pins), None)
|
||
xpin = next(iter(g.components["X1"].pins), None)
|
||
if pin and xpin:
|
||
msg = shortest_path(g, {}, "U3", pin, "X1", xpin)
|
||
print(f"note: shortest_path U3–X1 → {msg}")
|
||
|
||
baseline = {
|
||
"component_count": len(g.components),
|
||
"net_count": len(g.nets),
|
||
"graph_ok": not errs,
|
||
}
|
||
if BASELINE.is_file():
|
||
prev = json.loads(BASELINE.read_text(encoding="utf-8"))
|
||
for k in ("component_count", "net_count"):
|
||
if prev.get(k) != baseline.get(k):
|
||
errs.append(f"{k} {baseline.get(k)} != baseline {prev.get(k)}")
|
||
else:
|
||
BASELINE.write_text(json.dumps(baseline, indent=2) + "\n", encoding="utf-8")
|
||
print(f"Wrote baseline {BASELINE}")
|
||
|
||
print(json.dumps({"offline": baseline, "errors": errs}, indent=2))
|
||
return errs
|
||
|
||
|
||
def _check_live() -> list[str]:
|
||
from backend.config import settings
|
||
from backend.services.llm.deepseek_provider import _is_vision_model
|
||
from backend.services.llm.pdf_ingest import pdf_to_openai_content
|
||
|
||
if not settings.deepseek_api_key:
|
||
return ["DEEPSEEK_API_KEY not set"]
|
||
pdfs = list((ROOT / "library" / "datasheets").rglob("*.pdf"))[:1]
|
||
if not pdfs:
|
||
pdfs = list(SIMPLE.rglob("*.pdf"))
|
||
if not pdfs:
|
||
return ["no PDF found for live vision check"]
|
||
pdf = pdfs[0]
|
||
parts = pdf_to_openai_content(
|
||
pdf, vision=_is_vision_model(settings.deepseek_model), max_images=4,
|
||
)
|
||
n_img = sum(1 for p in parts if p.get("type") == "image_url")
|
||
print(json.dumps({"live_pdf": str(pdf), "content_parts": len(parts), "images": n_img}))
|
||
if n_img == 0 and _is_vision_model(settings.deepseek_model):
|
||
return ["vision model produced 0 page images"]
|
||
return []
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser(description=__doc__)
|
||
ap.add_argument("--live", action="store_true", help="Also exercise PDF page images")
|
||
args = ap.parse_args()
|
||
errs = _check_model_defaults() + _check_simple_project_offline()
|
||
if args.live:
|
||
errs += _check_live()
|
||
if errs:
|
||
print("SMOKE FAIL:", *errs, sep="\n - ")
|
||
return 1
|
||
print("SMOKE OK")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|