File size: 9,566 Bytes
75b11f2
 
 
667c4c1
 
4ae4d83
b9e63d0
4ae4d83
ce7e30d
b9e63d0
dbd8654
b85033e
19e9e7f
e2ed4eb
32c7a9d
1c73a09
e2ed4eb
 
 
7cf4ebd
e2ed4eb
7cf4ebd
19e9e7f
5cfd9ba
b9e63d0
667c4c1
75b11f2
 
 
 
 
 
 
 
667c4c1
 
 
 
 
 
 
e2ed4eb
7d3b1a7
7cf4ebd
 
 
 
 
 
 
 
 
 
667c4c1
 
 
 
 
 
 
 
 
effb7bb
5cfd9ba
 
32c7a9d
2ad1711
32c7a9d
667c4c1
 
 
e2ed4eb
667c4c1
e2ed4eb
667c4c1
 
 
 
 
 
 
 
 
 
 
effb7bb
32c7a9d
effb7bb
dbd8654
75b11f2
 
e2ed4eb
75b11f2
e2ed4eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dbd8654
e2ed4eb
19e9e7f
 
 
 
7cf4ebd
0f537c3
d3eb0b7
8a892b6
d3eb0b7
 
1c73a09
ff69e56
8a892b6
7cf4ebd
19e9e7f
 
 
7cf4ebd
 
19e9e7f
7cf4ebd
 
e2ed4eb
 
19e9e7f
32c7a9d
e2ed4eb
 
 
 
 
 
 
 
 
 
2224f62
e2ed4eb
 
 
 
 
 
 
d0ab5f5
e2ed4eb
 
 
 
 
 
 
 
 
 
 
 
 
 
1c73a09
667c4c1
4ae4d83
 
75b11f2
 
 
48cc02f
75b11f2
3610939
75b11f2
 
 
 
 
 
 
 
 
 
 
 
a3211f8
5cfd9ba
a3211f8
effb7bb
0f537c3
75b11f2
 
982f6ac
5cfd9ba
 
982f6ac
 
 
 
 
 
dbd8654
982f6ac
 
5cfd9ba
75b11f2
982f6ac
5cfd9ba
75b11f2
 
 
 
5cfd9ba
75b11f2
 
1c73a09
 
75b11f2
667c4c1
1c73a09
75b11f2
4ae4d83
75b11f2
dbd8654
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import os
import json
import requests
import joblib
import re
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from apify_client import ApifyClient
from google import genai

# --- 1. KONFIGURASI API ---
APIFY_TOKEN = os.getenv("APIFY_TOKEN")
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") 

if not GROQ_API_KEY:
    print("⚠️ WARNING: GROQ_API_KEY belum di-set!")
if not GEMINI_API_KEY:
    print("⚠️ WARNING: GEMINI_API_KEY belum di-set!")
if not APIFY_TOKEN:
    print("⚠️ WARNING: APIFY_TOKEN belum di-set!")

GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
apify_client = ApifyClient(APIFY_TOKEN)

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# --- 2. MUAT MODEL SVM (LOKAL) ---
print("Memuat model SVM dan TF-IDF Vectorizer...")
try:
    model_svm = joblib.load('model_sentimen_smart_ayce.pkl')
    vectorizer = joblib.load('vectorizer_smart_ayce.pkl')
    print("✅ Model SVM berhasil dimuat!")
except Exception as e:
    print(f"❌ ERROR: Gagal memuat model SVM: {e}")

# --- ASPECT KEYWORD MAP (Smart Aspect Mapping) ---
ASPECT_KEYWORDS = {
    "Kualitas Makanan": ["enak", "lezat", "makanan", "rasa", "menu", "segar", "matang", "mentah", "asin", "hambar", "bumbu", "daging", "seafood", "sushi", "dessert"],
    "Pelayanan": ["pelayan", "staf", "staff", "karyawan", "ramah", "jutek", "cepat", "responsif", "waitress", "waiter", "server"],
    "Kebersihan": ["bersih", "kotor", "higien", "AC", "toilet", "tempat", "meja", "lantai", "nyaman", "jorok", "bau"],
    "Harga": ["mahal", "murah", "harga", "worth", "ekonomis", "sebanding", "value", "bayar", "biaya", "promo"],
    "Variasi Menu": ["variasi", "pilihan", "beragam", "banyak menu", "macam", "jenis", "lengkap", "terbatas", "sedikit"],
    "Waktu Tunggu": ["antri", "antre", "lama", "tunggu", "queue", "booking", "reservasi", "penuh", "ramai"],
}

# --- FUNGSI PEMBERSIH TEKS (Untuk SVM) ---
def bersihkan_teks(teks):
    teks = str(teks).lower() 
    teks = re.sub(r'http\S+', '', teks) 
    teks = re.sub(r'[^a-z\s]', '', teks) 
    teks = re.sub(r'\s+', ' ', teks).strip() 
    return teks

# --- 3. FUNGSI ANALISIS SENTIMEN (FAST-SVM) ---
def analyze_sentiment(ulasan_list):
    aspek_list = ["Kualitas Makanan", "Pelayanan", "Kebersihan", "Harga", "Variasi Menu", "Waktu Tunggu"]
    raw_scores = {k: {"pos": 0, "neu": 0, "neg": 0} for k in aspek_list}
    
    for teks in ulasan_list[:100]:
        try:
            teks_bersih = bersihkan_teks(teks)
            if not teks_bersih:
                continue
                            
            teks_vektor = vectorizer.transform([teks_bersih])
            sentiment = model_svm.predict(teks_vektor)[0]
            
            teks_lower = teks.lower()
            matched_aspects = [
                asp for asp, keywords in ASPECT_KEYWORDS.items()
                if any(kw in teks_lower for kw in keywords)
            ]
            if not matched_aspects:
                matched_aspects = aspek_list
                
            for asp in matched_aspects:
                raw_scores[asp][sentiment] += 1
                
        except Exception as e:
            print(f"Sentiment Error: {e}")
            
    return raw_scores

# --- 4. FUNGSI REKOMENDASI DENGAN FALLBACK ---
def get_advanced_insights(ulasan_list):
    gabungan_ulasan = " | ".join(ulasan_list[:50])
    
    prompt = f"""
    Kamu adalah Konsultan Operasional Restoran. Tugasmu adalah membaca ulasan pelanggan berikut dan memberikan saran perbaikan kepada PEMILIK / MANAJEMEN restoran.
    
    Ulasan Pelanggan:
    {gabungan_ulasan}
    
    ATURAN MUTLAK:
    1. Bagian "rekomendasi" HARUS ditujukan kepada MANAJEMEN restoran. 
    2. DILARANG KERAS memberikan rekomendasi bergaya promosi untuk pelanggan.
    3. Batasi maksimal 30 kata kunci per kategori, dan maksimal 5 rekomendasi aksi.
    
    Kamu WAJIB mengembalikan respon HANYA dalam format JSON murni ini:
    {{
      "kata_kunci_positif": ["kata1", "kata2"],
      "kata_kunci_negatif": ["kata1", "kata2"],
      "rekomendasi": [
        {{"judul": "Tindakan Manajerial", "deskripsi": "Deskripsi tindakan untuk staf/manajemen", "prioritas": "High/Medium/Low"}}
      ]
    }}
    """
    
    #PERCOBAAN 1: MENGGUNAKAN GROQ (Llama 3.1)
    try:
        print("Mencoba Groq API...")
        headers = {
            "Authorization": f"Bearer {GROQ_API_KEY}",
            "Content-Type": "application/json"
        }
        payload = {
            "model": "llama-3.1-8b-instant", 
            "messages": [
                {"role": "system", "content": "You are a strict restaurant business consultant advising the management. Always output valid JSON."},
                {"role": "user", "content": prompt}
            ],
            "response_format": {"type": "json_object"}, 
            "temperature": 0.4,
            "max_tokens": 2048
        }
        
        response = requests.post(GROQ_URL, headers=headers, json=payload, timeout=20)
        
        if response.status_code == 200:
            data = response.json()
            t = data["choices"][0]["message"]["content"]
            return json.loads(t)
        else:
            print(f"Groq Gagal ({response.status_code}). Beralih ke Gemini...")
            raise Exception("Groq Timeout/Error")
            
    except Exception as e:
        #PERCOBAAN 2 (FALLBACK): MENGGUNAKAN GEMINI (Google GenAI SDK)
        print(f"Groq Error ({e}), Menjalankan Fallback Gemini via SDK...")
        if not GEMINI_API_KEY:
            return fallback_response()
            
        try:
            # Menggunakan format baru dari Google GenAI SDK
            client = genai.Client(api_key=GEMINI_API_KEY)
            
            gemini_response = client.models.generate_content(
                model="gemini-3.5-flash",
                contents=f"System: You are a strict restaurant business consultant advising the management. Always output valid JSON.\n\nUser: {prompt}"
            )
            
            # Bersihkan teks kalau seandainya AI membalas pakai format markdown (```json ... ```)
            bersih = gemini_response.text.strip()
            if bersih.startswith("```json"):
                bersih = bersih[7:-3].strip()
            elif bersih.startswith("```"):
                bersih = bersih[3:-3].strip()
                
            return json.loads(bersih)
                
        except Exception as gemini_e:
            print(f"Gemini Error: {gemini_e}")
            return fallback_response()

def fallback_response():
    return {
        "kata_kunci_positif": ["Sistem Antre"], 
        "kata_kunci_negatif": ["Server Sibuk"], 
        "rekomendasi": [{"judul": "Sistem Sedang Sibuk", "deskripsi": "AI Rekomendasi sedang mengalami antrean (Groq & Gemini down). Silakan evaluasi data sentimen di atas secara manual sementara waktu.", "prioritas": "Low"}]
    }

# --- 5. ENDPOINT ---
@app.post("/analyze")
async def analyze_realtime(request: Request):
    try:
        body = await request.json()
        target_url = body.get("url")
        
        run = apify_client.actor("compass/google-maps-reviews-scraper").call(
            run_input={"startUrls": [{"url": target_url}], "maxReviews": 150, "language": "id"}
        )
        
        ulasan_lengkap = []
        ulasan_teks_saja = []
        for item in apify_client.dataset(run["defaultDatasetId"]).iterate_items():
            if item.get("text"):
                ulasan_teks_saja.append(item["text"])
                ulasan_lengkap.append({
                    "nama": item.get("name", "Anonim"),
                    "rating": item.get("stars", 5),
                    "teks": item["text"]
                })
        if not ulasan_teks_saja:
            return {"status": "error", "message": "Ulasan tidak ditemukan."}

        raw_scores = analyze_sentiment(ulasan_teks_saja)
        insights_data = get_advanced_insights(ulasan_teks_saja) 
        
        sentiment_results = []
        tp = tn = tneu = 0
        for name, counts in raw_scores.items():
            total = counts["pos"] + counts["neu"] + counts["neg"]
            tp += counts["pos"]; tn += counts["neg"]; tneu += counts["neu"]
            
            p = round((counts["pos"]/total)*100) if total > 0 else 0
            n = round((counts["neg"]/total)*100) if total > 0 else 0
            neu = 100 - p - n if total > 0 else 100
            sentiment_results.append({"name": name, "positive": p, "neutral": neu, "negative": n})

        grand = tp + tn + tneu
        if grand == 0:
            over_p, over_neu, over_n = 0, 100, 0
        else:
            over_p = round((tp/grand)*100, 1); over_n = round((tn/grand)*100, 1)
            over_neu = round(100 - over_p - over_n, 1)

        return {
            "status": "success",
            "analysis": {
                "overall_sentiment": {"positive": over_p, "neutral": over_neu, "negative": over_n},
                "sentiment_score": sentiment_results,
                "kata_kunci": {
                    "positif": insights_data.get("kata_kunci_positif", []),
                    "negatif": insights_data.get("kata_kunci_negatif", [])
                },
                "contoh_review": ulasan_lengkap[:6], 
                "rekomendasi_list": insights_data.get("rekomendasi", [])
            }
        }
    except Exception as e:
        return {"status": "error", "message": str(e)}