Files changed (1) hide show
  1. app.py +227 -91
app.py CHANGED
@@ -1,138 +1,274 @@
1
- from fastapi import FastAPI, HTTPException
2
- from pydantic import BaseModel
3
- from fastapi.middleware.cors import CORSMiddleware
4
  import os
5
- import google.generativeai as genai
6
- from typing import List, Optional
7
  from uuid import uuid4
8
 
9
- # Konfigurasi Gemini API
10
- genai.configure(api_key=os.environ["GEMINI_API_KEY"])
11
- model = genai.GenerativeModel("gemini-1.5-flash")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # Init FastAPI
14
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- # CORS biar frontend bebas akses
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
20
- allow_credentials=True,
21
  allow_methods=["*"],
22
  allow_headers=["*"],
23
  )
24
 
25
- # === MODEL UNTUK REQUEST ===
26
  class PromptRequest(BaseModel):
27
  prompt: str
28
- max_new_tokens: int = 300
29
- temperature: float = 0.7
30
- history: Optional[List[str]] = []
31
 
32
  class GenerateSoalRequest(BaseModel):
33
  topic: str
34
- level: Optional[int] = 1
35
- tipe_pertanyaan: Optional[str] = None # "pilihan_ganda", "isian_kosong", "benar_salah"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- # === HELPER FUNCTION ===
38
  def format_prompt_soal(topic, tipe=None, level=1):
39
- tipe_instruksi = f"Tipe soal yang diminta adalah '{tipe}'." if tipe else "Tipe soal boleh bebas."
40
  return f"""
41
- Buatkan satu soal latihan grammar Bahasa Inggris untuk topik: "{topic}".
42
- {tipe_instruksi}
43
- Level kesulitan: {level}/5.
44
-
45
- Format keluaran harus dalam JSON seperti berikut:
46
- {{
47
- "text_pertanyaan": "Tulis pertanyaan di sini...",
48
- "tipe_pertanyaan": "pilihan_ganda | isian_kosong | benar_salah",
49
- "opsi": {{
50
- "a": "Pilihan A",
51
- "b": "Pilihan B",
52
- "c": "Pilihan C",
53
- "d": "Pilihan D"
54
- }},
55
- "jawaban_benar": "b",
56
- "penjelasan": "Berikan penjelasan edukatif tentang jawaban yang benar."
57
- }}
58
-
59
- Catatan:
60
- - Jika tipe soal bukan "pilihan_ganda", sesuaikan struktur 'opsi' (bisa null atau objek true/false).
61
- - Jangan tambahkan teks selain JSON.
62
- - Jangan tambahkan penjelasan tambahan di luar JSON.
63
-
64
- Mulai sekarang, buat 1 soal untuk topik di atas dan jawab dalam format JSON!
65
- """
66
-
67
- # === ENDPOINT UNTUK PERBAIKAN KELUARAN MODEL (EXISTING) ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  @app.post("/generate")
69
  async def generate_text(req: PromptRequest):
70
  try:
71
- system_prompt = f"""
72
- Kamu adalah asisten edukatif untuk sistem pembelajaran Bahasa Inggris.
73
-
74
- Berikut ini adalah hasil dari model LLaMA:
75
- --- MULAI ---
76
- {req.prompt}
77
- --- SELESAI ---
78
-
79
- Tugas kamu adalah:
80
- 1. Periksa dan perbaiki kesalahan tata bahasa dan struktur kalimat jika perlu.
81
- 2. Ubah seluruh jawaban ke dalam Bahasa Indonesia dengan gaya edukatif dan mudah dipahami oleh pelajar.
82
- 3. Pertahankan contoh kalimat dalam Bahasa Inggris sebagaimana aslinya. Jangan diterjemahkan.
83
- 4. Pertahankan istilah grammar atau kata yang diapit tanda petik seperti "present continuous", "am", "is", "are", dll.
84
- 5. Jangan menambahkan penjelasan yang tidak berkaitan dengan topik grammar dalam jawaban.
85
- 6. Fokus pada konteks pembelajaran Bahasa Inggris. Jangan membahas topik lain di luar itu.
86
- 7. Jika isi jawaban terlalu panjang, buatlah ringkasan yang jelas dan tetap lengkap.
87
-
88
- Langsung tampilkan hasil akhir dalam Bahasa Indonesia tanpa menjelaskan proses atau langkah-langkah pengerjaan.
89
- """
90
- messages = [{"role": "user", "parts": [{"text": system_prompt.strip()}]}]
91
- for msg in req.history or []:
92
- role = "user" if msg.startswith("User:") else "model"
93
- text = msg.replace("User: ", "").replace("Bot: ", "")
94
  messages.append({"role": role, "parts": [{"text": text}]})
95
 
96
- result = model.generate_content(
 
 
97
  messages,
98
  generation_config={
99
  "temperature": req.temperature,
100
  "max_output_tokens": req.max_new_tokens,
101
- }
102
  )
103
- return {"generated_text": result.text}
104
-
105
  except Exception as e:
106
  raise HTTPException(status_code=500, detail=str(e))
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
- # === ENDPOINT BARU UNTUK GENERATE SOAL DARI AI ===
110
  @app.post("/generate-soal")
111
  async def generate_soal(req: GenerateSoalRequest):
112
  try:
 
113
  prompt = format_prompt_soal(req.topic, req.tipe_pertanyaan, req.level)
114
- result = model.generate_content(
115
  prompt,
116
- generation_config={"temperature": 0.8, "max_output_tokens": 512}
117
  )
 
118
 
119
- # Bersihkan & parsing response (anggap Gemini kasih string JSON langsung)
120
- import json
121
- json_text = result.text.strip().split("```")[-1].strip() # Bersihin kalau ada blok kode
122
- soal_data = json.loads(json_text)
 
 
 
123
 
 
124
  soal_data["id"] = str(uuid4())
125
  soal_data["topic_id"] = req.topic
126
- soal_data["level"] = req.level or 1
127
  soal_data["is_ai_generated"] = True
128
- soal_data["created_at"] = str(os.getenv("TZ") or "2025-07-12T00:00:00Z")
129
-
130
  return {"soal": soal_data}
131
  except Exception as e:
132
- raise HTTPException(status_code=500, detail=f"Gagal generate soal: {e}")
133
-
134
-
135
- # === ROOT TEST ===
136
- @app.get("/")
137
- async def root():
138
- return {"message": "Gemini endpoint aktif bro! 🚀"}
 
 
 
 
1
  import os
2
+ import time
3
+ from typing import List, Optional, Any
4
  from uuid import uuid4
5
 
6
+ import google.generativeai as genai
7
+ from fastapi import FastAPI, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, Field
10
+
11
+ API_KEY = os.getenv("GEMINI_API_KEY", "").strip()
12
+ if not API_KEY:
13
+ raise RuntimeError("Defina GEMINI_API_KEY no Hugging Face Space.")
14
+
15
+ genai.configure(api_key=API_KEY)
16
+
17
+ _model = None
18
+ _model_name = None
19
+
20
+ def _clean_model_name(name: str) -> str:
21
+ name = (name or "").strip()
22
+ return name[7:] if name.startswith("models/") else name
23
+
24
+ def _available_generate_models() -> List[str]:
25
+ names = []
26
+ try:
27
+ for item in genai.list_models():
28
+ methods = getattr(item, "supported_generation_methods", None) or []
29
+ if "generateContent" in methods:
30
+ name = _clean_model_name(getattr(item, "name", ""))
31
+ if name:
32
+ names.append(name)
33
+ except Exception:
34
+ pass
35
+ return names
36
+
37
+ def get_model():
38
+ global _model, _model_name
39
+ if _model is not None:
40
+ return _model
41
 
42
+ requested = _clean_model_name(os.getenv("GEMINI_MODEL", ""))
43
+ available = _available_generate_models()
44
+
45
+ if requested:
46
+ chosen = requested
47
+ else:
48
+ preferred = [
49
+ "gemini-2.5-flash",
50
+ "gemini-2.0-flash",
51
+ "gemini-flash-latest",
52
+ "gemini-pro-latest",
53
+ ]
54
+ chosen = next((x for x in preferred if x in available), None)
55
+ if not chosen and available:
56
+ chosen = available[0]
57
+ if not chosen:
58
+ chosen = _clean_model_name(
59
+ os.getenv("GEMINI_FALLBACK_MODEL", "gemini-2.0-flash")
60
+ )
61
+
62
+ _model_name = chosen
63
+ _model = genai.GenerativeModel(chosen)
64
+ return _model
65
+
66
+ def current_model_name() -> str:
67
+ return _model_name or _clean_model_name(os.getenv("GEMINI_MODEL", "")) or "auto"
68
+
69
+ app = FastAPI(title="Gemini OpenAI-Compatible Endpoint", version="2.0.0")
70
 
 
71
  app.add_middleware(
72
  CORSMiddleware,
73
  allow_origins=["*"],
74
+ allow_credentials=False,
75
  allow_methods=["*"],
76
  allow_headers=["*"],
77
  )
78
 
 
79
  class PromptRequest(BaseModel):
80
  prompt: str
81
+ max_new_tokens: int = Field(default=300, ge=1, le=8192)
82
+ temperature: float = Field(default=0.7, ge=0.0, le=2.0)
83
+ history: List[str] = Field(default_factory=list)
84
 
85
  class GenerateSoalRequest(BaseModel):
86
  topic: str
87
+ level: int = Field(default=1, ge=1, le=5)
88
+ tipe_pertanyaan: Optional[str] = None
89
+
90
+ class ChatMessage(BaseModel):
91
+ role: str
92
+ content: Any
93
+
94
+ class ChatCompletionRequest(BaseModel):
95
+ model: Optional[str] = None
96
+ messages: List[ChatMessage]
97
+ temperature: float = Field(default=0.7, ge=0.0, le=2.0)
98
+ max_tokens: int = Field(default=512, ge=1, le=8192)
99
+ stream: bool = False
100
+
101
+ def normalize_content(content: Any) -> str:
102
+ if isinstance(content, str):
103
+ return content
104
+ if isinstance(content, list):
105
+ parts = []
106
+ for item in content:
107
+ if isinstance(item, dict) and "text" in item:
108
+ parts.append(str(item["text"]))
109
+ else:
110
+ parts.append(str(item))
111
+ return "\n".join(parts)
112
+ return "" if content is None else str(content)
113
+
114
+ def chat_messages_to_gemini(messages: List[ChatMessage]):
115
+ result = []
116
+ system = []
117
+
118
+ for msg in messages:
119
+ role = (msg.role or "user").lower()
120
+ text = normalize_content(msg.content)
121
+
122
+ if role == "system":
123
+ system.append(text)
124
+ continue
125
+
126
+ result.append({
127
+ "role": "model" if role == "assistant" else "user",
128
+ "parts": [{"text": text}],
129
+ })
130
+
131
+ if system:
132
+ prefix = "INSTRUÇÕES DO SISTEMA:\n" + "\n\n".join(system) + "\n\n"
133
+ if result and result[0]["role"] == "user":
134
+ result[0]["parts"][0]["text"] = prefix + result[0]["parts"][0]["text"]
135
+ else:
136
+ result.insert(0, {"role": "user", "parts": [{"text": prefix}]})
137
+
138
+ return result or [{"role": "user", "parts": [{"text": "Olá"}]}]
139
+
140
+ def extract_text(result) -> str:
141
+ try:
142
+ return result.text
143
+ except Exception:
144
+ return str(result)
145
 
 
146
  def format_prompt_soal(topic, tipe=None, level=1):
147
+ tipe_instrucao = f"Tipe soal solicitado: {tipe}." if tipe else "Tipo de questão livre."
148
  return f"""
149
+ Crie uma questão de prática de gramática inglesa sobre: "{topic}".
150
+ {tipe_instrucao}
151
+ Nível: {level}/5.
152
+ Responda SOMENTE com JSON válido com as chaves:
153
+ text_pertanyaan, tipe_pertanyaan, opsi, jawaban_benar, penjelasan.
154
+ """.strip()
155
+
156
+ @app.get("/")
157
+ async def root():
158
+ return {
159
+ "status": "online",
160
+ "service": "Gemini endpoint",
161
+ "model": current_model_name(),
162
+ "endpoints": {
163
+ "generate": "POST /generate",
164
+ "chat": "POST /v1/chat/completions",
165
+ "models": "GET /v1/models",
166
+ "health": "GET /health",
167
+ },
168
+ }
169
+
170
+ @app.head("/")
171
+ async def root_head():
172
+ return {}
173
+
174
+ @app.get("/health")
175
+ async def health():
176
+ return {"status": "ok", "model": current_model_name()}
177
+
178
+ @app.get("/v1/models")
179
+ async def models():
180
+ available = _available_generate_models()
181
+ selected = current_model_name()
182
+ if selected == "auto" and available:
183
+ selected = available[0]
184
+ return {
185
+ "object": "list",
186
+ "data": [{
187
+ "id": selected,
188
+ "object": "model",
189
+ "created": int(time.time()),
190
+ "owned_by": "google",
191
+ }],
192
+ }
193
+
194
  @app.post("/generate")
195
  async def generate_text(req: PromptRequest):
196
  try:
197
+ messages = []
198
+ for msg in req.history:
199
+ if msg.startswith("Bot:") or msg.startswith("Assistant:"):
200
+ role = "model"
201
+ text = msg.split(":", 1)[1].strip() if ":" in msg else msg
202
+ else:
203
+ role = "user"
204
+ text = msg.split(":", 1)[1].strip() if msg.startswith("User:") else msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  messages.append({"role": role, "parts": [{"text": text}]})
206
 
207
+ messages.append({"role": "user", "parts": [{"text": req.prompt}]})
208
+
209
+ result = get_model().generate_content(
210
  messages,
211
  generation_config={
212
  "temperature": req.temperature,
213
  "max_output_tokens": req.max_new_tokens,
214
+ },
215
  )
216
+ return {"generated_text": extract_text(result), "model": current_model_name()}
 
217
  except Exception as e:
218
  raise HTTPException(status_code=500, detail=str(e))
219
 
220
+ @app.post("/v1/chat/completions")
221
+ async def chat_completions(req: ChatCompletionRequest):
222
+ if req.stream:
223
+ raise HTTPException(status_code=400, detail="stream=true ainda não é suportado.")
224
+
225
+ try:
226
+ result = get_model().generate_content(
227
+ chat_messages_to_gemini(req.messages),
228
+ generation_config={
229
+ "temperature": req.temperature,
230
+ "max_output_tokens": req.max_tokens,
231
+ },
232
+ )
233
+ answer = extract_text(result)
234
+ return {
235
+ "id": "chatcmpl-" + uuid4().hex,
236
+ "object": "chat.completion",
237
+ "created": int(time.time()),
238
+ "model": current_model_name(),
239
+ "choices": [{
240
+ "index": 0,
241
+ "message": {"role": "assistant", "content": answer},
242
+ "finish_reason": "stop",
243
+ }],
244
+ }
245
+ except Exception as e:
246
+ raise HTTPException(status_code=500, detail=str(e))
247
 
 
248
  @app.post("/generate-soal")
249
  async def generate_soal(req: GenerateSoalRequest):
250
  try:
251
+ import json
252
  prompt = format_prompt_soal(req.topic, req.tipe_pertanyaan, req.level)
253
+ result = get_model().generate_content(
254
  prompt,
255
+ generation_config={"temperature": 0.8, "max_output_tokens": 512},
256
  )
257
+ text = extract_text(result).strip()
258
 
259
+ if text.startswith("```"):
260
+ lines = text.splitlines()[1:]
261
+ if lines and lines[-1].strip().startswith("```"):
262
+ lines = lines[:-1]
263
+ text = "\n".join(lines).strip()
264
+ if text.lower().startswith("json"):
265
+ text = text[4:].lstrip()
266
 
267
+ soal_data = json.loads(text)
268
  soal_data["id"] = str(uuid4())
269
  soal_data["topic_id"] = req.topic
270
+ soal_data["level"] = req.level
271
  soal_data["is_ai_generated"] = True
 
 
272
  return {"soal": soal_data}
273
  except Exception as e:
274
+ raise HTTPException(status_code=500, detail=f"Gagal generate soal: {e}")