autosource / scripts /deploy_space.py
Shanmuk4622's picture
deploy fully live isolated AutoSource demo
924a755 verified
Raw
History Blame Contribute Delete
7.06 kB
"""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()