- 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>
169 lines
No EOL
4.8 KiB
JavaScript
169 lines
No EOL
4.8 KiB
JavaScript
const express = require('express');
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const OpenAI = require('openai');
|
|
|
|
const router = express.Router();
|
|
|
|
const openai = new OpenAI({
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
organization: process.env.OPENAI_ORG_ID,
|
|
});
|
|
|
|
// Configure multer for temporary file uploads
|
|
const tempStorage = multer.diskStorage({
|
|
destination: function (req, file, cb) {
|
|
const uploadDir = path.join(__dirname, '../temp-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 tempUpload = multer({
|
|
storage: tempStorage,
|
|
limits: {
|
|
fileSize: 50 * 1024 * 1024, // 50MB limit for temporary files
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
// Allow common document and image types for chat
|
|
const allowedTypes = [
|
|
'application/pdf',
|
|
'text/plain',
|
|
'text/markdown',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'text/csv',
|
|
'application/json',
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp'
|
|
];
|
|
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Unsupported file type for temporary upload.'));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Upload file for temporary chat use
|
|
router.post('/temp-upload', tempUpload.single('file'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({
|
|
error: 'Validation Error',
|
|
message: 'No file uploaded'
|
|
});
|
|
}
|
|
|
|
console.log(`Temporary file uploaded: ${req.file.originalname}`);
|
|
|
|
// For images, we can use them directly with vision models
|
|
if (req.file.mimetype.startsWith('image/')) {
|
|
// Convert to base64 for vision API
|
|
const imageBuffer = fs.readFileSync(req.file.path);
|
|
const base64Image = imageBuffer.toString('base64');
|
|
|
|
res.json({
|
|
id: req.file.filename,
|
|
name: req.file.originalname,
|
|
type: 'image',
|
|
mimeType: req.file.mimetype,
|
|
size: req.file.size,
|
|
data: `data:${req.file.mimetype};base64,${base64Image}`,
|
|
message: 'Image uploaded for chat analysis'
|
|
});
|
|
} else {
|
|
// For documents, upload to OpenAI for processing
|
|
try {
|
|
const openaiFile = await openai.files.create({
|
|
file: fs.createReadStream(req.file.path),
|
|
purpose: 'assistants'
|
|
});
|
|
|
|
console.log(`Document uploaded to OpenAI: ${openaiFile.id}`);
|
|
|
|
res.json({
|
|
id: req.file.filename,
|
|
name: req.file.originalname,
|
|
type: 'document',
|
|
mimeType: req.file.mimetype,
|
|
size: req.file.size,
|
|
openaiFileId: openaiFile.id,
|
|
message: 'Document uploaded for chat analysis'
|
|
});
|
|
} catch (openaiError) {
|
|
console.error('Error uploading to OpenAI:', openaiError);
|
|
res.json({
|
|
id: req.file.filename,
|
|
name: req.file.originalname,
|
|
type: 'document',
|
|
mimeType: req.file.mimetype,
|
|
size: req.file.size,
|
|
error: 'Failed to process with OpenAI, but file uploaded locally',
|
|
message: 'Document uploaded locally (OpenAI processing failed)'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Schedule cleanup after 1 hour
|
|
setTimeout(() => {
|
|
if (fs.existsSync(req.file.path)) {
|
|
fs.unlinkSync(req.file.path);
|
|
console.log(`Cleaned up temporary file: ${req.file.filename}`);
|
|
}
|
|
}, 60 * 60 * 1000); // 1 hour
|
|
|
|
} catch (error) {
|
|
console.error('Error with temporary file upload:', 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);
|
|
}
|
|
});
|
|
|
|
// Get temporary file info (for chat reference)
|
|
router.get('/temp/:fileId', async (req, res, next) => {
|
|
try {
|
|
const { fileId } = req.params;
|
|
const filePath = path.join(__dirname, '../temp-uploads', fileId);
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Temporary file not found or expired'
|
|
});
|
|
}
|
|
|
|
const stats = fs.statSync(filePath);
|
|
const ext = path.extname(fileId);
|
|
|
|
res.json({
|
|
id: fileId,
|
|
exists: true,
|
|
size: stats.size,
|
|
uploadedAt: stats.birthtime,
|
|
extension: ext
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error getting temporary file info:', error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
module.exports = router; |