Spaces:
Paused
Paused
Update app.py
Browse files
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,
|
| 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
|
| 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
|
| 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("
|
| 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("
|
| 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}
|
| 151 |
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
| 153 |
old_code_trimmed = old_code.strip()
|
| 154 |
-
new_code = (ch_match.group(1)
|
| 155 |
-
.replace('&', '&')
|
| 156 |
-
.replace('<', '<')
|
| 157 |
-
.replace('>', '>')
|
| 158 |
-
.replace('"', '"')
|
| 159 |
-
.replace(''', "'"))
|
| 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"
|
| 176 |
-
"
|
| 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.
|
| 225 |
-
2.
|
| 226 |
-
3. <change> berisi kode baru
|
| 227 |
-
4.
|
| 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
|
| 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.
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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('&', '&')
|
| 58 |
+
.replace('<', '<')
|
| 59 |
+
.replace('>', '>')
|
| 60 |
+
.replace('"', '"')
|
| 61 |
+
.replace(''', "'"))
|
| 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 < 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 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|