Spaces:
Running on Zero
Running on Zero
Download agent_runner.py from cmuchancel/patent2sysml: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/spaces/cmuchancel/patent2sysml/resolve/main/agent_runner.py
- Command line
-
hf download hf://spaces/cmuchancel/patent2sysml/agent_runner.py
-
curl -L -o agent_runner.py https://huggingface.co/spaces/cmuchancel/patent2sysml/resolve/main/agent_runner.py
14.8 kB
| """Launch the upstream OpenCode agents. No extraction logic lives here.""" | |
| import json | |
| import os | |
| import re | |
| import shutil | |
| import signal | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import time | |
| from pathlib import Path | |
| from user_session import connected, session_env | |
| from research import ResearchRun, export_sessions, append, now, digest | |
| from workflow import COMMON, ORCHESTRATION, DECOMPOSITION, REVIEW, read_json, write_json | |
| ROOT = Path(__file__).resolve().parent | |
| UPSTREAM = ROOT / "vendor/Info-extraction" | |
| MODEL = os.getenv("OPENCODE_MODEL", "openai/gpt-6-luna") | |
| BOOK = os.getenv("SE_EPUB_PATH", str(ROOT / "assets/textbook.epub")) | |
| ORCHESTRATOR = "patent-decomposition-orchestrator" | |
| DECOMPOSER = "patent-functional-decomposer" | |
| def prepare_run(file): | |
| source, textbook = Path(file).resolve(), Path(BOOK).resolve() | |
| if source.suffix.lower() not in {".html", ".htm"} or not source.is_file(): | |
| raise ValueError("Upload a patent HTML file.") | |
| if not source.stat().st_size or source.stat().st_size > 10_000_000: | |
| raise ValueError("Upload a nonempty patent smaller than 10 MB.") | |
| if not textbook.is_file() or textbook.suffix.lower() != ".epub": | |
| raise ValueError("The backend textbook is missing. Configure SE_EPUB_PATH on the server.") | |
| if not shutil.which("opencode"): | |
| raise ValueError("OpenCode is not installed on this server.") | |
| from sysml_check import PARSER | |
| if not shutil.which("node") or not (PARSER / "syside-languageserver.js").is_file(): | |
| raise ValueError("Install Node.js and run python setup_parser.py before processing patents.") | |
| from sjs import translator | |
| translator() # Fail before model calls if the pinned translator is unavailable. | |
| runs = ROOT / "runs" | |
| runs.mkdir(exist_ok=True) | |
| run = Path(tempfile.mkdtemp(prefix="patent-", dir=runs)) | |
| (run / "input").mkdir() | |
| (run / "output").mkdir() | |
| stem = re.sub(r"[^a-zA-Z0-9_-]", "_", source.stem)[:80] or "patent" | |
| shutil.copyfile(source, run / "input" / (stem + ".html")) | |
| write_json(run / "research/input-source.json", {"original_filename": source.name, | |
| "stored_filename": stem + ".html"}) | |
| agents = {} | |
| roles = [(ORCHESTRATOR, "primary", ORCHESTRATION, | |
| ["workspace_get_patent_info", "workspace_get_workflow_status", "workspace_finish_batch"]), | |
| (DECOMPOSER, "subagent", DECOMPOSITION, | |
| ["workspace_get_patent_info", "workspace_get_workflow_status", "workspace_get_patent_context", "workspace_get_drafting_context", | |
| "workspace_save_question_plan", "workspace_save_sjs", "workspace_repair_sjs", "workspace_get_review_context", | |
| "workspace_plan_repair", "patent_RAG_html_clear_database", "patent_RAG_html_ingest_html", | |
| "patent_RAG_html_query", "systems_eng_context_retrieve_subsection_context", | |
| "workspace_read_patent_evidence", "workspace_read_textbook_evidence"]), | |
| ("patent-quality-reviewer", "subagent", REVIEW, | |
| ["workspace_get_review_context", "workspace_save_review", | |
| "workspace_read_patent_evidence", "workspace_read_textbook_evidence"])] | |
| for name, mode, protocol, allowed in roles: | |
| # Do not append an incompatible upstream JSON schema to the SJS instructions. | |
| body = COMMON + protocol | |
| body += "\nUse epub_path=" + json.dumps(str(textbook)) + " on every textbook retrieval." | |
| body += " Servers: patent_RAG_html, systems_eng_context, workspace. Use configured collection/database only." | |
| permissions = [{"action": "*", "resource": "*", "effect": "deny"}] | |
| permissions += [{"action": action, "resource": "*", "effect": "allow"} for action in allowed] | |
| if mode == "primary": | |
| permissions += [{"action": "subagent", "resource": child, "effect": "allow"} | |
| for child in [DECOMPOSER, "patent-quality-reviewer"]] | |
| agents[name] = {"description": name, "mode": mode, "system": body, | |
| "permissions": permissions, "steps": 80} | |
| config = { | |
| "$schema": "https://opencode.ai/config.json", "model": MODEL, | |
| "default_agent": ORCHESTRATOR, "agents": agents, "snapshots": False, | |
| "plugins": [str(ROOT / "research-plugin")], | |
| "permissions": [{"action": "*", "resource": "*", "effect": "deny"}], | |
| "mcp": {"servers": { | |
| "patent_RAG_html": { | |
| "type": "local", "codemode": False, | |
| "command": [sys.executable, str(ROOT / "capture_mcp.py"), | |
| str(UPSTREAM / "html-mcp/server.py"), str(run / "output/_evidence/patent.jsonl")], | |
| "environment": {"HTML_RAG_DB_PATH": str(run / "chroma"), | |
| "HTML_RAG_COLLECTION": "patent_run", "ANONYMIZED_TELEMETRY": "False", | |
| "PATENT_FILE": str(run / "input" / (stem + ".html"))}}, | |
| "systems_eng_context": { | |
| "type": "local", "codemode": False, | |
| "command": [sys.executable, str(ROOT / "capture_mcp.py"), | |
| str(ROOT / "textbook_index.py"), | |
| str(run / "output/_evidence/textbook.jsonl")]}, | |
| "workspace": {"type": "local", "codemode": False, | |
| "command": [sys.executable, str(ROOT / "workspace_mcp.py"), str(run)]} | |
| }}} | |
| (run / "opencode.jsonc").write_text(json.dumps(config, indent=2)) | |
| return run, stem | |
| def load_result(run, stem): | |
| """Require an actual successful artifact and manifest, not just CLI exit 0.""" | |
| result = run / "output" / stem / (stem + ".sjs.json") | |
| manifest = json.loads((run / "output/manifest.json").read_text()) | |
| if manifest.get("patents_succeeded") != 1 or manifest.get("patents_failed") != 0: | |
| raise ValueError("The agent reported an incomplete run. See its run log.") | |
| data = json.loads(result.read_text()) | |
| if manifest.get('cleanup_status') != 'ok' or manifest.get('artifact_sha256') != digest(result): | |
| raise ValueError('Successful cleanup and an unchanged approved SJS artifact are required.') | |
| evidence = read_json(result.with_suffix('.evidence.json'), {}) | |
| from quality import validate | |
| if validate({'sjs': data, **evidence}): | |
| raise ValueError('The saved SJS or its evidence failed validation.') | |
| return data, str(result) | |
| def cleanup(run): | |
| """Always clear this run's collection after agents stop, including failed runs.""" | |
| started = time.monotonic() | |
| try: | |
| if not (run / "chroma").exists(): | |
| result = {"status": "ok", "remaining_count": 0, "detail": "Database never created"} | |
| else: | |
| import chromadb | |
| client = chromadb.PersistentClient(path=str(run / "chroma")) | |
| names = [c.name for c in client.list_collections()] | |
| if "patent_run" in names: | |
| collection = client.get_collection("patent_run") | |
| ids = collection.get(include=[])["ids"] | |
| if ids: | |
| collection.delete(ids=ids) | |
| result = {"status": "ok", "remaining_count": collection.count(), "deleted_count": len(ids)} | |
| else: | |
| result = {"status": "ok", "remaining_count": 0} | |
| if result["remaining_count"]: | |
| raise ValueError("Patent database cleanup failed") | |
| except Exception as error: | |
| result = {"status": "error", "error": str(error)} | |
| append(run / "research/lifecycle.jsonl", {"event": "database_cleanup", "at": now(), | |
| "duration_seconds": time.monotonic() - started, "result": result}) | |
| return result | |
| def finalize_outputs(run, stem): | |
| """Validate/render saved reviewed JSON; also reusable after a host-only failure.""" | |
| from sysml_export import translate | |
| from sysml_check import check_sysml | |
| from rendering import diagram | |
| data, result = load_result(run, stem) | |
| result = Path(result) | |
| sysml = result.with_suffix(".sysml") | |
| profile, portable = translate(data) | |
| result.with_suffix('.profile.sysml').write_text(profile) | |
| sysml.write_text(portable) | |
| from sjs import REVISION, SHA256 | |
| write_json(run / 'output/validation/sjs-translation.json', { | |
| 'schema': 'sjs/1.2', 'translator': 'eandujar09/GradResearch', | |
| 'revision': REVISION, 'sha256': SHA256, 'roundtrip': 'passed', | |
| 'compatibility': 'patent2sysml portable projection v1', | |
| 'notes': 'Structural components, typed ports and flows; behavior details retained as documentation.'}) | |
| checked = check_sysml(sysml) | |
| append(run / "output/validation/parser-attempts.jsonl", {"at": now(), **checked}) | |
| write_json(run / "output/validation/sysml-parser.json", checked) | |
| if checked["status"] != "passed": | |
| raise ValueError("SysML validation did not pass; research record retained.") | |
| result.with_suffix(".svg").write_text(diagram(data)) | |
| return data, str(result) | |
| def run_agents(file, session=None): | |
| """Yield status, model JSON, model path, research ZIP (also on failure).""" | |
| if os.getenv("SPACE_ID") and not connected(session): | |
| raise ValueError("Connect your ChatGPT account for this session before processing a patent.") | |
| run, stem = prepare_run(file) | |
| prompt = "Run the hosted research protocol: contextual questions, draft SJS directly, targeted repairs and independent SJS review until approved, then finish." | |
| research = ResearchRun(run, MODEL, prompt) | |
| environment = {**session_env(session), "PWD": str(run), "PATENT_RESEARCH_DIR": str(run / "research")} | |
| command = ["opencode", "run", "--standalone", "--auto", "--agent", ORCHESTRATOR, | |
| "--model", MODEL, "--format", "json", prompt] | |
| write_json(run / "research/launch.json", {"command": command, "cwd": str(run), | |
| "environment_keys": sorted(environment), "model": MODEL}) | |
| process, data, result, failure, archive = None, None, None, None, None | |
| logfile = run / "opencode.jsonl" | |
| try: | |
| archive = research.checkpoint(required=True) # Verify durable storage before spending model tokens. | |
| yield "Starting agents and saving the research record…", None, None, None | |
| started, offset, checkpoint = time.monotonic(), 0, time.monotonic() | |
| with logfile.open("w") as log: | |
| process = subprocess.Popen(command, cwd=run, stdout=log, stderr=subprocess.STDOUT, | |
| env=environment, start_new_session=True) | |
| while process.poll() is None: | |
| if time.monotonic() - started > 15 and not (run / "research/model-events.jsonl").exists(): | |
| raise ValueError("Research model recorder did not start; stopping to avoid an unrecorded run.") | |
| if time.monotonic() - started > int(os.getenv("AGENT_TIMEOUT", "1800")): | |
| raise ValueError("Agent run timed out; partial research record retained.") | |
| with logfile.open() as reader: | |
| reader.seek(offset) | |
| for line in reader.readlines(): | |
| try: | |
| event = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| if event.get("type") == "error": | |
| failure = event.get("error", {}).get("message", "Agent error") | |
| if event.get("type") == "tool_use": | |
| tool = event.get("part", {}).get("tool", "agent") | |
| yield "Working: " + tool.replace("_", " "), None, None, None | |
| offset = reader.tell() | |
| if time.monotonic() - checkpoint > 60: | |
| research.checkpoint() | |
| checkpoint = time.monotonic() | |
| time.sleep(0.5) | |
| from research import json_lines | |
| for event in json_lines(logfile): | |
| if event.get("type") == "error": | |
| failure = event.get("error", {}).get("message", "Agent error") | |
| if failure or process.returncode: | |
| raise ValueError("OpenCode failed: " + str(failure or process.returncode)) | |
| result = run / "output" / stem / (stem + ".sjs.json") | |
| data = read_json(result) | |
| if not data or not (run / "output/manifest.json").exists(): | |
| state = read_json(run / 'output/state.json', {'revision': 0}) | |
| rev = state['revision'] | |
| review = read_json(run / f'output/reviews/review-{rev}.json', {}) | |
| findings = review.get('findings') or read_json(run / f'output/validation/structure-{rev}.json', []) | |
| details = '; '.join(f['description'] for f in findings if f.get('severity') in {'error', 'warning'}) | |
| raise ValueError('SJS needs further repair: ' + details if details else | |
| 'The agent stopped before saving an independently approved SJS model. Research record retained.') | |
| except Exception as error: | |
| failure = str(error) | |
| except (GeneratorExit, KeyboardInterrupt): | |
| failure = "Run cancelled; partial research record retained." | |
| raise | |
| finally: | |
| if process and process.poll() is None: | |
| os.killpg(process.pid, signal.SIGTERM) | |
| try: | |
| process.wait(timeout=5) | |
| except subprocess.TimeoutExpired: | |
| os.killpg(process.pid, signal.SIGKILL) | |
| process.wait() | |
| cleaned = cleanup(run) | |
| if cleaned["status"] != "ok": | |
| failure = failure or "Database cleanup failed." | |
| try: | |
| export_sessions(run, environment) | |
| if data and not failure: | |
| manifest = read_json(run / 'output/manifest.json') | |
| manifest['cleanup_status'] = 'ok' | |
| write_json(run / 'output/manifest.json', manifest) | |
| data, _ = load_result(run, stem) | |
| finalize_outputs(run, stem) | |
| research.meta["exit_code"] = process.returncode if process else None | |
| archive = research.finish("failed" if failure else "completed", failure) | |
| except Exception as error: | |
| failure = failure or str(error) | |
| research.meta.update(status="failed", error=failure, ended_at=now(), | |
| duration_seconds=time.monotonic() - research.started) | |
| research.save() | |
| archive = research.checkpoint(required=False) | |
| if research.last_error: | |
| failure += " Archive upload failed; download the local research ZIP." | |
| if failure: | |
| yield "Run incomplete: " + failure, None, None, archive | |
| else: | |
| yield "Complete. Reviewed SJS, SysML and research record saved.", data, str(result), archive | |