ruslanmv commited on
Commit
e465928
·
1 Parent(s): fed7eb0

First commit

Browse files
.github/workflows/sync-to-hf-space.yml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .github/workflows/sync-to-hf-space.yml
2
+ name: Sync to Hugging Face Space (matrix-ai)
3
+
4
+ on:
5
+ push:
6
+ branches: ["main", "master"]
7
+ workflow_dispatch:
8
+ inputs:
9
+ force:
10
+ description: "Force push HEAD to Space main (use for first-time sync or non-fast-forward)"
11
+ type: boolean
12
+ default: false
13
+ ref:
14
+ description: "Git ref to push (defaults to current HEAD)"
15
+ type: string
16
+ default: ""
17
+
18
+ jobs:
19
+ sync-to-hub:
20
+ runs-on: ubuntu-latest
21
+ concurrency:
22
+ group: hf-space-sync-matrix-ai
23
+ cancel-in-progress: false
24
+
25
+ env:
26
+ # Hard-wired to your Space:
27
+ HF_SPACE_REMOTE: https://ruslanmv:${HF_TOKEN}@huggingface.co/spaces/ruslanmv/matrix-ai
28
+
29
+ steps:
30
+ - name: Checkout
31
+ uses: actions/checkout@v4
32
+ with:
33
+ fetch-depth: 0
34
+ lfs: true
35
+
36
+ - name: Prepare Git
37
+ run: |
38
+ git config user.name "github-actions[bot]"
39
+ git config user.email "github-actions[bot]@users.noreply.github.com"
40
+ git lfs install --local
41
+
42
+ - name: Resolve ref to push
43
+ id: ref
44
+ run: |
45
+ REF="${{ inputs.ref }}"
46
+ if [ -z "$REF" ]; then
47
+ REF="${GITHUB_REF_NAME}"
48
+ fi
49
+ echo "ref=$REF" >> "$GITHUB_OUTPUT"
50
+
51
+ - name: Push to Hugging Face Space (normal)
52
+ if: ${{ inputs.force != true }}
53
+ env:
54
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
55
+ run: |
56
+ if [ -z "${HF_TOKEN}" ]; then
57
+ echo "❌ Missing HF_TOKEN secret. Add it under: Settings → Secrets and variables → Actions → New repository secret."
58
+ exit 1
59
+ fi
60
+ # Always push current HEAD to Space's main (handles master→main mapping)
61
+ git push "${HF_SPACE_REMOTE}" "HEAD:main"
62
+
63
+ - name: Push to Hugging Face Space (force)
64
+ if: ${{ inputs.force == true }}
65
+ env:
66
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
67
+ run: |
68
+ if [ -z "${HF_TOKEN}" ]; then
69
+ echo "❌ Missing HF_TOKEN secret. Add it under: Settings → Secrets and variables → Actions → New repository secret."
70
+ exit 1
71
+ fi
72
+ git push --force "${HF_SPACE_REMOTE}" "HEAD:main"
Dockerfile CHANGED
@@ -1,19 +1,35 @@
 
1
  FROM python:3.11-slim
2
 
3
- WORKDIR /app
4
-
5
  ENV PYTHONDONTWRITEBYTECODE=1 \
6
  PYTHONUNBUFFERED=1 \
7
- PIP_NO_CACHE_DIR=1
 
 
 
 
 
 
 
 
 
8
 
9
- RUN apt-get update && apt-get install -y --no-install-recommends build-essential && \
10
- rm -rf /var/lib/apt/lists/*
 
11
 
12
- COPY requirements.txt /app/requirements.txt
13
- RUN pip install --no-cache-dir -r /app/requirements.txt
14
 
15
- COPY . /app
 
 
 
16
 
17
- EXPOSE 7860
 
 
18
 
19
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
1
+ # syntax=docker/dockerfile:1
2
  FROM python:3.11-slim
3
 
4
+ # --- base env ---
 
5
  ENV PYTHONDONTWRITEBYTECODE=1 \
6
  PYTHONUNBUFFERED=1 \
7
+ PIP_NO_CACHE_DIR=1 \
8
+ UVICORN_WORKERS=2
9
+
10
+ # --- system deps ---
11
+ RUN apt-get update \
12
+ && apt-get install -y --no-install-recommends ca-certificates curl \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # --- app dir ---
16
+ WORKDIR /app
17
 
18
+ # --- python deps layer (better cache) ---
19
+ COPY requirements.txt ./
20
+ RUN pip install --upgrade pip && pip install -r requirements.txt
21
 
22
+ # --- copy app ---
23
+ COPY . .
24
 
25
+ # --- Spaces sets $PORT dynamically; honor it ---
26
+ ARG PORT=7860
27
+ ENV PORT=${PORT}
28
+ EXPOSE ${PORT}
29
 
30
+ # Optional: run as non-root (safer)
31
+ # RUN useradd -ms /bin/bash appuser && chown -R appuser:appuser /app
32
+ # USER appuser
33
 
34
+ # --- start ---
35
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "${PORT}"]
Makefile ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ====================================================================================
2
+ #
3
+ # M A T R I X - A I ::: C O N T R O L P R O G R A M
4
+ # "Know thyself."
5
+ #
6
+ # Access programs with: make help
7
+ #
8
+ # ====================================================================================
9
+
10
+ # System & Colors
11
+ BRIGHT_GREEN := $(shell tput -T screen setaf 10)
12
+ DIM_GREEN := $(shell tput -T screen setaf 2)
13
+ RESET := $(shell tput -T screen sgr0)
14
+
15
+ # Python / Venv
16
+ SYS_PYTHON := python3
17
+ VENV_DIR := .venv
18
+ PYTHON := $(VENV_DIR)/bin/python
19
+ PIP := $(PYTHON) -m pip
20
+
21
+ # App
22
+ APP_MODULE := app.main:app
23
+ PORT := 7860
24
+
25
+ # Docker / HF Spaces
26
+ IMG_NAME := matrix-ai:local
27
+ SPACE_URL ?= https://huggingface.co/spaces/ruslanmv/matrix-ai
28
+
29
+ # Files & Dirs
30
+ REQ := requirements.txt
31
+ TEST_DIR := tests
32
+
33
+ .DEFAULT_GOAL := help
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Help
37
+ # ---------------------------------------------------------------------------
38
+ help:
39
+ @echo
40
+ @echo "$(BRIGHT_GREEN)M A T R I X - A I ::: C O N T R O L P R O G R A M$(RESET)"
41
+ @echo
42
+ @printf "$(BRIGHT_GREEN) %-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "PROGRAM" "DESCRIPTION"
43
+ @printf "$(BRIGHT_GREEN) %-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "----------------------" "--------------------------------------------------------"
44
+ @echo
45
+ @echo "$(BRIGHT_GREEN)Environment$(RESET)"
46
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "venv" "Create virtualenv (.venv)"
47
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "install" "Install deps into venv (incremental)"
48
+ @echo
49
+ @echo "$(BRIGHT_GREEN)Quality$(RESET)"
50
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "lint" "ruff check"
51
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "fmt" "black + ruff fix"
52
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "test" "pytest"
53
+ @echo
54
+ @echo "$(BRIGHT_GREEN)Run$(RESET)"
55
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "run" "Run uvicorn (PORT=$(PORT))"
56
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "run-hot" "Run with --reload"
57
+ @echo
58
+ @echo "$(BRIGHT_GREEN)Docker$(RESET)"
59
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "docker-build" "Build local image ($(IMG_NAME))"
60
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "docker-run" "Run local container (maps $(PORT))"
61
+ @echo
62
+ @echo "$(BRIGHT_GREEN)HF Spaces helpers$(RESET)"
63
+ @printf " $(BRIGHT_GREEN)%-22s$(RESET) $(DIM_GREEN)%s$(RESET)\n" "space-url" "Echo the Space URL (set SPACE_URL=...)"
64
+ @echo
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Env
68
+ # ---------------------------------------------------------------------------
69
+ $(VENV_DIR)/bin/activate:
70
+ @test -d $(VENV_DIR) || $(SYS_PYTHON) -m venv $(VENV_DIR)
71
+
72
+ venv: $(VENV_DIR)/bin/activate
73
+ @echo "$(DIM_GREEN)-> Upgrading pip/setuptools/wheel$(RESET)"
74
+ @$(PIP) install -U pip setuptools wheel >/dev/null
75
+
76
+ install: venv
77
+ @echo "$(DIM_GREEN)-> Installing deps$(RESET)"
78
+ @$(PIP) install -r $(REQ)
79
+ @echo "$(BRIGHT_GREEN)OK$(RESET)"
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Quality
83
+ # ---------------------------------------------------------------------------
84
+ lint: venv
85
+ @$(PYTHON) -m ruff check app tests || true
86
+
87
+ fmt: venv
88
+ @$(PYTHON) -m black app tests || true
89
+ @$(PYTHON) -m ruff check --fix app tests || true
90
+
91
+ test: venv
92
+ @$(PYTHON) -m pytest -q --disable-warnings --maxfail=1 || true
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Run
96
+ # ---------------------------------------------------------------------------
97
+ run: install
98
+ @PORT=$(PORT) $(VENV_DIR)/bin/uvicorn $(APP_MODULE) --host 0.0.0.0 --port $(PORT)
99
+
100
+ run-hot: install
101
+ @PORT=$(PORT) $(VENV_DIR)/bin/uvicorn $(APP_MODULE) --host 0.0.0.0 --port $(PORT) --reload
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Docker
105
+ # ---------------------------------------------------------------------------
106
+ docker-build:
107
+ @docker build -t $(IMG_NAME) .
108
+
109
+ docker-run:
110
+ @docker run --rm -it -p $(PORT):$(PORT) -e PORT=$(PORT) $(IMG_NAME)
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # HF Helpers
114
+ # ---------------------------------------------------------------------------
115
+ space-url:
116
+ @echo "Space: $(SPACE_URL)"
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Clean
120
+ # ---------------------------------------------------------------------------
121
+ clean:
122
+ @rm -rf .venv __pycache__ .pytest_cache .ruff_cache .mypy_cache dist build *.egg-info
123
+
124
+ .PHONY: help venv install lint fmt test run run-hot docker-build docker-run space-url clean
app/main.py CHANGED
@@ -1,18 +1,101 @@
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI
 
 
 
2
  from .middleware import attach_middlewares
 
 
3
  from .routers import health, plan, chat
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  def create_app() -> FastAPI:
6
- """Creates and configures the FastAPI application instance."""
7
  app = FastAPI(
8
  title="matrix-ai",
9
- version="0.1.0",
10
- description="AI service for the Matrix EcoSystem"
 
 
 
 
11
  )
 
 
12
  attach_middlewares(app)
 
 
13
  app.include_router(health.router, tags=["Health"])
14
  app.include_router(plan.router, prefix="/v1", tags=["Planning"])
15
  app.include_router(chat.router, prefix="/v1", tags=["Chat"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  return app
17
 
 
18
  app = create_app()
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import time
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any, Dict
8
+
9
  from fastapi import FastAPI
10
+ from fastapi.responses import JSONResponse, RedirectResponse
11
+
12
+ # Your existing middleware bundle (req id, rate limit, etag, etc.)
13
  from .middleware import attach_middlewares
14
+
15
+ # Core API routers
16
  from .routers import health, plan, chat
17
 
18
+ # Optional UI (Home/Chat/Dev). If missing, we gracefully fall back to a JSON root.
19
+ try:
20
+ from .ui import router as ui_router # type: ignore
21
+ HAS_UI = True
22
+ except Exception: # pragma: no cover
23
+ HAS_UI = False
24
+
25
+
26
+ TAGS_METADATA = [
27
+ {"name": "Health", "description": "Liveness / readiness probes and basic service metadata."},
28
+ {"name": "Planning", "description": "AI plan generation for Matrix Guardian (/v1/plan)."},
29
+ {"name": "Chat", "description": "Lightweight RAG/Q&A about Matrix System (/v1/chat)."},
30
+ {"name": "UI", "description": "Minimal web UI (Home, Chat, Dev) if enabled."},
31
+ ]
32
+
33
+
34
+ @asynccontextmanager
35
+ async def lifespan(app: FastAPI):
36
+ """
37
+ Lightweight startup/shutdown hooks.
38
+ Stores process start time for basic diagnostics and logs boot/shutdown.
39
+ """
40
+ app.state.started_at = time.time()
41
+ app.state.version = os.getenv("APP_VERSION", "1.0.0")
42
+ logging.getLogger("uvicorn.error").info(
43
+ "matrix-ai starting (version=%s)", app.state.version
44
+ )
45
+ try:
46
+ yield
47
+ finally:
48
+ uptime = time.time() - getattr(app.state, "started_at", time.time())
49
+ logging.getLogger("uvicorn.error").info(
50
+ "matrix-ai shutting down (uptime=%.2fs)", uptime
51
+ )
52
+
53
+
54
  def create_app() -> FastAPI:
55
+ """Create and configure the FastAPI application instance."""
56
  app = FastAPI(
57
  title="matrix-ai",
58
+ version=os.getenv("APP_VERSION", "1.0.0"),
59
+ description="AI planning microservice for the Matrix EcoSystem",
60
+ openapi_tags=TAGS_METADATA,
61
+ docs_url="/docs",
62
+ redoc_url=None,
63
+ lifespan=lifespan,
64
  )
65
+
66
+ # Middlewares (request-id, gzip, rate-limit, idempotency headers, etc.)
67
  attach_middlewares(app)
68
+
69
+ # Core routers
70
  app.include_router(health.router, tags=["Health"])
71
  app.include_router(plan.router, prefix="/v1", tags=["Planning"])
72
  app.include_router(chat.router, prefix="/v1", tags=["Chat"])
73
+
74
+ # Optional UI (adds '/', '/chat', '/dev')
75
+ if HAS_UI:
76
+ app.include_router(ui_router, tags=["UI"])
77
+ else:
78
+ # Minimal root so HF Spaces / root health probes pass even without UI
79
+ @app.get("/", include_in_schema=False)
80
+ async def root() -> Dict[str, Any]:
81
+ return {
82
+ "ok": True,
83
+ "service": "matrix-ai",
84
+ "version": app.version,
85
+ "docs": "/docs",
86
+ "endpoints": {
87
+ "plan": "/v1/plan",
88
+ "chat": "/v1/chat",
89
+ "healthz": "/healthz",
90
+ },
91
+ }
92
+
93
+ # Optional convenience redirect to API docs
94
+ @app.get("/home", include_in_schema=False)
95
+ async def home_redirect():
96
+ return RedirectResponse(url="/docs", status_code=302)
97
+
98
  return app
99
 
100
+
101
  app = create_app()
app/templates/base.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>matrix-ai</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root { --bg:#0b1220; --card:#0f172a; --muted:#94a3b8; --text:#e2e8f0; --accent:#38bdf8; }
12
+ body { margin:0; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:var(--bg); color:var(--text); }
13
+ header { padding:20px; display:flex; gap:16px; align-items:center; border-bottom:1px solid #1f2a44; }
14
+ a { color: var(--accent); text-decoration: none; }
15
+ .wrap { max-width: 980px; margin: 0 auto; padding: 24px; }
16
+ .card { background:var(--card); border:1px solid #1f2a44; border-radius: 16px; padding: 20px; }
17
+ input, textarea { width: 100%; background:#0b1220; color:var(--text); border:1px solid #1f2a44; border-radius:12px; padding:12px; font-size:14px; }
18
+ button { background: var(--accent); color:#06202a; border:0; padding:10px 16px; border-radius: 12px; font-weight: 600; cursor:pointer; }
19
+ pre { background:#0b1220; border:1px solid #1f2a44; padding:12px; border-radius:12px; overflow:auto; }
20
+ nav a { margin-right: 16px; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <header>
25
+ <strong>matrix-ai</strong>
26
+ <nav>
27
+ <a href="/">Home</a>
28
+ <a href="/chat">Chat</a>
29
+ <a href="/dev">Dev</a>
30
+ <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
31
+ </nav>
32
+ </header>
33
+ <div class="wrap">
34
+ {% block body %}{% endblock %}
35
+ </div>
36
+ </body>
37
+ </html>
app/templates/chat.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block body %}
3
+ <div class="card">
4
+ <h3>Chat about Matrix System 1.0</h3>
5
+ <form method="post" style="display:grid; gap:12px;">
6
+ <textarea name="question" rows="4" placeholder="Ask anything about the Matrix EcoSystem, Guardian, or Hub...">{{ question or '' }}</textarea>
7
+ <div><button type="submit">Ask</button></div>
8
+ </form>
9
+ {% if answer %}
10
+ <h4>Answer</h4>
11
+ <pre>{{ answer }}</pre>
12
+ {% endif %}
13
+ </div>
14
+ {% endblock %}
app/templates/dev.html ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block body %}
3
+ <div class="card">
4
+ <h3>Dev – Exercise /v1/plan</h3>
5
+ <form method="post" style="display:grid; gap:12px;">
6
+ <textarea name="payload" rows="16" spellcheck="false">{{ sample }}</textarea>
7
+ <div style="display:flex; gap:8px;">
8
+ <button type="submit">Call /v1/plan</button>
9
+ </div>
10
+ </form>
11
+
12
+ {% if error %}
13
+ <h4>Error</h4>
14
+ <pre>{{ error }}</pre>
15
+ {% endif %}
16
+
17
+ {% if result %}
18
+ <h4>Response</h4>
19
+ <pre>{{ result }}</pre>
20
+ {% endif %}
21
+ </div>
22
+ {% endblock %}
app/templates/home.html ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block body %}
3
+ <div class="card">
4
+ <h2>Welcome to <em>matrix-ai</em></h2>
5
+ <p>This service generates short, low-risk remediation plans for Matrix-Guardian and offers a simple chat about Matrix System 1.0.</p>
6
+ <p>Use the navigation to explore the Chat and Dev tabs.</p>
7
+ </div>
8
+ {% endblock %}
app/ui.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, Form
2
+ from fastapi.responses import HTMLResponse
3
+ from fastapi.templating import Jinja2Templates
4
+ import httpx, os, json
5
+
6
+ router = APIRouter()
7
+ templates = Jinja2Templates(directory="app/templates")
8
+
9
+ def _self_base_url() -> str:
10
+ # When running inside HF Space Docker, use localhost + PORT
11
+ port = os.getenv("PORT", "7860")
12
+ return f"http://127.0.0.1:{port}"
13
+
14
+ @router.get("/", response_class=HTMLResponse)
15
+ async def home(request: Request):
16
+ return templates.TemplateResponse("home.html", {"request": request})
17
+
18
+ @router.get("/chat", response_class=HTMLResponse)
19
+ async def chat_get(request: Request):
20
+ return templates.TemplateResponse("chat.html", {"request": request, "answer": None})
21
+
22
+ @router.post("/chat", response_class=HTMLResponse)
23
+ async def chat_post(request: Request, question: str = Form(...)):
24
+ # Call your /v1/chat (or return a placeholder)
25
+ base_url = _self_base_url()
26
+ try:
27
+ async with httpx.AsyncClient(timeout=15.0) as client:
28
+ r = await client.post("/v1/chat", base_url=base_url, json={"query": question})
29
+ data = r.json()
30
+ answer = data.get("answer", "(no answer)")
31
+ except Exception as e:
32
+ answer = f"Error: {e}"
33
+ return templates.TemplateResponse("chat.html", {"request": request, "answer": answer, "question": question})
34
+
35
+ @router.get("/dev", response_class=HTMLResponse)
36
+ async def dev_get(request: Request):
37
+ # Prefill a realistic plan request used by Matrix-Guardian
38
+ sample = {
39
+ "context": {
40
+ "entity_uid": "matrix-ai",
41
+ "health": {"score": 0.64, "status": "degraded", "last_checked": "2025-09-27T00:00:00Z"},
42
+ "recent_checks": [
43
+ {"check": "http", "result": "fail", "latency_ms": 900, "ts": "2025-09-27T00:00:00Z"}
44
+ ],
45
+ },
46
+ "constraints": {"max_steps": 3, "risk": "low"},
47
+ }
48
+ return templates.TemplateResponse("dev.html", {"request": request, "sample": json.dumps(sample, indent=2)})
49
+
50
+ @router.post("/dev", response_class=HTMLResponse)
51
+ async def dev_post(request: Request, payload: str = Form(...)):
52
+ base_url = _self_base_url()
53
+ try:
54
+ body = json.loads(payload)
55
+ except Exception as e:
56
+ return templates.TemplateResponse("dev.html", {"request": request, "sample": payload, "error": f"Invalid JSON: {e}"})
57
+ try:
58
+ async with httpx.AsyncClient(timeout=15.0) as client:
59
+ r = await client.post("/v1/plan", base_url=base_url, json=body)
60
+ r.raise_for_status()
61
+ data = r.json()
62
+ pretty = json.dumps(data, indent=2)
63
+ return templates.TemplateResponse("dev.html", {"request": request, "sample": payload, "result": pretty})
64
+ except Exception as e:
65
+ return templates.TemplateResponse("dev.html", {"request": request, "sample": payload, "error": str(e)})
requirements.txt CHANGED
@@ -11,7 +11,9 @@ numpy==1.26.4
11
  orjson==3.10.3
12
  pyyaml==6.0.1
13
  tenacity==8.2.3
14
- # Dev dependencies
 
 
15
  pytest
16
  ruff
17
  mypy
 
11
  orjson==3.10.3
12
  pyyaml==6.0.1
13
  tenacity==8.2.3
14
+ jinja2==3.1.4
15
+
16
+ # Dev (optional)
17
  pytest
18
  ruff
19
  mypy