@@ -36,7 +36,7 @@ class PlaygroundSearchRequest(BaseModel):
3636
3737async def load_demo_repos ():
3838 """Load pre-indexed demo repos. Called from main.py on startup."""
39- global DEMO_REPO_IDS
39+ # Note: We mutate DEMO_REPO_IDS dict, no need for 'global' statement
4040 try :
4141 repos = repo_manager .list_repos ()
4242 for repo in repos :
@@ -89,15 +89,15 @@ def _get_limiter() -> PlaygroundLimiter:
8989async def get_playground_limits (req : Request ):
9090 """
9191 Get current rate limit status for this user.
92-
92+
9393 Frontend should call this on page load to show accurate remaining count.
9494 """
9595 session_token = _get_session_token (req )
9696 client_ip = _get_client_ip (req )
97-
97+
9898 limiter = _get_limiter ()
9999 result = limiter .check_limit (session_token , client_ip )
100-
100+
101101 return {
102102 "remaining" : result .remaining ,
103103 "limit" : result .limit ,
@@ -106,24 +106,92 @@ async def get_playground_limits(req: Request):
106106 }
107107
108108
109+ @router .get ("/session" )
110+ async def get_session_info (req : Request , response : Response ):
111+ """
112+ Get current session state including indexed repo info.
113+
114+ Returns complete session data for frontend state management.
115+ Creates a new session if none exists.
116+
117+ Response schema (see issue #127):
118+ {
119+ "session_id": "pg_abc123...",
120+ "created_at": "2025-12-24T10:00:00Z",
121+ "expires_at": "2025-12-25T10:00:00Z",
122+ "indexed_repo": {
123+ "repo_id": "repo_abc123",
124+ "github_url": "https://github.com/user/repo",
125+ "name": "repo",
126+ "indexed_at": "2025-12-24T10:05:00Z",
127+ "expires_at": "2025-12-25T10:05:00Z",
128+ "file_count": 198
129+ },
130+ "searches": {
131+ "used": 12,
132+ "limit": 50,
133+ "remaining": 38
134+ }
135+ }
136+ """
137+ session_token = _get_session_token (req )
138+ limiter = _get_limiter ()
139+
140+ # Check if Redis is available
141+ if not redis_client :
142+ logger .error ("Redis unavailable for session endpoint" )
143+ raise HTTPException (
144+ status_code = 503 ,
145+ detail = {
146+ "message" : "Service temporarily unavailable" ,
147+ "retry_after" : 30 ,
148+ }
149+ )
150+
151+ # Get existing session data
152+ session_data = limiter .get_session_data (session_token )
153+
154+ # If no session exists, create one
155+ if session_data .session_id is None :
156+ new_token = limiter ._generate_session_token ()
157+
158+ if limiter .create_session (new_token ):
159+ _set_session_cookie (response , new_token )
160+ session_data = limiter .get_session_data (new_token )
161+ logger .info ("Created new session via /session endpoint" ,
162+ session_token = new_token [:8 ])
163+ else :
164+ # Failed to create session (Redis issue)
165+ raise HTTPException (
166+ status_code = 503 ,
167+ detail = {
168+ "message" : "Failed to create session" ,
169+ "retry_after" : 30 ,
170+ }
171+ )
172+
173+ # Return formatted response
174+ return session_data .to_response (limit = limiter .SESSION_LIMIT_PER_DAY )
175+
176+
109177@router .post ("/search" )
110178async def playground_search (
111- request : PlaygroundSearchRequest ,
179+ request : PlaygroundSearchRequest ,
112180 req : Request ,
113181 response : Response
114182):
115183 """
116184 Public playground search - rate limited by session/IP.
117-
185+
118186 Sets httpOnly cookie on first request to track device.
119187 """
120188 session_token = _get_session_token (req )
121189 client_ip = _get_client_ip (req )
122-
190+
123191 # Rate limit check AND record
124192 limiter = _get_limiter ()
125193 limit_result = limiter .check_and_record (session_token , client_ip )
126-
194+
127195 if not limit_result .allowed :
128196 raise HTTPException (
129197 status_code = 429 ,
@@ -134,16 +202,16 @@ async def playground_search(
134202 "resets_at" : limit_result .resets_at .isoformat (),
135203 }
136204 )
137-
205+
138206 # Set session cookie if new token was created
139207 if limit_result .session_token :
140208 _set_session_cookie (response , limit_result .session_token )
141-
209+
142210 # Validate query
143211 valid_query , query_error = InputValidator .validate_search_query (request .query )
144212 if not valid_query :
145213 raise HTTPException (status_code = 400 , detail = f"Invalid query: { query_error } " )
146-
214+
147215 # Get demo repo ID
148216 repo_id = DEMO_REPO_IDS .get (request .demo_repo )
149217 if not repo_id :
@@ -156,12 +224,12 @@ async def playground_search(
156224 status_code = 404 ,
157225 detail = f"Demo repo '{ request .demo_repo } ' not available"
158226 )
159-
227+
160228 start_time = time .time ()
161-
229+
162230 try :
163231 sanitized_query = InputValidator .sanitize_string (request .query , max_length = 200 )
164-
232+
165233 # Check cache
166234 cached_results = cache .get_search_results (sanitized_query , repo_id )
167235 if cached_results :
@@ -172,7 +240,7 @@ async def playground_search(
172240 "remaining_searches" : limit_result .remaining ,
173241 "limit" : limit_result .limit ,
174242 }
175-
243+
176244 # Search
177245 results = await indexer .semantic_search (
178246 query = sanitized_query ,
@@ -181,12 +249,12 @@ async def playground_search(
181249 use_query_expansion = True ,
182250 use_reranking = True
183251 )
184-
252+
185253 # Cache results
186254 cache .set_search_results (sanitized_query , repo_id , results , ttl = 3600 )
187-
255+
188256 search_time = int ((time .time () - start_time ) * 1000 )
189-
257+
190258 return {
191259 "results" : results ,
192260 "count" : len (results ),
@@ -207,9 +275,24 @@ async def list_playground_repos():
207275 """List available demo repositories."""
208276 return {
209277 "repos" : [
210- {"id" : "flask" , "name" : "Flask" , "description" : "Python web framework" , "available" : "flask" in DEMO_REPO_IDS },
211- {"id" : "fastapi" , "name" : "FastAPI" , "description" : "Modern Python API" , "available" : "fastapi" in DEMO_REPO_IDS },
212- {"id" : "express" , "name" : "Express" , "description" : "Node.js framework" , "available" : "express" in DEMO_REPO_IDS },
278+ {
279+ "id" : "flask" ,
280+ "name" : "Flask" ,
281+ "description" : "Python web framework" ,
282+ "available" : "flask" in DEMO_REPO_IDS
283+ },
284+ {
285+ "id" : "fastapi" ,
286+ "name" : "FastAPI" ,
287+ "description" : "Modern Python API" ,
288+ "available" : "fastapi" in DEMO_REPO_IDS
289+ },
290+ {
291+ "id" : "express" ,
292+ "name" : "Express" ,
293+ "description" : "Node.js framework" ,
294+ "available" : "express" in DEMO_REPO_IDS
295+ },
213296 ]
214297 }
215298
0 commit comments