| """ |
| 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 |
|
|
| |
| 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) |
|
|
| |
| |
| app = create_app( |
| ShoppingEnvironment, |
| ShoppingAction, |
| ShoppingObservation, |
| env_name="shopping_agent", |
| ) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
|
|
| |
|
|
| @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(), |
| } |
|
|
|
|
| |
| 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() |
|
|