|
|
|
|
|
"""sandbox_serper.py β resilient Serper sandbox v2.1 |
|
|
|
|
|
Fixes |
|
|
----- |
|
|
* Moved `global _MAX_OUTBOUND, _SEM` declaration to the **top of `main()`** |
|
|
before any reference, eliminating the `SyntaxError: name used prior to |
|
|
global declaration`. |
|
|
* No functional changes otherwise. |
|
|
""" |
|
|
|
|
|
from __future__ import annotations |
|
|
import argparse, asyncio, logging, os, time, traceback |
|
|
from fastapi import FastAPI |
|
|
from fastapi.concurrency import run_in_threadpool |
|
|
from pydantic import BaseModel |
|
|
import uvicorn |
|
|
import time |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") |
|
|
log = logging.getLogger("sandbox_serper") |
|
|
|
|
|
app = FastAPI() |
|
|
|
|
|
class Req(BaseModel): |
|
|
env: str |
|
|
call: str |
|
|
timeout: int = 60 |
|
|
|
|
|
|
|
|
_MAX_OUTBOUND = int(os.getenv("MAX_OUTBOUND", "10")) |
|
|
_SEM = asyncio.Semaphore(_MAX_OUTBOUND) |
|
|
|
|
|
|
|
|
@app.post("/execute") |
|
|
async def execute(req: Req): |
|
|
|
|
|
async with _SEM: |
|
|
result = await run_in_threadpool(_safe_eval, req.env, |
|
|
req.call, req.timeout) |
|
|
|
|
|
return { |
|
|
"output": "", |
|
|
"result": result, |
|
|
"error": None if not str(result).startswith("[tool-error]") else result, |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def _safe_eval(env: str, call: str, timeout: int): |
|
|
start = time.time(); loc: dict = {} |
|
|
try: |
|
|
exec(env, {}, loc) |
|
|
exec(f"response = {call}", {}, loc) |
|
|
if time.time() - start > timeout: |
|
|
raise TimeoutError(f"wall-clock timeout for call {call}") |
|
|
return loc.get("response", "[tool-error] no response var") |
|
|
except Exception as e: |
|
|
log.error("tool error: %s\n%s", e, traceback.format_exc()) |
|
|
return f"[tool-error] {e}" |
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
global _MAX_OUTBOUND, _SEM |
|
|
|
|
|
ap = argparse.ArgumentParser() |
|
|
ap.add_argument("--port", type=int, default=1211) |
|
|
ap.add_argument("--workers", type=int, default=1) |
|
|
ap.add_argument("--reload", action="store_true") |
|
|
|
|
|
|
|
|
args = ap.parse_args() |
|
|
|
|
|
_SEM = asyncio.Semaphore(_MAX_OUTBOUND) |
|
|
|
|
|
if args.reload and args.workers > 1: |
|
|
raise SystemExit("--reload and --workers>1 are mutually exclusive") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if args.workers > 1: |
|
|
uvicorn.run("sandbox_serper:app", host="0.0.0.0", port=args.port, workers=args.workers) |
|
|
else: |
|
|
uvicorn.run(app, host="0.0.0.0", port=args.port, reload=args.reload) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |