Spaces:
Sleeping
Sleeping
| """Server entrypoint for the search environment. | |
| This provides a stateful HTTP server that maintains environment state | |
| between requests. For production use with multiple concurrent users, | |
| consider using WebSocket connections or separate environment instances per session. | |
| """ | |
| import argparse | |
| from contextlib import asynccontextmanager | |
| from typing import Any | |
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel | |
| from searcharena import ( | |
| SearchAction, | |
| SearchEnvConfig, | |
| SearchEnvironment, | |
| SearchObservation, | |
| create_sample_corpus, | |
| create_sample_tasks, | |
| ) | |
| # Request/Response models | |
| class ResetRequest(BaseModel): | |
| seed: int | None = None | |
| task_id: str | None = None | |
| class ResetResponse(BaseModel): | |
| observation: dict[str, Any] | |
| reward: float | |
| done: bool | |
| class StepRequest(BaseModel): | |
| action: SearchAction | |
| class StepResponse(BaseModel): | |
| observation: dict[str, Any] | |
| reward: float | |
| done: bool | |
| class HealthResponse(BaseModel): | |
| status: str | |
| # Global environment instance | |
| _env: SearchEnvironment | None = None | |
| def create_environment() -> SearchEnvironment: | |
| """Create one environment instance.""" | |
| config = SearchEnvConfig() | |
| return SearchEnvironment( | |
| config=config, | |
| corpus=create_sample_corpus(config), | |
| tasks=create_sample_tasks(), | |
| ) | |
| async def lifespan(app: FastAPI): | |
| """Initialize environment on startup.""" | |
| global _env | |
| _env = create_environment() | |
| yield | |
| if _env: | |
| _env.close() | |
| app = FastAPI( | |
| title="Search RL Environment", | |
| description="A stateful search environment for RL training", | |
| lifespan=lifespan, | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| ) | |
| async def root() -> dict[str, Any]: | |
| """Welcome page with API info.""" | |
| return { | |
| "name": "Search RL Environment", | |
| "description": "A stateful search environment for RL training", | |
| "endpoints": { | |
| "GET /": "This welcome page", | |
| "GET /docs": "Interactive API documentation (Swagger UI)", | |
| "GET /health": "Health check", | |
| "GET /tasks": "List available tasks", | |
| "GET /state": "Get current environment state", | |
| "GET /schema": "Get action/observation schemas", | |
| "POST /reset": "Reset environment and get first observation", | |
| "POST /step": "Execute an action (search/read/prune/answer)", | |
| }, | |
| "usage": { | |
| "1_reset": "POST /reset to get a task", | |
| "2_search": "POST /step with action_type='search' to find documents", | |
| "3_read": "POST /step with action_type='read' to read chunks", | |
| "4_answer": "POST /step with action_type='answer' to submit answer", | |
| }, | |
| } | |
| async def health() -> HealthResponse: | |
| """Health check endpoint.""" | |
| return HealthResponse(status="healthy") | |
| async def schema() -> dict[str, Any]: | |
| """Get action and observation schemas.""" | |
| return { | |
| "action": SearchAction.model_json_schema(), | |
| "observation": SearchObservation.model_json_schema(), | |
| } | |
| async def state() -> dict[str, Any]: | |
| """Get current environment state.""" | |
| if _env is None: | |
| raise HTTPException(status_code=500, detail="Environment not initialized") | |
| return _env.state.model_dump() | |
| async def reset(request: ResetRequest | None = None) -> ResetResponse: | |
| """Reset the environment and get the first observation.""" | |
| global _env | |
| if _env is None: | |
| raise HTTPException(status_code=500, detail="Environment not initialized") | |
| # Find task by ID if specified | |
| task = None | |
| if request and request.task_id: | |
| for t in _env.tasks: | |
| if t.task_id == request.task_id: | |
| task = t | |
| break | |
| if task is None: | |
| raise HTTPException(status_code=404, detail=f"Task not found: {request.task_id}") | |
| obs = _env.reset(task=task) | |
| return ResetResponse( | |
| observation=obs.model_dump(), | |
| reward=obs.reward if obs.reward is not None else 0.0, | |
| done=obs.done, | |
| ) | |
| async def step(request: StepRequest) -> StepResponse: | |
| """Execute an action and get the resulting observation.""" | |
| global _env | |
| if _env is None: | |
| raise HTTPException(status_code=500, detail="Environment not initialized") | |
| obs = _env.step(request.action) | |
| return StepResponse( | |
| observation=obs.model_dump(), | |
| reward=obs.reward if obs.reward is not None else 0.0, | |
| done=obs.done, | |
| ) | |
| async def metadata() -> dict[str, Any]: | |
| """Get environment metadata.""" | |
| if _env is None: | |
| raise HTTPException(status_code=500, detail="Environment not initialized") | |
| return _env.get_metadata().model_dump() | |
| async def list_tasks() -> dict[str, Any]: | |
| """List available tasks.""" | |
| if _env is None: | |
| raise HTTPException(status_code=500, detail="Environment not initialized") | |
| return { | |
| "tasks": [ | |
| { | |
| "task_id": t.task_id, | |
| "level": t.level, | |
| "domain": t.domain, | |
| "question": t.question, | |
| } | |
| for t in _env.tasks | |
| ] | |
| } | |
| def main() -> None: | |
| """Run the server directly.""" | |
| import uvicorn | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--host", default="0.0.0.0") | |
| parser.add_argument("--port", type=int, default=8000) | |
| args = parser.parse_args() | |
| uvicorn.run(app, host=args.host, port=args.port) | |
| if __name__ == "__main__": | |
| main() | |