Spaces:
Paused
Paused
| import os, json, asyncio, subprocess, uuid | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, BackgroundTasks, UploadFile | |
| from fastapi.responses import FileResponse, StreamingResponse | |
| from pydantic import BaseModel | |
| from typing import List, Optional, Dict, Any | |
| from pathlib import Path | |
| from model_router import route_chat, get_available_models_sync, refresh_model_cache | |
| from agent import CodingAgent | |
| from autocomplete import generate_autocomplete | |
| # Lazy codebase index | |
| CodebaseIndex = None | |
| def get_codebase_index(): | |
| global CodebaseIndex | |
| if CodebaseIndex is None: | |
| try: | |
| from codebase_index import CodebaseIndex as CBI | |
| CodebaseIndex = CBI | |
| except ImportError: | |
| raise HTTPException(status_code=503, detail="Codebase indexing unavailable") | |
| return CodebaseIndex | |
| async def startup_index(): | |
| try: | |
| from codebase_index import CodebaseIndex | |
| idx = CodebaseIndex("/app/sandbox") | |
| idx.build_index() | |
| indexers["/app/sandbox"] = idx | |
| except Exception as e: | |
| print(f"Startup indexing failed: {e}") | |
| async def lifespan(app: FastAPI): | |
| await refresh_model_cache() | |
| await startup_index() | |
| yield | |
| app = FastAPI(title="Codeki API", lifespan=lifespan) | |
| indexers = {} | |
| agents = {} | |
| STATIC_DIR = "static" | |
| class ChatRequest(BaseModel): | |
| model: str = "groq-llama-3.3-70b-versatile" | |
| messages: List[Dict[str, str]] | |
| context_files: Optional[List[str]] = None | |
| project_path: str = "/app/sandbox" | |
| class AgentTaskRequest(BaseModel): | |
| prompt: str | |
| model: str = "groq-llama-3.3-70b-versatile" | |
| project_path: str = "/app/sandbox" | |
| background: bool = False | |
| class AutocompleteRequest(BaseModel): | |
| prefix: str | |
| suffix: str = "" | |
| filepath: str | |
| language: str | |
| max_tokens: int = 64 | |
| model: str = "groq-llama-3.1-8b-instant" | |
| class IndexRequest(BaseModel): | |
| project_path: str | |
| class BugFixRequest(BaseModel): | |
| file_path: str | |
| error_output: str | |
| project_path: str | |
| class ReviewRequest(BaseModel): | |
| pr_diff: str | |
| project_path: str | |
| class ApplyRequest(BaseModel): | |
| task_id: str | |
| approved_indices: List[int] | |
| # ---------- API Routes ---------- | |
| async def get_models(): | |
| return get_available_models_sync() | |
| async def refresh_models(): | |
| await refresh_model_cache() | |
| return {"status": "ok", "count": len(get_available_models_sync())} | |
| async def chat(req: ChatRequest): | |
| context = "" | |
| if req.context_files: | |
| for f in req.context_files: | |
| if f == "__git__": | |
| import git | |
| try: | |
| repo = git.Repo(req.project_path) | |
| diff = repo.git.diff(repo.head.commit.tree) if repo.head.is_valid() else "" | |
| context += f"\n--- Git Diff ---\n{diff}\n" | |
| except: | |
| pass | |
| else: | |
| p = Path(req.project_path) / f | |
| if p.exists(): | |
| context += f"\n--- {f} ---\n{p.read_text()}\n" | |
| req.messages.insert(0, {"role": "system", "content": f"Relevant codebase context:\n{context}"}) | |
| async def event_stream(): | |
| try: | |
| async for token in route_chat(req.model, req.messages): | |
| yield f"data: {json.dumps({'token': token})}\n\n" | |
| yield "data: [DONE]\n\n" | |
| except Exception as e: | |
| yield f"data: {json.dumps({'token': f'ERROR: {str(e)}'})}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse(event_stream(), media_type="text/event-stream") | |
| # ---------- Agent endpoints with error handling ---------- | |
| async def run_agent(req: AgentTaskRequest, background_tasks: BackgroundTasks): | |
| try: | |
| agent = CodingAgent(project_path=req.project_path, model=req.model) | |
| task_id = str(uuid.uuid4()) | |
| agents[task_id] = agent | |
| if req.background: | |
| background_tasks.add_task(agent.run_task, req.prompt) | |
| return {"task_id": task_id, "status": "started"} | |
| else: | |
| result = await agent.run_task(req.prompt) | |
| return { | |
| "task_id": task_id, | |
| "status": agent.status, | |
| "logs": agent.logs, | |
| "pending_changes": result["pending_changes"] | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def approve_agent_changes(task_id: str): | |
| agent = agents.get(task_id) | |
| if not agent: | |
| raise HTTPException(404, "Agent not found") | |
| if agent.status != "awaiting_approval": | |
| raise HTTPException(400, "Agent not awaiting approval") | |
| try: | |
| await agent.approve_and_apply() | |
| return {"status": agent.status, "logs": agent.logs} | |
| except Exception as e: | |
| raise HTTPException(500, detail=str(e)) | |
| async def get_agent_status(task_id: str): | |
| agent = agents.get(task_id) | |
| if not agent: | |
| raise HTTPException(404, "Task not found") | |
| return { | |
| "status": agent.status, | |
| "logs": agent.logs, | |
| "pending_changes": agent.pending_changes | |
| } | |
| async def list_agent_tasks(): | |
| return {"tasks": list(agents.keys())} | |
| # ---------- Other endpoints ---------- | |
| async def autocomplete(req: AutocompleteRequest): | |
| suggestions = await generate_autocomplete(req) | |
| return {"completions": suggestions} | |
| async def index_project(req: IndexRequest): | |
| CBI = get_codebase_index() | |
| idx = CBI(req.project_path) | |
| idx.build_index() | |
| indexers[req.project_path] = idx | |
| return {"status": "indexed", "files": len(idx.file_list)} | |
| async def search_codebase(query: str, project_path: str = "/app/sandbox", top_k: int = 5): | |
| if project_path not in indexers: | |
| CBI = get_codebase_index() | |
| idx = CBI(project_path) | |
| try: | |
| idx.build_index() | |
| except Exception as e: | |
| raise HTTPException(503, f"Indexing failed: {str(e)}") | |
| indexers[project_path] = idx | |
| idx = indexers[project_path] | |
| results = idx.search(query, top_k) | |
| return {"results": results} | |
| async def bugfix(req: BugFixRequest): | |
| agent = CodingAgent(project_path=req.project_path, model="groq-llama-3.3-70b-versatile") | |
| result = await agent.fix_error(req.file_path, req.error_output) | |
| return result | |
| async def review_code(req: ReviewRequest): | |
| agent = CodingAgent(project_path=req.project_path, model="groq-llama-3.3-70b-versatile") | |
| review = await agent.review_pr(req.pr_diff) | |
| return {"review": review} | |
| async def git_operation(action: str, project_path: str, args: Optional[dict] = None): | |
| import git | |
| try: | |
| repo = git.Repo(project_path) | |
| if action == "commit": | |
| repo.git.add(A=True) | |
| repo.git.commit(m=args.get("message", "auto commit")) | |
| elif action == "log": | |
| logs = list(repo.iter_commits(max_count=10)) | |
| return {"log": [{"message": c.message, "hash": c.hexsha} for c in logs]} | |
| elif action == "branch": | |
| branches = [b.name for b in repo.branches] | |
| return {"branches": branches} | |
| elif action == "diff": | |
| diff_output = repo.git.diff(repo.head.commit.tree) if repo.head.is_valid() else "" | |
| return {"diff": diff_output} | |
| else: | |
| raise HTTPException(400, "Unknown action") | |
| except Exception as e: | |
| raise HTTPException(400, str(e)) | |
| async def terminal_ws(websocket: WebSocket): | |
| await websocket.accept() | |
| proc = None | |
| try: | |
| while True: | |
| cmd = await websocket.receive_text() | |
| if cmd.startswith("exec:"): | |
| command = cmd[5:] | |
| proc = await asyncio.create_subprocess_shell( | |
| command, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.PIPE, | |
| cwd="/app/sandbox" | |
| ) | |
| stdout, stderr = await proc.communicate() | |
| output = stdout.decode() + stderr.decode() | |
| await websocket.send_text(output) | |
| elif cmd == "ctrl-c" and proc: | |
| proc.terminate() | |
| except WebSocketDisconnect: | |
| pass | |
| async def list_files(project_path: str = "/app/sandbox"): | |
| def build_tree(path): | |
| tree = [] | |
| for item in sorted(os.listdir(path)): | |
| if item.startswith('.') or item == '__pycache__': | |
| continue | |
| full = os.path.join(path, item) | |
| node = { | |
| "name": item, | |
| "path": full.replace(project_path, "").lstrip("/"), | |
| "type": "folder" if os.path.isdir(full) else "file" | |
| } | |
| if os.path.isdir(full): | |
| node["children"] = build_tree(full) | |
| tree.append(node) | |
| return tree | |
| try: | |
| return build_tree(project_path) | |
| except Exception as e: | |
| raise HTTPException(400, str(e)) | |
| async def get_file(path: str, project_path: str = "/app/sandbox"): | |
| full_path = Path(project_path) / path | |
| if not full_path.exists(): | |
| raise HTTPException(404, "File not found") | |
| return {"content": full_path.read_text()} | |
| async def upload_file(file: UploadFile, project_path: str = "/app/sandbox"): | |
| dest = Path(project_path) / file.filename | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| content = await file.read() | |
| dest.write_bytes(content) | |
| return {"filename": file.filename, "size": len(content)} | |
| # ---------- Serve frontend ---------- | |
| async def serve_frontend(full_path: str): | |
| if full_path.startswith("api"): | |
| raise HTTPException(404) | |
| file_path = os.path.join(STATIC_DIR, full_path) | |
| if os.path.isfile(file_path): | |
| return FileResponse(file_path) | |
| index_path = os.path.join(STATIC_DIR, "index.html") | |
| if os.path.isfile(index_path): | |
| return FileResponse(index_path) | |
| raise HTTPException(404, detail="Not found") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |