first commit
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Reason Flow - AI Engineering Reasoning System
|
||||
|
||||
An intelligent AI system for complex engineering reasoning with continuous learning capabilities.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **MODEL1**: Engineering reasoning model that creates step-by-step plans
|
||||
- **QUERYMODEL**: Execution model that uses tools to carry out plans
|
||||
- **RAG Pipeline**: Document search and retrieval system
|
||||
- **6 Specialized Tools**: Query expansion, extraction, reports, web search, PDF search
|
||||
- **Feedback Loop**: Continuous learning system
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Express.js, PostgreSQL, Sequelize
|
||||
- **Frontend**: React, Socket.io
|
||||
- **AI Models**: Kimi K2 (Groq), OpenAI
|
||||
- **RAG**: LangChain, Vector Database
|
||||
- **Fine-tuning**: SFT, DPO, Correction Memory
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm run install-all
|
||||
```
|
||||
|
||||
2. Set up environment variables:
|
||||
```bash
|
||||
cp env.example .env
|
||||
# Edit .env with your API keys and database credentials
|
||||
```
|
||||
|
||||
3. Set up database:
|
||||
```bash
|
||||
# Create PostgreSQL database
|
||||
createdb reason_flow
|
||||
```
|
||||
|
||||
4. Start development servers:
|
||||
```bash
|
||||
npm run dev-full
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
reason_flow/
|
||||
├── server/ # Express.js backend
|
||||
│ ├── controllers/ # Route controllers
|
||||
│ ├── models/ # Database models
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── middleware/ # Custom middleware
|
||||
│ └── utils/ # Utility functions
|
||||
├── client/ # React frontend
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── services/ # API services
|
||||
│ │ └── utils/ # Utility functions
|
||||
├── data/ # Training data and documents
|
||||
├── tests/ # Test files
|
||||
└── uploads/ # File uploads
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Intelligent Planning**: AI generates step-by-step engineering plans
|
||||
- **Tool Execution**: Automated execution using specialized tools
|
||||
- **Document Search**: RAG-powered document retrieval
|
||||
- **Continuous Learning**: Feedback loop for model improvement
|
||||
- **Real-time Chat**: WebSocket-based communication
|
||||
- **File Upload**: Document processing and indexing
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `/api/health` - Health check
|
||||
- `/api/auth/*` - Authentication
|
||||
- `/api/chat/*` - Chat functionality
|
||||
- `/api/models/*` - Model management
|
||||
- `/api/documents/*` - Document management
|
||||
- `/api/feedback/*` - Feedback collection
|
||||
- `/api/tools/*` - Tool execution
|
||||
|
||||
## Development
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Build for production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,204 @@
|
||||
# Reason Flow Setup Guide
|
||||
|
||||
This guide will help you set up the Reason Flow AI engineering reasoning system.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v16 or higher)
|
||||
- PostgreSQL (v12 or higher)
|
||||
- Git
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Clone and Install Dependencies
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd reason_flow
|
||||
npm run install-all
|
||||
```
|
||||
|
||||
### 2. Environment Configuration
|
||||
|
||||
#### Option A: Interactive Setup (Recommended)
|
||||
```bash
|
||||
npm run env:setup
|
||||
```
|
||||
|
||||
#### Option B: Manual Setup
|
||||
```bash
|
||||
cp env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
### 3. Database Setup
|
||||
|
||||
```bash
|
||||
npm run db:setup
|
||||
```
|
||||
|
||||
### 4. Validate Configuration
|
||||
|
||||
```bash
|
||||
npm run config:validate
|
||||
```
|
||||
|
||||
### 5. Start Development Server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Detailed Setup
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The system uses environment variables for configuration. Key variables include:
|
||||
|
||||
#### Required Variables
|
||||
- `GROQ_API_KEY` - Your Groq API key for Kimi K2 model
|
||||
- `DB_PASSWORD` - PostgreSQL database password
|
||||
- `JWT_SECRET` - Secret key for JWT tokens
|
||||
|
||||
#### Optional Variables
|
||||
- `OPENAI_API_KEY` - For additional AI capabilities
|
||||
- `SERP_API_KEY` - For web search functionality
|
||||
- `REDIS_URL` - For caching (optional)
|
||||
|
||||
### Database Configuration
|
||||
|
||||
The system supports multiple environments:
|
||||
|
||||
- **Development**: `reason_flow_dev`
|
||||
- **Test**: `reason_flow_test`
|
||||
- **Production**: `reason_flow_prod`
|
||||
|
||||
### API Keys Setup
|
||||
|
||||
1. **Groq API Key**: Get from [Groq Console](https://console.groq.com/)
|
||||
2. **OpenAI API Key**: Get from [OpenAI Platform](https://platform.openai.com/)
|
||||
3. **SERP API Key**: Get from [SERP API](https://serpapi.com/)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### Environment Management
|
||||
- `npm run env:setup` - Interactive environment setup
|
||||
- `npm run env:validate` - Validate environment configuration
|
||||
- `npm run env:show` - Show current configuration
|
||||
|
||||
### Database Management
|
||||
- `npm run db:setup` - Complete database setup
|
||||
- `npm run db:init` - Initialize database
|
||||
- `npm run db:reset` - Reset database
|
||||
- `npm run db:migrate` - Run migrations
|
||||
- `npm run db:seed` - Run seeders
|
||||
- `npm run db:status` - Check database status
|
||||
- `npm run db:info` - Show database information
|
||||
|
||||
### Development
|
||||
- `npm run dev` - Start development server
|
||||
- `npm run dev-full` - Start both backend and frontend
|
||||
- `npm run test:groq` - Test Groq API integration
|
||||
|
||||
### Configuration
|
||||
- `npm run config:validate` - Validate all configuration
|
||||
- `npm run config:show` - Show current configuration
|
||||
|
||||
## Environment-Specific Configuration
|
||||
|
||||
### Development
|
||||
- Debug mode enabled
|
||||
- Verbose logging
|
||||
- Relaxed rate limiting
|
||||
- Hot reload enabled
|
||||
|
||||
### Production
|
||||
- Debug mode disabled
|
||||
- Optimized logging
|
||||
- Strict rate limiting
|
||||
- SSL recommended
|
||||
- Security headers enabled
|
||||
|
||||
### Test
|
||||
- Minimal logging
|
||||
- Test database
|
||||
- Mock external services
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Database Connection Failed**
|
||||
- Check PostgreSQL is running
|
||||
- Verify database credentials
|
||||
- Ensure database exists
|
||||
|
||||
2. **Groq API Errors**
|
||||
- Verify API key is correct
|
||||
- Check API quota and limits
|
||||
- Test connection: `npm run test:groq`
|
||||
|
||||
3. **Configuration Errors**
|
||||
- Run: `npm run config:validate`
|
||||
- Check required variables are set
|
||||
- Verify environment file exists
|
||||
|
||||
### Logs
|
||||
|
||||
- Application logs: `./logs/app.log`
|
||||
- Error logs: `./logs/error.log`
|
||||
- Combined logs: `./logs/combined.log`
|
||||
|
||||
### Health Checks
|
||||
|
||||
- API Health: `GET /api/health`
|
||||
- Model Status: `GET /api/models/status`
|
||||
- Database Status: `npm run db:status`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Production Deployment
|
||||
|
||||
1. **Change Default Secrets**
|
||||
- Update JWT_SECRET
|
||||
- Change admin password
|
||||
- Use strong database passwords
|
||||
|
||||
2. **Enable SSL**
|
||||
- Set DB_SSL=true
|
||||
- Use HTTPS in production
|
||||
- Configure proper CORS origins
|
||||
|
||||
3. **Environment Variables**
|
||||
- Never commit .env files
|
||||
- Use secure secret management
|
||||
- Rotate keys regularly
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Endpoints
|
||||
- `/api/health` - Basic health check
|
||||
- `/api/models/status` - Model status
|
||||
- `/api/feedback/stats` - Feedback statistics
|
||||
|
||||
### Logging
|
||||
- Structured JSON logging
|
||||
- Log rotation configured
|
||||
- Error tracking enabled
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check the logs for errors
|
||||
2. Validate configuration
|
||||
3. Test individual components
|
||||
4. Review the troubleshooting section
|
||||
|
||||
## Next Steps
|
||||
|
||||
After setup:
|
||||
1. Test the API endpoints
|
||||
2. Upload sample documents
|
||||
3. Create test conversations
|
||||
4. Verify model responses
|
||||
5. Set up monitoring
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "reason-flow-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"axios": "^1.6.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-syntax-highlighter": "^5.8.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"styled-components": "^6.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:8000"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Reason Flow - Advanced Engineering Reasoning System"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Reason Flow - Engineering AI</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Reason Flow",
|
||||
"name": "Reason Flow - Engineering AI",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3b82f6",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import ChatPage from './pages/ChatPage';
|
||||
import DocumentsPage from './pages/DocumentsPage';
|
||||
import ToolsPage from './pages/ToolsPage';
|
||||
import AdminPage from './pages/AdminPage';
|
||||
import Layout from './components/Layout';
|
||||
|
||||
function ProtectedRoute({ children }) {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '2rem',
|
||||
height: '2rem',
|
||||
border: '3px solid #e2e8f0',
|
||||
borderTop: '3px solid #3b82f6',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite'
|
||||
}}></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <Layout>{children}</Layout>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/dashboard" element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/chat" element={
|
||||
<ProtectedRoute>
|
||||
<ChatPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/documents" element={
|
||||
<ProtectedRoute>
|
||||
<DocumentsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/tools" element={
|
||||
<ProtectedRoute>
|
||||
<ToolsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/admin" element={
|
||||
<ProtectedRoute>
|
||||
<AdminPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,283 @@
|
||||
/* Layout Styles */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
left: -280px;
|
||||
transition: left 0.3s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #60a5fa, #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: #334155;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #cbd5e1;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #334155;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.125rem;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #b91c1c;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Top Bar */
|
||||
.topbar {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-toggle:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Page Content */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Mobile Overlay */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.menu-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './Layout.css';
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const navigation = [
|
||||
{ path: '/dashboard', label: 'Dashboard', icon: '🏠' },
|
||||
{ path: '/chat', label: 'Engineering Chat', icon: '💬' },
|
||||
{ path: '/documents', label: 'Knowledge Base', icon: '📚' },
|
||||
{ path: '/tools', label: 'Tool Execution', icon: '🔧' },
|
||||
...(user?.role === 'admin' ? [{ path: '/admin', label: 'Admin Panel', icon: '⚙️' }] : [])
|
||||
];
|
||||
|
||||
const isActive = (path) => location.pathname === path;
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
{/* Sidebar */}
|
||||
<aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
|
||||
<div className="sidebar-header">
|
||||
<div className="logo">
|
||||
<span className="logo-icon">🧠</span>
|
||||
<span className="logo-text">Reason Flow</span>
|
||||
</div>
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{navigation.map((item) => (
|
||||
<button
|
||||
key={item.path}
|
||||
className={`nav-item ${isActive(item.path) ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
navigate(item.path);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="nav-icon">{item.icon}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<div className="user-info">
|
||||
<div className="user-avatar">
|
||||
{user?.firstName?.[0]}{user?.lastName?.[0]}
|
||||
</div>
|
||||
<div className="user-details">
|
||||
<div className="user-name">{user?.firstName} {user?.lastName}</div>
|
||||
<div className="user-role">{user?.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="logout-btn" onClick={handleLogout}>
|
||||
🚪 Logout
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="main-content">
|
||||
{/* Top Bar */}
|
||||
<header className="topbar">
|
||||
<button
|
||||
className="menu-toggle"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<div className="topbar-title">
|
||||
{navigation.find(item => item.path === location.pathname)?.label || 'Reason Flow'}
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<div className="status-indicator">
|
||||
<span className="status-dot"></span>
|
||||
System Online
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="page-content">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile Overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="mobile-overlay"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
// Verify token and get user info
|
||||
axios.get('/api/auth/profile')
|
||||
.then(response => {
|
||||
setUser(response.data.data.user);
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem('token');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', { email, password });
|
||||
const { token, user } = response.data.data;
|
||||
|
||||
localStorage.setItem('token', token);
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
setUser(user);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || 'Login failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (userData) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/register', userData);
|
||||
const { token, user } = response.data.data;
|
||||
|
||||
localStorage.setItem('token', token);
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
setUser(user);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || 'Registration failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
loading
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f8fafc;
|
||||
color: #1e293b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* Reset and base styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
*:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,515 @@
|
||||
/* Admin Page Styles */
|
||||
.admin-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Admin Section */
|
||||
.admin-section {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Model Status */
|
||||
.model-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.model-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.model-header h3 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-indicator.unknown {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.model-card p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.model-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Feedback Management */
|
||||
.feedback-overview {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feedback-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.stat-value.positive {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-value.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-value.correction {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-value.suggestion {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Recent Feedback */
|
||||
.recent-feedback h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feedback-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feedback-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feedback-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.feedback-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feedback-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.type-badge.positive {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.type-badge.negative {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.type-badge.correction {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.type-badge.suggestion {
|
||||
background: #e9d5ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.feedback-rating {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.feedback-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.process-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.process-btn:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.process-btn:disabled {
|
||||
background: #10b981;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.feedback-comment,
|
||||
.feedback-correction {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feedback-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* System Actions */
|
||||
.system-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.action-group h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: #e2e8f0;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.model-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.system-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.feedback-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import './AdminPage.css';
|
||||
|
||||
const AdminPage = () => {
|
||||
const [systemStats, setSystemStats] = useState({
|
||||
users: 0,
|
||||
conversations: 0,
|
||||
documents: 0,
|
||||
feedback: 0,
|
||||
toolExecutions: 0
|
||||
});
|
||||
const [modelStatus, setModelStatus] = useState({});
|
||||
const [feedbackStats, setFeedbackStats] = useState({});
|
||||
const [recentFeedback, setRecentFeedback] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadAdminData();
|
||||
}, []);
|
||||
|
||||
const loadAdminData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load system statistics
|
||||
const [conversationsRes, documentsRes, feedbackRes, toolsRes, modelRes] = await Promise.all([
|
||||
axios.get('/api/chat/conversations?limit=1'),
|
||||
axios.get('/api/documents?limit=1'),
|
||||
axios.get('/api/feedback/stats'),
|
||||
axios.get('/api/tools/executions?limit=1'),
|
||||
axios.get('/api/models/status')
|
||||
]);
|
||||
|
||||
setSystemStats({
|
||||
users: 0, // Would need a users endpoint
|
||||
conversations: conversationsRes.data.data.pagination?.total || 0,
|
||||
documents: documentsRes.data.data.pagination?.total || documentsRes.data.data.documents?.length || 0,
|
||||
feedback: feedbackRes.data.data.total || 0,
|
||||
toolExecutions: toolsRes.data.data.pagination?.total || toolsRes.data.data.executions?.length || 0
|
||||
});
|
||||
|
||||
setModelStatus(modelRes.data.data);
|
||||
setFeedbackStats(feedbackRes.data.data);
|
||||
|
||||
// Load recent feedback
|
||||
const feedbackListRes = await axios.get('/api/feedback?limit=5');
|
||||
setRecentFeedback(feedbackListRes.data.data.feedback || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading admin data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processFeedback = async (feedbackId) => {
|
||||
try {
|
||||
await axios.put(`/api/feedback/${feedbackId}/process`);
|
||||
setMessage('✅ Feedback processed successfully');
|
||||
loadAdminData();
|
||||
} catch (error) {
|
||||
console.error('Error processing feedback:', error);
|
||||
setMessage(`❌ Failed to process feedback: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading admin dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="page-header">
|
||||
<h1>⚙️ Admin Panel</h1>
|
||||
<p>System administration and monitoring</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('✅') ? 'success' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Overview */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>📊 System Overview</h2>
|
||||
<p>Key system metrics and statistics</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">👥</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.users}</div>
|
||||
<div className="stat-label">Total Users</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">💬</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.conversations}</div>
|
||||
<div className="stat-label">Conversations</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📚</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.documents}</div>
|
||||
<div className="stat-label">Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">⭐</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.feedback}</div>
|
||||
<div className="stat-label">Feedback Items</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">🔧</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.toolExecutions}</div>
|
||||
<div className="stat-label">Tool Executions</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Status */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>🤖 Model Status</h2>
|
||||
<p>AI model health and performance</p>
|
||||
</div>
|
||||
|
||||
<div className="model-status-grid">
|
||||
<div className="model-card">
|
||||
<div className="model-header">
|
||||
<h3>🧠 MODEL1 (Planner)</h3>
|
||||
<span className={`status-indicator ${modelStatus.model1 || 'unknown'}`}>
|
||||
{modelStatus.model1 || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<p>Engineering plan generation and reasoning</p>
|
||||
<div className="model-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Status:</span>
|
||||
<span className="metric-value">{modelStatus.model1 || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="model-card">
|
||||
<div className="model-header">
|
||||
<h3>⚡ QUERYMODEL (Executor)</h3>
|
||||
<span className={`status-indicator ${modelStatus.queryModel || 'unknown'}`}>
|
||||
{modelStatus.queryModel || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<p>Plan execution and tool orchestration</p>
|
||||
<div className="model-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Status:</span>
|
||||
<span className="metric-value">{modelStatus.queryModel || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="model-card">
|
||||
<div className="model-header">
|
||||
<h3>🔍 RAG System</h3>
|
||||
<span className={`status-indicator ${modelStatus.rag || 'unknown'}`}>
|
||||
{modelStatus.rag || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<p>Document search and knowledge retrieval</p>
|
||||
<div className="model-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Status:</span>
|
||||
<span className="metric-value">{modelStatus.rag || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feedback Management */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>💬 Feedback Management</h2>
|
||||
<p>User feedback and system improvements</p>
|
||||
</div>
|
||||
|
||||
<div className="feedback-overview">
|
||||
<div className="feedback-stats">
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Total Feedback:</span>
|
||||
<span className="stat-value">{feedbackStats.total || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Positive:</span>
|
||||
<span className="stat-value positive">{feedbackStats.positive || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Negative:</span>
|
||||
<span className="stat-value negative">{feedbackStats.negative || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Corrections:</span>
|
||||
<span className="stat-value correction">{feedbackStats.correction || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Suggestions:</span>
|
||||
<span className="stat-value suggestion">{feedbackStats.suggestion || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{recentFeedback.length > 0 && (
|
||||
<div className="recent-feedback">
|
||||
<h3>Recent Feedback</h3>
|
||||
<div className="feedback-list">
|
||||
{recentFeedback.map((feedback) => (
|
||||
<div key={feedback.id} className="feedback-item">
|
||||
<div className="feedback-header">
|
||||
<div className="feedback-type">
|
||||
<span className={`type-badge ${feedback.feedback_type}`}>
|
||||
{feedback.feedback_type}
|
||||
</span>
|
||||
<span className="feedback-rating">
|
||||
{feedback.rating ? `⭐ ${feedback.rating}` : 'No rating'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="feedback-actions">
|
||||
<button
|
||||
className="process-btn"
|
||||
onClick={() => processFeedback(feedback.id)}
|
||||
disabled={feedback.is_processed}
|
||||
>
|
||||
{feedback.is_processed ? '✅ Processed' : '🔄 Process'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{feedback.comment && (
|
||||
<div className="feedback-comment">
|
||||
<strong>Comment:</strong> {feedback.comment}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feedback.corrected_content && (
|
||||
<div className="feedback-correction">
|
||||
<strong>Correction:</strong> {feedback.corrected_content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="feedback-meta">
|
||||
<span>ID: {feedback.id}</span>
|
||||
<span>Date: {new Date(feedback.created_at).toLocaleDateString()}</span>
|
||||
<span>Status: {feedback.is_processed ? 'Processed' : 'Pending'}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Actions */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>🛠️ System Actions</h2>
|
||||
<p>Administrative operations and maintenance</p>
|
||||
</div>
|
||||
|
||||
<div className="system-actions">
|
||||
<div className="action-group">
|
||||
<h3>Data Management</h3>
|
||||
<div className="action-buttons">
|
||||
<button className="action-btn primary">
|
||||
📊 Export Data
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🗑️ Cleanup Old Data
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🔄 Refresh Cache
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-group">
|
||||
<h3>Model Management</h3>
|
||||
<div className="action-buttons">
|
||||
<button className="action-btn primary">
|
||||
🚀 Retrain Models
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
📈 Update Metrics
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🔧 Test Models
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-group">
|
||||
<h3>System Maintenance</h3>
|
||||
<div className="action-buttons">
|
||||
<button className="action-btn primary">
|
||||
🔄 Restart Services
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
📋 Generate Report
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🔍 Run Diagnostics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
@@ -0,0 +1,631 @@
|
||||
/* Chat Page Styles */
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Chat Header */
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: linear-gradient(135deg, #1e293b, #334155);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-title h2 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chat-title p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Pending Plans */
|
||||
.pending-plans {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pending-plans h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.plans-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.plan-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.plan-status {
|
||||
background: #fbbf24;
|
||||
color: #92400e;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.plan-content pre {
|
||||
background: #f1f5f9;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #475569;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.plan-actions .btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.plan-actions .btn.approve {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-actions .btn.reject {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-actions .btn.execute {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-actions .btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Messages Container */
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-role {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.feedback-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feedback-btn:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
background: #f8fafc;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.message.user .message-text {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.message-text pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Message type specific styles */
|
||||
.message[data-message-type="system_prompt"] .message-content {
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #0ea5e9;
|
||||
}
|
||||
|
||||
.message[data-message-type="system_prompt"] .message-role {
|
||||
color: #0369a1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message[data-message-type="plan"] .message-content {
|
||||
background: #fefce8;
|
||||
border-left: 4px solid #eab308;
|
||||
}
|
||||
|
||||
.message[data-message-type="plan"] .message-role {
|
||||
color: #a16207;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message[data-message-type="execution_result"] .message-content {
|
||||
background: #f0fdf4;
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
|
||||
.message[data-message-type="execution_result"] .message-role {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message[data-message-type="error"] .message-content {
|
||||
background: #fef2f2;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.message[data-message-type="error"] .message-role {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Plan Action Buttons */
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.approve-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reject-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.modify-btn {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modify-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
/* Typing Indicator */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
background: #64748b;
|
||||
border-radius: 50%;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Chat Input */
|
||||
.chat-input {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.input-container textarea {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
transition: all 0.2s;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.input-container textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 1.5rem;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-actions .btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-actions .btn.secondary {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.modal-actions .btn.primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-actions .btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
align-self: flex-end;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.plan-actions .btn {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.chat-title h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.chat-title p {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import './ChatPage.css';
|
||||
|
||||
const ChatPage = () => {
|
||||
const [conversation, setConversation] = useState(null);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pendingPlans, setPendingPlans] = useState([]);
|
||||
const [showFeedback, setShowFeedback] = useState(null);
|
||||
const [feedbackData, setFeedbackData] = useState({
|
||||
feedbackType: 'positive',
|
||||
rating: 5,
|
||||
comment: '',
|
||||
correctedContent: ''
|
||||
});
|
||||
|
||||
const messagesEndRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
createConversation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation) {
|
||||
loadConversationMessages();
|
||||
}
|
||||
}, [conversation]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const createConversation = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/chat/conversations', {
|
||||
title: 'New Engineering Discussion'
|
||||
});
|
||||
setConversation(response.data.data.conversation);
|
||||
} catch (error) {
|
||||
console.error('Error creating conversation:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadConversationMessages = async () => {
|
||||
if (!conversation) return;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/chat/conversations/${conversation.id}`);
|
||||
const conv = response.data.data.conversation;
|
||||
|
||||
// Load all messages from the conversation
|
||||
const conversationMessages = conv.messages || [];
|
||||
setMessages(conversationMessages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
timestamp: msg.created_at,
|
||||
message_type: msg.message_type,
|
||||
id: msg.id
|
||||
})));
|
||||
|
||||
// Load pending plans
|
||||
loadPendingPlans();
|
||||
} catch (error) {
|
||||
console.error('Error loading conversation messages:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.trim() || !conversation) return;
|
||||
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: inputMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
const currentInput = inputMessage.trim().toLowerCase();
|
||||
setInputMessage('');
|
||||
setLoading(true);
|
||||
|
||||
// Do not locally infer approve/reject. Always send message to backend and let routing/LLM decide.
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/chat/message', {
|
||||
conversationId: conversation.id,
|
||||
content: inputMessage
|
||||
});
|
||||
|
||||
const assistantMessage = response.data.data.assistantMessage;
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
|
||||
// Check if this is a plan message and show approval prompt
|
||||
if (assistantMessage.message_type === 'plan') {
|
||||
loadPendingPlans();
|
||||
|
||||
// Add a system message prompting for approval
|
||||
setTimeout(() => {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `📋 **Engineering Plan Generated!** Please review the comprehensive plan above and choose your action:`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system_prompt',
|
||||
plan_id: assistantMessage.plan_id // Store the plan ID for the buttons
|
||||
}]);
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: 'Sorry, I encountered an error. Please try again.',
|
||||
timestamp: new Date().toISOString()
|
||||
}]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlanApproval = async () => {
|
||||
if (pendingPlans.length === 0) {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '❌ No pending plans to approve. Please ask an engineering question first.',
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestPlan = pendingPlans[0];
|
||||
await approvePlan(latestPlan.id);
|
||||
};
|
||||
|
||||
const handlePlanRejection = async () => {
|
||||
if (pendingPlans.length === 0) {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '❌ No pending plans to reject. Please ask an engineering question first.',
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestPlan = pendingPlans[0];
|
||||
await rejectPlan(latestPlan.id);
|
||||
|
||||
// Ask for a new plan
|
||||
setTimeout(() => {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '🔄 **Plan Rejected!** Please provide more details or ask for a different approach to your engineering question.',
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system_prompt'
|
||||
}]);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const loadPendingPlans = async () => {
|
||||
if (!conversation) return;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/chat/conversations/${conversation.id}`);
|
||||
const conv = response.data.data.conversation;
|
||||
|
||||
const plans = conv.messages
|
||||
.filter(m => m.message_type === 'plan' && m.plan_id)
|
||||
.map(m => ({
|
||||
id: m.plan_id,
|
||||
content: m.content,
|
||||
status: m.plan?.status || 'pending_approval',
|
||||
title: m.plan?.title || 'Generated Plan',
|
||||
created_at: m.created_at
|
||||
}))
|
||||
.filter(p => p.status === 'pending_approval' || p.status === 'draft');
|
||||
|
||||
setPendingPlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error loading pending plans:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const approvePlan = async (planId) => {
|
||||
try {
|
||||
await axios.put(`/api/models/model1/plan/${planId}`, {
|
||||
status: 'approved',
|
||||
approvalFeedback: 'Plan approved for execution'
|
||||
});
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `✅ **Plan Approved!** The plan has been approved and is now executing with engineering tools...`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system'
|
||||
}]);
|
||||
|
||||
loadPendingPlans();
|
||||
|
||||
// Automatically execute the plan after approval
|
||||
setTimeout(() => {
|
||||
executePlan(planId);
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error approving plan:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Approval Failed:** ${error.response?.data?.error || error.message}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const rejectPlan = async (planId) => {
|
||||
try {
|
||||
await axios.put(`/api/models/model1/plan/${planId}`, {
|
||||
status: 'rejected',
|
||||
approvalFeedback: 'Plan rejected by user'
|
||||
});
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Plan Rejected!** The plan has been rejected. You can ask for a new plan or modify your request.`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system'
|
||||
}]);
|
||||
|
||||
loadPendingPlans();
|
||||
} catch (error) {
|
||||
console.error('Error rejecting plan:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Rejection Failed:** ${error.response?.data?.error || error.message}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const executePlan = async (planId) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.post('/api/models/querymodel/orchestrate', {
|
||||
planId,
|
||||
options: { topK: 5 }
|
||||
});
|
||||
|
||||
const result = response.data.data.toolExecution;
|
||||
|
||||
// Format the orchestration results nicely
|
||||
let formattedContent = `🚀 **Plan Executed Successfully!**\n\n`;
|
||||
|
||||
if (result.expandedQuery) {
|
||||
formattedContent += `**📋 Expanded Query:**\n${result.expandedQuery.substring(0, 500)}...\n\n`;
|
||||
}
|
||||
|
||||
if (result.extraction && result.extraction.results) {
|
||||
formattedContent += `**📚 Document Search Results:** ${result.extraction.results.length} documents found\n`;
|
||||
formattedContent += `**Confidence Score:** ${(result.extraction.confidence * 100).toFixed(1)}%\n\n`;
|
||||
}
|
||||
|
||||
if (result.web) {
|
||||
formattedContent += `**🌐 Web Search:** ${result.web.totalResults} results found\n`;
|
||||
formattedContent += `**Answer:** ${result.web.answer}\n\n`;
|
||||
}
|
||||
|
||||
if (result.report && result.report.content) {
|
||||
formattedContent += `**📊 Generated Report:**\n${result.report.content}\n\n`;
|
||||
}
|
||||
|
||||
// formattedContent += `**⚡ Processing Time:** ${result.report?.processingTime || 'N/A'}s\n`;
|
||||
// formattedContent += `**🔢 Tokens Used:** ${result.report?.tokensUsed || 'N/A'}`;
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: formattedContent,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'execution_result'
|
||||
}]);
|
||||
|
||||
loadPendingPlans();
|
||||
} catch (error) {
|
||||
console.error('Error executing plan:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Execution Failed:** ${error.response?.data?.error || error.message}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitFeedback = async (messageId) => {
|
||||
try {
|
||||
await axios.post('/api/feedback/submit', {
|
||||
messageId,
|
||||
feedbackType: feedbackData.feedbackType,
|
||||
rating: feedbackData.rating,
|
||||
comment: feedbackData.comment,
|
||||
correctedContent: feedbackData.correctedContent
|
||||
});
|
||||
|
||||
setShowFeedback(null);
|
||||
setFeedbackData({
|
||||
feedbackType: 'positive',
|
||||
rating: 5,
|
||||
comment: '',
|
||||
correctedContent: ''
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-page">
|
||||
<div className="chat-container">
|
||||
{/* Chat Header */}
|
||||
<div className="chat-header">
|
||||
<div className="chat-title">
|
||||
<h2>🧠 Engineering Reasoning Chat</h2>
|
||||
<p>Advanced AI system for complex engineering problem solving</p>
|
||||
</div>
|
||||
<div className="chat-actions">
|
||||
<button
|
||||
className="action-btn secondary"
|
||||
onClick={createConversation}
|
||||
>
|
||||
🔄 New Conversation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Plans */}
|
||||
{pendingPlans.length > 0 && (
|
||||
<div className="pending-plans">
|
||||
<h3>📋 Pending Plans ({pendingPlans.length})</h3>
|
||||
<div className="plans-grid">
|
||||
{pendingPlans.map((plan) => (
|
||||
<div key={plan.id} className="plan-card">
|
||||
<div className="plan-header">
|
||||
<h4>{plan.title}</h4>
|
||||
<span className="plan-status">{plan.status}</span>
|
||||
</div>
|
||||
<div className="plan-content">
|
||||
<pre>{plan.content}</pre>
|
||||
</div>
|
||||
<div className="plan-actions">
|
||||
<button
|
||||
className="btn approve"
|
||||
onClick={() => approvePlan(plan.id)}
|
||||
>
|
||||
✅ Approve
|
||||
</button>
|
||||
<button
|
||||
className="btn reject"
|
||||
onClick={() => rejectPlan(plan.id)}
|
||||
>
|
||||
❌ Reject
|
||||
</button>
|
||||
<button
|
||||
className="btn execute"
|
||||
onClick={() => executePlan(plan.id)}
|
||||
>
|
||||
🚀 Execute
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="messages-container">
|
||||
<div className="messages">
|
||||
{messages.map((message, index) => (
|
||||
<div key={index} className={`message ${message.role}`} data-message-type={message.message_type}>
|
||||
<div className="message-avatar">
|
||||
{message.role === 'user' ? '👤' : '🤖'}
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="message-header">
|
||||
<span className="message-role">
|
||||
{message.role === 'user' ? 'You' : 'Engineering AI'}
|
||||
</span>
|
||||
<span className="message-time">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
{message.role === 'assistant' && (
|
||||
<button
|
||||
className="feedback-btn"
|
||||
onClick={() => setShowFeedback(message.id || index)}
|
||||
>
|
||||
💬 Feedback
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="message-text">
|
||||
<pre>{message.content}</pre>
|
||||
</div>
|
||||
|
||||
{/* Plan Action Buttons */}
|
||||
{message.message_type === 'system_prompt' && message.content.includes('Engineering Plan Generated') && message.plan_id && (
|
||||
<div className="plan-actions">
|
||||
<button
|
||||
className="action-btn approve-btn"
|
||||
onClick={() => approvePlan(message.plan_id)}
|
||||
disabled={loading}
|
||||
>
|
||||
✅ Approve Plan
|
||||
</button>
|
||||
<button
|
||||
className="action-btn reject-btn"
|
||||
onClick={() => rejectPlan(message.plan_id)}
|
||||
disabled={loading}
|
||||
>
|
||||
❌ Reject Plan
|
||||
</button>
|
||||
<button
|
||||
className="action-btn modify-btn"
|
||||
onClick={() => setInputMessage('Please modify the plan with the following changes: ')}
|
||||
disabled={loading}
|
||||
>
|
||||
🔄 Modify Plan
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="message assistant">
|
||||
<div className="message-avatar">🤖</div>
|
||||
<div className="message-content">
|
||||
<div className="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="chat-input">
|
||||
<div className="input-container">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ask me about engineering problems, request analysis, or describe what you need help with..."
|
||||
rows="3"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
className="send-btn"
|
||||
onClick={sendMessage}
|
||||
disabled={!inputMessage.trim() || loading}
|
||||
>
|
||||
{loading ? '⏳' : '📤'} Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feedback Modal */}
|
||||
{showFeedback && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h3>💬 Provide Feedback</h3>
|
||||
<button
|
||||
className="close-btn"
|
||||
onClick={() => setShowFeedback(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-content">
|
||||
<div className="form-group">
|
||||
<label>Feedback Type</label>
|
||||
<select
|
||||
value={feedbackData.feedbackType}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, feedbackType: e.target.value})}
|
||||
>
|
||||
<option value="positive">👍 Positive</option>
|
||||
<option value="negative">👎 Negative</option>
|
||||
<option value="correction">✏️ Correction</option>
|
||||
<option value="suggestion">💡 Suggestion</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Rating (1-5)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
value={feedbackData.rating}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, rating: parseInt(e.target.value)})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Comment</label>
|
||||
<textarea
|
||||
value={feedbackData.comment}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, comment: e.target.value})}
|
||||
placeholder="Share your thoughts about this response..."
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{feedbackData.feedbackType === 'correction' && (
|
||||
<div className="form-group">
|
||||
<label>Corrected Content</label>
|
||||
<textarea
|
||||
value={feedbackData.correctedContent}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, correctedContent: e.target.value})}
|
||||
placeholder="Provide the corrected version..."
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="btn secondary"
|
||||
onClick={() => setShowFeedback(null)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn primary"
|
||||
onClick={() => submitFeedback(showFeedback)}
|
||||
>
|
||||
Submit Feedback
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPage;
|
||||
@@ -0,0 +1,412 @@
|
||||
/* Dashboard Styles */
|
||||
.dashboard {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Dashboard Grid */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Quick Actions */
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-btn.blue:hover {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.action-btn.green:hover {
|
||||
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.action-btn.purple:hover {
|
||||
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
.action-btn.orange:hover {
|
||||
background: linear-gradient(135deg, #fed7aa, #fdba74);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* System Status */
|
||||
.system-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-name {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.status-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Recent Activity */
|
||||
.recent-activity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: 1.25rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.activity-meta {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.start-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* System Overview */
|
||||
.system-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.overview-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import './Dashboard.css';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [stats, setStats] = useState({
|
||||
conversations: 0,
|
||||
documents: 0,
|
||||
plans: 0,
|
||||
feedback: 0
|
||||
});
|
||||
const [recentActivity, setRecentActivity] = useState([]);
|
||||
const [systemStatus, setSystemStatus] = useState({
|
||||
model1: 'online',
|
||||
queryModel: 'online',
|
||||
rag: 'online'
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load conversations
|
||||
const conversationsRes = await axios.get('/api/chat/conversations?limit=5');
|
||||
|
||||
// Load documents
|
||||
const documentsRes = await axios.get('/api/documents?limit=5');
|
||||
|
||||
// Load system status
|
||||
const statusRes = await axios.get('/api/models/status');
|
||||
|
||||
setStats({
|
||||
conversations: conversationsRes.data.data.pagination.total,
|
||||
documents: documentsRes.data.data.pagination?.total || documentsRes.data.data.documents.length,
|
||||
plans: 0, // Will be calculated from conversations
|
||||
feedback: 0 // Will be loaded separately
|
||||
});
|
||||
|
||||
setRecentActivity(conversationsRes.data.data.conversations.slice(0, 5));
|
||||
setSystemStatus(statusRes.data.data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
title: 'Start Engineering Chat',
|
||||
description: 'Begin a new engineering conversation',
|
||||
icon: '💬',
|
||||
color: 'blue',
|
||||
action: () => navigate('/chat')
|
||||
},
|
||||
{
|
||||
title: 'Upload Documents',
|
||||
description: 'Add knowledge to the system',
|
||||
icon: '📚',
|
||||
color: 'green',
|
||||
action: () => navigate('/documents')
|
||||
},
|
||||
{
|
||||
title: 'Execute Tools',
|
||||
description: 'Run engineering tools and workflows',
|
||||
icon: '🔧',
|
||||
color: 'purple',
|
||||
action: () => navigate('/tools')
|
||||
},
|
||||
{
|
||||
title: 'View History',
|
||||
description: 'Browse past conversations',
|
||||
icon: '📖',
|
||||
color: 'orange',
|
||||
action: () => navigate('/chat')
|
||||
}
|
||||
];
|
||||
|
||||
const systemComponents = [
|
||||
{ name: 'MODEL1 (Planner)', status: systemStatus.model1, description: 'Engineering plan generation' },
|
||||
{ name: 'QUERYMODEL (Executor)', status: systemStatus.queryModel, description: 'Plan execution and tool orchestration' },
|
||||
{ name: 'RAG System', status: systemStatus.rag, description: 'Document search and knowledge retrieval' }
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="dashboard-header">
|
||||
<h1>Engineering Reasoning Dashboard</h1>
|
||||
<p>Advanced AI system for complex engineering problem solving</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">💬</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.conversations}</div>
|
||||
<div className="stat-label">Conversations</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📚</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.documents}</div>
|
||||
<div className="stat-label">Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📋</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.plans}</div>
|
||||
<div className="stat-label">Plans Generated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">⭐</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.feedback}</div>
|
||||
<div className="stat-label">Feedback Items</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="dashboard-grid">
|
||||
{/* Quick Actions */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>Quick Actions</h2>
|
||||
<p>Start working with the system</p>
|
||||
</div>
|
||||
<div className="quick-actions">
|
||||
{quickActions.map((action, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`action-btn ${action.color}`}
|
||||
onClick={action.action}
|
||||
>
|
||||
<span className="action-icon">{action.icon}</span>
|
||||
<div className="action-content">
|
||||
<div className="action-title">{action.title}</div>
|
||||
<div className="action-description">{action.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Status */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>System Status</h2>
|
||||
<p>Current system health</p>
|
||||
</div>
|
||||
<div className="system-status">
|
||||
{systemComponents.map((component, index) => (
|
||||
<div key={index} className="status-item">
|
||||
<div className="status-info">
|
||||
<div className="status-name">{component.name}</div>
|
||||
<div className="status-description">{component.description}</div>
|
||||
</div>
|
||||
<div className={`status-indicator ${component.status}`}>
|
||||
<span className="status-dot"></span>
|
||||
{component.status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>Recent Activity</h2>
|
||||
<p>Latest conversations and interactions</p>
|
||||
</div>
|
||||
<div className="recent-activity">
|
||||
{recentActivity.length > 0 ? (
|
||||
recentActivity.map((activity, index) => (
|
||||
<div key={index} className="activity-item">
|
||||
<div className="activity-icon">💬</div>
|
||||
<div className="activity-content">
|
||||
<div className="activity-title">{activity.title || 'Untitled Conversation'}</div>
|
||||
<div className="activity-meta">
|
||||
{new Date(activity.created_at).toLocaleDateString()} • {activity.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📝</div>
|
||||
<p>No recent activity</p>
|
||||
<button
|
||||
className="start-btn"
|
||||
onClick={() => navigate('/chat')}
|
||||
>
|
||||
Start Your First Conversation
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Overview */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>System Overview</h2>
|
||||
<p>How the engineering reasoning system works</p>
|
||||
</div>
|
||||
<div className="system-overview">
|
||||
<div className="overview-step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">MODEL1 Planning</div>
|
||||
<div className="step-description">Analyzes your engineering query and creates a structured plan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">Plan Approval</div>
|
||||
<div className="step-description">Review and approve the generated plan before execution</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-step">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">QUERYMODEL Execution</div>
|
||||
<div className="step-description">Executes the plan using specialized engineering tools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-step">
|
||||
<div className="step-number">4</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">Results & Feedback</div>
|
||||
<div className="step-description">Review results and provide feedback for continuous improvement</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,538 @@
|
||||
/* Documents Page Styles */
|
||||
.documents-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.upload-section,
|
||||
.search-section,
|
||||
.documents-section {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Upload Section */
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.file-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 1rem;
|
||||
background: #f9fafb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-input-label:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-info strong {
|
||||
color: #1e293b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-info span {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 150px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.upload-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.search-type-selector {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: #f1f5f9;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.search-type-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.search-type-btn.active {
|
||||
background: white;
|
||||
color: #3b82f6;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.search-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Search Results */
|
||||
.search-results {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.search-results h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.result-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.relevance-score {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-snippet {
|
||||
color: #475569;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.75rem;
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Documents Grid */
|
||||
.documents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.document-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.document-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.document-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.document-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.document-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background: #fecaca;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.document-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status.processed {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status.processing {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
color: #475569;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.search-input-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-type-selector {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.documents-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.upload-section,
|
||||
.search-section,
|
||||
.documents-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import './DocumentsPage.css';
|
||||
|
||||
const DocumentsPage = () => {
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [searchType, setSearchType] = useState('semantic');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [uploadFile, setUploadFile] = useState(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadDocuments();
|
||||
}, []);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/documents');
|
||||
setDocuments(response.data.data.documents || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading documents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async () => {
|
||||
if (!uploadFile) return;
|
||||
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
setMessage('');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('document', uploadFile);
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/documents/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
setUploadProgress(percentCompleted);
|
||||
}
|
||||
});
|
||||
|
||||
setMessage(`✅ Document uploaded successfully: ${response.data.data.document.original_filename}`);
|
||||
setUploadFile(null);
|
||||
setUploadProgress(0);
|
||||
loadDocuments();
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
setMessage(`❌ Upload failed: ${error.response?.data?.error || error.message}`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
setSearching(true);
|
||||
setSearchResults([]);
|
||||
|
||||
try {
|
||||
const endpoint = searchType === 'semantic' ? '/api/documents/search' : '/api/documents/graph-search';
|
||||
const response = await axios.get(endpoint, {
|
||||
params: { query: searchQuery }
|
||||
});
|
||||
|
||||
setSearchResults(response.data.data.results || []);
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
setMessage(`❌ Search failed: ${error.response?.data?.error || error.message}`);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDocument = async (documentId) => {
|
||||
if (!window.confirm('Are you sure you want to delete this document?')) return;
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/documents/${documentId}`);
|
||||
setMessage('✅ Document deleted successfully');
|
||||
loadDocuments();
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
setMessage(`❌ Delete failed: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="documents-page">
|
||||
<div className="page-header">
|
||||
<h1>📚 Knowledge Base</h1>
|
||||
<p>Upload and search engineering documents with advanced AI</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('✅') ? 'success' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Section */}
|
||||
<div className="upload-section">
|
||||
<div className="section-header">
|
||||
<h2>📤 Upload Documents</h2>
|
||||
<p>Add new documents to the knowledge base</p>
|
||||
</div>
|
||||
|
||||
<div className="upload-area">
|
||||
<div className="file-input-container">
|
||||
<input
|
||||
type="file"
|
||||
id="file-upload"
|
||||
accept=".pdf,.txt,.doc,.docx"
|
||||
onChange={(e) => setUploadFile(e.target.files?.[0])}
|
||||
className="file-input"
|
||||
/>
|
||||
<label htmlFor="file-upload" className="file-input-label">
|
||||
<div className="upload-icon">📁</div>
|
||||
<div className="upload-text">
|
||||
{uploadFile ? uploadFile.name : 'Choose a file to upload'}
|
||||
</div>
|
||||
<div className="upload-hint">PDF, TXT, DOC, DOCX files supported</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{uploadFile && (
|
||||
<div className="upload-actions">
|
||||
<div className="file-info">
|
||||
<strong>{uploadFile.name}</strong>
|
||||
<span>{formatFileSize(uploadFile.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
className="upload-btn"
|
||||
onClick={handleFileUpload}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span>Uploading... {uploadProgress}%</span>
|
||||
</>
|
||||
) : (
|
||||
'📤 Upload Document'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="search-section">
|
||||
<div className="section-header">
|
||||
<h2>🔍 Document Search</h2>
|
||||
<p>Find relevant information using AI-powered search</p>
|
||||
</div>
|
||||
|
||||
<div className="search-container">
|
||||
<div className="search-input-group">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search for engineering concepts, specifications, or procedures..."
|
||||
className="search-input"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<div className="search-type-selector">
|
||||
<button
|
||||
className={`search-type-btn ${searchType === 'semantic' ? 'active' : ''}`}
|
||||
onClick={() => setSearchType('semantic')}
|
||||
>
|
||||
🔍 Semantic
|
||||
</button>
|
||||
<button
|
||||
className={`search-type-btn ${searchType === 'graph' ? 'active' : ''}`}
|
||||
onClick={() => setSearchType('graph')}
|
||||
>
|
||||
🕸️ Graph RAG
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="search-btn"
|
||||
onClick={handleSearch}
|
||||
disabled={!searchQuery.trim() || searching}
|
||||
>
|
||||
{searching ? '⏳' : '🔍'} Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<div className="search-results">
|
||||
<h3>Search Results ({searchResults.length})</h3>
|
||||
<div className="results-grid">
|
||||
{searchResults.map((result, index) => (
|
||||
<div key={index} className="result-card">
|
||||
<div className="result-header">
|
||||
<h4>📄 {result.original_filename}</h4>
|
||||
<span className="relevance-score">
|
||||
{result.score?.toFixed(3)} relevance
|
||||
</span>
|
||||
</div>
|
||||
<div className="result-snippet">
|
||||
{result.snippet}
|
||||
</div>
|
||||
<div className="result-meta">
|
||||
<span>ID: {result.id}</span>
|
||||
<span>Size: {formatFileSize(result.file_size || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents List */}
|
||||
<div className="documents-section">
|
||||
<div className="section-header">
|
||||
<h2>📋 Document Library</h2>
|
||||
<p>Manage your uploaded documents</p>
|
||||
</div>
|
||||
|
||||
<div className="documents-grid">
|
||||
{documents.length > 0 ? (
|
||||
documents.map((doc) => (
|
||||
<div key={doc.id} className="document-card">
|
||||
<div className="document-header">
|
||||
<h4>📄 {doc.original_filename}</h4>
|
||||
<div className="document-actions">
|
||||
<button
|
||||
className="action-btn delete"
|
||||
onClick={() => deleteDocument(doc.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-meta">
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Size:</span>
|
||||
<span>{formatFileSize(doc.file_size)}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Uploaded:</span>
|
||||
<span>{new Date(doc.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Status:</span>
|
||||
<span className={`status ${doc.status}`}>{doc.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
{doc.extracted_text && (
|
||||
<div className="document-preview">
|
||||
<div className="preview-header">Content Preview:</div>
|
||||
<div className="preview-text">
|
||||
{doc.extracted_text.substring(0, 200)}...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📚</div>
|
||||
<h3>No documents yet</h3>
|
||||
<p>Upload your first document to start building your knowledge base</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsPage;
|
||||
@@ -0,0 +1,226 @@
|
||||
/* Login Page Styles */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-tabs {
|
||||
display: flex;
|
||||
background: #f1f5f9;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: white;
|
||||
color: #3b82f6;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.switch-btn:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-pattern {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background:
|
||||
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(180deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './LoginPage.css';
|
||||
|
||||
const LoginPage = () => {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
firstName: '',
|
||||
lastName: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login, register, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const result = isLogin
|
||||
? await login(formData.email, formData.password)
|
||||
: await register(formData);
|
||||
|
||||
if (result.success) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-container">
|
||||
<div className="login-header">
|
||||
<div className="logo">
|
||||
<span className="logo-icon">🧠</span>
|
||||
<span className="logo-text">Reason Flow</span>
|
||||
</div>
|
||||
<p className="login-subtitle">
|
||||
Advanced Engineering Reasoning System
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-form-container">
|
||||
<div className="form-tabs">
|
||||
<button
|
||||
className={`tab ${isLogin ? 'active' : ''}`}
|
||||
onClick={() => setIsLogin(true)}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${!isLogin ? 'active' : ''}`}
|
||||
onClick={() => setIsLogin(false)}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="login-form">
|
||||
{!isLogin && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="firstName">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
required={!isLogin}
|
||||
placeholder="Enter your first name"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="lastName">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
required={!isLogin}
|
||||
placeholder="Enter your last name"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
minLength="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-btn"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<span className="loading-spinner"></span>
|
||||
) : (
|
||||
isLogin ? 'Sign In' : 'Create Account'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>
|
||||
{isLogin ? "Don't have an account?" : "Already have an account?"}
|
||||
<button
|
||||
type="button"
|
||||
className="switch-btn"
|
||||
onClick={() => setIsLogin(!isLogin)}
|
||||
>
|
||||
{isLogin ? 'Register' : 'Login'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-background">
|
||||
<div className="bg-pattern"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,525 @@
|
||||
/* Tools Page Styles */
|
||||
.tools-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.tool-selection,
|
||||
.execution-history {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Tool Grid */
|
||||
.tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-card.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #f0f9ff;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.tool-card.blue.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.tool-card.green.selected {
|
||||
border-color: #10b981;
|
||||
background: #d1fae5;
|
||||
}
|
||||
|
||||
.tool-card.purple.selected {
|
||||
border-color: #8b5cf6;
|
||||
background: #e9d5ff;
|
||||
}
|
||||
|
||||
.tool-card.orange.selected {
|
||||
border-color: #f59e0b;
|
||||
background: #fed7aa;
|
||||
}
|
||||
|
||||
.tool-card.cyan.selected {
|
||||
border-color: #06b6d4;
|
||||
background: #cffafe;
|
||||
}
|
||||
|
||||
.tool-card.red.selected {
|
||||
border-color: #ef4444;
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tool-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-info p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Execution Panel */
|
||||
.execution-panel {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.input-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.execution-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.execute-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.execute-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.execute-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.execute-btn.disabled {
|
||||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||||
opacity: 0.8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.execute-btn.disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Execution History */
|
||||
.history-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.executions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.execution-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.execution-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.execution-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.execution-info h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.execution-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.execution-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-badge.info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Execution Details */
|
||||
.execution-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detail-section.error {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.detail-section h5 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.code-block,
|
||||
.error-block {
|
||||
background: #f1f5f9;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.error-block {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.execution-metrics {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.875rem;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.tool-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.execution-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.execution-status {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.execution-metrics {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.tool-selection,
|
||||
.execution-history {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import './ToolsPage.css';
|
||||
|
||||
const ToolsPage = () => {
|
||||
const [toolExecutions, setToolExecutions] = useState([]);
|
||||
const [selectedTool, setSelectedTool] = useState('');
|
||||
const [toolInput, setToolInput] = useState('');
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const tools = [
|
||||
{
|
||||
id: 'query_expander',
|
||||
name: 'Query Expander',
|
||||
description: 'Expands and refines engineering queries for better understanding',
|
||||
icon: '🔍',
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
id: 'extraction',
|
||||
name: 'Document Extraction',
|
||||
description: 'Extracts relevant information from uploaded documents',
|
||||
icon: '📄',
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
id: 'report1',
|
||||
name: 'Report Generator',
|
||||
description: 'Generates structured engineering reports',
|
||||
icon: '📊',
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
id: 'report2',
|
||||
name: 'File Generator',
|
||||
description: 'Creates downloadable engineering files',
|
||||
icon: '📁',
|
||||
color: 'orange'
|
||||
},
|
||||
{
|
||||
id: 'web_search',
|
||||
name: 'Web Search',
|
||||
description: 'Searches the web for current engineering information',
|
||||
icon: '🌐',
|
||||
color: 'cyan'
|
||||
},
|
||||
{
|
||||
id: 'encyclopedia_pdf',
|
||||
name: 'Encyclopedia Search',
|
||||
description: 'Searches internal PDF knowledge base',
|
||||
icon: '📚',
|
||||
color: 'red'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadToolExecutions();
|
||||
}, []);
|
||||
|
||||
const loadToolExecutions = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/tools/executions');
|
||||
setToolExecutions(response.data.data.executions || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading tool executions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const executeTool = async () => {
|
||||
setMessage('⚠️ **Manual tool execution is disabled.** Tools can only be executed as part of an approved engineering plan. Please use the Chat page to create and execute plans.');
|
||||
setSelectedTool('');
|
||||
setToolInput('');
|
||||
};
|
||||
|
||||
const retryExecution = async (executionId) => {
|
||||
try {
|
||||
await axios.post(`/api/tools/executions/${executionId}/retry`);
|
||||
setMessage('✅ Tool execution retried');
|
||||
loadToolExecutions();
|
||||
} catch (error) {
|
||||
console.error('Retry error:', error);
|
||||
setMessage(`❌ Retry failed: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'success';
|
||||
case 'failed': return 'error';
|
||||
case 'running': return 'warning';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tools-page">
|
||||
<div className="page-header">
|
||||
<h1>🔧 Engineering Tools</h1>
|
||||
<p>View tool execution history and results from approved engineering plans</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('✅') ? 'success' : message.includes('⚠️') ? 'warning' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Selection */}
|
||||
<div className="tool-selection">
|
||||
<div className="section-header">
|
||||
<h2>🛠️ Tool Information</h2>
|
||||
<p>Available engineering tools (execution requires approved plan)</p>
|
||||
</div>
|
||||
|
||||
<div className="tool-selector">
|
||||
<div className="tool-grid">
|
||||
{tools.map((tool) => (
|
||||
<button
|
||||
key={tool.id}
|
||||
className={`tool-card ${selectedTool === tool.id ? 'selected' : ''} ${tool.color}`}
|
||||
onClick={() => setSelectedTool(tool.id)}
|
||||
>
|
||||
<div className="tool-icon">{tool.icon}</div>
|
||||
<div className="tool-info">
|
||||
<h3>{tool.name}</h3>
|
||||
<p>{tool.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedTool && (
|
||||
<div className="execution-panel">
|
||||
<div className="panel-header">
|
||||
<h3>{tools.find(t => t.id === selectedTool)?.name} Information</h3>
|
||||
</div>
|
||||
<div className="panel-content">
|
||||
<div className="tool-info">
|
||||
<p><strong>Description:</strong> {tools.find(t => t.id === selectedTool)?.description}</p>
|
||||
<p><strong>Usage:</strong> This tool is automatically executed as part of approved engineering plans.</p>
|
||||
<p><strong>To use this tool:</strong></p>
|
||||
<ol>
|
||||
<li>Go to the Chat page</li>
|
||||
<li>Ask an engineering question</li>
|
||||
<li>Review the generated plan</li>
|
||||
<li>Approve the plan</li>
|
||||
<li>The system will automatically execute the appropriate tools</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="execution-actions">
|
||||
<button
|
||||
className="execute-btn disabled"
|
||||
onClick={executeTool}
|
||||
disabled={true}
|
||||
>
|
||||
🔒 Manual Execution Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Execution History */}
|
||||
<div className="execution-history">
|
||||
<div className="section-header">
|
||||
<h2>📋 Execution History</h2>
|
||||
<p>View past tool executions and their results</p>
|
||||
</div>
|
||||
|
||||
<div className="history-container">
|
||||
{toolExecutions.length > 0 ? (
|
||||
<div className="executions-list">
|
||||
{toolExecutions.map((execution) => (
|
||||
<div key={execution.id} className="execution-card">
|
||||
<div className="execution-header">
|
||||
<div className="execution-info">
|
||||
<h4>
|
||||
{tools.find(t => t.id === execution.tool_name)?.icon}
|
||||
{tools.find(t => t.id === execution.tool_name)?.name || execution.tool_name}
|
||||
</h4>
|
||||
<div className="execution-meta">
|
||||
<span className="execution-id">ID: {execution.id}</span>
|
||||
<span className="execution-time">{formatDate(execution.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="execution-status">
|
||||
<span className={`status-badge ${getStatusColor(execution.status)}`}>
|
||||
{execution.status}
|
||||
</span>
|
||||
{execution.status === 'failed' && (
|
||||
<button
|
||||
className="retry-btn"
|
||||
onClick={() => retryExecution(execution.id)}
|
||||
>
|
||||
🔄 Retry
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="execution-details">
|
||||
<div className="detail-section">
|
||||
<h5>Input Parameters</h5>
|
||||
<pre className="code-block">
|
||||
{JSON.stringify(execution.input_parameters, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{execution.output_result && (
|
||||
<div className="detail-section">
|
||||
<h5>Output Result</h5>
|
||||
<pre className="code-block">
|
||||
{JSON.stringify(execution.output_result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{execution.error_message && (
|
||||
<div className="detail-section error">
|
||||
<h5>Error Message</h5>
|
||||
<pre className="error-block">
|
||||
{execution.error_message}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="execution-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Duration:</span>
|
||||
<span className="metric-value">{execution.duration || 'N/A'}ms</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="metric-label">Tokens Used:</span>
|
||||
<span className="metric-value">{execution.tokens_used || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">🔧</div>
|
||||
<h3>No tool executions yet</h3>
|
||||
<p>Execute your first tool to see the results here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsPage;
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
# ===========================================
|
||||
# REASON FLOW - ENVIRONMENT CONFIGURATION
|
||||
# ===========================================
|
||||
|
||||
# Server Configuration
|
||||
PORT=8000
|
||||
NODE_ENV=development
|
||||
HOST=localhost
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=reason_flow
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=your_password_here
|
||||
DB_SSL=false
|
||||
|
||||
# Groq API Configuration
|
||||
GROQ_API_KEY=your_groq_api_key_here
|
||||
GROQ_MODEL=moonshotai/kimi-k2-instruct-0905
|
||||
GROQ_BASE_URL=https://api.groq.com
|
||||
|
||||
# OpenAI API (for embeddings and additional models)
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your_jwt_secret_here_make_it_long_and_secure
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# Admin User Configuration
|
||||
ADMIN_EMAIL=admin@reasonflow.com
|
||||
ADMIN_PASSWORD=admin123
|
||||
ADMIN_FIRST_NAME=Admin
|
||||
ADMIN_LAST_NAME=User
|
||||
|
||||
# File Upload Configuration
|
||||
MAX_FILE_SIZE=50MB
|
||||
UPLOAD_PATH=./uploads
|
||||
ALLOWED_FILE_TYPES=pdf,txt,doc,docx
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=./logs/app.log
|
||||
LOG_MAX_SIZE=10MB
|
||||
LOG_MAX_FILES=5
|
||||
|
||||
# Vector Database Configuration
|
||||
VECTOR_DB_URL=your_vector_db_url_here
|
||||
VECTOR_DB_API_KEY=your_vector_db_api_key_here
|
||||
|
||||
# Web Search API Configuration
|
||||
SERP_API_KEY=your_serp_api_key_here
|
||||
SERP_ENGINE=google
|
||||
|
||||
# Redis Configuration (for caching and sessions)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_PASSWORD=your_redis_password_here
|
||||
|
||||
# Model Configuration
|
||||
MODEL1_TEMPERATURE=0.3
|
||||
MODEL1_MAX_TOKENS=3000
|
||||
QUERYMODEL_TEMPERATURE=0.5
|
||||
QUERYMODEL_MAX_TOKENS=4000
|
||||
|
||||
# Fine-tuning Configuration
|
||||
FINE_TUNING_ENABLED=true
|
||||
FINE_TUNING_SCHEDULE=weekly
|
||||
FINE_TUNING_BATCH_SIZE=10
|
||||
|
||||
# Feedback Configuration
|
||||
FEEDBACK_PROCESSING_ENABLED=true
|
||||
FEEDBACK_BATCH_SIZE=50
|
||||
FEEDBACK_PROCESSING_SCHEDULE=daily
|
||||
|
||||
# Security Configuration
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
HELMET_ENABLED=true
|
||||
RATE_LIMIT_ENABLED=true
|
||||
|
||||
# Monitoring Configuration
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
METRICS_ENABLED=true
|
||||
PERFORMANCE_MONITORING=true
|
||||
|
||||
# Development Configuration
|
||||
DEBUG_MODE=true
|
||||
VERBOSE_LOGGING=true
|
||||
HOT_RELOAD=true
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "reason-flow",
|
||||
"version": "1.0.0",
|
||||
"description": "AI-powered engineering reasoning system with continuous learning",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
"start": "node server/index.js",
|
||||
"dev": "nodemon server/index.js",
|
||||
"build": "cd client && npm run build",
|
||||
"install-all": "npm install && cd client && npm install",
|
||||
"dev-full": "concurrently \"npm run dev\" \"cd client && npm start\"",
|
||||
"db:setup": "node setup-database.js",
|
||||
"db:init": "node server/utils/databaseManager.js init",
|
||||
"db:reset": "node server/utils/databaseManager.js reset",
|
||||
"db:migrate": "node server/utils/databaseManager.js migrate",
|
||||
"db:seed": "node server/utils/databaseManager.js seed",
|
||||
"db:status": "node server/utils/databaseManager.js status",
|
||||
"db:info": "node server/utils/databaseManager.js info",
|
||||
"test:groq": "node test-groq.js",
|
||||
"env:setup": "node setup-env.js",
|
||||
"env:validate": "node server/utils/validateConfig.js validate",
|
||||
"env:show": "node server/utils/validateConfig.js show",
|
||||
"config:validate": "node server/utils/validateConfig.js validate",
|
||||
"config:show": "node server/utils/validateConfig.js show"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.0.20",
|
||||
"@langchain/openai": "^0.0.14",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"axios": "^1.12.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"groq-sdk": "^0.7.0",
|
||||
"helmet": "^7.1.0",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"langchain": "^0.1.0",
|
||||
"ml-distance": "^4.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"openai": "^4.20.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.11.3",
|
||||
"rate-limiter-flexible": "^3.0.6",
|
||||
"sequelize": "^6.35.0",
|
||||
"socket.io": "^4.7.4",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
"engineering",
|
||||
"reasoning",
|
||||
"rag",
|
||||
"fine-tuning",
|
||||
"machine-learning"
|
||||
],
|
||||
"author": "Reason Flow Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Database setup script for Reason Flow
|
||||
require('dotenv').config();
|
||||
|
||||
const { setupDatabase, getDatabaseInfo } = require('./server/utils/databaseManager');
|
||||
const { checkDatabaseStatus } = require('./server/utils/databaseInit');
|
||||
|
||||
const main = async () => {
|
||||
console.log('🚀 Reason Flow Database Setup');
|
||||
console.log('==============================\n');
|
||||
|
||||
try {
|
||||
// Check if database is accessible
|
||||
console.log('1. Checking database connection...');
|
||||
const connected = await checkDatabaseStatus();
|
||||
if (!connected) {
|
||||
console.error('❌ Database connection failed. Please check your database configuration.');
|
||||
console.error(' Make sure PostgreSQL is running and your environment variables are set.');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ Database connection successful\n');
|
||||
|
||||
// Setup database
|
||||
console.log('2. Setting up database schema...');
|
||||
const setup = await setupDatabase({
|
||||
force: false,
|
||||
seed: true,
|
||||
migrations: true
|
||||
});
|
||||
|
||||
if (!setup) {
|
||||
console.error('❌ Database setup failed');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ Database setup completed\n');
|
||||
|
||||
// Show database info
|
||||
console.log('3. Database information:');
|
||||
await getDatabaseInfo();
|
||||
|
||||
console.log('\n🎉 Database setup completed successfully!');
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Start the server: npm run dev');
|
||||
console.log('2. Test the API: curl http://localhost:8000/api/health');
|
||||
console.log('3. Check model status: curl http://localhost:8000/api/models/status');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Setup failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
||||
Executable
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Environment setup script for Reason Flow
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const question = (query) => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(query, resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log('🚀 Reason Flow Environment Setup');
|
||||
console.log('================================\n');
|
||||
|
||||
try {
|
||||
// Check if .env already exists
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
const overwrite = await question('⚠️ .env file already exists. Overwrite? (y/N): ');
|
||||
if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
|
||||
console.log('Setup cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Please provide the following configuration values:\n');
|
||||
|
||||
// Collect configuration
|
||||
const config = {};
|
||||
|
||||
// Server Configuration
|
||||
console.log('📡 Server Configuration:');
|
||||
config.PORT = await question('Port (default: 8000): ') || '8000';
|
||||
config.NODE_ENV = await question('Environment (development/production/test, default: development): ') || 'development';
|
||||
|
||||
// Database Configuration
|
||||
console.log('\n🗄️ Database Configuration:');
|
||||
config.DB_HOST = await question('Database Host (default: localhost): ') || 'localhost';
|
||||
config.DB_PORT = await question('Database Port (default: 5432): ') || '5432';
|
||||
config.DB_NAME = await question('Database Name (default: reason_flow): ') || 'reason_flow';
|
||||
config.DB_USER = await question('Database User (default: postgres): ') || 'postgres';
|
||||
config.DB_PASSWORD = await question('Database Password: ');
|
||||
|
||||
// API Keys
|
||||
console.log('\n🔑 API Keys:');
|
||||
config.GROQ_API_KEY = await question('Groq API Key (required): ');
|
||||
config.OPENAI_API_KEY = await question('OpenAI API Key (optional): ');
|
||||
config.SERP_API_KEY = await question('SERP API Key (optional): ');
|
||||
|
||||
// JWT Configuration
|
||||
console.log('\n🔐 Security Configuration:');
|
||||
config.JWT_SECRET = await question('JWT Secret (or press Enter for auto-generated): ');
|
||||
if (!config.JWT_SECRET) {
|
||||
config.JWT_SECRET = require('crypto').randomBytes(64).toString('hex');
|
||||
console.log(`Auto-generated JWT Secret: ${config.JWT_SECRET}`);
|
||||
}
|
||||
|
||||
// Admin User
|
||||
console.log('\n👤 Admin User Configuration:');
|
||||
config.ADMIN_EMAIL = await question('Admin Email (default: admin@reasonflow.com): ') || 'admin@reasonflow.com';
|
||||
config.ADMIN_PASSWORD = await question('Admin Password (default: admin123): ') || 'admin123';
|
||||
|
||||
// Generate .env file
|
||||
const envContent = generateEnvContent(config);
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
|
||||
console.log('\n✅ Environment configuration created successfully!');
|
||||
console.log(`📁 Configuration saved to: ${envPath}`);
|
||||
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Review your configuration: node server/utils/validateConfig.js show');
|
||||
console.log('2. Validate configuration: node server/utils/validateConfig.js validate');
|
||||
console.log('3. Setup database: npm run db:setup');
|
||||
console.log('4. Start the server: npm run dev');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Setup failed:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
};
|
||||
|
||||
const generateEnvContent = (config) => {
|
||||
return `# Reason Flow Environment Configuration
|
||||
# Generated on ${new Date().toISOString()}
|
||||
|
||||
# Server Configuration
|
||||
PORT=${config.PORT}
|
||||
NODE_ENV=${config.NODE_ENV}
|
||||
HOST=localhost
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=${config.DB_HOST}
|
||||
DB_PORT=${config.DB_PORT}
|
||||
DB_NAME=${config.DB_NAME}
|
||||
DB_USER=${config.DB_USER}
|
||||
DB_PASSWORD=${config.DB_PASSWORD}
|
||||
DB_SSL=false
|
||||
|
||||
# Groq API Configuration
|
||||
GROQ_API_KEY=${config.GROQ_API_KEY}
|
||||
GROQ_MODEL=moonshotai/kimi-k2-instruct-0905
|
||||
GROQ_BASE_URL=https://api.groq.com
|
||||
|
||||
# OpenAI API Configuration
|
||||
OPENAI_API_KEY=${config.OPENAI_API_KEY}
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=${config.JWT_SECRET}
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# Admin User Configuration
|
||||
ADMIN_EMAIL=${config.ADMIN_EMAIL}
|
||||
ADMIN_PASSWORD=${config.ADMIN_PASSWORD}
|
||||
ADMIN_FIRST_NAME=Admin
|
||||
ADMIN_LAST_NAME=User
|
||||
|
||||
# File Upload Configuration
|
||||
MAX_FILE_SIZE=50MB
|
||||
UPLOAD_PATH=./uploads
|
||||
ALLOWED_FILE_TYPES=pdf,txt,doc,docx
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=./logs/app.log
|
||||
LOG_MAX_SIZE=10MB
|
||||
LOG_MAX_FILES=5
|
||||
|
||||
# CORS Configuration
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
|
||||
# Security Configuration
|
||||
HELMET_ENABLED=true
|
||||
RATE_LIMIT_ENABLED=true
|
||||
|
||||
# Development Configuration
|
||||
DEBUG_MODE=true
|
||||
VERBOSE_LOGGING=true
|
||||
HOT_RELOAD=true
|
||||
|
||||
# Monitoring Configuration
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
METRICS_ENABLED=true
|
||||
PERFORMANCE_MONITORING=true
|
||||
|
||||
# Model Configuration
|
||||
MODEL1_TEMPERATURE=0.3
|
||||
MODEL1_MAX_TOKENS=3000
|
||||
QUERYMODEL_TEMPERATURE=0.5
|
||||
QUERYMODEL_MAX_TOKENS=4000
|
||||
|
||||
# Fine-tuning Configuration
|
||||
FINE_TUNING_ENABLED=true
|
||||
FINE_TUNING_SCHEDULE=weekly
|
||||
FINE_TUNING_BATCH_SIZE=10
|
||||
|
||||
# Feedback Configuration
|
||||
FEEDBACK_PROCESSING_ENABLED=true
|
||||
FEEDBACK_BATCH_SIZE=50
|
||||
FEEDBACK_PROCESSING_SCHEDULE=daily
|
||||
`;
|
||||
};
|
||||
|
||||
main();
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Simple test script to verify Groq integration
|
||||
require('dotenv').config();
|
||||
|
||||
const { runAllTests } = require('./server/utils/testGroq');
|
||||
|
||||
console.log('🚀 Starting Groq Integration Test...\n');
|
||||
|
||||
runAllTests()
|
||||
.then(results => {
|
||||
console.log('\n✅ Test completed successfully!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n❌ Test failed:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import axios from "axios";
|
||||
import 'dotenv/config';
|
||||
|
||||
const API_KEY = process.env.TAVILY_API_KEY;
|
||||
|
||||
async function webSearch(query) {
|
||||
const response = await axios.post("https://api.tavily.com/search",
|
||||
{ query },
|
||||
{ headers: { Authorization: `Bearer ${API_KEY}` } }
|
||||
);
|
||||
|
||||
console.log(response.data.results);
|
||||
}
|
||||
|
||||
webSearch("who is the current president of united states");
|
||||
Reference in New Issue
Block a user