diff --git a/src/frontend/handlers.go b/src/frontend/handlers.go index 2170c1a..a9ad7b0 100644 --- a/src/frontend/handlers.go +++ b/src/frontend/handlers.go @@ -95,7 +95,7 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) http.Error(w, "product id not specified", http.StatusBadRequest) return } - log.Printf("[productHandler] id=%s currency=%s", id, currentCurrency(r)) + log.Printf("[productHandler] id=%s currency=%s session=%s", id, currentCurrency(r), sessionID(r)) p, err := fe.getProduct(r.Context(), id) if err != nil { http.Error(w, fmt.Sprintf("could not retrieve product: %+v", err), http.StatusInternalServerError) @@ -106,6 +106,7 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) http.Error(w, fmt.Sprintf("could not retrieve currencies: %+v", err), http.StatusInternalServerError) return } + cart, err := fe.getCart(r.Context(), sessionID(r)) if err != nil { http.Error(w, fmt.Sprintf("could not retrieve cart: %+v", err), http.StatusInternalServerError) @@ -120,17 +121,25 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) return } + recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id}) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get product recommendations: %+v", err), http.StatusInternalServerError) + return + } + log.Printf("cart size=%d", len(cart)) + product := struct { Item *pb.Product Price *pb.Money }{p, price} if err := templates.ExecuteTemplate(w, "product", map[string]interface{}{ - "user_currency": currentCurrency(r), - "currencies": currencies, - "product": product, - "session_id": sessionID(r), - "cart_size": len(cart), + "user_currency": currentCurrency(r), + "currencies": currencies, + "product": product, + "session_id": sessionID(r), + "recommendations": recommendations, + "cart_size": len(cart), }); err != nil { log.Println(err) } @@ -183,6 +192,12 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request return } + recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart)) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get product recommendations: %+v", err), http.StatusInternalServerError) + return + } + type cartItemView struct { Item *pb.Product Quantity int32 @@ -211,11 +226,12 @@ func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request } if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{ - "user_currency": currentCurrency(r), - "currencies": currencies, - "session_id": sessionID(r), - "cart_size": len(cart), - "items": items, + "user_currency": currentCurrency(r), + "currencies": currencies, + "session_id": sessionID(r), + "recommendations": recommendations, + "cart_size": len(cart), + "items": items, }); err != nil { log.Println(err) } @@ -265,3 +281,11 @@ func sessionID(r *http.Request) string { } return "" } + +func cartIDs(c []*pb.CartItem) []string { + out := make([]string, len(c)) + for i, v := range c { + out[i] = v.GetProductId() + } + return out +} diff --git a/src/frontend/main.go b/src/frontend/main.go index cda4a22..46ca919 100644 --- a/src/frontend/main.go +++ b/src/frontend/main.go @@ -37,6 +37,9 @@ type frontendServer struct { cartSvcAddr string cartSvcConn *grpc.ClientConn + + recommendationSvcAddr string + recommendationSvcConn *grpc.ClientConn } func main() { @@ -51,6 +54,7 @@ func main() { mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR") mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR") mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR") + mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_SERVICE_ADDR") var err error svc.currencySvcConn, err = grpc.DialContext(ctx, svc.currencySvcAddr, grpc.WithInsecure()) @@ -65,6 +69,10 @@ func main() { if err != nil { log.Fatalf("failed to connect cart service at %s: %+v", svc.cartSvcAddr, err) } + svc.recommendationSvcConn, err = grpc.DialContext(ctx, svc.recommendationSvcAddr, grpc.WithInsecure()) + if err != nil { + log.Fatalf("failed to connect recommendation service at %s: %+v", svc.recommendationSvcConn, err) + } r := mux.NewRouter() r.HandleFunc("/", ensureSessionID(svc.homeHandler)).Methods(http.MethodGet, http.MethodHead) diff --git a/src/frontend/rpc.go b/src/frontend/rpc.go index f295506..4e589f3 100644 --- a/src/frontend/rpc.go +++ b/src/frontend/rpc.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "google.golang.org/grpc/codes" @@ -74,3 +75,23 @@ func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money, From: money, ToCode: currency}) } + +func (fe *frontendServer) getRecommendations(ctx context.Context, userID string, productIDs []string) ([]*pb.Product, error) { + resp, err := pb.NewRecommendationServiceClient(fe.recommendationSvcConn).ListRecommendations(ctx, + &pb.ListRecommendationsRequest{UserId: userID, ProductIds: productIDs}) + if err != nil { + return nil, err + } + out := make([]*pb.Product, len(resp.GetProductIds())) + for i, v := range resp.GetProductIds() { + p, err := fe.getProduct(ctx, v) + if err != nil { + return nil, fmt.Errorf("failed to get recommended product info (#%s): %+v", v, err) + } + out[i] = p + } + if len(out) > 4 { + out = out[:4] // take only first four to fit the UI + } + return out, err +} diff --git a/src/frontend/templates/cart.html b/src/frontend/templates/cart.html index f20057c..b73fba0 100644 --- a/src/frontend/templates/cart.html +++ b/src/frontend/templates/cart.html @@ -14,8 +14,8 @@ {{ range $.items }}
- +
{{.Item.Name}}
@@ -45,6 +45,11 @@
{{ end }} + {{ if $.recommendations}} +
+ {{ template "recommendations" $.recommendations }} + {{ end }} +
diff --git a/src/frontend/templates/footer.html b/src/frontend/templates/footer.html index f4eef54..7b8d3f1 100644 --- a/src/frontend/templates/footer.html +++ b/src/frontend/templates/footer.html @@ -1,7 +1,6 @@ {{ define "footer" }} - - -session-id: {{$.session_id}} + + This is a demo application. {{ end }} diff --git a/src/frontend/templates/product.html b/src/frontend/templates/product.html index a4dae1d..332c3b4 100644 --- a/src/frontend/templates/product.html +++ b/src/frontend/templates/product.html @@ -6,7 +6,7 @@
-
@@ -43,6 +43,10 @@
+ {{ if $.recommendations}} +
+ {{ template "recommendations" $.recommendations }} + {{ end }}
diff --git a/src/frontend/templates/recommendations.html b/src/frontend/templates/recommendations.html new file mode 100644 index 0000000..e3c2728 --- /dev/null +++ b/src/frontend/templates/recommendations.html @@ -0,0 +1,21 @@ +{{ define "recommendations" }} +
Similar products
+
+ {{range . }} +
+
+ + + +
+ + {{ .Name }} + +
+
+
+ {{ end }} +
+{{ end }} diff --git a/src/recommendationservice/recommendation_server.py b/src/recommendationservice/recommendation_server.py index 1ea14f5..b75c3a0 100644 --- a/src/recommendationservice/recommendation_server.py +++ b/src/recommendationservice/recommendation_server.py @@ -12,13 +12,17 @@ class RecommendationService(demo_pb2_grpc.RecommendationServiceServicer): # fetch list of products from product catalog stub cat_response = stub.ListProducts(demo_pb2.Empty()) - num_prodcuts = len(cat_response.products) - num_return = min(max_responses, num_prodcuts) + + product_ids = [x.id for x in cat_response.products] + filtered_products = list(set(product_ids)-set(request.product_ids)) + + num_products = len(filtered_products) + num_return = min(max_responses, num_products) # sample list of indicies to return - indices = random.sample(range(num_prodcuts), num_return) + indices = random.sample(range(num_products), num_return) # fetch product ids from indices - prod_list = [cat_response.products[i].id for i in indices] + prod_list = [filtered_products[i] for i in indices] print("handling request: {}".format(prod_list)) # build and return response