|
2 | 2 | CodeIntel Backend API |
3 | 3 | FastAPI backend for codebase intelligence |
4 | 4 | """ |
5 | | -from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends |
| 5 | +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends, Request |
6 | 6 | from fastapi.middleware.cors import CORSMiddleware |
7 | 7 | from pydantic import BaseModel |
8 | 8 | from typing import Optional, List |
@@ -115,6 +115,158 @@ async def health_check(): |
115 | 115 | } |
116 | 116 |
|
117 | 117 |
|
| 118 | +# ============== PLAYGROUND (No Auth Required) ============== |
| 119 | + |
| 120 | +class PlaygroundSearchRequest(BaseModel): |
| 121 | + query: str |
| 122 | + demo_repo: str = "flask" |
| 123 | + max_results: int = 10 |
| 124 | + |
| 125 | +# Map demo repo names to actual repo IDs (will be populated on startup) |
| 126 | +DEMO_REPO_IDS = {} |
| 127 | + |
| 128 | +@app.on_event("startup") |
| 129 | +async def load_demo_repos(): |
| 130 | + """Load pre-indexed demo repos on startup""" |
| 131 | + global DEMO_REPO_IDS |
| 132 | + try: |
| 133 | + repos = repo_manager.list_repos() |
| 134 | + # Map common repo names to their IDs |
| 135 | + for repo in repos: |
| 136 | + name_lower = repo.get("name", "").lower() |
| 137 | + if "flask" in name_lower: |
| 138 | + DEMO_REPO_IDS["flask"] = repo["id"] |
| 139 | + elif "fastapi" in name_lower: |
| 140 | + DEMO_REPO_IDS["fastapi"] = repo["id"] |
| 141 | + elif "express" in name_lower: |
| 142 | + DEMO_REPO_IDS["express"] = repo["id"] |
| 143 | + elif "react" in name_lower: |
| 144 | + DEMO_REPO_IDS["react"] = repo["id"] |
| 145 | + print(f"📦 Loaded demo repos: {list(DEMO_REPO_IDS.keys())}") |
| 146 | + except Exception as e: |
| 147 | + print(f"⚠️ Could not load demo repos: {e}") |
| 148 | + |
| 149 | +# Simple in-memory rate limiting for playground (IP-based) |
| 150 | +from collections import defaultdict |
| 151 | +import time as time_module |
| 152 | + |
| 153 | +playground_rate_limits = defaultdict(list) |
| 154 | +PLAYGROUND_LIMIT = 10 # searches per hour |
| 155 | +PLAYGROUND_WINDOW = 3600 # 1 hour in seconds |
| 156 | + |
| 157 | +def check_playground_rate_limit(ip: str) -> tuple[bool, int]: |
| 158 | + """Check if IP is within rate limit. Returns (allowed, remaining)""" |
| 159 | + now = time_module.time() |
| 160 | + # Clean old entries |
| 161 | + playground_rate_limits[ip] = [t for t in playground_rate_limits[ip] if now - t < PLAYGROUND_WINDOW] |
| 162 | + |
| 163 | + remaining = PLAYGROUND_LIMIT - len(playground_rate_limits[ip]) |
| 164 | + if remaining <= 0: |
| 165 | + return False, 0 |
| 166 | + |
| 167 | + return True, remaining |
| 168 | + |
| 169 | +def record_playground_search(ip: str): |
| 170 | + """Record a playground search for rate limiting""" |
| 171 | + playground_rate_limits[ip].append(time_module.time()) |
| 172 | + |
| 173 | + |
| 174 | +@app.post("/api/playground/search") |
| 175 | +async def playground_search(request: PlaygroundSearchRequest, req: Request): |
| 176 | + """ |
| 177 | + Public playground search - no auth required, rate limited by IP. |
| 178 | + Only works with pre-indexed demo repositories. |
| 179 | + """ |
| 180 | + # Get client IP |
| 181 | + client_ip = req.client.host if req.client else "unknown" |
| 182 | + forwarded = req.headers.get("x-forwarded-for") |
| 183 | + if forwarded: |
| 184 | + client_ip = forwarded.split(",")[0].strip() |
| 185 | + |
| 186 | + # Check rate limit |
| 187 | + allowed, remaining = check_playground_rate_limit(client_ip) |
| 188 | + if not allowed: |
| 189 | + raise HTTPException( |
| 190 | + status_code=429, |
| 191 | + detail="Rate limit exceeded. Sign up for unlimited searches!" |
| 192 | + ) |
| 193 | + |
| 194 | + # Validate query |
| 195 | + valid_query, query_error = InputValidator.validate_search_query(request.query) |
| 196 | + if not valid_query: |
| 197 | + raise HTTPException(status_code=400, detail=f"Invalid query: {query_error}") |
| 198 | + |
| 199 | + # Get demo repo ID |
| 200 | + repo_id = DEMO_REPO_IDS.get(request.demo_repo) |
| 201 | + if not repo_id: |
| 202 | + # Fallback: try to find any indexed repo |
| 203 | + repos = repo_manager.list_repos() |
| 204 | + indexed_repos = [r for r in repos if r.get("status") == "indexed"] |
| 205 | + if indexed_repos: |
| 206 | + repo_id = indexed_repos[0]["id"] |
| 207 | + else: |
| 208 | + raise HTTPException( |
| 209 | + status_code=404, |
| 210 | + detail=f"Demo repo '{request.demo_repo}' not available. Available: {list(DEMO_REPO_IDS.keys())}" |
| 211 | + ) |
| 212 | + |
| 213 | + import time |
| 214 | + start_time = time.time() |
| 215 | + |
| 216 | + try: |
| 217 | + # Sanitize query |
| 218 | + sanitized_query = InputValidator.sanitize_string(request.query, max_length=200) |
| 219 | + |
| 220 | + # Check cache first |
| 221 | + cache_key = f"playground:{request.demo_repo}:{sanitized_query}" |
| 222 | + cached_results = cache.get_search_results(sanitized_query, repo_id) |
| 223 | + if cached_results: |
| 224 | + return { |
| 225 | + "results": cached_results, |
| 226 | + "count": len(cached_results), |
| 227 | + "cached": True, |
| 228 | + "remaining_searches": remaining |
| 229 | + } |
| 230 | + |
| 231 | + # Do search |
| 232 | + results = await indexer.semantic_search( |
| 233 | + query=sanitized_query, |
| 234 | + repo_id=repo_id, |
| 235 | + max_results=min(request.max_results, 10), # Cap at 10 for playground |
| 236 | + use_query_expansion=True, |
| 237 | + use_reranking=True |
| 238 | + ) |
| 239 | + |
| 240 | + # Cache results |
| 241 | + cache.set_search_results(sanitized_query, repo_id, results, ttl=3600) |
| 242 | + |
| 243 | + # Record for rate limiting |
| 244 | + record_playground_search(client_ip) |
| 245 | + |
| 246 | + return { |
| 247 | + "results": results, |
| 248 | + "count": len(results), |
| 249 | + "cached": False, |
| 250 | + "remaining_searches": remaining - 1 |
| 251 | + } |
| 252 | + except Exception as e: |
| 253 | + raise HTTPException(status_code=500, detail=str(e)) |
| 254 | + |
| 255 | + |
| 256 | +@app.get("/api/playground/repos") |
| 257 | +async def list_playground_repos(): |
| 258 | + """List available demo repositories for playground""" |
| 259 | + return { |
| 260 | + "repos": [ |
| 261 | + {"id": "flask", "name": "Flask", "description": "Python web framework", "available": "flask" in DEMO_REPO_IDS}, |
| 262 | + {"id": "fastapi", "name": "FastAPI", "description": "Modern Python API", "available": "fastapi" in DEMO_REPO_IDS}, |
| 263 | + {"id": "express", "name": "Express", "description": "Node.js framework", "available": "express" in DEMO_REPO_IDS}, |
| 264 | + ] |
| 265 | + } |
| 266 | + |
| 267 | + |
| 268 | +# ============== AUTHENTICATED ENDPOINTS ============== |
| 269 | + |
118 | 270 | @app.get("/api/repos") |
119 | 271 | async def list_repositories(auth: AuthContext = Depends(require_auth)): |
120 | 272 | """List all repositories for authenticated user""" |
@@ -357,7 +509,9 @@ async def search_code( |
357 | 509 | results = await indexer.semantic_search( |
358 | 510 | query=sanitized_query, |
359 | 511 | repo_id=request.repo_id, |
360 | | - max_results=min(request.max_results, 50) # Cap at 50 results |
| 512 | + max_results=min(request.max_results, 50), # Cap at 50 results |
| 513 | + use_query_expansion=True, |
| 514 | + use_reranking=True |
361 | 515 | ) |
362 | 516 |
|
363 | 517 | # Cache results |
|
0 commit comments