Files
CRM_Montzar/apps/api/src/ai/ask-count.ts
T

248 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
PERSON_IMPORT_SOURCES,
type PersonImportSource,
} from '@mento/shared';
export type CountEntity =
| 'lead'
| 'person'
| 'company'
| 'deal'
| 'invoice'
| 'quote'
| 'product'
| 'activity'
| 'ticket'
| 'note'
| 'cheque'
| 'reservation'
| 'routine'
| 'project';
export type CountPeriod = 'today' | 'yesterday' | 'week' | 'last_week' | 'month' | 'last_month';
export type LeadBucket = 'summary' | 'open' | 'archived' | 'converted';
export type DealBucket = 'all' | 'open' | 'won' | 'lost' | 'stale';
export type CountIntent = {
entity: CountEntity;
personSource?: PersonImportSource;
mood?: 'newcomer' | 'happy' | 'unhappy' | 'special';
leadBucket: LeadBucket;
dealBucket: DealBucket;
period?: CountPeriod;
/** متن باقی‌مانده برای تطبیق مرحله یا منبع لید */
hint: string;
};
const SOURCE_PATTERNS: Array<{ re: RegExp; source: PersonImportSource }> = [
{ re: /دستی/, source: 'manual' },
{ re: /اکسل/, source: 'excel' },
{ re: /دیدار/, source: 'didar' },
{ re: /ته\s*حساب/, source: 'tahesab' },
{ re: /گوشی|مخاطب\s*گوشی|مخاطبین/, source: 'phone' },
{ re: /فرم/, source: 'lead_form' },
{ re: /مونتیفای/, source: 'montify' },
{ re: /سایر|بقیه/, source: 'other' },
];
export function normalizeAskText(raw: string) {
return raw
.replace(/ي/g, 'ی')
.replace(/ك/g, 'ک')
.replace(/‌/g, ' ')
.replace(/[؟?!.,،؛:]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
export function isCountQuestion(q: string) {
return /چند|تعداد|چقدر|چنتا|چند\s*تا/.test(q);
}
function detectEntity(q: string): CountEntity | null {
if (/لید|سرنخ/.test(q)) return 'lead';
if (/پیش\s*فاکتور/.test(q)) return 'quote';
if (/فاکتور/.test(q)) return 'invoice';
if (/معاملات|معامله/.test(q)) return 'deal';
if (/چک/.test(q)) return 'cheque';
if (/رزرو|نوبت/.test(q)) return 'reservation';
if (/روتین/.test(q)) return 'routine';
if (/پروژه/.test(q)) return 'project';
if (/تیکت/.test(q)) return 'ticket';
if (/یادداشت/.test(q)) return 'note';
if (/فعالیت/.test(q)) return 'activity';
if (/محصول|کالا/.test(q)) return 'product';
if (/شرکت/.test(q) && !/مشتری/.test(q)) return 'company';
if (/مشتری|اشخاص|شخص ها|افراد|مخاطب/.test(q)) return 'person';
return null;
}
function detectPeriod(q: string): CountPeriod | undefined {
if (/دیروز/.test(q)) return 'yesterday';
if (/امروز/.test(q)) return 'today';
if (/هفته\s*(?:ی\s+)?(گذشته|قبل|پیش)/.test(q)) return 'last_week';
if (/این\s*هفته|هفته\s*جاری/.test(q)) return 'week';
if (/ماه\s*(?:ی\s+)?(گذشته|قبل|پیش)/.test(q)) return 'last_month';
if (/این\s*ماه|ماه\s*جاری/.test(q)) return 'month';
return undefined;
}
function detectLeadBucket(q: string): LeadBucket {
if (/بایگانی/.test(q)) return 'archived';
if (/تبدیل/.test(q)) return 'converted';
if (/باز|فعال|جاری/.test(q)) return 'open';
return 'summary';
}
function detectDealBucket(q: string): DealBucket {
if (/در\s*خطر|راکد|گندیده/.test(q)) return 'stale';
if (/برنده|موفق|بسته\s*شده\s*موفق/.test(q)) return 'won';
if (/باخته|از\s*دست\s*رفته|ناموفق/.test(q)) return 'lost';
if (/باز|جاری|فعال/.test(q)) return 'open';
return 'all';
}
function detectMood(q: string): CountIntent['mood'] {
if (/ناراضی/.test(q)) return 'unhappy';
if (/خوشحال/.test(q)) return 'happy';
if (/خاص/.test(q)) return 'special';
if (/تازه/.test(q)) return 'newcomer';
return undefined;
}
const HINT_STOP = [
'چند تا',
'ته حساب',
'مخاطب گوشی',
'مخاطبین',
'پیش فاکتور',
'بایگانی',
'مونتیفای',
'معاملات',
'مشتریان',
'سرنخ',
'لیدها',
'فعالیت',
'یادداشت',
'بگویید',
'داریم',
'دارید',
'هستند',
'تعداد',
'چقدر',
'چنتا',
'امروز',
'دیروز',
'جاری',
'تبدیل',
'برنده',
'باخته',
'ناراضی',
'خوشحال',
'محصول',
'فاکتور',
'معامله',
'مشتری',
'شرکت',
'پروژه',
'روتین',
'رزرو',
'نوبت',
'تیکت',
'منبع',
'مرحله',
'وضعیت',
'هفته',
'گذشته',
'پیش',
'قبل',
'لطفا',
'الان',
'شده',
'وارد',
'تازه',
'خاص',
'بگو',
'دارم',
'هست',
'است',
'جاری',
'فعال',
'باز',
'لید',
'کالا',
'چک',
'ماه',
'پیش',
'های',
'چند',
'کل',
'همه',
'این',
'یک',
'یه',
'از',
'با',
'در',
'تا',
'ها',
].sort((a, b) => b.length - a.length);
const PERSON_HINT_STOP = [
'ته حساب',
'مخاطب گوشی',
'مخاطبین',
'مونتیفای',
'ناراضی',
'خوشحال',
'دستی',
'اکسل',
'دیدار',
'گوشی',
'مخاطب',
'فرم',
'سایر',
'تازه',
'خاص',
].sort((a, b) => b.length - a.length);
function leftoverHint(q: string, entity: CountEntity) {
let text = ` ${q} `;
const words = entity === 'person' || entity === 'company'
? [...HINT_STOP, ...PERSON_HINT_STOP]
: HINT_STOP;
for (const word of [...words].sort((a, b) => b.length - a.length)) {
text = text.replaceAll(word, ' ');
}
return text.replace(/\s+/g, ' ').trim();
}
export function classifyCountQuestion(raw: string): CountIntent | null {
const q = normalizeAskText(raw);
if (!isCountQuestion(q)) return null;
const entity = detectEntity(q);
if (!entity) return null;
const personSource = SOURCE_PATTERNS.find((item) => item.re.test(q))?.source;
const period = detectPeriod(q);
let hint = leftoverHint(q, entity);
if (period && entity === 'lead' && !/مرحله|وضعیت|ستون/.test(q)) {
hint = hint.replace(/(^|\s)جدید(?=\s|$)/g, ' ').replace(/\s+/g, ' ').trim();
}
return {
entity,
personSource:
entity === 'person' || entity === 'company' ? personSource : undefined,
mood: entity === 'person' ? detectMood(q) : undefined,
leadBucket: detectLeadBucket(q),
dealBucket: detectDealBucket(q),
period,
hint,
};
}
export function personSourceKeys(): readonly PersonImportSource[] {
return PERSON_IMPORT_SOURCES;
}