Spaces:
Sleeping
Sleeping
Download solver.py from VeryOnly123/cc-solver: direct link, hf CLI and curl.
- Browser
- Download file 20.6 kB
-
https://huggingface.co/spaces/VeryOnly123/cc-solver/resolve/main/solver.py
- Command line
-
hf download hf://spaces/VeryOnly123/cc-solver/solver.py
-
curl -L -o solver.py https://huggingface.co/spaces/VeryOnly123/cc-solver/resolve/main/solver.py
20.6 kB
| #!/usr/bin/env python3 | |
| """Background task manager for the physics solver.""" | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import shutil | |
| import subprocess | |
| import threading | |
| import urllib.request | |
| from datetime import datetime, timezone | |
| # Solver source location | |
| SOLVER_SRC = "/app/solver" | |
| WORKSPACE_BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspaces") | |
| # Files that come from the repo — excluded from downloads | |
| SOLVER_SOURCE_FILES = {"run.py", "spawn.py", "outline.md"} | |
| DEFAULT_SOLVER_URL = os.environ.get( | |
| "SOLVER_REPO_URL", | |
| "https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/", | |
| ) | |
| tasks = {} | |
| tasks_lock = threading.Lock() | |
| def _log(task_id, msg): | |
| """Append a line to the task's log file AND print to stdout.""" | |
| task = tasks.get(task_id) | |
| log_path = None | |
| if task: | |
| log_path = os.path.join(task["workspace"], "solver.log") | |
| if log_path: | |
| try: | |
| with open(log_path, "a", encoding="utf-8") as f: | |
| f.write(f"[{_now()}] {msg}\n") | |
| except Exception: | |
| pass | |
| print(f"[{task_id}] {msg}", flush=True) | |
| def _now(): | |
| return datetime.now(timezone.utc).isoformat() | |
| def fetch_solver_files(repo_url): | |
| """Download solver files from GitHub raw URL into /app/solver/.""" | |
| os.makedirs(SOLVER_SRC, exist_ok=True) | |
| for name in ("run.py", "spawn.py", "outline.md", "config.json"): | |
| p = os.path.join(SOLVER_SRC, name) | |
| if os.path.exists(p): | |
| os.remove(p) | |
| print(f"[INIT] Fetching solver files from: {repo_url}", flush=True) | |
| files = ["run.py", "spawn.py", "outline.md", "config.json"] | |
| downloaded = [] | |
| errors = [] | |
| for name in files: | |
| url = f"{repo_url.rstrip('/')}/{name}" | |
| try: | |
| with urllib.request.urlopen(url, timeout=30) as resp: | |
| data = resp.read() | |
| with open(os.path.join(SOLVER_SRC, name), "wb") as f: | |
| f.write(data) | |
| downloaded.append(name) | |
| print(f"[INIT] Downloaded {name}", flush=True) | |
| except Exception as e: | |
| errors.append(f"{name}: {e}") | |
| print(f"[INIT] Failed to download {name}: {e}", flush=True) | |
| if not downloaded: | |
| print(f"[INIT] CRITICAL: No solver files downloaded. Errors: {errors}", flush=True) | |
| return False | |
| if errors: | |
| print(f"[INIT] Partial download. Missing: {errors}", flush=True) | |
| return True | |
| def active_count(): | |
| """Caller must hold tasks_lock.""" | |
| return sum(1 for t in tasks.values() if t["status"] in ("pending", "running")) | |
| def submit_task(problem_text, name=None, config_override=None, secret_key=None): | |
| """Create a new task, return task_id.""" | |
| print("[SUBMIT] submit_task() called", flush=True) | |
| try: | |
| task_id = f"task-{int(time.time()*1000)}" | |
| workspace = os.path.join(WORKSPACE_BASE, task_id) | |
| print(f"[SUBMIT] Creating workspace: {workspace}", flush=True) | |
| os.makedirs(workspace, exist_ok=True) | |
| # Write problem | |
| with open(os.path.join(workspace, "problem.md"), "w", encoding="utf-8") as f: | |
| f.write(problem_text) | |
| print(f"[SUBMIT] Wrote problem.md", flush=True) | |
| # Copy solver source into workspace | |
| copied = [] | |
| for fname in ("run.py", "spawn.py", "outline.md", "config.json"): | |
| src = os.path.join(SOLVER_SRC, fname) | |
| exists = os.path.exists(src) | |
| print(f"[SUBMIT] {fname}: exists={exists} at {src}", flush=True) | |
| if exists: | |
| shutil.copy2(src, os.path.join(workspace, fname)) | |
| copied.append(fname) | |
| print(f"[SUBMIT] Copied: {copied}", flush=True) | |
| # Apply config override if provided | |
| if config_override: | |
| config_path = os.path.join(workspace, "config.json") | |
| if os.path.exists(config_path): | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| current = json.load(f) | |
| current.update(config_override) | |
| with open(config_path, "w", encoding="utf-8") as f: | |
| json.dump(current, f, indent=2, ensure_ascii=False) | |
| print(f"[SUBMIT] Applied config override: {list(config_override.keys())}", flush=True) | |
| else: | |
| with open(config_path, "w", encoding="utf-8") as f: | |
| json.dump(config_override, f, indent=2, ensure_ascii=False) | |
| print(f"[SUBMIT] Created config from override", flush=True) | |
| # Save secret key if provided (private mode) | |
| is_private = bool(secret_key) | |
| if is_private: | |
| with open(os.path.join(workspace, ".secret"), "w", encoding="utf-8") as f: | |
| f.write(secret_key) | |
| task = { | |
| "task_id": task_id, | |
| "name": name or f"Problem {len(tasks)+1}", | |
| "status": "pending", | |
| "phase": "waiting", | |
| "workspace": workspace, | |
| "problem_preview": problem_text[:120], | |
| "problem_length": len(problem_text), | |
| "created_at": _now(), | |
| "started_at": None, | |
| "finished_at": None, | |
| "error": None, | |
| "is_private": is_private, | |
| } | |
| with tasks_lock: | |
| tasks[task_id] = task | |
| print(f"[SUBMIT] Task registered in memory", flush=True) | |
| # Create log file | |
| log_path = os.path.join(workspace, "solver.log") | |
| with open(log_path, "w", encoding="utf-8") as f: | |
| f.write(f"[{_now()}] Task {task_id} created\n") | |
| print(f"[SUBMIT] Created task {task_id} -> {workspace}", flush=True) | |
| # Spawn background thread — run.py manages its own timeout & concurrency | |
| t = threading.Thread(target=_run_solver, args=(task_id, workspace), daemon=True) | |
| t.start() | |
| print(f"[SUBMIT] Thread started", flush=True) | |
| return task_id | |
| except Exception as e: | |
| import traceback | |
| tb = traceback.format_exc() | |
| print(f"[SUBMIT] ERROR: {e}", flush=True) | |
| print(f"[SUBMIT] Traceback:\n{tb}", flush=True) | |
| raise | |
| def _run_solver(task_id, workspace): | |
| print(f"[{task_id}] _run_solver thread started", flush=True) | |
| with tasks_lock: | |
| tasks[task_id].update({"status": "running", "started_at": _now()}) | |
| _log(task_id, f"Starting solver in {workspace}") | |
| _log(task_id, f"ANTHROPIC_API_KEY: {'set' if os.environ.get('ANTHROPIC_API_KEY') else 'NOT SET'}") | |
| _log(task_id, f"ANTHROPIC_BASE_URL: {os.environ.get('ANTHROPIC_BASE_URL', '(default)')}") | |
| env = os.environ.copy() | |
| start = time.time() | |
| proc = None | |
| try: | |
| cmd = [sys.executable, os.path.join(workspace, "run.py"), workspace] | |
| print(f"[{task_id}] Running: {' '.join(cmd)}", flush=True) | |
| proc = subprocess.Popen( | |
| cmd, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| cwd=workspace, | |
| env=env, | |
| ) | |
| _log(task_id, f"spawned run.py with PID {proc.pid}") | |
| # Read stdout line by line, append to log | |
| def _read_output(): | |
| if proc.stdout: | |
| for line in iter(proc.stdout.readline, b""): | |
| decoded = line.decode("utf-8", errors="replace").strip() | |
| if decoded: | |
| _log(task_id, f"[stdout] {decoded}") | |
| reader = threading.Thread(target=_read_output, daemon=True) | |
| reader.start() | |
| # Monitor workspace for progress (no timeout — run.py handles its own) | |
| last_phase = "starting" | |
| last_state = _read_state(workspace) | |
| with tasks_lock: | |
| tasks[task_id]["file_history"] = [] | |
| while proc.poll() is None: | |
| phase = _infer_phase(workspace) | |
| if phase != last_phase: | |
| _log(task_id, f"Phase: {phase}") | |
| last_phase = phase | |
| # Track .state changes for history | |
| state = _read_state(workspace) | |
| if state and state != last_state: | |
| _log(task_id, f"State: {state}") | |
| history = STATE_TO_HISTORY.get(state, []) | |
| with tasks_lock: | |
| tasks[task_id]["file_history"] = [dict(h) for h in history] | |
| last_state = state | |
| with tasks_lock: | |
| tasks[task_id]["phase"] = phase | |
| tasks[task_id]["elapsed_s"] = round(time.time() - start) | |
| time.sleep(10) | |
| reader.join(timeout=3) | |
| _log(task_id, f"run.py exited with code {proc.returncode}") | |
| if proc.returncode != 0: | |
| _log(task_id, "Solver returned non-zero exit code") | |
| with tasks_lock: | |
| tasks[task_id].update({ | |
| "status": "error", | |
| "error": f"Solver exited with code {proc.returncode}", | |
| "finished_at": _now(), | |
| }) | |
| return | |
| summary = os.path.join(workspace, "final_summary.md") | |
| if os.path.exists(summary): | |
| _log(task_id, "Solver completed successfully") | |
| with tasks_lock: | |
| tasks[task_id].update({ | |
| "status": "complete", | |
| "phase": "complete", | |
| "finished_at": _now(), | |
| }) | |
| else: | |
| _log(task_id, "Solver finished but no summary file found") | |
| with tasks_lock: | |
| tasks[task_id].update({ | |
| "status": "error", | |
| "error": "Solver finished but no summary file was produced.", | |
| "finished_at": _now(), | |
| }) | |
| except FileNotFoundError: | |
| _log(task_id, f"FileNotFoundError: run.py not found") | |
| with tasks_lock: | |
| tasks[task_id].update({ | |
| "status": "error", | |
| "error": "run.py not found in workspace.", | |
| "finished_at": _now(), | |
| }) | |
| except Exception as e: | |
| import traceback | |
| tb = traceback.format_exc() | |
| _log(task_id, f"Exception: {e}") | |
| _log(task_id, tb) | |
| print(f"[{task_id}] Traceback:\n{tb}", flush=True) | |
| with tasks_lock: | |
| tasks[task_id].update({ | |
| "status": "error", | |
| "error": str(e), | |
| "finished_at": _now(), | |
| }) | |
| # Known solver workflow steps in execution order. Unknown files append at end alphabetically. | |
| WORKFLOW_STEPS = ["plan", "solution", "review", "final_summary"] | |
| # Mapping from .state value to completed steps | |
| STATE_TO_HISTORY = { | |
| "planner": [], # not started yet | |
| "builder": [{"step": "plan", "status": "history"}], | |
| "evaluator": [{"step": "plan", "status": "history"}, {"step": "solution", "status": "history"}], | |
| "done": [{"step": "plan", "status": "history"}, {"step": "solution", "status": "history"}, | |
| {"step": "review", "status": "history"}, {"step": "final_summary", "status": "history"}], | |
| } | |
| STATE_TO_PHASE = { | |
| "planner": "starting", | |
| "builder": "building", | |
| "evaluator": "evaluating", | |
| "done": "complete", | |
| } | |
| # Files to exclude from progress tracking | |
| PROGRESS_EXCLUDES = {"problem.md", "solver.log", "outline.md"} | |
| def _read_state(workspace): | |
| """Read .state file. Returns phase string (planner/builder/evaluator/done) or None.""" | |
| path = os.path.join(workspace, ".state") | |
| if not os.path.isfile(path): | |
| return None | |
| try: | |
| with open(path, encoding="utf-8") as f: | |
| return f.read().strip() | |
| except Exception: | |
| return None | |
| def _read_secret(workspace): | |
| """Read .secret file. Returns secret key string or None.""" | |
| path = os.path.join(workspace, ".secret") | |
| if not os.path.isfile(path): | |
| return None | |
| try: | |
| with open(path, encoding="utf-8") as f: | |
| return f.read().strip() | |
| except Exception: | |
| return None | |
| def _check_access(workspace, provided_key=None): | |
| """Check if provided_key grants access to a private task. Returns True if public or key matches.""" | |
| secret = _read_secret(workspace) | |
| if not secret: | |
| return True # not a private task | |
| if provided_key and provided_key == secret: | |
| return True | |
| return False | |
| def _build_history_from_state(workspace, state=None): | |
| """Build progress history from .state file, marking last completed step as 'current'.""" | |
| if state is None: | |
| state = _read_state(workspace) | |
| if state is None: | |
| return None # no .state → caller should fall back to file scanning | |
| history = STATE_TO_HISTORY.get(state) | |
| if history is None: | |
| return [] | |
| result = list(history) | |
| if result: | |
| result[-1] = dict(result[-1], status="current") | |
| return result | |
| def _check_files(workspace, file_history=None): | |
| """Return list of steps with history. file_history is an ordered list from monitoring.""" | |
| if file_history and len(file_history) > 0: | |
| # Use monitored history | |
| result = list(file_history) | |
| if result: | |
| result[-1] = dict(result[-1], status="current") | |
| return result | |
| # Priority 1: .state file | |
| from_state = _build_history_from_state(workspace) | |
| if from_state is not None: | |
| return from_state | |
| # Fallback: scan by mtime (for tasks created before this change or without .state) | |
| if not os.path.isdir(workspace): | |
| return [] | |
| mtimes = {} | |
| for fname in os.listdir(workspace): | |
| if fname.endswith(".md") and fname not in PROGRESS_EXCLUDES: | |
| step = fname[:-3] | |
| if step in WORKFLOW_STEPS: | |
| mtimes[step] = os.path.getmtime(os.path.join(workspace, fname)) | |
| if not mtimes: | |
| return [] | |
| latest = max(mtimes, key=mtimes.get) | |
| result = [] | |
| for step in WORKFLOW_STEPS: | |
| if step in mtimes: | |
| status = "current" if step == latest else "history" | |
| result.append({"step": step, "status": status}) | |
| other = sorted(s for s in mtimes if s not in set(WORKFLOW_STEPS)) | |
| for s in other: | |
| result.append({"step": s, "status": "history"}) | |
| return result | |
| def _infer_phase(workspace): | |
| # Priority: .state file first | |
| state = _read_state(workspace) | |
| if state and state in STATE_TO_PHASE: | |
| return STATE_TO_PHASE[state] | |
| # Fallback: scan .md files | |
| if os.path.exists(os.path.join(workspace, "final_summary.md")): | |
| return "complete" | |
| if os.path.exists(os.path.join(workspace, "review.md")): | |
| return "evaluating" | |
| if os.path.exists(os.path.join(workspace, "solution.md")): | |
| return "building" | |
| if os.path.exists(os.path.join(workspace, "plan.md")): | |
| return "planning" | |
| return "starting" | |
| def get_task(task_id, secret_key=None): | |
| with tasks_lock: | |
| task = tasks.get(task_id) | |
| if task: | |
| # Check access for private tasks | |
| if not _check_access(task["workspace"], secret_key): | |
| return None | |
| result = dict(task) | |
| result.pop("problem_preview", None) | |
| history = result.get("file_history") | |
| result["files_done"] = _check_files(task["workspace"], history) | |
| return result | |
| return None | |
| def get_task_result(task_id, secret_key=None): | |
| task = get_task(task_id, secret_key) | |
| if not task: | |
| return None | |
| summary = os.path.join(task.get("workspace", ""), "final_summary.md") | |
| if os.path.exists(summary): | |
| with open(summary, encoding="utf-8") as f: | |
| return f.read() | |
| return None | |
| def get_task_logs(task_id, lines=100, secret_key=None): | |
| with tasks_lock: | |
| task = tasks.get(task_id) | |
| if not task: | |
| return [f"Task {task_id} not found in memory."] | |
| if not _check_access(task["workspace"], secret_key): | |
| return ["Access denied: invalid or missing secret key."] | |
| log_path = os.path.join(task["workspace"], "solver.log") | |
| if not os.path.exists(log_path): | |
| return ["Waiting for solver to start..."] | |
| try: | |
| with open(log_path, encoding="utf-8") as f: | |
| all_lines = f.readlines() | |
| return [l.rstrip() for l in all_lines[-lines:]] | |
| except Exception as e: | |
| return [f"Error reading log: {e}"] | |
| def get_workspace_file(task_id, filename, secret_key=None): | |
| """Read a file from a task's workspace. Returns content or None if not found.""" | |
| with tasks_lock: | |
| task = tasks.get(task_id) | |
| if not task: | |
| return None | |
| if not _check_access(task["workspace"], secret_key): | |
| return None | |
| fpath = os.path.join(task["workspace"], filename) | |
| if not os.path.isfile(fpath): | |
| return None | |
| with open(fpath, encoding="utf-8") as f: | |
| return f.read() | |
| def list_workspace_files(task_id, secret_key=None): | |
| """List all non-hidden .md files in a task's workspace.""" | |
| with tasks_lock: | |
| task = tasks.get(task_id) | |
| if not task: | |
| return [] | |
| if not _check_access(task["workspace"], secret_key): | |
| return [] | |
| ws = task["workspace"] | |
| if not os.path.isdir(ws): | |
| return [] | |
| result = [] | |
| for fname in sorted(os.listdir(ws)): | |
| if fname.endswith(".md") and not fname.startswith("."): | |
| result.append(fname) | |
| return result | |
| def get_running_tasks(): | |
| """Return list of currently running tasks (for frontend recovery after page refresh).""" | |
| with tasks_lock: | |
| return [ | |
| { | |
| "task_id": t["task_id"], | |
| "status": t["status"], | |
| "phase": t["phase"], | |
| "problem": t["problem"], | |
| "created_at": t["created_at"], | |
| "files_done": _check_files(t["workspace"], t.get("file_history")), | |
| } | |
| for t in tasks.values() | |
| if t["status"] in ("pending", "running") | |
| ] | |
| def list_tasks(secret_key=None): | |
| with tasks_lock: | |
| return [ | |
| { | |
| "task_id": t["task_id"], | |
| "name": t["name"], | |
| "status": t["status"], | |
| "phase": t["phase"], | |
| "problem_preview": t.get("problem_preview", "") if not t.get("is_private") else "🔒", | |
| "problem_length": t.get("problem_length", 0), | |
| "created_at": t["created_at"], | |
| "started_at": t.get("started_at"), | |
| "finished_at": t.get("finished_at"), | |
| "files_done": _check_files(t["workspace"], t.get("file_history")), | |
| "error": t.get("error"), | |
| "is_private": t.get("is_private", False), | |
| } | |
| for t in sorted(tasks.values(), key=lambda x: x["created_at"], reverse=True) | |
| ] | |
| def delete_task(task_id, secret_key=None): | |
| """Remove a task from memory and delete its workspace. Returns True if found.""" | |
| with tasks_lock: | |
| task = tasks.get(task_id) | |
| if not task: | |
| return False | |
| if task.get("is_private") and not _check_access(task["workspace"], secret_key): | |
| return False | |
| task = tasks.pop(task_id, None) | |
| workspace = task.get("workspace", "") | |
| if os.path.isdir(workspace): | |
| shutil.rmtree(workspace) | |
| _log(task_id, f"Task {task_id} deleted") | |
| return True | |
| def cleanup_completed_tasks(): | |
| """Delete all completed and errored tasks. Returns count of deleted tasks.""" | |
| with tasks_lock: | |
| to_delete = [ | |
| tid for tid, t in tasks.items() | |
| if t["status"] in ("complete", "error") | |
| ] | |
| count = 0 | |
| for tid in to_delete: | |
| if delete_task(tid): | |
| count += 1 | |
| return count | |
| def update_task_config(task_id, config_dict): | |
| """Update config.json in a task's workspace. Only allowed for pending tasks.""" | |
| with tasks_lock: | |
| task = tasks.get(task_id) | |
| if not task: | |
| return None, "Task not found" | |
| if task["status"] != "pending": | |
| return None, f"Cannot update config for task in '{task['status']}' status (only 'pending')" | |
| config_path = os.path.join(task["workspace"], "config.json") | |
| try: | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| current = json.load(f) | |
| current.update(config_dict) | |
| with open(config_path, "w", encoding="utf-8") as f: | |
| json.dump(current, f, indent=2, ensure_ascii=False) | |
| return current, None | |
| except FileNotFoundError: | |
| return None, "config.json not found in workspace" | |
| except json.JSONDecodeError: | |
| return None, "Invalid config.json in workspace" | |