| import os |
| import httpx |
| from pathlib import Path |
| from fastapi import FastAPI, Request, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.responses import JSONResponse |
| from pydantic import BaseModel |
|
|
| app = FastAPI() |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "") |
| SESSIONS_DIR = Path("/tmp/sessions") |
| SESSIONS_DIR.mkdir(parents=True, exist_ok=True) |
|
|
|
|
| def session_dir(session_id: str) -> Path: |
| safe = "".join(c for c in session_id if c.isalnum() or c == "-")[:64] |
| d = SESSIONS_DIR / safe |
| d.mkdir(exist_ok=True) |
| return d |
|
|
|
|
| @app.get("/api/files/{session_id}") |
| async def list_files(session_id: str): |
| d = session_dir(session_id) |
| return {"files": sorted(f.name for f in d.iterdir() if f.is_file())} |
|
|
|
|
| class FileBody(BaseModel): |
| filename: str |
| content: str |
|
|
|
|
| @app.post("/api/files/{session_id}") |
| async def create_file(session_id: str, body: FileBody): |
| d = session_dir(session_id) |
| name = Path(body.filename).name |
| if not name: |
| raise HTTPException(400, "invalid filename") |
| (d / name).write_text(body.content, encoding="utf-8") |
| return {"ok": True, "filename": name, "bytes": len(body.content.encode())} |
|
|
|
|
| @app.get("/api/files/{session_id}/{filename}") |
| async def read_file(session_id: str, filename: str): |
| d = session_dir(session_id) |
| name = Path(filename).name |
| f = d / name |
| if not f.exists(): |
| raise HTTPException(404, f"'{name}' not found") |
| return {"content": f.read_text(encoding="utf-8")} |
|
|
|
|
| @app.delete("/api/files/{session_id}/{filename}") |
| async def delete_file(session_id: str, filename: str): |
| d = session_dir(session_id) |
| name = Path(filename).name |
| f = d / name |
| if not f.exists(): |
| raise HTTPException(404, f"'{name}' not found") |
| f.unlink() |
| return {"ok": True} |
|
|
|
|
| @app.post("/api/chat") |
| async def chat(req: Request): |
| if not ANTHROPIC_API_KEY: |
| return JSONResponse( |
| status_code=500, |
| content={"error": {"type": "config_error", "message": "ANTHROPIC_API_KEY not set on server."}}, |
| ) |
|
|
| payload = await req.json() |
|
|
| async with httpx.AsyncClient(timeout=60) as client: |
| try: |
| r = await client.post( |
| "https://api.anthropic.com/v1/messages", |
| headers={ |
| "x-api-key": ANTHROPIC_API_KEY, |
| "anthropic-version": "2023-06-01", |
| "content-type": "application/json", |
| }, |
| json=payload, |
| ) |
| return r.json() |
| except httpx.TimeoutException: |
| return JSONResponse( |
| status_code=504, |
| content={"error": {"type": "timeout", "message": "Request to AI timed out. Try again."}}, |
| ) |
| except Exception as e: |
| return JSONResponse( |
| status_code=502, |
| content={"error": {"type": "proxy_error", "message": str(e)}}, |
| ) |
|
|
|
|
| @app.get("/api/health") |
| async def health(): |
| return {"status": "ok", "key_set": bool(ANTHROPIC_API_KEY)} |
|
|
|
|
| app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend") |
|
|