CognxSafeTrack commited on
Commit
3b3d6cd
·
1 Parent(s): fb47dd6

fix(crm-agent): replace generateText+JSON.parse with generateStructuredData for intent detection

Browse files

The CRM command agent was always falling back to GENERAL_CHAT and
showing the menu again because LLMs wrap JSON in markdown fences,
breaking JSON.parse. Switch to generateStructuredData with a Zod schema
which guarantees structured output regardless of LLM quirks.

Also exposes generateStructuredData as a public method on AIService.

apps/api/src/routes/ai.ts CHANGED
@@ -383,20 +383,25 @@ export async function aiRoutes(fastify: FastifyInstance) {
383
  const { query } = bodySchema.parse(request.body);
384
  const organizationId = request.headers['x-organization-id'] as string;
385
 
386
- // Step 1: Detect intent using a lightweight prompt
387
- const systemPrompt = `You are a CRM Command Interpreter. Your job is to classify the user's intent into one of these:
388
- - LIST_CONTACTS: User wants to see their contacts or history.
389
- - SHOW_IMPORT: User wants to import data or Excel files.
390
- - START_CAMPAIGN: User wants to start or generate a campaign.
391
- - GENERAL_CHAT: Any other conversation.
 
 
 
 
 
392
 
393
- Return ONLY a JSON object: { "intent": "INTENT_NAME", "reply": "A brief, friendly acknowledgment in French" }`;
394
 
395
- const { text } = await aiService.generateText(systemPrompt, query, 0.1);
396
- let analysis;
397
  try {
398
- analysis = JSON.parse(text);
399
- } catch (e) {
 
400
  analysis = { intent: 'GENERAL_CHAT', reply: "Je suis à votre écoute. Comment puis-je vous aider ?" };
401
  }
402
 
 
383
  const { query } = bodySchema.parse(request.body);
384
  const organizationId = request.headers['x-organization-id'] as string;
385
 
386
+ // Step 1: Detect intent using structured output (avoids JSON.parse failures on markdown-wrapped responses)
387
+ const IntentSchema = z.object({
388
+ intent: z.enum(['LIST_CONTACTS', 'SHOW_IMPORT', 'START_CAMPAIGN', 'GENERAL_CHAT']),
389
+ reply: z.string().describe("A brief, friendly acknowledgment in French")
390
+ });
391
+
392
+ const intentPrompt = `You are a CRM Command Interpreter. Classify the user's intent:
393
+ - LIST_CONTACTS: User wants to see contacts or history.
394
+ - SHOW_IMPORT: User wants to import data or Excel files.
395
+ - START_CAMPAIGN: User wants to start or generate a campaign.
396
+ - GENERAL_CHAT: Anything else.
397
 
398
+ User message: "${query.replace(/"/g, "'")}"`
399
 
400
+ let analysis: { intent: string; reply: string };
 
401
  try {
402
+ const { data } = await aiService.generateStructuredData(intentPrompt, IntentSchema, 0.1);
403
+ analysis = data;
404
+ } catch {
405
  analysis = { intent: 'GENERAL_CHAT', reply: "Je suis à votre écoute. Comment puis-je vous aider ?" };
406
  }
407
 
packages/ai-sdk/src/index.ts CHANGED
@@ -351,9 +351,14 @@ export class AIService {
351
  throw new Error(`[AI_ERROR] All providers for ${ProviderCapability.TEXT} failed.`);
352
  }
353
 
 
 
 
 
 
354
  async handleCrmConversation(
355
- phoneNumber: string,
356
- organizationId: string,
357
  userMessage: string
358
  ): Promise<{ response: string, aiSource: string }> {
359
  const memoryKey = `crm:chat:${organizationId}:${phoneNumber}`;
 
351
  throw new Error(`[AI_ERROR] All providers for ${ProviderCapability.TEXT} failed.`);
352
  }
353
 
354
+ async generateStructuredData<T>(prompt: string, schema: z.ZodSchema<T>, temperature?: number, imageUrl?: string): Promise<{ data: T; aiSource: string; usage: TokenUsage }> {
355
+ const { data, source, usage } = await this.callWithFailover(prompt, schema, temperature, imageUrl);
356
+ return { data, aiSource: source, usage };
357
+ }
358
+
359
  async handleCrmConversation(
360
+ phoneNumber: string,
361
+ organizationId: string,
362
  userMessage: string
363
  ): Promise<{ response: string, aiSource: string }> {
364
  const memoryKey = `crm:chat:${organizationId}:${phoneNumber}`;