File size: 7,565 Bytes
362bbff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c64fcea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362bbff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329e3d3
362bbff
 
329e3d3
362bbff
329e3d3
 
362bbff
329e3d3
 
362bbff
329e3d3
362bbff
 
 
 
329e3d3
 
 
 
 
 
 
 
362bbff
 
329e3d3
 
 
362bbff
 
 
 
 
c64fcea
 
 
 
 
 
 
 
 
 
 
329e3d3
 
c64fcea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362bbff
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
"""
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()