first commit
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
const path = require('path');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
const appConfig = {
|
||||
// Server Configuration
|
||||
server: {
|
||||
port: parseInt(process.env.PORT) || 8000,
|
||||
host: process.env.HOST || 'localhost',
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||
},
|
||||
|
||||
// Database Configuration
|
||||
database: {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT) || 5432,
|
||||
name: process.env.DB_NAME || 'reason_flow',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === 'true'
|
||||
},
|
||||
|
||||
// API Configuration
|
||||
apis: {
|
||||
groq: {
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
model: process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct-0905',
|
||||
baseUrl: process.env.GROQ_BASE_URL || 'https://api.groq.com'
|
||||
},
|
||||
openai: {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
},
|
||||
serp: {
|
||||
apiKey: process.env.SERP_API_KEY,
|
||||
engine: process.env.SERP_ENGINE || 'google'
|
||||
}
|
||||
},
|
||||
|
||||
// Authentication Configuration
|
||||
auth: {
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
adminEmail: process.env.ADMIN_EMAIL || 'admin@reasonflow.com',
|
||||
adminPassword: process.env.ADMIN_PASSWORD || 'admin123',
|
||||
adminFirstName: process.env.ADMIN_FIRST_NAME || 'Admin',
|
||||
adminLastName: process.env.ADMIN_LAST_NAME || 'User'
|
||||
},
|
||||
|
||||
// File Upload Configuration
|
||||
upload: {
|
||||
maxFileSize: process.env.MAX_FILE_SIZE || '50MB',
|
||||
uploadPath: process.env.UPLOAD_PATH || './uploads',
|
||||
allowedFileTypes: (process.env.ALLOWED_FILE_TYPES || 'pdf,txt,doc,docx').split(',')
|
||||
},
|
||||
|
||||
// Rate Limiting Configuration
|
||||
rateLimit: {
|
||||
enabled: process.env.RATE_LIMIT_ENABLED !== 'false',
|
||||
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 900000
|
||||
},
|
||||
|
||||
// Logging Configuration
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
file: process.env.LOG_FILE || './logs/app.log',
|
||||
maxSize: process.env.LOG_MAX_SIZE || '10MB',
|
||||
maxFiles: parseInt(process.env.LOG_MAX_FILES) || 5
|
||||
},
|
||||
|
||||
// Model Configuration
|
||||
models: {
|
||||
model1: {
|
||||
temperature: parseFloat(process.env.MODEL1_TEMPERATURE) || 0.3,
|
||||
maxTokens: parseInt(process.env.MODEL1_MAX_TOKENS) || 3000
|
||||
},
|
||||
queryModel: {
|
||||
temperature: parseFloat(process.env.QUERYMODEL_TEMPERATURE) || 0.5,
|
||||
maxTokens: parseInt(process.env.QUERYMODEL_MAX_TOKENS) || 4000
|
||||
}
|
||||
},
|
||||
|
||||
// Fine-tuning Configuration
|
||||
fineTuning: {
|
||||
enabled: process.env.FINE_TUNING_ENABLED === 'true',
|
||||
schedule: process.env.FINE_TUNING_SCHEDULE || 'weekly',
|
||||
batchSize: parseInt(process.env.FINE_TUNING_BATCH_SIZE) || 10
|
||||
},
|
||||
|
||||
// Feedback Configuration
|
||||
feedback: {
|
||||
processingEnabled: process.env.FEEDBACK_PROCESSING_ENABLED === 'true',
|
||||
batchSize: parseInt(process.env.FEEDBACK_BATCH_SIZE) || 50,
|
||||
schedule: process.env.FEEDBACK_PROCESSING_SCHEDULE || 'daily'
|
||||
},
|
||||
|
||||
// Security Configuration
|
||||
security: {
|
||||
helmetEnabled: process.env.HELMET_ENABLED !== 'false',
|
||||
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||
},
|
||||
|
||||
// Monitoring Configuration
|
||||
monitoring: {
|
||||
healthCheckEnabled: process.env.HEALTH_CHECK_ENABLED !== 'false',
|
||||
metricsEnabled: process.env.METRICS_ENABLED === 'true',
|
||||
performanceMonitoring: process.env.PERFORMANCE_MONITORING === 'true'
|
||||
},
|
||||
|
||||
// Development Configuration
|
||||
development: {
|
||||
debugMode: process.env.DEBUG_MODE === 'true',
|
||||
verboseLogging: process.env.VERBOSE_LOGGING === 'true',
|
||||
hotReload: process.env.HOT_RELOAD === 'true'
|
||||
},
|
||||
|
||||
// Redis Configuration
|
||||
redis: {
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
password: process.env.REDIS_PASSWORD
|
||||
},
|
||||
|
||||
// Vector Database Configuration
|
||||
vectorDb: {
|
||||
url: process.env.VECTOR_DB_URL,
|
||||
apiKey: process.env.VECTOR_DB_API_KEY
|
||||
}
|
||||
};
|
||||
|
||||
// Validation function
|
||||
const validateConfig = () => {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// Required fields validation
|
||||
if (!appConfig.apis.groq.apiKey) {
|
||||
errors.push('GROQ_API_KEY is required');
|
||||
}
|
||||
|
||||
if (!appConfig.auth.jwtSecret) {
|
||||
errors.push('JWT_SECRET is required');
|
||||
}
|
||||
|
||||
if (!appConfig.database.password) {
|
||||
errors.push('DB_PASSWORD is required');
|
||||
}
|
||||
|
||||
// Production-specific validations
|
||||
if (appConfig.server.env === 'production') {
|
||||
if (appConfig.auth.jwtSecret === 'dev_secret_key_change_in_production') {
|
||||
errors.push('JWT_SECRET must be changed for production');
|
||||
}
|
||||
|
||||
if (!appConfig.apis.openai.apiKey) {
|
||||
warnings.push('OPENAI_API_KEY is recommended for production');
|
||||
}
|
||||
|
||||
if (appConfig.database.ssl === false) {
|
||||
warnings.push('Database SSL is recommended for production');
|
||||
}
|
||||
}
|
||||
|
||||
// Development warnings
|
||||
if (appConfig.server.env === 'development') {
|
||||
if (appConfig.auth.jwtSecret === 'dev_secret_key_change_in_production') {
|
||||
warnings.push('Using default JWT secret for development');
|
||||
}
|
||||
}
|
||||
|
||||
// Log warnings
|
||||
warnings.forEach(warning => {
|
||||
logger.warn(`Configuration warning: ${warning}`);
|
||||
});
|
||||
|
||||
// Throw errors
|
||||
if (errors.length > 0) {
|
||||
const errorMessage = `Configuration errors: ${errors.join(', ')}`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
logger.info('Configuration validated successfully');
|
||||
return true;
|
||||
};
|
||||
|
||||
// Get configuration for specific environment
|
||||
const getConfigForEnv = (env) => {
|
||||
const envConfig = { ...appConfig };
|
||||
|
||||
switch (env) {
|
||||
case 'production':
|
||||
envConfig.server.port = parseInt(process.env.PORT) || 8000;
|
||||
envConfig.server.host = '0.0.0.0';
|
||||
envConfig.development.debugMode = false;
|
||||
envConfig.development.verboseLogging = false;
|
||||
envConfig.logging.level = 'info';
|
||||
break;
|
||||
|
||||
case 'test':
|
||||
envConfig.server.port = parseInt(process.env.PORT) || 8001;
|
||||
envConfig.database.name = process.env.DB_NAME || 'reason_flow_test';
|
||||
envConfig.development.debugMode = false;
|
||||
envConfig.logging.level = 'error';
|
||||
break;
|
||||
|
||||
case 'development':
|
||||
default:
|
||||
envConfig.development.debugMode = true;
|
||||
envConfig.logging.level = 'debug';
|
||||
break;
|
||||
}
|
||||
|
||||
return envConfig;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
appConfig,
|
||||
validateConfig,
|
||||
getConfigForEnv
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
const logger = require('../utils/logger');
|
||||
const { getConfig } = require('./databaseConfig');
|
||||
|
||||
const sequelize = new Sequelize(getConfig());
|
||||
|
||||
// Test database connection
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
logger.info('Database connection established successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Unable to connect to the database:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Sync database (create tables)
|
||||
const syncDatabase = async (force = false) => {
|
||||
try {
|
||||
await sequelize.sync({ force, alter: !force });
|
||||
logger.info(`Database synchronized successfully (force: ${force})`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Database synchronization failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Drop all tables
|
||||
const dropDatabase = async () => {
|
||||
try {
|
||||
await sequelize.drop();
|
||||
logger.info('Database dropped successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Database drop failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Close database connection
|
||||
const closeConnection = async () => {
|
||||
try {
|
||||
await sequelize.close();
|
||||
logger.info('Database connection closed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error closing database connection:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
testConnection,
|
||||
syncDatabase,
|
||||
dropDatabase,
|
||||
closeConnection
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const databaseConfig = {
|
||||
development: {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'reason_flow_dev',
|
||||
username: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
dialect: 'postgres',
|
||||
logging: (msg) => logger.debug(msg),
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
},
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
freezeTableName: true
|
||||
}
|
||||
},
|
||||
|
||||
test: {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'reason_flow_test',
|
||||
username: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
dialect: 'postgres',
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
},
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
freezeTableName: true
|
||||
}
|
||||
},
|
||||
|
||||
production: {
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME,
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
dialect: 'postgres',
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 20,
|
||||
min: 5,
|
||||
acquire: 60000,
|
||||
idle: 10000
|
||||
},
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
freezeTableName: true
|
||||
},
|
||||
dialectOptions: {
|
||||
ssl: process.env.DB_SSL === 'true' ? {
|
||||
require: true,
|
||||
rejectUnauthorized: false
|
||||
} : false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getConfig = () => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const config = databaseConfig[env];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Database configuration not found for environment: ${env}`);
|
||||
}
|
||||
|
||||
// Validate required fields for production
|
||||
if (env === 'production') {
|
||||
const requiredFields = ['host', 'database', 'username', 'password'];
|
||||
const missingFields = requiredFields.filter(field => !config[field]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
throw new Error(`Missing required database configuration: ${missingFields.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Using database configuration for environment: ${env}`);
|
||||
return config;
|
||||
};
|
||||
|
||||
const validateConnection = async (sequelize) => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
logger.info('Database connection validated successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Database connection validation failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseUrl = () => {
|
||||
const config = getConfig();
|
||||
const { host, port, database, username, password } = config;
|
||||
|
||||
return `postgresql://${username}:${password}@${host}:${port}/${database}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
databaseConfig,
|
||||
getConfig,
|
||||
validateConnection,
|
||||
getDatabaseUrl
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const groqConfig = {
|
||||
// API Configuration
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
model: process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct-0905',
|
||||
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com',
|
||||
|
||||
// Model-specific configurations
|
||||
models: {
|
||||
MODEL1: {
|
||||
model: 'moonshotai/kimi-k2-instruct-0905',
|
||||
temperature: 0.3,
|
||||
maxTokens: 300,
|
||||
topP: 0.9,
|
||||
systemPrompt: `You are MODEL1, an expert engineering reasoning system. Your primary function is to analyze complex engineering problems and create detailed, step-by-step plans to solve them.`
|
||||
},
|
||||
QUERYMODEL: {
|
||||
model: 'moonshotai/kimi-k2-instruct-0905',
|
||||
temperature: 0.5,
|
||||
maxTokens: 400,
|
||||
topP: 0.9,
|
||||
systemPrompt: `You are QUERYMODEL, an expert engineering execution system. Your primary function is to execute engineering plans using various tools and resources.`
|
||||
}
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
requestsPerMinute: 60,
|
||||
requestsPerHour: 1000,
|
||||
burstLimit: 10
|
||||
},
|
||||
|
||||
// Retry configuration
|
||||
retry: {
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
backoffMultiplier: 2
|
||||
},
|
||||
|
||||
// Timeout configuration
|
||||
timeout: {
|
||||
request: 30000, // 30 seconds
|
||||
connection: 10000 // 10 seconds
|
||||
},
|
||||
|
||||
// Logging configuration
|
||||
logging: {
|
||||
logRequests: process.env.NODE_ENV === 'development',
|
||||
logResponses: process.env.NODE_ENV === 'development',
|
||||
logErrors: true
|
||||
}
|
||||
};
|
||||
|
||||
const validateConfig = () => {
|
||||
const errors = [];
|
||||
|
||||
if (!groqConfig.apiKey) {
|
||||
errors.push('GROQ_API_KEY is required');
|
||||
}
|
||||
|
||||
if (!groqConfig.model) {
|
||||
errors.push('GROQ_MODEL is required');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
logger.error('Groq configuration errors:', errors);
|
||||
throw new Error(`Groq configuration invalid: ${errors.join(', ')}`);
|
||||
}
|
||||
|
||||
logger.info('Groq configuration validated successfully');
|
||||
return true;
|
||||
};
|
||||
|
||||
const getModelConfig = (modelType) => {
|
||||
return groqConfig.models[modelType] || groqConfig.models.MODEL1;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
groqConfig,
|
||||
validateConfig,
|
||||
getModelConfig
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign({ userId }, process.env.JWT_SECRET, {
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
|
||||
});
|
||||
};
|
||||
|
||||
const register = async (req, res) => {
|
||||
try {
|
||||
const { email, password, firstName, lastName, role = 'user' } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'All fields are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.findOne({ where: { email } });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'User already exists with this email'
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Create user
|
||||
const user = await User.create({
|
||||
email,
|
||||
password_hash: passwordHash,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
role
|
||||
});
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
logger.info(`New user registered: ${email}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
role: user.role
|
||||
},
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Registration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Email and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Find user
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if (!user.is_active) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Account is deactivated'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await user.update({ last_login: new Date() });
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
logger.info(`User logged in: ${email}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
role: user.role
|
||||
},
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getProfile = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.userId, {
|
||||
attributes: { exclude: ['password_hash'] }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'User not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get profile error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateProfile = async (req, res) => {
|
||||
try {
|
||||
const { firstName, lastName, preferences } = req.body;
|
||||
const user = await User.findByPk(req.user.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'User not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Update user
|
||||
const updateData = {};
|
||||
if (firstName) updateData.first_name = firstName;
|
||||
if (lastName) updateData.last_name = lastName;
|
||||
if (preferences) updateData.preferences = { ...user.preferences, ...preferences };
|
||||
|
||||
await user.update(updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
role: user.role,
|
||||
preferences: user.preferences
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Update profile error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
getProfile,
|
||||
updateProfile
|
||||
};
|
||||
@@ -0,0 +1,356 @@
|
||||
const { Conversation, Message, Plan, User } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
const model1Service = require('../services/model1Service');
|
||||
const chatRouter = require('../services/chatRouter');
|
||||
|
||||
const createConversation = async (req, res) => {
|
||||
try {
|
||||
const { title } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const conversation = await Conversation.create({
|
||||
user_id: userId,
|
||||
title: title || 'New Conversation',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
logger.info(`New conversation created: ${conversation.id}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { conversation }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Create conversation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getConversations = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { page = 1, limit = 10, status } = req.query;
|
||||
|
||||
const whereClause = { user_id: userId };
|
||||
if (status) whereClause.status = status;
|
||||
|
||||
const conversations = await Conversation.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: Message,
|
||||
limit: 1,
|
||||
order: [['created_at', 'DESC']]
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
conversations: conversations.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: conversations.count,
|
||||
pages: Math.ceil(conversations.count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get conversations error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getConversation = async (req, res) => {
|
||||
try {
|
||||
const { conversationId } = req.params;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const conversation = await Conversation.findOne({
|
||||
where: {
|
||||
id: conversationId,
|
||||
user_id: userId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Message,
|
||||
order: [['created_at', 'ASC']]
|
||||
},
|
||||
{
|
||||
model: Plan,
|
||||
order: [['created_at', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Conversation not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { conversation }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get conversation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (req, res) => {
|
||||
try {
|
||||
const { conversationId, content, messageType = 'text' } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
// Verify conversation belongs to user
|
||||
const conversation = await Conversation.findOne({
|
||||
where: {
|
||||
id: conversationId,
|
||||
user_id: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Conversation not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Create user message
|
||||
const userMessage = await Message.create({
|
||||
conversation_id: conversationId,
|
||||
role: 'user',
|
||||
content,
|
||||
message_type: messageType
|
||||
});
|
||||
|
||||
// Get conversation history for context
|
||||
const conversationHistory = await Message.findAll({
|
||||
where: { conversation_id: conversationId },
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: 20 // Last 20 messages for context
|
||||
});
|
||||
|
||||
// Create properly ordered conversation history for context
|
||||
const orderedHistory = conversationHistory.reverse().map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
message_type: msg.message_type,
|
||||
timestamp: msg.created_at
|
||||
}));
|
||||
|
||||
// Route the message to determine response type
|
||||
const routingDecision = await chatRouter.routeMessage(content, {
|
||||
conversationId,
|
||||
userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
conversationHistory: orderedHistory
|
||||
});
|
||||
|
||||
let assistantMessage;
|
||||
|
||||
if (routingDecision.responseType === 'plan_needed' && routingDecision.isEngineeringQuestion) {
|
||||
// Generate engineering plan using MODEL1
|
||||
try {
|
||||
const planData = await model1Service.generatePlan(content, {
|
||||
conversationId,
|
||||
userId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Save the plan to database
|
||||
const plan = await Plan.create({
|
||||
conversation_id: conversationId,
|
||||
title: planData.title,
|
||||
description: planData.description,
|
||||
steps: planData.steps,
|
||||
status: 'draft',
|
||||
tools_required: planData.toolsRequired || [],
|
||||
estimated_duration: planData.estimatedDuration,
|
||||
complexity_score: planData.complexityScore,
|
||||
metadata: {
|
||||
safetyConsiderations: planData.safetyConsiderations,
|
||||
qualityChecks: planData.qualityChecks,
|
||||
processingTime: planData.processingTime,
|
||||
tokensUsed: planData.tokensUsed,
|
||||
model: planData.model
|
||||
}
|
||||
});
|
||||
|
||||
// Create assistant message with plan
|
||||
assistantMessage = await Message.create({
|
||||
conversation_id: conversationId,
|
||||
plan_id: plan.id,
|
||||
role: 'assistant',
|
||||
content: planData.rawContent || `# ${planData.title}\n\n${planData.description}\n\n## Steps:\n${planData.steps.map((step, index) => `${index + 1}. ${step}`).join('\n')}`,
|
||||
message_type: 'plan',
|
||||
metadata: {
|
||||
modelType: 'MODEL1',
|
||||
processingTime: planData.processingTime,
|
||||
tokensUsed: planData.tokensUsed,
|
||||
routingDecision: routingDecision.reasoning
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`MODEL1 plan generated: ${plan.id} for conversation: ${conversationId}`);
|
||||
} catch (error) {
|
||||
logger.error('MODEL1 plan generation failed:', error);
|
||||
|
||||
// Fallback to simple response if plan generation fails
|
||||
const fallbackResponse = await chatRouter.generateSimpleResponse(content, {
|
||||
conversationHistory: orderedHistory
|
||||
});
|
||||
assistantMessage = await Message.create({
|
||||
conversation_id: conversationId,
|
||||
role: 'assistant',
|
||||
content: fallbackResponse,
|
||||
message_type: 'text',
|
||||
metadata: {
|
||||
error: error.message,
|
||||
modelType: 'FALLBACK',
|
||||
routingDecision: routingDecision.reasoning
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Generate simple conversational response
|
||||
const responseContent = routingDecision.response || await chatRouter.generateSimpleResponse(content, {
|
||||
conversationHistory: orderedHistory
|
||||
});
|
||||
|
||||
assistantMessage = await Message.create({
|
||||
conversation_id: conversationId,
|
||||
role: 'assistant',
|
||||
content: responseContent,
|
||||
message_type: 'text',
|
||||
metadata: {
|
||||
modelType: 'REASONAI',
|
||||
routingDecision: routingDecision.reasoning,
|
||||
responseType: routingDecision.responseType
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Simple response generated for conversation: ${conversationId}`, {
|
||||
responseType: routingDecision.responseType,
|
||||
reasoning: routingDecision.reasoning
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Message sent in conversation: ${conversationId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { userMessage, assistantMessage }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Send message error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateConversation = async (req, res) => {
|
||||
try {
|
||||
const { conversationId } = req.params;
|
||||
const { title, status } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const conversation = await Conversation.findOne({
|
||||
where: {
|
||||
id: conversationId,
|
||||
user_id: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Conversation not found'
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (title) updateData.title = title;
|
||||
if (status) updateData.status = status;
|
||||
|
||||
await conversation.update(updateData);
|
||||
|
||||
logger.info(`Conversation updated: ${conversationId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { conversation }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Update conversation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConversation = async (req, res) => {
|
||||
try {
|
||||
const { conversationId } = req.params;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const conversation = await Conversation.findOne({
|
||||
where: {
|
||||
id: conversationId,
|
||||
user_id: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Conversation not found'
|
||||
});
|
||||
}
|
||||
|
||||
await conversation.destroy();
|
||||
|
||||
logger.info(`Conversation deleted: ${conversationId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Conversation deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Delete conversation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createConversation,
|
||||
getConversations,
|
||||
getConversation,
|
||||
sendMessage,
|
||||
updateConversation,
|
||||
deleteConversation
|
||||
};
|
||||
@@ -0,0 +1,299 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const pdf = require('pdf-parse');
|
||||
const { Document, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const logger = require('../utils/logger');
|
||||
const embeddingService = require('../services/embeddingService');
|
||||
const graphRagService = require('../services/graphRagService');
|
||||
|
||||
// Configure multer for file uploads
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const uploadPath = path.join(__dirname, '../../uploads');
|
||||
if (!fs.existsSync(uploadPath)) {
|
||||
fs.mkdirSync(uploadPath, { recursive: true });
|
||||
}
|
||||
cb(null, uploadPath);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB for testing
|
||||
fieldSize: 10 * 1024 * 1024, // 10MB for field values
|
||||
fieldNameSize: 100, // 100 bytes for field names
|
||||
files: 1 // Only 1 file at a time
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = ['.pdf', '.txt', '.doc', '.docx'];
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
|
||||
if (allowedTypes.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only PDF, TXT, DOC, DOCX files are allowed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const uploadDocument = async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No file uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const { category, tags } = req.body;
|
||||
let extractedText = '';
|
||||
|
||||
// Extract text from PDF
|
||||
if (req.file.mimetype === 'application/pdf') {
|
||||
try {
|
||||
const dataBuffer = fs.readFileSync(req.file.path);
|
||||
const pdfData = await pdf(dataBuffer);
|
||||
extractedText = pdfData.text;
|
||||
} catch (error) {
|
||||
logger.error('PDF extraction error:', error);
|
||||
extractedText = 'Error extracting text from PDF';
|
||||
}
|
||||
} else if (req.file.mimetype === 'text/plain') {
|
||||
extractedText = fs.readFileSync(req.file.path, 'utf8');
|
||||
}
|
||||
|
||||
// Create document record
|
||||
const document = await Document.create({
|
||||
filename: req.file.filename,
|
||||
original_filename: req.file.originalname,
|
||||
file_path: req.file.path,
|
||||
file_type: req.file.mimetype,
|
||||
file_size: req.file.size,
|
||||
content: extractedText,
|
||||
extracted_text: extractedText,
|
||||
category: category || 'general',
|
||||
tags: tags ? tags.split(',').map(tag => tag.trim()) : [],
|
||||
indexing_status: 'processing'
|
||||
});
|
||||
|
||||
// Generate and store embeddings (if text available)
|
||||
if (extractedText && extractedText.trim().length > 0) {
|
||||
try {
|
||||
const embedding = await embeddingService.embedText(extractedText.slice(0, 15000));
|
||||
await document.update({ embeddings: embedding, is_indexed: true, indexing_status: 'completed' });
|
||||
} catch (e) {
|
||||
logger.error('Embedding generation failed:', e);
|
||||
await document.update({ is_indexed: false, indexing_status: 'failed' });
|
||||
}
|
||||
} else {
|
||||
await document.update({ is_indexed: false, indexing_status: 'failed' });
|
||||
}
|
||||
|
||||
logger.info(`Document uploaded: ${document.id}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { document }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Upload document error:', error);
|
||||
|
||||
// Clean up uploaded file if document creation failed
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDocuments = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, category, search, isIndexed } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (category) whereClause.category = category;
|
||||
if (isIndexed !== undefined) whereClause.is_indexed = isIndexed === 'true';
|
||||
if (search) {
|
||||
whereClause[Op.or] = [
|
||||
{ original_filename: { [Op.iLike]: `%${search}%` } },
|
||||
{ extracted_text: { [Op.iLike]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const documents = await Document.findAndCountAll({
|
||||
where: whereClause,
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
documents: documents.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: documents.count,
|
||||
pages: Math.ceil(documents.count / parseInt(limit))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get documents error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDocument = async (req, res) => {
|
||||
try {
|
||||
const { documentId } = req.params;
|
||||
|
||||
const document = await Document.findByPk(documentId);
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Document not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { document }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get document error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const searchDocuments = async (req, res) => {
|
||||
try {
|
||||
const { query, category, limit = 10 } = req.query;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Search query is required'
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause = {
|
||||
is_indexed: true,
|
||||
...(category ? { category } : {})
|
||||
};
|
||||
|
||||
// Embed query and compute cosine similarity in JS for now
|
||||
const queryEmbedding = await embeddingService.embedText(query);
|
||||
|
||||
const candidates = await Document.findAll({
|
||||
where: whereClause,
|
||||
attributes: ['id', 'original_filename', 'extracted_text', 'embeddings', 'category', 'created_at']
|
||||
});
|
||||
|
||||
const scored = [];
|
||||
for (const doc of candidates) {
|
||||
const emb = doc.embeddings || [];
|
||||
const score = embeddingService.cosineSimilarity(queryEmbedding, emb);
|
||||
scored.push({
|
||||
id: doc.id,
|
||||
original_filename: doc.original_filename,
|
||||
snippet: (doc.extracted_text || '').slice(0, 300),
|
||||
category: doc.category,
|
||||
created_at: doc.created_at,
|
||||
score
|
||||
});
|
||||
}
|
||||
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
const top = scored.slice(0, parseInt(limit));
|
||||
|
||||
res.json({ success: true, data: { results: top } });
|
||||
} catch (error) {
|
||||
logger.error('Search documents error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const graphSearchDocuments = async (req, res) => {
|
||||
try {
|
||||
const { query, category } = req.query;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ success: false, error: 'Search query is required' });
|
||||
}
|
||||
|
||||
const result = await graphRagService.graphSearch({ query, category });
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Graph search error:', error);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDocument = async (req, res) => {
|
||||
try {
|
||||
const { documentId } = req.params;
|
||||
|
||||
const document = await Document.findByPk(documentId);
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Document not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Delete physical file
|
||||
if (fs.existsSync(document.file_path)) {
|
||||
fs.unlinkSync(document.file_path);
|
||||
}
|
||||
|
||||
// Delete database record
|
||||
await document.destroy();
|
||||
|
||||
logger.info(`Document deleted: ${documentId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Document deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Delete document error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uploadDocument,
|
||||
getDocuments,
|
||||
getDocument,
|
||||
searchDocuments,
|
||||
graphSearchDocuments,
|
||||
deleteDocument,
|
||||
upload // Export multer middleware
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
const { Feedback, Message, User, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const submitFeedback = async (req, res) => {
|
||||
try {
|
||||
const { messageId, feedbackType, rating, comment, correctedContent } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
// Validate feedback type
|
||||
const validTypes = ['positive', 'negative', 'correction', 'suggestion'];
|
||||
if (!validTypes.includes(feedbackType)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid feedback type'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate rating if provided
|
||||
if (rating && (rating < 1 || rating > 5)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Rating must be between 1 and 5'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify message exists if messageId provided
|
||||
if (messageId) {
|
||||
const message = await Message.findByPk(messageId);
|
||||
if (!message) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Message not found'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const feedback = await Feedback.create({
|
||||
user_id: userId,
|
||||
message_id: messageId,
|
||||
feedback_type: feedbackType,
|
||||
rating,
|
||||
comment,
|
||||
corrected_content: correctedContent,
|
||||
is_processed: false
|
||||
});
|
||||
|
||||
logger.info(`Feedback submitted: ${feedback.id} by user ${userId}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { feedback }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Submit feedback error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getFeedback = async (req, res) => {
|
||||
try {
|
||||
const { feedbackId } = req.params;
|
||||
|
||||
const feedback = await Feedback.findByPk(feedbackId, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
attributes: ['id', 'email', 'first_name', 'last_name']
|
||||
},
|
||||
{
|
||||
model: Message,
|
||||
attributes: ['id', 'content', 'role']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Feedback not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { feedback }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get feedback error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getFeedbackList = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, feedbackType, isProcessed, userId } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (feedbackType) whereClause.feedback_type = feedbackType;
|
||||
if (isProcessed !== undefined) whereClause.is_processed = isProcessed === 'true';
|
||||
if (userId) whereClause.user_id = userId;
|
||||
|
||||
const feedback = await Feedback.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
attributes: ['id', 'email', 'first_name', 'last_name']
|
||||
},
|
||||
{
|
||||
model: Message,
|
||||
attributes: ['id', 'content', 'role']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
feedback: feedback.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: feedback.count,
|
||||
pages: Math.ceil(feedback.count / parseInt(limit))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get feedback list error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processFeedback = async (req, res) => {
|
||||
try {
|
||||
const { feedbackId } = req.params;
|
||||
const { processingNotes } = req.body;
|
||||
|
||||
const feedback = await Feedback.findByPk(feedbackId);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Feedback not found'
|
||||
});
|
||||
}
|
||||
|
||||
await feedback.update({
|
||||
is_processed: true,
|
||||
processing_notes: processingNotes
|
||||
});
|
||||
|
||||
logger.info(`Feedback processed: ${feedbackId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { feedback }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Process feedback error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getFeedbackStats = async (req, res) => {
|
||||
try {
|
||||
const stats = await Feedback.findAll({
|
||||
attributes: [
|
||||
'feedback_type',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count'],
|
||||
[sequelize.fn('AVG', sequelize.col('rating')), 'avg_rating']
|
||||
],
|
||||
group: ['feedback_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
const totalFeedback = await Feedback.count();
|
||||
const processedFeedback = await Feedback.count({ where: { is_processed: true } });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
total: totalFeedback,
|
||||
processed: processedFeedback,
|
||||
unprocessed: totalFeedback - processedFeedback
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get feedback stats error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
submitFeedback,
|
||||
getFeedback,
|
||||
getFeedbackList,
|
||||
processFeedback,
|
||||
getFeedbackStats
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
const model1Service = require('../services/model1Service');
|
||||
const { Plan, Conversation } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const generatePlan = async (req, res) => {
|
||||
try {
|
||||
const { query, conversationId, context = {} } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Query is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify conversation belongs to user
|
||||
if (conversationId) {
|
||||
const conversation = await Conversation.findOne({
|
||||
where: {
|
||||
id: conversationId,
|
||||
user_id: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Conversation not found'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate plan using MODEL1
|
||||
const planData = await model1Service.generatePlan(query, context);
|
||||
|
||||
// Save plan if conversationId provided
|
||||
let savedPlan = null;
|
||||
if (conversationId) {
|
||||
savedPlan = await model1Service.savePlan(planData, conversationId, userId);
|
||||
}
|
||||
|
||||
logger.info(`MODEL1 plan generated for user: ${userId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
plan: savedPlan || planData,
|
||||
processingTime: planData.processingTime,
|
||||
tokensUsed: planData.tokensUsed,
|
||||
model: planData.model
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('MODEL1 plan generation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const validatePlan = async (req, res) => {
|
||||
try {
|
||||
const { planId } = req.params;
|
||||
const { feedback = [] } = req.body;
|
||||
|
||||
const result = await model1Service.validatePlan(planId, feedback);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('MODEL1 plan validation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getPlan = async (req, res) => {
|
||||
try {
|
||||
const { planId } = req.params;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { plan }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get plan error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updatePlan = async (req, res) => {
|
||||
try {
|
||||
const { planId } = req.params;
|
||||
const { title, description, steps, status, approvalFeedback } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (title) updateData.title = title;
|
||||
if (description) updateData.description = description;
|
||||
if (steps) updateData.steps = steps;
|
||||
if (status) updateData.status = status;
|
||||
if (approvalFeedback) updateData.approval_feedback = approvalFeedback;
|
||||
|
||||
await plan.update(updateData);
|
||||
|
||||
logger.info(`Plan updated: ${planId}, status: ${status}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { plan }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Update plan error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getModelStatus = async (req, res) => {
|
||||
try {
|
||||
const status = await model1Service.getModelStatus();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: status
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('MODEL1 status error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generatePlan,
|
||||
validatePlan,
|
||||
getPlan,
|
||||
updatePlan,
|
||||
getModelStatus
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
const { ModelVersion, TrainingData } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const getModelStatus = async (req, res) => {
|
||||
try {
|
||||
const activeModels = await ModelVersion.findAll({
|
||||
where: { is_active: true },
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
const modelStatus = {
|
||||
MODEL1: null,
|
||||
QUERYMODEL: null
|
||||
};
|
||||
|
||||
activeModels.forEach(model => {
|
||||
if (model.model_type === 'MODEL1') {
|
||||
modelStatus.MODEL1 = {
|
||||
id: model.id,
|
||||
version: model.version,
|
||||
deployment_status: model.deployment_status,
|
||||
performance_metrics: model.performance_metrics,
|
||||
last_updated: model.updated_at
|
||||
};
|
||||
} else if (model.model_type === 'QUERYMODEL') {
|
||||
modelStatus.QUERYMODEL = {
|
||||
id: model.id,
|
||||
version: model.version,
|
||||
deployment_status: model.deployment_status,
|
||||
performance_metrics: model.performance_metrics,
|
||||
last_updated: model.updated_at
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { modelStatus }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get model status error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getModelVersions = async (req, res) => {
|
||||
try {
|
||||
const { modelType, page = 1, limit = 10 } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (modelType) whereClause.model_type = modelType;
|
||||
|
||||
const models = await ModelVersion.findAndCountAll({
|
||||
where: whereClause,
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
models: models.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: models.count,
|
||||
pages: Math.ceil(models.count / parseInt(limit))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get model versions error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createModelVersion = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
modelName,
|
||||
modelType,
|
||||
baseModel,
|
||||
fineTuningMethod,
|
||||
hyperparameters = {}
|
||||
} = req.body;
|
||||
|
||||
if (!modelName || !modelType || !baseModel) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Model name, type, and base model are required'
|
||||
});
|
||||
}
|
||||
|
||||
const validModelTypes = ['MODEL1', 'QUERYMODEL'];
|
||||
if (!validModelTypes.includes(modelType)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid model type'
|
||||
});
|
||||
}
|
||||
|
||||
// Deactivate current active model of same type
|
||||
await ModelVersion.update(
|
||||
{ is_active: false },
|
||||
{ where: { model_type: modelType, is_active: true } }
|
||||
);
|
||||
|
||||
const model = await ModelVersion.create({
|
||||
model_name: modelName,
|
||||
version: `v${Date.now()}`,
|
||||
model_type: modelType,
|
||||
base_model: baseModel,
|
||||
fine_tuning_method: fineTuningMethod,
|
||||
hyperparameters,
|
||||
deployment_status: 'training'
|
||||
});
|
||||
|
||||
logger.info(`New model version created: ${model.id}`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { model }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Create model version error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateModelVersion = async (req, res) => {
|
||||
try {
|
||||
const { modelId } = req.params;
|
||||
const {
|
||||
deploymentStatus,
|
||||
performanceMetrics,
|
||||
modelPath,
|
||||
trainingLog
|
||||
} = req.body;
|
||||
|
||||
const model = await ModelVersion.findByPk(modelId);
|
||||
|
||||
if (!model) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Model version not found'
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (deploymentStatus) updateData.deployment_status = deploymentStatus;
|
||||
if (performanceMetrics) updateData.performance_metrics = performanceMetrics;
|
||||
if (modelPath) updateData.model_path = modelPath;
|
||||
if (trainingLog) updateData.training_log = trainingLog;
|
||||
|
||||
await model.update(updateData);
|
||||
|
||||
logger.info(`Model version updated: ${modelId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { model }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Update model version error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const activateModel = async (req, res) => {
|
||||
try {
|
||||
const { modelId } = req.params;
|
||||
|
||||
const model = await ModelVersion.findByPk(modelId);
|
||||
|
||||
if (!model) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Model version not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Deactivate other models of same type
|
||||
await ModelVersion.update(
|
||||
{ is_active: false },
|
||||
{ where: { model_type: model.model_type, is_active: true } }
|
||||
);
|
||||
|
||||
// Activate this model
|
||||
await model.update({
|
||||
is_active: true,
|
||||
deployment_status: 'deployed'
|
||||
});
|
||||
|
||||
logger.info(`Model activated: ${modelId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { model }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Activate model error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getTrainingData = async (req, res) => {
|
||||
try {
|
||||
const { modelVersionId, dataType, page = 1, limit = 10 } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (modelVersionId) whereClause.model_version_id = modelVersionId;
|
||||
if (dataType) whereClause.data_type = dataType;
|
||||
|
||||
const trainingData = await TrainingData.findAndCountAll({
|
||||
where: whereClause,
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
trainingData: trainingData.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: trainingData.count,
|
||||
pages: Math.ceil(trainingData.count / parseInt(limit))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get training data error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getModelStatus,
|
||||
getModelVersions,
|
||||
createModelVersion,
|
||||
updateModelVersion,
|
||||
activateModel,
|
||||
getTrainingData
|
||||
};
|
||||
@@ -0,0 +1,350 @@
|
||||
const queryModelService = require('../services/queryModelService');
|
||||
const { Plan, ToolExecution, Conversation, Message } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const executePlan = async (req, res) => {
|
||||
try {
|
||||
const { planId, options = {} } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
if (!planId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Plan ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify plan belongs to user
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (plan.status !== 'approved') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Plan must be approved before execution'
|
||||
});
|
||||
}
|
||||
|
||||
// Execute plan using QUERYMODEL
|
||||
const result = await queryModelService.executePlan(planId, options);
|
||||
|
||||
logger.info(`QUERYMODEL execution completed for plan: ${planId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL execution error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const executeTool = async (req, res) => {
|
||||
try {
|
||||
const { planId, toolName, toolType, inputParameters } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
if (!planId || !toolName || !toolType || !inputParameters) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Plan ID, tool name, type, and input parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify plan belongs to user
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Execute tool using QUERYMODEL
|
||||
const result = await queryModelService.executeTool(toolName, toolType, inputParameters, planId);
|
||||
|
||||
logger.info(`Tool executed: ${toolName} for plan: ${planId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { toolExecution: result }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Execute tool error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const orchestratePlan = async (req, res) => {
|
||||
try {
|
||||
const { planId, options = {} } = req.body;
|
||||
const userId = req.user.userId;
|
||||
|
||||
if (!planId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Plan ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify plan belongs to user
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (plan.status !== 'approved') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Plan must be approved before orchestration'
|
||||
});
|
||||
}
|
||||
|
||||
// Get the original user query from the conversation
|
||||
const originalMessage = await Message.findOne({
|
||||
where: {
|
||||
conversation_id: plan.conversation_id,
|
||||
role: 'user'
|
||||
},
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
const originalQuery = originalMessage ? originalMessage.content : plan.title;
|
||||
|
||||
// Execute orchestration using QUERYMODEL
|
||||
const result = await queryModelService.executeOrchestrate({
|
||||
query: originalQuery,
|
||||
category: 'engineering',
|
||||
topK: options.topK || 5,
|
||||
generateReport: true
|
||||
});
|
||||
|
||||
logger.info(`QUERYMODEL orchestration completed for plan: ${planId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { toolExecution: result }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL orchestration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getExecutionStatus = async (req, res) => {
|
||||
try {
|
||||
const { planId } = req.params;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get tool executions for this plan
|
||||
const toolExecutions = await ToolExecution.findAll({
|
||||
where: {
|
||||
plan_id: planId
|
||||
},
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
plan,
|
||||
toolExecutions
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get execution status error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getToolExecutions = async (req, res) => {
|
||||
try {
|
||||
const { planId, toolType, status, page = 1, limit = 10 } = req.query;
|
||||
const userId = req.user.userId;
|
||||
|
||||
const whereClause = {};
|
||||
if (planId) whereClause.plan_id = planId;
|
||||
if (toolType) whereClause.tool_type = toolType;
|
||||
if (status) whereClause.status = status;
|
||||
|
||||
// Verify plan belongs to user if planId provided
|
||||
if (planId) {
|
||||
const plan = await Plan.findOne({
|
||||
where: {
|
||||
id: planId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
where: {
|
||||
user_id: userId
|
||||
},
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows: executions } = await ToolExecution.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: Plan,
|
||||
where: {
|
||||
conversation: {
|
||||
user_id: userId
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Conversation,
|
||||
attributes: ['id', 'title', 'user_id']
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
executions,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: count,
|
||||
pages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get tool executions error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getModelStatus = async (req, res) => {
|
||||
try {
|
||||
const status = await queryModelService.getModelStatus();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: status
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL status error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
executePlan,
|
||||
executeTool,
|
||||
orchestratePlan,
|
||||
getExecutionStatus,
|
||||
getToolExecutions,
|
||||
getModelStatus
|
||||
};
|
||||
@@ -0,0 +1,250 @@
|
||||
const { ToolExecution, Plan, Document, sequelize } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
const queryModelService = require('../services/queryModelService');
|
||||
|
||||
const executeTool = async (req, res) => {
|
||||
try {
|
||||
const { planId, toolName, toolType, inputParameters } = req.body;
|
||||
|
||||
if (!planId || !toolName || !toolType || !inputParameters) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Plan ID, tool name, type, and input parameters are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify plan exists
|
||||
const plan = await Plan.findByPk(planId);
|
||||
if (!plan) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Plan not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Execute tool using QUERYMODEL service
|
||||
const toolExecution = await queryModelService.executeTool(
|
||||
toolName,
|
||||
toolType,
|
||||
inputParameters,
|
||||
planId
|
||||
);
|
||||
|
||||
logger.info(`Tool executed: ${toolName} for plan ${planId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { toolExecution }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Execute tool error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getToolExecutions = async (req, res) => {
|
||||
try {
|
||||
const { planId, toolType, status, page = 1, limit = 10 } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (planId) whereClause.plan_id = planId;
|
||||
if (toolType) whereClause.tool_type = toolType;
|
||||
if (status) whereClause.status = status;
|
||||
|
||||
const toolExecutions = await ToolExecution.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: Plan,
|
||||
attributes: ['id', 'title', 'status']
|
||||
},
|
||||
{
|
||||
model: Document,
|
||||
attributes: ['id', 'filename', 'original_filename']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
toolExecutions: toolExecutions.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: toolExecutions.count,
|
||||
pages: Math.ceil(toolExecutions.count / parseInt(limit))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get tool executions error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getToolExecution = async (req, res) => {
|
||||
try {
|
||||
const { executionId } = req.params;
|
||||
|
||||
const toolExecution = await ToolExecution.findByPk(executionId, {
|
||||
include: [
|
||||
{
|
||||
model: Plan,
|
||||
attributes: ['id', 'title', 'status']
|
||||
},
|
||||
{
|
||||
model: Document,
|
||||
attributes: ['id', 'filename', 'original_filename']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!toolExecution) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Tool execution not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { toolExecution }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get tool execution error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getToolStats = async (req, res) => {
|
||||
try {
|
||||
const stats = await ToolExecution.findAll({
|
||||
attributes: [
|
||||
'tool_name',
|
||||
'tool_type',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count'],
|
||||
[sequelize.fn('AVG', sequelize.col('execution_time')), 'avg_execution_time'],
|
||||
[sequelize.fn('SUM', sequelize.col('tokens_used')), 'total_tokens']
|
||||
],
|
||||
group: ['tool_name', 'tool_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
const totalExecutions = await ToolExecution.count();
|
||||
const successfulExecutions = await ToolExecution.count({ where: { status: 'completed' } });
|
||||
const failedExecutions = await ToolExecution.count({ where: { status: 'failed' } });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
summary: {
|
||||
total: totalExecutions,
|
||||
successful: successfulExecutions,
|
||||
failed: failedExecutions,
|
||||
successRate: totalExecutions > 0 ? (successfulExecutions / totalExecutions) * 100 : 0
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get tool stats error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const retryToolExecution = async (req, res) => {
|
||||
try {
|
||||
const { executionId } = req.params;
|
||||
|
||||
const toolExecution = await ToolExecution.findByPk(executionId);
|
||||
|
||||
if (!toolExecution) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Tool execution not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (toolExecution.status === 'running') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tool execution is already running'
|
||||
});
|
||||
}
|
||||
|
||||
// Reset execution status
|
||||
await toolExecution.update({
|
||||
status: 'pending',
|
||||
output_result: null,
|
||||
execution_time: null,
|
||||
error_message: null
|
||||
});
|
||||
|
||||
// Retry tool execution using QUERYMODEL service
|
||||
try {
|
||||
const retryExecution = await queryModelService.executeTool(
|
||||
toolExecution.tool_name,
|
||||
toolExecution.tool_type,
|
||||
toolExecution.input_parameters,
|
||||
toolExecution.plan_id
|
||||
);
|
||||
|
||||
// Update the original execution record with retry results
|
||||
await toolExecution.update({
|
||||
output_result: retryExecution.output_result,
|
||||
status: retryExecution.status,
|
||||
execution_time: retryExecution.execution_time,
|
||||
tokens_used: retryExecution.tokens_used,
|
||||
error_message: null
|
||||
});
|
||||
|
||||
logger.info(`Tool execution retried successfully: ${executionId}`);
|
||||
} catch (retryError) {
|
||||
logger.error(`Tool execution retry failed: ${executionId}`, retryError);
|
||||
|
||||
await toolExecution.update({
|
||||
status: 'failed',
|
||||
error_message: retryError.message
|
||||
});
|
||||
|
||||
throw retryError;
|
||||
}
|
||||
|
||||
logger.info(`Tool execution retried: ${executionId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { toolExecution }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Retry tool execution error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
executeTool,
|
||||
getToolExecutions,
|
||||
getToolExecution,
|
||||
getToolStats,
|
||||
retryToolExecution
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const { sequelize } = require('./config/database');
|
||||
const logger = require('./utils/logger');
|
||||
//const rateLimiter = require('./middleware/rateLimiter');
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
|
||||
// Import routes
|
||||
const authRoutes = require('./routes/auth');
|
||||
const modelRoutes = require('./routes/models');
|
||||
const chatRoutes = require('./routes/chat');
|
||||
const documentRoutes = require('./routes/documents');
|
||||
const feedbackRoutes = require('./routes/feedback');
|
||||
const toolRoutes = require('./routes/tools');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 8000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['https://yourdomain.com']
|
||||
: ['http://localhost:8000'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Compression and logging
|
||||
app.use(compression());
|
||||
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
|
||||
// Rate limiting
|
||||
// app.use(rateLimiter);
|
||||
|
||||
// Static files
|
||||
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||
|
||||
// API routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/models', modelRoutes);
|
||||
app.use('/api/chat', chatRoutes);
|
||||
app.use('/api/documents', documentRoutes);
|
||||
app.use('/api/feedback', feedbackRoutes);
|
||||
app.use('/api/tools', toolRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler);
|
||||
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({ error: 'Route not found' });
|
||||
});
|
||||
|
||||
// Database connection and server start
|
||||
const startServer = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
logger.info('Database connection established successfully');
|
||||
|
||||
// Sync database in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
await sequelize.sync({ alter: true });
|
||||
logger.info('Database synchronized');
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`Server running on port ${PORT}`);
|
||||
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
logger.info('SIGTERM received, shutting down gracefully');
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('SIGINT received, shutting down gracefully');
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
startServer();
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,73 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Access denied. No token provided.'
|
||||
});
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findByPk(decoded.userId);
|
||||
|
||||
if (!user || !user.is_active) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid token or user not found'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = { userId: user.id, role: user.role };
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
}
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Token expired'
|
||||
});
|
||||
}
|
||||
|
||||
logger.error('Authentication error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const authorize = (...roles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Access denied. Authentication required.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!roles.includes(req.user.role)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied. Insufficient permissions.'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
authorize
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Log error
|
||||
logger.error(err);
|
||||
|
||||
// Mongoose bad ObjectId
|
||||
if (err.name === 'CastError') {
|
||||
const message = 'Resource not found';
|
||||
error = { message, statusCode: 404 };
|
||||
}
|
||||
|
||||
// Mongoose duplicate key
|
||||
if (err.code === 11000) {
|
||||
const message = 'Duplicate field value entered';
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = Object.values(err.errors).map(val => val.message).join(', ');
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Sequelize validation error
|
||||
if (err.name === 'SequelizeValidationError') {
|
||||
const message = err.errors.map(e => e.message).join(', ');
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// Sequelize unique constraint error
|
||||
if (err.name === 'SequelizeUniqueConstraintError') {
|
||||
const message = 'Duplicate field value entered';
|
||||
error = { message, statusCode: 400 };
|
||||
}
|
||||
|
||||
// JWT errors
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
const message = 'Invalid token';
|
||||
error = { message, statusCode: 401 };
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
const message = 'Token expired';
|
||||
error = { message, statusCode: 401 };
|
||||
}
|
||||
|
||||
res.status(error.statusCode || 500).json({
|
||||
success: false,
|
||||
error: error.message || 'Server Error',
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = errorHandler;
|
||||
@@ -0,0 +1,24 @@
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
|
||||
const rateLimiter = new RateLimiterMemory({
|
||||
keyPrefix: 'middleware',
|
||||
points: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
|
||||
duration: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 900, // 15 minutes
|
||||
});
|
||||
|
||||
const rateLimiterMiddleware = async (req, res, next) => {
|
||||
try {
|
||||
const key = req.ip;
|
||||
await rateLimiter.consume(key);
|
||||
next();
|
||||
} catch (rejRes) {
|
||||
const secs = Math.round(rejRes.msBeforeNext / 1000) || 1;
|
||||
res.set('Retry-After', String(secs));
|
||||
res.status(429).json({
|
||||
error: 'Too Many Requests',
|
||||
retryAfter: secs
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = rateLimiterMiddleware;
|
||||
@@ -0,0 +1,87 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const validate = (schema) => {
|
||||
return (req, res, next) => {
|
||||
const { error } = schema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: error.details[0].message
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// Validation schemas
|
||||
const schemas = {
|
||||
register: Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
firstName: Joi.string().min(2).required(),
|
||||
lastName: Joi.string().min(2).required(),
|
||||
role: Joi.string().valid('user', 'admin', 'expert').optional()
|
||||
}),
|
||||
|
||||
login: Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required()
|
||||
}),
|
||||
|
||||
updateProfile: Joi.object({
|
||||
firstName: Joi.string().min(2).optional(),
|
||||
lastName: Joi.string().min(2).optional(),
|
||||
preferences: Joi.object().optional()
|
||||
}),
|
||||
|
||||
createConversation: Joi.object({
|
||||
title: Joi.string().min(1).optional()
|
||||
}),
|
||||
|
||||
sendMessage: Joi.object({
|
||||
conversationId: Joi.string().uuid().required(),
|
||||
content: Joi.string().min(1).required(),
|
||||
messageType: Joi.string().valid('text', 'plan', 'execution', 'feedback').optional()
|
||||
}),
|
||||
|
||||
submitFeedback: Joi.object({
|
||||
messageId: Joi.string().uuid().optional(),
|
||||
feedbackType: Joi.string().valid('positive', 'negative', 'correction', 'suggestion').required(),
|
||||
rating: Joi.number().min(1).max(5).optional(),
|
||||
comment: Joi.string().optional(),
|
||||
correctedContent: Joi.string().optional()
|
||||
}),
|
||||
|
||||
createModelVersion: Joi.object({
|
||||
modelName: Joi.string().min(1).required(),
|
||||
modelType: Joi.string().valid('MODEL1', 'QUERYMODEL').required(),
|
||||
baseModel: Joi.string().min(1).required(),
|
||||
fineTuningMethod: Joi.string().valid('SFT', 'DPO', 'PPO', 'LoRA', 'QLoRA').optional(),
|
||||
hyperparameters: Joi.object().optional()
|
||||
}),
|
||||
|
||||
executeTool: Joi.object({
|
||||
planId: Joi.string().uuid().required(),
|
||||
toolName: Joi.string().min(1).required(),
|
||||
toolType: Joi.string().valid('query_expander', 'extraction', 'report1', 'report2', 'web_search', 'encyclopedia_pdf').required(),
|
||||
inputParameters: Joi.object().required()
|
||||
}),
|
||||
|
||||
generatePlan: Joi.object({
|
||||
query: Joi.string().min(1).required(),
|
||||
conversationId: Joi.string().uuid().optional(),
|
||||
context: Joi.object().optional()
|
||||
}),
|
||||
|
||||
executePlan: Joi.object({
|
||||
planId: Joi.string().uuid().required(),
|
||||
options: Joi.object().optional()
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validate,
|
||||
schemas
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('users', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password_hash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
first_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
last_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM('user', 'admin', 'expert'),
|
||||
defaultValue: 'user',
|
||||
allowNull: false
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
allowNull: false
|
||||
},
|
||||
last_login: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
preferences: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('users', ['email']);
|
||||
await queryInterface.addIndex('users', ['role']);
|
||||
await queryInterface.addIndex('users', ['is_active']);
|
||||
await queryInterface.addIndex('users', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('users');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('conversations', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'completed', 'archived'),
|
||||
defaultValue: 'active',
|
||||
allowNull: false
|
||||
},
|
||||
context: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('conversations', ['user_id']);
|
||||
await queryInterface.addIndex('conversations', ['status']);
|
||||
await queryInterface.addIndex('conversations', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('conversations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('plans', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
conversation_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'conversations',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
steps: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('draft', 'pending_approval', 'approved', 'rejected', 'executing', 'completed', 'failed'),
|
||||
defaultValue: 'draft',
|
||||
allowNull: false
|
||||
},
|
||||
approval_feedback: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
execution_result: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
tools_required: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
defaultValue: [],
|
||||
allowNull: false
|
||||
},
|
||||
estimated_duration: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
complexity_score: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('plans', ['conversation_id']);
|
||||
await queryInterface.addIndex('plans', ['status']);
|
||||
await queryInterface.addIndex('plans', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('plans');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('messages', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
conversation_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'conversations',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
plan_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'plans',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM('user', 'assistant', 'system'),
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
message_type: {
|
||||
type: DataTypes.ENUM('text', 'plan', 'execution', 'feedback'),
|
||||
defaultValue: 'text',
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
tokens_used: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
allowNull: false
|
||||
},
|
||||
processing_time: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('messages', ['conversation_id']);
|
||||
await queryInterface.addIndex('messages', ['plan_id']);
|
||||
await queryInterface.addIndex('messages', ['role']);
|
||||
await queryInterface.addIndex('messages', ['message_type']);
|
||||
await queryInterface.addIndex('messages', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('messages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('documents', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
filename: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
original_filename: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
file_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
file_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
file_size: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
extracted_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
embeddings: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
is_indexed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
},
|
||||
indexing_status: {
|
||||
type: DataTypes.ENUM('pending', 'processing', 'completed', 'failed'),
|
||||
defaultValue: 'pending',
|
||||
allowNull: false
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
defaultValue: [],
|
||||
allowNull: false
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('documents', ['file_type']);
|
||||
await queryInterface.addIndex('documents', ['is_indexed']);
|
||||
await queryInterface.addIndex('documents', ['indexing_status']);
|
||||
await queryInterface.addIndex('documents', ['category']);
|
||||
await queryInterface.addIndex('documents', ['tags']);
|
||||
await queryInterface.addIndex('documents', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('documents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('feedback', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
message_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'messages',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
feedback_type: {
|
||||
type: DataTypes.ENUM('positive', 'negative', 'correction', 'suggestion'),
|
||||
allowNull: false
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
},
|
||||
comment: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
corrected_content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
is_processed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
},
|
||||
processing_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('feedback', ['user_id']);
|
||||
await queryInterface.addIndex('feedback', ['message_id']);
|
||||
await queryInterface.addIndex('feedback', ['feedback_type']);
|
||||
await queryInterface.addIndex('feedback', ['is_processed']);
|
||||
await queryInterface.addIndex('feedback', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('feedback');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('model_versions', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
model_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
model_type: {
|
||||
type: DataTypes.ENUM('MODEL1', 'QUERYMODEL'),
|
||||
allowNull: false
|
||||
},
|
||||
base_model: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
fine_tuning_method: {
|
||||
type: DataTypes.ENUM('SFT', 'DPO', 'PPO', 'LoRA', 'QLoRA'),
|
||||
allowNull: true
|
||||
},
|
||||
training_data_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
allowNull: false
|
||||
},
|
||||
performance_metrics: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
},
|
||||
deployment_status: {
|
||||
type: DataTypes.ENUM('training', 'testing', 'deployed', 'deprecated'),
|
||||
defaultValue: 'training',
|
||||
allowNull: false
|
||||
},
|
||||
model_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
hyperparameters: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
training_log: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('model_versions', ['model_name']);
|
||||
await queryInterface.addIndex('model_versions', ['model_type']);
|
||||
await queryInterface.addIndex('model_versions', ['is_active']);
|
||||
await queryInterface.addIndex('model_versions', ['deployment_status']);
|
||||
await queryInterface.addIndex('model_versions', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('model_versions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('training_data', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
model_version_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'model_versions',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
data_type: {
|
||||
type: DataTypes.ENUM('qa_pair', 'plan_pair', 'feedback', 'preference'),
|
||||
allowNull: false
|
||||
},
|
||||
input_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
output_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
context: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
quality_score: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true
|
||||
},
|
||||
is_validated: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
},
|
||||
validation_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
source: {
|
||||
type: DataTypes.ENUM('expert', 'user', 'generated', 'feedback'),
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('training_data', ['model_version_id']);
|
||||
await queryInterface.addIndex('training_data', ['data_type']);
|
||||
await queryInterface.addIndex('training_data', ['is_validated']);
|
||||
await queryInterface.addIndex('training_data', ['source']);
|
||||
await queryInterface.addIndex('training_data', ['quality_score']);
|
||||
await queryInterface.addIndex('training_data', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('training_data');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('tool_executions', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
plan_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'plans',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
document_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'documents',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
tool_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
tool_type: {
|
||||
type: DataTypes.ENUM('query_expander', 'extraction', 'report1', 'report2', 'web_search', 'encyclopedia_pdf'),
|
||||
allowNull: false
|
||||
},
|
||||
input_parameters: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false
|
||||
},
|
||||
output_result: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'running', 'completed', 'failed'),
|
||||
defaultValue: 'pending',
|
||||
allowNull: false
|
||||
},
|
||||
execution_time: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true
|
||||
},
|
||||
error_message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
tokens_used: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Create indexes
|
||||
await queryInterface.addIndex('tool_executions', ['plan_id']);
|
||||
await queryInterface.addIndex('tool_executions', ['document_id']);
|
||||
await queryInterface.addIndex('tool_executions', ['tool_name']);
|
||||
await queryInterface.addIndex('tool_executions', ['tool_type']);
|
||||
await queryInterface.addIndex('tool_executions', ['status']);
|
||||
await queryInterface.addIndex('tool_executions', ['created_at']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('tool_executions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Conversation = sequelize.define('Conversation', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'completed', 'archived'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
context: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'conversations',
|
||||
indexes: [
|
||||
{ fields: ['user_id'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return Conversation;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Document = sequelize.define('Document', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
filename: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
original_filename: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
file_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
file_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
file_size: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
extracted_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
embeddings: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
is_indexed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
indexing_status: {
|
||||
type: DataTypes.ENUM('pending', 'processing', 'completed', 'failed'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
defaultValue: []
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'documents',
|
||||
indexes: [
|
||||
{ fields: ['file_type'] },
|
||||
{ fields: ['is_indexed'] },
|
||||
{ fields: ['indexing_status'] },
|
||||
{ fields: ['category'] },
|
||||
{ fields: ['tags'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return Document;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Feedback = sequelize.define('Feedback', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
message_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'messages',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
feedback_type: {
|
||||
type: DataTypes.ENUM('positive', 'negative', 'correction', 'suggestion'),
|
||||
allowNull: false
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
},
|
||||
comment: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
corrected_content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
is_processed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
processing_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'feedback',
|
||||
indexes: [
|
||||
{ fields: ['user_id'] },
|
||||
{ fields: ['message_id'] },
|
||||
{ fields: ['feedback_type'] },
|
||||
{ fields: ['is_processed'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return Feedback;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Message = sequelize.define('Message', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
conversation_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'conversations',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
plan_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'plans',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM('user', 'assistant', 'system'),
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
message_type: {
|
||||
type: DataTypes.ENUM('text', 'plan', 'execution', 'feedback'),
|
||||
defaultValue: 'text'
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
tokens_used: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
processing_time: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0
|
||||
}
|
||||
}, {
|
||||
tableName: 'messages',
|
||||
indexes: [
|
||||
{ fields: ['conversation_id'] },
|
||||
{ fields: ['plan_id'] },
|
||||
{ fields: ['role'] },
|
||||
{ fields: ['message_type'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return Message;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const ModelVersion = sequelize.define('ModelVersion', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
model_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
model_type: {
|
||||
type: DataTypes.ENUM('MODEL1', 'QUERYMODEL'),
|
||||
allowNull: false
|
||||
},
|
||||
base_model: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
fine_tuning_method: {
|
||||
type: DataTypes.ENUM('SFT', 'DPO', 'PPO', 'LoRA', 'QLoRA'),
|
||||
allowNull: true
|
||||
},
|
||||
training_data_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
performance_metrics: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
deployment_status: {
|
||||
type: DataTypes.ENUM('training', 'testing', 'deployed', 'deprecated'),
|
||||
defaultValue: 'training'
|
||||
},
|
||||
model_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
hyperparameters: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
training_log: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'model_versions',
|
||||
indexes: [
|
||||
{ fields: ['model_name'] },
|
||||
{ fields: ['model_type'] },
|
||||
{ fields: ['is_active'] },
|
||||
{ fields: ['deployment_status'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return ModelVersion;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Plan = sequelize.define('Plan', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
conversation_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'conversations',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
steps: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('draft', 'pending_approval', 'approved', 'rejected', 'executing', 'completed', 'failed'),
|
||||
defaultValue: 'draft'
|
||||
},
|
||||
approval_feedback: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
execution_result: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
tools_required: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
defaultValue: []
|
||||
},
|
||||
estimated_duration: {
|
||||
type: DataTypes.INTEGER, // in minutes
|
||||
allowNull: true
|
||||
},
|
||||
complexity_score: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'plans',
|
||||
indexes: [
|
||||
{ fields: ['conversation_id'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return Plan;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const ToolExecution = sequelize.define('ToolExecution', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
plan_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'plans',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
document_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'documents',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
tool_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
tool_type: {
|
||||
type: DataTypes.ENUM('query_expander', 'extraction', 'report1', 'report2', 'web_search', 'encyclopedia_pdf'),
|
||||
allowNull: false
|
||||
},
|
||||
input_parameters: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false
|
||||
},
|
||||
output_result: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'running', 'completed', 'failed'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
execution_time: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true
|
||||
},
|
||||
error_message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
tokens_used: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'tool_executions',
|
||||
indexes: [
|
||||
{ fields: ['plan_id'] },
|
||||
{ fields: ['document_id'] },
|
||||
{ fields: ['tool_name'] },
|
||||
{ fields: ['tool_type'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['created_at'] }
|
||||
]
|
||||
});
|
||||
|
||||
return ToolExecution;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const TrainingData = sequelize.define('TrainingData', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
model_version_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'model_versions',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
data_type: {
|
||||
type: DataTypes.ENUM('qa_pair', 'plan_pair', 'feedback', 'preference'),
|
||||
allowNull: false
|
||||
},
|
||||
input_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
output_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
context: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
quality_score: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true
|
||||
},
|
||||
is_validated: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
validation_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
source: {
|
||||
type: DataTypes.ENUM('expert', 'user', 'generated', 'feedback'),
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'training_data',
|
||||
indexes: [
|
||||
{ fields: ['model_version_id'] },
|
||||
{ fields: ['data_type'] },
|
||||
{ fields: ['is_validated'] },
|
||||
{ fields: ['source'] },
|
||||
{ fields: ['quality_score'] }
|
||||
]
|
||||
});
|
||||
|
||||
return TrainingData;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
password_hash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
first_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
last_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM('user', 'admin', 'expert'),
|
||||
defaultValue: 'user'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
last_login: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
preferences: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
indexes: [
|
||||
{ fields: ['email'] },
|
||||
{ fields: ['role'] },
|
||||
{ fields: ['is_active'] }
|
||||
]
|
||||
});
|
||||
|
||||
return User;
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
const { sequelize } = require('../config/database');
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
// Import all models
|
||||
const User = require('./User')(sequelize, DataTypes);
|
||||
const Conversation = require('./Conversation')(sequelize, DataTypes);
|
||||
const Message = require('./Message')(sequelize, DataTypes);
|
||||
const Plan = require('./Plan')(sequelize, DataTypes);
|
||||
const Feedback = require('./Feedback')(sequelize, DataTypes);
|
||||
const Document = require('./Document')(sequelize, DataTypes);
|
||||
const TrainingData = require('./TrainingData')(sequelize, DataTypes);
|
||||
const ModelVersion = require('./ModelVersion')(sequelize, DataTypes);
|
||||
const ToolExecution = require('./ToolExecution')(sequelize, DataTypes);
|
||||
|
||||
// Define associations
|
||||
const defineAssociations = () => {
|
||||
// User associations
|
||||
User.hasMany(Conversation, { foreignKey: 'user_id' });
|
||||
User.hasMany(Feedback, { foreignKey: 'user_id' });
|
||||
|
||||
// Conversation associations
|
||||
Conversation.belongsTo(User, { foreignKey: 'user_id' });
|
||||
Conversation.hasMany(Message, { foreignKey: 'conversation_id' });
|
||||
Conversation.hasMany(Plan, { foreignKey: 'conversation_id' });
|
||||
|
||||
// Message associations
|
||||
Message.belongsTo(Conversation, { foreignKey: 'conversation_id' });
|
||||
Message.belongsTo(Plan, { foreignKey: 'plan_id' });
|
||||
|
||||
// Plan associations
|
||||
Plan.belongsTo(Conversation, { foreignKey: 'conversation_id' });
|
||||
Plan.hasMany(Message, { foreignKey: 'plan_id' });
|
||||
Plan.hasMany(ToolExecution, { foreignKey: 'plan_id' });
|
||||
|
||||
// Feedback associations
|
||||
Feedback.belongsTo(User, { foreignKey: 'user_id' });
|
||||
Feedback.belongsTo(Message, { foreignKey: 'message_id' });
|
||||
|
||||
// Document associations
|
||||
Document.hasMany(ToolExecution, { foreignKey: 'document_id' });
|
||||
|
||||
// TrainingData associations
|
||||
TrainingData.belongsTo(ModelVersion, { foreignKey: 'model_version_id' });
|
||||
|
||||
// ModelVersion associations
|
||||
ModelVersion.hasMany(TrainingData, { foreignKey: 'model_version_id' });
|
||||
|
||||
// ToolExecution associations
|
||||
ToolExecution.belongsTo(Plan, { foreignKey: 'plan_id' });
|
||||
ToolExecution.belongsTo(Document, { foreignKey: 'document_id' });
|
||||
};
|
||||
|
||||
// Initialize associations
|
||||
defineAssociations();
|
||||
|
||||
const models = {
|
||||
User,
|
||||
Conversation,
|
||||
Message,
|
||||
Plan,
|
||||
Feedback,
|
||||
Document,
|
||||
TrainingData,
|
||||
ModelVersion,
|
||||
ToolExecution,
|
||||
sequelize
|
||||
};
|
||||
|
||||
module.exports = models;
|
||||
@@ -0,0 +1,15 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { register, login, getProfile, updateProfile } = require('../controllers/authController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { validate, schemas } = require('../middleware/validation');
|
||||
|
||||
// Public routes
|
||||
router.post('/register', validate(schemas.register), register);
|
||||
router.post('/login', validate(schemas.login), login);
|
||||
|
||||
// Protected routes
|
||||
router.get('/profile', authenticate, getProfile);
|
||||
router.put('/profile', authenticate, validate(schemas.updateProfile), updateProfile);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,27 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
createConversation,
|
||||
getConversations,
|
||||
getConversation,
|
||||
sendMessage,
|
||||
updateConversation,
|
||||
deleteConversation
|
||||
} = require('../controllers/chatController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { validate, schemas } = require('../middleware/validation');
|
||||
|
||||
// All chat routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// Conversation management
|
||||
router.post('/conversations', validate(schemas.createConversation), createConversation);
|
||||
router.get('/conversations', getConversations);
|
||||
router.get('/conversations/:conversationId', getConversation);
|
||||
router.put('/conversations/:conversationId', updateConversation);
|
||||
router.delete('/conversations/:conversationId', deleteConversation);
|
||||
|
||||
// Messaging
|
||||
router.post('/message', validate(schemas.sendMessage), sendMessage);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
uploadDocument,
|
||||
getDocuments,
|
||||
getDocument,
|
||||
searchDocuments,
|
||||
graphSearchDocuments,
|
||||
deleteDocument,
|
||||
upload
|
||||
} = require('../controllers/documentController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
// All document routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// Document management
|
||||
router.post('/upload', upload.single('document'), uploadDocument);
|
||||
router.get('/', getDocuments);
|
||||
router.get('/search', searchDocuments);
|
||||
router.get('/graph-search', graphSearchDocuments);
|
||||
router.get('/:documentId', getDocument);
|
||||
router.delete('/:documentId', deleteDocument);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
submitFeedback,
|
||||
getFeedback,
|
||||
getFeedbackList,
|
||||
processFeedback,
|
||||
getFeedbackStats
|
||||
} = require('../controllers/feedbackController');
|
||||
const { authenticate, authorize } = require('../middleware/auth');
|
||||
const { validate, schemas } = require('../middleware/validation');
|
||||
|
||||
// All feedback routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// Feedback submission (public to authenticated users)
|
||||
router.post('/submit', validate(schemas.submitFeedback), submitFeedback);
|
||||
|
||||
// Feedback management (admin only)
|
||||
router.get('/stats', authorize('admin'), getFeedbackStats);
|
||||
router.get('/', authorize('admin'), getFeedbackList);
|
||||
router.get('/:feedbackId', authorize('admin'), getFeedback);
|
||||
router.put('/:feedbackId/process', authorize('admin'), processFeedback);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,57 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getModelStatus,
|
||||
getModelVersions,
|
||||
createModelVersion,
|
||||
updateModelVersion,
|
||||
activateModel,
|
||||
getTrainingData
|
||||
} = require('../controllers/modelController');
|
||||
const {
|
||||
generatePlan,
|
||||
validatePlan,
|
||||
getPlan,
|
||||
updatePlan,
|
||||
getModelStatus: getModel1Status
|
||||
} = require('../controllers/model1Controller');
|
||||
const {
|
||||
executePlan,
|
||||
executeTool,
|
||||
getExecutionStatus,
|
||||
getToolExecutions,
|
||||
getModelStatus: getQueryModelStatus,
|
||||
orchestratePlan
|
||||
} = require('../controllers/queryModelController');
|
||||
const { authenticate, authorize } = require('../middleware/auth');
|
||||
const { validate, schemas } = require('../middleware/validation');
|
||||
|
||||
// All model routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// Public model status
|
||||
router.get('/status', getModelStatus);
|
||||
|
||||
// MODEL1 routes
|
||||
router.post('/model1/generate-plan', validate(schemas.generatePlan), generatePlan);
|
||||
router.post('/model1/validate-plan/:planId', validatePlan);
|
||||
router.get('/model1/plan/:planId', getPlan);
|
||||
router.put('/model1/plan/:planId', updatePlan);
|
||||
router.get('/model1/status', getModel1Status);
|
||||
|
||||
// QUERYMODEL routes
|
||||
router.post('/querymodel/execute-plan', executePlan);
|
||||
router.post('/querymodel/execute-tool', executeTool);
|
||||
router.post('/querymodel/orchestrate', orchestratePlan);
|
||||
router.get('/querymodel/execution-status/:planId', getExecutionStatus);
|
||||
router.get('/querymodel/tool-executions', getToolExecutions);
|
||||
router.get('/querymodel/status', getQueryModelStatus);
|
||||
|
||||
// Model management (admin only)
|
||||
router.get('/versions', authorize('admin'), getModelVersions);
|
||||
router.post('/versions', authorize('admin'), validate(schemas.createModelVersion), createModelVersion);
|
||||
router.put('/versions/:modelId', authorize('admin'), updateModelVersion);
|
||||
router.put('/versions/:modelId/activate', authorize('admin'), activateModel);
|
||||
router.get('/training-data', authorize('admin'), getTrainingData);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
executeTool,
|
||||
getToolExecutions,
|
||||
getToolExecution,
|
||||
getToolStats,
|
||||
retryToolExecution
|
||||
} = require('../controllers/toolController');
|
||||
const { authenticate, authorize } = require('../middleware/auth');
|
||||
const { validate, schemas } = require('../middleware/validation');
|
||||
|
||||
// All tool routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// Tool execution
|
||||
router.post('/execute', validate(schemas.executeTool), executeTool);
|
||||
|
||||
// Tool management
|
||||
router.get('/executions', getToolExecutions);
|
||||
router.get('/executions/:executionId', getToolExecution);
|
||||
router.get('/stats', authorize('admin'), getToolStats);
|
||||
router.post('/executions/:executionId/retry', retryToolExecution);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,45 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { User } = require('../models');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
try {
|
||||
// Create admin user
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(adminPassword, saltRounds);
|
||||
|
||||
const adminUser = await User.create({
|
||||
email: 'admin@reasonflow.com',
|
||||
password_hash: passwordHash,
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
role: 'admin',
|
||||
is_active: true,
|
||||
preferences: {
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
language: 'en'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Admin user created:', adminUser.email);
|
||||
return adminUser;
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating admin user:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
try {
|
||||
await User.destroy({
|
||||
where: { email: 'admin@reasonflow.com' }
|
||||
});
|
||||
console.log('✅ Admin user removed');
|
||||
} catch (error) {
|
||||
console.error('❌ Error removing admin user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
const { TrainingData, ModelVersion } = require('../models');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
try {
|
||||
// Create sample model versions
|
||||
const model1Version = await ModelVersion.create({
|
||||
model_name: 'MODEL1-v1.0',
|
||||
version: 'v1.0',
|
||||
model_type: 'MODEL1',
|
||||
base_model: 'moonshotai/kimi-k2-instruct-0905',
|
||||
fine_tuning_method: 'SFT',
|
||||
training_data_count: 0,
|
||||
performance_metrics: {
|
||||
accuracy: 0.85,
|
||||
precision: 0.82,
|
||||
recall: 0.88,
|
||||
f1_score: 0.85
|
||||
},
|
||||
is_active: true,
|
||||
deployment_status: 'deployed',
|
||||
hyperparameters: {
|
||||
learning_rate: 0.0001,
|
||||
batch_size: 16,
|
||||
epochs: 10,
|
||||
temperature: 0.3
|
||||
},
|
||||
training_log: {
|
||||
start_time: new Date(),
|
||||
end_time: new Date(),
|
||||
total_epochs: 10,
|
||||
final_loss: 0.15
|
||||
}
|
||||
});
|
||||
|
||||
const queryModelVersion = await ModelVersion.create({
|
||||
model_name: 'QUERYMODEL-v1.0',
|
||||
version: 'v1.0',
|
||||
model_type: 'QUERYMODEL',
|
||||
base_model: 'moonshotai/kimi-k2-instruct-0905',
|
||||
fine_tuning_method: 'SFT',
|
||||
training_data_count: 0,
|
||||
performance_metrics: {
|
||||
accuracy: 0.88,
|
||||
precision: 0.85,
|
||||
recall: 0.90,
|
||||
f1_score: 0.87
|
||||
},
|
||||
is_active: true,
|
||||
deployment_status: 'deployed',
|
||||
hyperparameters: {
|
||||
learning_rate: 0.0001,
|
||||
batch_size: 16,
|
||||
epochs: 10,
|
||||
temperature: 0.5
|
||||
},
|
||||
training_log: {
|
||||
start_time: new Date(),
|
||||
end_time: new Date(),
|
||||
total_epochs: 10,
|
||||
final_loss: 0.12
|
||||
}
|
||||
});
|
||||
|
||||
// Create sample training data
|
||||
const sampleTrainingData = [
|
||||
{
|
||||
model_version_id: model1Version.id,
|
||||
data_type: 'qa_pair',
|
||||
input_text: 'How do I design a bridge for heavy traffic loads?',
|
||||
output_text: 'To design a bridge for heavy traffic loads, follow these steps: 1. Calculate the maximum load requirements based on traffic volume and vehicle types. 2. Determine the bridge span and select appropriate structural system (beam, arch, or suspension). 3. Design the superstructure with adequate load capacity and safety factors. 4. Design the substructure including piers and foundations. 5. Consider environmental factors like wind, seismic loads, and temperature variations. 6. Perform structural analysis and design checks according to relevant codes (AASHTO, ACI, etc.). 7. Ensure adequate clearance and geometric requirements. 8. Plan for maintenance and inspection access.',
|
||||
context: { domain: 'civil_engineering', complexity: 'high' },
|
||||
quality_score: 0.95,
|
||||
is_validated: true,
|
||||
source: 'expert'
|
||||
},
|
||||
{
|
||||
model_version_id: model1Version.id,
|
||||
data_type: 'qa_pair',
|
||||
input_text: 'What are the key considerations for electrical system design in a commercial building?',
|
||||
output_text: 'Key considerations for electrical system design in commercial buildings include: 1. Load calculation and demand analysis. 2. Power distribution system design (main panels, subpanels, feeders). 3. Lighting design and control systems. 4. Emergency and backup power systems. 5. Grounding and bonding systems. 6. Electrical room and equipment space planning. 7. Code compliance (NEC, local codes). 8. Energy efficiency and sustainability. 9. Fire safety and protection systems. 10. Coordination with other building systems.',
|
||||
context: { domain: 'electrical_engineering', complexity: 'medium' },
|
||||
quality_score: 0.92,
|
||||
is_validated: true,
|
||||
source: 'expert'
|
||||
},
|
||||
{
|
||||
model_version_id: queryModelVersion.id,
|
||||
data_type: 'plan_pair',
|
||||
input_text: 'Execute a structural analysis for a steel frame building',
|
||||
output_text: 'To execute structural analysis for a steel frame building: 1. Gather building plans and load information. 2. Model the structure in analysis software (ETABS, SAP2000, or similar). 3. Apply dead loads, live loads, and environmental loads. 4. Run linear static analysis. 5. Check member stresses against allowable limits. 6. Verify deflection limits are met. 7. Perform stability analysis for lateral loads. 8. Generate analysis reports and drawings. 9. Review results with design team. 10. Document findings and recommendations.',
|
||||
context: { domain: 'structural_engineering', complexity: 'high' },
|
||||
quality_score: 0.90,
|
||||
is_validated: true,
|
||||
source: 'expert'
|
||||
}
|
||||
];
|
||||
|
||||
for (const data of sampleTrainingData) {
|
||||
await TrainingData.create(data);
|
||||
}
|
||||
|
||||
console.log('✅ Sample training data created');
|
||||
console.log(` - MODEL1 version: ${model1Version.id}`);
|
||||
console.log(` - QUERYMODEL version: ${queryModelVersion.id}`);
|
||||
console.log(` - Training data entries: ${sampleTrainingData.length}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample training data:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
try {
|
||||
await TrainingData.destroy({ where: {} });
|
||||
await ModelVersion.destroy({ where: {} });
|
||||
console.log('✅ Sample training data removed');
|
||||
} catch (error) {
|
||||
console.error('❌ Error removing sample training data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
const groqService = require('./groqService');
|
||||
const model1Service = require('./model1Service');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class ChatRouter {
|
||||
constructor() {
|
||||
this.systemPrompt = `You are ReasonAI, a specialized engineering assistant. Your role is to:
|
||||
|
||||
1. **Identify Engineering Questions**: Determine if a user's message requires an engineering plan or is a simple question/conversation.
|
||||
|
||||
2. **Respond Appropriately**:
|
||||
- For engineering questions: Indicate that a plan should be generated
|
||||
- For simple questions/greetings: Provide helpful responses within your engineering scope
|
||||
- For conversation context: Use conversation history to provide relevant responses
|
||||
- For out-of-scope questions: Politely redirect to engineering topics
|
||||
|
||||
3. **Scope Boundaries**:
|
||||
- ✅ Engineering: Civil, mechanical, electrical, chemical, environmental, software engineering
|
||||
- ✅ Technical: Calculations, design, analysis, standards, codes, materials
|
||||
- ✅ Simple: Greetings, basic engineering concepts, "what is 2+2"
|
||||
- ✅ Conversation: Questions about previous messages, context, follow-ups
|
||||
- ❌ Out-of-scope: Sports, entertainment, politics, general knowledge outside engineering
|
||||
|
||||
4. **Context Awareness**: Use conversation history to provide relevant responses. If user asks about previous messages or context, respond helpfully.
|
||||
|
||||
5. Some questiosn might be engineering questions but not every questions requires a plan, if it can be answered straightforwardly, then it doesnt need a plan
|
||||
|
||||
6. **Response Format (STRICT)**: Respond ONLY with a single, minified JSON object. No prose, no code fences, no backticks, no explanations. Use DOUBLE QUOTES for all keys and string values. If unsure, default to simple_response. The JSON schema is:
|
||||
{
|
||||
"isEngineeringQuestion": boolean,
|
||||
"responseType": "plan_needed" | "simple_response" | "out_of_scope",
|
||||
"response": "Your response to the user",
|
||||
"reasoning": "Why you classified it this way"
|
||||
}`;
|
||||
}
|
||||
|
||||
async routeMessage(content, context = {}) {
|
||||
try {
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: this.systemPrompt
|
||||
}
|
||||
];
|
||||
|
||||
// Add conversation history for context
|
||||
if (context.conversationHistory && context.conversationHistory.length > 0) {
|
||||
context.conversationHistory.forEach(msg => {
|
||||
messages.push({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add current message
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: `Current user message: "${content}"\n\nContext: ${JSON.stringify(context)}\n\nConversation History: ${context.conversationHistory ? JSON.stringify(context.conversationHistory) : 'None'}`
|
||||
});
|
||||
|
||||
const response = await groqService.generateResponse(messages, {
|
||||
temperature: 0.3,
|
||||
max_tokens: 4000
|
||||
});
|
||||
|
||||
// Parse the JSON response
|
||||
let routingDecision;
|
||||
try {
|
||||
// Prefer fenced json block if present
|
||||
const fence = response.content.match(/```json\s*([\s\S]*?)```/i);
|
||||
const raw = fence ? fence[1] : (response.content.match(/\{[\s\S]*\}/) || [null])[0];
|
||||
if (!raw) throw new Error('No JSON found in response');
|
||||
|
||||
// Trim whitespace and attempt strict parse
|
||||
const candidate = raw.trim();
|
||||
routingDecision = JSON.parse(candidate);
|
||||
} catch (parseError) {
|
||||
// Downgrade to warn to avoid noisy error logs for non-strict JSON
|
||||
logger.warn('Routing JSON parse failed; falling back to heuristic classification');
|
||||
|
||||
// Smart fallback based on content analysis
|
||||
const lowerContent = content.toLowerCase();
|
||||
const engineeringKeywords = ['design', 'calculate', 'analyze', 'beam', 'steel', 'concrete', 'structure', 'load', 'stress', 'engineering', 'technical', 'specification', 'code', 'standard'];
|
||||
const isEngineering = engineeringKeywords.some(keyword => lowerContent.includes(keyword));
|
||||
|
||||
routingDecision = {
|
||||
isEngineeringQuestion: isEngineering,
|
||||
responseType: isEngineering ? 'plan_needed' : 'simple_response',
|
||||
response: isEngineering ?
|
||||
"I'll generate an engineering plan for your request." :
|
||||
"I'm ReasonAI, your engineering assistant. I can help with engineering questions, calculations, and technical problems. How can I assist you today?",
|
||||
reasoning: "Fallback routing due to parse error"
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(`Message routed as: ${routingDecision.responseType}`, {
|
||||
content: content.substring(0, 100),
|
||||
reasoning: routingDecision.reasoning,
|
||||
conversationHistoryLength: context.conversationHistory ? context.conversationHistory.length : 0
|
||||
});
|
||||
|
||||
return routingDecision;
|
||||
} catch (error) {
|
||||
logger.error('Chat routing error:', error);
|
||||
|
||||
// Fallback response
|
||||
return {
|
||||
isEngineeringQuestion: false,
|
||||
responseType: 'simple_response',
|
||||
response: "I'm ReasonAI, your engineering assistant. I'm here to help with engineering questions and technical problems. How can I assist you today?",
|
||||
reasoning: "Routing service error"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async generateSimpleResponse(content, context = {}) {
|
||||
try {
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are ReasonAI, a friendly engineering assistant. Respond helpfully to the user's message while staying within your engineering scope. Be concise and professional. Consider the conversation history for context.`
|
||||
}
|
||||
];
|
||||
|
||||
// Add conversation history for context
|
||||
if (context.conversationHistory && context.conversationHistory.length > 0) {
|
||||
context.conversationHistory.forEach(msg => {
|
||||
messages.push({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add current message
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: content
|
||||
});
|
||||
|
||||
const response = await groqService.generateResponse(messages, {
|
||||
temperature: 0.7,
|
||||
max_tokens: 300
|
||||
});
|
||||
|
||||
return response.content;
|
||||
} catch (error) {
|
||||
logger.error('Simple response generation error:', error);
|
||||
return "I'm ReasonAI, your engineering assistant. I can help with engineering questions, calculations, and technical problems. How can I assist you today?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ChatRouter();
|
||||
@@ -0,0 +1,55 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
let pipeline;
|
||||
try {
|
||||
// Lazy import to avoid startup cost when unused
|
||||
({ pipeline } = require('@xenova/transformers'));
|
||||
} catch (e) {
|
||||
logger.warn('Embedding pipeline not available. Did you install @xenova/transformers?');
|
||||
}
|
||||
|
||||
class EmbeddingService {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
this.extractor = null;
|
||||
this.modelName = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2';
|
||||
}
|
||||
|
||||
async initIfNeeded() {
|
||||
if (this.initialized) return;
|
||||
if (!pipeline) {
|
||||
throw new Error('Transformers pipeline not available');
|
||||
}
|
||||
this.extractor = await pipeline('feature-extraction', this.modelName);
|
||||
this.initialized = true;
|
||||
logger.info(`Embedding model loaded: ${this.modelName}`);
|
||||
}
|
||||
|
||||
async embedText(text) {
|
||||
if (!text || !text.trim()) return [];
|
||||
await this.initIfNeeded();
|
||||
const output = await this.extractor(text, { pooling: 'mean', normalize: true });
|
||||
// output is a Tensor; convert to plain JS array
|
||||
// Depending on version, .data or .tolist()
|
||||
const vector = Array.isArray(output) ? output : (output?.data ? Array.from(output.data) : output.tolist());
|
||||
return vector;
|
||||
}
|
||||
|
||||
cosineSimilarity(a, b) {
|
||||
if (!a || !b || a.length !== b.length || a.length === 0) return 0;
|
||||
let dot = 0;
|
||||
let normA = 0;
|
||||
let normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const va = a[i] || 0;
|
||||
const vb = b[i] || 0;
|
||||
dot += va * vb;
|
||||
normA += va * va;
|
||||
normB += vb * vb;
|
||||
}
|
||||
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denom ? dot / denom : 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new EmbeddingService();
|
||||
@@ -0,0 +1,144 @@
|
||||
const { Document } = require('../models');
|
||||
const embeddingService = require('./embeddingService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class GraphRagService {
|
||||
constructor() {
|
||||
this.similarityThreshold = parseFloat(process.env.GRAPH_RAG_SIM_THRESHOLD || '0.2');
|
||||
this.maxNeighbors = parseInt(process.env.GRAPH_RAG_MAX_NEIGHBORS || '10');
|
||||
this.maxResults = parseInt(process.env.GRAPH_RAG_MAX_RESULTS || '10');
|
||||
}
|
||||
|
||||
scoreSimilarity(a, b) {
|
||||
return embeddingService.cosineSimilarity(a, b);
|
||||
}
|
||||
|
||||
tagOverlap(tagsA = [], tagsB = []) {
|
||||
const setA = new Set((tagsA || []).map((t) => (t || '').toLowerCase()));
|
||||
const setB = new Set((tagsB || []).map((t) => (t || '').toLowerCase()));
|
||||
let overlap = 0;
|
||||
setA.forEach((t) => {
|
||||
if (setB.has(t)) overlap += 1;
|
||||
});
|
||||
return overlap;
|
||||
}
|
||||
|
||||
buildGraph(nodes) {
|
||||
// nodes: [{ id, embedding, tags }]
|
||||
const edges = new Map(); // id -> [{ id, score, reason }]
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const ni = nodes[i];
|
||||
const nj = nodes[j];
|
||||
const sim = this.scoreSimilarity(ni.embedding, nj.embedding);
|
||||
const tagScore = this.tagOverlap(ni.tags, nj.tags);
|
||||
const hybrid = sim + Math.min(tagScore, 3) * 0.05; // light tag bonus
|
||||
if (hybrid >= this.similarityThreshold) {
|
||||
if (!edges.has(ni.id)) edges.set(ni.id, []);
|
||||
if (!edges.has(nj.id)) edges.set(nj.id, []);
|
||||
edges.get(ni.id).push({ id: nj.id, score: hybrid, reason: { sim, tagScore } });
|
||||
edges.get(nj.id).push({ id: ni.id, score: hybrid, reason: { sim, tagScore } });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Trim neighbors
|
||||
edges.forEach((arr, k) => {
|
||||
arr.sort((a, b) => b.score - a.score);
|
||||
edges.set(k, arr.slice(0, this.maxNeighbors));
|
||||
});
|
||||
return edges;
|
||||
}
|
||||
|
||||
async graphSearch({ query, category }) {
|
||||
const queryEmbedding = await embeddingService.embedText(query);
|
||||
|
||||
// Load candidate docs
|
||||
const where = { is_indexed: true };
|
||||
if (category) where.category = category;
|
||||
const docs = await Document.findAll({
|
||||
where,
|
||||
attributes: ['id', 'original_filename', 'extracted_text', 'embeddings', 'tags', 'category', 'created_at']
|
||||
});
|
||||
|
||||
const nodes = docs
|
||||
.filter((d) => Array.isArray(d.embeddings) && d.embeddings.length > 0)
|
||||
.map((d) => ({ id: d.id, embedding: d.embeddings, tags: d.tags || [], ref: d }));
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
// Seed scores by query similarity
|
||||
const seedScores = nodes.map((n) => ({
|
||||
id: n.id,
|
||||
score: this.scoreSimilarity(queryEmbedding, n.embedding)
|
||||
}));
|
||||
|
||||
// Log similarity scores for debugging
|
||||
logger.info('Similarity scores:', seedScores.map(s => ({ id: s.id, score: s.score.toFixed(4) })));
|
||||
|
||||
seedScores.sort((a, b) => b.score - a.score);
|
||||
const seeds = seedScores.slice(0, Math.min(5, seedScores.length)).map((s) => s.id);
|
||||
|
||||
const graph = this.buildGraph(nodes);
|
||||
|
||||
// Expand neighborhoods from seeds
|
||||
const visited = new Set();
|
||||
const scored = new Map();
|
||||
|
||||
const pushScore = (id, add, meta) => {
|
||||
const prev = scored.get(id) || { score: 0, hops: Infinity, reasons: [] };
|
||||
const combined = {
|
||||
score: Math.max(prev.score, add),
|
||||
hops: Math.min(prev.hops, meta.hops),
|
||||
reasons: prev.reasons.length < 3 ? [...prev.reasons, meta] : prev.reasons
|
||||
};
|
||||
scored.set(id, combined);
|
||||
};
|
||||
|
||||
const queue = [];
|
||||
seeds.forEach((id) => queue.push({ id, hops: 0, via: null }));
|
||||
|
||||
while (queue.length > 0 && scored.size < 200) {
|
||||
const { id, hops, via } = queue.shift();
|
||||
if (visited.has(id) || hops > 2) continue;
|
||||
visited.add(id);
|
||||
|
||||
// Base score: similarity to query
|
||||
const node = nodes.find((n) => n.id === id);
|
||||
const base = this.scoreSimilarity(queryEmbedding, node.embedding);
|
||||
pushScore(id, base, { type: 'seed', hops });
|
||||
|
||||
const neighbors = graph.get(id) || [];
|
||||
neighbors.forEach((nbr) => {
|
||||
const pathScore = (base + nbr.score) / 2;
|
||||
pushScore(nbr.id, pathScore, { type: 'edge', hops: hops + 1, via: id, edgeScore: nbr.score });
|
||||
if (!visited.has(nbr.id)) {
|
||||
queue.push({ id: nbr.id, hops: hops + 1, via: id });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Format results
|
||||
const ranked = Array.from(scored.entries())
|
||||
.map(([id, info]) => {
|
||||
const ref = nodes.find((n) => n.id === id)?.ref;
|
||||
return {
|
||||
id,
|
||||
original_filename: ref?.original_filename,
|
||||
snippet: (ref?.extracted_text || '').slice(0, 400),
|
||||
category: ref?.category,
|
||||
created_at: ref?.created_at,
|
||||
score: Number(info.score.toFixed(4)),
|
||||
hops: info.hops,
|
||||
reasons: info.reasons
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, this.maxResults);
|
||||
|
||||
return { results: ranked };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new GraphRagService();
|
||||
@@ -0,0 +1,248 @@
|
||||
const Groq = require('groq-sdk');
|
||||
const logger = require('../utils/logger');
|
||||
const { groqConfig, validateConfig, getModelConfig } = require('../config/groq');
|
||||
|
||||
class GroqService {
|
||||
constructor() {
|
||||
// Validate configuration
|
||||
validateConfig();
|
||||
|
||||
this.groq = new Groq({
|
||||
apiKey: groqConfig.apiKey,
|
||||
baseURL: groqConfig.baseURL
|
||||
});
|
||||
this.model = groqConfig.model;
|
||||
this.config = groqConfig;
|
||||
}
|
||||
|
||||
async generateResponse(messages, options = {}) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await this.groq.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: messages,
|
||||
temperature: options.temperature || 0.7,
|
||||
max_tokens: options.maxTokens || 3000,
|
||||
top_p: options.topP || 0.9,
|
||||
stream: options.stream || false
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const processingTime = (endTime - startTime) / 1000;
|
||||
|
||||
const result = {
|
||||
content: response.choices[0].message.content,
|
||||
usage: response.usage,
|
||||
processingTime,
|
||||
model: this.model,
|
||||
finishReason: response.choices[0].finish_reason
|
||||
};
|
||||
|
||||
logger.info(`Groq API call completed in ${processingTime}s, tokens: ${result.usage.total_tokens}`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Groq API error:', error);
|
||||
throw new Error(`Groq API error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async generateEngineeringPlan(query, context = {}) {
|
||||
const systemPrompt = `You are an expert engineering consultant with deep knowledge in structural engineering, mechanical engineering, electrical engineering, and civil engineering.
|
||||
|
||||
Your task is to analyze engineering problems and create detailed, step-by-step plans to solve them.
|
||||
|
||||
Guidelines:
|
||||
1. Break down complex problems into clear, actionable steps
|
||||
2. Consider safety, feasibility, and best practices
|
||||
3. Include relevant calculations, standards, and regulations
|
||||
4. Suggest appropriate tools and resources
|
||||
5. Provide time estimates for each step
|
||||
6. Consider potential challenges and mitigation strategies
|
||||
|
||||
Context: ${JSON.stringify(context)}
|
||||
|
||||
Create a comprehensive plan for the following engineering question:`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: query }
|
||||
];
|
||||
|
||||
return await this.generateResponse(messages, {
|
||||
temperature: 0.3, // Lower temperature for more focused, technical responses
|
||||
maxTokens: 3000
|
||||
});
|
||||
}
|
||||
|
||||
async executePlan(plan, tools = []) {
|
||||
const systemPrompt = `You are an engineering execution specialist. You have access to various tools to help execute engineering plans.
|
||||
|
||||
Available tools:
|
||||
${tools.map(tool => `- ${tool.name}: ${tool.description}`).join('\n')}
|
||||
|
||||
Your task is to execute the given plan step by step, using the appropriate tools when needed.
|
||||
|
||||
Guidelines:
|
||||
1. Execute each step of the plan systematically
|
||||
2. Use tools when necessary to gather information or perform calculations
|
||||
3. Provide detailed results for each step
|
||||
4. Document any issues or deviations from the plan
|
||||
5. Ensure all safety and quality standards are met
|
||||
|
||||
Execute the following plan:`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: plan }
|
||||
];
|
||||
|
||||
return await this.generateResponse(messages, {
|
||||
temperature: 0.5,
|
||||
maxTokens: 4000
|
||||
});
|
||||
}
|
||||
|
||||
async expandQuery(query, context = {}) {
|
||||
const systemPrompt = `You are a query expansion specialist for engineering problems. Your task is to take a user's engineering question and expand it into a more comprehensive, detailed query that will help find the most relevant information.
|
||||
|
||||
Guidelines:
|
||||
1. Identify key engineering concepts and terminology
|
||||
2. Suggest related questions and considerations
|
||||
3. Include relevant standards, codes, and regulations
|
||||
4. Consider different engineering disciplines that might be relevant
|
||||
5. Add context about project scope, constraints, and requirements
|
||||
|
||||
Context: ${JSON.stringify(context)}
|
||||
|
||||
Expand the following engineering query:`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: query }
|
||||
];
|
||||
|
||||
return await this.generateResponse(messages, {
|
||||
temperature: 0.4,
|
||||
maxTokens: 1500
|
||||
});
|
||||
}
|
||||
|
||||
async generateReport(data, reportType = 'general') {
|
||||
const systemPrompts = {
|
||||
general: `You are an engineering report generator. Create a comprehensive, professional engineering report based on the provided data.`,
|
||||
technical: `You are a technical engineering report generator. Create a detailed technical report with calculations, analysis, and recommendations.`,
|
||||
summary: `You are an engineering summary generator. Create a concise executive summary of the engineering analysis and findings.`
|
||||
};
|
||||
|
||||
const systemPrompt = systemPrompts[reportType] || systemPrompts.general;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: `Generate a ${reportType} report based on this data: ${JSON.stringify(data)}` }
|
||||
];
|
||||
|
||||
return await this.generateResponse(messages, {
|
||||
temperature: 0.3,
|
||||
maxTokens: 2500
|
||||
});
|
||||
}
|
||||
|
||||
async searchAndAnalyze(query, searchResults = []) {
|
||||
const systemPrompt = `You are an engineering analysis specialist. Analyze the provided search results and provide a comprehensive analysis of the engineering question.
|
||||
|
||||
Guidelines:
|
||||
1. Synthesize information from multiple sources
|
||||
2. Identify key findings and insights
|
||||
3. Highlight important calculations, formulas, or methodologies
|
||||
4. Note any conflicting information or gaps
|
||||
5. Provide recommendations based on the analysis
|
||||
6. Cite relevant sources and standards
|
||||
|
||||
Search results: ${JSON.stringify(searchResults)}
|
||||
|
||||
Analyze the following engineering question:`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: query }
|
||||
];
|
||||
|
||||
return await this.generateResponse(messages, {
|
||||
temperature: 0.4,
|
||||
maxTokens: 3000
|
||||
});
|
||||
}
|
||||
|
||||
async validatePlan(plan, feedback = []) {
|
||||
const systemPrompt = `You are an engineering plan validator. Review the provided plan and feedback to determine if the plan is valid, complete, and follows engineering best practices.
|
||||
|
||||
Guidelines:
|
||||
1. Check for completeness and logical flow
|
||||
2. Verify technical accuracy
|
||||
3. Ensure safety considerations are addressed
|
||||
4. Validate against engineering standards
|
||||
5. Consider the provided feedback
|
||||
6. Suggest improvements if needed
|
||||
|
||||
Feedback: ${JSON.stringify(feedback)}
|
||||
|
||||
Validate the following plan:`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: plan }
|
||||
];
|
||||
|
||||
return await this.generateResponse(messages, {
|
||||
temperature: 0.2,
|
||||
maxTokens: 2000
|
||||
});
|
||||
}
|
||||
|
||||
async getModelInfo() {
|
||||
try {
|
||||
// Get available models
|
||||
const models = await this.groq.models.list();
|
||||
|
||||
return {
|
||||
currentModel: this.model,
|
||||
availableModels: models.data,
|
||||
apiStatus: 'connected'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting model info:', error);
|
||||
return {
|
||||
currentModel: this.model,
|
||||
availableModels: [],
|
||||
apiStatus: 'error',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
try {
|
||||
const testResponse = await this.generateResponse([
|
||||
{ role: 'user', content: 'Hello, this is a test message.' }
|
||||
], {
|
||||
maxTokens: 10
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
response: testResponse,
|
||||
model: this.model
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Groq connection test failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new GroqService();
|
||||
@@ -0,0 +1,290 @@
|
||||
const groqService = require('./groqService');
|
||||
const { Plan, Message, Conversation } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class Model1Service {
|
||||
constructor() {
|
||||
this.modelType = 'MODEL1';
|
||||
this.systemPrompt = `You are MODEL1, an expert engineering reasoning system. Your primary function is to analyze complex engineering problems and create detailed, step-by-step plans to solve them.
|
||||
|
||||
Your capabilities include:
|
||||
- Structural engineering analysis
|
||||
- Mechanical engineering design
|
||||
- Electrical engineering systems
|
||||
- Civil engineering projects
|
||||
- Computer Enginnering
|
||||
- Safety and compliance considerations
|
||||
- Cost estimation and feasibility analysis
|
||||
- Risk assessment and mitigation
|
||||
- All other engineering related courses
|
||||
|
||||
When creating plans, always:
|
||||
1. Break down complex problems into clear, actionable steps
|
||||
2. Consider safety, feasibility, and best practices
|
||||
3. Include relevant calculations, standards, and regulations
|
||||
4. Suggest appropriate tools and resources
|
||||
5. Provide time estimates for each step
|
||||
6. Consider potential challenges and mitigation strategies
|
||||
7. DO NOT give the answer, your job is to plan
|
||||
8. Only give plan when the user ask about actionable questions
|
||||
|
||||
Format your response as a structured plan with:
|
||||
- Title: Clear, descriptive title
|
||||
- Description: Brief overview of the problem and approach
|
||||
- Steps: Numbered list of detailed steps
|
||||
- Tools Required: List of tools needed
|
||||
- Estimated Duration: Total time estimate
|
||||
- Complexity Score: 1-10 scale
|
||||
- Safety Considerations: Key safety points
|
||||
- Quality Checks: Verification steps`;
|
||||
}
|
||||
|
||||
async generatePlan(query, context = {}) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Prepare context for the model
|
||||
const enhancedContext = {
|
||||
...context,
|
||||
timestamp: new Date().toISOString(),
|
||||
modelType: this.modelType
|
||||
};
|
||||
|
||||
// Generate the plan using Groq
|
||||
const response = await groqService.generateEngineeringPlan(query, enhancedContext);
|
||||
|
||||
const endTime = Date.now();
|
||||
const processingTime = (endTime - startTime) / 1000;
|
||||
|
||||
// Parse the response to extract structured plan data
|
||||
const planData = this.parsePlanResponse(response.content);
|
||||
|
||||
logger.info(`MODEL1 plan generated in ${processingTime}s for query: ${query.substring(0, 100)}...`);
|
||||
|
||||
return {
|
||||
...planData,
|
||||
processingTime,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
model: response.model,
|
||||
finishReason: response.finishReason
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('MODEL1 plan generation error:', error);
|
||||
throw new Error(`MODEL1 plan generation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
parsePlanResponse(content) {
|
||||
try {
|
||||
// Extract structured data from the response
|
||||
const lines = content.split('\n');
|
||||
let title = '';
|
||||
let description = '';
|
||||
let steps = [];
|
||||
let toolsRequired = [];
|
||||
let estimatedDuration = 0;
|
||||
let complexityScore = 5;
|
||||
let safetyConsiderations = [];
|
||||
let qualityChecks = [];
|
||||
|
||||
let currentSection = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.toLowerCase().includes('title:')) {
|
||||
title = trimmedLine.split(':')[1]?.trim() || 'Engineering Plan';
|
||||
} else if (trimmedLine.toLowerCase().includes('description:')) {
|
||||
description = trimmedLine.split(':')[1]?.trim() || '';
|
||||
} else if (trimmedLine.toLowerCase().includes('steps:')) {
|
||||
currentSection = 'steps';
|
||||
} else if (trimmedLine.toLowerCase().includes('tools required:')) {
|
||||
currentSection = 'tools';
|
||||
} else if (trimmedLine.toLowerCase().includes('estimated duration:')) {
|
||||
const duration = trimmedLine.split(':')[1]?.trim();
|
||||
estimatedDuration = this.parseDuration(duration);
|
||||
} else if (trimmedLine.toLowerCase().includes('complexity score:')) {
|
||||
const score = trimmedLine.split(':')[1]?.trim();
|
||||
complexityScore = parseInt(score) || 5;
|
||||
} else if (trimmedLine.toLowerCase().includes('safety considerations:')) {
|
||||
currentSection = 'safety';
|
||||
} else if (trimmedLine.toLowerCase().includes('quality checks:')) {
|
||||
currentSection = 'quality';
|
||||
} else if (trimmedLine.match(/^\d+\./)) {
|
||||
if (currentSection === 'steps') {
|
||||
steps.push(trimmedLine);
|
||||
}
|
||||
} else if (trimmedLine.startsWith('-')) {
|
||||
if (currentSection === 'tools') {
|
||||
toolsRequired.push(trimmedLine.substring(1).trim());
|
||||
} else if (currentSection === 'safety') {
|
||||
safetyConsiderations.push(trimmedLine.substring(1).trim());
|
||||
} else if (currentSection === 'quality') {
|
||||
qualityChecks.push(trimmedLine.substring(1).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no structured data found, create a basic plan
|
||||
if (!title) {
|
||||
title = 'Engineering Plan';
|
||||
description = content.substring(0, 200) + '...';
|
||||
steps = [content];
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
steps,
|
||||
toolsRequired,
|
||||
estimatedDuration,
|
||||
complexityScore,
|
||||
safetyConsiderations,
|
||||
qualityChecks,
|
||||
rawContent: content
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error parsing plan response:', error);
|
||||
return {
|
||||
title: 'Engineering Plan',
|
||||
description: content,
|
||||
steps: [content],
|
||||
toolsRequired: [],
|
||||
estimatedDuration: 60,
|
||||
complexityScore: 5,
|
||||
safetyConsiderations: [],
|
||||
qualityChecks: [],
|
||||
rawContent: content
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
parseDuration(duration) {
|
||||
if (!duration) return 60;
|
||||
|
||||
const match = duration.match(/(\d+)\s*(hour|hr|minute|min|day|d)/i);
|
||||
if (match) {
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2].toLowerCase();
|
||||
|
||||
switch (unit) {
|
||||
case 'hour':
|
||||
case 'hr':
|
||||
return value * 60;
|
||||
case 'minute':
|
||||
case 'min':
|
||||
return value;
|
||||
case 'day':
|
||||
case 'd':
|
||||
return value * 24 * 60;
|
||||
default:
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
return 60; // Default to 60 minutes
|
||||
}
|
||||
|
||||
async savePlan(planData, conversationId, userId) {
|
||||
try {
|
||||
const plan = await Plan.create({
|
||||
conversation_id: conversationId,
|
||||
title: planData.title,
|
||||
description: planData.description,
|
||||
steps: planData.steps,
|
||||
status: 'draft',
|
||||
tools_required: planData.toolsRequired,
|
||||
estimated_duration: planData.estimatedDuration,
|
||||
complexity_score: planData.complexityScore,
|
||||
metadata: {
|
||||
safetyConsiderations: planData.safetyConsiderations,
|
||||
qualityChecks: planData.qualityChecks,
|
||||
processingTime: planData.processingTime,
|
||||
tokensUsed: planData.tokensUsed,
|
||||
model: planData.model
|
||||
}
|
||||
});
|
||||
|
||||
// Create a message record for the plan
|
||||
await Message.create({
|
||||
conversation_id: conversationId,
|
||||
plan_id: plan.id,
|
||||
role: 'assistant',
|
||||
content: planData.rawContent,
|
||||
message_type: 'plan',
|
||||
metadata: {
|
||||
modelType: this.modelType,
|
||||
processingTime: planData.processingTime,
|
||||
tokensUsed: planData.tokensUsed
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Plan saved: ${plan.id} for conversation: ${conversationId}`);
|
||||
|
||||
return plan;
|
||||
} catch (error) {
|
||||
logger.error('Error saving plan:', error);
|
||||
throw new Error(`Failed to save plan: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async validatePlan(planId, feedback = []) {
|
||||
try {
|
||||
const plan = await Plan.findByPk(planId);
|
||||
if (!plan) {
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
const validationResponse = await groqService.validatePlan(plan.description, feedback);
|
||||
|
||||
// Update plan with validation results
|
||||
await plan.update({
|
||||
status: validationResponse.content.includes('valid') ? 'approved' : 'rejected',
|
||||
approval_feedback: validationResponse.content,
|
||||
metadata: {
|
||||
...plan.metadata,
|
||||
validation: {
|
||||
response: validationResponse.content,
|
||||
tokensUsed: validationResponse.usage.total_tokens,
|
||||
processingTime: validationResponse.processingTime
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Plan validated: ${planId}, status: ${plan.status}`);
|
||||
|
||||
return {
|
||||
plan,
|
||||
validation: validationResponse
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Plan validation error:', error);
|
||||
throw new Error(`Plan validation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getModelStatus() {
|
||||
try {
|
||||
const groqInfo = await groqService.getModelInfo();
|
||||
const connectionTest = await groqService.testConnection();
|
||||
|
||||
return {
|
||||
modelType: this.modelType,
|
||||
status: connectionTest.success ? 'active' : 'error',
|
||||
groqInfo,
|
||||
connectionTest,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('MODEL1 status check error:', error);
|
||||
return {
|
||||
modelType: this.modelType,
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Model1Service();
|
||||
@@ -0,0 +1,500 @@
|
||||
const groqService = require('./groqService');
|
||||
const { Plan, ToolExecution, Document } = require('../models');
|
||||
const logger = require('../utils/logger');
|
||||
const graphRagService = require('./graphRagService');
|
||||
const embeddingService = require('./embeddingService');
|
||||
const axios = require('axios');
|
||||
|
||||
class QueryModelService {
|
||||
constructor() {
|
||||
this.modelType = 'QUERYMODEL';
|
||||
this.systemPrompt = `You are QUERYMODEL, an expert engineering execution system. Your primary function is to execute engineering plans using various tools and resources.
|
||||
|
||||
Your capabilities include:
|
||||
- Executing step-by-step engineering plans
|
||||
- Using specialized tools for calculations, analysis, and reporting
|
||||
- Coordinating with external resources and databases
|
||||
- Generating detailed execution reports
|
||||
- Handling complex engineering workflows
|
||||
- Ensuring quality and safety standards
|
||||
|
||||
Available tools:
|
||||
- Query Expander: Enhance and clarify engineering queries
|
||||
- Extraction: Search and extract information from documents
|
||||
- Report1: Generate formatted engineering reports
|
||||
- Report2: Create detailed engineering files and documents
|
||||
- Web Search: Find current engineering information and standards
|
||||
- Encyclopedia PDF: Search specialized engineering documents
|
||||
|
||||
When executing plans, always:
|
||||
1. Follow the plan steps systematically
|
||||
2. Use appropriate tools for each step
|
||||
3. Document all results and findings
|
||||
4. Ensure quality and safety standards
|
||||
5. Provide detailed progress updates
|
||||
6. Handle errors and deviations gracefully`;
|
||||
}
|
||||
|
||||
async executePlan(planId, options = {}) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get the plan
|
||||
const plan = await Plan.findByPk(planId);
|
||||
if (!plan) {
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
if (plan.status !== 'approved') {
|
||||
throw new Error('Plan must be approved before execution');
|
||||
}
|
||||
|
||||
// Update plan status to executing
|
||||
await plan.update({ status: 'executing' });
|
||||
|
||||
// Prepare execution context
|
||||
const executionContext = {
|
||||
planId,
|
||||
planTitle: plan.title,
|
||||
planSteps: plan.steps,
|
||||
toolsRequired: plan.tools_required,
|
||||
estimatedDuration: plan.estimated_duration,
|
||||
complexityScore: plan.complexity_score,
|
||||
...options
|
||||
};
|
||||
|
||||
// Execute the plan using Groq
|
||||
const response = await groqService.executePlan(plan.description, plan.tools_required);
|
||||
|
||||
const endTime = Date.now();
|
||||
const processingTime = (endTime - startTime) / 1000;
|
||||
|
||||
// Parse execution results
|
||||
const executionResults = this.parseExecutionResponse(response.content);
|
||||
|
||||
// Update plan with execution results
|
||||
await plan.update({
|
||||
status: 'completed',
|
||||
execution_result: executionResults,
|
||||
metadata: {
|
||||
...plan.metadata,
|
||||
execution: {
|
||||
processingTime,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
model: response.model,
|
||||
finishReason: response.finishReason
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`QUERYMODEL execution completed in ${processingTime}s for plan: ${planId}`);
|
||||
|
||||
return {
|
||||
plan,
|
||||
executionResults,
|
||||
processingTime,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
model: response.model
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL execution error:', error);
|
||||
|
||||
// Update plan status to failed
|
||||
if (plan) {
|
||||
await plan.update({
|
||||
status: 'failed',
|
||||
execution_result: { error: error.message }
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`QUERYMODEL execution failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
parseExecutionResponse(content) {
|
||||
try {
|
||||
const lines = content.split('\n');
|
||||
let stepsCompleted = [];
|
||||
let results = [];
|
||||
let toolsUsed = [];
|
||||
let issues = [];
|
||||
let recommendations = [];
|
||||
|
||||
let currentSection = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.toLowerCase().includes('steps completed:')) {
|
||||
currentSection = 'steps';
|
||||
} else if (trimmedLine.toLowerCase().includes('results:')) {
|
||||
currentSection = 'results';
|
||||
} else if (trimmedLine.toLowerCase().includes('tools used:')) {
|
||||
currentSection = 'tools';
|
||||
} else if (trimmedLine.toLowerCase().includes('issues:')) {
|
||||
currentSection = 'issues';
|
||||
} else if (trimmedLine.toLowerCase().includes('recommendations:')) {
|
||||
currentSection = 'recommendations';
|
||||
} else if (trimmedLine.match(/^\d+\./)) {
|
||||
if (currentSection === 'steps') {
|
||||
stepsCompleted.push(trimmedLine);
|
||||
}
|
||||
} else if (trimmedLine.startsWith('-')) {
|
||||
if (currentSection === 'results') {
|
||||
results.push(trimmedLine.substring(1).trim());
|
||||
} else if (trimmedLine === 'tools') {
|
||||
toolsUsed.push(trimmedLine.substring(1).trim());
|
||||
} else if (currentSection === 'issues') {
|
||||
issues.push(trimmedLine.substring(1).trim());
|
||||
} else if (currentSection === 'recommendations') {
|
||||
recommendations.push(trimmedLine.substring(1).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stepsCompleted,
|
||||
results,
|
||||
toolsUsed,
|
||||
issues,
|
||||
recommendations,
|
||||
rawContent: content,
|
||||
executionStatus: issues.length > 0 ? 'completed_with_issues' : 'completed_successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error parsing execution response:', error);
|
||||
return {
|
||||
stepsCompleted: [],
|
||||
results: [content],
|
||||
toolsUsed: [],
|
||||
issues: [],
|
||||
recommendations: [],
|
||||
rawContent: content,
|
||||
executionStatus: 'completed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async executeTool(toolName, toolType, inputParameters, planId) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Create tool execution record
|
||||
const toolExecution = await ToolExecution.create({
|
||||
plan_id: planId,
|
||||
tool_name: toolName,
|
||||
tool_type: toolType,
|
||||
input_parameters: inputParameters,
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
let result;
|
||||
|
||||
// Execute the specific tool
|
||||
switch (toolType) {
|
||||
case 'query_expander':
|
||||
result = await this.executeQueryExpander(inputParameters);
|
||||
break;
|
||||
case 'extraction':
|
||||
result = await this.executeExtraction(inputParameters);
|
||||
break;
|
||||
case 'report1':
|
||||
result = await this.executeReport1(inputParameters);
|
||||
break;
|
||||
case 'report2':
|
||||
result = await this.executeReport2(inputParameters);
|
||||
break;
|
||||
case 'web_search':
|
||||
result = await this.executeWebSearch(inputParameters);
|
||||
break;
|
||||
case 'encyclopedia_pdf':
|
||||
result = await this.executeEncyclopediaPdf(inputParameters);
|
||||
break;
|
||||
case 'orchestrate':
|
||||
result = await this.executeOrchestrate(inputParameters);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown tool type: ${toolType}`);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const executionTime = (endTime - startTime) / 1000;
|
||||
|
||||
// Update tool execution record
|
||||
await toolExecution.update({
|
||||
output_result: result,
|
||||
status: 'completed',
|
||||
execution_time: executionTime,
|
||||
tokens_used: result.tokensUsed || 0
|
||||
});
|
||||
|
||||
logger.info(`Tool executed: ${toolName} in ${executionTime}s`);
|
||||
|
||||
return toolExecution;
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution error: ${toolName}`, error);
|
||||
|
||||
// Update tool execution record with error
|
||||
if (toolExecution) {
|
||||
await toolExecution.update({
|
||||
status: 'failed',
|
||||
error_message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Tool execution failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async executeQueryExpander(inputParameters) {
|
||||
const { query, context } = inputParameters;
|
||||
const response = await groqService.expandQuery(query, context);
|
||||
|
||||
return {
|
||||
expandedQuery: response.content,
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
processingTime: response.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
async executeOrchestrate(inputParameters) {
|
||||
const { query, category, topK = 5, generateReport = true } = inputParameters;
|
||||
|
||||
// 1) Expand query
|
||||
const expanded = await this.executeQueryExpander({ query, context: { category } });
|
||||
const expandedQuery = (expanded.expandedQuery || '').trim() || query;
|
||||
|
||||
// 2) Extract from RAG using original query (use 'general' category for now)
|
||||
const extraction = await this.executeExtraction({ query: query, category: 'general', topK });
|
||||
|
||||
// 3) If low confidence, augment with web search using original query
|
||||
let web = null;
|
||||
if (extraction.confidence < 0.7) {
|
||||
try {
|
||||
web = await this.executeWebSearch({ query: query, maxResults: 5, searchDepth: 'basic', includeAnswer: true });
|
||||
} catch (e) {
|
||||
// continue without web
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Optionally generate a brief report
|
||||
let report = null;
|
||||
if (generateReport) {
|
||||
// Get full document content for better report generation
|
||||
const documentDetails = await Promise.all(
|
||||
extraction.results.slice(0, 3).map(async (result) => {
|
||||
try {
|
||||
const doc = await Document.findByPk(result.id, {
|
||||
attributes: ['id', 'original_filename', 'extracted_text', 'category']
|
||||
});
|
||||
return {
|
||||
filename: result.original_filename,
|
||||
content: doc?.extracted_text || result.snippet,
|
||||
score: result.score,
|
||||
category: result.category
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
filename: result.original_filename,
|
||||
content: result.snippet,
|
||||
score: result.score,
|
||||
category: result.category
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const reportData = {
|
||||
query,
|
||||
expandedQuery,
|
||||
relevantDocuments: documentDetails,
|
||||
webAnswer: web?.answer || null,
|
||||
webCount: web?.totalResults || 0
|
||||
};
|
||||
const reportResp = await groqService.generateReport(reportData, 'summary');
|
||||
report = {
|
||||
content: reportResp.content,
|
||||
tokensUsed: reportResp.usage?.total_tokens,
|
||||
processingTime: reportResp.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
expandedQuery,
|
||||
extraction,
|
||||
web,
|
||||
report,
|
||||
decision: {
|
||||
usedWeb: !!web,
|
||||
confidence: extraction.confidence
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async executeExtraction(inputParameters) {
|
||||
const { query, topK = 5, category } = inputParameters;
|
||||
|
||||
// 1) Try Graph RAG first
|
||||
const graph = await graphRagService.graphSearch({ query, category });
|
||||
let results = graph.results.map((r) => ({
|
||||
id: r.id,
|
||||
original_filename: r.original_filename,
|
||||
snippet: r.snippet,
|
||||
category: r.category,
|
||||
score: r.score,
|
||||
source: 'graph'
|
||||
}));
|
||||
|
||||
// 2) If not enough, fallback to semantic search
|
||||
if (results.length < topK) {
|
||||
const queryEmbedding = await embeddingService.embedText(query);
|
||||
const where = { is_indexed: true };
|
||||
if (category) where.category = category;
|
||||
const docs = await Document.findAll({
|
||||
where,
|
||||
attributes: ['id', 'original_filename', 'extracted_text', 'embeddings', 'category']
|
||||
});
|
||||
|
||||
const scored = [];
|
||||
for (const d of docs) {
|
||||
const emb = d.embeddings || [];
|
||||
if (!Array.isArray(emb) || emb.length === 0) continue;
|
||||
const score = embeddingService.cosineSimilarity(queryEmbedding, emb);
|
||||
scored.push({
|
||||
id: d.id,
|
||||
original_filename: d.original_filename,
|
||||
snippet: (d.extracted_text || '').slice(0, 400),
|
||||
category: d.category,
|
||||
score,
|
||||
source: 'semantic'
|
||||
});
|
||||
}
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
const need = topK - results.length;
|
||||
results = results.concat(scored.slice(0, Math.max(0, need)));
|
||||
}
|
||||
|
||||
// Trim to topK and return
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
results = results.slice(0, topK);
|
||||
|
||||
// Confidence heuristic
|
||||
const confidence = results.length > 0 ? Math.min(0.99, Math.max(0.5, results[0].score)) : 0;
|
||||
|
||||
return {
|
||||
query,
|
||||
topK,
|
||||
results,
|
||||
confidence
|
||||
};
|
||||
}
|
||||
|
||||
async executeReport1(inputParameters) {
|
||||
const { data, format, context } = inputParameters;
|
||||
const response = await groqService.generateReport(data, 'technical');
|
||||
|
||||
return {
|
||||
report: response.content,
|
||||
format: format || 'technical',
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
processingTime: response.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
async executeReport2(inputParameters) {
|
||||
const { data, format, filename } = inputParameters;
|
||||
const response = await groqService.generateReport(data, format);
|
||||
|
||||
return {
|
||||
report: response.content,
|
||||
filename: filename || `report_${Date.now()}.txt`,
|
||||
format: format || 'general',
|
||||
tokensUsed: response.usage.total_tokens,
|
||||
processingTime: response.processingTime
|
||||
};
|
||||
}
|
||||
|
||||
async executeWebSearch(inputParameters) {
|
||||
const { query, maxResults = 5, searchDepth = 'basic', includeAnswer = true } = inputParameters;
|
||||
|
||||
const apiKey = process.env.TAVILY_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('TAVILY_API_KEY is not set');
|
||||
}
|
||||
|
||||
const url = 'https://api.tavily.com/search';
|
||||
const payload = {
|
||||
query,
|
||||
search_depth: searchDepth,
|
||||
include_answer: includeAnswer,
|
||||
max_results: Math.min(10, Math.max(1, maxResults))
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await axios.post(url, payload, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` }, // ✅ Correct way to pass API key
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
const data = resp.data || {};
|
||||
|
||||
const results = (data.results || []).map((r) => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
snippet: r.content || r.snippet || '',
|
||||
score: r.score ?? undefined,
|
||||
published: r.published_date || undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
query,
|
||||
answer: data.answer || null,
|
||||
results,
|
||||
totalResults: results.length,
|
||||
source: 'tavily',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Tavily web search error:', error?.response?.data || error.message);
|
||||
throw new Error('Web search failed');
|
||||
}
|
||||
}
|
||||
|
||||
async executeEncyclopediaPdf(inputParameters) {
|
||||
const { query, documents, context } = inputParameters;
|
||||
|
||||
// Use our RAG for offline PDF search (category filter could be 'encyclopedia')
|
||||
const graph = await graphRagService.graphSearch({ query, category: 'encyclopedia' });
|
||||
return {
|
||||
query,
|
||||
results: graph.results,
|
||||
totalResults: graph.results.length,
|
||||
source: 'offline_rag'
|
||||
};
|
||||
}
|
||||
|
||||
async getModelStatus() {
|
||||
try {
|
||||
const groqInfo = await groqService.getModelInfo();
|
||||
const connectionTest = await groqService.testConnection();
|
||||
|
||||
return {
|
||||
modelType: this.modelType,
|
||||
status: connectionTest.success ? 'active' : 'error',
|
||||
groqInfo,
|
||||
connectionTest,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('QUERYMODEL status check error:', error);
|
||||
return {
|
||||
modelType: this.modelType,
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new QueryModelService();
|
||||
@@ -0,0 +1,208 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const logger = require('./logger');
|
||||
|
||||
class ConfigLoader {
|
||||
constructor() {
|
||||
this.config = {};
|
||||
this.env = process.env.NODE_ENV || 'development';
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
try {
|
||||
// Load base configuration
|
||||
this.loadBaseConfig();
|
||||
|
||||
// Load environment-specific configuration
|
||||
this.loadEnvConfig();
|
||||
|
||||
// Validate configuration
|
||||
this.validateConfig();
|
||||
|
||||
logger.info(`Configuration loaded for environment: ${this.env}`);
|
||||
} catch (error) {
|
||||
logger.error('Configuration loading failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
loadBaseConfig() {
|
||||
// Load from appConfig.js
|
||||
const { appConfig } = require('../config/appConfig');
|
||||
this.config = { ...appConfig };
|
||||
}
|
||||
|
||||
loadEnvConfig() {
|
||||
const envFile = path.join(__dirname, '../../', `env.${this.env}`);
|
||||
|
||||
if (fs.existsSync(envFile)) {
|
||||
logger.info(`Loading environment configuration from: ${envFile}`);
|
||||
|
||||
// Parse environment file
|
||||
const envContent = fs.readFileSync(envFile, 'utf8');
|
||||
const envVars = this.parseEnvFile(envContent);
|
||||
|
||||
// Override configuration with environment-specific values
|
||||
Object.assign(process.env, envVars);
|
||||
|
||||
// Reload configuration with new environment variables
|
||||
const { appConfig } = require('../config/appConfig');
|
||||
this.config = { ...appConfig };
|
||||
} else {
|
||||
logger.warn(`Environment configuration file not found: ${envFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
parseEnvFile(content) {
|
||||
const envVars = {};
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse KEY=VALUE format
|
||||
const equalIndex = trimmedLine.indexOf('=');
|
||||
if (equalIndex > 0) {
|
||||
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||
const value = trimmedLine.substring(equalIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
const cleanValue = value.replace(/^["']|["']$/g, '');
|
||||
envVars[key] = cleanValue;
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// Required fields validation
|
||||
if (!this.config.apis.groq.apiKey) {
|
||||
errors.push('GROQ_API_KEY is required');
|
||||
}
|
||||
|
||||
if (!this.config.auth.jwtSecret) {
|
||||
errors.push('JWT_SECRET is required');
|
||||
}
|
||||
|
||||
if (!this.config.database.password) {
|
||||
errors.push('DB_PASSWORD is required');
|
||||
}
|
||||
|
||||
// Environment-specific validations
|
||||
if (this.env === 'production') {
|
||||
if (this.config.auth.jwtSecret === 'dev_secret_key_change_in_production') {
|
||||
errors.push('JWT_SECRET must be changed for production');
|
||||
}
|
||||
|
||||
if (!this.config.database.ssl) {
|
||||
warnings.push('Database SSL is recommended for production');
|
||||
}
|
||||
}
|
||||
|
||||
// Log warnings
|
||||
warnings.forEach(warning => {
|
||||
logger.warn(`Configuration warning: ${warning}`);
|
||||
});
|
||||
|
||||
// Throw errors
|
||||
if (errors.length > 0) {
|
||||
const errorMessage = `Configuration errors: ${errors.join(', ')}`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
get(key, defaultValue = null) {
|
||||
const keys = key.split('.');
|
||||
let value = this.config;
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k];
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
getServerConfig() {
|
||||
return this.config.server;
|
||||
}
|
||||
|
||||
getDatabaseConfig() {
|
||||
return this.config.database;
|
||||
}
|
||||
|
||||
getApiConfig() {
|
||||
return this.config.apis;
|
||||
}
|
||||
|
||||
getAuthConfig() {
|
||||
return this.config.auth;
|
||||
}
|
||||
|
||||
getUploadConfig() {
|
||||
return this.config.upload;
|
||||
}
|
||||
|
||||
getRateLimitConfig() {
|
||||
return this.config.rateLimit;
|
||||
}
|
||||
|
||||
getLoggingConfig() {
|
||||
return this.config.logging;
|
||||
}
|
||||
|
||||
getModelConfig() {
|
||||
return this.config.models;
|
||||
}
|
||||
|
||||
getSecurityConfig() {
|
||||
return this.config.security;
|
||||
}
|
||||
|
||||
getMonitoringConfig() {
|
||||
return this.config.monitoring;
|
||||
}
|
||||
|
||||
getDevelopmentConfig() {
|
||||
return this.config.development;
|
||||
}
|
||||
|
||||
isDevelopment() {
|
||||
return this.env === 'development';
|
||||
}
|
||||
|
||||
isProduction() {
|
||||
return this.env === 'production';
|
||||
}
|
||||
|
||||
isTest() {
|
||||
return this.env === 'test';
|
||||
}
|
||||
|
||||
getEnvironment() {
|
||||
return this.env;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const configLoader = new ConfigLoader();
|
||||
|
||||
module.exports = configLoader;
|
||||
@@ -0,0 +1,110 @@
|
||||
const { testConnection, syncDatabase, dropDatabase } = require('../config/database');
|
||||
const logger = require('./logger');
|
||||
|
||||
const initializeDatabase = async (force = false) => {
|
||||
try {
|
||||
console.log('🚀 Initializing database...');
|
||||
|
||||
// Test connection
|
||||
console.log('📡 Testing database connection...');
|
||||
const connected = await testConnection();
|
||||
if (!connected) {
|
||||
throw new Error('Database connection failed');
|
||||
}
|
||||
console.log('✅ Database connection successful');
|
||||
|
||||
// Sync database (create tables)
|
||||
console.log('🔧 Synchronizing database schema...');
|
||||
const synced = await syncDatabase(force);
|
||||
if (!synced) {
|
||||
throw new Error('Database synchronization failed');
|
||||
}
|
||||
console.log('✅ Database schema synchronized');
|
||||
|
||||
console.log('🎉 Database initialization completed successfully!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization failed:', error.message);
|
||||
logger.error('Database initialization error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetDatabase = async () => {
|
||||
try {
|
||||
console.log('🔄 Resetting database...');
|
||||
|
||||
// Drop all tables
|
||||
console.log('🗑️ Dropping all tables...');
|
||||
const dropped = await dropDatabase();
|
||||
if (!dropped) {
|
||||
throw new Error('Database drop failed');
|
||||
}
|
||||
console.log('✅ All tables dropped');
|
||||
|
||||
// Reinitialize
|
||||
console.log('🔧 Reinitializing database...');
|
||||
const initialized = await initializeDatabase(true);
|
||||
if (!initialized) {
|
||||
throw new Error('Database reinitialization failed');
|
||||
}
|
||||
console.log('✅ Database reset completed');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database reset failed:', error.message);
|
||||
logger.error('Database reset error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkDatabaseStatus = async () => {
|
||||
try {
|
||||
console.log('🔍 Checking database status...');
|
||||
|
||||
const connected = await testConnection();
|
||||
if (!connected) {
|
||||
console.log('❌ Database connection failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('✅ Database is connected and ready');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database status check failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Run initialization if this file is executed directly
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'init';
|
||||
|
||||
switch (command) {
|
||||
case 'init':
|
||||
initializeDatabase().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
break;
|
||||
case 'reset':
|
||||
resetDatabase().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
break;
|
||||
case 'status':
|
||||
checkDatabaseStatus().then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log('Usage: node databaseInit.js [init|reset|status]');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDatabase,
|
||||
resetDatabase,
|
||||
checkDatabaseStatus
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
const { initializeDatabase, resetDatabase, checkDatabaseStatus } = require('./databaseInit');
|
||||
const { sequelize } = require('../config/database');
|
||||
const logger = require('./logger');
|
||||
|
||||
// Import seeders
|
||||
const createAdminUser = require('../seeders/001_create_admin_user');
|
||||
const createSampleTrainingData = require('../seeders/002_create_sample_training_data');
|
||||
|
||||
const runMigrations = async () => {
|
||||
try {
|
||||
console.log('🔄 Running database migrations...');
|
||||
|
||||
// Import all migration files
|
||||
const migrations = [
|
||||
require('../migrations/001_create_users'),
|
||||
require('../migrations/002_create_conversations'),
|
||||
require('../migrations/003_create_plans'),
|
||||
require('../migrations/004_create_messages'),
|
||||
require('../migrations/005_create_documents'),
|
||||
require('../migrations/006_create_feedback'),
|
||||
require('../migrations/007_create_model_versions'),
|
||||
require('../migrations/008_create_training_data'),
|
||||
require('../migrations/009_create_tool_executions')
|
||||
];
|
||||
|
||||
for (const migration of migrations) {
|
||||
console.log(` Running migration: ${migration.up.name || 'unnamed'}`);
|
||||
await migration.up(sequelize.getQueryInterface(), sequelize.constructor);
|
||||
}
|
||||
|
||||
console.log('✅ All migrations completed successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
logger.error('Migration error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const runSeeders = async () => {
|
||||
try {
|
||||
console.log('🌱 Running database seeders...');
|
||||
|
||||
// Run seeders
|
||||
await createAdminUser.up(sequelize.getQueryInterface(), sequelize.constructor);
|
||||
await createSampleTrainingData.up(sequelize.getQueryInterface(), sequelize.constructor);
|
||||
|
||||
console.log('✅ All seeders completed successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Seeding failed:', error);
|
||||
logger.error('Seeding error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setupDatabase = async (options = {}) => {
|
||||
try {
|
||||
console.log('🚀 Setting up database...');
|
||||
|
||||
const {
|
||||
force = false,
|
||||
seed = true,
|
||||
migrations = true
|
||||
} = options;
|
||||
|
||||
// Check database status
|
||||
const status = await checkDatabaseStatus();
|
||||
if (!status) {
|
||||
throw new Error('Database connection failed');
|
||||
}
|
||||
|
||||
// Run migrations if requested
|
||||
if (migrations) {
|
||||
const migrated = await runMigrations();
|
||||
if (!migrated) {
|
||||
throw new Error('Migrations failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Run seeders if requested
|
||||
if (seed) {
|
||||
const seeded = await runSeeders();
|
||||
if (!seeded) {
|
||||
throw new Error('Seeding failed');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 Database setup completed successfully!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database setup failed:', error.message);
|
||||
logger.error('Database setup error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseInfo = async () => {
|
||||
try {
|
||||
console.log('📊 Database Information:');
|
||||
|
||||
// Get table information
|
||||
const [results] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
tableowner
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename;
|
||||
`);
|
||||
|
||||
console.log(` Tables: ${results.length}`);
|
||||
results.forEach(table => {
|
||||
console.log(` - ${table.tablename}`);
|
||||
});
|
||||
|
||||
// Get database size
|
||||
const [sizeResult] = await sequelize.query(`
|
||||
SELECT pg_size_pretty(pg_database_size(current_database())) as size;
|
||||
`);
|
||||
|
||||
console.log(` Database size: ${sizeResult[0].size}`);
|
||||
|
||||
return {
|
||||
tables: results,
|
||||
size: sizeResult[0].size
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting database info:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const backupDatabase = async (backupPath) => {
|
||||
try {
|
||||
console.log('💾 Creating database backup...');
|
||||
|
||||
// This would typically use pg_dump in a real implementation
|
||||
console.log(` Backup path: ${backupPath}`);
|
||||
console.log(' Note: Implement actual backup logic with pg_dump');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Backup failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Command line interface
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'help';
|
||||
|
||||
const runCommand = async () => {
|
||||
switch (command) {
|
||||
case 'init':
|
||||
await setupDatabase({ force: false, seed: true, migrations: true });
|
||||
break;
|
||||
case 'reset':
|
||||
await resetDatabase();
|
||||
break;
|
||||
case 'migrate':
|
||||
await runMigrations();
|
||||
break;
|
||||
case 'seed':
|
||||
await runSeeders();
|
||||
break;
|
||||
case 'status':
|
||||
await checkDatabaseStatus();
|
||||
break;
|
||||
case 'info':
|
||||
await getDatabaseInfo();
|
||||
break;
|
||||
case 'backup':
|
||||
const backupPath = args[1] || './backup.sql';
|
||||
await backupDatabase(backupPath);
|
||||
break;
|
||||
case 'help':
|
||||
default:
|
||||
console.log(`
|
||||
Database Manager Commands:
|
||||
init - Initialize database with migrations and seeders
|
||||
reset - Reset database (drop and recreate)
|
||||
migrate - Run migrations only
|
||||
seed - Run seeders only
|
||||
status - Check database connection status
|
||||
info - Show database information
|
||||
backup - Create database backup
|
||||
help - Show this help message
|
||||
|
||||
Usage: node databaseManager.js [command]
|
||||
`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
runCommand().then(() => {
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('Command failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runMigrations,
|
||||
runSeeders,
|
||||
setupDatabase,
|
||||
getDatabaseInfo,
|
||||
backupDatabase
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
const logsDir = path.join(__dirname, '../../logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: 'reason-flow' },
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'error.log'),
|
||||
level: 'error'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'combined.log')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Add console transport for development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
@@ -0,0 +1,134 @@
|
||||
const groqService = require('../services/groqService');
|
||||
const model1Service = require('../services/model1Service');
|
||||
const queryModelService = require('../services/queryModelService');
|
||||
const logger = require('./logger');
|
||||
|
||||
const testGroqConnection = async () => {
|
||||
console.log('🧪 Testing Groq API Connection...');
|
||||
|
||||
try {
|
||||
const connectionTest = await groqService.testConnection();
|
||||
|
||||
if (connectionTest.success) {
|
||||
console.log('✅ Groq API connection successful');
|
||||
console.log(`Model: ${connectionTest.model}`);
|
||||
console.log(`Response: ${connectionTest.response.content.substring(0, 100)}...`);
|
||||
} else {
|
||||
console.log('❌ Groq API connection failed');
|
||||
console.log(`Error: ${connectionTest.error}`);
|
||||
}
|
||||
|
||||
return connectionTest;
|
||||
} catch (error) {
|
||||
console.log('❌ Groq API test failed:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const testModel1 = async () => {
|
||||
console.log('\n🧪 Testing MODEL1 (Engineering Plan Generation)...');
|
||||
|
||||
try {
|
||||
const testQuery = "How do I design a bridge that can handle heavy traffic loads?";
|
||||
const planData = await model1Service.generatePlan(testQuery, {
|
||||
context: { test: true }
|
||||
});
|
||||
|
||||
console.log('✅ MODEL1 plan generation successful');
|
||||
console.log(`Title: ${planData.title}`);
|
||||
console.log(`Description: ${planData.description.substring(0, 100)}...`);
|
||||
console.log(`Steps: ${planData.steps.length}`);
|
||||
console.log(`Tools Required: ${planData.toolsRequired.length}`);
|
||||
console.log(`Processing Time: ${planData.processingTime}s`);
|
||||
console.log(`Tokens Used: ${planData.tokensUsed}`);
|
||||
|
||||
return planData;
|
||||
} catch (error) {
|
||||
console.log('❌ MODEL1 test failed:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const testQueryModel = async () => {
|
||||
console.log('\n🧪 Testing QUERYMODEL (Plan Execution)...');
|
||||
|
||||
try {
|
||||
const testPlan = "Execute the following engineering plan: Design a bridge for heavy traffic loads. Steps: 1. Calculate load requirements 2. Design structural elements 3. Check safety factors";
|
||||
const executionResult = await queryModelService.executePlan(testPlan, {
|
||||
test: true
|
||||
});
|
||||
|
||||
console.log('✅ QUERYMODEL execution successful');
|
||||
console.log(`Execution Status: ${executionResult.executionResults.executionStatus}`);
|
||||
console.log(`Steps Completed: ${executionResult.executionResults.stepsCompleted.length}`);
|
||||
console.log(`Results: ${executionResult.executionResults.results.length}`);
|
||||
console.log(`Processing Time: ${executionResult.processingTime}s`);
|
||||
console.log(`Tokens Used: ${executionResult.tokensUsed}`);
|
||||
|
||||
return executionResult;
|
||||
} catch (error) {
|
||||
console.log('❌ QUERYMODEL test failed:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const testToolExecution = async () => {
|
||||
console.log('\n🧪 Testing Tool Execution...');
|
||||
|
||||
try {
|
||||
const toolResult = await queryModelService.executeTool(
|
||||
'query_expander',
|
||||
'query_expander',
|
||||
{ query: 'bridge design', context: { test: true } },
|
||||
'test-plan-id'
|
||||
);
|
||||
|
||||
console.log('✅ Tool execution successful');
|
||||
console.log(`Tool: ${toolResult.tool_name}`);
|
||||
console.log(`Status: ${toolResult.status}`);
|
||||
console.log(`Execution Time: ${toolResult.execution_time}s`);
|
||||
|
||||
return toolResult;
|
||||
} catch (error) {
|
||||
console.log('❌ Tool execution test failed:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
const runAllTests = async () => {
|
||||
console.log('🚀 Starting Groq Integration Tests...\n');
|
||||
|
||||
const results = {
|
||||
connection: await testGroqConnection(),
|
||||
model1: await testModel1(),
|
||||
queryModel: await testQueryModel(),
|
||||
toolExecution: await testToolExecution()
|
||||
};
|
||||
|
||||
console.log('\n📊 Test Results Summary:');
|
||||
console.log(`Connection: ${results.connection.success ? '✅' : '❌'}`);
|
||||
console.log(`MODEL1: ${results.model1.success !== false ? '✅' : '❌'}`);
|
||||
console.log(`QUERYMODEL: ${results.queryModel.success !== false ? '✅' : '❌'}`);
|
||||
console.log(`Tool Execution: ${results.toolExecution.success !== false ? '✅' : '❌'}`);
|
||||
|
||||
const allPassed = Object.values(results).every(result =>
|
||||
result.success !== false
|
||||
);
|
||||
|
||||
console.log(`\n${allPassed ? '🎉 All tests passed!' : '⚠️ Some tests failed'}`);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Run tests if this file is executed directly
|
||||
if (require.main === module) {
|
||||
runAllTests().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testGroqConnection,
|
||||
testModel1,
|
||||
testQueryModel,
|
||||
testToolExecution,
|
||||
runAllTests
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
const configLoader = require('./configLoader');
|
||||
const logger = require('./logger');
|
||||
|
||||
const validateConfiguration = () => {
|
||||
try {
|
||||
console.log('🔍 Validating Reason Flow Configuration...\n');
|
||||
|
||||
const config = configLoader.getAll();
|
||||
const env = configLoader.getEnvironment();
|
||||
|
||||
console.log(`Environment: ${env}`);
|
||||
console.log(`Server: ${config.server.host}:${config.server.port}`);
|
||||
console.log(`Database: ${config.database.host}:${config.database.port}/${config.database.name}`);
|
||||
console.log(`CORS Origin: ${config.server.corsOrigin}\n`);
|
||||
|
||||
// Check required configurations
|
||||
const checks = [
|
||||
{
|
||||
name: 'Groq API Key',
|
||||
value: config.apis.groq.apiKey,
|
||||
required: true,
|
||||
valid: config.apis.groq.apiKey && config.apis.groq.apiKey !== 'your_groq_api_key_here'
|
||||
},
|
||||
{
|
||||
name: 'JWT Secret',
|
||||
value: config.auth.jwtSecret,
|
||||
required: true,
|
||||
valid: config.auth.jwtSecret && config.auth.jwtSecret !== 'your_jwt_secret_here_make_it_long_and_secure'
|
||||
},
|
||||
{
|
||||
name: 'Database Password',
|
||||
value: config.database.password,
|
||||
required: true,
|
||||
valid: config.database.password && config.database.password !== 'your_password_here'
|
||||
},
|
||||
{
|
||||
name: 'OpenAI API Key',
|
||||
value: config.apis.openai.apiKey,
|
||||
required: false,
|
||||
valid: config.apis.openai.apiKey && config.apis.openai.apiKey !== 'your_openai_api_key_here'
|
||||
},
|
||||
{
|
||||
name: 'SERP API Key',
|
||||
value: config.apis.serp.apiKey,
|
||||
required: false,
|
||||
valid: config.apis.serp.apiKey && config.apis.serp.apiKey !== 'your_serp_api_key_here'
|
||||
}
|
||||
];
|
||||
|
||||
console.log('Configuration Checks:');
|
||||
console.log('====================');
|
||||
|
||||
let allValid = true;
|
||||
let hasWarnings = false;
|
||||
|
||||
checks.forEach(check => {
|
||||
const status = check.valid ? '✅' : (check.required ? '❌' : '⚠️');
|
||||
const required = check.required ? '(Required)' : '(Optional)';
|
||||
|
||||
console.log(`${status} ${check.name} ${required}`);
|
||||
|
||||
if (!check.valid && check.required) {
|
||||
allValid = false;
|
||||
} else if (!check.valid && !check.required) {
|
||||
hasWarnings = true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n');
|
||||
|
||||
// Environment-specific checks
|
||||
if (env === 'production') {
|
||||
console.log('Production Environment Checks:');
|
||||
console.log('==============================');
|
||||
|
||||
const prodChecks = [
|
||||
{
|
||||
name: 'JWT Secret Security',
|
||||
valid: config.auth.jwtSecret !== 'dev_secret_key_change_in_production',
|
||||
message: 'JWT secret should be changed for production'
|
||||
},
|
||||
{
|
||||
name: 'Database SSL',
|
||||
valid: config.database.ssl,
|
||||
message: 'Database SSL is recommended for production'
|
||||
},
|
||||
{
|
||||
name: 'Debug Mode',
|
||||
valid: !config.development.debugMode,
|
||||
message: 'Debug mode should be disabled in production'
|
||||
},
|
||||
{
|
||||
name: 'Verbose Logging',
|
||||
valid: !config.development.verboseLogging,
|
||||
message: 'Verbose logging should be disabled in production'
|
||||
}
|
||||
];
|
||||
|
||||
prodChecks.forEach(check => {
|
||||
const status = check.valid ? '✅' : '⚠️';
|
||||
console.log(`${status} ${check.name}: ${check.message}`);
|
||||
|
||||
if (!check.valid) {
|
||||
hasWarnings = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
|
||||
// Summary
|
||||
if (allValid) {
|
||||
console.log('🎉 Configuration validation passed!');
|
||||
|
||||
if (hasWarnings) {
|
||||
console.log('⚠️ Some optional configurations are missing or need attention.');
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Configuration validation failed!');
|
||||
console.log('Please fix the required configuration issues before starting the application.');
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Configuration validation error:', error.message);
|
||||
logger.error('Configuration validation error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const showConfiguration = () => {
|
||||
try {
|
||||
console.log('📋 Current Configuration:');
|
||||
console.log('========================\n');
|
||||
|
||||
const config = configLoader.getAll();
|
||||
|
||||
console.log('Server Configuration:');
|
||||
console.log(` Port: ${config.server.port}`);
|
||||
console.log(` Host: ${config.server.host}`);
|
||||
console.log(` Environment: ${config.server.env}`);
|
||||
console.log(` CORS Origin: ${config.server.corsOrigin}\n`);
|
||||
|
||||
console.log('Database Configuration:');
|
||||
console.log(` Host: ${config.database.host}`);
|
||||
console.log(` Port: ${config.database.port}`);
|
||||
console.log(` Name: ${config.database.name}`);
|
||||
console.log(` User: ${config.database.user}`);
|
||||
console.log(` SSL: ${config.database.ssl}\n`);
|
||||
|
||||
console.log('API Configuration:');
|
||||
console.log(` Groq Model: ${config.apis.groq.model}`);
|
||||
console.log(` Groq API Key: ${config.apis.groq.apiKey ? 'Set' : 'Not Set'}`);
|
||||
console.log(` OpenAI API Key: ${config.apis.openai.apiKey ? 'Set' : 'Not Set'}`);
|
||||
console.log(` SERP API Key: ${config.apis.serp.apiKey ? 'Set' : 'Not Set'}\n`);
|
||||
|
||||
console.log('Security Configuration:');
|
||||
console.log(` JWT Secret: ${config.auth.jwtSecret ? 'Set' : 'Not Set'}`);
|
||||
console.log(` JWT Expires In: ${config.auth.jwtExpiresIn}`);
|
||||
console.log(` Helmet Enabled: ${config.security.helmetEnabled}\n`);
|
||||
|
||||
console.log('Development Configuration:');
|
||||
console.log(` Debug Mode: ${config.development.debugMode}`);
|
||||
console.log(` Verbose Logging: ${config.development.verboseLogging}`);
|
||||
console.log(` Hot Reload: ${config.development.hotReload}\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error showing configuration:', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Run validation if this file is executed directly
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'validate';
|
||||
|
||||
switch (command) {
|
||||
case 'validate':
|
||||
const isValid = validateConfiguration();
|
||||
process.exit(isValid ? 0 : 1);
|
||||
break;
|
||||
case 'show':
|
||||
showConfiguration();
|
||||
break;
|
||||
default:
|
||||
console.log('Usage: node validateConfig.js [validate|show]');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateConfiguration,
|
||||
showConfiguration
|
||||
};
|
||||
Reference in New Issue
Block a user