Skip to content

Commit 8a057bf

Browse files
authored
Merge pull request #129 from DevanshuNEU/feat/127-session-management
feat(playground): add session management endpoint (#127)
2 parents 03d498a + 5086c70 commit 8a057bf

5 files changed

Lines changed: 1294 additions & 105 deletions

File tree

backend/routes/playground.py

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class PlaygroundSearchRequest(BaseModel):
3636

3737
async 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:
8989
async 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")
110178
async 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

Comments
 (0)