forked from luopingyi/xc_agent_platform_demo
Phase1 skeleton: replace demo main.py with src.server contract
- Overlay ModelHub XC Phase1 (health/status, STRATEGY_ID gate, dry-run) - Dockerfile CMD -> python -m src.server; FROM stays platform TCR base - Remove demo main.py and requirements.txt (stdlib only) - Add tests/, scripts/, docs/, Makefile, config/examples (no secrets)
This commit is contained in:
107
scripts/dry_run.py
Executable file
107
scripts/dry_run.py
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a dry-run adaptation report (no platform side effects).
|
||||
|
||||
Reads config/candidates.example.json (or DRY_RUN_CANDIDATES path).
|
||||
Writes reports/dry-run-<Asia/Shanghai-timestamp>.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TZ = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
|
||||
def _load_candidates(path: Path) -> list[dict]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if isinstance(data, dict) and "candidates" in data:
|
||||
return list(data["candidates"])
|
||||
raise SystemExit(f"unexpected candidates schema in {path}")
|
||||
|
||||
|
||||
def _render(candidates: list[dict], ts: str, src: Path) -> str:
|
||||
lines = [
|
||||
f"# Dry-run 适配建议报告 — {ts} (Asia/Shanghai)",
|
||||
"",
|
||||
"> **模式:dry-run** — 本报告不产生平台侧真实提交。",
|
||||
"> **需 Victor 人工提交**:请登录模盒控制台按草稿确认后手工提交。",
|
||||
"> **积分:待平台核实**(发布智能体本身不得分;成功适配按账本计)。",
|
||||
"",
|
||||
"## 闸门状态",
|
||||
"",
|
||||
f"- ALLOW_LIVE_SUBMIT is 1: {os.environ.get('ALLOW_LIVE_SUBMIT', '0').strip() == '1'}",
|
||||
f"- LIVE_CONFIRM set: {'yes' if os.environ.get('LIVE_CONFIRM', '').strip() else 'no'}",
|
||||
f"- STRATEGY_ID present: {'yes' if os.environ.get('STRATEGY_ID', '').strip() else 'no'}",
|
||||
"",
|
||||
"## 输入",
|
||||
"",
|
||||
f"- 候选文件:`{src}`",
|
||||
f"- 候选条数:{len(candidates)}",
|
||||
"",
|
||||
"## 建议草稿(字段示意,非真实 POST)",
|
||||
"",
|
||||
]
|
||||
for i, c in enumerate(candidates, 1):
|
||||
model = c.get("modelAddress") or c.get("model_id") or "(missing model_id)"
|
||||
lines.append(f"### {i}. `{model}`")
|
||||
lines.append("")
|
||||
lines.append("```json")
|
||||
draft = {
|
||||
"modelAddress": model,
|
||||
"taskType": c.get("taskType", "text-generation"),
|
||||
"targetGpu": c.get("targetGpu", "(unset)"),
|
||||
"framework": c.get("framework", "(unset)"),
|
||||
"strategyId": "<PLATFORM_INJECTED_STRATEGY_ID>",
|
||||
"configParams": "<from build-config — not fetched in Phase1 dry-run>",
|
||||
"notes": c.get("notes", ""),
|
||||
}
|
||||
lines.append(json.dumps(draft, ensure_ascii=False, indent=2))
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
lines.append("- 风险:Phase1 未做唯一性/硬门预检;提交前请对照 do-not-resubmit 与队列空位。")
|
||||
lines.append("- **需 Victor 人工提交**(或经双重闸门的受控 live;本骨架默认拒绝 live)。")
|
||||
lines.append("- **积分:待平台核实**")
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
[
|
||||
"## 后续对接(Phase2)",
|
||||
"",
|
||||
"- **待对接 pending-submit**:尚未读取 `/workspace/modelhub-adapt/pending-submit.md`,未验证 A→B、PASS、硬门与 do-not-resubmit;本报告仅保留输入顺序。",
|
||||
"- 适配 W1 优先;同一 100 队列,≥95 全体停交;空位先 A 再 B,人工核验后决定,不凭示例报告提交。",
|
||||
"- 禁止 Makefile / 脚本调用 `/api/adapt/task/add` 或 `/agent_platform/strategies*` 写接口。",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
src = Path(
|
||||
os.environ.get(
|
||||
"DRY_RUN_CANDIDATES",
|
||||
str(ROOT / "config" / "candidates.example.json"),
|
||||
)
|
||||
)
|
||||
if not src.is_file():
|
||||
raise SystemExit(f"candidates file not found: {src}")
|
||||
|
||||
candidates = _load_candidates(src)
|
||||
now = datetime.now(TZ)
|
||||
ts = now.strftime("%Y%m%d-%H%M%S")
|
||||
out_dir = ROOT / "reports"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"dry-run-{ts}.md"
|
||||
out_path.write_text(_render(candidates, now.strftime("%Y-%m-%d %H:%M:%S"), src), encoding="utf-8")
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
100
scripts/smoke_health.py
Executable file
100
scripts/smoke_health.py
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke: GET /health must return 200 {"status":"ok"}.
|
||||
|
||||
Usage:
|
||||
# Against already-running server:
|
||||
SMOKE_BASE_URL=http://127.0.0.1:8080 python3 scripts/smoke_health.py
|
||||
|
||||
# Or let this script start a local server (needs STRATEGY_ID):
|
||||
STRATEGY_ID=test python3 scripts/smoke_health.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _get(url: str, timeout: float = 2.0) -> tuple[int, dict]:
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
return resp.status, json.loads(body)
|
||||
|
||||
|
||||
def _wait_health(base: str, attempts: int = 30) -> None:
|
||||
url = base.rstrip("/") + "/health"
|
||||
last_err: Exception | None = None
|
||||
for _ in range(attempts):
|
||||
try:
|
||||
code, data = _get(url)
|
||||
if code == 200 and data.get("status") == "ok":
|
||||
print(f"OK: GET {url} -> {code} {data}")
|
||||
return
|
||||
raise AssertionError(f"unexpected response: {code} {data}")
|
||||
except (urllib.error.URLError, ConnectionError, AssertionError, TimeoutError) as exc:
|
||||
last_err = exc
|
||||
time.sleep(0.2)
|
||||
raise SystemExit(f"smoke-health failed: {last_err}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
external = os.environ.get("SMOKE_BASE_URL", "").strip()
|
||||
if external:
|
||||
_wait_health(external)
|
||||
# optional status check
|
||||
try:
|
||||
code, data = _get(external.rstrip("/") + "/status")
|
||||
print(f"OK: GET /status -> {code} {data}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"WARN: /status check skipped: {exc}", file=sys.stderr)
|
||||
return
|
||||
|
||||
if not os.environ.get("STRATEGY_ID", "").strip():
|
||||
os.environ["STRATEGY_ID"] = "smoke-test-strategy"
|
||||
|
||||
port = os.environ.get("SMOKE_PORT", "18080")
|
||||
env = os.environ.copy()
|
||||
env["PORT"] = port
|
||||
env["HOST"] = "127.0.0.1"
|
||||
env["STRATEGY_ID"] = env.get("STRATEGY_ID") or "smoke-test-strategy"
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "src.server"],
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
try:
|
||||
_wait_health(f"http://127.0.0.1:{port}")
|
||||
code, data = _get(f"http://127.0.0.1:{port}/status")
|
||||
print(f"OK: GET /status -> {code} {data}")
|
||||
assert "strategy_id_present" in data
|
||||
assert data["strategy_id_present"] is True
|
||||
# never assert on secret values — only booleans
|
||||
finally:
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait(timeout=2)
|
||||
if proc.returncode not in (0, -signal.SIGTERM, signal.SIGTERM):
|
||||
# 0 or terminated by SIGTERM is fine
|
||||
err = (proc.stderr.read() or b"").decode("utf-8", errors="replace")
|
||||
if proc.returncode is not None and proc.returncode > 0:
|
||||
print(f"WARN: server exit code {proc.returncode}: {err}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
43
scripts/tag_release.py
Executable file
43
scripts/tag_release.py
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a local git tag only. Does NOT push. Does NOT register on ModelHub.
|
||||
|
||||
Usage:
|
||||
python3 scripts/tag_release.py v0.1.0
|
||||
python3 scripts/tag_release.py v0.1.0 --message "Phase1 skeleton"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Local git tag only (no push, no register)")
|
||||
parser.add_argument("tag", help="Tag name, e.g. v0.1.0")
|
||||
parser.add_argument("--message", "-m", default="", help="Annotated tag message")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not (ROOT / ".git").exists():
|
||||
print(
|
||||
"WARN: not a git repository yet. Init locally if needed; "
|
||||
"this script will not push or register.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# Still attempt so the failure is clear
|
||||
msg = args.message or f"Release {args.tag} (local tag only — no push / no ModelHub register)"
|
||||
cmd = ["git", "tag", "-a", args.tag, "-m", msg]
|
||||
print(f"Running (cwd={ROOT}): {' '.join(cmd)}")
|
||||
print("NOTE: This does NOT git push and does NOT call ModelHub APIs.")
|
||||
result = subprocess.run(cmd, cwd=str(ROOT))
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(result.returncode)
|
||||
print(f"OK: local tag {args.tag!r} created. Push tag yourself when ready for Kaniko.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user