From 693e7511c6ddda140cd8f2f3564663d3fabd6f08 Mon Sep 17 00:00:00 2001 From: "reza.g" Date: Sun, 11 Oct 2026 10:12:40 +0330 Subject: [PATCH] feat(nlu): clean up entity leakage and add generic Isfahan acoustic rules --- bot/app/nlu.py | 54 ++++++++++++++++++------------- bot/tools/send_analysis_to_bot.py | 15 ++++++--- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/bot/app/nlu.py b/bot/app/nlu.py index 00d12aa..b4f2332 100644 --- a/bot/app/nlu.py +++ b/bot/app/nlu.py @@ -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", diff --git a/bot/tools/send_analysis_to_bot.py b/bot/tools/send_analysis_to_bot.py index 1c96cd4..d3ddc99 100644 --- a/bot/tools/send_analysis_to_bot.py +++ b/bot/tools/send_analysis_to_bot.py @@ -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,