chore: cleaning up provider of prices

This commit is contained in:
2025-11-28 16:23:44 +01:00
parent b5c71e713b
commit e9d9c0e319
2 changed files with 37 additions and 119 deletions

View File

@@ -1,43 +1,18 @@
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Any, Literal from typing import Literal, Optional
import uvicorn import uvicorn, os, sys
import os
import sys
import json
import numpy as np
import pandas as pd
from supabase import create_client, Client from supabase import create_client, Client
try: # Local imports of registry and pricing function
# in docker container, paths are set via PYTHONPATH
from model_registry import ModelRegistry
from pricing import StateSpace, ElasticityBasedPricingFunction
except ImportError:
# local dev: add paths manually
lib_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../lib'))
procesing_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../experiments/procesing'))
sys.path.insert(0, lib_path)
sys.path.insert(0, procesing_path)
from model_registry import ModelRegistry
from pricing import StateSpace, ElasticityBasedPricingFunction
SUPABASE_URL = os.getenv("NEXT_PUBLIC_SUPABASE_URL", "")
SUPABASE_KEY = os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
# Config
app = FastAPI(title="PHANTOM Pricing Provider") app = FastAPI(title="PHANTOM Pricing Provider")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
app.add_middleware( supabase: Client = create_client(os.getenv("NEXT_PUBLIC_SUPABASE_URL"), os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY"))
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
registry = ModelRegistry() registry = ModelRegistry()
class PriceResponse(BaseModel): class PriceResponse(BaseModel):
@@ -49,93 +24,47 @@ class PriceResponse(BaseModel):
model_version: str = 'latest' model_version: str = 'latest'
@app.get("/health") @app.get("/health")
def health(): def health() -> dict:
redis_ok = registry.health_check() return {"status": "healthy", "redis": registry.health_check()}
return {"status": "healthy", "redis": redis_ok}
@app.get("/api/{mode}/price/{productId}") @app.get("/api/{mode}/price/{productId}", response_model=PriceResponse)
def get_price( def get_price(mode: Literal['hotel', 'airline'], productId: str, sessionId: Optional[str] = Query(None), experimentId: Optional[str] = Query(None)):
mode: Literal['hotel', 'airline'], product = supabase.table(f'{mode}_products').select("metadata").eq('id', productId).execute().data[0]
productId: str, if not product: raise HTTPException(404, f"Product {productId} not found")
sessionId: Optional[str] = Query(None),
experimentId: Optional[str] = Query(None)
) -> PriceResponse:
"""
Dynamic pricing endpoint.
1. Fetch product base price from Supabase metadata = product['metadata']
2. Load latest elasticity + pricing model from registry
3. Compute optimal price
5. Return price to client
"""
# fetch product
product_resp = supabase.table(f'{mode}_products').select("id, metadata").eq('id', productId).execute()
if not product_resp.data or len(product_resp.data) == 0:
raise HTTPException(status_code=404, detail=f"Product {productId} not found")
product = product_resp.data[0]
metadata = product.get('metadata', {})
base_price = metadata.get('base_price', 100.0) base_price = metadata.get('base_price', 100.0)
# load elasticity data
elasticity_df = registry.get_elasticity('latest') elasticity_df = registry.get_elasticity('latest')
if elasticity_df is None: if not elasticity_df:
# no model available, return base price return PriceResponse(productId=productId, price=base_price, base_price=base_price, markup=1.0)
final_price = base_price
elasticity_value = None
else:
# load or create pricing model
pricing_model = registry.get_pricing_model('latest')
if pricing_model is None: pricing_model = registry.get_pricing_model('latest') or ElasticityBasedPricingFunction().fit(elasticity_df)
# create default model product_elasticity = elasticity_df[elasticity_df['productId'] == productId]['elasticity'].iloc[0] if (elasticity_row := elasticity_df[elasticity_df['productId'] == productId]).any().any() else None
pricing_model = ElasticityBasedPricingFunction()
pricing_model.fit(elasticity_df)
# get elasticity for this product state = StateSpace(np.array([0.0]), np.array([base_price]), pd.DataFrame())
product_elasticity = elasticity_df[elasticity_df['productId'] == productId] optimal_price = pricing_model.transform(state, np.array([productId]))[0]
elasticity_value = float(product_elasticity['elasticity'].iloc[0]) if not product_elasticity.empty else None
# construct state space (single product)
state = StateSpace(
demand=np.array([0.0]), # demand not needed for elasticity-based pricing
prices=np.array([base_price]),
session_features=pd.DataFrame()
)
# compute optimal price
optimal_prices = pricing_model.transform(state, product_ids=np.array([productId]))
final_price = float(optimal_prices[0])
return PriceResponse( return PriceResponse(
productId=productId, productId=productId,
price=final_price, price=float(optimal_price),
base_price=base_price, base_price=base_price,
markup=final_price / base_price if base_price > 0 else 1.0, markup=optimal_price/base_price,
elasticity=elasticity_value, elasticity=float(product_elasticity) if product_elasticity is not None else None
model_version='latest'
) )
@app.get("/models") @app.get("/models")
def list_models(): def list_models(): return registry.list_models()
"""List all registered models in the registry."""
return registry.list_models()
@app.post("/models/reload") @app.post("/models/reload")
def reload_models(): def reload_models():
"""Force reload of models from registry (useful after pipeline updates).""" elasticity, pricing_model = registry.get_elasticity('latest'), registry.get_pricing_model('latest')
elasticity = registry.get_elasticity('latest')
pricing_model = registry.get_pricing_model('latest')
return { return {
"elasticity_loaded": elasticity is not None, "elasticity_loaded": bool(elasticity),
"n_products": len(elasticity) if elasticity is not None else 0, "n_products": len(elasticity) if elasticity is not None else 0,
"pricing_model_loaded": pricing_model is not None, "pricing_model_loaded": bool(pricing_model),
"model_class": pricing_model.__class__.__name__ if pricing_model else None "model_class": pricing_model.__class__.__name__ if pricing_model else None
} }
if __name__ == "__main__": if __name__ == "__main__":
port = int(os.getenv("PROVIDER_PORT", "5001")) uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PROVIDER_PORT", "5001")))
uvicorn.run(app, host="0.0.0.0", port=port)

View File

@@ -1,26 +1,15 @@
# Web Framework & API fastapi
fastapi>=0.104.0 uvicorn[standard]
uvicorn[standard]>=0.24.0 pydantic
pydantic>=2.0.0 numpy
pandas
# Core ML/Data Science scikit-learn
numpy>=1.24.0 redis
pandas>=2.0.0 supabase
scikit-learn>=1.3.0
# Data Storage & Messaging
redis>=5.0.0
supabase>=2.0.0
confluent-kafka>=2.3.0 confluent-kafka>=2.3.0
kafka-python>=2.0.2 kafka-python
graphviz
# Visualization & Graphing
graphviz>=0.20.1
# Utilities
python-dotenv>=1.0.0 python-dotenv>=1.0.0
requests>=2.31.0 requests>=2.31.0
typing-extensions>=4.8.0 typing-extensions>=4.8.0
# Serialization
pickle5>=0.0.11; python_version < '3.8' pickle5>=0.0.11; python_version < '3.8'