diff --git a/.github/workflows/hai-agent-check.yml b/.github/workflows/hai-agent-check.yml new file mode 100644 index 0000000..1be54a9 --- /dev/null +++ b/.github/workflows/hai-agent-check.yml @@ -0,0 +1,31 @@ +name: HAI Agent Branch Check + +on: + push: + branches: + - "**" + pull_request: + branches: + - "**" + workflow_dispatch: + +jobs: + run-hai-agent: + if: github.event_name != 'push' || startsWith(github.ref, 'refs/heads/') + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run HAI agent with cache fallback + env: + HAI_SOURCE_URL: ${{ vars.HAI_SOURCE_URL }} + run: python scripts/run_hai_agent.py diff --git a/.github/workflows/production-workflow.yml b/.github/workflows/production-workflow.yml new file mode 100644 index 0000000..a4674f7 --- /dev/null +++ b/.github/workflows/production-workflow.yml @@ -0,0 +1,29 @@ +name: Production Workflow + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: production-workflow-${{ github.ref }} + cancel-in-progress: true + +jobs: + production-check: + name: Run production workflow + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Verify key production assets + run: | + test -f index.html || { echo "Production check failed: missing /index.html at repository root."; exit 1; } + test -f robots.txt || { echo "Production check failed: missing /robots.txt at repository root."; exit 1; } + test -f sitemap.xml || { echo "Production check failed: missing /sitemap.xml at repository root."; exit 1; } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/data/hai_cache.db b/data/hai_cache.db new file mode 100644 index 0000000..76a7ccc Binary files /dev/null and b/data/hai_cache.db differ diff --git a/scripts/run_hai_agent.py b/scripts/run_hai_agent.py new file mode 100644 index 0000000..283feb5 --- /dev/null +++ b/scripts/run_hai_agent.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Run HAI agent with network-first mode and SQLite cache fallback.""" + +from __future__ import annotations + +import json +import os +import sqlite3 +import sys +import urllib.error +import urllib.request +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_DB_PATH = REPO_ROOT / "data" / "hai_cache.db" + + +def ensure_cache_schema(connection: sqlite3.Connection) -> None: + connection.execute( + """ + CREATE TABLE IF NOT EXISTS hai_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recorded_at TEXT NOT NULL, + source TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + """ + ) + connection.commit() + + +def fetch_live_payload(url: str, timeout: int) -> dict[str, Any]: + request = urllib.request.Request( + url, + headers={"Accept": "application/json", "User-Agent": "pollnex-hai-agent/1.0"}, + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + body = response.read().decode("utf-8") + parsed = json.loads(body) + if isinstance(parsed, dict): + return parsed + return {"value": parsed} + + +def save_snapshot(connection: sqlite3.Connection, source: str, payload: dict[str, Any]) -> None: + connection.execute( + """ + INSERT INTO hai_snapshots (recorded_at, source, payload_json) + VALUES (?, ?, ?) + """, + ( + datetime.now(timezone.utc).isoformat(), + source, + json.dumps(payload, sort_keys=True, separators=(",", ":")), + ), + ) + connection.commit() + + +def load_latest_snapshot(connection: sqlite3.Connection) -> dict[str, Any] | None: + row = connection.execute( + """ + SELECT payload_json + FROM hai_snapshots + ORDER BY id DESC + LIMIT 1 + """ + ).fetchone() + if not row: + return None + return json.loads(row[0]) + + +def normalize_payload(payload: dict[str, Any]) -> dict[str, Any]: + normalized = dict(payload) + if "hai_score" not in normalized and "score" in normalized: + normalized["hai_score"] = normalized["score"] + return normalized + + +def main() -> int: + db_path = Path(os.getenv("HAI_CACHE_DB", str(DEFAULT_DB_PATH))) + source_url = os.getenv("HAI_SOURCE_URL", "").strip() + timeout_seconds = int(os.getenv("HAI_REQUEST_TIMEOUT_SECONDS", "8")) + + db_path.parent.mkdir(parents=True, exist_ok=True) + with sqlite3.connect(db_path) as connection: + ensure_cache_schema(connection) + + runtime_mode = "cached" + payload: dict[str, Any] | None = None + + if source_url: + try: + payload = normalize_payload(fetch_live_payload(source_url, timeout_seconds)) + save_snapshot(connection, "network", payload) + runtime_mode = "network" + except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as error: + print(f"[WARN] Network source unavailable, falling back to cached DB: {error}") + + if payload is None: + payload = load_latest_snapshot(connection) + if payload is None: + print( + "[ERROR] No cached HAI snapshot found and network source is unavailable.", + file=sys.stderr, + ) + return 1 + + print(f"[INFO] HAI agent runtime mode: {runtime_mode}") + print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())