333 lines
9.3 KiB
TypeScript
333 lines
9.3 KiB
TypeScript
import { randomUUID } from 'crypto';
|
|
import { AiProcess, AiProcessVersion, AiToolConfig } from '../src/models/ai-process.model';
|
|
|
|
// Bootstrap NestJS to get proper services
|
|
async function getTenantContext(tenantSlugOrId: string) {
|
|
const { NestFactory } = await import('@nestjs/core');
|
|
const { AppModule } = await import('../src/app.module');
|
|
const { TenantDatabaseService } = await import('../src/tenant/tenant-database.service');
|
|
|
|
// Create app context (without listening)
|
|
const app = await NestFactory.createApplicationContext(AppModule, {
|
|
logger: false,
|
|
});
|
|
|
|
const tenantDbService = app.get(TenantDatabaseService);
|
|
|
|
// Resolve tenant ID
|
|
const tenantId = await tenantDbService.resolveTenantId(tenantSlugOrId);
|
|
|
|
// Get proper Knex connection
|
|
const knex = await tenantDbService.getTenantKnexById(tenantId);
|
|
|
|
return { tenantId, knex, app };
|
|
}
|
|
|
|
/**
|
|
* Seed script for demo AI Process: Register New Pet
|
|
*
|
|
* This process demonstrates:
|
|
* - Conditional logic (find or create account/contact)
|
|
* - Tool usage (findAccount, createAccount, findContact, createContact, createPet)
|
|
* - Sequential execution
|
|
* - LLM decision nodes with structured JSON output
|
|
*
|
|
* Usage:
|
|
* npm run seed:demo-process -- <tenant-slug-or-id>
|
|
*/
|
|
|
|
const demoProcessGraph = {
|
|
id: 'register_new_pet',
|
|
name: 'Register New Pet',
|
|
description: 'Complete pet registration workflow with account and contact resolution',
|
|
allowCycles: false,
|
|
nodes: [
|
|
{
|
|
id: 'start',
|
|
type: 'Start',
|
|
position: { x: 250, y: 50 },
|
|
data: { label: 'Start' },
|
|
},
|
|
{
|
|
id: 'extract_info',
|
|
type: 'LLMDecisionNode',
|
|
position: { x: 250, y: 150 },
|
|
data: {
|
|
label: 'Extract Pet Info',
|
|
promptTemplate: `Extract pet registration information from the user message.
|
|
|
|
User message: {{state.message}}
|
|
|
|
Extract:
|
|
- Pet name (required)
|
|
- Pet species (required, e.g., "dog", "cat", "bird")
|
|
- Pet breed (optional)
|
|
- Pet age (optional, as number)
|
|
- Owner first name (required)
|
|
- Owner last name (required)
|
|
- Owner email (optional)
|
|
- Owner phone (optional)
|
|
- Account/Company name (optional, defaults to owner's full name)
|
|
|
|
Return JSON with these exact fields.`,
|
|
inputKeys: ['message'],
|
|
outputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
petName: { type: 'string' },
|
|
species: { type: 'string' },
|
|
breed: { type: 'string' },
|
|
age: { type: 'number' },
|
|
ownerFirstName: { type: 'string' },
|
|
ownerLastName: { type: 'string' },
|
|
ownerEmail: { type: 'string' },
|
|
ownerPhone: { type: 'string' },
|
|
accountName: { type: 'string' },
|
|
},
|
|
required: ['petName', 'species', 'ownerFirstName', 'ownerLastName'],
|
|
},
|
|
model: { name: 'gpt-4o', temperature: 0 },
|
|
},
|
|
},
|
|
{
|
|
id: 'find_account',
|
|
type: 'ToolNode',
|
|
position: { x: 250, y: 280 },
|
|
data: {
|
|
label: 'Find Account',
|
|
toolName: 'findAccount',
|
|
argsTemplate: {
|
|
name: '{{state.accountName}}',
|
|
email: '{{state.ownerEmail}}',
|
|
},
|
|
outputMapping: {
|
|
found: 'accountFound',
|
|
accountId: 'accountId',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'create_account',
|
|
type: 'ToolNode',
|
|
position: { x: 450, y: 380 },
|
|
data: {
|
|
label: 'Create Account',
|
|
toolName: 'createAccount',
|
|
argsTemplate: {
|
|
name: '{{state.accountName}}',
|
|
email: '{{state.ownerEmail}}',
|
|
phone: '{{state.ownerPhone}}',
|
|
},
|
|
outputMapping: {
|
|
accountId: 'accountId',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'find_contact',
|
|
type: 'ToolNode',
|
|
position: { x: 250, y: 480 },
|
|
data: {
|
|
label: 'Find Contact',
|
|
toolName: 'findContact',
|
|
argsTemplate: {
|
|
firstName: '{{state.ownerFirstName}}',
|
|
lastName: '{{state.ownerLastName}}',
|
|
email: '{{state.ownerEmail}}',
|
|
accountId: '{{state.accountId}}',
|
|
},
|
|
outputMapping: {
|
|
found: 'contactFound',
|
|
contactId: 'contactId',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'create_contact',
|
|
type: 'ToolNode',
|
|
position: { x: 450, y: 580 },
|
|
data: {
|
|
label: 'Create Contact',
|
|
toolName: 'createContact',
|
|
argsTemplate: {
|
|
firstName: '{{state.ownerFirstName}}',
|
|
lastName: '{{state.ownerLastName}}',
|
|
email: '{{state.ownerEmail}}',
|
|
phone: '{{state.ownerPhone}}',
|
|
accountId: '{{state.accountId}}',
|
|
},
|
|
outputMapping: {
|
|
contactId: 'contactId',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'create_pet',
|
|
type: 'ToolNode',
|
|
position: { x: 250, y: 680 },
|
|
data: {
|
|
label: 'Create Pet Record',
|
|
toolName: 'createPet',
|
|
argsTemplate: {
|
|
name: '{{state.petName}}',
|
|
species: '{{state.species}}',
|
|
breed: '{{state.breed}}',
|
|
age: '{{state.age}}',
|
|
ownerId: '{{state.contactId}}',
|
|
},
|
|
outputMapping: {
|
|
petId: 'petId',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'end',
|
|
type: 'End',
|
|
position: { x: 250, y: 780 },
|
|
data: { label: 'End' },
|
|
},
|
|
],
|
|
edges: [
|
|
{ id: 'e1', source: 'start', target: 'extract_info' },
|
|
{ id: 'e2', source: 'extract_info', target: 'find_account' },
|
|
{
|
|
id: 'e3',
|
|
source: 'find_account',
|
|
target: 'find_contact',
|
|
condition: { '==': [{ var: 'accountFound' }, true] },
|
|
},
|
|
{
|
|
id: 'e4',
|
|
source: 'find_account',
|
|
target: 'create_account',
|
|
condition: { '==': [{ var: 'accountFound' }, false] },
|
|
},
|
|
{ id: 'e5', source: 'create_account', target: 'find_contact' },
|
|
{
|
|
id: 'e6',
|
|
source: 'find_contact',
|
|
target: 'create_pet',
|
|
condition: { '==': [{ var: 'contactFound' }, true] },
|
|
},
|
|
{
|
|
id: 'e7',
|
|
source: 'find_contact',
|
|
target: 'create_contact',
|
|
condition: { '==': [{ var: 'contactFound' }, false] },
|
|
},
|
|
{ id: 'e8', source: 'create_contact', target: 'create_pet' },
|
|
{ id: 'e9', source: 'create_pet', target: 'end' },
|
|
],
|
|
};
|
|
|
|
const demoTools = [
|
|
'findAccount',
|
|
'createAccount',
|
|
'findContact',
|
|
'createContact',
|
|
'createPet',
|
|
];
|
|
|
|
async function seedDemoProcess(tenantSlugOrId: string) {
|
|
let app;
|
|
try {
|
|
console.log(`\n🌱 Seeding demo AI process for tenant: ${tenantSlugOrId}\n`);
|
|
|
|
const context = await getTenantContext(tenantSlugOrId);
|
|
const { tenantId, knex, app: nestApp } = context;
|
|
app = nestApp;
|
|
|
|
console.log(`✓ Resolved tenant ID: ${tenantId}`);
|
|
console.log(`✓ Connected to tenant database`);
|
|
|
|
// Check if process already exists
|
|
const existing = await AiProcess.query(knex)
|
|
.where('name', demoProcessGraph.name)
|
|
.first();
|
|
|
|
if (existing) {
|
|
console.log(`⚠ Process "${demoProcessGraph.name}" already exists (ID: ${existing.id})`);
|
|
console.log(` To create a new version, update via the UI.`);
|
|
return;
|
|
}
|
|
|
|
// Create process in transaction
|
|
await knex.transaction(async (trx) => {
|
|
const processId = randomUUID();
|
|
const userId = 'system'; // System user for seed data
|
|
|
|
// Create process
|
|
await AiProcess.query(trx).insert({
|
|
id: processId,
|
|
name: demoProcessGraph.name,
|
|
description: demoProcessGraph.description,
|
|
latestVersion: 1,
|
|
createdBy: userId,
|
|
});
|
|
console.log(`✓ Created process: ${demoProcessGraph.name} (${processId})`);
|
|
|
|
// Create initial version
|
|
// Note: In production, this would call the compiler service
|
|
// For seed, we're storing a simplified version
|
|
await AiProcessVersion.query(trx).insert({
|
|
id: randomUUID(),
|
|
processId,
|
|
version: 1,
|
|
graphJson: demoProcessGraph,
|
|
compiledJson: {
|
|
graphId: demoProcessGraph.id,
|
|
version: 1,
|
|
nodes: demoProcessGraph.nodes,
|
|
edges: demoProcessGraph.edges,
|
|
startNodeId: 'start',
|
|
endNodeIds: ['end'],
|
|
adjacency: {},
|
|
},
|
|
createdBy: userId,
|
|
});
|
|
console.log(`✓ Created process version 1`);
|
|
|
|
// Enable demo tools for tenant
|
|
for (const toolName of demoTools) {
|
|
const existingTool = await AiToolConfig.query(trx)
|
|
.where('tool_name', toolName)
|
|
.first();
|
|
|
|
if (!existingTool) {
|
|
await AiToolConfig.query(trx).insert({
|
|
id: randomUUID(),
|
|
toolName,
|
|
enabled: true,
|
|
});
|
|
console.log(`✓ Enabled tool: ${toolName}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`\n✅ Demo process seeded successfully!\n`);
|
|
console.log(`Next steps:`);
|
|
console.log(` 1. Navigate to /ai-processes in your frontend`);
|
|
console.log(` 2. Open the "${demoProcessGraph.name}" process`);
|
|
console.log(` 3. Test it by sending a message like:`);
|
|
console.log(` "Register a dog named Max, owned by John Smith (john@email.com)"`);
|
|
console.log();
|
|
|
|
if (app) await app.close();
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error('❌ Seed failed:', error);
|
|
if (app) await app.close();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Get tenant from command line args
|
|
const tenantSlugOrId = process.argv[2];
|
|
|
|
if (!tenantSlugOrId) {
|
|
console.error('Usage: npm run seed:demo-process -- <tenant-slug-or-id>');
|
|
process.exit(1);
|
|
}
|
|
|
|
seedDemoProcess(tenantSlugOrId);
|