aatmk-panse
feat: environment redesign — real CSV data, shaped rewards, difficulty tiers
329e3d3
"""
FastAPI application for the Shopify Store Audit & Remediation Environment.
Endpoints:
POST /reset - Reset the environment (accepts task selection)
POST /step - Execute an action
GET /state - Get current environment state
GET /schema - Get action/observation schemas
WS /ws - WebSocket for persistent sessions
"""
try:
from openenv.core.env_server.http_server import create_app
except Exception as e:
raise ImportError(
"openenv is required. Install with: pip install openenv-core[core]"
) from e
try:
from ..models import ShopifyStoreAuditAction, ShopifyStoreAuditObservation
from .shopify_store_audit_environment import ShopifyStoreAuditEnvironment
except ImportError:
from models import ShopifyStoreAuditAction, ShopifyStoreAuditObservation
from server.shopify_store_audit_environment import ShopifyStoreAuditEnvironment
from fastapi.responses import HTMLResponse, JSONResponse
try:
from .tasks import ALL_TASKS
except ImportError:
from server.tasks import ALL_TASKS
try:
from .graders import grade_product_listing_qa, grade_seo_collection_optimization, grade_full_store_audit
except ImportError:
from server.graders import grade_product_listing_qa, grade_seo_collection_optimization, grade_full_store_audit
GRADER_MAP = {
"product_listing_qa": grade_product_listing_qa,
"seo_collection_optimization": grade_seo_collection_optimization,
"full_store_audit": grade_full_store_audit,
}
app = create_app(
ShopifyStoreAuditEnvironment,
ShopifyStoreAuditAction,
ShopifyStoreAuditObservation,
env_name="shopify_store_audit",
max_concurrent_envs=2,
)
LANDING_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Shopify Store Audit - OpenEnv</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
background:#0a0a0a;color:#e5e5e5;min-height:100vh;display:flex;align-items:center;justify-content:center}
.container{max-width:720px;padding:48px 32px;text-align:center}
h1{font-size:2.2rem;margin-bottom:8px;background:linear-gradient(135deg,#34d399,#3b82f6);
-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.subtitle{color:#a3a3a3;font-size:1.05rem;margin-bottom:36px}
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:36px}
.card{background:#171717;border:1px solid #262626;border-radius:12px;padding:20px 16px;text-align:left}
.card h3{font-size:.95rem;margin-bottom:4px;color:#f5f5f5}
.card .tag{display:inline-block;font-size:.7rem;padding:2px 8px;border-radius:99px;
margin-bottom:10px;font-weight:600;text-transform:uppercase}
.easy .tag{background:#064e3b;color:#6ee7b7}.med .tag{background:#78350f;color:#fcd34d}
.hard .tag{background:#7f1d1d;color:#fca5a5}
.card p{font-size:.82rem;color:#a3a3a3;line-height:1.45}
.endpoints{background:#171717;border:1px solid #262626;border-radius:12px;
padding:24px;text-align:left;margin-bottom:28px}
.endpoints h2{font-size:1rem;margin-bottom:14px;color:#d4d4d4}
.ep{display:flex;align-items:center;gap:10px;margin-bottom:8px;font-size:.85rem}
.method{font-weight:700;font-size:.75rem;padding:2px 8px;border-radius:4px;min-width:48px;text-align:center}
.get{background:#1e3a5f;color:#93c5fd}.post{background:#3b1f0b;color:#fdba74}
.ws{background:#312e81;color:#c4b5fd}
code{color:#a78bfa;font-size:.82rem}
.try-it{margin-top:24px;font-size:.82rem;color:#737373}
.try-it code{background:#262626;padding:3px 8px;border-radius:4px;font-size:.78rem}
.footer{color:#525252;font-size:.78rem;margin-top:8px}
.footer a{color:#60a5fa;text-decoration:none}
</style>
</head>
<body>
<div class="container">
<h1>🛒 Shopify Store Audit</h1>
<p class="subtitle">RL environment with real Shopify product data &mdash; 45 products, 184 discoverable issues, shaped rewards</p>
<div class="cards">
<div class="card easy"><span class="tag">Easy · Full hints</span>
<h3>Product Listing QA</h3>
<p>8 issues · 25 steps<br>Issues listed with suggested commands. Agent fills in params.</p></div>
<div class="card med"><span class="tag">Medium · Descriptions only</span>
<h3>SEO &amp; Collections</h3>
<p>12 issues · 35 steps<br>Issue descriptions shown. Agent picks commands &amp; params.</p></div>
<div class="card hard"><span class="tag">Hard · Explore</span>
<h3>Full Store Audit</h3>
<p>20 issues · 50 steps<br>Only category counts. Agent must discover issues itself.</p></div>
</div>
<div class="endpoints">
<h2>API Endpoints</h2>
<div class="ep"><span class="method get">GET</span><code>/health</code> &mdash; health check</div>
<div class="ep"><span class="method get">GET</span><code>/tasks</code> &mdash; enumerate tasks &amp; graders</div>
<div class="ep"><span class="method post">POST</span><code>/reset</code> &mdash; reset (body: <code>{}</code>)</div>
<div class="ep"><span class="method post">POST</span><code>/step</code> &mdash; action (body: <code>{"action":{"command":"...","params":{}}}</code>)</div>
<div class="ep"><span class="method get">GET</span><code>/state</code> &mdash; current state</div>
<div class="ep"><span class="method get">GET</span><code>/schema</code> &mdash; action/observation schemas</div>
<div class="ep"><span class="method post">POST</span><code>/grade</code> &mdash; run grader on sample</div>
<div class="ep"><span class="method ws">WS</span><code>/ws</code> &mdash; persistent WebSocket session</div>
</div>
<p class="footer">Real CSV data &middot; 18 Shopify GraphQL commands &middot; Shaped rewards &middot; Regression penalties &middot; Randomised episodes<br>
<a href="https://huggingface.co/spaces/devaatmik/shopify-store-audit/blob/main/README.md">Full docs</a> &middot;
Built for <a href="https://huggingface.co/blog/openenv">OpenEnv</a></p>
</div>
</body>
</html>"""
@app.get("/tasks")
async def list_tasks():
"""Enumerate all tasks with their grader info. Used by competition validators."""
tasks = []
for task_id, cfg in ALL_TASKS.items():
tasks.append({
"id": cfg.task_id,
"title": cfg.title,
"description": cfg.description,
"difficulty": cfg.difficulty,
"max_steps": cfg.max_steps,
"num_issues": cfg.num_issues,
"hint_level": cfg.hint_level,
"has_grader": task_id in GRADER_MAP,
"grader": f"server.graders.grade_{task_id}",
})
return {"tasks": tasks, "count": len(tasks)}
@app.post("/grade")
async def grade_task(body: dict):
"""Run a grader on the provided observation/sample. Used by competition validators."""
task_id = body.get("task_id", body.get("task", ""))
sample = body.get("sample", body.get("observation", body))
grader = GRADER_MAP.get(task_id)
if grader is None:
return JSONResponse(
status_code=400,
content={"error": f"Unknown task '{task_id}'. Available: {list(GRADER_MAP.keys())}"},
)
try:
score = grader(sample)
return {"task_id": task_id, "score": score, "valid": 0.0 <= score <= 1.0}
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
@app.get("/", response_class=HTMLResponse)
async def root():
return LANDING_HTML
def main(host: str = "0.0.0.0", port: int = 8000):
import uvicorn
uvicorn.run(app, host=host, port=port)
if __name__ == "__main__":
main()