Configure the AI assistant for more information.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
assigneeWhere,
|
assigneeWhere,
|
||||||
noteAccessWhere,
|
noteAccessWhere,
|
||||||
ownerWhere,
|
ownerWhere,
|
||||||
|
ticketAccessWhere,
|
||||||
} from '../common/record-scope';
|
} from '../common/record-scope';
|
||||||
import {
|
import {
|
||||||
BILLING_PLAN_LABELS,
|
BILLING_PLAN_LABELS,
|
||||||
@@ -21,10 +22,20 @@ import {
|
|||||||
type BillingPlanCode,
|
type BillingPlanCode,
|
||||||
type UserRole,
|
type UserRole,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
|
import { startOfJalaliMonth, toJalali } from '../common/jalali';
|
||||||
import { AiService } from './ai.service';
|
import { AiService } from './ai.service';
|
||||||
|
|
||||||
type CatalogItem = AskMentoSourceDto;
|
type CatalogItem = AskMentoSourceDto;
|
||||||
|
|
||||||
|
type PeriodRange = { key: string; label: string; start: Date; end: Date };
|
||||||
|
|
||||||
|
type PeriodCounts = {
|
||||||
|
today: number;
|
||||||
|
yesterday: number;
|
||||||
|
week: number;
|
||||||
|
month: number;
|
||||||
|
};
|
||||||
|
|
||||||
function clip(value: string | null | undefined, max = 280) {
|
function clip(value: string | null | undefined, max = 280) {
|
||||||
const text = (value ?? '').replace(/\s+/g, ' ').trim();
|
const text = (value ?? '').replace(/\s+/g, ' ').trim();
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
@@ -39,6 +50,75 @@ function planLabel(code: string) {
|
|||||||
return BILLING_PLAN_LABELS[code as BillingPlanCode] ?? code;
|
return BILLING_PLAN_LABELS[code as BillingPlanCode] ?? code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addDaysToDayKey(dayKey: string, days: number): string {
|
||||||
|
const [y, m, d] = dayKey.split('-').map(Number);
|
||||||
|
const dt = new Date(Date.UTC(y!, m! - 1, d!));
|
||||||
|
dt.setUTCDate(dt.getUTCDate() + days);
|
||||||
|
return dt.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayBoundsFromKey(dayKey: string) {
|
||||||
|
return {
|
||||||
|
start: new Date(`${dayKey}T00:00:00+03:30`),
|
||||||
|
end: new Date(`${dayKey}T23:59:59.999+03:30`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** بازههای تقویمی ایران: امروز، دیروز، این هفته (شنبه تا جمعه)، این ماه شمسی */
|
||||||
|
function tehranPeriods(now = new Date()) {
|
||||||
|
const dayKey = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Tehran',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
}).format(now);
|
||||||
|
const weekday = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: 'Asia/Tehran',
|
||||||
|
weekday: 'short',
|
||||||
|
}).format(now);
|
||||||
|
const sinceSaturday: Record<string, number> = {
|
||||||
|
Sat: 0,
|
||||||
|
Sun: 1,
|
||||||
|
Mon: 2,
|
||||||
|
Tue: 3,
|
||||||
|
Wed: 4,
|
||||||
|
Thu: 5,
|
||||||
|
Fri: 6,
|
||||||
|
};
|
||||||
|
const weekOffset = sinceSaturday[weekday] ?? 0;
|
||||||
|
const yesterdayKey = addDaysToDayKey(dayKey, -1);
|
||||||
|
const weekStartKey = addDaysToDayKey(dayKey, -weekOffset);
|
||||||
|
const weekEndKey = addDaysToDayKey(weekStartKey, 6);
|
||||||
|
const today = { key: dayKey, label: 'today', ...dayBoundsFromKey(dayKey) };
|
||||||
|
const yesterday = {
|
||||||
|
key: yesterdayKey,
|
||||||
|
label: 'yesterday',
|
||||||
|
...dayBoundsFromKey(yesterdayKey),
|
||||||
|
};
|
||||||
|
const week: PeriodRange = {
|
||||||
|
key: `${weekStartKey}..${weekEndKey}`,
|
||||||
|
label: 'this_week',
|
||||||
|
start: dayBoundsFromKey(weekStartKey).start,
|
||||||
|
end: dayBoundsFromKey(weekEndKey).end,
|
||||||
|
};
|
||||||
|
const { jy, jm } = (() => {
|
||||||
|
const [gy, gm, gd] = dayKey.split('-').map(Number);
|
||||||
|
return toJalali(gy!, gm!, gd!);
|
||||||
|
})();
|
||||||
|
const monthStart = startOfJalaliMonth(jy, jm);
|
||||||
|
const month: PeriodRange = {
|
||||||
|
key: `${jy}-${String(jm).padStart(2, '0')}`,
|
||||||
|
label: 'this_jalali_month',
|
||||||
|
start: monthStart,
|
||||||
|
end: now,
|
||||||
|
};
|
||||||
|
return { now, dayKey, today, yesterday, week, month };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPeriodCounts(labelFa: string, counts: PeriodCounts) {
|
||||||
|
return `${labelFa}: today=${counts.today} | yesterday=${counts.yesterday} | this_week=${counts.week} | this_month=${counts.month}`;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiAskService {
|
export class AiAskService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -57,12 +137,14 @@ export class AiAskService {
|
|||||||
sources?: Array<{ type?: string; id?: string; label?: string }>;
|
sources?: Array<{ type?: string; id?: string; label?: string }>;
|
||||||
}>(
|
}>(
|
||||||
`You are ${brand.name}, a Persian CRM assistant for this user's account.
|
`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).
|
"محیط" means a workspace/organization the user belongs to.
|
||||||
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.
|
"مشتری" / "مشتری جدید" means a Person (contact) record.
|
||||||
Answer ONLY from the CRM context. Never say a workspace fact is unavailable if it appears in the context.
|
Period stats use Asia/Tehran calendar: today, yesterday, this_week (Saturday–Friday), this_month (current Jalali month).
|
||||||
|
Workspace facts and Period stats are complete and authoritative for counts of customers, companies, activities, deals, won deals, invoices, quotes, products, notes, and tickets.
|
||||||
|
Answer ONLY from the CRM context. Never say a listed count is unavailable.
|
||||||
Do not invent customers, amounts, dates, or promises.
|
Do not invent customers, amounts, dates, or promises.
|
||||||
Return JSON: { "answer": string, "sources": [{ "type": "person"|"note"|"activity"|"deal"|"memory", "id": string, "label": string }] }
|
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. Workspace facts do not need sources.
|
Use at most 4 sources, and only ids that appear in the context. Workspace/period facts do not need sources; when naming customers, prefer person sources.
|
||||||
Write the answer in Persian, concise, with exact numbers and names when present.`,
|
Write the answer in Persian, concise, with exact numbers and names when present.`,
|
||||||
`Question:\n${question.trim()}\n\nCRM context:\n${context}`,
|
`Question:\n${question.trim()}\n\nCRM context:\n${context}`,
|
||||||
);
|
);
|
||||||
@@ -108,6 +190,19 @@ Rules:
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async countInPeriods(
|
||||||
|
count: (start: Date, end: Date) => Promise<number>,
|
||||||
|
periods: ReturnType<typeof tehranPeriods>,
|
||||||
|
): Promise<PeriodCounts> {
|
||||||
|
const [today, yesterday, week, month] = await Promise.all([
|
||||||
|
count(periods.today.start, periods.today.end),
|
||||||
|
count(periods.yesterday.start, periods.yesterday.end),
|
||||||
|
count(periods.week.start, periods.week.end),
|
||||||
|
count(periods.month.start, periods.month.end),
|
||||||
|
]);
|
||||||
|
return { today, yesterday, week, month };
|
||||||
|
}
|
||||||
|
|
||||||
private async buildContext(actor: Actor, personId?: string) {
|
private async buildContext(actor: Actor, personId?: string) {
|
||||||
if (personId) {
|
if (personId) {
|
||||||
const person = await this.prisma.person.findFirst({
|
const person = await this.prisma.person.findFirst({
|
||||||
@@ -121,33 +216,52 @@ Rules:
|
|||||||
if (!person) throw new NotFoundException('شخص پیدا نشد');
|
if (!person) throw new NotFoundException('شخص پیدا نشد');
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const periods = tehranPeriods();
|
||||||
const todayStart = new Date(now);
|
const { now, dayKey } = periods;
|
||||||
todayStart.setHours(0, 0, 0, 0);
|
const orgId = actor.organizationId;
|
||||||
const todayEnd = new Date(now);
|
|
||||||
todayEnd.setHours(23, 59, 59, 999);
|
|
||||||
const dealScope = ownerWhere(actor);
|
const dealScope = ownerWhere(actor);
|
||||||
const activityScope = assigneeWhere(actor);
|
const activityScope = assigneeWhere(actor);
|
||||||
const personScope = ownerWhere(actor);
|
const personScope = ownerWhere(actor);
|
||||||
|
const companyScope = ownerWhere(actor);
|
||||||
|
const invoiceScope = ownerWhere(actor);
|
||||||
|
const quoteScope = ownerWhere(actor);
|
||||||
|
const ticketScope = ticketAccessWhere(actor);
|
||||||
|
|
||||||
|
const createdBetween =
|
||||||
|
(start: Date, end: Date) => ({ createdAt: { gte: start, lte: end } });
|
||||||
|
|
||||||
const [
|
const [
|
||||||
org,
|
org,
|
||||||
user,
|
user,
|
||||||
memberships,
|
memberships,
|
||||||
ownerBilling,
|
ownerBilling,
|
||||||
peopleCount,
|
teamCount,
|
||||||
|
peopleTotal,
|
||||||
|
companiesTotal,
|
||||||
|
productsTotal,
|
||||||
openDealCount,
|
openDealCount,
|
||||||
staleDealCount,
|
staleDealCount,
|
||||||
overdueActivityCount,
|
overdueActivityCount,
|
||||||
todayActivityCount,
|
activitiesDueToday,
|
||||||
teamCount,
|
peoplePeriods,
|
||||||
|
companyPeriods,
|
||||||
|
activityPeriods,
|
||||||
|
dealCreatedPeriods,
|
||||||
|
dealWonPeriods,
|
||||||
|
invoicePeriods,
|
||||||
|
quotePeriods,
|
||||||
|
productPeriods,
|
||||||
|
notePeriods,
|
||||||
|
ticketPeriods,
|
||||||
|
peopleToday,
|
||||||
|
recentPeople,
|
||||||
notes,
|
notes,
|
||||||
activities,
|
activities,
|
||||||
deals,
|
deals,
|
||||||
memories,
|
memories,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.prisma.organization.findUnique({
|
this.prisma.organization.findUnique({
|
||||||
where: { id: actor.organizationId },
|
where: { id: orgId },
|
||||||
select: { name: true, businessField: true },
|
select: { name: true, businessField: true },
|
||||||
}),
|
}),
|
||||||
this.prisma.user.findUnique({
|
this.prisma.user.findUnique({
|
||||||
@@ -172,7 +286,7 @@ Rules:
|
|||||||
owner: {
|
owner: {
|
||||||
memberships: {
|
memberships: {
|
||||||
some: {
|
some: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
role: 'owner',
|
role: 'owner',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
@@ -186,19 +300,24 @@ Rules:
|
|||||||
maxOrganizations: true,
|
maxOrganizations: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
this.prisma.membership.count({
|
||||||
|
where: { organizationId: orgId, isActive: true },
|
||||||
|
}),
|
||||||
this.prisma.person.count({
|
this.prisma.person.count({
|
||||||
where: { organizationId: actor.organizationId, ...personScope },
|
where: { organizationId: orgId, ...personScope },
|
||||||
|
}),
|
||||||
|
this.prisma.company.count({
|
||||||
|
where: { organizationId: orgId, ...companyScope },
|
||||||
|
}),
|
||||||
|
this.prisma.product.count({
|
||||||
|
where: { organizationId: orgId },
|
||||||
|
}),
|
||||||
|
this.prisma.deal.count({
|
||||||
|
where: { organizationId: orgId, status: 'open', ...dealScope },
|
||||||
}),
|
}),
|
||||||
this.prisma.deal.count({
|
this.prisma.deal.count({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
status: 'open',
|
|
||||||
...dealScope,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.prisma.deal.count({
|
|
||||||
where: {
|
|
||||||
organizationId: actor.organizationId,
|
|
||||||
status: 'open',
|
status: 'open',
|
||||||
...dealScope,
|
...dealScope,
|
||||||
OR: [{ isStale: true }, { isRotten: true }],
|
OR: [{ isStale: true }, { isRotten: true }],
|
||||||
@@ -206,7 +325,7 @@ Rules:
|
|||||||
}),
|
}),
|
||||||
this.prisma.activity.count({
|
this.prisma.activity.count({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
...activityScope,
|
...activityScope,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
dueAt: { lt: now },
|
dueAt: { lt: now },
|
||||||
@@ -214,18 +333,157 @@ Rules:
|
|||||||
}),
|
}),
|
||||||
this.prisma.activity.count({
|
this.prisma.activity.count({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
...activityScope,
|
...activityScope,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
dueAt: { gte: todayStart, lte: todayEnd },
|
dueAt: { gte: periods.today.start, lte: periods.today.end },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.prisma.membership.count({
|
this.countInPeriods(
|
||||||
where: { organizationId: actor.organizationId, isActive: true },
|
(start, end) =>
|
||||||
|
this.prisma.person.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...personScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.company.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...companyScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.activity.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...activityScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.deal.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...dealScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.deal.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...dealScope,
|
||||||
|
status: 'won',
|
||||||
|
updatedAt: { gte: start, lte: end },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.invoice.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...invoiceScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.quote.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...quoteScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.product.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.note.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...noteAccessWhere(actor),
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.countInPeriods(
|
||||||
|
(start, end) =>
|
||||||
|
this.prisma.ticket.count({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...ticketScope,
|
||||||
|
...createdBetween(start, end),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
periods,
|
||||||
|
),
|
||||||
|
this.prisma.person.findMany({
|
||||||
|
where: {
|
||||||
|
organizationId: orgId,
|
||||||
|
...personScope,
|
||||||
|
createdAt: { gte: periods.today.start, lte: periods.today.end },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
mobile: true,
|
||||||
|
phone: true,
|
||||||
|
email: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 20,
|
||||||
|
}),
|
||||||
|
this.prisma.person.findMany({
|
||||||
|
where: { organizationId: orgId, ...personScope },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
mobile: true,
|
||||||
|
phone: true,
|
||||||
|
email: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 8,
|
||||||
}),
|
}),
|
||||||
this.prisma.note.findMany({
|
this.prisma.note.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
...noteAccessWhere(actor),
|
...noteAccessWhere(actor),
|
||||||
...(personId ? { personId } : {}),
|
...(personId ? { personId } : {}),
|
||||||
},
|
},
|
||||||
@@ -241,7 +499,7 @@ Rules:
|
|||||||
}),
|
}),
|
||||||
this.prisma.activity.findMany({
|
this.prisma.activity.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
...assigneeWhere(actor),
|
...assigneeWhere(actor),
|
||||||
...(personId ? { personId } : {}),
|
...(personId ? { personId } : {}),
|
||||||
},
|
},
|
||||||
@@ -259,7 +517,7 @@ Rules:
|
|||||||
}),
|
}),
|
||||||
this.prisma.deal.findMany({
|
this.prisma.deal.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: orgId,
|
||||||
...ownerWhere(actor),
|
...ownerWhere(actor),
|
||||||
...(personId ? { personId } : { status: 'open' }),
|
...(personId ? { personId } : { status: 'open' }),
|
||||||
},
|
},
|
||||||
@@ -279,7 +537,7 @@ Rules:
|
|||||||
}),
|
}),
|
||||||
personId
|
personId
|
||||||
? this.prisma.memoryItem.findMany({
|
? this.prisma.memoryItem.findMany({
|
||||||
where: { organizationId: actor.organizationId, personId },
|
where: { organizationId: orgId, personId },
|
||||||
select: { id: true, content: true, updatedAt: true },
|
select: { id: true, content: true, updatedAt: true },
|
||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
take: 8,
|
take: 8,
|
||||||
@@ -293,7 +551,7 @@ Rules:
|
|||||||
? BUSINESS_FIELD_LABELS[org.businessField]
|
? BUSINESS_FIELD_LABELS[org.businessField]
|
||||||
: 'نامشخص';
|
: 'نامشخص';
|
||||||
const envLines = memberships.map((row) => {
|
const envLines = memberships.map((row) => {
|
||||||
const current = row.organizationId === actor.organizationId ? ' [current]' : '';
|
const current = row.organizationId === orgId ? ' [current]' : '';
|
||||||
return `${row.organization.name}${current} (role=${roleLabel(row.role)})`;
|
return `${row.organization.name}${current} (role=${roleLabel(row.role)})`;
|
||||||
});
|
});
|
||||||
const billingDays = ownerBilling
|
const billingDays = ownerBilling
|
||||||
@@ -305,13 +563,33 @@ Rules:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
const peopleTodayNames = peopleToday
|
||||||
|
.map((p) => `${p.firstName} ${p.lastName}`.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`Today: ${now.toISOString()}`,
|
`Today (Asia/Tehran): ${dayKey} | now=${now.toISOString()}`,
|
||||||
|
`Periods: yesterday=${periods.yesterday.key} | this_week=${periods.week.key} (Sat–Fri) | this_month=${periods.month.key} (Jalali)`,
|
||||||
'Workspace facts (authoritative):',
|
'Workspace facts (authoritative):',
|
||||||
`User: ${user?.fullName ?? '—'} | email: ${user?.email ?? '—'} | mobile: ${user?.mobile ?? '—'} | role in current environment: ${roleLabel(actor.role)}`,
|
`User: ${user?.fullName ?? '—'} | email: ${user?.email ?? '—'} | mobile: ${user?.mobile ?? '—'} | role in current environment: ${roleLabel(actor.role)}`,
|
||||||
`Environments (محیط) count: ${memberships.length}`,
|
`Environments (محیط) count: ${memberships.length}`,
|
||||||
`Environments: ${envLines.join(' | ') || 'none'}`,
|
`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}`,
|
`Current environment: ${org?.name ?? '—'} | business field: ${field} | team members: ${teamCount}`,
|
||||||
|
`Totals: people=${peopleTotal} | companies=${companiesTotal} | products=${productsTotal} | open deals=${openDealCount} | at-risk deals=${staleDealCount} | overdue activities=${overdueActivityCount} | activities due today=${activitiesDueToday}`,
|
||||||
|
'Period stats (created/registered unless noted):',
|
||||||
|
formatPeriodCounts('People / مشتریان جدید', peoplePeriods),
|
||||||
|
peopleTodayNames.length
|
||||||
|
? `New customers today names: ${peopleTodayNames.join('، ')}`
|
||||||
|
: 'New customers today names: (none)',
|
||||||
|
formatPeriodCounts('Companies / شرکتها', companyPeriods),
|
||||||
|
formatPeriodCounts('Activities / فعالیتها (created)', activityPeriods),
|
||||||
|
formatPeriodCounts('Deals / معاملات (created)', dealCreatedPeriods),
|
||||||
|
formatPeriodCounts('Won deals / معاملات برنده (updatedAt while won)', dealWonPeriods),
|
||||||
|
formatPeriodCounts('Invoices / فاکتورها', invoicePeriods),
|
||||||
|
formatPeriodCounts('Quotes / پیشفاکتورها', quotePeriods),
|
||||||
|
formatPeriodCounts('Products / محصولات', productPeriods),
|
||||||
|
formatPeriodCounts('Notes / یادداشتها', notePeriods),
|
||||||
|
formatPeriodCounts('Tickets / تیکتها', ticketPeriods),
|
||||||
];
|
];
|
||||||
if (ownerBilling && billingDays != null) {
|
if (ownerBilling && billingDays != null) {
|
||||||
lines.push(
|
lines.push(
|
||||||
@@ -319,12 +597,14 @@ Rules:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
personId ? `Focus personId: ${personId}` : 'Scope: current user records in the current environment',
|
personId
|
||||||
|
? `Focus personId: ${personId}`
|
||||||
|
: 'Scope: current user records in the current environment',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (personId) {
|
if (personId) {
|
||||||
const person = await this.prisma.person.findFirst({
|
const person = await this.prisma.person.findFirst({
|
||||||
where: { id: personId, organizationId: actor.organizationId },
|
where: { id: personId, organizationId: orgId },
|
||||||
include: { company: { select: { name: true } } },
|
include: { company: { select: { name: true } } },
|
||||||
});
|
});
|
||||||
if (person) {
|
if (person) {
|
||||||
@@ -337,6 +617,17 @@ Rules:
|
|||||||
`Person: ${person.firstName} ${person.lastName} | position: ${person.position ?? '—'} | company: ${person.company?.name ?? '—'} | mood: ${person.mood} | phone: ${person.mobile || person.phone || '—'} | email: ${person.email ?? '—'}`,
|
`Person: ${person.firstName} ${person.lastName} | position: ${person.position ?? '—'} | company: ${person.company?.name ?? '—'} | mood: ${person.mood} | phone: ${person.mobile || person.phone || '—'} | email: ${person.email ?? '—'}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const seenPeople = new Set<string>();
|
||||||
|
for (const p of [...peopleToday, ...recentPeople]) {
|
||||||
|
if (seenPeople.has(p.id)) continue;
|
||||||
|
seenPeople.add(p.id);
|
||||||
|
const label = `${p.firstName} ${p.lastName}`.trim();
|
||||||
|
catalog.push({ type: 'person', id: p.id, label });
|
||||||
|
lines.push(
|
||||||
|
`Person ${p.id} ${label} created=${p.createdAt.toISOString()} phone=${p.mobile || p.phone || '—'} email=${p.email ?? '—'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of memories) {
|
for (const item of memories) {
|
||||||
@@ -381,6 +672,6 @@ Rules:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { context: lines.join('\n').slice(0, 12000), catalog };
|
return { context: lines.join('\n').slice(0, 14000), catalog };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user