from fastapi import FastAPI, HTTPException, Request, Response from pydantic import BaseModel import subprocess import json import os import sys app = FastAPI(title="Firm Workspace Proxy") WORKSPACE_DIR = os.environ.get("FIRM_WORKSPACE", "/app/workspace") # Define request/response models class QueryRequest(BaseModel): query: str class EntityRequest(BaseModel): type: str id: str @app.get("/health") def health(): return {"status": "ok"} @app.get("/api-docs") def api_docs(): return { "endpoints": [ { "path": "/health", "method": "GET", "purpose": "Returns HTTP 200 when the app is ready" }, { "path": "/api-docs", "method": "GET", "purpose": "Documents all available API endpoints" }, { "path": "/query", "method": "POST", "purpose": "Run a Firm query string", "request": {"query": "from task | where is_completed == false"}, "response": {"result": "[...JSON array...]"} }, { "path": "/get", "method": "POST", "purpose": "Get a specific entity by type and ID", "request": {"type": "person", "id": "john_doe"}, "response": {"result": "{...JSON object...}"} }, { "path": "/list/{type}", "method": "GET", "purpose": "List all entity IDs for a given type", "request": {}, "response": {"result": "[...JSON array...]"} } ] } @app.post("/query") async def run_query(req: QueryRequest): try: # We run the firm cli directly instead of bridging stdio JSON-RPC # which is much simpler and error-proof for a REST proxy. proc = subprocess.run( ["firm", "query", req.query, "--workspace", WORKSPACE_DIR, "--format", "json"], capture_output=True, text=True ) if proc.returncode != 0: raise HTTPException(status_code=400, detail=proc.stderr) try: return {"result": json.loads(proc.stdout)} except json.JSONDecodeError: return {"result": proc.stdout} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/get") async def run_get(req: EntityRequest): try: proc = subprocess.run( ["firm", "get", req.type, req.id, "--workspace", WORKSPACE_DIR, "--format", "json"], capture_output=True, text=True ) if proc.returncode != 0: raise HTTPException(status_code=400, detail=proc.stderr) try: return {"result": json.loads(proc.stdout)} except json.JSONDecodeError: return {"result": proc.stdout} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/list/{type}") async def run_list(type: str): try: proc = subprocess.run( ["firm", "list", type, "--workspace", WORKSPACE_DIR, "--format", "json"], capture_output=True, text=True ) if proc.returncode != 0: raise HTTPException(status_code=400, detail=proc.stderr) try: return {"result": json.loads(proc.stdout)} except json.JSONDecodeError: return {"result": proc.stdout} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)