WIP - improve list filtering explanation
This commit is contained in:
@@ -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<string, any>; 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 <monday> <today>`,
|
||||
` "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: <apiName>, operator: <op>, 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}`);
|
||||
|
||||
Reference in New Issue
Block a user