import os import httpx from fastapi import FastAPI, HTTPException, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pydantic import BaseModel from pathlib import Path from dotenv import load_dotenv load_dotenv() app = FastAPI() CODESTRAL_API_KEY = os.getenv("CODESTRAL_API_KEY") CODESTRAL_FIM_ENDPOINT = "https://codestral.mistral.ai/v1/fim/completions" CODESTRAL_CHAT_ENDPOINT = "https://codestral.mistral.ai/v1/chat/completions" # --- Pydantic 모델 (변경 없음) --- class HtmlGenRequest(BaseModel): prompt: str class CodeGenRequest(BaseModel): prompt: str language: str class CodeCompleteRequest(BaseModel): prefix: str suffix: str language: str # --- 경로 설정 (루트 디렉토리 기준) --- # main.py와 index.html, style.css, script.js가 모두 같은 레벨(/app)에 위치 CURRENT_DIR = Path(__file__).resolve().parent # /app # --- API 엔드포인트 (로직 변경 없음, 이전과 동일) --- @app.post("/api/generate-html") async def generate_html_endpoint(payload: HtmlGenRequest): if not CODESTRAL_API_KEY: raise HTTPException(status_code=500, detail="API Key not configured on server. Check Hugging Face Space Secrets.") async with httpx.AsyncClient(timeout=60.0) as client: try: response = await client.post( CODESTRAL_CHAT_ENDPOINT, headers={"Authorization": f"Bearer {CODESTRAL_API_KEY}", "Content-Type": "application/json", "Accept": "application/json"}, json={"model": "codestral-latest", "messages": [{"role": "system", "content": "You are an expert HTML and web designer..."}, {"role": "user", "content": payload.prompt}], "temperature": 0.3} ) response.raise_for_status() data = response.json() html_content = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip() or "" return {"html": html_content} except httpx.HTTPStatusError as e: print(f"Codestral API Error (HTML): {e.response.status_code} - {e.response.text}") raise HTTPException(status_code=e.response.status_code, detail=f"Codestral API error: {e.response.text}") except Exception as e: print(f"Server Error (HTML): {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/generate-code") async def generate_code_endpoint(payload: CodeGenRequest): if not CODESTRAL_API_KEY: raise HTTPException(status_code=500, detail="API Key not configured on server.") async with httpx.AsyncClient(timeout=60.0) as client: try: response = await client.post( CODESTRAL_CHAT_ENDPOINT, headers={"Authorization": f"Bearer {CODESTRAL_API_KEY}", "Content-Type": "application/json", "Accept": "application/json"}, json={"model": "codestral-latest", "messages": [{"role": "system", "content": f"You are an expert {payload.language} programmer..."}, {"role": "user", "content": payload.prompt}], "temperature": 0.2} ) response.raise_for_status() data = response.json() code_content = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip() or f"// AI {payload.language} code generation failed." return {"code": code_content} except httpx.HTTPStatusError as e: print(f"Codestral API Error (Code): {e.response.status_code} - {e.response.text}") raise HTTPException(status_code=e.response.status_code, detail=f"Codestral API error: {e.response.text}") except Exception as e: print(f"Server Error (Code): {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/complete-code") async def complete_code_endpoint(payload: CodeCompleteRequest): if not CODESTRAL_API_KEY: raise HTTPException(status_code=500, detail="API Key not configured on server.") if payload.prefix is None or payload.suffix is None: raise HTTPException(status_code=400, detail="Prefix and suffix are required.") async with httpx.AsyncClient(timeout=30.0) as client: try: api_payload = {"model": "codestral-latest", "prompt": payload.prefix, "suffix": payload.suffix, "temperature": 0.1} response = await client.post( CODESTRAL_FIM_ENDPOINT, headers={"Authorization": f"Bearer {CODESTRAL_API_KEY}", "Content-Type": "application/json", "Accept": "application/json"}, json=api_payload ) response.raise_for_status() data = response.json() completion_text = data.get("choices", [{}])[0].get("text", "") return {"completion": completion_text} except httpx.HTTPStatusError as e: print(f"Codestral API Error (FIM): {e.response.status_code} - {e.response.text}") raise HTTPException(status_code=e.response.status_code, detail=f"Codestral API error: {e.response.text}") except Exception as e: print(f"Server Error (FIM): {e}") raise HTTPException(status_code=500, detail=str(e)) # --- 프론트엔드 정적 파일 및 SPA 라우팅 (루트 디렉토리 기준) --- # style.css와 script.js를 위한 StaticFiles 마운트 # '/static' 경로로 요청오면 현재 디렉토리의 파일들을 서빙 (예: /static/style.css -> ./style.css) # 하지만 HTML에서 처럼 직접 참조하므로, # 아래의 index.html을 서빙하는 라우트와 함께, 개별 파일 서빙 라우트가 필요. # CSS 파일 서빙 @app.get("/style.css") async def serve_css(): return FileResponse(CURRENT_DIR / "style.css", media_type="text/css") # JavaScript 파일 서빙 @app.get("/script.js") async def serve_js(): return FileResponse(CURRENT_DIR / "script.js", media_type="application/javascript") # 루트 경로 ("/") 및 다른 모든 경로에 대해 index.html을 제공 (SPA 동작) # API 경로 (/api/...)는 이것보다 먼저 정의되어야 함 @app.get("/{full_path:path}") async def serve_spa(request: Request, full_path: str): # API 호출이 아닌 경우에만 index.html 반환 if full_path.startswith("api/"): # 이미 위에서 처리됨. 혹시 모를 경우 대비. raise HTTPException(status_code=404, detail="API endpoint not found here.") return FileResponse(CURRENT_DIR / "index.html", media_type="text/html") # 만약 위 방식이 복잡하다면, StaticFiles를 하나로 마운트하고 html=True 사용: # app.mount("/", StaticFiles(directory=CURRENT_DIR, html=True), name="static_root") # 이 경우, index.html, style.css, script.js 모두 CURRENT_DIR에서 직접 서빙됨. # /style.css 요청 시 ./style.css 파일 서빙. # / 요청 시 ./index.html 파일 서빙. # 이 방식이 더 간단하며, 위 개별 라우트는 필요 없을 수 있습니다. # 둘 중 하나를 선택하세요. 여기서는 더 명시적인 개별 라우트 방식을 남겨두었습니다. # StaticFiles(directory=".") 로 현재 디렉토리를 지정해도 됩니다. if __name__ == "__main__": import uvicorn # Hugging Face Spaces에서는 CMD 명령어로 uvicorn이 실행되므로 이 부분은 로컬 테스트용입니다. # 포트 7860은 Hugging Face Spaces의 기본 포트입니다. print(f"로컬 테스트 서버 시작: http://localhost:7860") print(f"index.html은 다음 경로에 있어야 합니다: {CURRENT_DIR / 'index.html'}") uvicorn.run(app, host="0.0.0.0", port=7860)