"""Deploy AutoSource as a fully live Docker-based Hugging Face Space. The resulting container includes the MCP server, A2A transport, all agents, the AP2 gate, seeded data, and replay traces. Each visitor gets an isolated ephemeral warehouse. Only Groq keys are synchronized; values are never printed. Run: python -m scripts.deploy_space --golden golden-candidate-02 --sync-secrets """ from __future__ import annotations import argparse import json import shutil import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from core.config import PROJECT_ROOT, api_key, traces_dir # noqa: E402 SPACE_README = """--- title: AutoSource emoji: 🤝 colorFrom: green colorTo: yellow sdk: docker app_port: 7860 pinned: false license: mit --- # AutoSource — live autonomous procurement AutoSource is a network of cooperating agents that detects a stock shortage over **MCP**, hands the request off over **A2A**, negotiates with multiple vendors under deterministic budget/floor guardrails, and asks a human to approve an **AP2-shaped mandate** before writing a mock purchase order. ## Try it 1. In the sidebar, leave **Live follow** selected. 2. Choose an item and press **Trigger stock check & negotiate**. 3. Watch the five-agent activity board and negotiation timeline. 4. Approve or reject the mandate when it appears. 5. Open **Under the hood** to inspect prompts, LLM calls, events, transcripts, and your private warehouse. Each browser session receives an isolated SQLite warehouse. Public runs are limited and serialized to protect the free API quota. Replay mode always works without making an API call. All payments are **MOCK**; no real money can move. Source: https://github.com/Shanmuk4622/AutoSource-Autonomous-Multi-Agent-Procurement-Negotiation-Network """ SPACE_DOCKERFILE = """FROM python:3.11-slim RUN useradd -m -u 1000 user WORKDIR /app COPY --chown=user:user requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY --chown=user:user . . RUN mkdir -p /tmp/autosource-sessions && chown -R user:user /tmp/autosource-sessions USER user ENV HOME=/home/user \ PYTHONUNBUFFERED=1 \ PYTHONIOENCODING=utf-8 \ AUTOSOURCE_HOSTED=1 \ AUTOSOURCE_SESSION_ROOT=/tmp/autosource-sessions \ AUTOSOURCE_LIVE_LOCK=/tmp/autosource-live.lock \ AUTOSOURCE_RPM=8 \ GROQ_MODEL=llama-3.1-8b-instant EXPOSE 7860 CMD ["streamlit", "run", "ui/app.py", "--server.port", "7860", \ "--server.address", "0.0.0.0", "--server.headless", "true", \ "--browser.gatherUsageStats", "false"] """ RUNTIME_PACKAGES = ( "agents", "config", "core", "mcp_server", "orchestrator", "protocols", "scripts", "ui", ) def _copy_runtime(stage: Path) -> None: for package in RUNTIME_PACKAGES: shutil.copytree( PROJECT_ROOT / package, stage / package, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), ) shutil.copytree(PROJECT_ROOT / "data" / "seed", stage / "data" / "seed") shutil.copy2(PROJECT_ROOT / "data" / "schema.sql", stage / "data" / "schema.sql") # Only the judge prompt is imported by the dashboard's debug view. (stage / "eval").mkdir() (stage / "eval" / "__init__.py").write_text("", encoding="utf-8") shutil.copy2(PROJECT_ROOT / "eval" / "judge.py", stage / "eval" / "judge.py") if (PROJECT_ROOT / "LICENSE").exists(): shutil.copy2(PROJECT_ROOT / "LICENSE", stage / "LICENSE") def _bundle_replays(stage: Path, run_ids: list[str]) -> None: target = stage / "traces" (target / "negotiations").mkdir(parents=True) call_log = traces_dir() / "llm_calls.jsonl" if call_log.exists(): shutil.copy2(call_log, target / call_log.name) for run_id in run_ids: for suffix in (".jsonl", ".mandates.json", ".console.log"): source = traces_dir() / f"{run_id}{suffix}" if source.exists(): shutil.copy2(source, target / source.name) events_file = traces_dir() / f"{run_id}.jsonl" if not events_file.exists(): continue for line in events_file.read_text(encoding="utf-8").splitlines(): try: ref = json.loads(line).get("data", {}).get("transcript_ref") except json.JSONDecodeError: continue if ref: source = PROJECT_ROOT / ref if source.exists(): shutil.copy2(source, target / "negotiations" / source.name) def _sync_groq_secrets(api, space: str) -> None: synced = [] for name in ("GROQ_API_KEY", "GROQ_API_KEY_2"): value = api_key(name) if value: api.add_space_secret( repo_id=space, key=name, value=value, description="AutoSource live negotiation provider key", ) synced.append(name) if not synced: raise RuntimeError("No GROQ_API_KEY found locally; live Space cannot negotiate") print("[deploy_space] synchronized secret names: " + ", ".join(synced)) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--space", default="Shanmuk4622/autosource") parser.add_argument("--golden", default="golden-candidate-02", help="comma-separated replay run IDs to bundle") parser.add_argument("--sync-secrets", action="store_true", help="write local Groq keys to HF Space secrets") args = parser.parse_args() token = api_key("HF_TOKEN") if not token: raise SystemExit("[deploy_space] HF_TOKEN missing") stage = Path(tempfile.mkdtemp(prefix="autosource-space-")) try: (stage / "README.md").write_text(SPACE_README, encoding="utf-8") (stage / "requirements.txt").write_text( (PROJECT_ROOT / "requirements.txt").read_text(encoding="utf-8"), encoding="utf-8", ) (stage / "Dockerfile").write_text(SPACE_DOCKERFILE, encoding="utf-8") _copy_runtime(stage) run_ids = [value.strip() for value in args.golden.split(",") if value.strip()] _bundle_replays(stage, run_ids) # Defense in depth: the upload staging tree must never contain secrets or DBs. for pattern in (".env", "*.key", "*.pem", "*.db", "*.db-journal"): for stray in stage.rglob(pattern): stray.unlink() from huggingface_hub import HfApi api = HfApi(token=token) api.create_repo(args.space, repo_type="space", space_sdk="docker", exist_ok=True) if args.sync_secrets: _sync_groq_secrets(api, args.space) api.upload_folder( folder_path=str(stage), repo_id=args.space, repo_type="space", commit_message="deploy fully live isolated AutoSource demo", ) finally: shutil.rmtree(stage, ignore_errors=True) print(f"[deploy_space] live at https://huggingface.co/spaces/{args.space}") if __name__ == "__main__": main()