From c89dc04d4cde9ad0c4850d0424ee24ca8996b4d7 Mon Sep 17 00:00:00 2001 From: Francisco Gaona Date: Thu, 9 Apr 2026 21:46:29 +0200 Subject: [PATCH] WIP - improve list filtering explanation --- .../src/ai-assistant/ai-assistant.service.ts | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/backend/src/ai-assistant/ai-assistant.service.ts b/backend/src/ai-assistant/ai-assistant.service.ts index 1e3670b..fa1079d 100644 --- a/backend/src/ai-assistant/ai-assistant.service.ts +++ b/backend/src/ai-assistant/ai-assistant.service.ts @@ -49,7 +49,7 @@ type AiSearchPayload = { @Injectable() export class AiAssistantService { private readonly logger = new Logger(AiAssistantService.name); - private readonly defaultModel = process.env.OPENAI_MODEL || 'gpt-4o'; + private readonly defaultModel = process.env.OPENAI_MODEL || 'gpt-5.4-mini'; private readonly conversationState = new Map< string, { fields: Record; updatedAt: number } @@ -2255,20 +2255,52 @@ export class AiAssistantService { const formatInstructions = parser.getFormatInstructions(); const today = new Date().toISOString(); + const systemPromptLines = [ + `You are a CRM search assistant. The user is browsing a list of "${objectDefinition.label || objectDefinition.apiName}" records.`, + ``, + `Your task: decide whether the user input is a KEYWORD search or a STRUCTURED QUERY, then return the correct JSON plan.`, + ``, + `=== STRATEGY DECISION RULES ===`, + `Use strategy="query" when the user:`, + ` - Describes a record attribute or property value (e.g. "list all golden retrievers" → filter the race field)`, + ` - Uses phrases like "list all X", "show all X", "find all X", "filter by X", "where X is Y"`, + ` - Mentions a specific field value, status, category, type, breed, color, size, or any distinguishing characteristic`, + ` - Requests records matching a condition (e.g. "created this week", "price > 100", "active only")`, + ` - Asks for sorted or ordered results (e.g. "newest first", "alphabetically")`, + ``, + `Use strategy="keyword" ONLY when the user types a bare search term with no clear field or attribute intent`, + ` - e.g. a lone name or identifier: "rex", "john", "acme corp"`, + ``, + `=== HOW TO MAP USER LANGUAGE TO FIELDS ===`, + `When the user describes a characteristic or property value, find the best-matching field from the Fields list`, + `and apply a "contains" filter (or "eq" for exact values). Do NOT fall back to keyword just because the`, + `field name is not explicitly mentioned — infer the intent from context.`, + ``, + `EXAMPLES (Object = Dog, fields include: name, race, size, color, createdAt):`, + ` "list all cocker spaniels" → strategy=query, filter: race contains "cocker spaniel"`, + ` "show golden retrievers" → strategy=query, filter: race contains "golden retriever"`, + ` "large dogs" → strategy=query, filter: size contains "large"`, + ` "dogs added this week" → strategy=query, filter: createdAt between `, + ` "rex" → strategy=keyword, keyword="rex"`, + ``, + `=== OUTPUT FORMAT ===`, + `Return a JSON object with these keys:`, + ` strategy : "keyword" or "query"`, + ` explanation : short plain-language description of what the search does`, + ` keyword : the search term when strategy is "keyword", otherwise null`, + ` filters : array of filter objects for strategy="query" (empty array otherwise)`, + ` sort : {field, direction} when sorting is requested, otherwise null`, + ``, + `Each filter object: { field: , operator: , value?, values?, from?, to? }`, + `Valid operators: eq | neq | gt | gte | lt | lte | contains | startsWith | endsWith | in | notIn | isNull | notNull | between`, + `Use "between" with {from, to} for date ranges.`, + `Only use field apiName values exactly as they appear in the Fields list provided.`, + ``, + formatInstructions, + ].join('\n'); + const response = await model.invoke([ - new SystemMessage( - `You are a CRM search assistant. Decide whether the user input is a keyword search or a structured query.` + - `\nReturn a JSON object with keys: strategy, explanation, keyword, filters, sort.` + - `\n- strategy must be "keyword" or "query".` + - `\n- explanation must be a short sentence explaining the approach.` + - `\n- keyword should be the search term when strategy is "keyword", otherwise null.` + - `\n- filters is an array of {field, operator, value, values, from, to}.` + - `\n- operators must be one of eq, neq, gt, gte, lt, lte, contains, startsWith, endsWith, in, notIn, isNull, notNull, between.` + - `\n- Use between with from/to when the user gives date ranges like "yesterday" or "last week".` + - `\n- sort should be {field, direction} when sorting is requested.` + - `\n- Only use field apiName values exactly as provided.` + - `\n${formatInstructions}`, - ), + new SystemMessage(systemPromptLines), new HumanMessage( `Object: ${objectDefinition.label || objectDefinition.apiName}.\n` + `Fields: ${JSON.stringify(fields)}.\n` + @@ -2281,6 +2313,8 @@ export class AiAssistantService { const parsed = await parser.parse(content); const normalizedPlan = this.normalizeSearchPlan(parsed, message, objectDefinition); + console.log('AI search plan:', normalizedPlan); + if (normalizedPlan.strategy === 'query') { const aiExplanation = await this.generateQueryExplanationWithAi( model, @@ -2462,6 +2496,9 @@ export class AiAssistantService { .join(' ') : String(response.content || ''); const cleaned = content.trim().replace(/\s+/g, ' '); + + console.log('AI-generated query explanation:', cleaned); + return cleaned || null; } catch (error) { this.logger.warn(`AI query explanation refinement failed: ${error.message}`);