Neha Singh
Restore web UI compatibility endpoints
539132a
Raw
History Blame Contribute Delete
6.34 kB
"""
FastAPI app for the Shopping Agent — powered by OpenEnv.
Uses openenv.core.env_server.http_server.create_app() to expose the
ShoppingEnvironment over HTTP + WebSocket endpoints that any EnvClient
(including ShoppingEnvClient) can consume.
Endpoints auto-provided by OpenEnv:
POST /reset → Reset environment
POST /step → Execute action
GET /state → Current episode state
GET /health → Health check
GET /schema → Action/Observation schemas
WS /ws → WebSocket persistent session
Custom endpoints added below:
GET / → Web UI
GET /profile → User personality profile
"""
import os
import sys
from pathlib import Path
import uvicorn
from pydantic import BaseModel, Field
# Ensure project root is importable
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from openenv.core.env_server.http_server import create_app
from openenv_models import ShoppingAction, ShoppingObservation
from server.shopping_environment import ShoppingEnvironment
from memory_engine import load_profile
from rl_agent import RLShoppingAgent
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
_agent = RLShoppingAgent()
_ui_env: ShoppingEnvironment | None = None
class UIRestBody(BaseModel):
"""Payload for the custom UI reset endpoint."""
task: str | None = Field(default=None)
query: str | None = Field(default=None)
# --- Create the OpenEnv app -------------------------------------------------
# Pass the CLASS (factory), not an instance — create_app creates per-session.
app = create_app(
ShoppingEnvironment,
ShoppingAction,
ShoppingObservation,
env_name="shopping_agent",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# --- Custom endpoints -------------------------------------------------------
@app.get("/", include_in_schema=False)
async def serve_frontend():
"""Serve the web UI."""
index_path = STATIC_DIR / "index.html"
if index_path.exists():
return FileResponse(str(index_path), media_type="text/html")
return JSONResponse(content={"message": "Shopping Agent OpenEnv server is running."})
@app.get("/profile")
async def profile():
"""Return the loaded user personality profile summary."""
prof = load_profile()
return {
"personality_summary": prof.personality_summary[:500],
"preferences": {
k: getattr(prof, k)
for k in [
"price_sensitivity", "quality_preference", "risk_aversion",
"research_depth", "brand_trust", "exploration_vs_repeat",
"review_dependence", "return_preference", "decision_speed",
"discount_sensitivity",
]
},
"decision_process": prof.decision_process,
"semantic_conclusions": [
{"conclusion": c.get("conclusion", ""), "confidence": c.get("confidence", 0)}
for c in prof.semantic_conclusions[:6]
],
"shopping_goals": prof.shopping_goals[:400],
}
@app.post("/ui/reset")
async def ui_reset(body: UIRestBody) -> dict:
"""Reset a dedicated UI session with the richer legacy payload."""
global _ui_env
_ui_env = ShoppingEnvironment()
observation = _ui_env.reset(task=body.task, query=body.query or "")
profile_prefs = _ui_env._user_profile.get_prefs_for_category(observation.category)
return {
"query": observation.query,
"product_count": len(_ui_env.catalog),
"products": list(_ui_env.catalog),
"scored_products": [
{
"id": item["product"]["id"],
"name": item["product"]["name"],
"score": item["personality_score"],
"rank": item["rank"],
}
for item in _ui_env._scored_products
],
"personality_traits": profile_prefs,
"ideal_product": (
_ui_env._scored_products[0]["product"]["name"]
if _ui_env._scored_products
else None
),
"ideal_score": (
_ui_env._scored_products[0]["personality_score"]
if _ui_env._scored_products
else 0.0
),
}
@app.post("/auto-run")
async def auto_run() -> dict:
"""Run the autonomous RL helper flow used by the bundled web UI."""
if _ui_env is None or not _ui_env.catalog:
return JSONResponse(
status_code=400,
content={"error": "No active UI session. Start with /ui/reset first."},
)
attempts = _agent.run_episode(
products=_ui_env.catalog,
scored_products=_ui_env._scored_products,
)
stats = _agent.get_stats()
ideal = _ui_env._scored_products[0] if _ui_env._scored_products else None
return {
"attempts": attempts,
"episode_number": stats["episode_count"],
"total_episode_reward": round(sum(a["reward"] for a in attempts), 4),
"success": any(a["is_success"] for a in attempts),
"success_attempt": next(
(a["attempt"] for a in attempts if a["is_success"]),
None,
),
"ideal_product": (
{
"name": ideal["product"]["name"],
"score": ideal["personality_score"],
"id": ideal["product"]["id"],
}
if ideal
else None
),
"agent_stats": stats,
}
@app.get("/agent-stats")
async def agent_stats() -> dict:
"""Return persisted RL helper stats for the web UI."""
return _agent.get_stats()
@app.post("/agent-reset")
async def agent_reset() -> dict:
"""Reset persisted RL helper weights for the web UI."""
_agent.reset_weights()
return {
"message": "Agent weights reset. Learning starts from scratch.",
"stats": _agent.get_stats(),
}
# Mount static files
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
def main():
"""Run the server directly."""
uvicorn.run(app, host="0.0.0.0", port=8000)
if __name__ == "__main__":
main()