First commit
Browse files- .github/workflows/sync-to-hf-space.yml +72 -0
- Dockerfile +26 -10
- Makefile +124 -0
- app/main.py +86 -3
- app/templates/base.html +37 -0
- app/templates/chat.html +14 -0
- app/templates/dev.html +22 -0
- app/templates/home.html +8 -0
- app/ui.py +65 -0
- requirements.txt +3 -1
.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 |
-
|
| 4 |
-
|
| 5 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
PYTHONUNBUFFERED=1 \
|
| 7 |
-
PIP_NO_CACHE_DIR=1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
| 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 |
-
"""
|
| 7 |
app = FastAPI(
|
| 8 |
title="matrix-ai",
|
| 9 |
-
version="
|
| 10 |
-
description="AI
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|