Ali2206's picture
Add no-trailing-slash route to AIAccessibility
79eabc4
from fastapi import FastAPI, APIRouter, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field, conint
from typing import List, Optional
import os
import sys
import json
from dotenv import load_dotenv, find_dotenv
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if ROOT_DIR not in sys.path:
sys.path.append(ROOT_DIR)
load_dotenv(find_dotenv(), override=True)
class CheckRequest(BaseModel):
ui_text: str = Field(..., description="Paste chatbot response or UI text to review")
primary_color: Optional[str] = Field(None, description="Hex color like #111827 for contrast checks")
background_color: Optional[str] = Field(None, description="Hex background color like #ffffff")
class Finding(BaseModel):
category: str
issue: str
fix: str
class CheckResponse(BaseModel):
score: conint(ge=0, le=100)
summary: str
findings: List[Finding]
quick_fixes: List[str]
from openai import OpenAI
SYSTEM = (
"You are an Accessibility Checker for conversational AI UIs. Evaluate pasted copy for readability, clarity, bias, and inclusivity; "
"if colors are provided, include contrast guidance (WCAG AA). Provide pragmatic, specific fixes. Keep answers concise and actionable."
)
class AccessibilityService:
def __init__(self) -> None:
key = (os.getenv("OPENAI_API_KEY") or "").strip().strip('"').strip("'")
if not key:
raise RuntimeError("OPENAI_API_KEY is not set")
base_url = (os.getenv("OPENAI_BASE_URL") or "").strip().strip('"').strip("'") or None
kwargs = {"api_key": key}
if base_url:
kwargs["base_url"] = base_url
self.client = OpenAI(**kwargs)
self.model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
def check(self, req: CheckRequest) -> CheckResponse:
text_header = (f"Primary: {req.primary_color}\nBackground: {req.background_color}\n" if req.primary_color or req.background_color else "")
text_body = ("UI Text:\n" + req.ui_text.strip()) if req.ui_text else ""
user_parts: List[dict] = []
if text_header or text_body:
user_parts.append({"type": "text", "text": (text_header + text_body).strip()})
resp = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user_parts if user_parts else (text_header + text_body)},
],
temperature=0.2,
response_format={
"type": "json_schema",
"json_schema": {
"name": "CheckResponse",
"schema": {
"type": "object",
"additionalProperties": False,
"required": ["score", "summary", "findings", "quick_fixes"],
"properties": {
"score": {"type": "integer", "minimum": 0, "maximum": 100},
"summary": {"type": "string"},
"findings": {
"type": "array",
"items": {"type": "object", "required": ["category", "issue", "fix"], "properties": {
"category": {"type": "string"},
"issue": {"type": "string"},
"fix": {"type": "string"}
}}
},
"quick_fixes": {"type": "array", "items": {"type": "string"}}
}
}
}
},
)
content = resp.choices[0].message.content
try:
data = json.loads(content)
except Exception:
start = content.find('{'); end = content.rfind('}')
if start != -1 and end != -1 and end > start:
data = json.loads(content[start:end+1])
else:
raise RuntimeError("Invalid JSON from model")
return CheckResponse.model_validate(data)
app = FastAPI(title="Accessibility Checker", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
router = APIRouter(prefix="/access", tags=["access"])
@router.get("/")
def index() -> FileResponse:
"""Serve the Accessibility Checker page."""
base = os.path.dirname(__file__)
return FileResponse(os.path.join(base, "static", "index.html"))
@router.get("")
def index_no_slash() -> FileResponse:
"""Serve the Accessibility Checker page (no trailing slash)."""
base = os.path.dirname(__file__)
return FileResponse(os.path.join(base, "static", "index.html"))
@router.get("/health")
def health() -> dict:
return {"status":"ok","openai_key": bool(os.getenv("OPENAI_API_KEY")), "model": os.getenv("OPENAI_MODEL","gpt-4o-mini")}
@router.post("/check", response_model=CheckResponse)
def check(payload: CheckRequest) -> CheckResponse:
try:
return AccessibilityService().check(payload)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Accessibility error: {exc}")
app.include_router(router)
@app.get("/", response_class=FileResponse)
def index() -> FileResponse:
base = os.path.dirname(__file__)
return FileResponse(os.path.join(base, "static", "index.html"))