Add Ask Mento, customer mood, and Pipecat speech-to-text.

Ship assistant answers, contact moods, voice dictation, and the related CRM UI so these can be deployed to production.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-03 14:20:55 +03:30
co-authored by Cursor
parent 0dd2966f3c
commit 7757c0837d
43 changed files with 5156 additions and 1741 deletions
+25 -14
View File
@@ -32,17 +32,6 @@ export class AiService {
noteType?: string;
personName?: string | null;
}): Promise<NoteAiExtract> {
if (!this.isEnabled()) {
throw new ServiceUnavailableException('هوش مصنوعی فعال نیست');
}
const model = this.config.get<string>('AI_MODEL')?.trim() || 'gpt-4o-mini';
const baseUrl =
this.config.get<string>('AI_BASE_URL')?.trim() ||
'https://api.openai.com/v1';
const apiKey = this.config.get<string>('AI_API_KEY')!.trim();
const timeoutMs = Number(this.config.get<string>('AI_TIMEOUT_MS') || 45000);
const system = `You analyze CRM notes in Persian/Farsi and extract structured data.
Return ONLY valid JSON with this exact shape:
{
@@ -65,6 +54,25 @@ Rules:
`Note:\n${input.body.trim()}`,
].filter(Boolean);
const raw = await this.completeJson<Partial<NoteAiExtract>>(
system,
userParts.join('\n'),
);
return this.normalizeExtract(raw);
}
async completeJson<T>(system: string, user: string): Promise<T> {
if (!this.isEnabled()) {
throw new ServiceUnavailableException('هوش مصنوعی فعال نیست');
}
const model = this.config.get<string>('AI_MODEL')?.trim() || 'gpt-4o-mini';
const baseUrl =
this.config.get<string>('AI_BASE_URL')?.trim() ||
'https://api.openai.com/v1';
const apiKey = this.config.get<string>('AI_API_KEY')!.trim();
const timeoutMs = Number(this.config.get<string>('AI_TIMEOUT_MS') || 45000);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -81,7 +89,7 @@ Rules:
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: system },
{ role: 'user', content: userParts.join('\n') },
{ role: 'user', content: user },
],
}),
signal: controller.signal,
@@ -102,15 +110,18 @@ Rules:
throw new BadRequestException('پاسخ مدل خالی بود');
}
return this.normalizeExtract(JSON.parse(raw) as Partial<NoteAiExtract>);
return JSON.parse(raw) as T;
} catch (err) {
if (err instanceof BadRequestException || err instanceof ServiceUnavailableException) {
throw err;
}
if (err instanceof SyntaxError) {
throw new BadRequestException('پاسخ مدل قابل خواندن نبود');
}
if (err instanceof Error && err.name === 'AbortError') {
throw new ServiceUnavailableException('زمان پاسخ مدل هوش مصنوعی تمام شد');
}
throw new ServiceUnavailableException('تحلیل یادداشت با خطا مواجه شد');
throw new ServiceUnavailableException('ارتباط با مدل هوش مصنوعی ناموفق بود');
} finally {
clearTimeout(timer);
}