Edit the artificial intelligence of the Mentact assistant
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user