pocket-tutor / scripts /smoke_space.py
Codex
Enhance CARD.md with detailed training data and validation section; update smoke_space.py with default problem prompts and validation checks
e396fbd
from __future__ import annotations
# ruff: noqa: E402
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
ROOT_DIR = Path(__file__).resolve().parents[1]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from core.analyzer import build_tutor_prompt
from env.config import QUESTION_LIMIT, SPACE_URL
DEFAULT_QUESTION = "Can you show me how to solve 3(2x - 5) = 21 without jumping straight to the answer?"
DEFAULT_GRADE = "High school"
DEFAULT_MODE = "Step-by-step"
DEFAULT_PROBLEM = "Upload a homework image or type the question to begin."
DEFAULT_KNOWNS = "- No givens identified yet."
DEFAULT_STRATEGY = "No strategy generated yet."
DEFAULT_STEPS = "No worked steps generated yet."
DEFAULT_CHECK = "No answer check generated yet."
DEFAULT_HINT = "Ask for a hint after the first explanation."
DEFAULT_PARENT = "Parent support note will appear here."
def _derive_space_base_url(space_url: str) -> str:
"""Converts the human Space URL into the hf.space API host."""
parts = [part for part in space_url.rstrip("/").split("/") if part]
if len(parts) < 2:
raise ValueError(f"Could not derive a Space host from {space_url!r}.")
namespace, repo_name = parts[-2], parts[-1]
return f"https://{namespace}-{repo_name}.hf.space"
def _request_json(
url: str,
payload: dict[str, Any] | None = None,
token: str | None = None,
) -> Any:
"""Sends a JSON request and returns the decoded payload."""
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(url, data=data, headers=headers, method="POST")
with urllib.request.urlopen(request, timeout=600) as response:
return json.loads(response.read().decode("utf-8"))
def _stream_completion(url: str, token: str | None = None) -> list[Any]:
"""Reads the final SSE completion event from a Gradio queue stream."""
headers = {"Accept": "text/event-stream"}
if token:
headers["Authorization"] = f"Bearer {token}"
request = urllib.request.Request(url, headers=headers, method="GET")
event_name = ""
pending_data: list[str] = []
with urllib.request.urlopen(request, timeout=600) as response:
for raw_line in response:
line = raw_line.decode("utf-8", errors="replace").strip()
if not line:
continue
if line.startswith("event:"):
event_name = line.split(":", 1)[1].strip()
pending_data = []
continue
if line.startswith("data:"):
pending_data.append(line.split(":", 1)[1].lstrip())
if event_name == "complete":
return json.loads("\n".join(pending_data))
raise RuntimeError("Space stream ended before a completion event was received.")
def _run_space_smoke(
space_base_url: str,
question: str,
grade_band: str,
help_mode: str,
token: str | None,
) -> list[Any]:
"""Submits a text-only tutoring request to the live Space."""
submit_url = f"{space_base_url}/gradio_api/call/v2/analyze_homework_ui"
result_url = f"{space_base_url}/gradio_api/call/analyze_homework_ui"
payload = {
"image_file": None,
"question": question,
"audio_path": None,
"grade_band": grade_band,
"help_mode": help_mode,
}
response = _request_json(submit_url, payload=payload, token=token)
event_id = response.get("event_id")
if not event_id:
raise RuntimeError(f"Space response did not include an event_id: {response!r}")
return _stream_completion(f"{result_url}/{event_id}", token=token)
def main() -> int:
"""Validates the deployed Space response contract."""
parser = argparse.ArgumentParser(
description="Smoke test the Pocket Tutor Space API."
)
parser.add_argument(
"--space-url",
default=_derive_space_base_url(SPACE_URL),
help="Base hf.space URL for the deployed Space.",
)
parser.add_argument(
"--token",
default=os.environ.get("HF_TOKEN"),
help="Optional Hugging Face token for authenticated requests.",
)
parser.add_argument(
"--question",
default=DEFAULT_QUESTION,
help="Text-only homework prompt used for the smoke test.",
)
parser.add_argument(
"--grade",
default=DEFAULT_GRADE,
help="Learner level used for the smoke test.",
)
parser.add_argument(
"--mode",
default=DEFAULT_MODE,
help="Help mode used for the smoke test.",
)
args = parser.parse_args()
prompt = build_tutor_prompt(
question=args.question[:QUESTION_LIMIT],
transcript="",
grade_band=args.grade,
help_mode=args.mode,
image_status="No image uploaded.",
)
print(f"[smoke-space] Calling {args.space_url}")
print(f"[smoke-space] Question: {args.question}")
print(f"[smoke-space] Grade: {args.grade} | Mode: {args.mode}")
print(f"[smoke-space] Prompt chars: {len(prompt)}")
try:
outputs = _run_space_smoke(
args.space_url,
question=args.question[:QUESTION_LIMIT],
grade_band=args.grade,
help_mode=args.mode,
token=args.token,
)
except (
urllib.error.URLError,
urllib.error.HTTPError,
RuntimeError,
json.JSONDecodeError,
) as exc:
print(f"[smoke-space] Smoke test failed: {exc}", file=sys.stderr)
return 1
if len(outputs) != 9:
print(f"[smoke-space] Unexpected output count: {len(outputs)}", file=sys.stderr)
print(outputs, file=sys.stderr)
return 1
labels = [
"student_context",
"model_output",
"problem",
"knowns",
"strategy",
"steps",
"check",
"hint",
"parent",
]
named_outputs = dict(zip(labels, outputs, strict=True))
for label, value in named_outputs.items():
print(f"[{label}]")
print(value)
print()
failures = [
label
for label, default in (
("problem", DEFAULT_PROBLEM),
("knowns", DEFAULT_KNOWNS),
("strategy", DEFAULT_STRATEGY),
("steps", DEFAULT_STEPS),
("check", DEFAULT_CHECK),
("hint", DEFAULT_HINT),
("parent", DEFAULT_PARENT),
)
if str(named_outputs[label]).strip() in {"", default}
]
if failures:
print(
"[smoke-space] Missing expected model sections: " + ", ".join(failures),
file=sys.stderr,
)
return 1
print("[smoke-space] Space smoke test passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())