Skip to content

Commit

Permalink
Merge pull request #9 from Ulas-Scan/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
iyoubee authored Jun 10, 2024
2 parents c98ecaa + 8685e6f commit 91056d9
Show file tree
Hide file tree
Showing 23 changed files with 996 additions and 209 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ jobs:
-e DB_NAME=${{ secrets.DB_NAME }} \
-e DB_PORT=${{ secrets.DB_PORT }} \
-e APP_ENV=${{ secrets.APP_ENV }} \
-e ML_URL=${{ secrets.ML_URL }} \
-e ML_API_KEY=${{ secrets.ML_API_KEY }} \
-e GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} \
$GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/ulascan/$APP_NAME:latest \
&& docker image prune -f"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![Deploy to GCE](https://github.com/Ulas-Scan/UlaScan_BE/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/Ulas-Scan/UlaScan_BE/actions/workflows/deploy.yml)

# Ulascan: Bangkit 2024 Batch 6 Capstone Project Backend

Welcome to the backend repository for the Bangkit 2024 Batch 6 capstone project! This backend serves the mobile app for our project.
Expand Down Expand Up @@ -43,4 +45,4 @@ To set up the backend for the capstone project, follow these steps:
4. **Run the Application**:
```sh
go run main.go
```
```
35 changes: 35 additions & 0 deletions constants/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package constants

const (
PROMPT_SUMMARIZE = `Tolong summarize product reviews di bawah ini maksimal 5 kalimat, dan gunakan Bahasa Indonesia:
Output:
{
"summary": summary
}
`

PROMPT_ANALYZE = ` Harap tentukan jumlah komentar di bawah ini yang menggambarkan kondisi baik, buruk, atau tidak memberikan informasi yang cukup di setiap aspek, yaitu:
1. packaging atau pengemasan,
2. delivery atau pengiriman,
3. respon penjual atau respon admin,
4. product condition atau kondisi produk.
Pastikan bahwa total jumlah komentar tidak melebihi 100, tidak ada yang tumpang tindih, dan setiap komentar hanya diklasifikasikan ke satu kategori. Hanya perlu menampilkan jumlah komentar tanpa rincian komentar aslinya:
jumlah = (komentar positif / (komentar positif + komentar negatif)) * 100
jumlah dalam format float atau bilangan desimal.
PENTING: Output harus berupa JSON yang menyimpan score dalam bentuk desimal atau float bukan string dengan format sebagai berikut:
{
"packaging": float(jumlah),
"delivery": float(jumlah),
"admin_response": float(jumlah),
"product_condition": float(jumlah)
}
Contoh Output:
{
"packaging": 12.34,
"delivery": 56.78,
"admin_response": 91.01,
"product_condition": 11.12
}
`
)
212 changes: 212 additions & 0 deletions controller/ml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package controller

import (
"net/http"
"net/url"
"strings"

"ulascan-be/dto"
"ulascan-be/service"
"ulascan-be/utils"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
)

type (
MLController interface {
GetSentimentAnalysisAndSummarization(ctx *gin.Context)
}

mlController struct {
tokopediaService service.TokopediaService
modelService service.ModelService
geminiService service.GeminiService
historyService service.HistoryService
}
)

func NewMLController(
ts service.TokopediaService,
ms service.ModelService,
gs service.GeminiService,
hs service.HistoryService,
) MLController {
return &mlController{
tokopediaService: ts,
modelService: ms,
geminiService: gs,
historyService: hs,
}
}

func (c *mlController) GetSentimentAnalysisAndSummarization(ctx *gin.Context) {
productUrl := ctx.Query("product_url")
if productUrl == "" {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, dto.ErrProductUrlMissing.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}

parsedUrl, err := url.Parse(productUrl)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, err.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}

pathParts := strings.Split(parsedUrl.Path, "/")
if len(pathParts) < 3 {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, dto.ErrProductUrlWrongFormat.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}

productReq := dto.GetProductRequest{
ShopDomain: pathParts[1],
ProductKey: pathParts[2],
ProductUrl: "https://www.tokopedia.com/" + pathParts[1] + "/" + pathParts[2],
}

product, err := c.tokopediaService.GetProduct(ctx, productReq)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_PRODUCT_ID, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}

shopAvatar, err := c.tokopediaService.GetShopAvatar(ctx, productReq.ShopDomain)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_SHOP_AVATAR, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}

// fmt.Println("=== PRODUCT ID ===")
// fmt.Println(product)

reviewsReq := dto.GetReviewsRequest{
ProductUrl: productReq.ProductUrl,
ProductId: product.ProductId,
}

reviews, err := c.tokopediaService.GetReviews(ctx, reviewsReq)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}

// fmt.Println("=== REVIEWS ===")
// fmt.Println(reviews)

// Assuming `reviews` is a slice of a struct with `Message` and `Rating` fields
statements := make([]string, len(reviews))
ratingSum := 0.0

for i, review := range reviews {
statements[i] = review.Message
ratingSum += float64(review.Rating)
}

var ratingAvg float64
if len(reviews) > 0 {
ratingAvg = ratingSum / float64(len(reviews))
} else {
ratingAvg = 0.0 // or handle the case where there are no reviews
}

predictReq := dto.PredictRequest{
Statements: statements,
}

// fmt.Println("=== PREDICT REQ ===")
// fmt.Println(predictReq)

predictResult, err := c.modelService.Predict(ctx, predictReq)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_PREDICT, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}

var builder strings.Builder
for _, review := range reviews {
builder.WriteString(review.Message)
builder.WriteString("\n")
}
concatenatedMessage := builder.String()

analyzeResult, err := c.geminiService.Analyze(ctx, concatenatedMessage)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_ANALYZE, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}

summarizeResult, err := c.geminiService.Summarize(ctx, concatenatedMessage)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_ANALYZE, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}

userID, exists := ctx.Get("user_id")
if exists {
userIDStr, ok := userID.(string)
if !ok {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_HISTORY, "Invalid user ID type", nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}

userUUID, err := uuid.Parse(userIDStr)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_HISTORY, "Invalid user ID format", nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}

history := dto.HistoryCreateRequest{
UserID: userUUID,
ProductID: product.ProductId,
Rating: len(reviews),
Ulasan: predictResult.CountNegative + predictResult.CountPositive,
Bintang: ratingAvg,
URL: productReq.ProductUrl,
ProductName: product.ProductName,
PositiveCount: predictResult.CountPositive,
NegativeCount: predictResult.CountNegative,
Packaging: analyzeResult.Packaging,
Delivery: analyzeResult.Delivery,
AdminResponse: analyzeResult.AdminResponse,
ProductCondition: analyzeResult.ProductCondition,
Content: summarizeResult,
}
_, err = c.historyService.CreateHistory(ctx, history)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_HISTORY, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
}

res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_REVIEWS, dto.MLResult{
ProductName: product.ProductName,
ProductDescription: product.ProductDescription,
Rating: len(reviews),
Ulasan: predictResult.CountNegative + predictResult.CountPositive,
Bintang: ratingAvg,
ImageUrls: product.ImageUrls,
ShopName: product.ShopName,
ShopAvatar: shopAvatar,
CountNegative: predictResult.CountNegative,
CountPositive: predictResult.CountPositive,
Packaging: analyzeResult.Packaging,
Delivery: analyzeResult.Delivery,
AdminResponse: analyzeResult.AdminResponse,
ProductCondition: analyzeResult.ProductCondition,
Summary: summarizeResult,
})
ctx.JSON(http.StatusOK, res)
}
80 changes: 0 additions & 80 deletions controller/tokopedia.go

This file was deleted.

11 changes: 11 additions & 0 deletions dto/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dto

import "errors"

var (
ErrCreateHttpRequest = errors.New("failed to create http request")
ErrSendsHttpRequest = errors.New("failed to sends http request")
ErrReadHttpResponseBody = errors.New("failed to read http response body")
ErrParseJson = errors.New("failed to parse response json")
ErrNotOk = errors.New("received non-200 response code")
)
20 changes: 20 additions & 0 deletions dto/gemini.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dto

const (
// Failed
MESSAGE_FAILED_ANALYZE = "failed analyze"

// Success
// MESSAGE_SUCCESS_PREDICT = "success predict"
)

var (
// ErrMarshallJson = errors.New("failed to marshall request body json")
)

type AnalyzeResponse struct {
Packaging float32 `json:"packaging"`
Delivery float32 `json:"delivery"`
AdminResponse float32 `json:"admin_response"`
ProductCondition float32 `json:"product_condition"`
}
Loading

0 comments on commit 91056d9

Please sign in to comment.