UNPKG

mcp-talent-server

Version:

Model Context Protocol server for talent management tools

408 lines (385 loc) 19.6 kB
import { z } from 'zod'; import { PutObjectCommand } from '@aws-sdk/client-s3'; import archiver from 'archiver'; import axios from 'axios'; import { Folder, Image } from '../models/gallery.model.js'; import s3, { BUCKET_NAME } from '../services/aws.js'; import dotenv from 'dotenv'; dotenv.config({ debug: false }); const GalleryZipExportInput = z.object({ folderNames: z .array(z.string()) .describe('Folder names to search for'), imageNames: z .array(z.string()) .describe('Image names to search for'), foldersSearchQuery: z.string().describe('MongoDB query object to search folders'), imagesSearchQuery: z.string().describe('MongoDB query object to search images'), includeSubfolders: z .boolean() .default(true) .describe('Whether to include images from subfolders'), maxImages: z .number() .min(1) .max(100) .default(50) .describe('Maximum number of images to include in zip'), }); export class GalleryZipExporter { async buildFolderPath(folderId) { const pathParts = []; let currentFolderId = folderId; while (currentFolderId) { const folder = await Folder.findOne({ _id: currentFolderId, }).lean(); if (!folder) break; pathParts.unshift(folder.name); currentFolderId = folder.parentFolderId?.toString() || null; } return pathParts.join('/'); } async downloadImage(url) { try { const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 30000, // 30 second timeout }); return Buffer.from(response.data); } catch (error) { console.error(`Failed to download image from ${url}:`, error); throw new Error(`Failed to download image: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createZipFile(searchResults, query) { console.log("Creating zip file"); return new Promise((resolve, reject) => { const archive = archiver('zip', { zlib: { level: 9 }, // Maximum compression }); const buffers = []; archive.on('data', (chunk) => { buffers.push(chunk); }); archive.on('end', () => { const zipBuffer = Buffer.concat(buffers); resolve(zipBuffer); }); archive.on('error', (err) => { reject(err); }); // Process images and add to zip const processImages = async () => { try { for (const result of searchResults) { const { image, folderPath } = result; // Download image const imageBuffer = await this.downloadImage(image.url.replace(/ /g, '+')); // Create safe filename const safeFilename = image.originalName.replace(/[<>:"/\\|?*]/g, '_'); const zipPath = folderPath ? `${folderPath}/${safeFilename}` : safeFilename; // Add to zip archive.append(imageBuffer, { name: zipPath }); } // Add metadata file const metadata = { exportDate: new Date().toISOString(), searchQuery: query, totalImages: searchResults.length, searchCriteria: { minimumScore: 8, scoringMethod: 'exact_match(10), starts_with(9), contains(7), fuzzy_match(6)', }, images: searchResults.map((r) => ({ originalName: r.image.originalName, folderPath: r.folderPath, uploadDate: r.image.createdAt, size: r.image.size, tags: r.image.tags, matchScore: r.matchScore || 0, matchType: r.matchType || 'unknown', analysis: { description: r.image.analysis.description, labels: r.image.analysis.labels.slice(0, 5), // Top 5 labels }, })), }; archive.append(JSON.stringify(metadata, null, 2), { name: 'export-metadata.json', }); // Finalize the archive archive.finalize(); } catch (error) { archive.destroy(); reject(error); } }; processImages(); }); } async uploadZipToS3(zipBuffer, filename) { console.log("Uploading zip to S3"); try { const uploadCommand = new PutObjectCommand({ Bucket: BUCKET_NAME, Key: `gallery-exports/${filename}`, Body: zipBuffer, ContentType: 'application/zip', ACL: 'public-read', Metadata: { 'export-type': 'gallery-search-results', }, }); await s3.send(uploadCommand); // Generate public URL const publicUrl = `https://${BUCKET_NAME}.s3.${process.env.AWS_REGION || ''}.amazonaws.com/gallery-exports/${filename}`; return publicUrl; } catch (error) { console.error('Failed to upload zip to S3:', error); throw new Error(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async exportGallerySearch(input) { try { // Validate input this.validateExportInput(input); const searchResults = await this.exportImagesAndFolders(input.folderNames, input.imageNames, input.maxImages, input.includeSubfolders, input.foldersSearchQuery, input.imagesSearchQuery); if (searchResults.length === 0) { throw new Error(`No images found matching folders: [${input.folderNames.join(', ')}] and images: [${input.imageNames.join(', ')}]. Try different search terms.`); } // Create zip file with error handling const zipBuffer = await this.createZipFileWithValidation(searchResults, input.folderNames.join(' ')); // Generate unique filename with proper sanitization const filename = this.generateSafeFilename(input.folderNames.join(' ')); // Upload to S3 with retry logic const downloadUrl = await this.uploadZipToS3WithRetry(zipBuffer, filename); // Get unique folders const folders = this.extractUniqueFolders(searchResults); return { downloadUrl, filename, stats: { totalImages: searchResults.length, zipSizeBytes: zipBuffer.length, searchQuery: input.folderNames.concat(input.imageNames).join(' '), exportDate: new Date().toISOString(), folders, }, }; } catch (error) { console.error('Gallery export failed:', error); this.logExportError(error, input); throw error; } } validateExportInput(input) { if (!input.folderNames || input.folderNames.length === 0) { if (!input.imageNames || input.imageNames.length === 0) { throw new Error('Either folderNames or imageNames must be provided'); } } // Validate folder names for (const folderName of input.folderNames || []) { if (typeof folderName !== 'string' || folderName.trim().length === 0) { throw new Error('All folder names must be non-empty strings'); } } // Validate image names for (const imageName of input.imageNames || []) { if (typeof imageName !== 'string' || imageName.trim().length === 0) { throw new Error('All image names must be non-empty strings'); } } } async createZipFileWithValidation(searchResults, query) { if (searchResults.length === 0) { throw new Error('No search results to create zip file'); } try { return await this.createZipFile(searchResults, query); } catch (error) { console.error('Failed to create zip file:', error); throw new Error(`Zip creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } generateSafeFilename(query) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const safeQuery = query .replace(/[^a-zA-Z0-9\s]/g, '') .replace(/\s+/g, '_') .substring(0, 20) .toLowerCase(); return `gallery_export_${safeQuery || 'search'}_${timestamp}.zip`; } async uploadZipToS3WithRetry(zipBuffer, filename, maxRetries = 3) { let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await this.uploadZipToS3(zipBuffer, filename); } catch (error) { lastError = error instanceof Error ? error : new Error('Unknown upload error'); console.warn(`Upload attempt ${attempt} failed:`, lastError.message); if (attempt < maxRetries) { // Wait before retry (exponential backoff) await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } } } throw new Error(`Failed to upload after ${maxRetries} attempts: ${lastError?.message}`); } extractUniqueFolders(searchResults) { return [...new Set(searchResults .map((r) => r.folderPath) .filter((path) => Boolean(path && path.trim())))]; } logExportError(error, input) { const errorLog = { timestamp: new Date().toISOString(), error: error instanceof Error ? error.message : 'Unknown error', input: { folderNames: input.folderNames, imageNames: input.imageNames, maxImages: input.maxImages, includeSubfolders: input.includeSubfolders, }, }; console.error('Gallery export error details:', JSON.stringify(errorLog, null, 2)); } async exportImagesAndFolders(folderNames, imageNames, maxImages, includeSubfolders, foldersSearchQuery, imagesSearchQuery) { const folderQuery = JSON.parse(foldersSearchQuery); const imgaeQuery = JSON.parse(imagesSearchQuery); try { const folders = await Folder.find(folderQuery).lean(); const images = await Image.find({ // userId, $and: [ imgaeQuery, { folderId: { $in: folders.map((f) => f._id) }, // only exact match here }, ], }).lean(); // Add images from matching folders const folderIds = folders.map((f) => f._id.toString()); // if (includeSubfolders && folderIds.length > 0) { // // Get all subfolders recursively for folders // const allSubfolders = await this.getAllSubfolders(folderIds, userId); // folderIds.push(...allSubfolders.map((f: FolderType) => f._id)); // } const results = []; if (folderIds.length > 0) { for (const image of images) { if (!results.some((r) => r.image._id?.toString() === image._id.toString())) { const folder = folders.find((f) => f._id.toString() === image.folderId.toString()); if (folder) { const folderPath = await this.buildFolderPath(folder._id.toString()); console.log('folderPath', folderPath); results.push({ image: image, folder: folder, folderPath, matchScore: 10, matchType: 'folder_name', }); } } } } return results; } catch (error) { console.error('Error exporting images and folders:', error); throw error; } } } export const GalleryZipExportDescription = `**Gallery Zip Export – Intelligent Media Asset Packaging Tool** You are tasked with designing and/or describing the complete logic and output for an automated, intelligent tool that enables exporting and packaging talent gallery images into downloadable ZIP files. This tool must utilize advanced search and scoring algorithms, database and naming logic, robust cloud storage integration, and detailed metadata/export reporting. The output must always present a single downloadable ZIP file link, accompanied by comprehensive metadata and statistics. At every stage, first explain your reasoning for each decision or output before providing the results. Below, follow the steps and requirements in order. Examples are provided to illustrate expected reasoning, workflows, inputs, and outputs. # Steps 1. **Input Interpretation & Parameter Extraction** - Identify the user's request scope (e.g., specific platform content, creator folders, date ranges, etc.) from the command or use case. - Parse/validate all parameters: folderNames, imageNames, foldersSearchQuery, imagesSearchQuery, maxImages, includeSubfolders. 2. **Smart Search & Scoring** - For each supplied folderName and image criteria, build and explain search queries against the database (MongoDB) that reflect supplied filters, recursion, and any custom requirements. - For every folder and image found, apply the advanced scoring algorithm: - Exact (10/10), Prefix (9/10), Contains (7/10), Fuzzy (6/10), multi-term bonus, and include only items with score ≥8/10. - Describe WHY each folder/image is selected, detailing the matching rules and score breakdown. 3. **Validation & Error Handling** - For all scored images, confirm existence and accessibility. - Document reasoning and action for any missing or problematic files (e.g., skipped, retried). 4. **Packaging** - Retrieve all qualifying images, structure them in a ZIP file mirroring the original folder/naming hierarchy. - Reason through folder structure preservation, recursive subfolders, and metadata inclusion. 5. **Upload & Delivery** - Automatically upload the ZIP file to AWS S3 (with security, named uniquely). - Generate a time-limited, secure public download link. - Ensure robust retry logic and performance practices (including concurrent processing, real-time tracking). 6. **Export Reporting & Statistics** - Prepare a metadata report detailing: - Number of images included, folders involved, export time, ZIP file size, creation timestamp, expiration time. - Detailed scoring breakdown (for folders/images selected), inclusion criteria, and errors encountered. - Quality control, access control, how cloud/storage/cleanup was performed. 7. **Return Output** - First summarize your reasoning and all selection/validation processes. - Then, provide the final structured response as specified below. # Output Format All final outputs must be provided in a valid, unwrapped JSON object (not inside code blocks), with these keys: - download_url: [public ZIP link] - file_metadata: {size_bytes, created_at, expires_at} - export_statistics: {image_count, folder_count, processing_time_sec} - match_details: [list of {folder/image, search_terms, score, match_type, included: true/false, notes}] - error_reports: [any missing files, failures, and explanation of recovery or omission] Precede the JSON with your step-by-step reasoning for search, scoring, and error handling (as prose, 1-2 paragraphs). # Examples **Example 1 – Request** - Input: "Export all Instagram content for Yosef Haiem" - Parameters: folderNames=["Yosef Haiem"], imageNames=["IG*"], includeSubfolders=true, maxImages=50 **Example 1 – Reasoning** The request targets all Instagram media in the "Yosef Haiem" folder, including subfolders. I searched for folders named "Yosef Haiem" using a prefix and exact match, then queried all images with the IG platform prefix. Each image was scored using the algorithm (exact prefix for "IG" = 9/10), and only those scoring ≥8/10 were included. Any missing files were logged and excluded from packaging. The ZIP preserved all folder structure and was securely uploaded to S3. **Example 1 – Output** { "download_url": "https://s3.amazonaws.com/bucket/hash_export_yosef_IG.zip", "file_metadata": { "size_bytes": 83456789, "created_at": "2024-07-10T13:56:04Z", "expires_at": "2024-07-17T13:56:04Z" }, "export_statistics": { "image_count": 37, "folder_count": 1, "processing_time_sec": 48.2 }, "match_details": [ {"file": "IG Story 001.jpg", "search_terms": ["IG*"], "score": 9.0, "match_type": "Prefix", "included": true, "notes": ""}, {"file": "IG Live 008.jpg", "search_terms": ["IG*"], "score": 8.5, "match_type": "Prefix", "included": true, "notes": ""}, {"file": "YT Age 01.jpg", "search_terms": ["IG*"], "score": 6.0, "match_type": "Fuzzy", "included": false, "notes": "Score below threshold"} ], "error_reports": [ {"file": "IG Reel 003.jpg", "issue": "File missing from storage", "recovery": "Omitted from ZIP"} ] } (**Real examples should be scaled up for complex, multi-folder or multi-platform requests, following this same structure.**) # Notes - image name is save as originalName and folder name is save as name - folder names in query can be partial so use regex to fetch folders in mongodb query - always use regex based search searching amoung image names - Always perform and explain reasoning and query logic before presenting the final output. - Never skip error reporting (missing, skipped, failed files) even if the list is empty. - Always preserve security, quality control, and workflow integrity as described. - Output must be JSON as detailed above, preceded by prose reasoning/steps. Never wrap JSON in code blocks. - In output always mention which folder and image are found which are not available in database. **Reminder**: Begin each output with a paragraph or two summarizing reasoning, search/matching/scoring steps, and error recovery, then return the requested fields in a single JSON object. Always output exactly one downloadable ZIP link and include all mandatory metadata/statistics.`; export const galleryZipExportSchema = GalleryZipExportInput; //# sourceMappingURL=gallery-zip-export.js.map