|
|
|
|
|
import os |
|
|
import json |
|
|
from typing import Dict, List, Optional |
|
|
|
|
|
from .text_utils import normalize_question |
|
|
|
|
|
MANUAL_QA_PATH = "data/manual_qa.jsonl" |
|
|
|
|
|
|
|
|
MANUAL_ANSWERS: Dict[str, str] = {} |
|
|
|
|
|
|
|
|
|
|
|
MANUAL_LIST: List[Dict[str, str]] = [] |
|
|
|
|
|
|
|
|
_FALLBACK_MANUAL_QA: List[Dict[str, str]] = [ |
|
|
{ |
|
|
"q": "ປະຫວັດສາດມີຄວາມສໍາຄັນຈັ່ງໃດ໋?", |
|
|
"a": ( |
|
|
"ປະຫວັດສາດມີຄວາມສໍາຄັນເພາະຊ່ວຍໃຫ້ເຮົາຮູ້ປະຫວັດຄວາມເປັນມາຂອງຊາດ " |
|
|
"ຂອງບັນພະບຸລຸດ ແລະຂອງສັງຄົມມະນຸດ. ມັນເຮັດໃຫ້ເຮົ້າມີຄວາມພາກພູມໃຈ " |
|
|
"ຮູ້ບຸນຄຸນປູ່ຍ່າຕາຍາຍ ແລະຮູ້ຫນ້າທີ່ຂອງຕົນເອງໃນການຮັກສາແລະພັດທະນາປະເທດຊາດ." |
|
|
), |
|
|
}, |
|
|
{ |
|
|
"q": "ປະຫວັດສາດແມ່ນຫຍັງ?", |
|
|
"a": ( |
|
|
"ປະຫວັດສາດແມ່ນວິທະຍາສາດທີ່ສຶກສາເຫດການຕ່າງໆໃນອະດີດຂອງສັງຄົມມະນຸດ " |
|
|
"ຕັ້ງແຕ່ການເກີດຂຶ້ນ ການເຕີບໂຕ ແລະການປ່ຽນແປງຕາມເວລາ." |
|
|
), |
|
|
}, |
|
|
{ |
|
|
"q": "ຫຼັກຖານປະຫວັດສາດມີຫຍັງແດ່?", |
|
|
"a": ( |
|
|
"ຫຼັກຖານປະຫວັດສາດມີ 3 ປະເພດຫຼັກ: ຫຼັກຖານຜ່ານການບອກເລົ່າ, " |
|
|
"ຫຼັກຖານປະເພດວັດຖຸ ແລະເອກະສານປະເພດການບັນທຶກ." |
|
|
), |
|
|
}, |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _add_pair(q: str, a: str) -> str: |
|
|
""" |
|
|
Add/overwrite a (q,a) pair into MANUAL_ANSWERS + MANUAL_LIST. |
|
|
Returns normalized key. |
|
|
""" |
|
|
q_clean = q.strip() |
|
|
a_clean = a.strip() |
|
|
if not q_clean or not a_clean: |
|
|
return "" |
|
|
|
|
|
norm_q = normalize_question(q_clean) |
|
|
|
|
|
|
|
|
MANUAL_ANSWERS[norm_q] = a_clean |
|
|
|
|
|
|
|
|
global MANUAL_LIST |
|
|
MANUAL_LIST = [row for row in MANUAL_LIST if row["norm_q"] != norm_q] |
|
|
MANUAL_LIST.append({"norm_q": norm_q, "q": q_clean, "a": a_clean}) |
|
|
|
|
|
return norm_q |
|
|
|
|
|
|
|
|
def _load_fallback(): |
|
|
for pair in _FALLBACK_MANUAL_QA: |
|
|
q = pair.get("q", "") |
|
|
a = pair.get("a", "") |
|
|
if q and a: |
|
|
_add_pair(q, a) |
|
|
|
|
|
|
|
|
def _load_from_file(): |
|
|
if not os.path.exists(MANUAL_QA_PATH): |
|
|
return |
|
|
|
|
|
with open(MANUAL_QA_PATH, "r", encoding="utf-8") as f: |
|
|
for line in f: |
|
|
line = line.strip() |
|
|
if not line: |
|
|
continue |
|
|
try: |
|
|
obj = json.loads(line) |
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
|
|
|
q = obj.get("q", "") |
|
|
a = obj.get("a", "") |
|
|
if q and a: |
|
|
_add_pair(q, a) |
|
|
|
|
|
|
|
|
def _load_all(): |
|
|
MANUAL_ANSWERS.clear() |
|
|
MANUAL_LIST.clear() |
|
|
_load_fallback() |
|
|
_load_from_file() |
|
|
|
|
|
|
|
|
def _save_to_file(): |
|
|
""" |
|
|
Rewrite manual_qa.jsonl from MANUAL_LIST. |
|
|
(Includes fallback entries once we start saving – that’s OK.) |
|
|
""" |
|
|
os.makedirs(os.path.dirname(MANUAL_QA_PATH), exist_ok=True) |
|
|
with open(MANUAL_QA_PATH, "w", encoding="utf-8") as f: |
|
|
for row in MANUAL_LIST: |
|
|
json.dump({"q": row["q"], "a": row["a"]}, f, ensure_ascii=False) |
|
|
f.write("\n") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_manual_qa(question: str, answer: str) -> str: |
|
|
""" |
|
|
Add new manual Q&A (or overwrite existing same question). |
|
|
Updates in-memory index and rewrites file. |
|
|
Returns normalized key. |
|
|
""" |
|
|
if not question.strip() or not answer.strip(): |
|
|
raise ValueError("Question and answer must not be empty.") |
|
|
|
|
|
norm_q = _add_pair(question, answer) |
|
|
_save_to_file() |
|
|
return norm_q |
|
|
|
|
|
|
|
|
def list_manual_qa() -> List[Dict[str, str]]: |
|
|
""" |
|
|
Return a copy of all manual Q&A rows for displaying in UI. |
|
|
Each row: { 'norm_q', 'q', 'a' }. |
|
|
""" |
|
|
return list(MANUAL_LIST) |
|
|
|
|
|
|
|
|
def get_manual_qa(norm_q: str) -> Optional[Dict[str, str]]: |
|
|
""" |
|
|
Look up one Q&A by normalized key (for loading into edit boxes). |
|
|
""" |
|
|
for row in MANUAL_LIST: |
|
|
if row["norm_q"] == norm_q: |
|
|
return row |
|
|
return None |
|
|
|
|
|
|
|
|
def update_manual_qa(old_norm_q: str, new_q: str, new_a: str) -> str: |
|
|
""" |
|
|
Update an existing entry identified by old_norm_q. |
|
|
Allows changing question text (so key may change). |
|
|
Returns the new normalized key. |
|
|
""" |
|
|
if not old_norm_q: |
|
|
raise ValueError("No entry selected for update.") |
|
|
if not new_q.strip() or not new_a.strip(): |
|
|
raise ValueError("Question and answer must not be empty.") |
|
|
|
|
|
|
|
|
global MANUAL_LIST |
|
|
MANUAL_LIST = [row for row in MANUAL_LIST if row["norm_q"] != old_norm_q] |
|
|
if old_norm_q in MANUAL_ANSWERS: |
|
|
MANUAL_ANSWERS.pop(old_norm_q, None) |
|
|
|
|
|
|
|
|
norm_q = _add_pair(new_q, new_a) |
|
|
_save_to_file() |
|
|
return norm_q |
|
|
|
|
|
|
|
|
def delete_manual_qa(norm_q: str) -> None: |
|
|
""" |
|
|
Delete an entry by normalized key. |
|
|
""" |
|
|
if not norm_q: |
|
|
raise ValueError("No entry selected for deletion.") |
|
|
|
|
|
global MANUAL_LIST |
|
|
before = len(MANUAL_LIST) |
|
|
MANUAL_LIST = [row for row in MANUAL_LIST if row["norm_q"] != norm_q] |
|
|
|
|
|
if len(MANUAL_LIST) == before: |
|
|
raise ValueError("Entry not found.") |
|
|
|
|
|
if norm_q in MANUAL_ANSWERS: |
|
|
MANUAL_ANSWERS.pop(norm_q, None) |
|
|
|
|
|
_save_to_file() |
|
|
|
|
|
|
|
|
|
|
|
_load_all() |
|
|
|