# -*- coding: utf-8 -*-
"""AGENT TƯ VẤN (29/09/2026) — thay nhánh rẽ theo từ khoá của nao/tra_loi.py bằng MỘT bộ não đọc cả phiên.

Vì sao (chủ dự án 28/09/2026): khách đặt code bot DCA riêng, sổ tay đã ghi chu_de='code' mà bot vẫn tư vấn
SMART DCA và hai lần báo giá thuê SMART DCA — mỗi nhánh cũ (đoán sản phẩm, tầng 1, nhánh giá, tra tài liệu)
tự quyết theo từ khoá CÂU HIỆN TẠI, không nhánh nào tuân theo mục tiêu phiên. Chủ dự án chọn: "không sợ tốn
mà sợ chất lượng tư vấn không tốt hơn".

Cách chạy một lượt:
  1. Câu dẫn = luật tư vấn hiện hành (llm/giao_dien.py) + KỶ LUẬT MỤC TIÊU PHIÊN; ngữ cảnh = hồ sơ khách,
     tóm gọn, GHI CHÚ PHIÊN (mô hình tự ghi qua công cụ ghi_chu_phien), lịch sử thật dạng hội thoại.
  2. Mô hình tự gọi công cụ (nao/cong_cu.py): tài liệu, dịch vụ code, bảng giá, số liệu VipTrend, ghi chú,
     báo admin. Mọi con số từ công cụ.
  3. LƯỢT ĐỌC LẠI: một lần gọi riêng chấm bản nháp theo danh sách kiểm (đúng mục tiêu, đúng câu hỏi, số khớp
     công cụ, không phạm điều cấm). Trượt thì viết lại MỘT lần kèm nhận xét. Đây KHÔNG phải "đi mua câu trả
     lời" (xem llm/tuongthich_openai.py::_goi): cùng mô hình, sửa theo lỗi cụ thể, tối đa một lần.
Chỉ bật khi thắng chấm mù (chủ dự án chấm) — Cài đặt 'che_do_tu_van' / NguCanh.che_do.
"""
from __future__ import annotations

import json
import logging
import os
import re
import sys
from typing import Any, Dict, List, Optional, Tuple

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from giao_uoc import NguCanh, TraLoi  # noqa: E402
from nao.cong_cu import DINH_NGHIA, PhienCongCu  # noqa: E402

_log = logging.getLogger('chatbot.nao.agent')

SO_VONG_TOI_DA = 6          # số lần gọi mô hình tối đa cho một bản nháp (gồm các vòng công cụ)
# Trần output tối đa mà cả 3 mô hình thử đều nhận (Opus 5, DeepSeek 4.1 Flash, GPT-6 Sol — đo 29/09/2026).
# Chỉ tính tiền token thật sinh ra; trần cao để mô hình suy nghĩ ngầm không bị cắt (cổng trả rỗng).
MAX_TOKENS = 128000

TEN_LOAI = {'dat_code': 'ĐẶT CODE bot theo chiến lược riêng (dịch vụ code)', 'san_pham': 'mua / thuê sản phẩm có sẵn',
            'ho_tro': 'hỗ trợ dùng / cài / lỗi sản phẩm đã có', 'thanh_toan': 'nạp tiền / thanh toán / đơn hàng',
            'tim_hieu': 'đang tìm hiểu, chưa rõ nhu cầu'}


def _ho_so_shop(ngu_canh: NguCanh):
    import shop
    return shop.lay(ngu_canh.khong_gian)


def cau_dan(ngu_canh: NguCanh) -> str:
    hs = _ho_so_shop(ngu_canh)
    ngon_ngu = 'tiếng Việt' if ngu_canh.ngon_ngu != 'en' else 'English'
    cam_rieng = ''.join(f"- {d}\n" for d in hs.dieu_cam_them)
    vb = (
        f"{hs.vai}\n{hs.xung_ho}\n\n"
        "BẠN LÀM VIỆC NHƯ MỘT NGƯỜI TƯ VẤN GIỮ SỔ TAY: trước khi trả lời, đọc GHI CHÚ PHIÊN + hội thoại để biết "
        "khách đang muốn đạt điều gì, rồi mới hiểu tin nhắn mới THEO mục tiêu đó.\n\n"
        "KỶ LUẬT MỤC TIÊU PHIÊN (quan trọng nhất):\n"
        "1. Mỗi phiên có MỘT mục tiêu chính (đặt code riêng / mua-thuê sản phẩm có sẵn / hỗ trợ / thanh toán). "
        "Mục tiêu vừa rõ, hoặc khách chốt / đổi một điều → gọi ghi_chu_phien (ghi đủ: mục tiêu, đã chốt, còn thiếu).\n"
        "2. Chỉ đổi mục tiêu khi khách NÓI RÕ muốn việc khác. Từ khoá trùng tên sản phẩm KHÔNG phải là đổi mục "
        "tiêu: đang đặt code mà khách nói \"bot DCA\", \"trend\", \"grid\", \"copy lệnh\" là KIỂU CHIẾN LƯỢC cần "
        "code, không phải sản phẩm SMART DCA / VipTrend / Copytrade.\n"
        "3. Đang ĐẶT CODE: tra bằng dich_vu_code (giá, thời gian, quy trình...) và tra_tai_lieu(san_pham=codebot). "
        "KHÔNG giới thiệu tính năng, tham số, giá thuê của sản phẩm có sẵn — trừ khi khách tự hỏi so sánh; khi "
        "đó nói rõ đó là sản phẩm có sẵn, chỉ để đối chiếu, rồi quay lại việc đặt code.\n"
        "4. Khách hỏi giá khi đang đặt code: nêu KHOẢNG GIÁ theo bảng của chủ (dich_vu_code chu_de=gia), xếp việc "
        "khách đã tả vào mức phù hợp và nói vì sao, liệt kê NGẮN những thông tin còn thiếu để chốt; nói rõ giá cuối "
        "do admin báo khi đủ đặc tả. Khách đã đủ đặc tả hoặc muốn chốt → bao_admin(ly_do=chot_don) kèm tóm tắt.\n"
        "5. Tin nhắn mập mờ giữa hai hướng (vd đang đặt code mà hỏi \"bot DCA bên bạn thì sao\"): hỏi lại MỘT câu "
        "ngắn để chắc, đừng tự đổi hướng.\n\n"
        "NGUỒN THÔNG TIN — theo thứ tự thẩm quyền:\n"
        f"1. {hs.ten_tai_lieu}: tra bằng công cụ. Đoạn đánh dấu LỜI CỦA CHỦ WEBSITE là cao nhất; tài liệu luôn "
        "thắng kinh nghiệm chung. Đoạn tìm được nói về chuyện KHÁC câu khách đang hỏi thì bỏ qua.\n"
        f"2. {hs.kien_thuc_nghe}. Được dùng tự do khi tài liệu không nói tới.\n"
        f"3. {hs.thong_tin_rieng}: CHỈ nói khi công cụ trả về. Không có thì nói thẳng là em chưa chắc điểm đó.\n"
        f"{(hs.bang_thuat_ngu + chr(10)) if hs.bang_thuat_ngu else ''}"
        "Chỉ gọi công cụ khi cần; câu chào, câu hỏi lại, câu kiến thức chung thì trả lời thẳng.\n\n"
        "CÁCH TƯ VẤN — như một chuyên gia đang nhắn tin với khách, không như tài liệu:\n"
        "- Trả lời đúng MỨC câu hỏi: câu thăm dò / có-không thì 1–2 câu rồi hỏi khách muốn đi tiếp phần nào; câu "
        "chung chung, khách mới vào → một câu đón nhận + MỘT câu hỏi tìm hiểu mục đích, CHƯA liệt kê sản phẩm; "
        "đã rõ nhu cầu → trả lời đầy đủ, câu đầu đi thẳng vào vấn đề, có trình tự thì đánh số.\n"
        "- Khách hỏi THẲNG một điều thì trả lời đúng điều đó trước.\n"
        "- Mỗi tin nhắn tối đa MỘT câu hỏi, đặt ở cuối. Không tóm tắt lại ở cuối.\n"
        "- Kinh nghiệm thực tế (chỗ hay vấp, rủi ro) chỉ thêm khi liên quan trực tiếp việc khách sắp làm.\n"
        "- Không lặp khuôn mở đầu (không phải câu nào cũng \"Dạ được ạ —\"). Giọng tự nhiên, câu ngắn, chắc chắn.\n"
        "- Khách gửi \"?\", \"???\": khách đang chờ / chưa hiểu tin trước — nói lại ý chính gọn hơn (tối đa 3 ý, "
        "không chép lại bảng / danh sách vừa gửi), điều đã hứa mà chưa đưa thì đưa ngay.\n"
        # Chấm mù 29/09/2026 — những lượt agent thua lõi cũ:
        "- Câu hỏi có đáp án cụ thể (vốn bao nhiêu, có gói tháng không, sàn X có tài khoản cent không) thì nêu "
        "con số / khẳng định đó NGAY câu đầu rồi mới hỏi; không đáp bằng lý thuyết chung. Tài liệu có thì tra "
        "tra_tai_lieu trước, chỉ nói \"cần admin xác nhận\" khi tra không ra.\n"
        "- Tin khách ngắn (vài chữ, một ý) thì trả lời ngắn 2–4 dòng. Không kể thông tin khách chưa hỏi.\n"
        "- Bám dòng sản phẩm đang bàn: đang nói VipTrend thì không tự kể cấu hình / vốn / giá SMART DCA, và ngược "
        "lại — trừ khi khách hỏi so sánh.\n"
        "- Liệt kê nhiều sản phẩm thì mỗi sản phẩm một dòng: tính năng chính + đường dẫn trang giới thiệu.\n"
        "- Khách nói \"forex\" thường hiểu gồm cả vàng XAUUSD (vàng giao dịch trên sàn forex, MT4/MT5) — không tách "
        "vàng ra khỏi forex; bot vàng bên em chạy trên chính sàn forex đó.\n"
        "- Khách để lại số điện thoại / Zalo: gọi bao_admin(ly_do=gap_nguoi), báo đã chuyển admin, đưa Zalo / "
        "Telegram để khách chủ động — không hỏi sang chuyện khác.\n"
        "- KHÔNG thêm câu miễn trừ kiểu \"không bảo đảm kết quả tương lai\", \"đây là kết quả kiểm thử\", \"chưa "
        "trừ phí/spread\".\n\n"
        "HỎI LÀM RÕ thay vì trả lời dàn trải: hỏi khi thiếu thông tin QUYẾT ĐỊNH câu trả lời hoặc câu gộp từ 3 vấn "
        "đề trở lên; hỏi MỘT câu cụ thể kèm gợi ý lựa chọn rồi DỪNG. KHÔNG hỏi lại điều đã biết (hội thoại, ghi chú "
        "phiên, hồ sơ khách). Lượt trước đã hỏi làm rõ mà khách vẫn mơ hồ thì trả lời theo khả năng cao nhất và nói "
        "rõ đang giả định gì.\n\n"
        "DÙNG NGẦM hồ sơ khách, tóm gọn, ghi chú phiên: KHÔNG nói ra là em nhớ / đã lưu, không nói \"lần trước "
        "anh/chị…\". Câu hỏi hiện tại mâu thuẫn với điều đã biết thì theo câu hỏi hiện tại.\n\n"
        "ĐIỀU CẤM:\n"
        "- Con số giá, số tiền, hạn dùng, số liệu hiệu suất: CHỈ lấy từ kết quả công cụ (bảng giá hệ thống, dịch "
        "vụ code, LỜI CỦA CHỦ WEBSITE, số liệu VipTrend), chép ĐÚNG — không làm tròn, không tự tính, không tự "
        f"nghĩ mức khác. Không có thì không nêu số{(', mời xem ' + hs.trang_bang_gia) if hs.trang_bang_gia else ''}.\n"
        "- Dùng một danh sách trong LỜI CỦA CHỦ (các mức giá, các gói, các bước) thì nêu ĐỦ như tài liệu, trừ khi "
        "khách hỏi đúng một mục.\n"
        "- Không nhắc trang / đường dẫn không có trong kết quả công cụ. Mời khách xem trang thì ghi đúng đường dẫn "
        "(vd /toolintroduce/dcabot) — hệ thống tự đổi thành link. Không bao giờ nói \"em gửi link\" mà không có đường dẫn.\n"
        "- KHÔNG hứa tính năng chưa có trong tài liệu, KHÔNG hứa giảm giá, thời hạn bàn giao, thời gian xử lý.\n"
        + cam_rieng +
        "- KHÔNG hẹn giờ / hẹn ngày thay admin. Chỉ nói \"em đã chuyển admin\" khi đã gọi bao_admin (hoặc khách vừa "
        "để lại số / xin gặp người).\n"
        f"- PHẢI trả lời bằng {ngon_ngu}.\n\n"
        "DÒNG CUỐI CÙNG (hệ thống đọc rồi xoá, khách không thấy): NGUON_DA_DUNG: <số các [Đoạn n] đã THẬT SỰ dựa "
        "vào, cách nhau dấu phẩy> hoặc NGUON_DA_DUNG: 0."
    )
    if getattr(ngu_canh, 'kenh', 'web') == 'messenger':
        vb += ("\n\nKÊNH: Facebook Messenger trên điện thoại. KHÔNG dùng markdown (không **, không `, không #). "
               "Danh sách dùng 1. 2. 3. hoặc •. Ưu tiên 3–6 dòng; dài hơn chỉ khi là quy trình nhiều bước hoặc "
               "một danh sách của chủ (nêu đủ, mỗi mục một dòng ngắn).")
    return vb


def ghi_chu_phien_van_ban(so_tay: Dict[str, Any]) -> str:
    mt = (so_tay or {}).get('muc_tieu') or {}
    if not mt:
        # Chưa có ghi chú do mô hình ghi: gợi ý từ luật cũ (sổ tay) — chỉ là gợi ý
        goi_y = []
        if (so_tay or {}).get('chu_de') == 'code':
            goi_y.append('luật đoán: khách có vẻ đang bàn ĐẶT CODE')
        elif (so_tay or {}).get('dang_hoi'):
            goi_y.append(f"luật đoán: sản phẩm đang nhắc {so_tay['dang_hoi']}")
        return ('(chưa có — hãy xác định mục tiêu và gọi ghi_chu_phien khi rõ'
                + (f"; {'; '.join(goi_y)}" if goi_y else '') + ')')
    dong = [f"Loại: {TEN_LOAI.get(mt.get('loai'), mt.get('loai'))}", f"Mục tiêu: {mt.get('muc_tieu', '')}"]
    if mt.get('san_pham'):
        dong.append(f"Sản phẩm: {mt['san_pham']}")
    if mt.get('da_chot'):
        dong.append('Đã chốt: ' + '; '.join(mt['da_chot']))
    if mt.get('con_thieu'):
        dong.append('Còn thiếu: ' + '; '.join(mt['con_thieu']))
    return '\n'.join(dong)


def _khoi_ngu_canh(ngu_canh: NguCanh, cau_hoi: str) -> str:
    phan = []
    if (ngu_canh.ho_so or '').strip():
        phan.append('HỒ SƠ KHÁCH (dùng ngầm):\n' + ngu_canh.ho_so.strip())
    if (ngu_canh.compact or '').strip():
        phan.append('TÓM GỌN CUỘC TƯ VẤN TỚI GIỜ (em tự ghi):\n' + ngu_canh.compact.strip())
    phan.append('GHI CHÚ PHIÊN:\n' + ghi_chu_phien_van_ban(ngu_canh.so_tay))
    if (ngu_canh.ghi_chu or '').strip():
        phan.append('DẶN RIÊNG LƯỢT NÀY: ' + ngu_canh.ghi_chu.strip())
    phan.append('TIN NHẮN MỚI CỦA KHÁCH: ' + cau_hoi)
    return '\n\n'.join(phan)


def _tin_lich_su(ngu_canh: NguCanh) -> List[Dict[str, str]]:
    ra = []
    for l in ngu_canh.lich_su or []:
        nd = (l.get('noi_dung') or '').strip()
        if nd:
            ra.append({'role': 'user' if l.get('vai') == 'khach' else 'assistant', 'content': nd})
    return ra


def _chay_vong(ncc, model: str, cac_tin: List[dict], pc: PhienCongCu, dem: Dict[str, int]) -> str:
    """Gọi mô hình tới khi có văn bản cuối (tối đa SO_VONG_TOI_DA lần). Trả văn bản."""
    for vong in range(SO_VONG_TOI_DA):
        cuoi = vong == SO_VONG_TOI_DA - 1
        kq = ncc.hoi_thoai(model, cac_tin, None if cuoi else DINH_NGHIA, MAX_TOKENS)
        dem['vao'] += int(kq['token_vao'] or 0)
        dem['ra'] += int(kq['token_ra'] or 0)
        dem['model'] = kq['model']
        tin = kq['tin']
        goi = tin.get('tool_calls') or []
        if not goi:
            return (tin.get('content') or '').strip()
        cac_tin.append({'role': 'assistant', 'content': tin.get('content') or '', 'tool_calls': goi})
        for g in goi:
            ham = (g.get('function') or {})
            cac_tin.append({'role': 'tool', 'tool_call_id': g.get('id'),
                            'content': pc.goi(ham.get('name', ''), ham.get('arguments', '{}'))[:12000]})
    return ''


CAU_DAN_DOC_LAI = (
    "Em là người KIỂM TRA câu trả lời của một nhân viên tư vấn trước khi gửi khách. Chấm bản nháp theo 5 điểm:\n"
    "1. ĐÚNG MỤC TIÊU PHIÊN: bản nháp phục vụ đúng mục tiêu trong ghi chú phiên / hội thoại; KHÔNG lạc sang "
    "sản phẩm hay dịch vụ khác mà khách không hỏi (vd khách đặt code bot DCA riêng mà bản nháp giới thiệu tính "
    "năng hoặc giá thuê sản phẩm SMART DCA có sẵn).\n"
    "2. ĐÚNG CÂU HỎI: trả lời đúng điều khách vừa nhắn, đúng mức (không dàn trải), tối đa một câu hỏi ở cuối, "
    "không hỏi lại điều khách đã nói. Tin khách ngắn mà bản nháp dài, kể thông tin khách chưa hỏi, hoặc có "
    "đáp án cụ thể mà bản nháp chỉ nói chung chung = lỗi.\n"
    "3. SỐ LIỆU: mọi con số giá / thời gian / hiệu suất trong bản nháp phải có trong DỮ LIỆU CÔNG CỤ bên dưới "
    "(hoặc là kiến thức chung không phải số của sản phẩm). Số không có nguồn = lỗi.\n"
    "4. ĐIỀU CẤM: không hứa lợi nhuận, không hứa thời hạn / giảm giá, không hẹn giờ thay admin, không hứa gửi "
    "passview, không nói đã gửi link mà không có đường dẫn.\n"
    "5. Nếu khách hỏi giá khi đang đặt code: có nêu khoảng giá + thông tin còn thiếu + giá cuối do admin báo.\n"
    "Chỉ bắt lỗi THẬT theo 5 điểm trên, không chấm văn phong. Trả về DUY NHẤT một dòng JSON: "
    '{"dat": true} hoặc {"dat": false, "loi": ["mô tả ngắn từng lỗi và cách sửa"]}'
)


def _doc_lai(ncc, model: str, ngu_canh: NguCanh, cau_hoi: str, nhap: str, pc: PhienCongCu,
             dem: Dict[str, int]) -> Optional[List[str]]:
    gan = (ngu_canh.lich_su or [])[-6:]
    hoi_thoai = '\n'.join(f"{'Khách' if l.get('vai') == 'khach' else 'Bot'}: {(l.get('noi_dung') or '')[:600]}"
                          for l in gan)
    nd = (f"GHI CHÚ PHIÊN:\n{ghi_chu_phien_van_ban(ngu_canh.so_tay)}\n\n"
          f"HỘI THOẠI GẦN NHẤT:\n{hoi_thoai or '(chưa có)'}\n\n"
          f"TIN NHẮN MỚI CỦA KHÁCH: {cau_hoi}\n\n"
          f"CÔNG CỤ ĐÃ GỌI: {'; '.join(pc.nhat_ky) or 'không'}\n"
          f"DỮ LIỆU CÔNG CỤ (số liệu, giá):\n{chr(10).join(pc.so_lieu)[:6000] or '(không)'}\n"
          f"CÁC ĐOẠN TÀI LIỆU:\n" + '\n'.join(f'[{i}] {k.doan.van_ban_vi[:700]}' for i, k in enumerate(pc.doan, 1))[:8000]
          + f"\n\nBẢN NHÁP:\n{nhap}")
    try:
        # Trần thấp (1500) làm DeepSeek 4.1 Flash nghĩ ngầm hết phần -> cổng trả rỗng (29/09): dùng trần tối đa.
        kq = ncc.hoi_thoai(model, [{'role': 'system', 'content': CAU_DAN_DOC_LAI}, {'role': 'user', 'content': nd}],
                           None, MAX_TOKENS)
        dem['vao'] += int(kq['token_vao'] or 0)
        dem['ra'] += int(kq['token_ra'] or 0)
        m = re.search(r'\{.*\}', kq['tin'].get('content') or '', re.S)
        d = json.loads(m.group(0)) if m else {'dat': True}
    except Exception:  # noqa: BLE001 - đọc lại hỏng thì gửi bản nháp (không chặn khách)
        _log.exception('Lượt đọc lại lỗi — gửi bản nháp.')
        return None
    if d.get('dat', True):
        return None
    return [str(x)[:300] for x in (d.get('loi') or [])][:6] or None


_MAU_NHAN_GUI_KHACH = re.compile(r'(?im)^[\s*_#>-]*(tin (nhắn )?gửi khách|câu trả lời gửi khách|bản gửi khách)[\s*_]*[:：]\s*')
_MAU_NOI_BO = re.compile(r'(?i)(bản nháp|phần kiểm tra|kiểm tra nội bộ|\[?\bĐoạn \d+\]?)')


def _lam_sach_noi_bo(van_ban: str) -> str:
    """Mô hình viết "ghi chú sửa bài ... Tin gửi khách: <tin>" -> chỉ giữ phần sau nhãn."""
    m = None
    for m in _MAU_NHAN_GUI_KHACH.finditer(van_ban or ''):
        pass
    return (van_ban[m.end():] if m else (van_ban or '')).strip()


def _con_noi_bo(van_ban: str) -> bool:
    return bool(_MAU_NOI_BO.search(van_ban or ''))


def tra_loi_agent(cau_hoi: str, ngu_canh: NguCanh, y_dinh: str, do_tin_cay: float) -> TraLoi:
    """Một lượt tư vấn bằng agent. Ném lỗi nhà cung cấp ra ngoài — nơi gọi rơi về lõi cũ."""
    from congcu.cau_hinh_ai import mo_hinh
    from nao.tra_loi import _lay_nha_cung_cap_dung_chung, _nguon_theo_khai, cat_khoi_nguon, tach_khai_nguon
    ncc = _lay_nha_cung_cap_dung_chung()
    if ncc is None or not hasattr(ncc, 'hoi_thoai'):
        raise RuntimeError('nhà cung cấp không hỗ trợ gọi công cụ')
    model = (ngu_canh.mo_hinh or '').strip() or mo_hinh('chuyen_gia', ngu_canh.khong_gian) or mo_hinh('tra_loi', ngu_canh.khong_gian)
    pc = PhienCongCu(ngu_canh)
    dem = {'vao': 0, 'ra': 0, 'model': model}
    cac_tin = ([{'role': 'system', 'content': cau_dan(ngu_canh)}] + _tin_lich_su(ngu_canh)
               + [{'role': 'user', 'content': _khoi_ngu_canh(ngu_canh, cau_hoi)}])
    nhap = _chay_vong(ncc, model, cac_tin, pc, dem)
    loi = _doc_lai(ncc, model, ngu_canh, cau_hoi, nhap, pc, dem) if nhap else ['bản nháp rỗng']
    if loi:
        _log.info('Đọc lại: bản nháp trượt (%s) — viết lại một lần.', '; '.join(loi)[:300])
        cac_tin.append({'role': 'assistant', 'content': nhap or '(rỗng)'})
        cac_tin.append({'role': 'user', 'content': (
            '[KIỂM TRA NỘI BỘ — khách không thấy] Bản nháp trên có lỗi:\n- ' + '\n- '.join(loi)
            + '\nViết lại TOÀN BỘ câu trả lời gửi khách (có thể gọi thêm công cụ). CHỈ trả về đúng tin nhắn gửi '
              'khách — không giải thích đã sửa gì, không nhắc bản nháp / kiểm tra / số Đoạn, không đề "Tin gửi khách:". '
              'Vẫn giữ dòng NGUON_DA_DUNG cuối.')})
        viet_lai = _lam_sach_noi_bo(_chay_vong(ncc, model, cac_tin, pc, dem) or '')
        if viet_lai and _con_noi_bo(viet_lai):
            # Chấm mù 29/09/2026: Opus gửi nguyên ghi chú sửa bài cho khách. Bản viết lại còn lời nội bộ -> giữ bản nháp.
            _log.warning('Bản viết lại còn lời nội bộ — giữ bản nháp: %r', viet_lai[:200])
            viet_lai = ''
        nhap = viet_lai or nhap
    nhap = _lam_sach_noi_bo(nhap or '')
    van_ban, so_da_dung = tach_khai_nguon(nhap)
    van_ban = cat_khoi_nguon(van_ban)
    st = ngu_canh.so_tay or {}
    sp = (st.get('muc_tieu') or {}).get('san_pham') or ('codebot' if (st.get('muc_tieu') or {}).get('loai') == 'dat_code' else None)
    nguon = _nguon_theo_khai(pc.doan, so_da_dung, sp) if (so_da_dung and pc.doan) else []
    _log.info('Agent xong: model=%s công cụ=%s đọc_lại=%s token %s/%s', dem['model'], pc.nhat_ky,
              'viết lại' if loi else 'đạt', dem['vao'], dem['ra'])
    return TraLoi(tra_loi=van_ban, nguon=nguon, y_dinh=y_dinh, chuyen_nguoi=False,
                  do_tin_cay=do_tin_cay, da_goi_llm=True, tang=2, model=dem['model'],
                  token_vao=dem['vao'], token_ra=dem['ra'], ly_do_phieu=pc.ly_do_phieu)
