From a0bdb09c0342103f72e26ed079313667483b4b40 Mon Sep 17 00:00:00 2001 From: Francisco Gaona Date: Fri, 10 Apr 2026 09:07:06 +0200 Subject: [PATCH] WIP - resolving related look ups for search filtering --- .../src/ai-assistant/ai-assistant.service.ts | 85 +++++++++++++++++-- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/backend/src/ai-assistant/ai-assistant.service.ts b/backend/src/ai-assistant/ai-assistant.service.ts index fa1079d..f5e418f 100644 --- a/backend/src/ai-assistant/ai-assistant.service.ts +++ b/backend/src/ai-assistant/ai-assistant.service.ts @@ -707,11 +707,21 @@ export class AiAssistantService { console.log('AI search plan (query):', plan); + // Resolve any LOOKUP filter values (user typed a name, we find the related record ID) + const resolvedFilters = await this.resolveRelatedFilters( + resolvedTenantId, + userId, + objectDefinition, + plan.filters || [], + ); + + console.log('Resolved filters for query strategy:', resolvedFilters); + const filtered = await this.objectService.searchRecordsWithFilters( resolvedTenantId, payload.objectApiName, userId, - plan.filters || [], + resolvedFilters, { page, pageSize }, plan.sort || undefined, ); @@ -2207,6 +2217,59 @@ export class AiAssistantService { return extracted; } + /** + * Resolves LOOKUP filter values from human-readable names to actual record IDs. + * When the AI produces a filter like { field: "familyId", operator: "eq", value: "Gaona Family" }, + * this method looks up "Gaona Family" in the referenced object and replaces the value with its ID. + */ + private async resolveRelatedFilters( + tenantId: string, + userId: string, + objectDefinition: any, + filters: AiSearchFilter[], + ): Promise { + if (!filters || filters.length === 0) return filters; + + const lookupFieldMap = new Map(); // apiName → referenceObject + for (const field of objectDefinition.fields || []) { + if (field.type === 'LOOKUP' && field.referenceObject) { + lookupFieldMap.set(field.apiName, field.referenceObject); + } + } + + const resolved: AiSearchFilter[] = []; + for (const filter of filters) { + const referenceObject = lookupFieldMap.get(filter.field); + if ( + referenceObject && + filter.value && + typeof filter.value === 'string' && + !this.isUuid(filter.value) + ) { + // Try to resolve the name to an ID in the referenced object + try { + console.log(`Resolving LOOKUP filter: ${filter.field} → searching "${filter.value}" in ${referenceObject}`); + const relatedRecord = await this.searchForExistingRecord(tenantId, userId, referenceObject, filter.value); + if (relatedRecord?.id) { + console.log(`Resolved "${filter.value}" → ID: ${relatedRecord.id}`); + resolved.push({ ...filter, operator: 'eq', value: relatedRecord.id }); + } else { + // Could not resolve; keep as-is so we get 0 results rather than wrong ones + console.warn(`Could not resolve related record "${filter.value}" in ${referenceObject}; keeping original filter`); + resolved.push(filter); + } + } catch (err) { + this.logger.warn(`Failed to resolve lookup filter for ${filter.field}: ${err.message}`); + resolved.push(filter); + } + } else { + resolved.push(filter); + } + } + + return resolved; + } + private async buildSearchPlan( tenantId: string, message: string, @@ -2250,6 +2313,7 @@ export class AiAssistantService { apiName: field.apiName, label: field.label, type: field.type, + ...(field.referenceObject ? { referenceObject: field.referenceObject } : {}), })); const formatInstructions = parser.getFormatInstructions(); @@ -2276,12 +2340,19 @@ export class AiAssistantService { `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"`, + `EXAMPLES (Object = Dog, fields include: name, race, size, color, createdAt, familyId[LOOKUP→Family]):`, + ` "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 `, + ` "dogs belonging to Gaona Family" → strategy=query, filter: familyId eq "Gaona Family"`, + ` "rex" → strategy=keyword, keyword="rex"`, + ``, + `=== LOOKUP FIELDS ===`, + `Fields with type LOOKUP store a reference (ID) to another object (referenceObject).`, + `When the user refers to a related record by its name (e.g. "belonging to Gaona Family"),`, + `output the human-readable name as the filter value — the system will resolve it to the correct ID.`, + `Use the LOOKUP field apiName (e.g. familyId) as the filter field.`, ``, `=== OUTPUT FORMAT ===`, `Return a JSON object with these keys:`,