Ricky01anjay commited on
Commit
f10c568
Β·
verified Β·
1 Parent(s): a153b34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +36 -48
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  """
2
  AI HTML Editor β€” Flask Server
3
  Semua pemrosesan (AI call, find/change, fuzzy match) dilakukan di sisi server.
@@ -5,7 +6,7 @@ Job disimpan di memori dengan TTL 1 hari.
5
  """
6
 
7
  from flask import Flask, request, jsonify, Response
8
- import uuid, time, re, json, threading, requests
9
 
10
  app = Flask(__name__)
11
 
@@ -48,9 +49,17 @@ def update_job(jid: str, **kwargs):
48
  JOBS[jid].update(kwargs)
49
 
50
  # ─────────────────────────────────────────────
51
- # ALGORITMA FIND & CHANGE (ported dari JS)
52
  # ─────────────────────────────────────────────
53
 
 
 
 
 
 
 
 
 
54
  def levenshtein(a: str, b: str) -> int:
55
  la, lb = len(a), len(b)
56
  if la == 0: return lb
@@ -72,7 +81,6 @@ def calculate_similarity(s1: str, s2: str) -> float:
72
  return (len(longer) - levenshtein(longer, shorter)) / len(longer)
73
 
74
  def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
75
- """Sama persis dengan fuzzyReplace() di JS."""
76
  search_trimmed = search.strip()
77
  if not search_trimmed:
78
  return {"success": False, "result": source}
@@ -89,7 +97,7 @@ def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
89
  except re.error:
90
  pass
91
 
92
- # Level 2 β€” sliding window + Levenshtein (hanya jika < 3000 chars)
93
  if len(search_trimmed) > 3000:
94
  return {"success": False, "result": source}
95
 
@@ -120,24 +128,18 @@ def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
120
 
121
  return {"success": False, "result": source}
122
 
123
-
124
  def apply_find_change(code_base: str, ai_response: str) -> dict:
125
- """
126
- Parse respons AI dan terapkan semua blok <find>/<element>/<change>.
127
- Return dict: {success, code, message, action_label}
128
- """
129
  exec_match = re.search(r'<execution>([\s\S]*?)</execution>', ai_response, re.IGNORECASE)
130
  if not exec_match:
131
- raise ValueError("Gunakan format tag <execution>.")
132
 
133
  exec_content = exec_match.group(1)
134
-
135
  respond_match = re.search(r'<responding>([\s\S]*?)</responding>', exec_content, re.IGNORECASE)
136
  respond_text = respond_match.group(1).strip() if respond_match else "Kode berhasil diperbarui."
137
 
138
  find_blocks = re.findall(r'<find>([\s\S]*?)</find>', exec_content, re.IGNORECASE)
139
  if not find_blocks:
140
- raise ValueError("Kamu wajib menggunakan tag <find> berisi <element> dan <change>.")
141
 
142
  temp_code = code_base
143
  success_count = 0
@@ -147,16 +149,13 @@ def apply_find_change(code_base: str, ai_response: str) -> dict:
147
  el_match = re.search(r'<element>([\s\S]*?)</element>', block, re.IGNORECASE)
148
  ch_match = re.search(r'<change>([\s\S]*?)</change>', block, re.IGNORECASE)
149
  if not el_match or not ch_match:
150
- raise ValueError(f"Blok <find> ke-{i+1} tidak memiliki <element> atau <change>.")
151
 
152
- old_code = el_match.group(1)
 
 
 
153
  old_code_trimmed = old_code.strip()
154
- new_code = (ch_match.group(1)
155
- .replace('&amp;', '&')
156
- .replace('&lt;', '<')
157
- .replace('&gt;', '>')
158
- .replace('&quot;', '"')
159
- .replace('&#39;', "'"))
160
 
161
  if old_code in temp_code:
162
  temp_code = temp_code.replace(old_code, new_code, 1)
@@ -172,15 +171,13 @@ def apply_find_change(code_base: str, ai_response: str) -> dict:
172
  is_fuzzy = True
173
  else:
174
  raise ValueError(
175
- f"Isi dari <element> pada <find> ke-{i+1}:\n{old_code}\n\n"
176
- "TIDAK DITEMUKAN di codebase meskipun fitur Similarity Fallback sudah aktif. "
177
- "Salin teks dengan lebih teliti!"
178
  )
179
 
180
  action_label = f"Diperbarui ({success_count} bagian){' ✨ Auto-Fix' if is_fuzzy else ''}"
181
  return {"code": temp_code, "message": respond_text, "action_label": action_label}
182
 
183
-
184
  # ─────────────────────────────────────────────
185
  # AI CALL
186
  # ─────────────────────────────────────────────
@@ -212,7 +209,6 @@ def call_ai(prompt: str, model: str = DEFAULT_MODEL) -> str:
212
  return str(answer)
213
  raise RuntimeError(f"Format respons API tidak dikenali: {str(data)[:200]}")
214
 
215
-
216
  def build_system_prompt(code_base: str) -> str:
217
  return f"""Kamu adalah AI HTML Editor. Tugasmu HANYA mengubah bagian tertentu dari kode (Partial Update).
218
  Codebase saat ini:
@@ -220,14 +216,13 @@ Codebase saat ini:
220
  {code_base}
221
  ```
222
 
223
- ATURAN WAJIB:
224
- 1. Gunakan blok <find> untuk setiap perubahan.
225
- 2. <element> HARUS berisi teks/kode lama yang IDENTIK PERSIS dengan Codebase.
226
- 3. <change> berisi kode baru penggantinya.
227
- 4. Boleh memakai banyak blok <find> sekaligus.
228
- 5. Balas HANYA dengan format <execution> seperti contoh di bawah. DILARANG KERAS menulis ulang seluruh kode!
229
 
230
- CONTOH JAWABAN:
231
  <execution>
232
  <responding>Saya telah mengganti teks H1 dan warna tombol.</responding>
233
  <find>
@@ -240,24 +235,27 @@ CONTOH JAWABAN:
240
  </find>
241
  </execution>"""
242
 
243
-
244
  def run_ai_loop(jid: str, user_instruction: str, code_base: str,
245
  loop: int = 0, error_feedback: str = None, max_loops: int = 10,
246
  model: str = DEFAULT_MODEL):
247
- """Rekursif di thread terpisah β€” sama dengan runAILoop() JS."""
248
  if loop >= max_loops:
249
  update_job(jid,
250
  status="error",
251
- error="Gagal merubah kode setelah 10x loop perbaikan. AI terus menerus gagal mencocokkan kode dengan Codebase. Coba beri instruksi yang lebih jelas.",
252
  loop_count=loop,
253
  )
254
  return
255
 
256
  try:
257
  prompt = build_system_prompt(code_base) + f"\n\nInstruksi User: {user_instruction}"
 
258
  if error_feedback:
259
- prompt += (f"\n\n[ERROR PADA JAWABANMU SEBELUMNYA]:\n{error_feedback}\n"
260
- "Perbaiki pencarian teksmu. Jika kau kesulitan, setidaknya salin elemen persis seperti aslinya.")
 
 
 
261
 
262
  ai_text = call_ai(prompt, model=model)
263
  result = apply_find_change(code_base, ai_text)
@@ -269,7 +267,7 @@ def run_ai_loop(jid: str, user_instruction: str, code_base: str,
269
  )
270
 
271
  except Exception as exc:
272
- # Self-healing: retry
273
  threading.Thread(
274
  target=run_ai_loop,
275
  args=(jid, user_instruction, code_base, loop + 1, str(exc), max_loops),
@@ -277,17 +275,12 @@ def run_ai_loop(jid: str, user_instruction: str, code_base: str,
277
  daemon=True,
278
  ).start()
279
 
280
-
281
  # ─────────────────────────────────────────────
282
  # ROUTES
283
  # ─────────────────────────────────────────────
284
 
285
  @app.route("/api/chat", methods=["POST"])
286
  def api_chat():
287
- """
288
- Body: { instruction: str, code_base: str }
289
- Return: { job_id: str }
290
- """
291
  body = request.get_json(silent=True) or {}
292
  instruction = body.get("instruction", "").strip()
293
  code_base = body.get("code_base", "")
@@ -308,10 +301,8 @@ def api_chat():
308
 
309
  return jsonify({"job_id": jid}), 202
310
 
311
-
312
  @app.route("/api/job/<jid>", methods=["GET"])
313
  def api_job_status(jid):
314
- """Poll status job."""
315
  job = get_job(jid)
316
  if not job:
317
  return jsonify({"error": "Job tidak ditemukan atau sudah expired (> 1 hari)"}), 404
@@ -328,17 +319,14 @@ def api_job_status(jid):
328
 
329
  return jsonify(resp)
330
 
331
-
332
  @app.route("/api/job/<jid>", methods=["DELETE"])
333
  def api_delete_job(jid):
334
- """Hapus job secara manual."""
335
  with _lock:
336
  if jid in JOBS:
337
  del JOBS[jid]
338
  return jsonify({"deleted": True})
339
  return jsonify({"error": "Job tidak ditemukan"}), 404
340
 
341
-
342
  # ─────────────────────────────────────────────
343
  # UI (embedded HTML)
344
  # ─────────────────────────────────────────────
 
1
+
2
  """
3
  AI HTML Editor β€” Flask Server
4
  Semua pemrosesan (AI call, find/change, fuzzy match) dilakukan di sisi server.
 
6
  """
7
 
8
  from flask import Flask, request, jsonify, Response
9
+ import uuid, time, re, threading, requests
10
 
11
  app = Flask(__name__)
12
 
 
49
  JOBS[jid].update(kwargs)
50
 
51
  # ─────────────────────────────────────────────
52
+ # ALGORITMA FIND & CHANGE
53
  # ─────────────────────────────────────────────
54
 
55
+ def decode_html_entities(text: str) -> str:
56
+ """Perbaiki jika AI mengembalikan HTML yang ter-escape secara tidak sengaja."""
57
+ return (text.replace('&amp;', '&')
58
+ .replace('&lt;', '<')
59
+ .replace('&gt;', '>')
60
+ .replace('&quot;', '"')
61
+ .replace('&#39;', "'"))
62
+
63
  def levenshtein(a: str, b: str) -> int:
64
  la, lb = len(a), len(b)
65
  if la == 0: return lb
 
81
  return (len(longer) - levenshtein(longer, shorter)) / len(longer)
82
 
83
  def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
 
84
  search_trimmed = search.strip()
85
  if not search_trimmed:
86
  return {"success": False, "result": source}
 
97
  except re.error:
98
  pass
99
 
100
+ # Level 2 β€” sliding window + Levenshtein
101
  if len(search_trimmed) > 3000:
102
  return {"success": False, "result": source}
103
 
 
128
 
129
  return {"success": False, "result": source}
130
 
 
131
  def apply_find_change(code_base: str, ai_response: str) -> dict:
 
 
 
 
132
  exec_match = re.search(r'<execution>([\s\S]*?)</execution>', ai_response, re.IGNORECASE)
133
  if not exec_match:
134
+ raise ValueError("AI Gagal menggunakan format tag <execution>.")
135
 
136
  exec_content = exec_match.group(1)
 
137
  respond_match = re.search(r'<responding>([\s\S]*?)</responding>', exec_content, re.IGNORECASE)
138
  respond_text = respond_match.group(1).strip() if respond_match else "Kode berhasil diperbarui."
139
 
140
  find_blocks = re.findall(r'<find>([\s\S]*?)</find>', exec_content, re.IGNORECASE)
141
  if not find_blocks:
142
+ raise ValueError("AI tidak memberikan tag <find> yang berisi <element> dan <change>.")
143
 
144
  temp_code = code_base
145
  success_count = 0
 
149
  el_match = re.search(r'<element>([\s\S]*?)</element>', block, re.IGNORECASE)
150
  ch_match = re.search(r'<change>([\s\S]*?)</change>', block, re.IGNORECASE)
151
  if not el_match or not ch_match:
152
+ raise ValueError(f"Blok <find> ke-{i+1} formatnya salah (kehilangan element/change).")
153
 
154
+ # Decode entitas HTML yang mungkin tersalin sebagai &lt; dsb
155
+ old_code = decode_html_entities(el_match.group(1))
156
+ new_code = decode_html_entities(ch_match.group(1))
157
+
158
  old_code_trimmed = old_code.strip()
 
 
 
 
 
 
159
 
160
  if old_code in temp_code:
161
  temp_code = temp_code.replace(old_code, new_code, 1)
 
171
  is_fuzzy = True
172
  else:
173
  raise ValueError(
174
+ f"Kode pada <element> ke-{i+1} TIDAK DITEMUKAN persis di Codebase.\n"
175
+ f"Teks yang kamu kirim:\n{old_code[:100]}..."
 
176
  )
177
 
178
  action_label = f"Diperbarui ({success_count} bagian){' ✨ Auto-Fix' if is_fuzzy else ''}"
179
  return {"code": temp_code, "message": respond_text, "action_label": action_label}
180
 
 
181
  # ─────────────────────────────────────────────
182
  # AI CALL
183
  # ─────────────────────────────────────────────
 
209
  return str(answer)
210
  raise RuntimeError(f"Format respons API tidak dikenali: {str(data)[:200]}")
211
 
 
212
  def build_system_prompt(code_base: str) -> str:
213
  return f"""Kamu adalah AI HTML Editor. Tugasmu HANYA mengubah bagian tertentu dari kode (Partial Update).
214
  Codebase saat ini:
 
216
  {code_base}
217
  ```
218
 
219
+ ATURAN WAJIB PENGGUNAAN BLOK <find>:
220
+ 1. <element> HARUS berisi teks/kode yang IDENTIK PERSIS dengan Codebase (termasuk spasi, tab, dan baris kosong). JANGAN perbaiki indentasi di dalam <element>!
221
+ 2. JANGAN memasukkan potongan kode yang terlalu panjang. Ambil sependek mungkin (1-4 baris) asalkan UNIK.
222
+ 3. <change> berisi kode baru yang akan menggantikan <element> tersebut.
223
+ 4. Balas HANYA dengan format <execution> seperti contoh. DILARANG menulis ulang seluruh codebase di luar tag.
 
224
 
225
+ CONTOH JAWABAN YANG BENAR:
226
  <execution>
227
  <responding>Saya telah mengganti teks H1 dan warna tombol.</responding>
228
  <find>
 
235
  </find>
236
  </execution>"""
237
 
 
238
  def run_ai_loop(jid: str, user_instruction: str, code_base: str,
239
  loop: int = 0, error_feedback: str = None, max_loops: int = 10,
240
  model: str = DEFAULT_MODEL):
241
+ """Rekursif self-healing AI."""
242
  if loop >= max_loops:
243
  update_job(jid,
244
  status="error",
245
+ error="Gagal merubah kode setelah 10x loop perbaikan. AI terus menerus gagal mencocokkan kode dengan Codebase. Mohon salin sebagian kecil kode secara manual ke dalam chat jika ingin diubah.",
246
  loop_count=loop,
247
  )
248
  return
249
 
250
  try:
251
  prompt = build_system_prompt(code_base) + f"\n\nInstruksi User: {user_instruction}"
252
+
253
  if error_feedback:
254
+ prompt += (f"\n\n[PENTING! ERROR PADA JAWABANMU SEBELUMNYA]:\n{error_feedback}\n\n"
255
+ "INSTRUKSI PERBAIKAN:\n"
256
+ "1. Kesalahan ini terjadi karena teks di <element> beda spasi/karakter dengan codebase.\n"
257
+ "2. SOLUSI: Ambil potongan kode yang LEBIH PENDEK (1-2 baris saja) yang unik.\n"
258
+ "3. Jangan mengubah spasi dan indentasi sedikitpun saat menyalin ke <element>!")
259
 
260
  ai_text = call_ai(prompt, model=model)
261
  result = apply_find_change(code_base, ai_text)
 
267
  )
268
 
269
  except Exception as exc:
270
+ # Self-healing: retry dengan mengirimkan error ke AI
271
  threading.Thread(
272
  target=run_ai_loop,
273
  args=(jid, user_instruction, code_base, loop + 1, str(exc), max_loops),
 
275
  daemon=True,
276
  ).start()
277
 
 
278
  # ─────────────────────────────────────────────
279
  # ROUTES
280
  # ─────────────────────────────────────────────
281
 
282
  @app.route("/api/chat", methods=["POST"])
283
  def api_chat():
 
 
 
 
284
  body = request.get_json(silent=True) or {}
285
  instruction = body.get("instruction", "").strip()
286
  code_base = body.get("code_base", "")
 
301
 
302
  return jsonify({"job_id": jid}), 202
303
 
 
304
  @app.route("/api/job/<jid>", methods=["GET"])
305
  def api_job_status(jid):
 
306
  job = get_job(jid)
307
  if not job:
308
  return jsonify({"error": "Job tidak ditemukan atau sudah expired (> 1 hari)"}), 404
 
319
 
320
  return jsonify(resp)
321
 
 
322
  @app.route("/api/job/<jid>", methods=["DELETE"])
323
  def api_delete_job(jid):
 
324
  with _lock:
325
  if jid in JOBS:
326
  del JOBS[jid]
327
  return jsonify({"deleted": True})
328
  return jsonify({"error": "Job tidak ditemukan"}), 404
329
 
 
330
  # ─────────────────────────────────────────────
331
  # UI (embedded HTML)
332
  # ─────────────────────────────────────────────