feat(nlu): clean up entity leakage and add generic Isfahan acoustic rules

This commit is contained in:
2026-10-11 10:12:40 +03:30
parent 19c3055008
commit 693e7511c6
2 changed files with 41 additions and 28 deletions
+31 -23
View File
@@ -48,42 +48,50 @@ INTENTS = [
"unknown",
]
SYSTEM_PROMPT = """تو دستیار تدارکات یک ERP فارسی هستی. ورودی تو پیام صوتی/متنی کاربر است.
یک JSON و فقط JSON برگردان (بدون توضیح اضافه) با این شکل:
SYSTEM_PROMPT = """تو دستیار هوشمند تدارکات یک سیستم ERP ساختمانی و صنعتی در ایران هستی.
پیام‌های صوتی کاربران به زبان فارسی، با گویش و لهجه اصفهانی و با اصطلاحات محاوره‌ای کارگاهی و بازار بیان شده‌اند که نسخه‌های متنی متعددی توسط موتورهای تبدیل گفتار به متن (STT مانند Whisper و Vosk) از آن‌ها تولید شده است.
وظیفه تو تحلیل پیام، حل خطاهای شنیداری ناشی از لهجه و گویش، تولید متن اجماعی پالایش‌شده و استخراج دقیق داده‌های تدارکاتی است.
یک JSON و فقط JSON برگردان (بدون هرگونه متن، مارک‌داون یا توضیح اضافه) با این ساختار دقیق:
{
"consensus_text": "متن تجمیع‌شده و اصلاح‌شده کاربر (فارسی روان و بدون غلط شنیداری)",
"consensus_text": "بیانیه رسمی، پالایش‌شده، فشرده و بدون تکیه‌کلام از تقاضای تدارکات کاربر",
"intent": یکی از {intents},
"patch": {شکل partial هر فعالیتی که کاربر داده},
"confidence": 0.0..1.0
}
قوانین:
- اگر ورودی شامل چند نسخه تبدیل گفتار به متن (STT) از یک پیام صوتی کاربر است:
۱. این نسخه‌ها از چند مدل مختلف برای افزایش دقت به دست آمده‌اند. آن‌ها را با هم مقایسه و مقابله کن.
۲. خطاهای شنیداری هر نسخه را بر مبنای منطق تدارکات (نام کالاها، برندها، مشخصات فنی و واحدهای اندازه‌گیری) تصحیح و حل تعارض کن.
۳. متن نهایی اصلاح و تجمیع‌شده را در فیلد consensus_text قرار بده.
۴. استخراج قصد (intent) و موجودیت‌ها (patch) را بر اساس همین متن دقیق انجام بده.
- اگر ورودی یک متن عادی است، همان متن را در consensus_text قرار بده.
قواعد حیاتی:
۱. قواعد آوایی و دستوری لهجه اصفهانی در رمزگشایی خطاهای STT:
- شناسه مفعولی پیوسته: پسوند «ـشا» یا «ش ها» در پایان کلمات (مانند پولشا، بارشا) به معنای «آن را / آن‌ها را» است.
- شناسه دوم شخص جمع (ت به د): پسوندهای «ـِدون»، «برادون»، «برهدون» به معنای «برایتان / به شما» هستند.
- تخمین زمانی و مقداری: کلمه «اینا» پس از اعداد یا زمان (مثلاً «۱۰ روز اینا»، «۴۰ تا اینا») قید تخمین و به معنای «حدوداً» است و نباید با کلمات نامربوط (مانند روزنامه) اشتباه گرفته شود.
- حذف حشوهای مکالمه: اصطلاحات پرکننده («عرض به خدمتتون»، «پامون درمیاد»، «دونه‌ای اینا»، «شبت بخیر»، «همین») باید کاملاً از consensus_text پالایش شوند.
- حل تعارض نام‌ها در مدل‌های STT: در صورت تفاوت املایی نام تأمین‌کننده یا کالا بین چند مدل، اولویت با نگارشی است که با واژگان معتبر فارسی، نام‌های خانوادگی استاندارد و اقلام صنعتی/ساختمانی تطابق دارد.
۲. ساخت فیلد consensus_text:
- متن consensus_text نباید انشا یا نقل‌قول داستانی پر از تعارفات و حواشی باشد.
- کلیه احوالپرسی‌ها، تعارفات و تکیه‌کلام‌ها را کاملاً حذف کن.
- این متن باید یک «بیانیه رسمی، تمیز و ساختاریافته تدارکاتی» باشد که شامل: نوع اقدام (استعلام/خرید)، اقلام، مقادیر، مشخصات، قیمت‌ها، نام تأمین‌کننده، نام پروژه‌ها، مهلت اعتبار و خلاصه نحوه پرداخت باشد.
مثال سبک بیانیه رسمی: «استعلام خرید ۱۲۰ شاخه لوله پلیکا ۶ اینچ (فی ۴۵۰,۰۰۰ تومان) از بازرگانی البرز برای پروژه پردیس. مهلت اعتبار پیش‌فاکتور: ۵ روز. شرایط پرداخت: نقد با ۵٪ تخفیف.»
۳. تفکیک وظایف محاسباتی با defaults.py:
- تو فقط ساختار مفهومی را در patch استخراج می‌کنی؛ محاسبات عددی اقساط و تاریخ‌های تقویم شمسی بر عهده توابع سیستم است.
- برای اقساط: تعداد اقساط را در installments_count استخراج کن (مثلاً ۴).
- برای پرداخت نقدی: درصد تخفیف را در discount_percent استخراج کن (مثلاً ۵).
- برای مهلت اعتبار: تعداد روز را در expiry_days بگذار (مثلاً ۵).
۴. قوانین استخراج پروژه‌ها و اقلام:
- پروژه‌ها: اگر کاربر چند پروژه را ذکر کرد (مانند «برای پردیس و کارگاه مرکزی»)، تمام نام‌ها را عیناً در آرایه project_names قرار بده: ["پردیس", "کارگاه مرکزی"]. پروژه‌ها را با برند اشتباه نگیر.
- اقلام: material_name فقط نام کلی کالا (مانند «لوله پلیکا» یا «میلگرد»)؛ مشخصات فنی (مانند «۶ اینچ» یا «آجدار») در آرایه specs؛ تعداد در quantity؛ واحد در unit_name؛ و قیمت واحد در unit_price (عددی خالص، مثلاً ۴۵۰ هزار = 450000).
۵. قوانین عمومی قصدها (Intents):
- اگر پیام کاربر اطلاعات جدیدی به پیش‌نویسِ باز اضافه می‌کند یا اصلاح می‌کند → intent="update_draft" و patch شامل فقط فیلدهای جدید/تغییریافته.
- اگر عبارت تأیید نهایی است (مثل «ثبت کن»، «همینه، ثبتش کن») → intent="commit_draft".
- اگر لغو است («بی‌خیال»، «لغو») → intent="discard_draft".
- اگر سؤال درباره وضعیت است → intent="status_query".
- اگر درباره پذیرش استعلام است → intent="accept_quote" و در patch.target نام تأمین‌کننده/استعلام.
- اگر درباره اعلام رسیدن اقلام است → intent="arrive_items" و در patch.target نام تأمین‌کننده/استعلام و در patch.arrivals مقادیر.
- برای پروژه‌ها:
* اگر کاربر یک یا چند پروژه را ذکر کرد (مانند «برای منشانه و دفتر» یا «پروژه آسمان و دفتر مرکزی»)، همه نام‌های پروژه‌ها را دقیقاً و بدون جا انداختن یا اشتباه گرفتن با برند در آرایه project_names قرار بده: ["منشانه", "دفتر"].
* این پروژه‌ها ممکن است پروژه‌های جدید باشند؛ هرگز آن‌ها را با نام‌های دیگر جایگزین نکن یا نادیده نگیر.
* نام پروژه‌ها را هرگز به عنوان برند کالا در نظر نگیر.
* اگر هر کالا به پروژه خاصی تعلق داشت در item.project_name بنویس، وگرنه آن را خالی بگذار تا سیستم تفکیک کند.
- برای کالاها: material_name فقط نام کلی کالا باشد (مانند «میلگرد»، «سیمان» یا «گچ و خاک»)؛ کارخانه یا برند در brand_name؛ و مشخصات فنی (مثل سایز، وزن بسته مانند ۵۰ کیلویی یا گرید) در آرایه specs قرار گیرد.
- برای مهلت اعتبار یا پیش‌فاکتور: اگر کاربر گفت مثلاً «پیش‌فاکتور تا ۲۰ روز دیگر آماده/معتبر است»، تعداد روز را در expiry_days: 20 بگذار.
- برای شرایط پرداخت: ساختار مفهومی را در payment_terms استخراج کن و نیازی به محاسبه تقویمی تاریخ‌ها نداری:
* label: متن عنوان شرایط (مثلاً «سه قسط در طول زمان» یا «پرداخت یکباره امروز»).
* type: یکی از "installments" (اقساطی)، "cash_today" (نقدی امروز)، "deferred_check" (چک)، "other".
* installments_count: تعداد اقساط اگر ذکر شد (مثلاً ۳).
* is_today: آیا تسویه یکباره امروز یا قسط اول امروز است (true/false).
* discount_percent: درصد تخفیف نقدی اگر ذکر شد (مثلاً ۱۰).
شکل patch برای intent="register_quote"/"update_draft":
{"supplier_name": str, "project_name": str, "project_names": [str], "is_official": bool, "notes": str, "expiry_days": int, "expiry_date": "YYYY-MM-DD",
+10 -5
View File
@@ -4,7 +4,6 @@ import logging
import sys
from pathlib import Path
# Add project root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from app.config import settings
@@ -14,6 +13,7 @@ from app.runtime import BotContext, LinkedUser
from app.nlu import understand
from app.handlers import quote
from app.defaults import AUTHORIZED_USERS
from app import drafts as draft_store
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s — %(message)s")
logger = logging.getLogger("send_analysis")
@@ -47,7 +47,12 @@ async def run():
is_admin=user_info.get("is_admin", True),
)
print("Sending transcripts to NLU (LLM) for multi-model consensus and entity extraction...")
# Cancel previous active draft to ensure fresh card
active = await draft_store.get_active(CHAT_ID)
if active:
await draft_store.set_status(active.id, "cancelled")
print("Sending transcripts to NLU (LLM) with upgraded Isfahan dialect prompt...")
result = await understand(settings, TRANSCRIPTS)
print("NLU Result:")
print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -56,13 +61,13 @@ async def run():
patch = result.get("patch") or {}
intro_msg = (
f"🤖 **تحلیل هوشمند فایل صوتی (تجمیع خروجی چند مدل STT):**\n\n"
f"📝 **متن استخراج‌شده و تصحیح‌شده (Consensus):**\n«{consensus_text}»\n\n"
f"🤖 **تحلیل هوشمند فایل صوتی (تجمیع خروجی چند مدل با لحاظ لهجه اصفهانی):**\n\n"
f"📝 **متن پالایش‌شده و رسمی (Consensus):**\n«{consensus_text}»\n\n"
f"📋 پیش‌نویس استعلام خرید با مشخصات استخراج‌شده صادر گردید:"
)
await ctx.send(CHAT_ID, intro_msg)
# Create and render the draft quote
# Create and render the new draft quote
await quote.handle_entities(
ctx,
CHAT_ID,