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 -- */ 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 -- '); process.exit(1); } seedDemoProcess(tenantSlugOrId);