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 we found subagent results, use them; otherwise use defaults
|
||||||
if (subagentResult) {
|
if (subagentResult) {
|
||||||
console.log('=== DEEP AGENT: Using subagent result ===');
|
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 {
|
return {
|
||||||
reply: replyText,
|
reply: replyText,
|
||||||
action: subagentResult.action || 'clarify',
|
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.',
|
'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.',
|
'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:',
|
'Core Responsibilities:',
|
||||||
'- Parse user requests to understand what records they want to create or find',
|
'- Parse user requests to understand what records they want to create or find',
|
||||||
'- Identify the record type (Account, Contact, ContactDetail, etc.)',
|
'- Identify the record type (Account, Contact, ContactDetail, etc.)',
|
||||||
@@ -289,20 +308,22 @@ export class AiAssistantService {
|
|||||||
'- When user says "Create X under Y account":',
|
'- When user says "Create X under Y account":',
|
||||||
' 1. X is NOT an Account - it is a Contact or child record',
|
' 1. X is NOT an Account - it is a Contact or child record',
|
||||||
' 2. Y is the parent Account name',
|
' 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',
|
'- Accounts are top-level organizations/companies',
|
||||||
'- Contacts are people/entities that belong to Accounts',
|
'- Contacts are people/entities that belong to Accounts',
|
||||||
'- ContactDetails are phone/email/address records for Contacts or Accounts',
|
'- ContactDetails are phone/email/address records for Contacts or Accounts',
|
||||||
'',
|
'',
|
||||||
'Multi-Step Example:',
|
'Multi-Step Example:',
|
||||||
'- User: "Create Chipi under Jeannete Staley account"',
|
'- User: "Create Chipi under Jeannete Staley account"',
|
||||||
' Step 1: Invoke subagent to find/create Account "Jeannete Staley" (objectApiName: Account)',
|
' Step 1: Invoke subagent "Find Account Jeannete Staley (objectApiName: Account)"',
|
||||||
' Step 2: Once account exists, invoke subagent to create Contact "Chipi" linked to that Account (objectApiName: Contact)',
|
' 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:',
|
'When invoking the subagent:',
|
||||||
'- Include the record type in parentheses: "Find Account Jeannete Staley (objectApiName: Account)"',
|
'- To search: "Find <Type> <Name> (objectApiName: <Type>)"',
|
||||||
'- For child records: "Create Contact Chipi for Account <id> (objectApiName: Contact)"',
|
'- To create: "Create <Type> <Name> (objectApiName: <Type>)"',
|
||||||
'- Pass the user\'s full request with context',
|
'- Include any known IDs: "Create Contact Chipi accountId=abc123 (objectApiName: Contact)"',
|
||||||
'',
|
'',
|
||||||
'DO NOT try to create records yourself - ALWAYS use the subagent for:',
|
'DO NOT try to create records yourself - ALWAYS use the subagent for:',
|
||||||
' * Finding existing records',
|
' * Finding existing records',
|
||||||
@@ -500,6 +521,9 @@ export class AiAssistantService {
|
|||||||
// Try to infer from keywords and patterns
|
// Try to infer from keywords and patterns
|
||||||
const lowerMsg = messageText.toLowerCase();
|
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
|
// 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);
|
const underAccountMatch = messageText.match(/create\s+([\w\s]+?)\s+(?:under|for)\s+([\w\s]+?)\s+account/i);
|
||||||
if (underAccountMatch) {
|
if (underAccountMatch) {
|
||||||
@@ -514,7 +538,7 @@ export class AiAssistantService {
|
|||||||
extractedContext.objectApiName = 'Contact';
|
extractedContext.objectApiName = 'Contact';
|
||||||
}
|
}
|
||||||
console.log('Inferred child object type:', extractedContext.objectApiName);
|
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';
|
extractedContext.objectApiName = 'Account';
|
||||||
} else if (lowerMsg.includes('contact') && !lowerMsg.includes('contact detail')) {
|
} else if (lowerMsg.includes('contact') && !lowerMsg.includes('contact detail')) {
|
||||||
extractedContext.objectApiName = 'Contact';
|
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
|
// Clean the message text from system context annotations
|
||||||
const cleanMessage = messageText
|
const cleanMessage = messageText
|
||||||
.replace(/\[System Context:[^\]]+\]/g, '')
|
.replace(/\[System Context:[^\]]+\]/g, '')
|
||||||
@@ -574,6 +607,10 @@ export class AiAssistantService {
|
|||||||
console.log('=== SUBAGENT: Load Context ===');
|
console.log('=== SUBAGENT: Load Context ===');
|
||||||
return this.loadContext(tenantId, current);
|
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) => {
|
.addNode('extractFields', async (current: AiAssistantState) => {
|
||||||
console.log('=== SUBAGENT: Extract Fields ===');
|
console.log('=== SUBAGENT: Extract Fields ===');
|
||||||
return this.extractFields(tenantId, current);
|
return this.extractFields(tenantId, current);
|
||||||
@@ -607,6 +644,7 @@ export class AiAssistantService {
|
|||||||
record: current.record,
|
record: current.record,
|
||||||
missingFields: current.missingFields,
|
missingFields: current.missingFields,
|
||||||
extractedFields: current.extractedFields,
|
extractedFields: current.extractedFields,
|
||||||
|
foundExisting: current.record?.wasFound || false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -617,7 +655,11 @@ export class AiAssistantService {
|
|||||||
})
|
})
|
||||||
.addEdge(START, 'transformInput')
|
.addEdge(START, 'transformInput')
|
||||||
.addEdge('transformInput', 'loadContext')
|
.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')
|
.addEdge('extractFields', 'decideNext')
|
||||||
.addConditionalEdges('decideNext', (current: AiAssistantState) => {
|
.addConditionalEdges('decideNext', (current: AiAssistantState) => {
|
||||||
return current.action === 'create_record' ? 'createRecord' : 'respondMissing';
|
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(
|
private async extractFields(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
state: AiAssistantState,
|
state: AiAssistantState,
|
||||||
|
|||||||
Reference in New Issue
Block a user