File size: 7,059 Bytes
924a755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""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()