WIP - some success creating related records
This commit is contained in:
@@ -234,6 +234,13 @@ export class AiAssistantService {
|
||||
// If we found subagent results, use them; otherwise use defaults
|
||||
if (subagentResult) {
|
||||
console.log('=== DEEP AGENT: Using subagent result ===');
|
||||
|
||||
// If a record was found/created, log it prominently
|
||||
if (subagentResult.record) {
|
||||
const wasFound = subagentResult.foundExisting || subagentResult.record.wasFound;
|
||||
console.log(`!!! Record ${wasFound ? 'FOUND' : 'CREATED'}: ID = ${subagentResult.record.id}, Name = ${subagentResult.record.name || 'N/A'}`);
|
||||
}
|
||||
|
||||
return {
|
||||
reply: replyText,
|
||||
action: subagentResult.action || 'clarify',
|
||||
@@ -279,6 +286,18 @@ export class AiAssistantService {
|
||||
'You are an AI assistant helping users interact with a CRM system through natural conversation.',
|
||||
'Your role is to understand user requests and coordinate with specialized subagents to fulfill them.',
|
||||
'',
|
||||
'CRITICAL RULE: ALWAYS SEARCH BEFORE CREATE',
|
||||
'- Before creating ANY record, you MUST first search to see if it already exists',
|
||||
'- Use the subagent to search: "Find Account Jeannete Staley (objectApiName: Account)"',
|
||||
'- Only create if the search returns no results',
|
||||
'- If a record is found, USE its ID - do not create a duplicate',
|
||||
'',
|
||||
'CRITICAL RULE: REMEMBER PREVIOUS RESULTS',
|
||||
'- When the subagent returns a record (found or created), save its ID',
|
||||
'- Use that exact ID in subsequent operations',
|
||||
'- Example: After finding Account with id="123", use that ID when creating related Contact',
|
||||
'- DO NOT ask the subagent to find the same record again - you already have it',
|
||||
'',
|
||||
'Core Responsibilities:',
|
||||
'- Parse user requests to understand what records they want to create or find',
|
||||
'- Identify the record type (Account, Contact, ContactDetail, etc.)',
|
||||
@@ -289,20 +308,22 @@ export class AiAssistantService {
|
||||
'- When user says "Create X under Y account":',
|
||||
' 1. X is NOT an Account - it is a Contact or child record',
|
||||
' 2. Y is the parent Account name',
|
||||
' 3. You must: (a) First ensure Y account exists, (b) Then create X as a Contact under Y',
|
||||
' 3. You must: (a) First SEARCH for Y account, (b) If not found, CREATE it, (c) Then create X as a Contact under Y',
|
||||
'- Accounts are top-level organizations/companies',
|
||||
'- Contacts are people/entities that belong to Accounts',
|
||||
'- ContactDetails are phone/email/address records for Contacts or Accounts',
|
||||
'',
|
||||
'Multi-Step Example:',
|
||||
'- User: "Create Chipi under Jeannete Staley account"',
|
||||
' Step 1: Invoke subagent to find/create Account "Jeannete Staley" (objectApiName: Account)',
|
||||
' Step 2: Once account exists, invoke subagent to create Contact "Chipi" linked to that Account (objectApiName: Contact)',
|
||||
' Step 1: Invoke subagent "Find Account Jeannete Staley (objectApiName: Account)"',
|
||||
' Step 2a: If found, note the account ID from response',
|
||||
' Step 2b: If not found, invoke subagent "Create Account Jeannete Staley (objectApiName: Account)" and note the ID',
|
||||
' Step 3: Invoke subagent "Create Contact Chipi for accountId=<the ID from step 2> (objectApiName: Contact)"',
|
||||
'',
|
||||
'When invoking the subagent:',
|
||||
'- Include the record type in parentheses: "Find Account Jeannete Staley (objectApiName: Account)"',
|
||||
'- For child records: "Create Contact Chipi for Account <id> (objectApiName: Contact)"',
|
||||
'- Pass the user\'s full request with context',
|
||||
'- To search: "Find <Type> <Name> (objectApiName: <Type>)"',
|
||||
'- To create: "Create <Type> <Name> (objectApiName: <Type>)"',
|
||||
'- Include any known IDs: "Create Contact Chipi accountId=abc123 (objectApiName: Contact)"',
|
||||
'',
|
||||
'DO NOT try to create records yourself - ALWAYS use the subagent for:',
|
||||
' * Finding existing records',
|
||||
@@ -500,6 +521,9 @@ export class AiAssistantService {
|
||||
// Try to infer from keywords and patterns
|
||||
const lowerMsg = messageText.toLowerCase();
|
||||
|
||||
// Check if this is a search/find operation
|
||||
const isFindOperation = lowerMsg.includes('find') || lowerMsg.includes('search') || lowerMsg.includes('look for');
|
||||
|
||||
// Pattern: "Create X under/for Y account" - X is NOT an account, it's a child record
|
||||
const underAccountMatch = messageText.match(/create\s+([\w\s]+?)\s+(?:under|for)\s+([\w\s]+?)\s+account/i);
|
||||
if (underAccountMatch) {
|
||||
@@ -514,7 +538,7 @@ export class AiAssistantService {
|
||||
extractedContext.objectApiName = 'Contact';
|
||||
}
|
||||
console.log('Inferred child object type:', extractedContext.objectApiName);
|
||||
} else if (lowerMsg.includes('account') && (lowerMsg.includes('create') || lowerMsg.includes('add'))) {
|
||||
} else if (lowerMsg.includes('account') && (lowerMsg.includes('create') || lowerMsg.includes('add') || isFindOperation)) {
|
||||
extractedContext.objectApiName = 'Account';
|
||||
} else if (lowerMsg.includes('contact') && !lowerMsg.includes('contact detail')) {
|
||||
extractedContext.objectApiName = 'Contact';
|
||||
@@ -541,6 +565,15 @@ export class AiAssistantService {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a record ID being passed from previous operation
|
||||
const recordIdMatch = messageText.match(/(?:accountId|contactId|recordId)=(\S+)/i);
|
||||
if (recordIdMatch) {
|
||||
if (!extractedFields) extractedFields = {};
|
||||
const idField = recordIdMatch[0].split('=')[0];
|
||||
extractedFields[idField] = recordIdMatch[1];
|
||||
console.log('Extracted ID from message:', { [idField]: recordIdMatch[1] });
|
||||
}
|
||||
|
||||
// Clean the message text from system context annotations
|
||||
const cleanMessage = messageText
|
||||
.replace(/\[System Context:[^\]]+\]/g, '')
|
||||
@@ -574,6 +607,10 @@ export class AiAssistantService {
|
||||
console.log('=== SUBAGENT: Load Context ===');
|
||||
return this.loadContext(tenantId, current);
|
||||
})
|
||||
.addNode('searchExisting', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Search Existing ===');
|
||||
return this.searchExistingRecord(tenantId, userId, current);
|
||||
})
|
||||
.addNode('extractFields', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Extract Fields ===');
|
||||
return this.extractFields(tenantId, current);
|
||||
@@ -607,6 +644,7 @@ export class AiAssistantService {
|
||||
record: current.record,
|
||||
missingFields: current.missingFields,
|
||||
extractedFields: current.extractedFields,
|
||||
foundExisting: current.record?.wasFound || false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -617,7 +655,11 @@ export class AiAssistantService {
|
||||
})
|
||||
.addEdge(START, 'transformInput')
|
||||
.addEdge('transformInput', 'loadContext')
|
||||
.addEdge('loadContext', 'extractFields')
|
||||
.addEdge('loadContext', 'searchExisting')
|
||||
.addConditionalEdges('searchExisting', (current: AiAssistantState) => {
|
||||
// If record was found, skip to formatOutput
|
||||
return current.record ? 'formatOutput' : 'extractFields';
|
||||
})
|
||||
.addEdge('extractFields', 'decideNext')
|
||||
.addConditionalEdges('decideNext', (current: AiAssistantState) => {
|
||||
return current.action === 'create_record' ? 'createRecord' : 'respondMissing';
|
||||
@@ -669,6 +711,97 @@ export class AiAssistantService {
|
||||
};
|
||||
}
|
||||
|
||||
private async searchExistingRecord(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
state: AiAssistantState,
|
||||
): Promise<AiAssistantState> {
|
||||
if (!state.objectDefinition || !state.message) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Check if this is a find/search operation or if we should check for existing
|
||||
const lowerMessage = state.message.toLowerCase();
|
||||
const isFindOperation = lowerMessage.includes('find') || lowerMessage.includes('search') || lowerMessage.includes('look for');
|
||||
const isCreateOperation = lowerMessage.includes('create') || lowerMessage.includes('add');
|
||||
|
||||
// Extract the name to search for
|
||||
let searchName: string | null = null;
|
||||
|
||||
// Pattern: "Find Account X"
|
||||
const findMatch = state.message.match(/(?:find|search|look for)\s+(?:\w+\s+)?(.+?)(?:\(|$)/i);
|
||||
if (findMatch) {
|
||||
searchName = findMatch[1].trim();
|
||||
} else if (isCreateOperation) {
|
||||
// Pattern: "Create Account X" - check if X already exists
|
||||
const createMatch = state.message.match(/(?:create|add)\s+(?:\w+\s+)?(.+?)(?:\s+(?:under|for|with)|\(|$)/i);
|
||||
if (createMatch) {
|
||||
searchName = createMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!searchName) {
|
||||
console.log('No search name extracted, skipping search');
|
||||
return state;
|
||||
}
|
||||
|
||||
console.log(`Searching for existing ${state.objectDefinition.apiName}: "${searchName}"`);
|
||||
|
||||
try {
|
||||
// Use Meilisearch if available
|
||||
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||
|
||||
if (this.meilisearchService.isEnabled()) {
|
||||
const displayField = this.getDisplayFieldForObject(state.objectDefinition);
|
||||
const meiliMatch = await this.meilisearchService.searchRecord(
|
||||
resolvedTenantId,
|
||||
state.objectDefinition.apiName,
|
||||
searchName,
|
||||
displayField,
|
||||
);
|
||||
|
||||
if (meiliMatch?.id) {
|
||||
console.log('Found existing record via Meilisearch:', meiliMatch.id);
|
||||
return {
|
||||
...state,
|
||||
record: { ...meiliMatch, wasFound: true },
|
||||
action: 'create_record',
|
||||
reply: `Found existing ${state.objectDefinition.label || state.objectDefinition.apiName} "${searchName}" (ID: ${meiliMatch.id}).`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to database search
|
||||
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||
const tableName = this.toTableName(
|
||||
state.objectDefinition.apiName,
|
||||
state.objectDefinition.label,
|
||||
state.objectDefinition.pluralLabel,
|
||||
);
|
||||
const displayField = this.getDisplayFieldForObject(state.objectDefinition);
|
||||
|
||||
const record = await knex(tableName)
|
||||
.whereRaw('LOWER(??) = ?', [displayField, searchName.toLowerCase()])
|
||||
.first();
|
||||
|
||||
if (record?.id) {
|
||||
console.log('Found existing record via database:', record.id);
|
||||
return {
|
||||
...state,
|
||||
record: { ...record, wasFound: true },
|
||||
action: 'create_record',
|
||||
reply: `Found existing ${state.objectDefinition.label || state.objectDefinition.apiName} "${searchName}" (ID: ${record.id}).`,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('No existing record found, will proceed to create');
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.error('Error searching for existing record:', error.message);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
private async extractFields(
|
||||
tenantId: string,
|
||||
state: AiAssistantState,
|
||||
|
||||
Reference in New Issue
Block a user