from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, FastAPI, Header, HTTPException, Query from apipilot.adapters.openai import OpenAIAdapter from apipilot.fastapi import install_apipilot from apipilot.tool import CompositeTool app = FastAPI(title="ApiPilot Demo Backend", version="0.1.1") PRODUCTS: dict[int, dict[str, Any]] = { 1: {"id": 1, "name": "Mouse ergonomico", "category": "accessori", "price": 34.99, "stock": 12}, 2: {"id": 2, "name": "Tastiera meccanica", "category": "accessori", "price": 89.99, "stock": 3}, 3: {"id": 3, "name": "Monitor 27\"", "category": "schermi", "price": 219.0, "stock": 7}, 4: {"id": 4, "name": "Laptop 14\"", "category": "computer", "price": 899.0, "stock": 0}, } ORDERS: list[dict[str, Any]] = [ {"id": 1001, "user": "demo-user", "items": [{"product_id": 1, "qty": 2}], "total": 69.98}, ] DEFAULT_USER = {"id": "demo-user", "role": "admin"} def get_current_user_optional(authorization: str | None = Header(default=None)) -> dict[str, Any]: if not authorization: return DEFAULT_USER prefix = "Bearer " token = authorization[len(prefix):] if authorization.startswith(prefix) else authorization USERS = { "admin-token": {"id": "admin", "role": "admin"}, "user-token": {"id": "mario", "role": "user"}, } return USERS.get(token, DEFAULT_USER) def allow_tool(user: Any, record: Any) -> bool: return True router = APIRouter(prefix="/api/v1") @router.get( "/products", summary="Elenca prodotti", openapi_extra={ "x-apipilot": { "enabled": True, "name": "list_products", "description": ( "Elenca i prodotti disponibili. Puoi filtrare per categoria, " "disponibilità in magazzino o ordinare per prezzo. Usa questo tool " "quando l'utente chiede di vedere il catalogo o cercare prodotti." ), } }, ) async def list_products( category: str | None = Query(default=None, description="Categoria prodotto"), in_stock_only: bool = Query(default=False, description="Solo prodotti disponibili"), min_price: float | None = Query(default=None, ge=0, description="Prezzo minimo"), max_price: float | None = Query(default=None, ge=0, description="Prezzo massimo"), limit: int = Query(default=20, ge=1, le=200), ): data = list(PRODUCTS.values()) if category: data = [p for p in data if p["category"] == category] if in_stock_only: data = [p for p in data if p["stock"] > 0] if min_price is not None: data = [p for p in data if p["price"] >= min_price] if max_price is not None: data = [p for p in data if p["price"] <= max_price] return {"items": data[:limit], "total": len(data)} @router.get( "/products/{product_id}", summary="Dettaglio prodotto", openapi_extra={ "x-apipilot": { "enabled": True, "name": "get_product", "description": "Ottieni i dettagli di un prodotto dal suo ID.", } }, ) async def get_product(product_id: int): p = PRODUCTS.get(product_id) if not p: raise HTTPException(status_code=404, detail="Product not found") return p @router.post( "/orders", summary="Crea ordine", openapi_extra={ "x-apipilot": { "enabled": True, "name": "create_order", "description": ( "Crea un nuovo ordine. " "Specifica gli articoli come lista di product_id e qty." ), } }, ) async def create_order( items: list[dict[str, int]], user: dict[str, Any] = Depends(get_current_user_optional), ): total = 0.0 resolved: list[dict[str, Any]] = [] for row in items: pid = row.get("product_id") qty = int(row.get("qty", 1)) product = PRODUCTS.get(int(pid)) if not product: raise HTTPException(status_code=400, detail=f"Unknown product_id {pid}") if product["stock"] < qty: raise HTTPException( status_code=400, detail=f"Insufficient stock for {product['name']}" ) product["stock"] -= qty row_total = product["price"] * qty total += row_total resolved.append({"product_id": pid, "qty": qty, "unit_price": product["price"]}) oid = 1000 + len(ORDERS) + 1 order = {"id": oid, "user": user["id"], "items": resolved, "total": round(total, 2)} ORDERS.append(order) return order @router.post( "/products/{product_id}/restock", summary="Rifornisci prodotto", openapi_extra={ "x-apipilot": { "enabled": True, "name": "restock_product", "description": "Aggiungi unità a magazzino per un prodotto.", } }, ) async def restock_product(product_id: int, quantity: int, user=Depends(get_current_user_optional)): _ = user p = PRODUCTS.get(product_id) if not p: raise HTTPException(status_code=404, detail="Product not found") p["stock"] += quantity return {"id": product_id, "stock": p["stock"]} @router.get( "/orders/my", summary="I miei ordini", openapi_extra={ "x-apipilot": { "enabled": True, "name": "list_my_orders", "description": "Elenca gli ordini dell'utente corrente (demo-user senza token).", } }, ) async def list_my_orders(user=Depends(get_current_user_optional)): mine = [o for o in ORDERS if o["user"] == user["id"]] return {"items": mine, "total": len(mine), "user": user["id"]} app.include_router(router) pilot = install_apipilot( app, get_current_user=get_current_user_optional, allow_tool=allow_tool, default_timeout=10.0, expose_http_endpoints=True, http_prefix="/apipilot", require_auth_for_http=False, ) async def _find_buy_recommendation(args: dict[str, Any], user: Any): budget = float(args.get("budget") or 0) category = args.get("category") list_args: dict[str, Any] = {"in_stock_only": True, "limit": 50} if category: list_args["category"] = category res = await pilot.execute(user=user, name="list_products", arguments=list_args) if not res["success"]: return {"error": res["error"]} products = (res["data"] or {}).get("items", []) if budget and budget > 0: products = [p for p in products if p["price"] <= budget] if not products: return {"recommendation": None, "reason": "Nessun prodotto soddisfa i criteri"} best = max(products, key=lambda p: p.get("stock", 0)) return { "recommendation": best, "alternatives_count": len(products) - 1, } pilot.add_tool( CompositeTool( name="recommend_product", description=( "Consiglia un prodotto all'utente in base a budget e categoria. " "Usa questo tool quando l'utente chiede un suggerimento o una raccomandazione." ), parameters_schema={ "type": "object", "properties": { "budget": {"type": "number", "description": "Budget massimo disponibile."}, "category": {"type": "string", "description": "Categoria desiderata (opzionale)."}, }, "required": [], }, run_fn=_find_buy_recommendation, timeout=20.0, ) ) @app.get("/healthz") async def healthz(): return {"ok": True, "version": "0.1.1"} @app.get("/demo/chat", tags=["demo"]) async def demo_chat(user=Depends(get_current_user_optional)): tools = await pilot.tools_for(user) openai_tools = OpenAIAdapter.to_tools(tools) return { "user": user["id"], "role": user.get("role"), "auth_note": ( "Demo senza autenticazione obbligatoria. " "Senza header Authorization usi l'utente di default demo-user/admin. " "Con Authorization: Bearer admin-token o Bearer user-token " "puoi impersonare altri utenti per test." ), "available_tools": [t["function"]["name"] for t in openai_tools], "openai_tools": openai_tools, "example_agent_cycle": ( "1. Usa tools_for(user) per ottenere i tool. " "2. Convertili con OpenAIAdapter.to_tools. " "3. Invia al modello. " "4. Ricevi la tool_call. " "5. Parse con OpenAIAdapter.parse_tool_call. " "6. Esegui con pilot.execute. " "7. Format risposta con OpenAIAdapter.make_tool_result. " "8. Rimanda al modello per la risposta finale." ), } if __name__ == "__main__": import uvicorn uvicorn.run("examples.demo_backend:app", host="127.0.0.1", port=9292, reload=True)