Edit the artificial intelligence of the Mentact assistant

This commit is contained in:
2026-09-21 16:58:49 +03:30
parent f5a4eb577b
commit d7d82102af
4 changed files with 248 additions and 23 deletions
+149 -9
View File
@@ -11,7 +11,16 @@ import {
noteAccessWhere,
ownerWhere,
} from '../common/record-scope';
import { BUSINESS_FIELD_LABELS, brand, brandSuggestionFa, isBusinessField } from '@mento/shared';
import {
BILLING_PLAN_LABELS,
BUSINESS_FIELD_LABELS,
USER_ROLE_LABELS,
brand,
brandSuggestionFa,
isBusinessField,
type BillingPlanCode,
type UserRole,
} from '@mento/shared';
import { AiService } from './ai.service';
type CatalogItem = AskMentoSourceDto;
@@ -22,6 +31,14 @@ function clip(value: string | null | undefined, max = 280) {
return text.length > max ? `${text.slice(0, max)}…` : text;
}
function roleLabel(role: string) {
return USER_ROLE_LABELS[role as UserRole] ?? role;
}
function planLabel(code: string) {
return BILLING_PLAN_LABELS[code as BillingPlanCode] ?? code;
}
@Injectable()
export class AiAskService {
constructor(
@@ -39,12 +56,14 @@ export class AiAskService {
answer?: string;
sources?: Array<{ type?: string; id?: string; label?: string }>;
}>(
`You are ${brand.name}, a Persian CRM sales assistant.
Answer ONLY from the CRM context. If the context is insufficient, say so in Persian.
`You are ${brand.name}, a Persian CRM assistant for this user's account.
"محیط" means a workspace/organization the user belongs to (listed under Workspace facts).
Workspace facts are complete and authoritative. Use them for questions about how many environments the user has, the current environment, role, subscription, team size, and record counts.
Answer ONLY from the CRM context. Never say a workspace fact is unavailable if it appears in the context.
Do not invent customers, amounts, dates, or promises.
Return JSON: { "answer": string, "sources": [{ "type": "person"|"note"|"activity"|"deal"|"memory", "id": string, "label": string }] }
Use at most 4 sources, and only ids that appear in the context.
Write the answer in Persian, concise, with dates when present.`,
Use at most 4 sources, and only ids that appear in the context. Workspace facts do not need sources.
Write the answer in Persian, concise, with exact numbers and names when present.`,
`Question:\n${question.trim()}\n\nCRM context:\n${context}`,
);
@@ -102,11 +121,108 @@ Rules:
if (!person) throw new NotFoundException('شخص پیدا نشد');
}
const [org, notes, activities, deals, memories] = await Promise.all([
const now = new Date();
const todayStart = new Date(now);
todayStart.setHours(0, 0, 0, 0);
const todayEnd = new Date(now);
todayEnd.setHours(23, 59, 59, 999);
const dealScope = ownerWhere(actor);
const activityScope = assigneeWhere(actor);
const personScope = ownerWhere(actor);
const [
org,
user,
memberships,
ownerBilling,
peopleCount,
openDealCount,
staleDealCount,
overdueActivityCount,
todayActivityCount,
teamCount,
notes,
activities,
deals,
memories,
] = await Promise.all([
this.prisma.organization.findUnique({
where: { id: actor.organizationId },
select: { name: true, businessField: true },
}),
this.prisma.user.findUnique({
where: { id: actor.userId },
select: { fullName: true, email: true, mobile: true },
}),
this.prisma.membership.findMany({
where: {
userId: actor.userId,
isActive: true,
organization: { isActive: true },
},
select: {
role: true,
organizationId: true,
organization: { select: { name: true } },
},
orderBy: { createdAt: 'asc' },
}),
this.prisma.billingAccount.findFirst({
where: {
owner: {
memberships: {
some: {
organizationId: actor.organizationId,
role: 'owner',
isActive: true,
},
},
},
},
select: {
planCode: true,
status: true,
expiresAt: true,
maxOrganizations: true,
},
}),
this.prisma.person.count({
where: { organizationId: actor.organizationId, ...personScope },
}),
this.prisma.deal.count({
where: {
organizationId: actor.organizationId,
status: 'open',
...dealScope,
},
}),
this.prisma.deal.count({
where: {
organizationId: actor.organizationId,
status: 'open',
...dealScope,
OR: [{ isStale: true }, { isRotten: true }],
},
}),
this.prisma.activity.count({
where: {
organizationId: actor.organizationId,
...activityScope,
status: 'open',
dueAt: { lt: now },
},
}),
this.prisma.activity.count({
where: {
organizationId: actor.organizationId,
...activityScope,
status: 'open',
dueAt: { gte: todayStart, lte: todayEnd },
},
}),
this.prisma.membership.count({
where: { organizationId: actor.organizationId, isActive: true },
}),
this.prisma.note.findMany({
where: {
organizationId: actor.organizationId,
@@ -176,11 +292,35 @@ Rules:
org?.businessField && isBusinessField(org.businessField)
? BUSINESS_FIELD_LABELS[org.businessField]
: 'نامشخص';
const envLines = memberships.map((row) => {
const current = row.organizationId === actor.organizationId ? ' [current]' : '';
return `${row.organization.name}${current} (role=${roleLabel(row.role)})`;
});
const billingDays = ownerBilling
? Math.max(
0,
Math.ceil(
(ownerBilling.expiresAt.getTime() - now.getTime()) /
(24 * 60 * 60 * 1000),
),
)
: null;
const lines: string[] = [
`Today: ${new Date().toISOString()}`,
`Organization: ${org?.name ?? '—'} | business field: ${field}`,
personId ? `Focus personId: ${personId}` : 'Scope: current user records',
`Today: ${now.toISOString()}`,
'Workspace facts (authoritative):',
`User: ${user?.fullName ?? '—'} | email: ${user?.email ?? '—'} | mobile: ${user?.mobile ?? '—'} | role in current environment: ${roleLabel(actor.role)}`,
`Environments (محیط) count: ${memberships.length}`,
`Environments: ${envLines.join(' | ') || 'none'}`,
`Current environment: ${org?.name ?? '—'} | business field: ${field} | team members: ${teamCount} | people: ${peopleCount} | open deals: ${openDealCount} | at-risk deals: ${staleDealCount} | overdue activities: ${overdueActivityCount} | activities due today: ${todayActivityCount}`,
];
if (ownerBilling && billingDays != null) {
lines.push(
`Subscription of current environment owner: ${planLabel(ownerBilling.planCode)} | status=${ownerBilling.status} | days remaining=${billingDays} | max environments=${ownerBilling.maxOrganizations}`,
);
}
lines.push(
personId ? `Focus personId: ${personId}` : 'Scope: current user records in the current environment',
);
if (personId) {
const person = await this.prisma.person.findFirst({