Instructions to use punsaisuwan/frankenmoe-python-typescript with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use punsaisuwan/frankenmoe-python-typescript with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir frankenmoe-python-typescript punsaisuwan/frankenmoe-python-typescript
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
v2: Add router, persona enforcement, repetition guard logic
Browse files- moe_fixes.py +586 -0
moe_fixes.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
moe_fixes.py
|
| 3 |
+
============
|
| 4 |
+
รวม fix จาก Adversarial + Multi-turn Testing
|
| 5 |
+
|
| 6 |
+
Fix ล่าสุด (Round 7):
|
| 7 |
+
1. เพิ่ม is_degenerate_query() + DEGENERATE_FALLBACK_RESPONSE — ตรวจจับ
|
| 8 |
+
Query ที่ไม่มีตัวอักษรที่มีความหมายเพียงพอ (ว่างเปล่า, มีแต่ Emoji,
|
| 9 |
+
มีแต่สัญลักษณ์เช่น "???!!!") ก่อนเข้า Pipeline ทั้งหมด
|
| 10 |
+
แก้ปัญหา: จาก Adversarial Test พบว่า Query ประเภทนี้ (Empty, Emoji-only,
|
| 11 |
+
Symbol-only) ถูกส่งเข้า LLM Classifier + Reasoning Expert + Translation
|
| 12 |
+
Layer เต็มขั้นตอนทุกครั้ง ทำให้ Latency สูงถึง 4-14 วินาทีโดยไม่จำเป็น
|
| 13 |
+
และเสี่ยง Hallucination (พบคำตอบเพี้ยนเรื่อง "crab" ที่ไม่เกี่ยวข้อง)
|
| 14 |
+
หรือ Language Leakage (พบคำว่า "riêng" หลุดเข้ามาในคำตอบภาษาไทย)
|
| 15 |
+
ใช้คู่กับ Fast-Path Guard ใน moe_orchestrator.py ที่ตอบกลับทันทีโดย
|
| 16 |
+
ไม่ต้อง Generate เลย (Latency ≈ 0s)
|
| 17 |
+
|
| 18 |
+
Fix รอบก่อนหน้า (Round 6 — คงเดิม):
|
| 19 |
+
2. แก้ Bug: GREETING_CLOSING_WORDS ใช้ Substring Match ทำให้ "บาย" (bye)
|
| 20 |
+
จับ False Positive กับคำว่า "อธิบาย" (explain) — แยกเป็น 2 กลุ่มตาม
|
| 21 |
+
ความเสี่ยง (Safe Substring / ต้อง Exact Match)
|
| 22 |
+
|
| 23 |
+
Fix รอบก่อนหน้านั้น (Round 5 — คงเดิม):
|
| 24 |
+
3. route_query() รับ previous_expert — History Continuation แทนการผ่าน
|
| 25 |
+
LLM Classifier ทุกครั้งที่ Query ไม่มี Keyword ชัดเจน
|
| 26 |
+
4. is_code_refusal() — จับ Pattern ปฏิเสธงานที่ไม่สมเหตุสมผลของ
|
| 27 |
+
Python/TypeScript Expert คู่กับ Retry Logic ก่อนบันทึกเข้า History
|
| 28 |
+
|
| 29 |
+
Fix รอบก่อนหน้านั้น (คงเดิมทั้งหมด):
|
| 30 |
+
5. ALLOWED_PATTERN รองรับ Emoji Unicode Range
|
| 31 |
+
6. strip_translation_preamble() ใช้ตำแหน่งตัวอักษรไทยตัวแรก
|
| 32 |
+
7. enforce_persona() บังคับ ผม/ครับ แบบ Deterministic
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
import re
|
| 36 |
+
from dataclasses import dataclass, field
|
| 37 |
+
from typing import Optional
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ======================================================================
|
| 41 |
+
# 0) DEGENERATE QUERY GUARD (Round 7: Fast-Path ก่อนเข้า Pipeline ทั้งหมด)
|
| 42 |
+
# ======================================================================
|
| 43 |
+
|
| 44 |
+
# จำนวนตัวอักษรที่มีความหมาย (ตัวหนังสือ/ตัวเลข/underscore ทุกภาษา) ขั้นต่ำ
|
| 45 |
+
# ที่ต้องมีถึงจะถือว่า Query นี้ "มีเนื้อหาให้ตอบจริง" — ต่ำกว่านี้ถือว่า
|
| 46 |
+
# Degenerate (ว่าง/มีแต่ Emoji/มีแต่สัญลักษณ์) ไม่ควรส่งเข้า Model เลย
|
| 47 |
+
MIN_MEANINGFUL_LENGTH = 2
|
| 48 |
+
|
| 49 |
+
# \w ใน Python 3 regex ครอบคลุม Unicode Word Character ทุกภาษาโดย Default
|
| 50 |
+
# (รวมภาษาไทย, อังกฤษ, ตัวเลข, underscore) ส่วน Emoji/สัญลักษณ์ทั้งหมด
|
| 51 |
+
# (Category So, Po, Sm ฯลฯ) จะถูกจับด้วย \W เพราะไม่ใช่ Word Character
|
| 52 |
+
_NON_MEANINGFUL_REGEX = re.compile(r'\W', flags=re.UNICODE)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def is_degenerate_query(query: str) -> bool:
|
| 56 |
+
"""
|
| 57 |
+
คืนค่า True ถ้า Query ไม่มีตัวอักษรที่มีความหมายเพียงพอให้ตอบ
|
| 58 |
+
เช่น "", " ", "\\n\\n\\n", "🐍🐍🐍", "???!!!...", "?"
|
| 59 |
+
คืนค่า False ถ้ามีเนื้อหาจริง เช่น "help", "ทำไง", "def foo(): pass"
|
| 60 |
+
"""
|
| 61 |
+
stripped = query.strip()
|
| 62 |
+
if len(stripped) == 0:
|
| 63 |
+
return True
|
| 64 |
+
meaningful_chars = _NON_MEANINGFUL_REGEX.sub('', stripped)
|
| 65 |
+
return len(meaningful_chars) < MIN_MEANINGFUL_LENGTH
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
DEGENERATE_FALLBACK_RESPONSE = (
|
| 69 |
+
"ขอโทษครับ ผมไม่แน่ใจว่าคุณต้องการถามอะไรครับ "
|
| 70 |
+
"ลองพิมพ์คำถามหรือรายละเอียดเพิ่มเติมให้ผมช่วยได้ไหมครับ"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ======================================================================
|
| 75 |
+
# 0b) GREETING / CLOSING QUICK-REPLY (Round 8: ลด Latency ของ Small-talk)
|
| 76 |
+
# ======================================================================
|
| 77 |
+
# ต่างจาก is_degenerate_query() ตรงที่นี่ใช้ Exact-Match แบบเข้มงวด
|
| 78 |
+
# เพื่อป้องกันไม่ให้ Query ที่มีเนื้อหาจริงแซมมา (เช่น "ขอบคุณสำหรับ
|
| 79 |
+
# function นี้นะครับ") ถูก Quick-Reply ผิดพลาดจนพลาดเนื้อหาที่ User
|
| 80 |
+
# ต้องการคุยต่อจริงๆ — ใช้เฉพาะ Query ที่เป็น Greeting/Closing ล้วนๆ
|
| 81 |
+
# แก้ปัญหา: จาก Multi-turn Test พบว่า "สวัสดี" และ "ขอบคุณครับ" เสีย
|
| 82 |
+
# Latency 4.48s และ 10.06s เพราะผ่าน llm_classifier + reasoning +
|
| 83 |
+
# Translation Layer เต็มขั้นตอน ทั้งที่ไม่มีเนื้อหาให้ Generate จริง
|
| 84 |
+
|
| 85 |
+
_POLITE_SUFFIXES = ["ครับ", "ค่ะ", "จ้า", "นะครับ", "นะคะ", "น้า"]
|
| 86 |
+
|
| 87 |
+
GREETING_KEYWORDS = {"สวัสดี", "หวัดดี", "หวัดดีจ้า", "hi", "hello", "hey"}
|
| 88 |
+
CLOSING_KEYWORDS = {"บาย", "บ๊ายบาย", "ลาก่อน", "แล้วเจอกัน", "bye", "goodbye"}
|
| 89 |
+
THANKS_KEYWORDS = {"ขอบคุณ", "ขอบคุณมาก", "ขอบใจ", "thanks", "thank you", "thx"}
|
| 90 |
+
|
| 91 |
+
GREETING_QUICK_REPLY_TEXT = "สวัสดีครับ มีอะไรให้ผมช่วยไหมครับ"
|
| 92 |
+
CLOSING_QUICK_REPLY_TEXT = "ลาก่อนครับ แล้วพบกันใหม่นะครับ"
|
| 93 |
+
THANKS_QUICK_REPLY_TEXT = "ด้วยความยินดีครับ มีอะไรให้ผมช่วยเพิ่มอีกไหมครับ"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _normalize_for_greeting_match(query: str) -> str:
|
| 97 |
+
text = query.strip()
|
| 98 |
+
text = re.sub(r'[!.?~๏๛]+$', '', text).strip()
|
| 99 |
+
for suffix in _POLITE_SUFFIXES:
|
| 100 |
+
if text.endswith(suffix):
|
| 101 |
+
text = text[: -len(suffix)].strip()
|
| 102 |
+
break
|
| 103 |
+
return text.lower()
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def get_greeting_quick_reply(query: str) -> Optional[str]:
|
| 107 |
+
"""
|
| 108 |
+
คืนค่า Canned Response ถ้า query เป็น Greeting/Closing/Thanks ล้วนๆ
|
| 109 |
+
(Exact Match หลังตัดคำลงท้ายสุภาพ) คืนค่า None ถ้ามีเนื้อหาอื่นแซมอยู่
|
| 110 |
+
หรือไม่เข้าเงื่อนไขใดเลย — เพื่อให้ระบบ Generate ตามปกติ
|
| 111 |
+
"""
|
| 112 |
+
normalized = _normalize_for_greeting_match(query)
|
| 113 |
+
if not normalized:
|
| 114 |
+
return None
|
| 115 |
+
if normalized in GREETING_KEYWORDS:
|
| 116 |
+
return GREETING_QUICK_REPLY_TEXT
|
| 117 |
+
if normalized in CLOSING_KEYWORDS:
|
| 118 |
+
return CLOSING_QUICK_REPLY_TEXT
|
| 119 |
+
if normalized in THANKS_KEYWORDS:
|
| 120 |
+
return THANKS_QUICK_REPLY_TEXT
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ======================================================================
|
| 125 |
+
# 0c) REPETITION LOOP DETECTOR (Round 9: ป้องกัน Generation Collapse)
|
| 126 |
+
# ======================================================================
|
| 127 |
+
# แก้ปัญหา: จาก Multi-turn Test พบว่าทั้ง Python Expert และ Reasoning
|
| 128 |
+
# Expert สามารถหลุดเข้า "Repetition Trap" ได้ — วนพูดวลีเดิมซ้ำๆไม่รู้จบ
|
| 129 |
+
# (เช่น "ผมขออภัยที่ทำให้คุณสับสน" ซ้ำ 50+ ครั้ง หรือรายชื่อ Google Service
|
| 130 |
+
# ซ้ำวนไม่จบ) จนชน max_tokens ทำให้ Latency พุ่งสูงสุดถึง 39.6 วินาที
|
| 131 |
+
# และคำตอบไม่มีเนื้อหาที่ต้องการเลย เกิดข้าม Expert คนละตัว ยืนยันว่าเป็น
|
| 132 |
+
# ปัญหาระดับ Sampler Configuration ไม่ใช่ปัญหาเฉพาะ LoRA Adapter ตัวใด
|
| 133 |
+
|
| 134 |
+
_REPETITION_LOOP_REGEX = re.compile(r'(.{6,50}?)\1{3,}', re.DOTALL)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def is_repetitive_response(text: str) -> bool:
|
| 138 |
+
"""
|
| 139 |
+
คืนค่า True ถ้าพบ Substring ความยาว 6-50 ตัวอักษรที่ซ้ำติดกัน
|
| 140 |
+
ตั้งแต่ 4 ครั้งขึ้นไป (Repetition Collapse) — threshold ถูกตั้งให้
|
| 141 |
+
ไม่ False Positive กับโค้ดที่มี Pattern ซ้ำแบบปกติ (เช่น Code Block
|
| 142 |
+
ตัวอย่าง 2 อันที่คล้ายกัน) เพราะต้องซ้ำ 4 ครั้งขึ้นไปถึงจะ Flag
|
| 143 |
+
"""
|
| 144 |
+
return bool(_REPETITION_LOOP_REGEX.search(text))
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ======================================================================
|
| 148 |
+
# 1) WEIGHTED ROUTER
|
| 149 |
+
# ======================================================================
|
| 150 |
+
|
| 151 |
+
@dataclass
|
| 152 |
+
class ExpertKeywords:
|
| 153 |
+
name: str
|
| 154 |
+
keywords: list = field(default_factory=list)
|
| 155 |
+
weight_multiplier: float = 1.0
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
EXPERT_REGISTRY = [
|
| 159 |
+
ExpertKeywords("python", ["python", "ไพธอน", "def ", ".py", "pip", "pandas", "numpy"]),
|
| 160 |
+
ExpertKeywords("typescript", ["typescript", ".ts", "interface", "npm", "const ", "javascript", "react", "node.js"]),
|
| 161 |
+
ExpertKeywords("reasoning", [
|
| 162 |
+
"คำนวณ", "อธิบายทีละขั้นตอน", "วิเคราะห์เหตุผล",
|
| 163 |
+
"โจทย์คณิตศาสตร์", "แก้สมการ", "หาค่า", "step by step"
|
| 164 |
+
]),
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
CODE_DEBUG_SIGNALS = ["โค้ด", "code", "error", "บัค", "bug", "พัง", "ฟังก์ชัน", "function"]
|
| 168 |
+
OUTPUT_SIGNAL_WORDS = ["output เป็น", "แปลงเป็น", "เขียนเป็น", "ทำเป็น", "output as", "convert to"]
|
| 169 |
+
AMBIGUOUS_THRESHOLD = 0.15
|
| 170 |
+
|
| 171 |
+
GREETING_SAFE_SUBSTRING_WORDS = ["สวัสดี", "ขอบคุณ", "หวัดดี", "ลาก่อน"]
|
| 172 |
+
GREETING_EXACT_MATCH_WORDS = ["บาย", "bye", "บ๊ายบาย"]
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _has_greeting_or_closing(query: str) -> bool:
|
| 176 |
+
if any(w in query for w in GREETING_SAFE_SUBSTRING_WORDS):
|
| 177 |
+
return True
|
| 178 |
+
normalized = re.sub(r'[^\w\u0E00-\u0E7F]', '', query).strip().lower()
|
| 179 |
+
return normalized in GREETING_EXACT_MATCH_WORDS
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _keyword_score(query: str, expert: "ExpertKeywords") -> float:
|
| 183 |
+
q_lower = query.lower()
|
| 184 |
+
score = 0.0
|
| 185 |
+
for kw in expert.keywords:
|
| 186 |
+
count = q_lower.count(kw.lower())
|
| 187 |
+
if count > 0:
|
| 188 |
+
position = q_lower.find(kw.lower())
|
| 189 |
+
position_boost = 1.0 + max(0, (30 - position) / 30) * 0.3
|
| 190 |
+
score += count * position_boost
|
| 191 |
+
return score * expert.weight_multiplier
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def route_query(query: str, previous_expert: Optional[str] = None) -> dict:
|
| 195 |
+
"""
|
| 196 |
+
previous_expert: Expert ที่ใช้ใน Turn ก่อนหน้า (สำหรับ Multi-turn Conversation)
|
| 197 |
+
ถ้าไม่ส่งมา (None) ทำงานแบบ Stateless เหมือนเดิม — Backward Compatible 100%
|
| 198 |
+
"""
|
| 199 |
+
q_lower = query.lower()
|
| 200 |
+
has_debug_signal = any(sig in q_lower for sig in CODE_DEBUG_SIGNALS)
|
| 201 |
+
has_output_signal = any(sig in q_lower for sig in OUTPUT_SIGNAL_WORDS)
|
| 202 |
+
has_greeting = _has_greeting_or_closing(query)
|
| 203 |
+
|
| 204 |
+
scores = {e.name: _keyword_score(query, e) for e in EXPERT_REGISTRY}
|
| 205 |
+
sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
| 206 |
+
|
| 207 |
+
top_expert, top_score = sorted_scores[0]
|
| 208 |
+
second_expert, second_score = sorted_scores[1] if len(sorted_scores) > 1 else (None, 0.0)
|
| 209 |
+
|
| 210 |
+
lang_score = scores.get("python", 0.0) + scores.get("typescript", 0.0)
|
| 211 |
+
|
| 212 |
+
if has_debug_signal and lang_score == 0.0:
|
| 213 |
+
if previous_expert in ("python", "typescript") and not has_greeting:
|
| 214 |
+
return {
|
| 215 |
+
"expert": previous_expert,
|
| 216 |
+
"method": "history_continuation_debug",
|
| 217 |
+
"scores": scores,
|
| 218 |
+
"is_ambiguous": False,
|
| 219 |
+
}
|
| 220 |
+
return {"expert": None, "method": "none", "scores": scores, "is_ambiguous": True}
|
| 221 |
+
|
| 222 |
+
if top_score == 0.0:
|
| 223 |
+
if previous_expert is not None and not has_greeting:
|
| 224 |
+
return {
|
| 225 |
+
"expert": previous_expert,
|
| 226 |
+
"method": "history_continuation",
|
| 227 |
+
"scores": scores,
|
| 228 |
+
"is_ambiguous": False,
|
| 229 |
+
}
|
| 230 |
+
return {"expert": None, "method": "none", "scores": scores, "is_ambiguous": True}
|
| 231 |
+
|
| 232 |
+
diff_ratio = (top_score - second_score) / max(top_score, 1e-6)
|
| 233 |
+
is_ambiguous = diff_ratio < AMBIGUOUS_THRESHOLD
|
| 234 |
+
|
| 235 |
+
if has_output_signal:
|
| 236 |
+
is_ambiguous = True
|
| 237 |
+
|
| 238 |
+
return {
|
| 239 |
+
"expert": top_expert,
|
| 240 |
+
"method": "keyword_match_weighted",
|
| 241 |
+
"scores": scores,
|
| 242 |
+
"is_ambiguous": is_ambiguous,
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# ======================================================================
|
| 247 |
+
# 2) LANGUAGE PURITY FILTER (Emoji-Aware)
|
| 248 |
+
# ======================================================================
|
| 249 |
+
|
| 250 |
+
ALLOWED_PATTERN = re.compile(
|
| 251 |
+
r'[^\u0E00-\u0E7F'
|
| 252 |
+
r'a-zA-Z0-9'
|
| 253 |
+
r'\s'
|
| 254 |
+
r'.,!?;:\'"()\[\]{}<>@#$%^&*_+=\-/\\|`~๐-๙×÷°≈≠±'
|
| 255 |
+
r'\U0001F1E6-\U0001F1FF'
|
| 256 |
+
r'\U0001F300-\U0001F5FF'
|
| 257 |
+
r'\U0001F600-\U0001F64F'
|
| 258 |
+
r'\U0001F680-\U0001F6FF'
|
| 259 |
+
r'\U0001F700-\U0001F77F'
|
| 260 |
+
r'\U0001F780-\U0001F7FF'
|
| 261 |
+
r'\U0001F800-\U0001F8FF'
|
| 262 |
+
r'\U0001F900-\U0001F9FF'
|
| 263 |
+
r'\U0001FA00-\U0001FA6F'
|
| 264 |
+
r'\U0001FA70-\U0001FAFF'
|
| 265 |
+
r'\u2600-\u26FF'
|
| 266 |
+
r'\u2700-\u27BF'
|
| 267 |
+
r'\uFE0F'
|
| 268 |
+
r'\u200D]'
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def find_foreign_chars(text: str) -> list:
|
| 273 |
+
return ALLOWED_PATTERN.findall(text)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def detect_language_leakage_words(text: str) -> list:
|
| 277 |
+
words = re.findall(r'\S+', text)
|
| 278 |
+
suspicious_words = []
|
| 279 |
+
for word in words:
|
| 280 |
+
foreign = find_foreign_chars(word)
|
| 281 |
+
if foreign:
|
| 282 |
+
suspicious_words.append({"word": word, "foreign_chars": foreign})
|
| 283 |
+
return suspicious_words
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def clean_or_flag_response(text: str, auto_retry_callback=None) -> dict:
|
| 287 |
+
suspicious = detect_language_leakage_words(text)
|
| 288 |
+
|
| 289 |
+
if not suspicious:
|
| 290 |
+
return {"text": text, "is_clean": True, "flagged_words": []}
|
| 291 |
+
|
| 292 |
+
print(f"[WARNING] พบคำที่อาจเป็น language leakage: {suspicious}")
|
| 293 |
+
|
| 294 |
+
if auto_retry_callback is not None:
|
| 295 |
+
print("[INFO] กำลัง retry เพื่อแก้ language leakage...")
|
| 296 |
+
new_text = auto_retry_callback()
|
| 297 |
+
recheck = detect_language_leakage_words(new_text)
|
| 298 |
+
if not recheck:
|
| 299 |
+
return {"text": new_text, "is_clean": True, "flagged_words": []}
|
| 300 |
+
print("[WARNING] Retry แล้วยังพบ leakage อยู่ — คืนคำตอบเดิมพร้อม flag")
|
| 301 |
+
|
| 302 |
+
return {"text": text, "is_clean": False, "flagged_words": suspicious}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# ======================================================================
|
| 306 |
+
# 3) HEDGE-PHRASE LIMITER
|
| 307 |
+
# ======================================================================
|
| 308 |
+
|
| 309 |
+
HEDGE_PHRASE_PATTERN = re.compile(r'ผม(?:คิด|เชื่อ|เข้าใจ)?ว่า')
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def limit_hedge_phrases(text: str, max_allowed: int = 1) -> str:
|
| 313 |
+
matches = list(HEDGE_PHRASE_PATTERN.finditer(text))
|
| 314 |
+
if len(matches) <= max_allowed:
|
| 315 |
+
return text
|
| 316 |
+
|
| 317 |
+
result_parts = []
|
| 318 |
+
last_end = 0
|
| 319 |
+
count = 0
|
| 320 |
+
for m in matches:
|
| 321 |
+
if count < max_allowed:
|
| 322 |
+
result_parts.append(text[last_end:m.end()])
|
| 323 |
+
else:
|
| 324 |
+
result_parts.append(text[last_end:m.start()])
|
| 325 |
+
last_end = m.end()
|
| 326 |
+
count += 1
|
| 327 |
+
result_parts.append(text[last_end:])
|
| 328 |
+
|
| 329 |
+
cleaned = ''.join(result_parts)
|
| 330 |
+
cleaned = re.sub(r'\s{2,}', ' ', cleaned)
|
| 331 |
+
cleaned = re.sub(r'\s+([.,!?ครับค่ะ])', r'\1', cleaned)
|
| 332 |
+
cleaned = re.sub(r',\s*,', ',', cleaned)
|
| 333 |
+
return cleaned.strip()
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# ======================================================================
|
| 337 |
+
# 4) REFUSAL DETECTORS
|
| 338 |
+
# ======================================================================
|
| 339 |
+
|
| 340 |
+
REFUSAL_PATTERNS = [
|
| 341 |
+
"ไม่สามารถแปล",
|
| 342 |
+
"ไม่สามารถที่จะแปล",
|
| 343 |
+
"ยากต่อการเข้าใจถ้าแปล",
|
| 344 |
+
"ขออภัยที่ไม่สามารถ",
|
| 345 |
+
"ยังไม่สามารถแปลเป็นภาษาไทยได้",
|
| 346 |
+
"ไม่ได้เป็นข้อความภาษาไทย",
|
| 347 |
+
"cannot translate",
|
| 348 |
+
]
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def is_refusal(text: str) -> bool:
|
| 352 |
+
return any(p in text for p in REFUSAL_PATTERNS)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
CODE_REFUSAL_PATTERNS = [
|
| 356 |
+
"ไม่สามารถช่วยคุณเขียนโค้ด",
|
| 357 |
+
"ไม่สามารถเขียนโค้ด",
|
| 358 |
+
"ผมไม่สามารถเขียน",
|
| 359 |
+
"ลองเขียนเองนะ",
|
| 360 |
+
"ไม่สามารถช่วยเขียน",
|
| 361 |
+
"ขอแนะนำให้คุณลองเขียนเอง",
|
| 362 |
+
"ไม่สามารถสร้างโค้ด",
|
| 363 |
+
]
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def is_code_refusal(text: str) -> bool:
|
| 367 |
+
return any(p in text for p in CODE_REFUSAL_PATTERNS)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ======================================================================
|
| 371 |
+
# 5) PREAMBLE STRIPPER
|
| 372 |
+
# ======================================================================
|
| 373 |
+
|
| 374 |
+
_FALLBACK_PREAMBLE_PATTERNS = [
|
| 375 |
+
r"^here'?s\s+(the\s+)?translation.*?:\s*",
|
| 376 |
+
r"^translation\s*:\s*",
|
| 377 |
+
r"^คำแปล\s*:\s*",
|
| 378 |
+
r"^แปลว่า\s*:\s*",
|
| 379 |
+
]
|
| 380 |
+
_FALLBACK_PREAMBLE_REGEX = re.compile("|".join(_FALLBACK_PREAMBLE_PATTERNS), flags=re.IGNORECASE)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def _find_first_thai_char_index(text: str) -> Optional[int]:
|
| 384 |
+
for i, ch in enumerate(text):
|
| 385 |
+
if '\u0E00' <= ch <= '\u0E7F':
|
| 386 |
+
return i
|
| 387 |
+
return None
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def strip_translation_preamble(text: str) -> str:
|
| 391 |
+
text = text.strip()
|
| 392 |
+
first_thai_idx = _find_first_thai_char_index(text)
|
| 393 |
+
|
| 394 |
+
if first_thai_idx is not None and first_thai_idx > 0:
|
| 395 |
+
preamble = text[:first_thai_idx]
|
| 396 |
+
if re.search(r'translat', preamble, re.IGNORECASE):
|
| 397 |
+
remainder = text[first_thai_idx:]
|
| 398 |
+
remainder = re.sub(r'^[\s\-:>*]+', '', remainder)
|
| 399 |
+
return remainder.strip()
|
| 400 |
+
|
| 401 |
+
stripped = _FALLBACK_PREAMBLE_REGEX.sub("", text, count=1)
|
| 402 |
+
return stripped.strip()
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
# ======================================================================
|
| 406 |
+
# 6) PERSONA ENFORCER
|
| 407 |
+
# ======================================================================
|
| 408 |
+
|
| 409 |
+
def enforce_persona(text: str) -> str:
|
| 410 |
+
text = text.replace("ดิฉัน", "ผม")
|
| 411 |
+
text = text.replace("ฉัน", "ผม")
|
| 412 |
+
text = text.replace("ค่ะ", "ครับ")
|
| 413 |
+
return text
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
# ======================================================================
|
| 417 |
+
# 7) PER-EXPERT CONFIG
|
| 418 |
+
# ======================================================================
|
| 419 |
+
|
| 420 |
+
@dataclass
|
| 421 |
+
class ExpertConfig:
|
| 422 |
+
name: str
|
| 423 |
+
max_tokens: int = 512
|
| 424 |
+
temp: float = 0.7
|
| 425 |
+
top_p: float = 0.9
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
EXPERT_CONFIGS = {
|
| 429 |
+
"python": ExpertConfig(name="python", max_tokens=800, temp=0.3, top_p=0.9),
|
| 430 |
+
"typescript": ExpertConfig(name="typescript", max_tokens=800, temp=0.3, top_p=0.9),
|
| 431 |
+
"reasoning": ExpertConfig(name="reasoning", max_tokens=1000, temp=0.6, top_p=0.9),
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def get_config(expert_name: str) -> ExpertConfig:
|
| 436 |
+
return EXPERT_CONFIGS.get(expert_name, ExpertConfig(name=expert_name))
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
# ======================================================================
|
| 440 |
+
# 8) PERSONA CONSISTENCY (System Prompt Injection)
|
| 441 |
+
# ======================================================================
|
| 442 |
+
|
| 443 |
+
PERSONA_MALE_SUFFIX = (
|
| 444 |
+
"\n\nกฎการตอบ: ให้ใช้คำลงท้าย ครับ และแทนตัวเองว่า ผม "
|
| 445 |
+
"อย่างสม่ำเสมอตลอดคำตอบ ห้ามใช้ ค่ะ หรือ ฉัน เด็ดขาด"
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def inject_persona(system_prompt: str = "") -> str:
|
| 450 |
+
return system_prompt + PERSONA_MALE_SUFFIX
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
# ======================================================================
|
| 454 |
+
# SELF-TEST
|
| 455 |
+
# ======================================================================
|
| 456 |
+
|
| 457 |
+
if __name__ == "__main__":
|
| 458 |
+
print("=" * 70)
|
| 459 |
+
print("TEST: is_degenerate_query (Round 7 — Fast-Path Guard)")
|
| 460 |
+
print("=" * 70)
|
| 461 |
+
degenerate_cases = ["", " ", "\n\n\n", "🐍🐍🐍", "???!!!...", "?", "{{}} }}"]
|
| 462 |
+
for q in degenerate_cases:
|
| 463 |
+
result = is_degenerate_query(q)
|
| 464 |
+
print(f"Query: {q!r} -> is_degenerate={result}")
|
| 465 |
+
assert result is True, f"❌ FAIL: {q!r} ควรถูก Flag เป็น Degenerate!"
|
| 466 |
+
print("✅ PASS: Query ว่าง/Emoji-only/Symbol-only ถูก Flag ถูกต้องหมด\n")
|
| 467 |
+
|
| 468 |
+
meaningful_cases = ["help", "ทำไง", "def foo(): pass", "เขียน Python function ที่บวกเลขสองตัว"]
|
| 469 |
+
for q in meaningful_cases:
|
| 470 |
+
result = is_degenerate_query(q)
|
| 471 |
+
print(f"Query: {q!r} -> is_degenerate={result}")
|
| 472 |
+
assert result is False, f"❌ FAIL: {q!r} มีเนื้อหาจริง ไม่ควรถูก Flag!"
|
| 473 |
+
print("✅ PASS: Query ที่มีเนื้อหาจริงไม่ถูก Flag ผิดเลย\n")
|
| 474 |
+
|
| 475 |
+
print("=" * 70)
|
| 476 |
+
print("TEST: get_greeting_quick_reply (Round 8)")
|
| 477 |
+
print("=" * 70)
|
| 478 |
+
greeting_true_cases = ["สวัสดี", "สวัสดีครับ", "หวัดดี", "hi", "hello"]
|
| 479 |
+
for q in greeting_true_cases:
|
| 480 |
+
result = get_greeting_quick_reply(q)
|
| 481 |
+
assert result == GREETING_QUICK_REPLY_TEXT, f"❌ FAIL: {q!r} ควรได้ Greeting Reply!"
|
| 482 |
+
print("✅ PASS: Greeting คำล้วนๆ ได้ Quick-Reply ถูกต้อง")
|
| 483 |
+
|
| 484 |
+
closing_true_cases = ["บาย", "bye", "ลาก่อน"]
|
| 485 |
+
for q in closing_true_cases:
|
| 486 |
+
result = get_greeting_quick_reply(q)
|
| 487 |
+
assert result == CLOSING_QUICK_REPLY_TEXT, f"❌ FAIL: {q!r} ควรได้ Closing Reply!"
|
| 488 |
+
print("✅ PASS: Closing คำล้วนๆ ได้ Quick-Reply ถูกต้อง")
|
| 489 |
+
|
| 490 |
+
thanks_true_cases = ["ขอบคุณ", "ขอบคุณครับ", "ขอบคุณมากครับ", "thanks"]
|
| 491 |
+
for q in thanks_true_cases:
|
| 492 |
+
result = get_greeting_quick_reply(q)
|
| 493 |
+
assert result == THANKS_QUICK_REPLY_TEXT, f"❌ FAIL: {q!r} ควรได้ Thanks Reply!"
|
| 494 |
+
print("✅ PASS: Thanks คำล้วนๆ ได้ Quick-Reply ถูกต้อง")
|
| 495 |
+
|
| 496 |
+
false_positive_cases = [
|
| 497 |
+
"อธิบายเพิ่มอีกหน่อย",
|
| 498 |
+
"คุณช่วยอะไรได้บ้าง",
|
| 499 |
+
"ขอบคุณสำหรับ function นี้นะครับ",
|
| 500 |
+
"แก้ให้หน่อย",
|
| 501 |
+
]
|
| 502 |
+
for q in false_positive_cases:
|
| 503 |
+
result = get_greeting_quick_reply(q)
|
| 504 |
+
assert result is None, f"❌ FAIL: {q!r} มีเนื้อหาจริง ไม่ควรถูก Quick-Reply!"
|
| 505 |
+
print("✅ PASS: Query ที่มีเนื้อหาจริงแซมอยู่ไม่ถูก Quick-Reply ผิดเลย\n")
|
| 506 |
+
|
| 507 |
+
print("=" * 70)
|
| 508 |
+
print("TEST: is_repetitive_response (Round 9)")
|
| 509 |
+
print("=" * 70)
|
| 510 |
+
looped_text = "ผมขออภัยที่ทำให้คุณสับสน " * 15
|
| 511 |
+
assert is_repetitive_response(looped_text) is True, "❌ FAIL: ควรจับ Loop ได้!"
|
| 512 |
+
print("✅ PASS: จับ Repetition Loop ภาษาไทยได้ถูกต้อง")
|
| 513 |
+
|
| 514 |
+
looped_list = "ดูข้อมูลจาก Google Maps, " * 20
|
| 515 |
+
assert is_repetitive_response(looped_list) is True, "❌ FAIL: ควรจับ Loop แบบ List ได้!"
|
| 516 |
+
print("✅ PASS: จับ Repetition Loop แบบรายการซ้ำได้ถูกต้อง")
|
| 517 |
+
|
| 518 |
+
normal_code_response = (
|
| 519 |
+
"แน่นอนครับ นี่คือฟังก์ชันที่เพิ่ม docstring:\n\n"
|
| 520 |
+
"```python\ndef add_numbers(a, b):\n"
|
| 521 |
+
' """บวกเลขสองตัวเข้าด้วยกัน"""\n'
|
| 522 |
+
" return a + b\n```\n\n"
|
| 523 |
+
"ใน docstring นี้ ผมอธิบายว่าฟังก์ชันนี้จะบวกเลขสองตัวเข้าด้วยกัน "
|
| 524 |
+
"และระบุประเภทของพารามิเตอร์ที่ใช้ครับ"
|
| 525 |
+
)
|
| 526 |
+
assert is_repetitive_response(normal_code_response) is False, "❌ FAIL: คำตอบปกติไม่ควรถูก Flag!"
|
| 527 |
+
print("✅ PASS: คำตอบปกติที่มีโค้ดซ้ำเล็กน้อยไม่ถูก Flag ผิด\n")
|
| 528 |
+
|
| 529 |
+
print("=" * 70)
|
| 530 |
+
print("TEST: Weighted Router (Stateless)")
|
| 531 |
+
print("=" * 70)
|
| 532 |
+
for q in ["เขียน Python function ที่บวกเลขสองตัว", "def foo(): pass"]:
|
| 533 |
+
result = route_query(q)
|
| 534 |
+
print(f"Query: {q!r} -> {result}\n")
|
| 535 |
+
|
| 536 |
+
print("=" * 70)
|
| 537 |
+
print("TEST: Router — History Continuation")
|
| 538 |
+
print("=" * 70)
|
| 539 |
+
r1 = route_query("แก้ให้หน่อย", previous_expert="python")
|
| 540 |
+
assert r1["expert"] == "python" and r1["method"] == "history_continuation"
|
| 541 |
+
print("✅ PASS: 'แก้ให้หน่อย' ใช้ previous_expert ต่อ\n")
|
| 542 |
+
|
| 543 |
+
r2 = route_query("อธิบายเพิ่มอีกหน่อย", previous_expert="python")
|
| 544 |
+
assert r2["expert"] == "python", "❌ FAIL: ควรใช้ previous_expert ต่อ!"
|
| 545 |
+
print("✅ PASS: 'อธิบายเพิ่มอีกหน่อย' ไม่ถูกเข้าใจผิดว่าเป็นการลาก่อนแล้ว\n")
|
| 546 |
+
|
| 547 |
+
r2b = route_query("อธิบายว่าแก้ตรงไหน", previous_expert="python")
|
| 548 |
+
assert r2b["expert"] == "python", "❌ FAIL: Bug 'บาย' ใน 'อธิบาย' กลับมาอีกแล้ว!"
|
| 549 |
+
print("✅ PASS: 'อธิบายว่าแก้ตรงไหน' ไม่ถูก Flag ผิดเป็น Greeting แล้ว\n")
|
| 550 |
+
|
| 551 |
+
r3 = route_query("แก้ให้หน่อย", previous_expert=None)
|
| 552 |
+
assert r3["expert"] is None
|
| 553 |
+
print("✅ PASS: ไม่มี previous_expert ยังทำงานแบบเดิม\n")
|
| 554 |
+
|
| 555 |
+
r4 = route_query("ขอบคุณครับ", previous_expert="typescript")
|
| 556 |
+
assert r4["expert"] is None, "❌ FAIL: คำขอบคุณจริงๆต้องยัง Flag เป็น Greeting!"
|
| 557 |
+
print("✅ PASS: 'ขอบคุณครับ' ยังถูก Flag เป็น Greeting ถูกต้อง\n")
|
| 558 |
+
|
| 559 |
+
r5 = route_query("บาย", previous_expert="python")
|
| 560 |
+
assert r5["expert"] is None, "❌ FAIL: คำว่า 'บาย' โดดๆต้องยัง Flag เป็น Greeting!"
|
| 561 |
+
print("✅ PASS: 'บาย' คำเดียวโดดๆ ยังถูก Flag เป็น Greeting ถูกต้อง\n")
|
| 562 |
+
|
| 563 |
+
print("=" * 70)
|
| 564 |
+
print("TEST: is_code_refusal")
|
| 565 |
+
print("=" * 70)
|
| 566 |
+
assert is_code_refusal("ขออภัยครับ ผมไม่สามารถช่วยคุณเขียนโค้ด Python ที่บวกเลขสองตัวได้ครับ") is True
|
| 567 |
+
assert is_code_refusal("```python\ndef add(a, b):\n return a + b\n```\nครับ") is False
|
| 568 |
+
print("✅ PASS: Code Refusal Detector ทำงานถูกต้อง\n")
|
| 569 |
+
|
| 570 |
+
print("=" * 70)
|
| 571 |
+
print("TEST: Language purity filter — Emoji")
|
| 572 |
+
print("=" * 70)
|
| 573 |
+
result_emoji = clean_or_flag_response("ผมไม่สามารถทำให้🐍🐍🐍เป็นคำว่า python ได้ครับ 😊👍")
|
| 574 |
+
assert result_emoji["is_clean"] is True
|
| 575 |
+
print("✅ PASS: Emoji ไม่ถูก Flag ผิด\n")
|
| 576 |
+
|
| 577 |
+
print("=" * 70)
|
| 578 |
+
print("TEST: Persona enforcer")
|
| 579 |
+
print("=" * 70)
|
| 580 |
+
fixed = enforce_persona("ขออภัยค่ะ ฉันไม่เข้าใจคำถามนี้ค่ะ")
|
| 581 |
+
assert "ค่ะ" not in fixed and "ฉัน" not in fixed
|
| 582 |
+
print("✅ PASS\n")
|
| 583 |
+
|
| 584 |
+
print("=" * 70)
|
| 585 |
+
print("🎉 SELF-TEST ทั้งหมดผ่านเรียบร้อย")
|
| 586 |
+
print("=" * 70)
|