CognxSafeTrack commited on
Commit ·
3b3d6cd
1
Parent(s): fb47dd6
fix(crm-agent): replace generateText+JSON.parse with generateStructuredData for intent detection
Browse filesThe 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 +16 -11
- packages/ai-sdk/src/index.ts +7 -2
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
|
| 387 |
-
const
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
-
|
| 394 |
|
| 395 |
-
|
| 396 |
-
let analysis;
|
| 397 |
try {
|
| 398 |
-
|
| 399 |
-
|
|
|
|
| 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}`;
|