From 1d2f495e2f6e6196c9df297c4b478cad0f7da3ff Mon Sep 17 00:00:00 2001 From: Nicola Miotto Date: Thu, 17 Jul 2025 10:59:41 +0200 Subject: [PATCH] fix slug and reporting --- src/commands/report.ts | 267 +++-- src/commands/simple-organization-setup.ts | 549 +++++---- src/generated/core/OpenAPI.ts | 38 +- src/generated/core/request.ts | 534 +++++---- src/generated/services/ReplicasService.ts | 1241 ++++++++++++--------- src/utils/files.ts | 652 +++++++---- 6 files changed, 1923 insertions(+), 1358 deletions(-) diff --git a/src/commands/report.ts b/src/commands/report.ts index 728d049..9c14fb8 100644 --- a/src/commands/report.ts +++ b/src/commands/report.ts @@ -1,12 +1,16 @@ -import { Command } from 'commander'; -import chalk from 'chalk'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ConfigManager } from '../config/manager'; -import { configureOpenAPI } from '../utils/openapi-config'; -import inquirer from 'inquirer'; -import { ApiError, UsersService, ReplicasService, TrainingService, KnowledgeBaseService } from '../generated/index'; -import { ProgressManager } from '../utils/progress'; +import { Command } from "commander"; +import chalk from "chalk"; +import * as fs from "fs-extra"; +import * as path from "path"; +import { ConfigManager } from "../config/manager"; +import { configureOpenAPI } from "../utils/openapi-config"; +import inquirer from "inquirer"; +import { + ApiError, + ReplicasService, + KnowledgeBaseService, +} from "../generated/index"; +import { ProgressManager } from "../utils/progress"; interface ReportOptions { output?: string; @@ -17,106 +21,124 @@ interface ReportOptions { veryVerbose?: boolean; } -export async function reportCommand(folderPath?: string, options: ReportOptions = {}): Promise { - const targetPath = folderPath || '.'; +export async function reportCommand( + folderPath?: string, + options: ReportOptions = {} +): Promise { + const targetPath = folderPath || "."; const progress = new ProgressManager(); try { // Load configurations const effectiveConfig = await ConfigManager.getEffectiveConfig(targetPath); - + // Get API key let apiKey = options.apiKey || effectiveConfig.apiKey; - + if (!apiKey && !options.nonInteractive) { const { apiKeyInput } = await inquirer.prompt({ - type: 'password', - name: 'apiKeyInput', - message: 'API Key:', - validate: (input: string) => input.trim().length > 0 || 'API key is required' + type: "password", + name: "apiKeyInput", + message: "API Key:", + validate: (input: string) => + input.trim().length > 0 || "API key is required", }); apiKey = apiKeyInput; } - + if (!apiKey) { - console.error(chalk.red('❌ No API key found. Please run "sensay claim-key" first or provide --apikey option.')); + console.error( + chalk.red( + '❌ No API key found. Please run "sensay claim-key" first or provide --apikey option.' + ) + ); process.exit(1); } // Get output file path - const defaultOutput = path.join(targetPath, 'organization-report.md'); + const defaultOutput = path.join(targetPath, "organization-report.md"); let outputPath = options.output || defaultOutput; - + if (!options.output && !options.silent && !options.nonInteractive) { const { outputInput } = await inquirer.prompt({ - type: 'input', - name: 'outputInput', - message: 'Output file path:', - default: defaultOutput + type: "input", + name: "outputInput", + message: "Output file path:", + default: defaultOutput, }); outputPath = outputInput; } // Configure the OpenAPI client - configureOpenAPI({ + configureOpenAPI({ apiKey, - verbose: options.verbose, - veryVerbose: options.veryVerbose + verbose: options.verbose, + veryVerbose: options.veryVerbose, }); // Start generating report - const mainSpinner = progress.createSpinner('report', 'Generating organization report...'); + const mainSpinner = progress.createSpinner( + "report", + "Generating organization report..." + ); try { // Fetch all entity data - mainSpinner.text = 'Fetching replicas...'; + mainSpinner.text = "Fetching replicas..."; const replicasResponse = await ReplicasService.getV1Replicas(); const replicas = replicasResponse.items || []; // Fetch knowledge base items for all replicas - mainSpinner.text = 'Fetching knowledge base items...'; + mainSpinner.text = "Fetching knowledge base items..."; const knowledgeBasePromises = replicas.map(async (replica) => { try { - const response = await KnowledgeBaseService.getV1ReplicasKnowledgeBase(replica.uuid!); + const response = + await KnowledgeBaseService.getV1ReplicasKnowledgeBase( + replica.uuid! + ); return { replicaUuid: replica.uuid!, items: response.items || [] }; } catch (error) { // If fetching fails for a replica, return empty array return { replicaUuid: replica.uuid!, items: [] }; } }); - + const knowledgeBaseResults = await Promise.all(knowledgeBasePromises); const knowledgeBaseMap = new Map( - knowledgeBaseResults.map(result => [result.replicaUuid, result.items]) + knowledgeBaseResults.map((result) => [result.replicaUuid, result.items]) ); // Generate the markdown report - mainSpinner.text = 'Generating markdown report...'; - const report = generateMarkdownReport(effectiveConfig, replicas, knowledgeBaseMap); + mainSpinner.text = "Generating markdown report..."; + const report = generateMarkdownReport( + effectiveConfig, + replicas, + knowledgeBaseMap + ); // Write the report to file - mainSpinner.text = 'Writing report to file...'; + mainSpinner.text = "Writing report to file..."; await fs.ensureDir(path.dirname(outputPath)); - await fs.writeFile(outputPath, report, 'utf8'); + await fs.writeFile(outputPath, report, "utf8"); mainSpinner.succeed(`Report generated successfully: ${outputPath}`); - + if (!options.silent) { - console.log(chalk.green(`\n✓ Organization report saved to: ${outputPath}`)); + console.log( + chalk.green(`\n✓ Organization report saved to: ${outputPath}`) + ); } - } catch (error: any) { - mainSpinner.fail('Failed to generate report'); + mainSpinner.fail("Failed to generate report"); throw error; } - } catch (error: any) { - console.error(chalk.red('\n❌ Report generation failed:')); - + console.error(chalk.red("\n❌ Report generation failed:")); + if (error instanceof ApiError) { console.error(chalk.red(`Status: ${error.status}`)); console.error(chalk.red(`Error: ${error.message}`)); - + if (error.body) { const body = error.body as any; if (body.request_id) { @@ -135,16 +157,19 @@ export async function reportCommand(folderPath?: string, options: ReportOptions console.error(chalk.red(`Message: ${body.message}`)); } } - + // Log request details in verbose mode if (options.verbose || options.veryVerbose) { - console.error(chalk.gray('\nRequest details:')); + console.error(chalk.gray("\nRequest details:")); console.error(chalk.gray(`URL: ${error.url}`)); console.error(chalk.gray(`Method: ${error.request.method}`)); if (error.request.headers) { - console.error(chalk.gray('Headers:')); + console.error(chalk.gray("Headers:")); Object.entries(error.request.headers).forEach(([key, value]) => { - if (key.toLowerCase().includes('secret') || key.toLowerCase().includes('key')) { + if ( + key.toLowerCase().includes("secret") || + key.toLowerCase().includes("key") + ) { console.error(chalk.gray(` ${key}: [REDACTED]`)); } else { console.error(chalk.gray(` ${key}: ${value}`)); @@ -155,8 +180,8 @@ export async function reportCommand(folderPath?: string, options: ReportOptions } else { console.error(chalk.red(`Error: ${error.message || error}`)); } - - if (process.env.NODE_ENV !== 'test') { + + if (process.env.NODE_ENV !== "test") { process.exit(1); } else { throw error; @@ -170,97 +195,107 @@ function generateMarkdownReport( knowledgeBaseMap: Map ): string { const lines: string[] = []; - + // Header lines.push(`# Organization Report`); - lines.push(''); + lines.push(""); lines.push(`**Generated:** ${new Date().toISOString()}`); - lines.push(`**Organization ID:** ${config.organizationId || 'Unknown'}`); - lines.push(''); - + lines.push(`**Organization ID:** ${config.organizationId || "Unknown"}`); + lines.push(""); + // Replicas Section - lines.push('## Replicas'); - lines.push(''); - + lines.push("## Replicas"); + lines.push(""); + if (replicas.length === 0) { - lines.push('*No replicas found*'); + lines.push("*No replicas found*"); } else { // Table header - lines.push('| Name | Model | UUID | System Message | Created |'); - lines.push('|------|-------|------|----------------|---------|'); - + lines.push("| Name | Model | UUID | System Message | Created |"); + lines.push("|------|-------|------|----------------|---------|"); + // Table rows - replicas.forEach(replica => { - const name = replica.name || 'N/A'; - const model = replica.model || 'N/A'; - const uuid = replica.uuid || 'N/A'; - const systemMessage = replica.systemMessage ? - (replica.systemMessage.length > 50 ? - replica.systemMessage.substring(0, 47) + '...' : - replica.systemMessage) : 'N/A'; - const created = replica.createdAt ? new Date(replica.createdAt).toLocaleDateString() : 'N/A'; - - lines.push(`| ${name} | ${model} | ${uuid} | ${systemMessage} | ${created} |`); + replicas.forEach((replica) => { + const name = replica.name || "N/A"; + const model = replica.model || "N/A"; + const uuid = replica.uuid || "N/A"; + const systemMessage = replica.systemMessage + ? replica.systemMessage.length > 50 + ? replica.systemMessage.substring(0, 47) + "..." + : replica.systemMessage + : "N/A"; + const created = replica.createdAt + ? new Date(replica.createdAt).toLocaleDateString() + : "N/A"; + + lines.push( + `| ${name} | ${model} | ${uuid} | ${systemMessage} | ${created} |` + ); }); } - - lines.push(''); - + + lines.push(""); + // Knowledge Base Items Section - lines.push('## Knowledge Base Items'); - lines.push(''); - + lines.push("## Knowledge Base Items"); + lines.push(""); + let hasAnyItems = false; - replicas.forEach(replica => { + replicas.forEach((replica) => { const items = knowledgeBaseMap.get(replica.uuid!) || []; if (items.length > 0) { hasAnyItems = true; lines.push(`### Replica: ${replica.name}`); - lines.push(''); - + lines.push(""); + // Table header - lines.push('| Filename | Status | Type | Size | KB ID |'); - lines.push('|----------|---------|------|------|-------|'); - + lines.push("| Filename | Status | Type | Size | KB ID |"); + lines.push("|----------|---------|------|------|-------|"); + // Table rows - items.forEach(item => { - const filename = item.filename || 'N/A'; - const status = item.status || 'N/A'; - const type = item.type || 'N/A'; - const size = item.size ? `${(item.size / 1024).toFixed(2)} KB` : 'N/A'; - const kbId = item.id || 'N/A'; - + items.forEach((item) => { + const filename = item.file.name || "N/A"; + const status = item.status || "N/A"; + const type = item.file.mimeType || "N/A"; + const size = item.file.size + ? (item.file.size >>> 10) + "." + (item.file.size & 0x3ff) + "KB" + : "N/A"; + const kbId = item.id || "N/A"; + lines.push(`| ${filename} | ${status} | ${type} | ${size} | ${kbId} |`); }); - - lines.push(''); + + lines.push(""); } }); - + if (!hasAnyItems) { - lines.push('*No knowledge base items found*'); - lines.push(''); + lines.push("*No knowledge base items found*"); + lines.push(""); } - - return lines.join('\n'); + + return lines.join("\n"); } export function setupReportCommand(program: Command): void { // Add the main command const cmd = program - .command('report [folder-path]') - .alias('r') - .description('Generate a markdown report for the organization') - .option('-o, --output ', 'Output file path (default: ./organization-report.md)') - .option('--apikey ', 'API key for authentication') - .option('-s, --silent', 'Skip interactive prompts and use defaults') + .command("report [folder-path]") + .alias("r") + .description("Generate a markdown report for the organization") + .option( + "-o, --output ", + "Output file path (default: ./organization-report.md)" + ) + .option("--apikey ", "API key for authentication") + .option("-s, --silent", "Skip interactive prompts and use defaults") .action((folderPath, options) => { const globalOptions = program.opts(); - return reportCommand(folderPath, { - ...options, + return reportCommand(folderPath, { + ...options, nonInteractive: globalOptions.nonInteractive, verbose: globalOptions.verbose, - veryVerbose: globalOptions.veryVerbose + veryVerbose: globalOptions.veryVerbose, }); }); @@ -268,7 +303,7 @@ export function setupReportCommand(program: Command): void { cmd.configureHelp({ formatHelp: (cmd, helper) => { const termWidth = helper.padWidth(cmd, helper); - + let str = `Sensay CLI 1.0.1 - Organization Report Generator Usage: ${helper.commandUsage(cmd)} @@ -283,10 +318,10 @@ Options: `; // Options - const options = cmd.options.filter(o => o.flags); - options.forEach(option => { + const options = cmd.options.filter((o) => o.flags); + options.forEach((option) => { const flags = helper.optionTerm(option); - const description = helper.optionDescription(option) || ''; + const description = helper.optionDescription(option) || ""; str += ` ${flags.padEnd(termWidth)}${description}\n`; }); @@ -304,6 +339,6 @@ Examples: sensay report --apikey sk-1234... --silent`; return str; - } + }, }); -} \ No newline at end of file +} diff --git a/src/commands/simple-organization-setup.ts b/src/commands/simple-organization-setup.ts index e5ef6a5..3a05da5 100644 --- a/src/commands/simple-organization-setup.ts +++ b/src/commands/simple-organization-setup.ts @@ -1,17 +1,13 @@ -import { Command } from 'commander'; -import inquirer from 'inquirer'; -import chalk from 'chalk'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { - ApiError, - UsersService, - ReplicasService -} from '../generated/index'; -import { ConfigManager } from '../config/manager'; -import { FileProcessor } from '../utils/files'; -import { ProgressManager } from '../utils/progress'; -import { configureOpenAPI } from '../utils/openapi-config'; +import { Command } from "commander"; +import inquirer from "inquirer"; +import chalk from "chalk"; +import * as path from "path"; +import * as fs from "fs-extra"; +import { ApiError, UsersService, ReplicasService } from "../generated/index"; +import { ConfigManager } from "../config/manager"; +import { FileProcessor } from "../utils/files"; +import { ProgressManager } from "../utils/progress"; +import { configureOpenAPI } from "../utils/openapi-config"; interface SetupOptions { folderPath?: string; @@ -31,34 +27,36 @@ interface ReplicaFolder { systemMessage?: string; } -async function scanForReplicaFolders(targetPath: string): Promise { +async function scanForReplicaFolders( + targetPath: string +): Promise { const replicaFolders: ReplicaFolder[] = []; - + try { const entries = await fs.readdir(targetPath, { withFileTypes: true }); - + for (const entry of entries) { if (entry.isDirectory()) { const folderPath = path.join(targetPath, entry.name); - + // Check if this folder has a system-message.txt (indicator of a replica folder) - const systemMessagePath = path.join(folderPath, 'system-message.txt'); + const systemMessagePath = path.join(folderPath, "system-message.txt"); if (await fs.pathExists(systemMessagePath)) { - const systemMessage = await fs.readFile(systemMessagePath, 'utf-8'); - + const systemMessage = await fs.readFile(systemMessagePath, "utf-8"); + // Check for model.txt file - let modelName = 'claude-3-5-haiku-latest'; // default - const modelPath = path.join(folderPath, 'model.txt'); + let modelName = "claude-3-5-haiku-latest"; // default + const modelPath = path.join(folderPath, "model.txt"); if (await fs.pathExists(modelPath)) { - const modelContent = await fs.readFile(modelPath, 'utf-8'); + const modelContent = await fs.readFile(modelPath, "utf-8"); modelName = modelContent.trim() || modelName; } - + replicaFolders.push({ path: folderPath, name: entry.name, modelName, - systemMessage + systemMessage, }); } } @@ -66,15 +64,18 @@ async function scanForReplicaFolders(targetPath: string): Promise { - const targetPath = folderPath || '.'; +export async function simpleOrganizationSetupCommand( + folderPath?: string, + options: SetupOptions = {} +): Promise { + const targetPath = folderPath || "."; const absolutePath = path.resolve(targetPath); - - console.log(chalk.blue('🚀 Sensay Multi-Replica Setup\n')); + + console.log(chalk.blue("🚀 Sensay Multi-Replica Setup\n")); console.log(chalk.cyan(`📂 Working with folder: ${absolutePath}\n`)); const progress = new ProgressManager(); @@ -82,36 +83,50 @@ export async function simpleOrganizationSetupCommand(folderPath?: string, option try { // Scan for replica folders const replicaFolders = await scanForReplicaFolders(targetPath); - + if (replicaFolders.length === 0) { - console.error(chalk.red('❌ No replica folders found. Each replica folder should contain a system-message.txt file.')); + console.error( + chalk.red( + "❌ No replica folders found. Each replica folder should contain a system-message.txt file." + ) + ); process.exit(1); } - - console.log(chalk.blue(`📦 Found ${replicaFolders.length} replica${replicaFolders.length > 1 ? 's' : ''} to process:\n`)); - replicaFolders.forEach(folder => { - console.log(chalk.cyan(` - ${folder.name} (model: ${folder.modelName})`)); + + console.log( + chalk.blue( + `📦 Found ${replicaFolders.length} replica${ + replicaFolders.length > 1 ? "s" : "" + } to process:\n` + ) + ); + replicaFolders.forEach((folder) => { + console.log( + chalk.cyan(` - ${folder.name} (model: ${folder.modelName})`) + ); }); console.log(); - + // Load configurations const { projectConfig } = await ConfigManager.getMergedConfig(targetPath); const effectiveConfig = await ConfigManager.getEffectiveConfig(targetPath); if (!effectiveConfig.apiKey) { - console.error(chalk.red('❌ No API key found. Please run "sensay claim-key" first.')); - if (process.env.NODE_ENV !== 'test') { + console.error( + chalk.red('❌ No API key found. Please run "sensay claim-key" first.') + ); + if (process.env.NODE_ENV !== "test") { process.exit(1); } else { - throw new Error('No API key found'); + throw new Error("No API key found"); } } // Configure the OpenAPI client - configureOpenAPI({ - ...effectiveConfig, - verbose: options.verbose, - veryVerbose: options.veryVerbose + configureOpenAPI({ + ...effectiveConfig, + verbose: options.verbose, + veryVerbose: options.veryVerbose, }); // Get or prompt for configuration values @@ -126,61 +141,82 @@ export async function simpleOrganizationSetupCommand(folderPath?: string, option if (options.nonInteractive) { // In non-interactive mode, use existing config or fail if (!currentConfig.userName || !currentConfig.userEmail) { - console.error(chalk.red('❌ Missing required configuration. In non-interactive mode, you must either:')); - console.error(chalk.red(' 1. Provide command line options: --user-name, --user-email')); - console.error(chalk.red(' 2. Or have these values in your project config file (sensay.config.json)')); + console.error( + chalk.red( + "❌ Missing required configuration. In non-interactive mode, you must either:" + ) + ); + console.error( + chalk.red( + " 1. Provide command line options: --user-name, --user-email" + ) + ); + console.error( + chalk.red( + " 2. Or have these values in your project config file (sensay.config.json)" + ) + ); process.exit(1); } userName = currentConfig.userName; userEmail = currentConfig.userEmail; } else { const questions = [ - { - type: 'input', - name: 'userName', - message: 'User name:', - default: currentConfig.userName, - when: !currentConfig.userName, - validate: (input: string) => input.trim().length > 0 || 'User name is required' - }, - { - type: 'input', - name: 'userEmail', - message: 'User email:', - default: currentConfig.userEmail, - when: !currentConfig.userEmail, - validate: (input: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(input) || 'Please enter a valid email address'; - } - } + { + type: "input", + name: "userName", + message: "User name:", + default: currentConfig.userName, + when: !currentConfig.userName, + validate: (input: string) => + input.trim().length > 0 || "User name is required", + }, + { + type: "input", + name: "userEmail", + message: "User email:", + default: currentConfig.userEmail, + when: !currentConfig.userEmail, + validate: (input: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return ( + emailRegex.test(input) || "Please enter a valid email address" + ); + }, + }, ]; const answers = await inquirer.prompt(questions); - + userName = userName || currentConfig.userName || answers.userName; userEmail = userEmail || currentConfig.userEmail || answers.userEmail; } } // Save project configuration (without replicaName since we have multiple) - await ConfigManager.saveProjectConfig({ - ...projectConfig, - userName, - userEmail, - }, targetPath); + await ConfigManager.saveProjectConfig( + { + ...projectConfig, + userName, + userEmail, + }, + targetPath + ); // Step 1: Create or get user - const userSpinner = progress.createSpinner('user', 'Creating/getting user...'); + const userSpinner = progress.createSpinner( + "user", + "Creating/getting user..." + ); let user; try { user = await UsersService.getV1UsersMe(); userSpinner.succeed(`User found: ${user.name || user.id}`); } catch (error) { try { - user = await UsersService.postV1Users('2025-03-25', { + user = await UsersService.postV1Users("2025-03-25", { name: userName!, - email: userEmail! + email: userEmail!, }); userSpinner.succeed(`User created: ${user.name || user.id}`); } catch (createError: any) { @@ -190,206 +226,328 @@ export async function simpleOrganizationSetupCommand(folderPath?: string, option } // Update config with user ID - await ConfigManager.saveProjectConfig({ - ...await ConfigManager.getProjectConfig(targetPath), - userId: user.id, - }, targetPath); + await ConfigManager.saveProjectConfig( + { + ...(await ConfigManager.getProjectConfig(targetPath)), + userId: user.id, + }, + targetPath + ); // First, fetch all existing replicas to check for name matches - const existingReplicasSpinner = progress.createSpinner('fetch-replicas', 'Fetching existing replicas...'); + const existingReplicasSpinner = progress.createSpinner( + "fetch-replicas", + "Fetching existing replicas..." + ); let existingReplicas: any[] = []; try { const replicasResponse = await ReplicasService.getV1Replicas(); existingReplicas = replicasResponse.items || []; - existingReplicasSpinner.succeed(`Found ${existingReplicas.length} existing replicas`); + existingReplicasSpinner.succeed( + `Found ${existingReplicas.length} existing replicas` + ); } catch (error) { - existingReplicasSpinner.warn('Could not fetch existing replicas'); + existingReplicasSpinner.warn("Could not fetch existing replicas"); } // Process each replica folder const processedReplicas: any[] = []; const failedReplicas: any[] = []; - const allUploadResults: Array<{ replicaUuid: string; replicaName: string; uploadResults: any[]; totalFiles: number }> = []; - + const allUploadResults: Array<{ + replicaUuid: string; + replicaName: string; + uploadResults: any[]; + totalFiles: number; + }> = []; + for (let i = 0; i < replicaFolders.length; i++) { const replicaFolder = replicaFolders[i]; - console.log(chalk.blue(`\n🤖 Processing replica ${i + 1}/${replicaFolders.length}: ${replicaFolder.name}\n`)); - + console.log( + chalk.blue( + `\n🤖 Processing replica ${i + 1}/${replicaFolders.length}: ${ + replicaFolder.name + }\n` + ) + ); + try { // Step 2: Check if replica already exists by name - const replicaSpinner = progress.createSpinner(`replica-${i}`, `Checking for existing replica: ${replicaFolder.name}...`); - + const replicaSpinner = progress.createSpinner( + `replica-${i}`, + `Checking for existing replica: ${replicaFolder.name}...` + ); + let replica; - const existingReplica = existingReplicas.find(r => r.name === replicaFolder.name); - + const existingReplica = existingReplicas.find( + (r) => r.name === replicaFolder.name + ); + if (existingReplica) { // Replica exists, update it replicaSpinner.text = `Updating existing replica: ${replicaFolder.name}...`; - - await ReplicasService.putV1Replicas(existingReplica.uuid, '2025-03-25', { - name: replicaFolder.name, - shortDescription: existingReplica.shortDescription || `AI replica for ${replicaFolder.name}`, - greeting: existingReplica.greeting || 'Hello! How can I help you today?', - ownerID: user.id, - slug: existingReplica.slug, // Keep the existing slug - llm: { - model: replicaFolder.modelName as any || 'claude-3-5-haiku-latest', - memoryMode: 'rag-search', - systemMessage: replicaFolder.systemMessage || 'You are a helpful AI assistant.', - tools: [] + + await ReplicasService.putV1Replicas( + existingReplica.uuid, + "2025-03-25", + { + name: replicaFolder.name, + shortDescription: + existingReplica.shortDescription || + `AI replica for ${replicaFolder.name}`, + greeting: + existingReplica.greeting || "Hello! How can I help you today?", + ownerID: user.id, + slug: existingReplica.slug, // Keep the existing slug + llm: { + model: + (replicaFolder.modelName as any) || "claude-3-5-haiku-latest", + memoryMode: "rag-search", + systemMessage: + replicaFolder.systemMessage || + "You are a helpful AI assistant.", + tools: [], + }, } - }); - + ); + replica = await ReplicasService.getV1Replicas1(existingReplica.uuid); - replicaSpinner.succeed(`Updated existing replica: ${replica.name} (model: ${replicaFolder.modelName})`); + replicaSpinner.succeed( + `Updated existing replica: ${replica.name} (model: ${replicaFolder.modelName})` + ); } else { // Create new replica with folder name as slug replicaSpinner.text = `Creating new replica: ${replicaFolder.name}...`; - const slug = replicaFolder.name.toLowerCase().replace(/[^a-z0-9]/g, '-'); - - const replicaCreateResponse = await ReplicasService.postV1Replicas('2025-03-25', { - name: replicaFolder.name, - shortDescription: `AI replica for ${replicaFolder.name}`, - greeting: 'Hello! How can I help you today?', - ownerID: user.id, - slug: slug, - llm: { - model: replicaFolder.modelName as any || 'claude-3-5-haiku-latest', - memoryMode: 'rag-search', - systemMessage: replicaFolder.systemMessage || 'You are a helpful AI assistant.', - tools: [] + const slug = + user.id.slice(0, 5) + + replicaFolder.name.toLowerCase().replace(/[^a-z0-9]/g, "-"); + + const replicaCreateResponse = await ReplicasService.postV1Replicas( + "2025-03-25", + { + name: replicaFolder.name, + shortDescription: `AI replica for ${replicaFolder.name}`, + greeting: "Hello! How can I help you today?", + ownerID: user.id, + slug: slug, + llm: { + model: + (replicaFolder.modelName as any) || "claude-3-5-haiku-latest", + memoryMode: "rag-search", + systemMessage: + replicaFolder.systemMessage || + "You are a helpful AI assistant.", + tools: [], + }, } - }); - + ); + if (replicaCreateResponse.success && replicaCreateResponse.uuid) { - replica = await ReplicasService.getV1Replicas1(replicaCreateResponse.uuid); - replicaSpinner.succeed(`Created new replica: ${replica.name} (model: ${replicaFolder.modelName})`); + replica = await ReplicasService.getV1Replicas1( + replicaCreateResponse.uuid + ); + replicaSpinner.succeed( + `Created new replica: ${replica.name} (model: ${replicaFolder.modelName})` + ); } else { - throw new Error('Failed to create replica'); + throw new Error("Failed to create replica"); } } // Step 3: Process training data for this replica - const { files, skipped } = await FileProcessor.scanTrainingFiles(replicaFolder.path); + const { files, skipped } = await FileProcessor.scanTrainingFiles( + replicaFolder.path + ); let uploadResults: any[] = []; - + // Always clear existing training data first (even if no new files to upload) try { - await FileProcessor.clearExistingTrainingData(replica.uuid, options.force, options.nonInteractive); + await FileProcessor.clearExistingTrainingData( + replica.uuid, + options.force, + options.nonInteractive + ); } catch (error: any) { - if (error.message.includes('Use --force to automatically delete it')) { + if ( + error.message.includes("Use --force to automatically delete it") + ) { // User needs to use force flag in non-interactive mode - console.error(chalk.red('❌ ' + error.message)); + console.error(chalk.red("❌ " + error.message)); process.exit(1); } else { - console.log(chalk.yellow('⚠️ Warning: Could not clear existing training data completely')); + console.log( + chalk.yellow( + "⚠️ Warning: Could not clear existing training data completely" + ) + ); console.log(chalk.gray(` ${error.message}`)); } } - + if (files.length === 0) { - console.log(chalk.yellow('⚠️ No training data found in training-data folder')); - console.log(chalk.blue('ℹ️ Replica training data has been cleared and is ready for new content')); + console.log( + chalk.yellow("⚠️ No training data found in training-data folder") + ); + console.log( + chalk.blue( + "ℹ️ Replica training data has been cleared and is ready for new content" + ) + ); } else { FileProcessor.displayFilesSummary(files, skipped); // Upload training data - const trainingSpinner = progress.createSpinner(`training-${i}`, 'Uploading training data...'); - + const trainingSpinner = progress.createSpinner( + `training-${i}`, + "Uploading training data..." + ); + try { - uploadResults = await FileProcessor.uploadTrainingFiles(replica.uuid, files, trainingSpinner); - const successful = uploadResults.filter(r => r.success).length; - const failed = uploadResults.filter(r => !r.success).length; - + uploadResults = await FileProcessor.uploadTrainingFiles( + replica.uuid, + files, + trainingSpinner + ); + + const successful = uploadResults.filter((r) => r.success).length; + const failed = uploadResults.filter((r) => !r.success).length; + if (failed > 0) { - trainingSpinner.succeed(`Training data upload completed: ${successful} successful, ${failed} failed`); - console.log(chalk.yellow('\n⚠️ Failed uploads:')); - uploadResults.filter(r => !r.success).forEach(r => { - console.log(chalk.gray(` - ${r.file.relativePath}: ${r.error}`)); - }); + trainingSpinner.succeed( + `Training data upload completed: ${successful} successful, ${failed} failed` + ); + console.log(chalk.yellow("\n⚠️ Failed uploads:")); + uploadResults + .filter((r) => !r.success) + .forEach((r) => { + console.log( + chalk.gray(` - ${r.file.relativePath}: ${r.error}`) + ); + }); } else { - trainingSpinner.succeed(`Training data uploaded: ${files.length} files processed`); + trainingSpinner.succeed( + `Training data uploaded: ${files.length} files processed` + ); } - } catch (error: any) { - trainingSpinner.fail(`Training data upload failed: ${error.message}`); + trainingSpinner.fail( + `Training data upload failed: ${error.message}` + ); // Don't throw - just warn and continue - console.log(chalk.yellow('⚠️ Some training files may not have uploaded successfully')); + console.log( + chalk.yellow( + "⚠️ Some training files may not have uploaded successfully" + ) + ); } // Store upload results for batch monitoring later - if (uploadResults.length > 0 && uploadResults.some(r => r.success)) { + if ( + uploadResults.length > 0 && + uploadResults.some((r) => r.success) + ) { allUploadResults.push({ replicaUuid: replica.uuid, replicaName: replicaFolder.name, uploadResults, - totalFiles: files.length + totalFiles: files.length, }); } } - + processedReplicas.push({ name: replicaFolder.name, uuid: replica.uuid, model: replicaFolder.modelName, trainingFiles: files.length, - uploadResults: uploadResults + uploadResults: uploadResults, }); - } catch (error: any) { - console.error(chalk.red(`\n❌ Failed to process replica ${replicaFolder.name}: ${error.message}`)); + console.error( + chalk.red( + `\n❌ Failed to process replica ${replicaFolder.name}: ${error.message}` + ) + ); failedReplicas.push({ name: replicaFolder.name, - error: error.message + error: error.message, }); } } // Final summary - console.log(chalk.green('\n✅ Multi-replica setup completed!')); + console.log(chalk.green("\n✅ Multi-replica setup completed!")); console.log(chalk.cyan(`👤 User: ${userName} (${user.id})`)); - + if (processedReplicas.length > 0) { - console.log(chalk.green(`\n✅ Successfully processed ${processedReplicas.length} replica${processedReplicas.length > 1 ? 's' : ''}:`)); - processedReplicas.forEach(r => { + console.log( + chalk.green( + `\n✅ Successfully processed ${processedReplicas.length} replica${ + processedReplicas.length > 1 ? "s" : "" + }:` + ) + ); + processedReplicas.forEach((r) => { console.log(chalk.cyan(` - ${r.name} (${r.uuid})`)); - console.log(chalk.gray(` Model: ${r.model}, Training files: ${r.trainingFiles}`)); + console.log( + chalk.gray( + ` Model: ${r.model}, Training files: ${r.trainingFiles}` + ) + ); }); } - + if (failedReplicas.length > 0) { - console.log(chalk.red(`\n❌ Failed to process ${failedReplicas.length} replica${failedReplicas.length > 1 ? 's' : ''}:`)); - failedReplicas.forEach(r => { + console.log( + chalk.red( + `\n❌ Failed to process ${failedReplicas.length} replica${ + failedReplicas.length > 1 ? "s" : "" + }:` + ) + ); + failedReplicas.forEach((r) => { console.log(chalk.red(` - ${r.name}: ${r.error}`)); }); } - + // Monitor training status for all replicas that had successful uploads if (allUploadResults.length > 0) { - console.log(chalk.blue(`\n🔄 Monitoring training progress for all ${allUploadResults.length} replica${allUploadResults.length > 1 ? 's' : ''}...`)); - + console.log( + chalk.blue( + `\n🔄 Monitoring training progress for all ${ + allUploadResults.length + } replica${allUploadResults.length > 1 ? "s" : ""}...` + ) + ); + for (const replicaData of allUploadResults) { - console.log(chalk.cyan(`\n📊 Checking training status for ${replicaData.replicaName}...`)); + console.log( + chalk.cyan( + `\n📊 Checking training status for ${replicaData.replicaName}...` + ) + ); try { await FileProcessor.pollTrainingStatus( - replicaData.replicaUuid, - replicaData.uploadResults, + replicaData.replicaUuid, + replicaData.uploadResults, replicaData.totalFiles ); } catch (error: any) { - console.log(chalk.yellow(`⚠️ Training status monitoring for ${replicaData.replicaName} ended with error: ${error.message}`)); + console.log( + chalk.yellow( + `⚠️ Training status monitoring for ${replicaData.replicaName} ended with error: ${error.message}` + ) + ); } } } - } catch (error: any) { - console.error(chalk.red('\n❌ Setup failed:')); - + console.error(chalk.red("\n❌ Setup failed:")); + if (error instanceof ApiError) { // Properly typed API error from generated client console.error(chalk.red(`Status: ${error.status}`)); console.error(chalk.red(`Error: ${error.message}`)); - + // Try to get additional error details from the body if (error.body) { const body = error.body as any; @@ -400,16 +558,16 @@ export async function simpleOrganizationSetupCommand(folderPath?: string, option console.error(chalk.gray(`Fingerprint: ${body.fingerprint}`)); } } - } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { + } else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") { // Network error - console.error(chalk.red('Network error: Could not reach the API')); - console.error(chalk.red('Please check your internet connection')); + console.error(chalk.red("Network error: Could not reach the API")); + console.error(chalk.red("Please check your internet connection")); } else { // Other error console.error(chalk.red(`Error: ${error.message || error}`)); } - - if (process.env.NODE_ENV !== 'test') { + + if (process.env.NODE_ENV !== "test") { process.exit(1); } else { throw error; @@ -419,18 +577,23 @@ export async function simpleOrganizationSetupCommand(folderPath?: string, option export function setupSimpleOrganizationSetupCommand(program: Command): void { const cmd = program - .command('simple-organization-setup [folder-path]') - .description('Set up user and multiple replicas with training data from subfolders') - .option('-u, --user-name ', 'user name for the account') - .option('-e, --user-email ', 'user email address') - .option('-f, --force', 'skip confirmation before deleting existing training data') + .command("simple-organization-setup [folder-path]") + .description( + "Set up user and multiple replicas with training data from subfolders" + ) + .option("-u, --user-name ", "user name for the account") + .option("-e, --user-email ", "user email address") + .option( + "-f, --force", + "skip confirmation before deleting existing training data" + ) .action((folderPath, options) => { const globalOptions = program.opts(); - return simpleOrganizationSetupCommand(folderPath, { - ...options, + return simpleOrganizationSetupCommand(folderPath, { + ...options, nonInteractive: globalOptions.nonInteractive, verbose: globalOptions.verbose, - veryVerbose: globalOptions.veryVerbose + veryVerbose: globalOptions.veryVerbose, }); }); @@ -438,7 +601,7 @@ export function setupSimpleOrganizationSetupCommand(program: Command): void { cmd.configureHelp({ formatHelp: (cmd, helper) => { const termWidth = helper.padWidth(cmd, helper); - + let str = `Sensay CLI 1.0.1 - Multi-Replica Organization Setup Usage: ${helper.commandUsage(cmd)} @@ -451,14 +614,14 @@ should contain: Options: `; - + // Add options - cmd.options.forEach(option => { + cmd.options.forEach((option) => { const flags = helper.optionTerm(option); const description = helper.optionDescription(option); str += ` ${flags.padEnd(termWidth)}${description}\n`; }); - + str += ` Examples: sensay simple-organization-setup @@ -480,6 +643,6 @@ Folder Structure Example: └── faqs.md`; return str; - } + }, }); -} \ No newline at end of file +} diff --git a/src/generated/core/OpenAPI.ts b/src/generated/core/OpenAPI.ts index eedc58b..ae4bda7 100644 --- a/src/generated/core/OpenAPI.ts +++ b/src/generated/core/OpenAPI.ts @@ -2,31 +2,31 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiRequestOptions } from "./ApiRequestOptions"; type Resolver = (options: ApiRequestOptions) => Promise; type Headers = Record; export type OpenAPIConfig = { - BASE: string; - VERSION: string; - WITH_CREDENTIALS: boolean; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - HEADERS?: Headers | Resolver | undefined; - ENCODE_PATH?: ((path: string) => string) | undefined; + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: "include" | "omit" | "same-origin"; + TOKEN?: string | Resolver | undefined; + USERNAME?: string | Resolver | undefined; + PASSWORD?: string | Resolver | undefined; + HEADERS?: Headers | Resolver | undefined; + ENCODE_PATH?: ((path: string) => string) | undefined; }; export const OpenAPI: OpenAPIConfig = { - BASE: 'https://api.sensay.io', - VERSION: '2025-03-25', - WITH_CREDENTIALS: false, - CREDENTIALS: 'include', - TOKEN: undefined, - USERNAME: undefined, - PASSWORD: undefined, - HEADERS: undefined, - ENCODE_PATH: undefined, + BASE: "https://api.sensay.io", + VERSION: "2025-03-25", + WITH_CREDENTIALS: false, + CREDENTIALS: "include", + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, }; diff --git a/src/generated/core/request.ts b/src/generated/core/request.ts index f83d711..c8633a2 100644 --- a/src/generated/core/request.ts +++ b/src/generated/core/request.ts @@ -2,285 +2,312 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isDefined = (value: T | null | undefined): value is Exclude => { - return value !== undefined && value !== null; +import { ApiError } from "./ApiError"; +import type { ApiRequestOptions } from "./ApiRequestOptions"; +import type { ApiResult } from "./ApiResult"; +import { CancelablePromise } from "./CancelablePromise"; +import type { OnCancel } from "./CancelablePromise"; +import type { OpenAPIConfig } from "./OpenAPI"; + +export const isDefined = ( + value: T | null | undefined +): value is Exclude => { + return value !== undefined && value !== null; }; export const isString = (value: any): value is string => { - return typeof value === 'string'; + return typeof value === "string"; }; export const isStringWithValue = (value: any): value is string => { - return isString(value) && value !== ''; + return isString(value) && value !== ""; }; export const isBlob = (value: any): value is Blob => { - return ( - typeof value === 'object' && - typeof value.type === 'string' && - typeof value.stream === 'function' && - typeof value.arrayBuffer === 'function' && - typeof value.constructor === 'function' && - typeof value.constructor.name === 'string' && - /^(Blob|File)$/.test(value.constructor.name) && - /^(Blob|File)$/.test(value[Symbol.toStringTag]) - ); + return ( + typeof value === "object" && + typeof value.type === "string" && + typeof value.stream === "function" && + typeof value.arrayBuffer === "function" && + typeof value.constructor === "function" && + typeof value.constructor.name === "string" && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); }; export const isFormData = (value: any): value is FormData => { - return value instanceof FormData; + return value instanceof FormData; }; export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString("base64"); + } }; export const getQueryString = (params: Record): string => { - const qs: string[] = []; + const qs: string[] = []; - const append = (key: string, value: any) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; - const process = (key: string, value: any) => { - if (isDefined(value)) { - if (Array.isArray(value)) { - value.forEach(v => { - process(key, v); - }); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => { - process(`${key}[${k}]`, v); - }); - } else { - append(key, value); - } - } - }; + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach((v) => { + process(key, v); + }); + } else if (typeof value === "object") { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; - Object.entries(params).forEach(([key, value]) => { - process(key, value); - }); + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); - if (qs.length > 0) { - return `?${qs.join('&')}`; - } + if (qs.length > 0) { + return `?${qs.join("&")}`; + } - return ''; + return ""; }; const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace("{api-version}", config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); - const url = `${config.BASE}${path}`; - if (options.query) { - return `${url}${getQueryString(options.query)}`; - } - return url; + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; }; -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); +export const getFormData = ( + options: ApiRequestOptions +): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); - const process = (key: string, value: any) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; - Object.entries(options.formData) - .filter(([_, value]) => isDefined(value)) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; }; type Resolver = (options: ApiRequestOptions) => Promise; -export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { - if (typeof resolver === 'function') { - return (resolver as Resolver)(options); - } - return resolver; +export const resolve = async ( + options: ApiRequestOptions, + resolver?: T | Resolver +): Promise => { + if (typeof resolver === "function") { + return (resolver as Resolver)(options); + } + return resolver; }; -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]); - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - }) - .filter(([_, value]) => isDefined(value)) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } +export const getHeaders = async ( + config: OpenAPIConfig, + options: ApiRequestOptions +): Promise => { + const [token, username, password, additionalHeaders] = await Promise.all([ + resolve(options, config.TOKEN), + resolve(options, config.USERNAME), + resolve(options, config.PASSWORD), + resolve(options, config.HEADERS), + ]); + + const headers = Object.entries({ + Accept: "application/json", + ...additionalHeaders, + ...options.headers, + }) + .filter(([_, value]) => isDefined(value)) + .reduce( + (headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), + {} as Record + ); - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } + if (isStringWithValue(token)) { + headers["Authorization"] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers["Authorization"] = `Basic ${credentials}`; + } + + if (options.body !== undefined) { + if (options.mediaType) { + headers["Content-Type"] = options.mediaType; + } else if (isBlob(options.body)) { + headers["Content-Type"] = options.body.type || "application/octet-stream"; + } else if (isString(options.body)) { + headers["Content-Type"] = "text/plain"; + } else if (!isFormData(options.body)) { + headers["Content-Type"] = "application/json"; } + } - return new Headers(headers); + return new Headers(headers); }; export const getRequestBody = (options: ApiRequestOptions): any => { - if (options.body !== undefined) { - if (options.mediaType?.includes('/json')) { - return JSON.stringify(options.body) - } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) { - return options.body; - } else { - return JSON.stringify(options.body); - } + if (options.body !== undefined) { + if (options.mediaType?.includes("/json")) { + return JSON.stringify(options.body); + } else if ( + isString(options.body) || + isBlob(options.body) || + isFormData(options.body) + ) { + return options.body; + } else { + return JSON.stringify(options.body); } - return undefined; + } + return undefined; }; export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: any, - formData: FormData | undefined, - headers: Headers, - onCancel: OnCancel + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Headers, + onCancel: OnCancel ): Promise => { - const controller = new AbortController(); + const controller = new AbortController(); - const request: RequestInit = { - headers, - body: body ?? formData, - method: options.method, - signal: controller.signal, - }; + const request: RequestInit = { + headers, + body: body ?? formData, + method: options.method, + signal: controller.signal, + }; - if (config.WITH_CREDENTIALS) { - request.credentials = config.CREDENTIALS; - } + if (config.WITH_CREDENTIALS) { + request.credentials = config.CREDENTIALS; + } - onCancel(() => controller.abort()); + onCancel(() => controller.abort()); - return await fetch(url, request); + return await fetch(url, request); }; -export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers.get(responseHeader); - if (isString(content)) { - return content; - } +export const getResponseHeader = ( + response: Response, + responseHeader?: string +): string | undefined => { + if (responseHeader) { + const content = response.headers.get(responseHeader); + if (isString(content)) { + return content; } - return undefined; + } + return undefined; }; export const getResponseBody = async (response: Response): Promise => { - if (response.status !== 204) { - try { - const contentType = response.headers.get('Content-Type'); - if (contentType) { - const jsonTypes = ['application/json', 'application/problem+json'] - const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type)); - if (isJSON) { - return await response.json(); - } else { - return await response.text(); - } - } - } catch (error) { - console.error(error); + if (response.status !== 204) { + try { + const contentType = response.headers.get("Content-Type"); + if (contentType) { + const jsonTypes = ["application/json", "application/problem+json"]; + const isJSON = jsonTypes.some((type) => + contentType.toLowerCase().startsWith(type) + ); + if (isJSON) { + return await response.json(); + } else { + return await response.text(); } + } + } catch (error) { + console.error(error); } - return undefined; + } + return undefined; }; -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } +export const catchErrorCodes = ( + options: ApiRequestOptions, + result: ApiResult +): void => { + const errors: Record = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 500: "Internal Server Error", + 502: "Bad Gateway", + 503: "Service Unavailable", + ...options.errors, + }; + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + const errorStatus = result.status ?? "unknown"; + const errorStatusText = result.statusText ?? "unknown"; + const errorBody = (() => { + try { + return JSON.stringify(result.body, null, 2); + } catch (e) { + return undefined; + } + })(); + + throw new ApiError( + options, + result, + `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` + ); + } }; /** @@ -290,33 +317,48 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): * @returns CancelablePromise * @throws ApiError */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options); - - if (!onCancel.isCancelled) { - const response = await sendRequest(config, options, url, body, formData, headers, onCancel); - const responseBody = await getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: response.ok, - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); +export const request = ( + config: OpenAPIConfig, + options: ApiRequestOptions +): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options); + + if (!onCancel.isCancelled) { + const response = await sendRequest( + config, + options, + url, + body, + formData, + headers, + onCancel + ); + + const responseBody = await getResponseBody(response); + const responseHeader = getResponseHeader( + response, + options.responseHeader + ); + + const result: ApiResult = { + url, + ok: response.ok, + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); }; diff --git a/src/generated/services/ReplicasService.ts b/src/generated/services/ReplicasService.ts index ef99313..0f61dd7 100644 --- a/src/generated/services/ReplicasService.ts +++ b/src/generated/services/ReplicasService.ts @@ -2,593 +2,722 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { replicaUUID_parameter } from '../models/replicaUUID_parameter'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; +import type { replicaUUID_parameter } from "../models/replicaUUID_parameter"; +import type { CancelablePromise } from "../core/CancelablePromise"; +import { OpenAPI } from "../core/OpenAPI"; +import { request as __request } from "../core/request"; export class ReplicasService { + /** + * List replicas + * List replicas with pagination with optional filtering. Only Replicas that are public or belong to the authenticated user are returned. + * @param ownerUuid Filters by the owner UUID of the Replicas + * @param ownerId The replica owner ID. + * @param page Pagination: The page number to return + * @param pageIndex Pagination: The page index to return + * @param pageSize Pagination: The number of items per page + * @param slug Filters by the replica's slug + * @param search Search: by name of Replica, sorted in ascending order + * @param tags Filters by tags associated to Replicas + * @param sort Sorts by name or popularity of Replicas in ascending order + * @param integration Filters by integration + * @param xApiVersion + * @returns any List of Replicas + * @throws ApiError + */ + public static getV1Replicas( + ownerUuid?: string, + ownerId?: string, + page?: number, + pageIndex: number = 1, + pageSize: number = 24, + slug?: string, + search?: string, + tags?: Array< + | "AI" + | "Academic" + | "Arts" + | "Blockchain" + | "Business" + | "Celebrity" + | "Charity" + | "Developer" + | "Educator" + | "Europe" + | "Fashion" + | "Finance" + | "Food" + | "Health & Fitness" + | "History" + | "Italian" + | "Kids" + | "Language" + | "Law" + | "Leadership" + | "Lifestyle" + | "Literature" + | "Love" + | "Military" + | "Modelling" + | "Motivation" + | "Movies" + | "Music" + | "North America" + | "Philosophy" + | "Politics" + | "Religion" + | "Science" + | "Self-Help" + | "Sensay" + | "Sports" + | "Technology" + | "Web" + | "Wisdom" + | "blockchain" + | "engage2earn" + | "female" + | "investment" + | "male" + | "meme" + | "miniapp" + | "telegram" + | "web3" + >, + sort: "name" | "popularity" = "name", + integration?: "telegram" | "discord", + xApiVersion: string = "2025-03-25" + ): CancelablePromise<{ /** - * List replicas - * List replicas with pagination with optional filtering. Only Replicas that are public or belong to the authenticated user are returned. - * @param ownerUuid Filters by the owner UUID of the Replicas - * @param ownerId The replica owner ID. - * @param page Pagination: The page number to return - * @param pageIndex Pagination: The page index to return - * @param pageSize Pagination: The number of items per page - * @param slug Filters by the replica's slug - * @param search Search: by name of Replica, sorted in ascending order - * @param tags Filters by tags associated to Replicas - * @param sort Sorts by name or popularity of Replicas in ascending order - * @param integration Filters by integration - * @param xApiVersion - * @returns any List of Replicas - * @throws ApiError + * Indicates the status of the request */ - public static getV1Replicas( - ownerUuid?: string, - ownerId?: string, - page?: number, - pageIndex: number = 1, - pageSize: number = 24, - slug?: string, - search?: string, - tags?: Array<'AI' | 'Academic' | 'Arts' | 'Blockchain' | 'Business' | 'Celebrity' | 'Charity' | 'Developer' | 'Educator' | 'Europe' | 'Fashion' | 'Finance' | 'Food' | 'Health & Fitness' | 'History' | 'Italian' | 'Kids' | 'Language' | 'Law' | 'Leadership' | 'Lifestyle' | 'Literature' | 'Love' | 'Military' | 'Modelling' | 'Motivation' | 'Movies' | 'Music' | 'North America' | 'Philosophy' | 'Politics' | 'Religion' | 'Science' | 'Self-Help' | 'Sensay' | 'Sports' | 'Technology' | 'Web' | 'Wisdom' | 'blockchain' | 'engage2earn' | 'female' | 'investment' | 'male' | 'meme' | 'miniapp' | 'telegram' | 'web3'>, - sort: 'name' | 'popularity' = 'name', - integration?: 'telegram' | 'discord', - xApiVersion: string = '2025-03-25', - ): CancelablePromise<{ - /** - * Indicates the status of the request - */ - success: boolean; - type: string; - /** - * Array of replica items for the current page. Will be an empty array if no items exist. - */ - items: Array<{ - /** - * The name of the replica. - */ - name: string; - /** - * The purpose of the replica. This field is not used for training the replica. - */ - purpose?: string; - /** - * A short description of your replica. This field is not used for training the replica. - */ - shortDescription: string; - /** - * The first thing your replica will say when you start a conversation with them. - */ - greeting: string; - /** - * The replica type. - * `individual`: A replica of yourself. - * `character`: A replica of a character: can be anything you want. - * `brand`: A replica of a business persona or organization. - * - */ - type?: 'individual' | 'character' | 'brand'; - /** - * The replica owner ID. - */ - ownerID: string; - /** - * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. - */ - private?: boolean; - /** - * Emails of users who can use the replica when the replica is private. - */ - whitelistEmails?: Array; - /** - * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. - */ - slug: string; - /** - * The tags associated with the replica. Tags help categorize replicas and make them easier to find. - */ - tags?: Array; - /** - * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif - */ - profileImage?: string; - /** - * Suggested questions when starting a conversation. - */ - suggestedQuestions?: Array; - llm: { - /** - * The LLM model of the replica. - */ - model?: 'gpt-4o' | 'claude-3-5-haiku-latest' | 'claude-3-7-sonnet-latest' | 'claude-4-sonnet-20250514' | 'grok-2-latest' | 'grok-3-beta' | 'deepseek-chat' | 'o3-mini' | 'gpt-4o-mini' | 'huggingface-eva' | 'huggingface-dolphin-llama'; - /** - * Deprecated. The system will automatically choose the best approach. - * @deprecated - */ - memoryMode?: 'prompt-caching' | 'rag-search'; - /** - * Who is your replica? How do you want it to talk, respond and act. - */ - systemMessage?: string; - /** - * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information - * - */ - tools?: Array<'getTokenInfo' | 'getUdaoTokenInfo' | 'getSensayTokenInfo' | 'getTokenInfoMEAI' | 'answerToLife' | 'toolhouse' | 'brightUnionGetQuoteTool' | 'brightUnionGetCoverablesTool'>; - }; - /** - * Text that can be used to generate a voice preview. - */ - voicePreviewText?: string; - /** - * The replica UUID - */ - uuid: string; - /** - * The URL of the profile image of the replica. Please use `profileImage` instead. - * @deprecated - */ - profile_image: string | null; - /** - * The description of the replica. Please use `shortDescription` instead. - * @deprecated - */ - short_description: string | null; - /** - * The introduction of the replica. Please use `greeting` instead. - * @deprecated - */ - introduction: string | null; - /** - * The date and time the replica was created. - */ - created_at: string | null; - /** - * The UUID of the owner of the replica. Please migrate to the new User ID system and use `ownerID` instead. - * @deprecated - */ - owner_uuid: string | null; - /** - * Whether the replica has voice support. - */ - voice_enabled: boolean; - /** - * Whether the replica has video support. - */ - video_enabled: boolean; - /** - * The total number of chat history items related to this replica, for all users of the organization. - */ - chat_history_count: number | null; - /** - * The replica's tone, personality and behaviour, Please use `llm.systemMessage` instead. - * @deprecated - */ - system_message: string; - /** - * The Discord integration of the replica. - */ - discord_integration: any | null; - /** - * The Telegram integration of the replica. - */ - telegram_integration: any | null; - /** - * The ElevenLabs voice ID associated with this replica. - */ - elevenLabsID?: string | null; - }>; - /** - * The total number of replica items available across all pages - */ - total: number; - }> { - return __request(OpenAPI, { - method: 'GET', - url: '/v1/replicas', - headers: { - 'X-API-Version': xApiVersion, - }, - query: { - 'owner_uuid': ownerUuid, - 'ownerID': ownerId, - 'page': page, - 'page_index': pageIndex, - 'page_size': pageSize, - 'slug': slug, - 'search': search, - 'tags': tags, - 'sort': sort, - 'integration': integration, - }, - errors: { - 400: `Bad Request`, - 401: `Unauthorized`, - 404: `Not Found`, - 415: `Unsupported Media Type`, - 500: `Internal Server Error`, - }, - }); - } + success: boolean; + type: string; /** - * Create a replica - * Creates a new replica. - * @param xApiVersion - * @param requestBody - * @returns any The created replica - * @throws ApiError + * Array of replica items for the current page. Will be an empty array if no items exist. */ - public static postV1Replicas( - xApiVersion: string = '2025-03-25', - requestBody?: { - /** - * The name of the replica. - */ - name: string; - /** - * The purpose of the replica. This field is not used for training the replica. - */ - purpose?: string; - /** - * A short description of your replica. This field is not used for training the replica. - */ - shortDescription: string; - /** - * The first thing your replica will say when you start a conversation with them. - */ - greeting: string; - /** - * The replica type. - * `individual`: A replica of yourself. - * `character`: A replica of a character: can be anything you want. - * `brand`: A replica of a business persona or organization. - * - */ - type?: 'individual' | 'character' | 'brand'; - /** - * The replica owner ID. - */ - ownerID: string; - /** - * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. - */ - private?: boolean; - /** - * Emails of users who can use the replica when the replica is private. - */ - whitelistEmails?: Array; - /** - * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. - */ - slug: string; - /** - * The tags associated with the replica. Tags help categorize replicas and make them easier to find. - */ - tags?: Array; - /** - * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif - */ - profileImage?: string; - /** - * Suggested questions when starting a conversation. - */ - suggestedQuestions?: Array; - llm: { - /** - * The LLM model of the replica. - */ - model?: 'gpt-4o' | 'claude-3-5-haiku-latest' | 'claude-3-7-sonnet-latest' | 'claude-4-sonnet-20250514' | 'grok-2-latest' | 'grok-3-beta' | 'deepseek-chat' | 'o3-mini' | 'gpt-4o-mini' | 'huggingface-eva' | 'huggingface-dolphin-llama'; - /** - * Deprecated. The system will automatically choose the best approach. - * @deprecated - */ - memoryMode?: 'prompt-caching' | 'rag-search'; - /** - * Who is your replica? How do you want it to talk, respond and act. - */ - systemMessage?: string; - /** - * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information - * - */ - tools?: Array<'getTokenInfo' | 'getUdaoTokenInfo' | 'getSensayTokenInfo' | 'getTokenInfoMEAI' | 'answerToLife' | 'toolhouse' | 'brightUnionGetQuoteTool' | 'brightUnionGetCoverablesTool'>; - }; - /** - * Text that can be used to generate a voice preview. - */ - voicePreviewText?: string; - }, - ): CancelablePromise<{ + items: Array<{ + /** + * The name of the replica. + */ + name: string; + /** + * The purpose of the replica. This field is not used for training the replica. + */ + purpose?: string; + /** + * A short description of your replica. This field is not used for training the replica. + */ + shortDescription: string; + /** + * The first thing your replica will say when you start a conversation with them. + */ + greeting: string; + /** + * The replica type. + * `individual`: A replica of yourself. + * `character`: A replica of a character: can be anything you want. + * `brand`: A replica of a business persona or organization. + * + */ + type?: "individual" | "character" | "brand"; + /** + * The replica owner ID. + */ + ownerID: string; + /** + * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. + */ + private?: boolean; + /** + * Emails of users who can use the replica when the replica is private. + */ + whitelistEmails?: Array; + /** + * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. + */ + slug: string; + /** + * The tags associated with the replica. Tags help categorize replicas and make them easier to find. + */ + tags?: Array; + /** + * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif + */ + profileImage?: string; + /** + * Suggested questions when starting a conversation. + */ + suggestedQuestions?: Array; + llm: { /** - * Indicates if the replica was created successfully + * The LLM model of the replica. */ - success: boolean; + model?: + | "gpt-4o" + | "claude-3-5-haiku-latest" + | "claude-3-7-sonnet-latest" + | "claude-4-sonnet-20250514" + | "grok-2-latest" + | "grok-3-beta" + | "deepseek-chat" + | "o3-mini" + | "gpt-4o-mini" + | "huggingface-eva" + | "huggingface-dolphin-llama"; /** - * The replica UUID + * Deprecated. The system will automatically choose the best approach. + * @deprecated */ - uuid: string; - }> { - return __request(OpenAPI, { - method: 'POST', - url: '/v1/replicas', - headers: { - 'X-API-Version': xApiVersion, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Bad Request`, - 401: `Unauthorized`, - 404: `Not Found`, - 409: `Conflict`, - 415: `Unsupported Media Type`, - 500: `Internal Server Error`, - }, - }); - } - /** - * Get a replica - * Get an existing replica. - * @param replicaUuid - * @param xApiVersion - * @returns any The requested replica - * @throws ApiError - */ - public static getV1Replicas1( - replicaUuid: replicaUUID_parameter, - xApiVersion: string = '2025-03-25', - ): CancelablePromise<{ - /** - * The name of the replica. - */ - name: string; - /** - * The purpose of the replica. This field is not used for training the replica. - */ - purpose?: string; - /** - * A short description of your replica. This field is not used for training the replica. - */ - shortDescription: string; + memoryMode?: "prompt-caching" | "rag-search"; /** - * The first thing your replica will say when you start a conversation with them. + * Who is your replica? How do you want it to talk, respond and act. */ - greeting: string; + systemMessage?: string; /** - * The replica type. - * `individual`: A replica of yourself. - * `character`: A replica of a character: can be anything you want. - * `brand`: A replica of a business persona or organization. + * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information * */ - type?: 'individual' | 'character' | 'brand'; - /** - * The replica owner ID. - */ - ownerID: string; - /** - * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. - */ - private?: boolean; + tools?: Array< + | "getTokenInfo" + | "getUdaoTokenInfo" + | "getSensayTokenInfo" + | "getTokenInfoMEAI" + | "answerToLife" + | "toolhouse" + | "brightUnionGetQuoteTool" + | "brightUnionGetCoverablesTool" + >; + }; + /** + * Text that can be used to generate a voice preview. + */ + voicePreviewText?: string; + /** + * The replica UUID + */ + uuid: string; + /** + * The URL of the profile image of the replica. Please use `profileImage` instead. + * @deprecated + */ + profile_image: string | null; + /** + * The description of the replica. Please use `shortDescription` instead. + * @deprecated + */ + short_description: string | null; + /** + * The introduction of the replica. Please use `greeting` instead. + * @deprecated + */ + introduction: string | null; + /** + * The date and time the replica was created. + */ + created_at: string | null; + /** + * The UUID of the owner of the replica. Please migrate to the new User ID system and use `ownerID` instead. + * @deprecated + */ + owner_uuid: string | null; + /** + * Whether the replica has voice support. + */ + voice_enabled: boolean; + /** + * Whether the replica has video support. + */ + video_enabled: boolean; + /** + * The total number of chat history items related to this replica, for all users of the organization. + */ + chat_history_count: number | null; + /** + * The replica's tone, personality and behaviour, Please use `llm.systemMessage` instead. + * @deprecated + */ + system_message: string; + /** + * The Discord integration of the replica. + */ + discord_integration: any | null; + /** + * The Telegram integration of the replica. + */ + telegram_integration: any | null; + /** + * The ElevenLabs voice ID associated with this replica. + */ + elevenLabsID?: string | null; + }>; + /** + * The total number of replica items available across all pages + */ + total: number; + }> { + return __request(OpenAPI, { + method: "GET", + url: "/v1/replicas", + headers: { + "X-API-Version": xApiVersion, + }, + query: { + owner_uuid: ownerUuid, + ownerID: ownerId, + page: page, + page_index: pageIndex, + page_size: pageSize, + slug: slug, + search: search, + tags: tags, + sort: sort, + integration: integration, + }, + errors: { + 400: `Bad Request`, + 401: `Unauthorized`, + 404: `Not Found`, + 415: `Unsupported Media Type`, + 500: `Internal Server Error`, + }, + }); + } + /** + * Create a replica + * Creates a new replica. + * @param xApiVersion + * @param requestBody + * @returns any The created replica + * @throws ApiError + */ + public static postV1Replicas( + xApiVersion: string = "2025-03-25", + requestBody?: { + /** + * The name of the replica. + */ + name: string; + /** + * The purpose of the replica. This field is not used for training the replica. + */ + purpose?: string; + /** + * A short description of your replica. This field is not used for training the replica. + */ + shortDescription: string; + /** + * The first thing your replica will say when you start a conversation with them. + */ + greeting: string; + /** + * The replica type. + * `individual`: A replica of yourself. + * `character`: A replica of a character: can be anything you want. + * `brand`: A replica of a business persona or organization. + * + */ + type?: "individual" | "character" | "brand"; + /** + * The replica owner ID. + */ + ownerID: string; + /** + * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. + */ + private?: boolean; + /** + * Emails of users who can use the replica when the replica is private. + */ + whitelistEmails?: Array; + /** + * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. + */ + slug: string; + /** + * The tags associated with the replica. Tags help categorize replicas and make them easier to find. + */ + tags?: Array; + /** + * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif + */ + profileImage?: string; + /** + * Suggested questions when starting a conversation. + */ + suggestedQuestions?: Array; + llm: { /** - * Emails of users who can use the replica when the replica is private. + * The LLM model of the replica. */ - whitelistEmails?: Array; + model?: + | "gpt-4o" + | "claude-3-5-haiku-latest" + | "claude-3-7-sonnet-latest" + | "claude-4-sonnet-20250514" + | "grok-2-latest" + | "grok-3-beta" + | "deepseek-chat" + | "o3-mini" + | "gpt-4o-mini" + | "huggingface-eva" + | "huggingface-dolphin-llama"; /** - * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. + * Deprecated. The system will automatically choose the best approach. + * @deprecated */ - slug: string; + memoryMode?: "prompt-caching" | "rag-search"; /** - * The tags associated with the replica. Tags help categorize replicas and make them easier to find. + * Who is your replica? How do you want it to talk, respond and act. */ - tags?: Array; + systemMessage?: string; /** - * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif + * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information + * */ - profileImage?: string; + tools?: Array< + | "getTokenInfo" + | "getUdaoTokenInfo" + | "getSensayTokenInfo" + | "getTokenInfoMEAI" + | "answerToLife" + | "toolhouse" + | "brightUnionGetQuoteTool" + | "brightUnionGetCoverablesTool" + >; + }; + /** + * Text that can be used to generate a voice preview. + */ + voicePreviewText?: string; + } + ): CancelablePromise<{ + /** + * Indicates if the replica was created successfully + */ + success: boolean; + /** + * The replica UUID + */ + uuid: string; + }> { + return __request(OpenAPI, { + method: "POST", + url: "/v1/replicas", + headers: { + "X-API-Version": xApiVersion, + }, + body: requestBody, + mediaType: "application/json", + errors: { + 400: `Bad Request`, + 401: `Unauthorized`, + 404: `Not Found`, + 409: `Conflict`, + 415: `Unsupported Media Type`, + 500: `Internal Server Error`, + }, + }); + } + /** + * Get a replica + * Get an existing replica. + * @param replicaUuid + * @param xApiVersion + * @returns any The requested replica + * @throws ApiError + */ + public static getV1Replicas1( + replicaUuid: replicaUUID_parameter, + xApiVersion: string = "2025-03-25" + ): CancelablePromise<{ + /** + * The name of the replica. + */ + name: string; + /** + * The purpose of the replica. This field is not used for training the replica. + */ + purpose?: string; + /** + * A short description of your replica. This field is not used for training the replica. + */ + shortDescription: string; + /** + * The first thing your replica will say when you start a conversation with them. + */ + greeting: string; + /** + * The replica type. + * `individual`: A replica of yourself. + * `character`: A replica of a character: can be anything you want. + * `brand`: A replica of a business persona or organization. + * + */ + type?: "individual" | "character" | "brand"; + /** + * The replica owner ID. + */ + ownerID: string; + /** + * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. + */ + private?: boolean; + /** + * Emails of users who can use the replica when the replica is private. + */ + whitelistEmails?: Array; + /** + * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. + */ + slug: string; + /** + * The tags associated with the replica. Tags help categorize replicas and make them easier to find. + */ + tags?: Array; + /** + * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif + */ + profileImage?: string; + /** + * Suggested questions when starting a conversation. + */ + suggestedQuestions?: Array; + llm: { + /** + * The LLM model of the replica. + */ + model?: + | "gpt-4o" + | "claude-3-5-haiku-latest" + | "claude-3-7-sonnet-latest" + | "claude-4-sonnet-20250514" + | "grok-2-latest" + | "grok-3-beta" + | "deepseek-chat" + | "o3-mini" + | "gpt-4o-mini" + | "huggingface-eva" + | "huggingface-dolphin-llama"; + /** + * Deprecated. The system will automatically choose the best approach. + * @deprecated + */ + memoryMode?: "prompt-caching" | "rag-search"; + /** + * Who is your replica? How do you want it to talk, respond and act. + */ + systemMessage?: string; + /** + * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information + * + */ + tools?: Array< + | "getTokenInfo" + | "getUdaoTokenInfo" + | "getSensayTokenInfo" + | "getTokenInfoMEAI" + | "answerToLife" + | "toolhouse" + | "brightUnionGetQuoteTool" + | "brightUnionGetCoverablesTool" + >; + }; + /** + * Text that can be used to generate a voice preview. + */ + voicePreviewText?: string; + /** + * The replica UUID + */ + uuid: string; + }> { + return __request(OpenAPI, { + method: "GET", + url: "/v1/replicas/{replicaUUID}", + path: { + replicaUUID: replicaUuid, + }, + headers: { + "X-API-Version": xApiVersion, + }, + errors: { + 400: `Bad Request`, + 401: `Unauthorized`, + 404: `Not Found`, + 415: `Unsupported Media Type`, + 500: `Internal Server Error`, + }, + }); + } + /** + * Delete a replica + * Deletes a replica by UUID. + * @param replicaUuid + * @param xApiVersion + * @returns any Replica has been deleted + * @throws ApiError + */ + public static deleteV1Replicas( + replicaUuid: replicaUUID_parameter, + xApiVersion: string = "2025-03-25" + ): CancelablePromise<{ + /** + * Indicates if the replica was deleted successfully + */ + success: boolean; + }> { + return __request(OpenAPI, { + method: "DELETE", + url: "/v1/replicas/{replicaUUID}", + path: { + replicaUUID: replicaUuid, + }, + headers: { + "X-API-Version": xApiVersion, + }, + errors: { + 400: `Bad Request`, + 401: `Unauthorized`, + 404: `Not Found`, + 415: `Unsupported Media Type`, + 500: `Internal Server Error`, + }, + }); + } + /** + * Updates a replica + * Updates an existing replica. + * @param replicaUuid + * @param xApiVersion + * @param requestBody + * @returns any The request outcome + * @throws ApiError + */ + public static putV1Replicas( + replicaUuid: replicaUUID_parameter, + xApiVersion: string = "2025-03-25", + requestBody?: { + /** + * The name of the replica. + */ + name: string; + /** + * The purpose of the replica. This field is not used for training the replica. + */ + purpose?: string; + /** + * A short description of your replica. This field is not used for training the replica. + */ + shortDescription: string; + /** + * The first thing your replica will say when you start a conversation with them. + */ + greeting: string; + /** + * The replica type. + * `individual`: A replica of yourself. + * `character`: A replica of a character: can be anything you want. + * `brand`: A replica of a business persona or organization. + * + */ + type?: "individual" | "character" | "brand"; + /** + * The replica owner ID. + */ + ownerID: string; + /** + * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. + */ + private?: boolean; + /** + * Emails of users who can use the replica when the replica is private. + */ + whitelistEmails?: Array; + /** + * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. + */ + slug: string; + /** + * The tags associated with the replica. Tags help categorize replicas and make them easier to find. + */ + tags?: Array; + /** + * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif + */ + profileImage?: string; + /** + * Suggested questions when starting a conversation. + */ + suggestedQuestions?: Array; + llm: { /** - * Suggested questions when starting a conversation. + * The LLM model of the replica. */ - suggestedQuestions?: Array; - llm: { - /** - * The LLM model of the replica. - */ - model?: 'gpt-4o' | 'claude-3-5-haiku-latest' | 'claude-3-7-sonnet-latest' | 'claude-4-sonnet-20250514' | 'grok-2-latest' | 'grok-3-beta' | 'deepseek-chat' | 'o3-mini' | 'gpt-4o-mini' | 'huggingface-eva' | 'huggingface-dolphin-llama'; - /** - * Deprecated. The system will automatically choose the best approach. - * @deprecated - */ - memoryMode?: 'prompt-caching' | 'rag-search'; - /** - * Who is your replica? How do you want it to talk, respond and act. - */ - systemMessage?: string; - /** - * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information - * - */ - tools?: Array<'getTokenInfo' | 'getUdaoTokenInfo' | 'getSensayTokenInfo' | 'getTokenInfoMEAI' | 'answerToLife' | 'toolhouse' | 'brightUnionGetQuoteTool' | 'brightUnionGetCoverablesTool'>; - }; + model?: + | "gpt-4o" + | "claude-3-5-haiku-latest" + | "claude-3-7-sonnet-latest" + | "claude-4-sonnet-20250514" + | "grok-2-latest" + | "grok-3-beta" + | "deepseek-chat" + | "o3-mini" + | "gpt-4o-mini" + | "huggingface-eva" + | "huggingface-dolphin-llama"; /** - * Text that can be used to generate a voice preview. + * Deprecated. The system will automatically choose the best approach. + * @deprecated */ - voicePreviewText?: string; + memoryMode?: "prompt-caching" | "rag-search"; /** - * The replica UUID + * Who is your replica? How do you want it to talk, respond and act. */ - uuid: string; - }> { - return __request(OpenAPI, { - method: 'GET', - url: '/v1/replicas/{replicaUUID}', - path: { - 'replicaUUID': replicaUuid, - }, - headers: { - 'X-API-Version': xApiVersion, - }, - errors: { - 400: `Bad Request`, - 401: `Unauthorized`, - 404: `Not Found`, - 415: `Unsupported Media Type`, - 500: `Internal Server Error`, - }, - }); - } - /** - * Delete a replica - * Deletes a replica by UUID. - * @param replicaUuid - * @param xApiVersion - * @returns any Replica has been deleted - * @throws ApiError - */ - public static deleteV1Replicas( - replicaUuid: replicaUUID_parameter, - xApiVersion: string = '2025-03-25', - ): CancelablePromise<{ + systemMessage?: string; /** - * Indicates if the replica was deleted successfully + * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information + * */ - success: boolean; - }> { - return __request(OpenAPI, { - method: 'DELETE', - url: '/v1/replicas/{replicaUUID}', - path: { - 'replicaUUID': replicaUuid, - }, - headers: { - 'X-API-Version': xApiVersion, - }, - errors: { - 400: `Bad Request`, - 401: `Unauthorized`, - 404: `Not Found`, - 415: `Unsupported Media Type`, - 500: `Internal Server Error`, - }, - }); + tools?: Array< + | "getTokenInfo" + | "getUdaoTokenInfo" + | "getSensayTokenInfo" + | "getTokenInfoMEAI" + | "answerToLife" + | "toolhouse" + | "brightUnionGetQuoteTool" + | "brightUnionGetCoverablesTool" + >; + }; + /** + * Text that can be used to generate a voice preview. + */ + voicePreviewText?: string; } + ): CancelablePromise<{ /** - * Updates a replica - * Updates an existing replica. - * @param replicaUuid - * @param xApiVersion - * @param requestBody - * @returns any The request outcome - * @throws ApiError + * Indicates if the replica was created successfully */ - public static putV1Replicas( - replicaUuid: replicaUUID_parameter, - xApiVersion: string = '2025-03-25', - requestBody?: { - /** - * The name of the replica. - */ - name: string; - /** - * The purpose of the replica. This field is not used for training the replica. - */ - purpose?: string; - /** - * A short description of your replica. This field is not used for training the replica. - */ - shortDescription: string; - /** - * The first thing your replica will say when you start a conversation with them. - */ - greeting: string; - /** - * The replica type. - * `individual`: A replica of yourself. - * `character`: A replica of a character: can be anything you want. - * `brand`: A replica of a business persona or organization. - * - */ - type?: 'individual' | 'character' | 'brand'; - /** - * The replica owner ID. - */ - ownerID: string; - /** - * Visibility of the replica. When set to `true`, only the owner and users on the allowlist will be able to find the replica and chat with it. - */ - private?: boolean; - /** - * Emails of users who can use the replica when the replica is private. - */ - whitelistEmails?: Array; - /** - * The slug of the replica. Slugs can be used by API consumers to determine the URLs where replicas can be found. - */ - slug: string; - /** - * The tags associated with the replica. Tags help categorize replicas and make them easier to find. - */ - tags?: Array; - /** - * The URL of the profile image of the replica. The image will be downloaded, optimized and stored on our servers, so the URL in the response will be different. Supported formats: .jpg, .jpeg, .png, .bmp, .webp, .avif - */ - profileImage?: string; - /** - * Suggested questions when starting a conversation. - */ - suggestedQuestions?: Array; - llm: { - /** - * The LLM model of the replica. - */ - model?: 'gpt-4o' | 'claude-3-5-haiku-latest' | 'claude-3-7-sonnet-latest' | 'claude-4-sonnet-20250514' | 'grok-2-latest' | 'grok-3-beta' | 'deepseek-chat' | 'o3-mini' | 'gpt-4o-mini' | 'huggingface-eva' | 'huggingface-dolphin-llama'; - /** - * Deprecated. The system will automatically choose the best approach. - * @deprecated - */ - memoryMode?: 'prompt-caching' | 'rag-search'; - /** - * Who is your replica? How do you want it to talk, respond and act. - */ - systemMessage?: string; - /** - * The replica's tools. Tools enable agents to interact with the world. `getTokenInfo`: Allows replica to get token information - * - */ - tools?: Array<'getTokenInfo' | 'getUdaoTokenInfo' | 'getSensayTokenInfo' | 'getTokenInfoMEAI' | 'answerToLife' | 'toolhouse' | 'brightUnionGetQuoteTool' | 'brightUnionGetCoverablesTool'>; - }; - /** - * Text that can be used to generate a voice preview. - */ - voicePreviewText?: string; - }, - ): CancelablePromise<{ - /** - * Indicates if the replica was created successfully - */ - success: boolean; - }> { - return __request(OpenAPI, { - method: 'PUT', - url: '/v1/replicas/{replicaUUID}', - path: { - 'replicaUUID': replicaUuid, - }, - headers: { - 'X-API-Version': xApiVersion, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Bad Request`, - 401: `Unauthorized`, - 404: `Not Found`, - 409: `Conflict`, - 415: `Unsupported Media Type`, - 500: `Internal Server Error`, - }, - }); - } + success: boolean; + }> { + return __request(OpenAPI, { + method: "PUT", + url: "/v1/replicas/{replicaUUID}", + path: { + replicaUUID: replicaUuid, + }, + headers: { + "X-API-Version": xApiVersion, + }, + body: requestBody, + mediaType: "application/json", + errors: { + 400: `Bad Request`, + 401: `Unauthorized`, + 404: `Not Found`, + 409: `Conflict`, + 415: `Unsupported Media Type`, + 500: `Internal Server Error`, + }, + }); + } } diff --git a/src/utils/files.ts b/src/utils/files.ts index 689ccf2..12fec78 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -1,9 +1,9 @@ -import * as fs from 'fs-extra'; -import * as path from 'path'; -import chalk from 'chalk'; -import { TrainingService, KnowledgeBaseService } from '../generated/index'; -import ora from 'ora'; -import inquirer from 'inquirer'; +import * as fs from "fs-extra"; +import * as path from "path"; +import chalk from "chalk"; +import { TrainingService, KnowledgeBaseService } from "../generated/index"; +import ora from "ora"; +import inquirer from "inquirer"; export interface FileInfo { path: string; @@ -21,43 +21,64 @@ export interface UploadResult { } export class FileProcessor { - private static readonly SUPPORTED_EXTENSIONS = ['.txt', '.md', '.json', '.csv', '.log', '.pdf', '.docx']; + private static readonly SUPPORTED_EXTENSIONS = [ + ".txt", + ".md", + ".json", + ".csv", + ".log", + ".pdf", + ".docx", + ]; private static readonly MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB (API limit) - private static readonly TEXT_EXTENSIONS = ['.txt', '.md', '.json', '.csv', '.log']; - private static readonly FILE_EXTENSIONS = ['.pdf', '.docx']; + private static readonly TEXT_EXTENSIONS = [ + ".txt", + ".md", + ".json", + ".csv", + ".log", + ]; + private static readonly FILE_EXTENSIONS = [".pdf", ".docx"]; static async readSystemMessage(folderPath: string): Promise { - const systemMessagePath = path.join(folderPath, 'system-message.txt'); - + const systemMessagePath = path.join(folderPath, "system-message.txt"); + try { if (await fs.pathExists(systemMessagePath)) { - return await fs.readFile(systemMessagePath, 'utf-8'); + return await fs.readFile(systemMessagePath, "utf-8"); } } catch (error) { console.error(chalk.red(`Error reading system-message.txt: ${error}`)); } - + return null; } - static async scanTrainingFiles(folderPath: string): Promise<{ files: FileInfo[]; skipped: string[] }> { - const trainingDataPath = path.join(folderPath, 'training-data'); + static async scanTrainingFiles( + folderPath: string + ): Promise<{ files: FileInfo[]; skipped: string[] }> { + const trainingDataPath = path.join(folderPath, "training-data"); const files: FileInfo[] = []; const skipped: string[] = []; - if (!await fs.pathExists(trainingDataPath)) { + if (!(await fs.pathExists(trainingDataPath))) { return { files, skipped }; } - await FileProcessor.scanDirectory(trainingDataPath, trainingDataPath, files, skipped); - + await FileProcessor.scanDirectory( + trainingDataPath, + trainingDataPath, + files, + skipped + ); + return { files, skipped }; } private static async scanDirectory( - dirPath: string, - basePath: string, - files: FileInfo[], + dirPath: string, + basePath: string, + files: FileInfo[], skipped: string[] ): Promise { const entries = await fs.readdir(dirPath, { withFileTypes: true }); @@ -70,7 +91,7 @@ export class FileProcessor { await FileProcessor.scanDirectory(fullPath, basePath, files, skipped); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); - + if (!FileProcessor.SUPPORTED_EXTENSIONS.includes(ext)) { skipped.push(relativePath); continue; @@ -78,14 +99,18 @@ export class FileProcessor { try { const stats = await fs.stat(fullPath); - + if (stats.size > FileProcessor.MAX_FILE_SIZE) { - skipped.push(`${relativePath} (too large: ${FileProcessor.formatFileSize(stats.size)})`); + skipped.push( + `${relativePath} (too large: ${FileProcessor.formatFileSize( + stats.size + )})` + ); continue; } - const content = await fs.readFile(fullPath, 'utf-8'); - + const content = await fs.readFile(fullPath, "utf-8"); + files.push({ path: fullPath, relativePath, @@ -100,7 +125,7 @@ export class FileProcessor { } static formatFileSize(bytes: number): string { - const units = ['B', 'KB', 'MB', 'GB']; + const units = ["B", "KB", "MB", "GB"]; let size = bytes; let unitIndex = 0; @@ -113,109 +138,146 @@ export class FileProcessor { } static displayFilesSummary(files: FileInfo[], skipped: string[]): void { - console.log(chalk.blue('\n📁 File Processing Summary:')); - + console.log(chalk.blue("\n📁 File Processing Summary:")); + if (files.length > 0) { console.log(chalk.green(`✅ ${files.length} files will be processed:`)); - files.forEach(file => { - console.log(` ${chalk.cyan(file.relativePath)} (${FileProcessor.formatFileSize(file.size)})`); + files.forEach((file) => { + console.log( + ` ${chalk.cyan(file.relativePath)} (${FileProcessor.formatFileSize( + file.size + )})` + ); }); } if (skipped.length > 0) { console.log(chalk.yellow(`⚠️ ${skipped.length} files skipped:`)); - skipped.forEach(file => { + skipped.forEach((file) => { console.log(` ${chalk.gray(file)}`); }); } if (files.length === 0 && skipped.length === 0) { - console.log(chalk.yellow('⚠️ No training files found in training-data folder')); + console.log( + chalk.yellow("⚠️ No training files found in training-data folder") + ); } } - static async uploadTrainingFiles(replicaUuid: string, files: FileInfo[], spinner?: any): Promise { + static async uploadTrainingFiles( + replicaUuid: string, + files: FileInfo[], + spinner?: any + ): Promise { if (!spinner) { - console.log(chalk.blue(`\n📤 Uploading ${files.length} training files...`)); + console.log( + chalk.blue(`\n📤 Uploading ${files.length} training files...`) + ); } - + const results: UploadResult[] = []; - + for (let i = 0; i < files.length; i++) { const file = files[i]; - + if (spinner) { - spinner.text = `Uploading ${file.relativePath}... (${i + 1}/${files.length})`; + spinner.text = `Uploading ${file.relativePath}... (${i + 1}/${ + files.length + })`; } - - const result = await FileProcessor.uploadFileWithRetry(replicaUuid, file, 3, spinner); + + const result = await FileProcessor.uploadFileWithRetry( + replicaUuid, + file, + 3, + spinner + ); results.push(result); - + if (spinner) { - const successful = results.filter(r => r.success).length; - const failed = results.filter(r => !r.success).length; - spinner.text = `Upload progress: ${successful} successful, ${failed} failed (${i + 1}/${files.length})`; + const successful = results.filter((r) => r.success).length; + const failed = results.filter((r) => !r.success).length; + spinner.text = `Upload progress: ${successful} successful, ${failed} failed (${ + i + 1 + }/${files.length})`; } else { if (result.success) { - console.log(chalk.green(` ✅ ${result.file.relativePath} uploaded successfully`)); + console.log( + chalk.green( + ` ✅ ${result.file.relativePath} uploaded successfully` + ) + ); } else { - console.error(chalk.red(` ❌ ${result.file.relativePath} failed after ${result.attempts} attempts: ${result.error}`)); + console.error( + chalk.red( + ` ❌ ${result.file.relativePath} failed after ${result.attempts} attempts: ${result.error}` + ) + ); } } } - + // Summary of upload results (only show if not using spinner, as the spinner success message will show this) if (!spinner) { - const successful = results.filter(r => r.success).length; - const failed = results.filter(r => !r.success).length; - + const successful = results.filter((r) => r.success).length; + const failed = results.filter((r) => !r.success).length; + console.log(chalk.blue(`\n📊 Upload Summary:`)); console.log(chalk.green(` ✅ Successful: ${successful}`)); if (failed > 0) { console.log(chalk.red(` ❌ Failed: ${failed}`)); - console.log(chalk.yellow('\n⚠️ Failed uploads:')); - results.filter(r => !r.success).forEach(r => { - console.log(chalk.gray(` - ${r.file.relativePath}: ${r.error}`)); - }); + console.log(chalk.yellow("\n⚠️ Failed uploads:")); + results + .filter((r) => !r.success) + .forEach((r) => { + console.log(chalk.gray(` - ${r.file.relativePath}: ${r.error}`)); + }); } } - + return results; } - private static async uploadFileWithRetry(replicaUuid: string, file: FileInfo, maxAttempts: number = 3, spinner?: any): Promise { + private static async uploadFileWithRetry( + replicaUuid: string, + file: FileInfo, + maxAttempts: number = 3, + spinner?: any + ): Promise { const ext = path.extname(file.path).toLowerCase(); - let lastError = ''; - + let lastError = ""; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { if (attempt > 1) { if (spinner) { spinner.text = `Retrying ${file.relativePath} (attempt ${attempt}/${maxAttempts})...`; } else { - console.log(chalk.yellow(` 🔄 Retrying ${file.relativePath} (attempt ${attempt}/${maxAttempts})...`)); + console.log( + chalk.yellow( + ` 🔄 Retrying ${file.relativePath} (attempt ${attempt}/${maxAttempts})...` + ) + ); } // Wait a bit before retrying - await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); } - + let knowledgeBaseID: number; - - if (FileProcessor.TEXT_EXTENSIONS.includes(ext)) { - knowledgeBaseID = await FileProcessor.uploadTextTraining(replicaUuid, file, !!spinner); - } else if (FileProcessor.FILE_EXTENSIONS.includes(ext)) { - knowledgeBaseID = await FileProcessor.uploadFileTraining(replicaUuid, file, !!spinner); - } else { - throw new Error(`Unsupported file type: ${ext}`); - } - + + knowledgeBaseID = await FileProcessor.uploadFileTraining( + replicaUuid, + file, + !!spinner + ); + return { file, success: true, knowledgeBaseID, - attempts: attempt + attempts: attempt, }; - } catch (error: any) { lastError = error.message || error.toString(); if (attempt === maxAttempts) { @@ -223,137 +285,163 @@ export class FileProcessor { } } } - + return { file, success: false, error: lastError, - attempts: maxAttempts + attempts: maxAttempts, }; } - private static async uploadTextTraining(replicaUuid: string, file: FileInfo, silent: boolean = false): Promise { + private static async uploadTextTraining( + replicaUuid: string, + file: FileInfo, + silent: boolean = false + ): Promise { if (!silent) { console.log(chalk.gray(` Uploading text: ${file.relativePath}...`)); } - + // Step 1: Create knowledge base entry - const createResponse = await TrainingService.postV1ReplicasTraining(replicaUuid); - + const createResponse = await TrainingService.postV1ReplicasTraining( + replicaUuid + ); + if (!createResponse.success || !createResponse.knowledgeBaseID) { - throw new Error('Failed to create knowledge base entry'); + throw new Error("Failed to create knowledge base entry"); } - + // Step 2: Update with text content await TrainingService.putV1ReplicasTraining( replicaUuid, createResponse.knowledgeBaseID, { - rawText: file.content + rawText: file.content, } ); - + return createResponse.knowledgeBaseID; } - private static async uploadFileTraining(replicaUuid: string, file: FileInfo, silent: boolean = false): Promise { + private static async uploadFileTraining( + replicaUuid: string, + file: FileInfo, + silent: boolean = false + ): Promise { if (!silent) { console.log(chalk.gray(` Uploading file: ${file.relativePath}...`)); } - + // Step 1: Create knowledge base entry for file upload - const fileKbResponse = await KnowledgeBaseService.postV1ReplicasKnowledgeBase( - replicaUuid, - '2025-03-25', - { - filename: path.basename(file.path), - autoRefresh: false - } - ); - + const fileKbResponse = + await KnowledgeBaseService.postV1ReplicasKnowledgeBase( + replicaUuid, + "2025-03-25", + { + filename: path.basename(file.path), + autoRefresh: false, + } + ); + const fileResult = fileKbResponse.results[0]; - if ('error' in fileResult) { - throw new Error(`Failed to create file knowledge base: ${fileResult.error}`); + if ("error" in fileResult) { + throw new Error( + `Failed to create file knowledge base: ${fileResult.error}` + ); } - + if (!fileResult.signedURL) { - throw new Error('Failed to get signed URL for file upload'); + throw new Error("Failed to get signed URL for file upload"); } - + // Step 2: Upload file to signed URL const fileBuffer = await fs.readFile(file.path); - + const uploadResponse = await fetch(fileResult.signedURL, { - method: 'PUT', + method: "PUT", body: fileBuffer, headers: { - 'Content-Type': 'application/octet-stream' - } + "Content-Type": "text/plain", + }, }); - + if (!uploadResponse.ok) { throw new Error(`Upload failed with status: ${uploadResponse.status}`); } - + return fileResult.knowledgeBaseID!; } - static async clearExistingTrainingData(replicaUuid: string, force: boolean = false, nonInteractive: boolean = false): Promise { - console.log(chalk.blue('\n🗄️ Checking existing training data...')); - + static async clearExistingTrainingData( + replicaUuid: string, + force: boolean = false, + nonInteractive: boolean = false + ): Promise { + console.log(chalk.blue("\n🗄️ Checking existing training data...")); + try { let totalDeleted = 0; let totalFailed = 0; let foundAnyEntries = false; let spinner: any = null; - + // Keep looping until no more training entries are found while (true) { // Get training entries for this replica const trainingResponse = await TrainingService.getV1Training1(); - + if (!trainingResponse.success) { - throw new Error('Failed to fetch existing training data'); + throw new Error("Failed to fetch existing training data"); } - + // Filter entries for this replica const replicaEntries = trainingResponse.items.filter( (item: any) => item.replica_uuid === replicaUuid ); - + if (replicaEntries.length === 0) { // No more entries found, we're done break; } - + if (!foundAnyEntries) { // First batch found - show count and ask for confirmation foundAnyEntries = true; - console.log(chalk.yellow(`📊 Found ${replicaEntries.length} existing training entries`)); - + console.log( + chalk.yellow( + `📊 Found ${replicaEntries.length} existing training entries` + ) + ); + // Ask for confirmation unless force flag is set if (!force) { if (nonInteractive) { - throw new Error('Existing training data found. Use --force to automatically delete it, or run interactively to confirm.'); + throw new Error( + "Existing training data found. Use --force to automatically delete it, or run interactively to confirm." + ); } - + const { confirmDelete } = await inquirer.prompt({ - type: 'confirm', - name: 'confirmDelete', - message: 'This will delete all existing training data for this replica. Continue?', - default: false + type: "confirm", + name: "confirmDelete", + message: + "This will delete all existing training data for this replica. Continue?", + default: false, }); - + if (!confirmDelete) { - console.log(chalk.yellow('⚠️ Training data deletion cancelled by user')); + console.log( + chalk.yellow("⚠️ Training data deletion cancelled by user") + ); return; } } - - console.log(chalk.blue('🗑️ Clearing existing training data...')); - spinner = ora('Deleting training entries...').start(); + + console.log(chalk.blue("🗑️ Clearing existing training data...")); + spinner = ora("Deleting training entries...").start(); } - + // Delete all entries in this batch for (const entry of replicaEntries) { try { @@ -362,249 +450,357 @@ export class FileProcessor { spinner.text = `Deleting training entries... ${totalDeleted} deleted so far`; } catch (error: any) { totalFailed++; - console.log(chalk.red(`\n❌ Failed to delete entry ${entry.id}: ${error.message}`)); + console.log( + chalk.red( + `\n❌ Failed to delete entry ${entry.id}: ${error.message}` + ) + ); if (spinner) { spinner.text = `Deleting training entries... ${totalDeleted} deleted, ${totalFailed} failed`; } } } - + // Small delay to avoid overwhelming the API - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } - + if (!foundAnyEntries) { - console.log(chalk.green('✅ No existing training data found - starting fresh')); + console.log( + chalk.green("✅ No existing training data found - starting fresh") + ); return; } - + if (spinner) { - spinner.succeed(chalk.green(`✅ Training data cleanup completed: ${totalDeleted} deleted, ${totalFailed} failed`)); + spinner.succeed( + chalk.green( + `✅ Training data cleanup completed: ${totalDeleted} deleted, ${totalFailed} failed` + ) + ); } - + if (totalFailed > 0) { - console.log(chalk.yellow(`⚠️ ${totalFailed} entries could not be deleted - they may need manual cleanup`)); + console.log( + chalk.yellow( + `⚠️ ${totalFailed} entries could not be deleted - they may need manual cleanup` + ) + ); } - } catch (error: any) { - console.error(chalk.red(`❌ Failed to clear existing training data: ${error.message}`)); + console.error( + chalk.red(`❌ Failed to clear existing training data: ${error.message}`) + ); throw error; } } - static async pollTrainingStatus(replicaUuid: string, uploadResults: UploadResult[], totalFiles: number): Promise { - const successfulUploads = uploadResults.filter(r => r.success); + static async pollTrainingStatus( + replicaUuid: string, + uploadResults: UploadResult[], + totalFiles: number + ): Promise { + const successfulUploads = uploadResults.filter((r) => r.success); if (successfulUploads.length === 0) { - console.log(chalk.yellow('\n⚠️ No files were uploaded successfully, skipping training status check.')); + console.log( + chalk.yellow( + "\n⚠️ No files were uploaded successfully, skipping training status check." + ) + ); return; } - console.log(chalk.blue(`\n🔄 Monitoring training progress for ${successfulUploads.length} files...`)); - - const spinner = ora('Checking training status...').start(); - const knowledgeBaseIDs = successfulUploads.map(r => r.knowledgeBaseID!); - + console.log( + chalk.blue( + `\n🔄 Monitoring training progress for ${successfulUploads.length} files...` + ) + ); + + const spinner = ora("Checking training status...").start(); + const knowledgeBaseIDs = successfulUploads.map((r) => r.knowledgeBaseID!); + try { let allReady = false; let attempts = 0; const maxAttempts = 360; // 30 minutes with 5-second intervals let latestTrainingResponse: any = null; - + while (!allReady && attempts < maxAttempts) { attempts++; - + // Get all training entries for this replica (handle pagination) let allReplicaEntries: any[] = []; let fetchSuccessful = false; - + try { let page = 1; - + while (true) { - const trainingResponse = await TrainingService.getV1Training1(undefined, undefined, page.toString(), '100'); - + const trainingResponse = await TrainingService.getV1Training1( + undefined, + undefined, + page.toString(), + "100" + ); + if (!trainingResponse.success) { - throw new Error('Failed to fetch training status'); + throw new Error("Failed to fetch training status"); } - + const replicaEntriesInPage = trainingResponse.items.filter( - (item: any) => item.replica_uuid === replicaUuid && knowledgeBaseIDs.includes(item.id) + (item: any) => + item.replica_uuid === replicaUuid && + knowledgeBaseIDs.includes(item.id) ); - + allReplicaEntries.push(...replicaEntriesInPage); - + // If we got fewer items than the limit, we've reached the end if (trainingResponse.items.length < 100) { break; } - + page++; } - + fetchSuccessful = true; } catch (error: any) { // Log the error but continue polling const remainingTime = (maxAttempts - attempts) * 5; // seconds const minutes = Math.floor(remainingTime / 60); const seconds = remainingTime % 60; - const timeDisplay = `${minutes}:${seconds.toString().padStart(2, '0')}`; - + const timeDisplay = `${minutes}:${seconds + .toString() + .padStart(2, "0")}`; + spinner.stop(); - console.log(chalk.yellow(`⚠️ API error: ${error.message} - retrying... | Timeout in ${timeDisplay}`)); + console.log( + chalk.yellow( + `⚠️ API error: ${error.message} - retrying... | Timeout in ${timeDisplay}` + ) + ); spinner.start(); spinner.text = `Retrying status check after error...`; - + // Wait 5 seconds before next attempt - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } - + if (!fetchSuccessful) { continue; // Skip processing if fetch failed } - + latestTrainingResponse = { success: true, items: allReplicaEntries }; const replicaEntries = allReplicaEntries; - + // Count entries by status const statusCounts: { [key: string]: number } = {}; replicaEntries.forEach((entry: any) => { const status = entry.status; statusCounts[status] = (statusCounts[status] || 0) + 1; }); - + // Create compact status display with priorities (most important statuses first) - const statusPriority = ['ERR_FILE_PROCESSING', 'ERR_TEXT_PROCESSING', 'ERR_TEXT_TO_VECTOR', 'SYNC_ERROR', 'PROCESSING', 'READY', 'VECTOR_CREATED', 'AWAITING_UPLOAD', 'SUPABASE_ONLY', 'BLANK']; + const statusPriority = [ + "ERR_FILE_PROCESSING", + "ERR_TEXT_PROCESSING", + "ERR_TEXT_TO_VECTOR", + "SYNC_ERROR", + "PROCESSING", + "READY", + "VECTOR_CREATED", + "AWAITING_UPLOAD", + "SUPABASE_ONLY", + "BLANK", + ]; const statusDisplay = statusPriority - .filter(status => statusCounts[status] > 0) - .map(status => `${status}: ${statusCounts[status]}`) + .filter((status) => statusCounts[status] > 0) + .map((status) => `${status}: ${statusCounts[status]}`) .concat( // Add any other statuses not in the priority list Object.entries(statusCounts) .filter(([status]) => !statusPriority.includes(status)) .map(([status, count]) => `${status}: ${count}`) ) - .join(' | '); - + .join(" | "); + const remainingTime = (maxAttempts - attempts) * 5; // seconds const minutes = Math.floor(remainingTime / 60); const seconds = remainingTime % 60; - const timeDisplay = `${minutes}:${seconds.toString().padStart(2, '0')}`; - + const timeDisplay = `${minutes}:${seconds.toString().padStart(2, "0")}`; + // Show status breakdown - temporarily stop spinner to show clean status if (statusDisplay) { spinner.stop(); - console.log(chalk.blue(`📊 Training Status: ${statusDisplay} | Timeout in ${timeDisplay}`)); + console.log( + chalk.blue( + `📊 Training Status: ${statusDisplay} | Timeout in ${timeDisplay}` + ) + ); spinner.start(); spinner.text = `Monitoring training progress...`; } else { spinner.text = `Checking training status... - Timeout in ${timeDisplay}`; } - - const readyCount = statusCounts['READY'] || 0; - const vectorCreatedCount = statusCounts['VECTOR_CREATED'] || 0; + + const readyCount = statusCounts["READY"] || 0; + const vectorCreatedCount = statusCounts["VECTOR_CREATED"] || 0; const completedCount = readyCount + vectorCreatedCount; const errorCount = Object.entries(statusCounts) - .filter(([status]) => status.startsWith('ERR_') || status === 'SYNC_ERROR') + .filter( + ([status]) => status.startsWith("ERR_") || status === "SYNC_ERROR" + ) .reduce((sum, [, count]) => sum + count, 0); - + if (completedCount === successfulUploads.length) { allReady = true; - spinner.succeed(chalk.green(`✅ All ${successfulUploads.length} files have been processed and are ready!`)); + spinner.succeed( + chalk.green( + `✅ All ${successfulUploads.length} files have been processed and are ready!` + ) + ); } else if (errorCount > 0) { - spinner.warn(chalk.yellow(`⚠️ ${errorCount} files encountered errors during processing`)); - + spinner.warn( + chalk.yellow( + `⚠️ ${errorCount} files encountered errors during processing` + ) + ); + // Show which files had errors - const errorEntries = replicaEntries.filter((entry: any) => - entry.status.startsWith('ERR_') || entry.status === 'SYNC_ERROR' + const errorEntries = replicaEntries.filter( + (entry: any) => + entry.status.startsWith("ERR_") || entry.status === "SYNC_ERROR" ); - - console.log(chalk.red('\n❌ Files with errors:')); + + console.log(chalk.red("\n❌ Files with errors:")); errorEntries.forEach((entry: any) => { - console.log(chalk.gray(` - ID ${entry.id}: ${entry.status} ${entry.filename ? `(${entry.filename})` : ''}`)); + console.log( + chalk.gray( + ` - ID ${entry.id}: ${entry.status} ${ + entry.filename ? `(${entry.filename})` : "" + }` + ) + ); }); - + allReady = true; // Stop polling as we have errors } else if (attempts >= maxAttempts) { - spinner.fail(chalk.red('⏰ Training status check timed out after 30 minutes')); - - console.log(chalk.yellow('\n📊 Final status breakdown:')); + spinner.fail( + chalk.red("⏰ Training status check timed out after 30 minutes") + ); + + console.log(chalk.yellow("\n📊 Final status breakdown:")); Object.entries(statusCounts) .sort(([a], [b]) => a.localeCompare(b)) .forEach(([status, count]) => { let color = chalk.gray; - if (status === 'READY' || status === 'VECTOR_CREATED') color = chalk.green; - else if (status === 'PROCESSING') color = chalk.blue; - else if (status.startsWith('ERR_') || status === 'SYNC_ERROR') color = chalk.red; - else if (status === 'AWAITING_UPLOAD' || status === 'SUPABASE_ONLY') color = chalk.yellow; - + if (status === "READY" || status === "VECTOR_CREATED") + color = chalk.green; + else if (status === "PROCESSING") color = chalk.blue; + else if (status.startsWith("ERR_") || status === "SYNC_ERROR") + color = chalk.red; + else if ( + status === "AWAITING_UPLOAD" || + status === "SUPABASE_ONLY" + ) + color = chalk.yellow; + console.log(color(` ${status}: ${count}`)); }); - + allReady = true; } else { // Wait 5 seconds before next check - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); } } - + // Verify file count matches (get all training entries for this replica) let allTrainingEntries: any[] = []; let page = 1; - + while (true) { - const trainingResponse = await TrainingService.getV1Training1(undefined, undefined, page.toString(), '100'); - + const trainingResponse = await TrainingService.getV1Training1( + undefined, + undefined, + page.toString(), + "100" + ); + if (!trainingResponse.success) { break; // Don't fail the whole process if this verification fails } - + const replicaEntriesInPage = trainingResponse.items.filter( (item: any) => item.replica_uuid === replicaUuid ); - + allTrainingEntries.push(...replicaEntriesInPage); - + // If we got fewer items than the limit, we've reached the end if (trainingResponse.items.length < 100) { break; } - + page++; } - + if (allTrainingEntries.length > 0) { const apiTrainingCount = allTrainingEntries.length; - - console.log(chalk.blue('\n📊 Training Status Summary:')); + + console.log(chalk.blue("\n📊 Training Status Summary:")); console.log(chalk.green(` 📁 Local files scanned: ${totalFiles}`)); - console.log(chalk.green(` 📤 Files uploaded: ${successfulUploads.length}`)); - console.log(chalk.green(` 🗄️ Files in training system: ${apiTrainingCount}`)); - + console.log( + chalk.green(` 📤 Files uploaded: ${successfulUploads.length}`) + ); + console.log( + chalk.green(` 🗄️ Files in training system: ${apiTrainingCount}`) + ); + if (apiTrainingCount !== totalFiles) { - console.log(chalk.yellow(`\n⚠️ Mismatch detected: ${totalFiles} local files vs ${apiTrainingCount} in system`)); - + console.log( + chalk.yellow( + `\n⚠️ Mismatch detected: ${totalFiles} local files vs ${apiTrainingCount} in system` + ) + ); + // Show which files might be missing - const apiEntries = latestTrainingResponse.items.filter((item: any) => item.replica_uuid === replicaUuid); - const uploadedFileNames = successfulUploads.map(r => path.basename(r.file.path)); - const apiFileNames = apiEntries.map((entry: any) => entry.filename).filter(Boolean); - - const missingInApi = uploadedFileNames.filter(name => !apiFileNames.includes(name)); + const apiEntries = latestTrainingResponse.items.filter( + (item: any) => item.replica_uuid === replicaUuid + ); + const uploadedFileNames = successfulUploads.map((r) => + path.basename(r.file.path) + ); + const apiFileNames = apiEntries + .map((entry: any) => entry.filename) + .filter(Boolean); + + const missingInApi = uploadedFileNames.filter( + (name) => !apiFileNames.includes(name) + ); if (missingInApi.length > 0) { - console.log(chalk.red('\n❌ Files missing from training system:')); + console.log(chalk.red("\n❌ Files missing from training system:")); missingInApi.forEach((fileName: string) => { console.log(chalk.gray(` - ${fileName}`)); }); } } else { - console.log(chalk.green('✅ File count matches - all files are accounted for!')); + console.log( + chalk.green("✅ File count matches - all files are accounted for!") + ); } } - } catch (error: any) { - spinner.fail(chalk.red(`Failed to check training status: ${error.message}`)); - console.log(chalk.yellow('⚠️ Training status monitoring stopped due to persistent errors')); + spinner.fail( + chalk.red(`Failed to check training status: ${error.message}`) + ); + console.log( + chalk.yellow( + "⚠️ Training status monitoring stopped due to persistent errors" + ) + ); // Don't throw - just end the monitoring gracefully } } -} \ No newline at end of file +}