ideas-generator/server/routes/vectorStores.js
DJP ca4ed4976d Add dual-agent system with file upload support and compact UI
- Implement dual-agent architecture supporting both Chat Completions and Responses API
- Add comprehensive file upload functionality for responses agents with image and PDF support
- Integrate OpenAI Conversations API for persistent file context across messages
- Add compact UI design with reduced font sizes and tighter spacing for better chat focus
- Include vector store management, document upload system, and admin dashboard enhancements
- Fix conversation persistence ensuring uploaded files remain accessible in follow-up questions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 14:45:25 -04:00

359 lines
No EOL
10 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { VectorStore, Document, Assistant } = require('../models');
const OpenAI = require('openai');
const responsesService = require('../utils/responses');
const router = express.Router();
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
organization: process.env.OPENAI_ORG_ID,
});
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
const uniqueName = uuidv4() + path.extname(file.originalname);
cb(null, uniqueName);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 100 * 1024 * 1024, // 100MB limit
},
fileFilter: (req, file, cb) => {
// Allow common document types
const allowedTypes = [
'application/pdf',
'text/plain',
'text/markdown',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/csv',
'application/json'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Unsupported file type. Please upload PDF, TXT, MD, DOC, DOCX, CSV, or JSON files.'));
}
}
});
// Create a new vector store
router.post('/', async (req, res, next) => {
try {
const { name, description, expiresAfter } = req.body;
if (!name) {
return res.status(400).json({
error: 'Validation Error',
message: 'Vector store name is required'
});
}
console.log(`Creating vector store: ${name}`);
// Create vector store in OpenAI
const openaiVectorStore = await openai.beta.vectorStores.create({
name,
expires_after: expiresAfter ? {
anchor: "last_active_at",
days: expiresAfter.days || 7
} : undefined
});
// Store in our database
const vectorStore = await VectorStore.create({
openaiVectorStoreId: openaiVectorStore.id,
name,
description,
fileCount: openaiVectorStore.file_counts?.total || 0,
bytesUsed: openaiVectorStore.usage_bytes || 0,
status: openaiVectorStore.status,
expiresAfter,
lastActiveAt: new Date(openaiVectorStore.last_active_at * 1000),
metadata: {
createdBy: 'admin', // You might want to track who created it
openaiMetadata: openaiVectorStore.metadata
}
});
console.log(`Vector store created: ${vectorStore.id} (OpenAI: ${openaiVectorStore.id})`);
res.json({
vectorStore: {
id: vectorStore.id,
name: vectorStore.name,
description: vectorStore.description,
fileCount: vectorStore.fileCount,
bytesUsed: vectorStore.bytesUsed,
status: vectorStore.status,
openaiId: vectorStore.openaiVectorStoreId,
createdAt: vectorStore.createdAt
}
});
} catch (error) {
console.error('Error creating vector store:', error);
next(error);
}
});
// List vector stores
router.get('/', async (req, res, next) => {
try {
const vectorStores = await VectorStore.findAll({
order: [['createdAt', 'DESC']],
include: [{
model: Document,
as: 'documents',
attributes: ['id', 'name', 'fileSize', 'status']
}]
});
res.json({
vectorStores: vectorStores.map(vs => ({
id: vs.id,
name: vs.name,
description: vs.description,
fileCount: vs.fileCount,
bytesUsed: vs.bytesUsed,
status: vs.status,
openaiId: vs.openaiVectorStoreId,
documents: vs.documents,
createdAt: vs.createdAt,
lastActiveAt: vs.lastActiveAt
}))
});
} catch (error) {
console.error('Error listing vector stores:', error);
next(error);
}
});
// Upload file to vector store
router.post('/:vectorStoreId/files', upload.single('file'), async (req, res, next) => {
try {
const { vectorStoreId } = req.params;
const { assistantId, purpose = 'assistants' } = req.body;
if (!req.file) {
return res.status(400).json({
error: 'Validation Error',
message: 'No file uploaded'
});
}
const vectorStore = await VectorStore.findByPk(vectorStoreId);
if (!vectorStore) {
return res.status(404).json({
error: 'Not Found',
message: 'Vector store not found'
});
}
console.log(`Uploading file ${req.file.originalname} to vector store ${vectorStore.name}`);
// Upload file to OpenAI
const openaiFile = await openai.files.create({
file: fs.createReadStream(req.file.path),
purpose: 'assistants'
});
console.log(`File uploaded to OpenAI: ${openaiFile.id}`);
// Add file to vector store
await openai.beta.vectorStores.files.create(
vectorStore.openaiVectorStoreId,
{
file_id: openaiFile.id
}
);
// Store document record
const document = await Document.create({
openaiFileId: openaiFile.id,
name: req.file.filename,
originalName: req.file.originalname,
filePath: req.file.path,
fileSize: req.file.size,
mimeType: req.file.mimetype,
purpose,
status: 'processed',
vectorStoreId: vectorStore.id,
assistantId: assistantId || null,
uploadedByUserId: null, // You might want to track who uploaded it
metadata: {
openaiBytes: openaiFile.bytes,
openaiCreatedAt: openaiFile.created_at
}
});
// Update vector store file count
await vectorStore.increment('fileCount');
await vectorStore.increment('bytesUsed', { by: req.file.size });
await vectorStore.update({ lastActiveAt: new Date() });
console.log(`Document stored: ${document.id}`);
res.json({
document: {
id: document.id,
name: document.originalName,
fileSize: document.fileSize,
mimeType: document.mimeType,
status: document.status,
openaiFileId: document.openaiFileId,
createdAt: document.createdAt
},
message: 'File uploaded successfully'
});
} catch (error) {
console.error('Error uploading file:', error);
// Clean up uploaded file if there was an error
if (req.file && fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path);
}
next(error);
}
});
// Delete vector store
router.delete('/:vectorStoreId', async (req, res, next) => {
try {
const { vectorStoreId } = req.params;
const vectorStore = await VectorStore.findByPk(vectorStoreId);
if (!vectorStore) {
return res.status(404).json({
error: 'Not Found',
message: 'Vector store not found'
});
}
console.log(`Deleting vector store: ${vectorStore.name}`);
// Delete from OpenAI
try {
await openai.beta.vectorStores.del(vectorStore.openaiVectorStoreId);
} catch (openaiError) {
console.warn(`Failed to delete OpenAI vector store: ${openaiError.message}`);
}
// Delete associated documents and clean up files
const documents = await Document.findAll({
where: { vectorStoreId: vectorStore.id }
});
for (const doc of documents) {
// Clean up local file
if (doc.filePath && fs.existsSync(doc.filePath)) {
fs.unlinkSync(doc.filePath);
}
await doc.destroy();
}
await vectorStore.destroy();
console.log(`Vector store deleted: ${vectorStoreId}`);
res.json({
message: 'Vector store deleted successfully'
});
} catch (error) {
console.error('Error deleting vector store:', error);
next(error);
}
});
// Get vector store documents
router.get('/:vectorStoreId/documents', async (req, res, next) => {
try {
const { vectorStoreId } = req.params;
const documents = await Document.findAll({
where: { vectorStoreId },
order: [['createdAt', 'DESC']]
});
res.json({
documents: documents.map(doc => ({
id: doc.id,
name: doc.originalName,
fileSize: doc.fileSize,
mimeType: doc.mimeType,
status: doc.status,
openaiFileId: doc.openaiFileId,
createdAt: doc.createdAt
}))
});
} catch (error) {
console.error('Error getting vector store documents:', error);
next(error);
}
});
// Get OpenAI vector stores (for agent configuration)
router.get('/openai', async (req, res, next) => {
try {
console.log('Fetching vector stores from OpenAI API...');
const { limit = 20, order = 'desc' } = req.query;
const vectorStoresResponse = await responsesService.listVectorStores({
limit: parseInt(limit),
order
});
// Format for frontend consumption
const formattedStores = vectorStoresResponse.data?.map(store => ({
id: store.id,
name: store.name || `Vector Store ${store.id.substring(0, 8)}`,
file_counts: store.file_counts || { total: 0 },
status: store.status,
created_at: store.created_at,
last_active_at: store.last_active_at
})) || [];
console.log(`Retrieved ${formattedStores.length} vector stores from OpenAI`);
res.json({
vectorStores: formattedStores,
has_more: vectorStoresResponse.has_more || false,
total: formattedStores.length
});
} catch (error) {
console.error('Error fetching OpenAI vector stores:', error);
// Return empty result on error rather than failing completely
res.json({
vectorStores: [],
has_more: false,
total: 0,
error: 'Failed to fetch vector stores from OpenAI'
});
}
});
module.exports = router;