Spaces:
Running
Running
Commit ·
e8a6607
0
Parent(s):
Initial commit for AI coding assistant
Browse files- Dockerfile +89 -0
- README.md +11 -0
- node-backend/.env.development +6 -0
- node-backend/.env.production +5 -0
- node-backend/Dockerfile +22 -0
- node-backend/package-lock.json +0 -0
- node-backend/package.json +56 -0
- node-backend/src/api/controllers/gpt-chat-controller.ts +53 -0
- node-backend/src/api/controllers/index.ts +3 -0
- node-backend/src/api/routes/gpt-chat-router.ts +13 -0
- node-backend/src/api/routes/index.ts +12 -0
- node-backend/src/api/socket/socket.ts +97 -0
- node-backend/src/logger.ts +67 -0
- node-backend/src/queues/gpt-chat-queue-services.ts +146 -0
- node-backend/src/queues/index.ts +6 -0
- node-backend/src/queues/queue.ts +185 -0
- node-backend/src/redis/index.ts +16 -0
- node-backend/src/redis/redisClient.ts +186 -0
- node-backend/src/redis/redisServices.ts +35 -0
- node-backend/src/redis/socket_redis.ts +34 -0
- node-backend/src/server.ts +277 -0
- node-backend/tsconfig.json +15 -0
- python-ai-service/QueueAndWorker/gpt_chat.py +67 -0
- python-ai-service/QueueAndWorker/model_manager.py +38 -0
- python-ai-service/QueueAndWorker/queue_manager.py +38 -0
- python-ai-service/QueueAndWorker/worker_manager.py +104 -0
- python-ai-service/classInfra.py +21 -0
- python-ai-service/main.py +132 -0
- react-frontend/dist/assets/index-BKJNczi-.css +1 -0
- react-frontend/dist/assets/index-BKxufNR1.js +0 -0
- react-frontend/dist/favicon.svg +1 -0
- react-frontend/dist/icons.svg +24 -0
- react-frontend/dist/index.html +14 -0
- requirements.txt +10 -0
- start.sh +15 -0
Dockerfile
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# Production CPU Runtime Environment
|
| 3 |
+
# ==========================================
|
| 4 |
+
FROM python:3.11-slim
|
| 5 |
+
|
| 6 |
+
# Install system dependencies, Redis server, OpenBLAS for CPU matrix math, and Node.js
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
curl \
|
| 9 |
+
gnupg \
|
| 10 |
+
build-essential \
|
| 11 |
+
cmake \
|
| 12 |
+
pkg-config \
|
| 13 |
+
git \
|
| 14 |
+
libopenblas-dev \
|
| 15 |
+
redis-server \
|
| 16 |
+
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
| 17 |
+
&& apt-get install -y nodejs \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Install PM2 globally to manage background processes
|
| 22 |
+
RUN npm install -g pm2
|
| 23 |
+
|
| 24 |
+
# Setup non-root user for Hugging Face compliance (UID 1000)
|
| 25 |
+
RUN useradd -m -u 1000 user
|
| 26 |
+
WORKDIR /home/user/app
|
| 27 |
+
|
| 28 |
+
# Force llama-cpp-python to compile cleanly for CPU optimization
|
| 29 |
+
ENV CMAKE_ARGS="-DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS"
|
| 30 |
+
|
| 31 |
+
# Create cache directory for Hugging Face model downloads and set ownership
|
| 32 |
+
ENV HF_HOME=/home/user/app/.cache/huggingface
|
| 33 |
+
RUN mkdir -p $HF_HOME
|
| 34 |
+
|
| 35 |
+
# --- Setup Python Worker ---
|
| 36 |
+
COPY requirements.txt .
|
| 37 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 38 |
+
COPY python-ai-service/ ./python-ai-service
|
| 39 |
+
|
| 40 |
+
# --- Setup Node Backend ---
|
| 41 |
+
COPY node-backend/package*.json ./node-backend/
|
| 42 |
+
RUN cd node-backend && npm ci --only=production
|
| 43 |
+
COPY node-backend/ ./node-backend
|
| 44 |
+
|
| 45 |
+
# --- Setup Pre-built Frontend Static Files ---
|
| 46 |
+
# Copies directly from your react-frontend/dist folder
|
| 47 |
+
COPY react-frontend/dist ./node-backend/public
|
| 48 |
+
|
| 49 |
+
# Copy startup script
|
| 50 |
+
COPY start.sh .
|
| 51 |
+
RUN chmod +x start.sh
|
| 52 |
+
|
| 53 |
+
# Set environment variables
|
| 54 |
+
ENV PORT=7860
|
| 55 |
+
ENV NODE_ENV=production
|
| 56 |
+
ENV APP_ENV=production
|
| 57 |
+
ENV REDIS_URL=redis://127.0.0.1:6379
|
| 58 |
+
|
| 59 |
+
# Set write/read permissions for the Hugging Face non-root user
|
| 60 |
+
RUN chown -R user:user /home/user/app
|
| 61 |
+
USER user
|
| 62 |
+
|
| 63 |
+
# Create a PM2 ecosystem file to launch Redis, Node Backend, and Python Worker
|
| 64 |
+
RUN echo 'module.exports = { \
|
| 65 |
+
apps: [ \
|
| 66 |
+
{ \
|
| 67 |
+
name: "redis-server", \
|
| 68 |
+
script: "redis-server", \
|
| 69 |
+
args: "--bind 127.0.0.1 --protected-mode no" \
|
| 70 |
+
}, \
|
| 71 |
+
{ \
|
| 72 |
+
name: "node-backend", \
|
| 73 |
+
script: "cd node-backend && npm start", \
|
| 74 |
+
env: { PORT: "7860", REDIS_URL: "redis://127.0.0.1:6379" } \
|
| 75 |
+
}, \
|
| 76 |
+
{ \
|
| 77 |
+
name: "python-worker", \
|
| 78 |
+
script: "python -m python-ai-service.main", \
|
| 79 |
+
env: { REDIS_URL: "redis://127.0.0.1:6379" } \
|
| 80 |
+
} \
|
| 81 |
+
] \
|
| 82 |
+
};' > ecosystem.config.js
|
| 83 |
+
|
| 84 |
+
# Expose Hugging Face Space default web port
|
| 85 |
+
EXPOSE 7860
|
| 86 |
+
|
| 87 |
+
# Start Redis, Node, and Python simultaneously
|
| 88 |
+
# CMD ["pm2-runtime", "ecosystem.config.js"]
|
| 89 |
+
CMD ["./start.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: My Multi Service App
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
This is my multi-service application running Node, React, and Python via Docker.
|
node-backend/.env.development
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FRONTEND_URL=https://127.0.0.1:5173
|
| 2 |
+
HOST=127.0.0.1
|
| 3 |
+
PORT=3000
|
| 4 |
+
|
| 5 |
+
SSL_CERT=./src/ssl/certificate.crt
|
| 6 |
+
SSL_KEY=./src/ssl/certificate.key
|
node-backend/.env.production
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PORT=7860
|
| 2 |
+
|
| 3 |
+
REDIS_URL=redis://127.0.0.1:6379
|
| 4 |
+
|
| 5 |
+
FRONTEND_URL=https://subi333-ai_coding_assistant.hf.space
|
node-backend/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build stage
|
| 2 |
+
FROM node:20-alpine AS builder
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
COPY package*.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY . .
|
| 7 |
+
# Compiles your TypeScript code into a clean JavaScript /dist folder
|
| 8 |
+
RUN npm run build
|
| 9 |
+
|
| 10 |
+
# Stage 2: Production execution stage
|
| 11 |
+
FROM node:20-alpine AS production
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
ENV NODE_ENV=production
|
| 14 |
+
COPY package*.json ./
|
| 15 |
+
# Installs only lightweight production dependencies (no devDependencies)
|
| 16 |
+
RUN npm ci --only=production
|
| 17 |
+
# Copies only the compiled JavaScript files from the builder stage
|
| 18 |
+
COPY --from=builder /app/dist ./dist
|
| 19 |
+
# If you use raw assets like folders or views, copy them here too
|
| 20 |
+
|
| 21 |
+
EXPOSE 5000
|
| 22 |
+
CMD ["node", "dist/index.js"]
|
node-backend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
node-backend/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Ai-chat-backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node dist/index.js",
|
| 8 |
+
"dev": "powershell -Command \"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; nodemon --exec ts-node src/server.ts\"",
|
| 9 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 10 |
+
},
|
| 11 |
+
"keywords": [],
|
| 12 |
+
"author": "",
|
| 13 |
+
"license": "ISC",
|
| 14 |
+
"type": "commonjs",
|
| 15 |
+
"dependencies": {
|
| 16 |
+
"@socket.io/redis-adapter": "^8.3.0",
|
| 17 |
+
"@socket.io/redis-emitter": "^5.1.0",
|
| 18 |
+
"bcryptjs": "^3.0.2",
|
| 19 |
+
"bson": "^6.10.4",
|
| 20 |
+
"bullmq": "^5.66.1",
|
| 21 |
+
"busboy": "^1.6.0",
|
| 22 |
+
"cookie-parser": "^1.4.7",
|
| 23 |
+
"cors": "^2.8.5",
|
| 24 |
+
"crypto": "^1.0.1",
|
| 25 |
+
"dotenv": "^16.4.5",
|
| 26 |
+
"express": "^5.2.1",
|
| 27 |
+
"express-rate-limit": "^8.5.2",
|
| 28 |
+
"helmet": "^8.0.0",
|
| 29 |
+
"http": "^0.0.1-security",
|
| 30 |
+
"https": "^1.0.0",
|
| 31 |
+
"morgan": "^1.10.0",
|
| 32 |
+
"nanoid": "^3.3.11",
|
| 33 |
+
"nodemailer": "^7.0.11",
|
| 34 |
+
"pino": "^10.3.1",
|
| 35 |
+
"pino-pretty": "^13.1.3",
|
| 36 |
+
"redis": "^6.1.0",
|
| 37 |
+
"socket.io": "^4.8.1",
|
| 38 |
+
"ua-parser-js": "^2.0.6",
|
| 39 |
+
"ulid": "^3.0.1",
|
| 40 |
+
"uuid": "^11.1.0"
|
| 41 |
+
},
|
| 42 |
+
"devDependencies": {
|
| 43 |
+
"@types/cookie-parser": "^1.4.8",
|
| 44 |
+
"@types/cors": "^2.8.18",
|
| 45 |
+
"@types/crypto-js": "^4.2.2",
|
| 46 |
+
"@types/express": "^5.0.2",
|
| 47 |
+
"@types/node": "^22.19.7",
|
| 48 |
+
"@types/nodemailer": "^7.0.4",
|
| 49 |
+
"concurrently": "^9.2.1",
|
| 50 |
+
"cross-env": "^10.1.0",
|
| 51 |
+
"nodemon": "^3.1.14",
|
| 52 |
+
"ts-node": "^10.9.2",
|
| 53 |
+
"tsx": "^4.20.6",
|
| 54 |
+
"typescript": "^5.9.2"
|
| 55 |
+
}
|
| 56 |
+
}
|
node-backend/src/api/controllers/gpt-chat-controller.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from 'express';
|
| 2 |
+
import { GptChatQueueService } from '../../queues/gpt-chat-queue-services';
|
| 3 |
+
import logger from '../../logger';
|
| 4 |
+
|
| 5 |
+
export const useGptChatController = (gptChatQueueService: GptChatQueueService) => ({
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
gptChatController: async (req: Request, res: Response, next: NextFunction) => {
|
| 9 |
+
try {
|
| 10 |
+
const { aiInput, msgSession } = req.body;
|
| 11 |
+
logger.debug(`Prompt received - ${aiInput}`);
|
| 12 |
+
|
| 13 |
+
const anonId = req.headers['x-anonuser-id'] as string;
|
| 14 |
+
|
| 15 |
+
if (!anonId) { // 👈 runtime + TS check. Before using req.user.id check !req.user. See authmiddleware to see this
|
| 16 |
+
res.status(401).json({
|
| 17 |
+
success: false,
|
| 18 |
+
message: 'Unauthorized',
|
| 19 |
+
});
|
| 20 |
+
return;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
if (!aiInput) {
|
| 25 |
+
res.status(400).json({
|
| 26 |
+
success: false,
|
| 27 |
+
message: 'Missing prompt',
|
| 28 |
+
});
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Send early response
|
| 33 |
+
res.status(200).json({
|
| 34 |
+
success: true,
|
| 35 |
+
status: 'processing',
|
| 36 |
+
message: 'Prompt submitted to AI model',
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Send to queue
|
| 40 |
+
await gptChatQueueService.queueGptChatPrompt(aiInput, msgSession, anonId);
|
| 41 |
+
|
| 42 |
+
return;
|
| 43 |
+
} catch (err: any) {
|
| 44 |
+
let msg = 'Prompt submission error';
|
| 45 |
+
res.status(500).json({
|
| 46 |
+
success: false,
|
| 47 |
+
message: msg,
|
| 48 |
+
error: err.message,
|
| 49 |
+
});
|
| 50 |
+
return;
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
});
|
node-backend/src/api/controllers/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useGptChatController } from './gpt-chat-controller';
|
| 2 |
+
|
| 3 |
+
export { useGptChatController };
|
node-backend/src/api/routes/gpt-chat-router.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from 'express';
|
| 2 |
+
import { useGptChatController } from '../controllers';
|
| 3 |
+
import { GptChatQueueService } from '../../queues/gpt-chat-queue-services';
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
export function gptChatRoute(gptChatQueueService: GptChatQueueService) {
|
| 7 |
+
const router: Router = Router();
|
| 8 |
+
|
| 9 |
+
const chatController = useGptChatController(gptChatQueueService);
|
| 10 |
+
router.post('/aichat', chatController.gptChatController);
|
| 11 |
+
|
| 12 |
+
return router;
|
| 13 |
+
};
|
node-backend/src/api/routes/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from 'express';
|
| 2 |
+
import { gptChatRoute } from './gpt-chat-router';
|
| 3 |
+
import { GptChatQueueService } from '../../queues/gpt-chat-queue-services';
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
export function appRouter(gptChatQueueService: GptChatQueueService) {
|
| 7 |
+
const router: Router = Router();
|
| 8 |
+
|
| 9 |
+
router.use('/api/v1/users', gptChatRoute(gptChatQueueService));
|
| 10 |
+
|
| 11 |
+
return router;
|
| 12 |
+
}
|
node-backend/src/api/socket/socket.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Server as SocketIOServer, Socket } from 'socket.io';
|
| 2 |
+
import { Server as HTTPSServer } from 'https';
|
| 3 |
+
import http, { Server as HttpServer } from "http";
|
| 4 |
+
|
| 5 |
+
import dotenv from 'dotenv';
|
| 6 |
+
import logger from '../../logger';
|
| 7 |
+
import { createAdapter } from "@socket.io/redis-adapter";
|
| 8 |
+
import { getPubSubRedisInfra } from '../../redis';
|
| 9 |
+
import { createClient } from "redis";
|
| 10 |
+
|
| 11 |
+
const envFile: string = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
|
| 12 |
+
|
| 13 |
+
dotenv.config({ path: envFile });
|
| 14 |
+
|
| 15 |
+
const jwt_access_secret = process.env.JWT_ACCESS_SECRET || '';
|
| 16 |
+
let io: SocketIOServer;
|
| 17 |
+
|
| 18 |
+
const allowedOrigins = [
|
| 19 |
+
process.env.FRONTEND_URL,
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
export const initializeSocket = async (server: HTTPSServer | HttpServer): Promise<SocketIOServer> => {
|
| 23 |
+
|
| 24 |
+
io = new SocketIOServer(server, {
|
| 25 |
+
cors: {
|
| 26 |
+
origin: allowedOrigins[0],
|
| 27 |
+
methods: ['GET', 'POST'],
|
| 28 |
+
credentials: true || false,
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// const pubRedisInfra = getPubSubRedisInfra('pub-redis-infra');
|
| 33 |
+
// const subRedisInfra = getPubSubRedisInfra('sub-redis-infra');
|
| 34 |
+
// // For kafka service emit event using a redis client to listen
|
| 35 |
+
// io.adapter(createAdapter(pubRedisInfra.redis, subRedisInfra.redis));
|
| 36 |
+
const subscriber = createClient();
|
| 37 |
+
|
| 38 |
+
await subscriber.connect();
|
| 39 |
+
|
| 40 |
+
subscriber.subscribe("ai_response", (message: any) => { // This event is generating from python ai service
|
| 41 |
+
const data = JSON.parse(message);
|
| 42 |
+
// data.event = "gptChatRes"
|
| 43 |
+
io.to(`user:${data.userId}`).emit(data.event, {
|
| 44 |
+
result: data.payload.result,
|
| 45 |
+
msgSession: data.msg_session
|
| 46 |
+
}
|
| 47 |
+
);
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
io.on('connection', (socket: Socket) => { // in-built listener
|
| 51 |
+
logger.debug(`✅ A user connected: ${socket.id}`);
|
| 52 |
+
|
| 53 |
+
socket.on('disconnect', async() => { // in-built listener
|
| 54 |
+
logger.debug(`✅ A user disconnected: ${socket.id}`);
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
socket.on('admin-login', async(adminEmail: string) => { // Custom listener
|
| 58 |
+
logger.info(`✅ Admin logined : ${adminEmail}`); //
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
socket.on('admin-logout', async(adminEmail: string) => { // Custom listener
|
| 62 |
+
// removeAdmin(socket.id);
|
| 63 |
+
logger.info(`✅ Admin logged out : ${adminEmail}`); //
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
socket.on('join-user-room', (userId: string) => {
|
| 67 |
+
socket.join(`user:${userId}`);
|
| 68 |
+
logger.debug(`✅ A user joined the socket user-room: ${userId}`);
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
socket.on('leave-user-room', (userId: string) => {
|
| 72 |
+
socket.leave(`user:${userId}`);
|
| 73 |
+
logger.debug(`✅ A user left the socket user-room: ${userId}`);
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
//Test function
|
| 77 |
+
socket.on('hello', (callback) => {
|
| 78 |
+
logger.debug('Received hello');
|
| 79 |
+
callback('world');
|
| 80 |
+
});
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
return io;
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
export const broadcastGptChatRes = async (
|
| 87 |
+
gptChatRes: any
|
| 88 |
+
): Promise<void> => {
|
| 89 |
+
|
| 90 |
+
if (!io) {
|
| 91 |
+
logger.error('Socket.io is not initialized');
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
+
logger.debug('gptChatRes');
|
| 95 |
+
console.log("Emitting:", `user:${gptChatRes.userId}`);
|
| 96 |
+
io.to(`user:${gptChatRes.userId}`).emit('gptChatRes', { gptChatRes: gptChatRes });
|
| 97 |
+
};
|
node-backend/src/logger.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pino, { LoggerOptions, Logger } from 'pino';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
const envFile: string = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
|
| 6 |
+
dotenv.config({ path: envFile });
|
| 7 |
+
|
| 8 |
+
const isProduction = process.env.NODE_ENV === 'production';
|
| 9 |
+
|
| 10 |
+
const options: LoggerOptions = {
|
| 11 |
+
level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug')
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// 1. FOR PRODUCTION (JSON Output Layout)
|
| 15 |
+
// Appends a clean "caller" property tracking the file and line number
|
| 16 |
+
options.hooks = {
|
| 17 |
+
logMethod(inputArgs: any[], method, level) {
|
| 18 |
+
// Generate a quick call stack trace
|
| 19 |
+
const stack = new Error().stack;
|
| 20 |
+
|
| 21 |
+
// Split and target line 3 (which points back to where logger.info was called)
|
| 22 |
+
const callerLine = stack?.split('\n')[3];
|
| 23 |
+
|
| 24 |
+
// Use RegExp to isolate the file path, row, and column position
|
| 25 |
+
const match = callerLine?.match(/\((.*):(\d+):(\d+)\)/) || callerLine?.match(/at\s+(.*):(\d+):(\d+)/);
|
| 26 |
+
|
| 27 |
+
let callerContext = 'unknown';
|
| 28 |
+
if (match) {
|
| 29 |
+
const filePath = match[1];
|
| 30 |
+
const line = match[2];
|
| 31 |
+
const fileName = path.basename(filePath);
|
| 32 |
+
callerContext = `${fileName}:${line}`;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Inject the context directly into the log arguments array
|
| 36 |
+
return method.apply(this, [ { caller: callerContext }, ...inputArgs ]);
|
| 37 |
+
}
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
// Pretty logs only in dev
|
| 41 |
+
if (!isProduction) {
|
| 42 |
+
options.transport = {
|
| 43 |
+
target: 'pino-pretty',
|
| 44 |
+
options: {
|
| 45 |
+
colorize: true,
|
| 46 |
+
messageFormat: '\x1b[36m[{caller}]\x1b[0m {msg}',
|
| 47 |
+
translateTime: 'HH:MM:ss',
|
| 48 |
+
ignore: 'pid,hostname,caller'
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const logger: Logger = pino(options);
|
| 54 |
+
|
| 55 |
+
export default logger;
|
| 56 |
+
|
| 57 |
+
/* Usage
|
| 58 |
+
Log level Dev Prod
|
| 59 |
+
debug ✅ ❌
|
| 60 |
+
info ✅ ✅
|
| 61 |
+
warn ✅ ✅
|
| 62 |
+
error ✅ ✅ */
|
| 63 |
+
|
| 64 |
+
// e.g., output - in prod. - {"level":20,"time":...,"pid":12345,"hostname":"server-1","ip":"192.168.1.10","msg":"WiFi IP detected"}
|
| 65 |
+
// e.g., output in dev. -
|
| 66 |
+
// 15:42:11 DEBUG WiFi IP detected
|
| 67 |
+
// ip: 192.168.1.10
|
node-backend/src/queues/gpt-chat-queue-services.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Queue, QueueEvents, Job } from 'bullmq';
|
| 2 |
+
import IORedis from 'ioredis';
|
| 3 |
+
import logger from '../logger';
|
| 4 |
+
|
| 5 |
+
// Gpt Chat service class
|
| 6 |
+
export class GptChatQueueService {
|
| 7 |
+
|
| 8 |
+
private queue: Queue;
|
| 9 |
+
private queueEvents: QueueEvents; // ONLY for listening to events
|
| 10 |
+
private connection: IORedis;
|
| 11 |
+
private queueEventsRegistry: Map<string, QueueEvents> = new Map();
|
| 12 |
+
private queueName: string;
|
| 13 |
+
|
| 14 |
+
constructor(gptChatQueueRedis: IORedis, queue: Queue, queueName: string) {
|
| 15 |
+
this.queueName = queueName;
|
| 16 |
+
this.queue = queue;
|
| 17 |
+
this.connection = gptChatQueueRedis;
|
| 18 |
+
this.queueEvents = new QueueEvents(queueName, {
|
| 19 |
+
connection: this.connection
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
this.queueEventsRegistry.set(queueName, this.queueEvents); // Register queue events for later use, e.g., when shut down etc.
|
| 23 |
+
|
| 24 |
+
this.setupEventListeners();
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Setup all event listeners
|
| 28 |
+
private setupEventListeners(): void {
|
| 29 |
+
|
| 30 |
+
// Job started processing
|
| 31 |
+
this.queueEvents.on('active', async({ jobId }: { jobId: string }) => {
|
| 32 |
+
logger.debug(`Job ${jobId} started processing: ${this.queueName}`);
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Job completed
|
| 36 |
+
this.queueEvents.on('completed', async({ jobId }: { jobId: string }) => {
|
| 37 |
+
logger.debug(`Job ${jobId} completed: ${this.queueName}`);
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// Job failed
|
| 41 |
+
this.queueEvents.on('failed', async({ jobId, failedReason }: { jobId: string, failedReason: string }) => {
|
| 42 |
+
logger.error({failedReason}, `Job ${jobId} failed: ${this.queueName}`);
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Job progress
|
| 46 |
+
// this.queueEvents.on('progress', ({ jobId, data }: { jobId: string, data: number }) => {
|
| 47 |
+
// logger.info(`Job ${jobId} progress: ${data}%`);
|
| 48 |
+
// this.updateProgress(jobId, data);
|
| 49 |
+
// });
|
| 50 |
+
|
| 51 |
+
// Stalled job
|
| 52 |
+
// this.queueEvents.on('stalled', ({ jobId }: { jobId: string }) => {
|
| 53 |
+
// logger.warn(`Job ${jobId} stalled`);
|
| 54 |
+
// this.infoJobStatus(jobId, 'stalled');
|
| 55 |
+
// });
|
| 56 |
+
|
| 57 |
+
// Removed job
|
| 58 |
+
this.queueEvents.on('removed', ({ jobId }: { jobId: string }) => {
|
| 59 |
+
logger.debug(`Job ${jobId} removed: ${this.queueName}`);
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
// Error in queue
|
| 63 |
+
this.queueEvents.on('error', (error: Error) => {
|
| 64 |
+
logger.error({err: error}, `QueueEvents error: ${this.queueName}`);
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Queue gpt chat prompt
|
| 69 |
+
async queueGptChatPrompt(
|
| 70 |
+
prompt: string,
|
| 71 |
+
msgSession: string,
|
| 72 |
+
userId: string,
|
| 73 |
+
priority = 1
|
| 74 |
+
) {
|
| 75 |
+
return this.queue.add('gpt-chat-process', {
|
| 76 |
+
prompt,
|
| 77 |
+
msgSession,
|
| 78 |
+
userId,
|
| 79 |
+
type: 'gpt-chat-process',
|
| 80 |
+
queuedAt: new Date().toISOString()
|
| 81 |
+
}, {
|
| 82 |
+
priority,
|
| 83 |
+
delay: 100
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Get queue metrics
|
| 88 |
+
async getQueueMetrics() {
|
| 89 |
+
const [waiting, active, completed, failed, delayed] = await Promise.all([
|
| 90 |
+
this.queue.getWaitingCount(),
|
| 91 |
+
this.queue.getActiveCount(),
|
| 92 |
+
this.queue.getCompletedCount(),
|
| 93 |
+
this.queue.getFailedCount(),
|
| 94 |
+
this.queue.getDelayedCount()
|
| 95 |
+
]);
|
| 96 |
+
|
| 97 |
+
return {
|
| 98 |
+
waiting,
|
| 99 |
+
active,
|
| 100 |
+
completed,
|
| 101 |
+
failed,
|
| 102 |
+
delayed,
|
| 103 |
+
total: waiting + active + completed + failed + delayed
|
| 104 |
+
};
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Clean old jobs
|
| 108 |
+
async cleanOldJobs(olderThanHours = 24) {
|
| 109 |
+
const timestamp = Date.now() - (olderThanHours * 60 * 60 * 1000);
|
| 110 |
+
|
| 111 |
+
// Clean completed jobs
|
| 112 |
+
await this.queue.clean(timestamp, 1000, 'completed');
|
| 113 |
+
|
| 114 |
+
// Clean failed jobs (keep for longer)
|
| 115 |
+
const failedTimestamp = Date.now() - (olderThanHours * 3 * 60 * 60 * 1000);
|
| 116 |
+
await this.queue.clean(failedTimestamp, 1000, 'failed');
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
async closeQueueEvents(): Promise<void> {
|
| 121 |
+
|
| 122 |
+
if (!this.queueEventsRegistry || this.queueEventsRegistry.size === 0) {
|
| 123 |
+
logger.info(`✅ No active QueueEvents: ${this.queueName}`);
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
logger.info(`Closing ${this.queueEventsRegistry.size} ${this.queueEventsRegistry.size > 1 ? 'QueueEvents...' : 'QueueEvent...'}`);
|
| 128 |
+
|
| 129 |
+
await Promise.allSettled(
|
| 130 |
+
Array.from(this.queueEventsRegistry.entries()).map(async ([name, queue]) => {
|
| 131 |
+
try {
|
| 132 |
+
// logger.info(`Closing QueueEvents: ${name}`);
|
| 133 |
+
await queue.close();
|
| 134 |
+
logger.info(`✅ Closed QueueEvents: ${name}`);
|
| 135 |
+
} catch (err: any) {
|
| 136 |
+
logger.error({ err }, `❌ Failed closing QueueEvents: ${name}`);
|
| 137 |
+
throw err;
|
| 138 |
+
}
|
| 139 |
+
})
|
| 140 |
+
);
|
| 141 |
+
|
| 142 |
+
this.queueEventsRegistry.clear();
|
| 143 |
+
logger.info('✅ All QueueEvents closed');
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
}
|
node-backend/src/queues/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { QueueInfra } from './queue';
|
| 3 |
+
|
| 4 |
+
export {
|
| 5 |
+
QueueInfra,
|
| 6 |
+
};
|
node-backend/src/queues/queue.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import IORedis from 'ioredis';
|
| 2 |
+
import { Queue, Job } from 'bullmq';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
import logger from '../logger';
|
| 6 |
+
// import { workerRedis } from './workerRedis';
|
| 7 |
+
|
| 8 |
+
const envFile: string = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
|
| 9 |
+
|
| 10 |
+
dotenv.config({ path: envFile });
|
| 11 |
+
|
| 12 |
+
export class QueueInfra {
|
| 13 |
+
public readonly queueRedis: IORedis;
|
| 14 |
+
public readonly queue: Queue;
|
| 15 |
+
private initialized = false;
|
| 16 |
+
private queueName: string;
|
| 17 |
+
|
| 18 |
+
constructor(queueRedis: IORedis, queueName: string) {
|
| 19 |
+
this.queueRedis = queueRedis;
|
| 20 |
+
this.queueName = queueName;
|
| 21 |
+
|
| 22 |
+
// Create the queue for email related tasks
|
| 23 |
+
this.queue = new Queue(queueName, {
|
| 24 |
+
connection: this.queueRedis, // Pass connection object
|
| 25 |
+
|
| 26 |
+
defaultJobOptions: {
|
| 27 |
+
attempts: 3,
|
| 28 |
+
backoff: {
|
| 29 |
+
type: 'exponential', // Options: 'fixed' | 'exponential'
|
| 30 |
+
delay: 3000 // Delay in ms
|
| 31 |
+
},
|
| 32 |
+
removeOnComplete: {
|
| 33 |
+
count: 1000, // Keep last 1000 completed jobs
|
| 34 |
+
age: 24 * 3600 // OR keep for 24 hours (optional)
|
| 35 |
+
},
|
| 36 |
+
removeOnFail: {
|
| 37 |
+
count: 6000, // Keep last 5000 failed jobs
|
| 38 |
+
age: 72 * 3600 // OR keep for 72 hours (optional)
|
| 39 |
+
},
|
| 40 |
+
|
| 41 |
+
// Timeouts are handled at worker level
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
async init() {
|
| 48 |
+
if (this.initialized) return;
|
| 49 |
+
this.initialized = true;
|
| 50 |
+
|
| 51 |
+
// this.setupRedisEventListeners();
|
| 52 |
+
this.setupQueueEventListeners();
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/*private setupRedisEventListeners(): void {
|
| 56 |
+
// Add event handlers
|
| 57 |
+
this.emailQueueRedis.on('connect', () => {
|
| 58 |
+
logger.info('✅ Redis connected (Email queue redis)');
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
this.emailQueueRedis.on('ready', () => {
|
| 62 |
+
logger.info('✅ Redis ready (Email queue redis)');
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
this.emailQueueRedis.on('error', (err) => {
|
| 66 |
+
logger.error({ err }, `❌ Redis connection error (Email queue redis): ${err.message}`);
|
| 67 |
+
// Don't crash the app on Redis errors
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
this.emailQueueRedis.on('close', () => {
|
| 71 |
+
logger.info('⚠️ Redis connection closed (Email queue redis)');
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
this.emailQueueRedis.on('reconnecting', (delay: number) => {
|
| 75 |
+
logger.info(`🔄 Redis reconnecting in ${delay}ms (email queue redis)`);
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
this.emailQueueRedis.on('end', () => {
|
| 79 |
+
logger.info('🔴 Redis connection ended (email queue redis)');
|
| 80 |
+
});
|
| 81 |
+
} */
|
| 82 |
+
|
| 83 |
+
private setupQueueEventListeners(): void {
|
| 84 |
+
// The callback receives a Job object directly, not an args object
|
| 85 |
+
this.queue.on('waiting', (job: Job) => {
|
| 86 |
+
logger.info(`📥 Job ${job.id} is waiting: ${this.queueName}`);
|
| 87 |
+
logger.info(`Job data: ${job.data}: ${this.queueName}`);
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
this.queue.on('error', (err: Error) => {
|
| 91 |
+
logger.error({ err }, `❌ Queue error: ${err.message}: ${this.queueName}`);
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
this.queue.on('progress', (job: string, progress: any) => {
|
| 95 |
+
logger.debug(`📊 Job ${job} progress: ${progress} : ${this.queueName}`);
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
// Other available events:
|
| 99 |
+
this.queue.on('paused', () => {
|
| 100 |
+
logger.info(`⏸️ Queue paused: ${this.queueName}`);
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
this.queue.on('resumed', () => {
|
| 104 |
+
logger.info(`▶️ Queue resumed: ${this.queueName}`);
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
this.queue.on('cleaned', (jobs: string[], type: string) => {
|
| 108 |
+
logger.debug(`🧹 Cleaned ${jobs.length} ${type} jobs: ${this.queueName}`);
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
this.queue.on('removed', (job: string) => {
|
| 112 |
+
logger.debug(`🗑️ Job ${job} removed: ${this.queueName}`);
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
// Redis-specific events
|
| 116 |
+
this.queue.on('ioredis:close', () => {
|
| 117 |
+
logger.info(`🔌 Redis connection closed: ${this.queueName}`);
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Test connection on startup
|
| 122 |
+
public async testQueueRedisConnection() {
|
| 123 |
+
try {
|
| 124 |
+
await this.queueRedis.ping();
|
| 125 |
+
logger.info(`✅ Redis connection test passed: ${this.queueName}`);
|
| 126 |
+
} catch (err: any) {
|
| 127 |
+
logger.error({err}, `❌ Redis connection test failed: ${err.message}: ${this.queueName}`);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
public async closeQueue() {
|
| 132 |
+
logger.info(`⏳ Closing ${this.queueName}...`);
|
| 133 |
+
if (!this.queue) {
|
| 134 |
+
logger.info(`✅ ${this.queueName} is not active.`);
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
try {
|
| 139 |
+
await this.queue.close();
|
| 140 |
+
logger.info(`✅ ${this.queueName} closed`);
|
| 141 |
+
} catch (err: any) {
|
| 142 |
+
logger.error({ err }, `❌ Failed to close ${this.queueName}`);
|
| 143 |
+
throw err; // propagate to main shutdown
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
public async closeQueueRedis() {
|
| 148 |
+
logger.info(`⏳ Closing Redis connection: ${this.queueName}...`);
|
| 149 |
+
|
| 150 |
+
if(!this.queueRedis) {
|
| 151 |
+
logger.info(`✅ Redis connection: ${this.queueName} is not active`);
|
| 152 |
+
return;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
try {
|
| 156 |
+
await this.queueRedis.quit(); // Graceful Redis shutdown
|
| 157 |
+
logger.info(`✅ Redis connection closed: ${this.queueName} `);
|
| 158 |
+
} catch (err: any) {
|
| 159 |
+
logger.error({ err }, `❌ Failed to close Redis connection: ${this.queueName}`);
|
| 160 |
+
throw err; // propagate to main shutdown
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
/*
|
| 168 |
+
Never do:
|
| 169 |
+
|
| 170 |
+
try {
|
| 171 |
+
everything
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
Always do:
|
| 175 |
+
|
| 176 |
+
START log
|
| 177 |
+
if exists
|
| 178 |
+
try risky close
|
| 179 |
+
SUCCESS log
|
| 180 |
+
FAIL log
|
| 181 |
+
|
| 182 |
+
This is architecturally correct
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
*/
|
node-backend/src/redis/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { RedisClientInfra } from './redisClient';
|
| 2 |
+
import { APIRedisService } from './redisServices';
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
initPubSubRedis,
|
| 6 |
+
getPubSubRedisInfra,
|
| 7 |
+
closePubSubRedis
|
| 8 |
+
} from './socket_redis';
|
| 9 |
+
|
| 10 |
+
export {
|
| 11 |
+
RedisClientInfra,
|
| 12 |
+
APIRedisService,
|
| 13 |
+
initPubSubRedis,
|
| 14 |
+
getPubSubRedisInfra,
|
| 15 |
+
closePubSubRedis
|
| 16 |
+
};
|
node-backend/src/redis/redisClient.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/redis/client.ts
|
| 2 |
+
import IORedis from 'ioredis';
|
| 3 |
+
import { Queue, Job } from 'bullmq';
|
| 4 |
+
import dotenv from 'dotenv';
|
| 5 |
+
|
| 6 |
+
import logger from '../logger';
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
const envFile: string = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
|
| 10 |
+
|
| 11 |
+
dotenv.config({ path: envFile });
|
| 12 |
+
|
| 13 |
+
export class RedisClientInfra {
|
| 14 |
+
public readonly redis: IORedis;
|
| 15 |
+
// public readonly apiUserActionsQueue: Queue;
|
| 16 |
+
private initialized: boolean;
|
| 17 |
+
private redisName: string;
|
| 18 |
+
|
| 19 |
+
constructor(redisName: string) {
|
| 20 |
+
this.redis = new IORedis({
|
| 21 |
+
host: process.env.REDIS_HOST1 || 'localhost',
|
| 22 |
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
| 23 |
+
|
| 24 |
+
// Connection options
|
| 25 |
+
maxRetriesPerRequest: null,
|
| 26 |
+
enableReadyCheck: false,
|
| 27 |
+
enableOfflineQueue: true,
|
| 28 |
+
|
| 29 |
+
// Socket options (CRITICAL for preventing ECONNABORTED)
|
| 30 |
+
retryStrategy: (times: number) => {
|
| 31 |
+
logger.info(`Redis connection attempt(${redisName}) ${times}`);
|
| 32 |
+
if (times > 10) {
|
| 33 |
+
logger.info(`Too many redis connection attempts(${redisName})`);
|
| 34 |
+
return null;
|
| 35 |
+
}
|
| 36 |
+
const delay = Math.min(times * 50, 2000);
|
| 37 |
+
return delay;
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
// Timeout settings
|
| 41 |
+
connectTimeout: 10000, // 10 seconds to connect
|
| 42 |
+
commandTimeout: 30000, // 30 seconds for commands
|
| 43 |
+
|
| 44 |
+
reconnectOnError: (err: any) => {
|
| 45 |
+
// Reconnect on network errors but not on command errors
|
| 46 |
+
const targetErrors = ['READONLY', 'ECONNRESET', 'ETIMEDOUT'];
|
| 47 |
+
return targetErrors.some(error => err.message.includes(error));
|
| 48 |
+
},
|
| 49 |
+
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
this.initialized = false;
|
| 53 |
+
this.redisName = redisName;
|
| 54 |
+
|
| 55 |
+
/*
|
| 56 |
+
// Create a new queue for API service
|
| 57 |
+
this.apiUserActionsQueue = new Queue('api-user-actions-queue', {
|
| 58 |
+
connection: this.apiRedis, // Pass connection object
|
| 59 |
+
|
| 60 |
+
defaultJobOptions: {
|
| 61 |
+
attempts: 3,
|
| 62 |
+
backoff: {
|
| 63 |
+
type: 'exponential', // Options: 'fixed' | 'exponential
|
| 64 |
+
delay: 3000 // Delay in ms
|
| 65 |
+
},
|
| 66 |
+
removeOnComplete: {
|
| 67 |
+
count: 1000, // Keep last 1000 completed jobs
|
| 68 |
+
age: 24 * 3600 // OR keep for 24 hours (optional)
|
| 69 |
+
},
|
| 70 |
+
removeOnFail: {
|
| 71 |
+
count: 6000, // Keep last 5000 failed jobs
|
| 72 |
+
age: 72 * 3600 // OR keep for 72 hours (optional)
|
| 73 |
+
},
|
| 74 |
+
}
|
| 75 |
+
}); */
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
async init() {
|
| 79 |
+
if (this.initialized) return;
|
| 80 |
+
this.initialized = true;
|
| 81 |
+
|
| 82 |
+
this.setupRedisEventListeners();
|
| 83 |
+
// this.setupUserActionsQueueEventListeners();
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Handle connection events
|
| 87 |
+
private setupRedisEventListeners(): void {
|
| 88 |
+
this.redis.on('connect', () => {
|
| 89 |
+
logger.info(`✅ Redis Connected (${this.redisName})`);
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
this.redis.on('ready', () => {
|
| 93 |
+
logger.info(`🟢 Redis ready (${this.redisName})`);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
this.redis.on('error', (err: any) => {
|
| 97 |
+
logger.error({ err }, `❌ Redis connection error (${this.redisName}): ${err.message}`);
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
this.redis.on('close', () => {
|
| 101 |
+
logger.info(`⚠️ Redis connection closed (${this.redisName})`);
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
this.redis.on('reconnecting', (delay: number) => {
|
| 105 |
+
logger.info(`🔄 Redis reconnecting in ${delay}ms (${this.redisName})`);
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
this.redis.on('end', () => {
|
| 109 |
+
logger.info(`🔴 Redis connection ended (${this.redisName})`);
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/*private setupApiUserActionsQueueEventListeners(): void {
|
| 115 |
+
// The callback receives a Job object directly, not an args object
|
| 116 |
+
this.apiUserActionsQueue.on('waiting', (job: Job) => {
|
| 117 |
+
logger.info(`📥 Job ${job.id} is waiting`);
|
| 118 |
+
logger.info(`Job data: ${job.data}`);
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
this.apiUserActionsQueue.on('error', (err: Error) => {
|
| 122 |
+
logger.error({ err }, `❌ Queue error: ${err.message}`);
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
this.apiUserActionsQueue.on('progress', (job: string, progress: any) => {
|
| 126 |
+
logger.debug(`📊 Job ${job} progress: ${progress}`);
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
// Other available events:
|
| 130 |
+
this.apiUserActionsQueue.on('paused', () => {
|
| 131 |
+
logger.info('⏸️ Queue paused');
|
| 132 |
+
});
|
| 133 |
+
|
| 134 |
+
this.apiUserActionsQueue.on('resumed', () => {
|
| 135 |
+
logger.info('▶️ Queue resumed');
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
this.apiUserActionsQueue.on('cleaned', (jobs: string[], type: string) => {
|
| 139 |
+
logger.debug(`🧹 Cleaned ${jobs.length} ${type} jobs`);
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
this.apiUserActionsQueue.on('removed', (job: string) => {
|
| 143 |
+
logger.debug(`🗑️ Job ${job} removed`);
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
// Redis-specific events
|
| 147 |
+
this.apiUserActionsQueue.on('ioredis:close', () => {
|
| 148 |
+
logger.info('🔌 Redis connection closed(API redis)');
|
| 149 |
+
});
|
| 150 |
+
} */
|
| 151 |
+
|
| 152 |
+
// Test connection on startup
|
| 153 |
+
public async testRedisConnection() {
|
| 154 |
+
try {
|
| 155 |
+
await this.redis.ping();
|
| 156 |
+
logger.info(`✅ Redis connection test passed(${this.redisName})`);
|
| 157 |
+
} catch (err: any) {
|
| 158 |
+
logger.error({err}, `❌ Redis connection test failed(${this.redisName}): ${err.message}`);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
async redisGracefulShutdown(): Promise<void> {
|
| 163 |
+
logger.info(`⏳ Closing Redis connection for ${this.redisName}...`);
|
| 164 |
+
|
| 165 |
+
if(!this.redis) {
|
| 166 |
+
logger.info(`✅ Redis connection for ${this.redisName} is not active`);
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
try {
|
| 171 |
+
await this.redis.quit(); // Graceful Redis shutdown
|
| 172 |
+
logger.info(`✅ Redis connection closed for ${this.redisName}`);
|
| 173 |
+
} catch (err: any) {
|
| 174 |
+
logger.error({ err }, `❌ Failed to close redis connection for ${this.redisName}`);
|
| 175 |
+
throw err; // propagate to main shutdown
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
/*
|
| 183 |
+
If age exceeded → delete
|
| 184 |
+
OR
|
| 185 |
+
If count exceeded → delete
|
| 186 |
+
*/
|
node-backend/src/redis/redisServices.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import IORedis from 'ioredis';
|
| 3 |
+
import { Queue } from 'bullmq';
|
| 4 |
+
|
| 5 |
+
export class APIRedisService {
|
| 6 |
+
// public connection: IORedis;
|
| 7 |
+
// public userActionsQueue: Queue;
|
| 8 |
+
|
| 9 |
+
constructor(
|
| 10 |
+
private readonly connection: IORedis,/*private readonly userActionsQueue: Queue*/) {
|
| 11 |
+
// this.connection = redis;
|
| 12 |
+
// this.userActionsQueue = queue;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
async set(key: string, value: any, ttlSeconds?: number) {
|
| 16 |
+
const data = JSON.stringify(value);
|
| 17 |
+
|
| 18 |
+
if (ttlSeconds) {
|
| 19 |
+
await this.connection.set(key, data, 'EX', ttlSeconds);
|
| 20 |
+
} else {
|
| 21 |
+
await this.connection.set(key, data);
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
async get<T = any>(key: string): Promise<T | null> {
|
| 27 |
+
const data = await this.connection.get(key);
|
| 28 |
+
return data ? JSON.parse(data) : null;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
async del(key: string) {
|
| 33 |
+
await this.connection.del(key);
|
| 34 |
+
}
|
| 35 |
+
}
|
node-backend/src/redis/socket_redis.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { RedisClientInfra } from './';
|
| 2 |
+
|
| 3 |
+
let pubRedisInfra: RedisClientInfra;
|
| 4 |
+
let subRedisInfra: RedisClientInfra;
|
| 5 |
+
|
| 6 |
+
export async function initPubSubRedis(pubRedisName: string, subRedisName: string) {
|
| 7 |
+
|
| 8 |
+
pubRedisInfra = new RedisClientInfra(pubRedisName);
|
| 9 |
+
subRedisInfra = new RedisClientInfra(subRedisName);
|
| 10 |
+
|
| 11 |
+
await pubRedisInfra.init();
|
| 12 |
+
await subRedisInfra.init();
|
| 13 |
+
|
| 14 |
+
await pubRedisInfra.testRedisConnection();
|
| 15 |
+
await subRedisInfra.testRedisConnection();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function getPubSubRedisInfra(label: string) {
|
| 19 |
+
if (label === 'sub-redis-infra') {
|
| 20 |
+
return subRedisInfra;
|
| 21 |
+
} else {
|
| 22 |
+
return pubRedisInfra;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export async function closePubSubRedis() {
|
| 27 |
+
if(pubRedisInfra) {
|
| 28 |
+
await pubRedisInfra.redisGracefulShutdown();
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if(subRedisInfra) {
|
| 32 |
+
await subRedisInfra.redisGracefulShutdown();
|
| 33 |
+
}
|
| 34 |
+
}
|
node-backend/src/server.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express, { Request, Response, NextFunction, Express } from 'express'; //use express module to create a server obj
|
| 2 |
+
import IORedis from 'ioredis';
|
| 3 |
+
// import { Queue } from 'bullmq';
|
| 4 |
+
import { rateLimit, ipKeyGenerator } from 'express-rate-limit';
|
| 5 |
+
|
| 6 |
+
//For flutter app .dev purpose
|
| 7 |
+
import http, { Server as HttpServer } from "http";
|
| 8 |
+
|
| 9 |
+
import https, { Server as HttpsServer } from 'https';
|
| 10 |
+
import cors from 'cors';
|
| 11 |
+
import helmet from 'helmet';
|
| 12 |
+
import path from 'path';
|
| 13 |
+
import dotenv from 'dotenv';
|
| 14 |
+
import cookieParser from 'cookie-parser';
|
| 15 |
+
|
| 16 |
+
import logger from './logger';
|
| 17 |
+
|
| 18 |
+
import { appRouter } from './api/routes/index'; // import a centralized routes group
|
| 19 |
+
import { initializeSocket } from './api/socket/socket';
|
| 20 |
+
import { RedisClientInfra } from './redis';
|
| 21 |
+
import { QueueInfra } from './queues';
|
| 22 |
+
// import { GptChatWorker } from './workers';
|
| 23 |
+
import { GptChatQueueService } from './queues/gpt-chat-queue-services';
|
| 24 |
+
import {
|
| 25 |
+
initPubSubRedis,
|
| 26 |
+
closePubSubRedis
|
| 27 |
+
} from './redis';
|
| 28 |
+
|
| 29 |
+
import * as fs from 'fs';
|
| 30 |
+
|
| 31 |
+
// Load env file based on NODE_ENV
|
| 32 |
+
const envFile: string = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
|
| 33 |
+
|
| 34 |
+
dotenv.config({ path: envFile });
|
| 35 |
+
|
| 36 |
+
const certPath = process.env.SSL_CERT || './ssl/certificate.crt';
|
| 37 |
+
const keyPath = process.env.SSL_KEY || './ssl/certificate.key';
|
| 38 |
+
|
| 39 |
+
const allowedOrigins = [process.env.FRONTEND_URL as string];
|
| 40 |
+
|
| 41 |
+
const host: string | undefined = process.env.HOST;
|
| 42 |
+
const port: number = Number(process.env.PORT || 7860);
|
| 43 |
+
|
| 44 |
+
const app: Express = express(); // create a server
|
| 45 |
+
|
| 46 |
+
// Global rate limiting
|
| 47 |
+
const globalLimiter = rateLimit({
|
| 48 |
+
windowMs: 1 * 1000, // 1 second
|
| 49 |
+
max: 10000, // 10,000 requests per second globally
|
| 50 |
+
handler: (req: Request, res: Response) => {
|
| 51 |
+
res.status(429).json({ error: 'System overloaded' });
|
| 52 |
+
}
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
// Express rate limiter configuration
|
| 56 |
+
const ipLimiter = rateLimit({
|
| 57 |
+
windowMs: 10 * 1000, // 10 Sec window
|
| 58 |
+
max: 60, // Maximum 60 requests per 10 Secs per user
|
| 59 |
+
keyGenerator: (req: Request) => ipKeyGenerator(req.ip ?? 'unknown-ip'),
|
| 60 |
+
message: { message: 'Too many requests from this IP, please try again after some time' },
|
| 61 |
+
headers: true,
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
const corsOptions = {
|
| 65 |
+
origin: function (origin: string | undefined, callback: Function) {
|
| 66 |
+
// Allow requests with no origin (like mobile apps or curl requests)
|
| 67 |
+
console.log("CORS callback origin:", origin);
|
| 68 |
+
console.log("Allowed:", allowedOrigins);
|
| 69 |
+
|
| 70 |
+
if (!origin) {
|
| 71 |
+
console.log("No origin");
|
| 72 |
+
return callback(null, true);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if (allowedOrigins.includes(origin)) {
|
| 76 |
+
console.log("Origin allowed");
|
| 77 |
+
callback(null, origin);
|
| 78 |
+
} else {
|
| 79 |
+
console.log("Cors blocked");
|
| 80 |
+
return callback(new Error("Not allowed by CORS"));
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
},
|
| 84 |
+
credentials: true,
|
| 85 |
+
exposedHeaders: ['Authorization'],
|
| 86 |
+
allowedHeaders: [
|
| 87 |
+
'Content-Type',
|
| 88 |
+
'Authorization',
|
| 89 |
+
'X-Requested-With',
|
| 90 |
+
'x-socket-id',
|
| 91 |
+
'x-anonuser-id',
|
| 92 |
+
// 'Cookie'
|
| 93 |
+
],
|
| 94 |
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
if (process.env.NODE_ENV !== 'production') {
|
| 98 |
+
app.use((req, res, next) => {
|
| 99 |
+
console.log("Method:", req.method);
|
| 100 |
+
console.log("Origin:", req.headers.origin);
|
| 101 |
+
next();
|
| 102 |
+
});
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
app.use(cors(corsOptions));
|
| 106 |
+
app.options(/.*/, cors(corsOptions));
|
| 107 |
+
|
| 108 |
+
app.use(globalLimiter);
|
| 109 |
+
app.use(ipLimiter);
|
| 110 |
+
app.use(cookieParser());
|
| 111 |
+
|
| 112 |
+
app.use(helmet());
|
| 113 |
+
app.use(express.json()); //middleware to parse JSON
|
| 114 |
+
|
| 115 |
+
let server: HttpsServer | HttpServer;
|
| 116 |
+
if (process.env.NODE_ENV === 'production') {
|
| 117 |
+
// HTTP for production
|
| 118 |
+
server = http.createServer(app); //This binding is required to handle some low-level server event handling
|
| 119 |
+
} else {
|
| 120 |
+
// HTTPS for development
|
| 121 |
+
const cert: Buffer = fs.readFileSync(certPath);
|
| 122 |
+
const key: Buffer = fs.readFileSync(keyPath);
|
| 123 |
+
server = https.createServer({ key, cert }, app); //This binding is required to handle some low-level server event handling
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
let aiQueueRedisInfra: RedisClientInfra;
|
| 127 |
+
let aiQueueInfra: QueueInfra;
|
| 128 |
+
// let gptChatWorker: GptChatWorker;
|
| 129 |
+
let gptChatQueueService: GptChatQueueService;
|
| 130 |
+
|
| 131 |
+
async function initQueueWorkers() {
|
| 132 |
+
// Setup redis connection
|
| 133 |
+
aiQueueRedisInfra = new RedisClientInfra('gpt-chat-redis');
|
| 134 |
+
aiQueueRedisInfra.init();
|
| 135 |
+
aiQueueRedisInfra.testRedisConnection();
|
| 136 |
+
|
| 137 |
+
// Setup and start queue
|
| 138 |
+
aiQueueInfra = new QueueInfra(aiQueueRedisInfra.redis, 'gpt-chat');
|
| 139 |
+
aiQueueInfra.init();
|
| 140 |
+
aiQueueInfra.testQueueRedisConnection();
|
| 141 |
+
|
| 142 |
+
// Setup and start worker
|
| 143 |
+
// gptChatWorker = new GptChatWorker(aiQueueRedisInfra.redis, 'gpt-chat');
|
| 144 |
+
// gptChatWorker.init();
|
| 145 |
+
|
| 146 |
+
gptChatQueueService = new GptChatQueueService(aiQueueRedisInfra.redis, aiQueueInfra.queue, 'gpt-chat');
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
async function bootStrap() {
|
| 151 |
+
|
| 152 |
+
try {
|
| 153 |
+
logger.info('🚀 Server starting...');
|
| 154 |
+
|
| 155 |
+
// Redis socket notification
|
| 156 |
+
await initPubSubRedis('api-pub-redis', 'api-sub-redis');
|
| 157 |
+
|
| 158 |
+
await initializeSocket(server); //Initialize socket.io with the server
|
| 159 |
+
|
| 160 |
+
// Start API Queue and workers
|
| 161 |
+
await initQueueWorkers();
|
| 162 |
+
|
| 163 |
+
if (gptChatQueueService) {
|
| 164 |
+
app.use(appRouter(gptChatQueueService));
|
| 165 |
+
} else {
|
| 166 |
+
logger.error('Some services are still not ready, restart the server');
|
| 167 |
+
}
|
| 168 |
+
server.listen(port , () => {
|
| 169 |
+
logger.info(`Server running on ${host}:${port}`);
|
| 170 |
+
});
|
| 171 |
+
} catch (err: any) {
|
| 172 |
+
logger.error('Server startup failed', err);
|
| 173 |
+
process.exit(1);
|
| 174 |
+
}
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
bootStrap();
|
| 178 |
+
|
| 179 |
+
//Confirm that the server is listening after successfull start
|
| 180 |
+
server.on('listening', () => {
|
| 181 |
+
logger.info('Server successfully started');
|
| 182 |
+
logger.info(`Server is listening on localhost:${port}`);
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
//Handle error when server starts
|
| 186 |
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
| 187 |
+
logger.error({err}, `Server error: , ${err.message}`);
|
| 188 |
+
if (err.code === 'EADDRINUSE') { //In case any error happened by the selected port number
|
| 189 |
+
logger.warn(`Port ${port} is already in use`);
|
| 190 |
+
//////////////////////////////////////
|
| 191 |
+
//code to handle to manually select a port and manual restart of server using electron
|
| 192 |
+
//////////////////////////////////////
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
server.on('close', () => {
|
| 197 |
+
logger.info('All API connections closed');
|
| 198 |
+
});
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
const closeServer = () => {
|
| 202 |
+
return new Promise<void>((resolve) => {
|
| 203 |
+
const timeout = setTimeout(() => {
|
| 204 |
+
logger.warn('Force closing server (timeout)');
|
| 205 |
+
resolve();
|
| 206 |
+
}, 10000);
|
| 207 |
+
|
| 208 |
+
server.close(() => {
|
| 209 |
+
clearTimeout(timeout); // ⭐ stop timeout
|
| 210 |
+
logger.info('HTTP server closed');
|
| 211 |
+
resolve();
|
| 212 |
+
});
|
| 213 |
+
});
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
//Server and database shut down function
|
| 218 |
+
const serverShutdown = async (): Promise<void> => {
|
| 219 |
+
|
| 220 |
+
try {
|
| 221 |
+
logger.info('Server shutting down started...');
|
| 222 |
+
await closeServer();
|
| 223 |
+
// Stop all Workers, Queueu events, Queueu and Redis for queue
|
| 224 |
+
if (aiQueueInfra) {
|
| 225 |
+
// await gptChatWorker.closeGptChatWorker(); // First close all workers related to the queue
|
| 226 |
+
await gptChatQueueService.closeQueueEvents(); // Then close all queue events related to queue
|
| 227 |
+
await aiQueueInfra.closeQueue(); // Then close all queue
|
| 228 |
+
await aiQueueInfra.closeQueueRedis(); // At last close the redis connection which makes the queue possible
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
} catch (err: any) {
|
| 232 |
+
logger.error({err}, '❌ Error stopping schedulers');
|
| 233 |
+
}
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
const withTimeout = (promise: Promise<any>, timeoutMs: number, operation: string): Promise<any> => {
|
| 237 |
+
return Promise.race([
|
| 238 |
+
promise,
|
| 239 |
+
new Promise((_, reject) =>
|
| 240 |
+
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms for ${operation}`)), timeoutMs)
|
| 241 |
+
)
|
| 242 |
+
]);
|
| 243 |
+
};
|
| 244 |
+
|
| 245 |
+
let isGracefullShuttingDown = false;
|
| 246 |
+
|
| 247 |
+
const gracefulShutdown = async (signal?: string): Promise<void> => {
|
| 248 |
+
if (isGracefullShuttingDown) {
|
| 249 |
+
logger.warn(`Shutdown already running. Ignoring ${signal}`);
|
| 250 |
+
return;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
isGracefullShuttingDown = true;
|
| 254 |
+
logger.info(`Received ${signal}. Starting graceful shutdown...`);
|
| 255 |
+
|
| 256 |
+
try {
|
| 257 |
+
logger.info('🚨 Starting graceful shutdown process...');
|
| 258 |
+
|
| 259 |
+
logger.info('🛑 Server shutdown called...');
|
| 260 |
+
await withTimeout(serverShutdown(), 60000, 'serverShutdown');
|
| 261 |
+
logger.info('✅ Server shutdown completed');
|
| 262 |
+
|
| 263 |
+
logger.info('🎉 All cleanup completed successfully');
|
| 264 |
+
process.exit(0);
|
| 265 |
+
|
| 266 |
+
} catch (err: any) {
|
| 267 |
+
logger.error({err}, '❌ Shutdown error');
|
| 268 |
+
process.exit(1);
|
| 269 |
+
}
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
process.removeAllListeners('SIGTERM');
|
| 273 |
+
process.removeAllListeners('SIGINT');
|
| 274 |
+
|
| 275 |
+
// Signal listeners
|
| 276 |
+
process.on('SIGTERM', gracefulShutdown);
|
| 277 |
+
process.on('SIGINT', gracefulShutdown);
|
node-backend/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
| 4 |
+
"module": "commonjs",
|
| 5 |
+
"typeRoots": ["./src/api/types", "./node_modules/@types"],
|
| 6 |
+
"types": ["node", "express"],
|
| 7 |
+
"outDir": "./dist", /* Specify what module code is generated. */
|
| 8 |
+
"rootDir": "./src",
|
| 9 |
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
| 10 |
+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
| 11 |
+
"strict": true, /* Enable all strict type-checking options. */
|
| 12 |
+
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
| 13 |
+
},
|
| 14 |
+
|
| 15 |
+
}
|
python-ai-service/QueueAndWorker/gpt_chat.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# from llama_cpp import Llama
|
| 2 |
+
import logging
|
| 3 |
+
from .model_manager import get_model
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
def gpt_chat_process(prompt: str, msg_session: str, user_id: str):
|
| 8 |
+
try:
|
| 9 |
+
model = get_model()
|
| 10 |
+
if not model:
|
| 11 |
+
logger.error("Model instance could not be retrieved")
|
| 12 |
+
return {'error': 'Model unavailable', 'user_id': user_id}
|
| 13 |
+
|
| 14 |
+
response = model.create_chat_completion(
|
| 15 |
+
messages=[
|
| 16 |
+
{
|
| 17 |
+
"role": "system",
|
| 18 |
+
"content": (
|
| 19 |
+
"You are Qwen2.5-Coder, a specialized AI coding assistant. "
|
| 20 |
+
"Your task is to analyze the user request and apply these strict rules:\n\n"
|
| 21 |
+
|
| 22 |
+
"1. If the request asks about who developed you, who your creator is, or asks for "
|
| 23 |
+
"developer profile information, you MUST reply with exactly this message format:\n"
|
| 24 |
+
"\"This coding assistant was developed by [Subeesh Palamadathil]. You can find more information "
|
| 25 |
+
"and connect on GitHub: [GitHub Profile](https://github.com/Subeesh4020) and "
|
| 26 |
+
"LinkedIn: [LinkedIn Profile](https://www.linkedin.com/in/subeesh-palamadathil-170249193/).\"\n\n"
|
| 27 |
+
|
| 28 |
+
"1. If the request is a general greeting, conversational chit-chat, or entirely unrelated to "
|
| 29 |
+
"software development, programming, or coding, you MUST reply with exactly this sentence: "
|
| 30 |
+
"'Please ask any coding related questions. I am a coding assistant.' Do not provide any code.\n\n"
|
| 31 |
+
"2. If the request is related to programming, provide only clean, efficient, and well-commented "
|
| 32 |
+
"code wrapped in a standard markdown code block. Do not include conversational filler or explanations."
|
| 33 |
+
)
|
| 34 |
+
# "content": (
|
| 35 |
+
# "You are an expert software engineer. Provide only clean, efficient, "
|
| 36 |
+
# "and well-commented code based on the user request. Do not include "
|
| 37 |
+
# "any introductory or concluding conversational explanations. Output "
|
| 38 |
+
# "the response wrapped in a standard markdown code block."
|
| 39 |
+
# )
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"role": "user",
|
| 43 |
+
# Example prompt: "Write a typescript function to validate email"
|
| 44 |
+
"content": f"{prompt}"
|
| 45 |
+
}
|
| 46 |
+
],
|
| 47 |
+
temperature=0.2, # Lower temperature is critical for accurate, deterministic code
|
| 48 |
+
max_tokens=1000 # Increased tokens since code files are larger than social posts
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
choices = response.get("choices", [])
|
| 52 |
+
if not choices:
|
| 53 |
+
raise ValueError("Model returned an empty choices array")
|
| 54 |
+
|
| 55 |
+
# Accessing the message content safely
|
| 56 |
+
message = choices[0].get("message", {})
|
| 57 |
+
content = message.get("content", "")
|
| 58 |
+
|
| 59 |
+
return {
|
| 60 |
+
'result': content.strip(),
|
| 61 |
+
'msg_session': msg_session,
|
| 62 |
+
'user_id': user_id
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.exception(f"Failed to generate code for user {user_id}")
|
| 67 |
+
return {'error': 'Code generation failed', 'user_id': user_id}
|
python-ai-service/QueueAndWorker/model_manager.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from llama_cpp import Llama
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
_model = None
|
| 10 |
+
|
| 11 |
+
def load_model():
|
| 12 |
+
global _model
|
| 13 |
+
|
| 14 |
+
if _model is None:
|
| 15 |
+
logger.info("Qwen2.5-coder-3b-instruct model loading started")
|
| 16 |
+
try:
|
| 17 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 18 |
+
model_path_qwen = BASE_DIR / "AiModels" / "CodingModel" / "qwen2.5-coder-3b-instruct-q4_k_m.gguf"
|
| 19 |
+
model_path_qwen = os.environ.get('MODEL_PATH', str(model_path_qwen))
|
| 20 |
+
|
| 21 |
+
_model = Llama(
|
| 22 |
+
model_path=model_path_qwen,
|
| 23 |
+
n_ctx=4096,
|
| 24 |
+
n_threads=2, # adjust based on your CPU
|
| 25 |
+
verbose=False
|
| 26 |
+
)
|
| 27 |
+
logger.info("✅ Qwen2.5-coder-3b-instruct model loaded")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logger.exception("Failed to load Qwen2.5-coder-3b-instruct model")
|
| 30 |
+
|
| 31 |
+
def get_model():
|
| 32 |
+
return _model
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# Do not bake the .gguf file into your Docker image. This makes your Docker builds incredibly slow and
|
| 36 |
+
# fills up your local storage.The Best Practice: Mount the folder containing your AI models as a Docker Volume
|
| 37 |
+
# on your Oracle Server.
|
| 38 |
+
# This allows your container to read the model file directly from the host system disk.
|
python-ai-service/QueueAndWorker/queue_manager.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import asyncio
|
| 2 |
+
# from typing import Dict, Any
|
| 3 |
+
# from bullmq import Queue
|
| 4 |
+
# import logging
|
| 5 |
+
|
| 6 |
+
# logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
# class QueueManager:
|
| 9 |
+
# def __init__(self, queue_name: str, redis_url: str = "redis://127.0.0.1:6379"):
|
| 10 |
+
# self.queue_name = queue_name
|
| 11 |
+
# self.redis_url = redis_url
|
| 12 |
+
# self.queue = None
|
| 13 |
+
|
| 14 |
+
# async def connect(self) -> None:
|
| 15 |
+
# """Initializes the connection to the BullMQ Redis queue."""
|
| 16 |
+
# if not self.queue:
|
| 17 |
+
# # Passing connection string via opts dict
|
| 18 |
+
# self.queue = Queue(self.queue_name, opts={"connection": self.redis_url})
|
| 19 |
+
# logger.info(f"📡 Connected to Queue: {self.queue_name}")
|
| 20 |
+
|
| 21 |
+
# async def add_job(self, job_name: str, data: Dict[str, Any], priority: int = 0) -> Any:
|
| 22 |
+
# """Adds a job into the queue with optional configurations."""
|
| 23 |
+
# if not self.queue:
|
| 24 |
+
# await self.connect()
|
| 25 |
+
|
| 26 |
+
# # Inject the model string cleanly into the payload dictionary
|
| 27 |
+
# opts = {"priority": priority} if priority > 0 else {}
|
| 28 |
+
# # Call queue.add using only the valid signature parameters
|
| 29 |
+
# job = await self.queue.add(job_name, data, opts)
|
| 30 |
+
# logger.debug(f"➕ Job added! ID: {job.id} | Name: {job_name}")
|
| 31 |
+
# return job
|
| 32 |
+
|
| 33 |
+
# async def disconnect(self) -> None:
|
| 34 |
+
# """Gracefully closes the queue connections."""
|
| 35 |
+
# if self.queue:
|
| 36 |
+
# await self.queue.close()
|
| 37 |
+
# self.queue = None
|
| 38 |
+
# logger.info(f"🛑 Disconnected from Queue: {self.queue_name}")
|
python-ai-service/QueueAndWorker/worker_manager.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
from typing import Any
|
| 5 |
+
import logging
|
| 6 |
+
from bullmq import Worker, Job
|
| 7 |
+
import redis
|
| 8 |
+
# from socketio_emitter import Emitter
|
| 9 |
+
|
| 10 |
+
from .gpt_chat import gpt_chat_process
|
| 11 |
+
# from TextGeneration.generate_ai_desc_producer import send_ai_gen_desc_message
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
class WorkerManager:
|
| 15 |
+
def __init__(self, queue_name: str, redis_url: str = "redis://127.0.0.1:6379"):
|
| 16 |
+
self.queue_name = queue_name
|
| 17 |
+
# 2. Defensive handling in case someone explicitly passes None as redis_url
|
| 18 |
+
if redis_url is None:
|
| 19 |
+
redis_url = "redis://127.0.0.1:6379"
|
| 20 |
+
|
| 21 |
+
self.redis_url = redis_url
|
| 22 |
+
self.worker = None
|
| 23 |
+
self.redis_client = redis.Redis.from_url(redis_url)
|
| 24 |
+
# self.io_emitter = Emitter(client=self.redis_client, namespace="/")
|
| 25 |
+
|
| 26 |
+
async def job_processor(self, job: Job, token: str) -> Any:
|
| 27 |
+
"""The core business logic that runs when a job is picked up."""
|
| 28 |
+
logger.debug(f"⚙️ Processing Job {job.id} [{job.name}]...")
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
# Simulate background task workload (e.g., calling an LLM or database script)
|
| 32 |
+
# await asyncio.sleep(2)
|
| 33 |
+
if job.name == 'gpt-chat-process':
|
| 34 |
+
prompt = job.data.get('prompt')
|
| 35 |
+
msg_session = job.data.get('msgSession')
|
| 36 |
+
user_id = job.data.get('userId')
|
| 37 |
+
|
| 38 |
+
result = await asyncio.to_thread(gpt_chat_process, prompt, msg_session, user_id)
|
| 39 |
+
|
| 40 |
+
logger.debug(result)
|
| 41 |
+
text_content = result.get('result')
|
| 42 |
+
msg_session = result.get('msg_session')
|
| 43 |
+
user_id = result.get('user_id')
|
| 44 |
+
# Regex tracking to grab exactly what sits inside the markdown ticks
|
| 45 |
+
code_match = re.search(r'```(?:python)?\n(.*?)```', text_content, re.DOTALL)
|
| 46 |
+
clean_code = code_match.group(1).strip() if code_match else text_content.strip()
|
| 47 |
+
|
| 48 |
+
self.redis_client.publish(
|
| 49 |
+
"ai_response",
|
| 50 |
+
json.dumps({
|
| 51 |
+
"event": "gptChatRes",
|
| 52 |
+
"userId": user_id,
|
| 53 |
+
"msg_session": msg_session,
|
| 54 |
+
"payload": {
|
| 55 |
+
"result": clean_code
|
| 56 |
+
}
|
| 57 |
+
})
|
| 58 |
+
)
|
| 59 |
+
except Exception:
|
| 60 |
+
logger.exception("Message send failed")
|
| 61 |
+
|
| 62 |
+
# This return value will be stored in Redis as the job result
|
| 63 |
+
return {"status": "success", "processed_data": job.data}
|
| 64 |
+
|
| 65 |
+
def setup_event_listeners(self) -> None:
|
| 66 |
+
"""Hooks into specific job lifecycles using event callbacks."""
|
| 67 |
+
if not self.worker:
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
# @self.worker.on("completed")
|
| 71 |
+
def on_completed(job: Job, result: Any, token: str):
|
| 72 |
+
logger.debug(f"✅ Event: Job {job.id} completed! Result: {result}")
|
| 73 |
+
|
| 74 |
+
self.worker.on("completed", on_completed)
|
| 75 |
+
|
| 76 |
+
# @self.worker.on("failed")
|
| 77 |
+
def on_failed(job: Job, error: Exception, token: str):
|
| 78 |
+
logger.debug(f"❌ Event: Job {job.id} failed! Reason: {error}")
|
| 79 |
+
|
| 80 |
+
self.worker.on("failed", on_failed)
|
| 81 |
+
|
| 82 |
+
# @self.worker.on("active")
|
| 83 |
+
def on_active(job: Job, token: str):
|
| 84 |
+
logger.debug(f"🏃 Event: Job {job.id} is now active.")
|
| 85 |
+
|
| 86 |
+
self.worker.on("active", on_active)
|
| 87 |
+
|
| 88 |
+
async def start(self) -> None:
|
| 89 |
+
"""Starts the background worker instance."""
|
| 90 |
+
if not self.worker:
|
| 91 |
+
self.worker = Worker(
|
| 92 |
+
self.queue_name,
|
| 93 |
+
self.job_processor,
|
| 94 |
+
opts={"connection": self.redis_url}
|
| 95 |
+
)
|
| 96 |
+
self.setup_event_listeners()
|
| 97 |
+
logger.info(f"🚀 Worker is running and listening to queue: {self.queue_name}")
|
| 98 |
+
|
| 99 |
+
async def stop(self) -> None:
|
| 100 |
+
"""Gracefully closes the worker loop."""
|
| 101 |
+
if self.worker:
|
| 102 |
+
await self.worker.close()
|
| 103 |
+
self.worker = None
|
| 104 |
+
logger.info(f"🛑 Worker stopped safely.")
|
python-ai-service/classInfra.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# from kafka_python.producer_manager import ProducerManager
|
| 5 |
+
# from kafka_python.consumer_manager import ConsumerManager
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
QUEUE_NAME = "gpt-chat"
|
| 10 |
+
REDIS_URI = os.getenv('REDIS_URI')
|
| 11 |
+
|
| 12 |
+
# CREATE MANAGERS
|
| 13 |
+
# producer_manager = ProducerManager()
|
| 14 |
+
# consumer_manager = ConsumerManager()
|
| 15 |
+
|
| 16 |
+
# from QueueAndWorker.queue_manager import QueueManager
|
| 17 |
+
from QueueAndWorker.worker_manager import WorkerManager
|
| 18 |
+
|
| 19 |
+
# Init queue and workers
|
| 20 |
+
# queue_service = QueueManager(QUEUE_NAME, REDIS_URI)
|
| 21 |
+
worker_service = WorkerManager(QUEUE_NAME, REDIS_URI)
|
python-ai-service/main.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import signal
|
| 4 |
+
import time
|
| 5 |
+
import sys
|
| 6 |
+
import asyncio
|
| 7 |
+
|
| 8 |
+
# from kafka_python.topic_setup import create_topics
|
| 9 |
+
# from ModerationService.image_moderation_consumer import ImageModerationConsumer
|
| 10 |
+
# from TextGeneration.generate_ai_desc_consumer import GenAiDescConsumer
|
| 11 |
+
from classInfra import worker_service
|
| 12 |
+
from QueueAndWorker.model_manager import load_model
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Check if the environment is production. Default to 'development' if not set.
|
| 16 |
+
ENVIRONMENT = os.getenv("APP_ENV", "development")
|
| 17 |
+
|
| 18 |
+
# Automatically pick the level based on the environment
|
| 19 |
+
if ENVIRONMENT == "production":
|
| 20 |
+
LOG_LEVEL = logging.INFO # Hides DEBUG noise in production
|
| 21 |
+
else:
|
| 22 |
+
LOG_LEVEL = logging.DEBUG # Shows EVERYTHING in development
|
| 23 |
+
|
| 24 |
+
logging.basicConfig(
|
| 25 |
+
level=LOG_LEVEL,
|
| 26 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# logging.getLogger("kafka").setLevel(logging.WARNING)
|
| 30 |
+
|
| 31 |
+
# Works all of these if set logging.DEBUG
|
| 32 |
+
# logger.debug()
|
| 33 |
+
# logger.info()
|
| 34 |
+
# logger.warning()
|
| 35 |
+
# logger.error()
|
| 36 |
+
# logger.critical()
|
| 37 |
+
|
| 38 |
+
logging.basicConfig(level=logging.INFO)
|
| 39 |
+
|
| 40 |
+
logger = logging.getLogger(__name__)
|
| 41 |
+
|
| 42 |
+
# def kafka_init():
|
| 43 |
+
|
| 44 |
+
# create_topics()
|
| 45 |
+
|
| 46 |
+
# # START PRODUCERS
|
| 47 |
+
# producer_manager.create_producer(
|
| 48 |
+
# producer_name="image-moderation-producer",
|
| 49 |
+
# max_retries=6
|
| 50 |
+
# )
|
| 51 |
+
|
| 52 |
+
# producer_manager.create_producer(
|
| 53 |
+
# producer_name="text-gen-producer",
|
| 54 |
+
# max_retries=6
|
| 55 |
+
# )
|
| 56 |
+
|
| 57 |
+
# # START CONSUMERS
|
| 58 |
+
# image_moderation_consumer = ImageModerationConsumer()
|
| 59 |
+
# image_moderation_consumer.start()
|
| 60 |
+
|
| 61 |
+
# text_generation_consumer = GenAiDescConsumer()
|
| 62 |
+
# text_generation_consumer.start()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
shutdown_event = None
|
| 66 |
+
|
| 67 |
+
def signal_handler(sig, frame):
|
| 68 |
+
logger.info(
|
| 69 |
+
"🛑 Shutdown signal received"
|
| 70 |
+
)
|
| 71 |
+
shutdown_event.set()
|
| 72 |
+
|
| 73 |
+
# MAIN APPLICATION
|
| 74 |
+
async def main():
|
| 75 |
+
global shutdown_event
|
| 76 |
+
exit_code = 0
|
| 77 |
+
|
| 78 |
+
# logger.info(
|
| 79 |
+
# "🚀 Starting moderation service"
|
| 80 |
+
# )
|
| 81 |
+
|
| 82 |
+
# logger.info(
|
| 83 |
+
# "Starting topic creation"
|
| 84 |
+
# )
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
# Simulate adding jobs via our Producer class
|
| 88 |
+
# await queue_service.connect()
|
| 89 |
+
# Start up the background worker service
|
| 90 |
+
await worker_service.start()
|
| 91 |
+
load_model()
|
| 92 |
+
|
| 93 |
+
# CREATE TOPICS
|
| 94 |
+
# 💡 FIX: Force the blocking Kafka call to execute on a separate thread
|
| 95 |
+
|
| 96 |
+
# await asyncio.to_thread(kafka_init)
|
| 97 |
+
|
| 98 |
+
shutdown_event = asyncio.Event()
|
| 99 |
+
# Wait until Ctrl+C or SIGTERM
|
| 100 |
+
await shutdown_event.wait()
|
| 101 |
+
|
| 102 |
+
# KEEP SERVICE RUNNING - no need this because we already has await shutdown_event.wait()
|
| 103 |
+
# while True:
|
| 104 |
+
# await asyncio.sleep(60)
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.exception("❌ Error starting service")
|
| 107 |
+
exit_code = 1
|
| 108 |
+
finally:
|
| 109 |
+
logging.info("Cleaning up...")
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
# consumer_manager.stop_all_consumers()
|
| 113 |
+
# producer_manager.stop_all_producers()
|
| 114 |
+
|
| 115 |
+
# Clean queue and worker
|
| 116 |
+
await worker_service.stop()
|
| 117 |
+
# await queue_service.disconnect()
|
| 118 |
+
|
| 119 |
+
logger.info(
|
| 120 |
+
"✅ Graceful shutdown completed"
|
| 121 |
+
)
|
| 122 |
+
except Exception as e:
|
| 123 |
+
logger.exception("❌ Graceful shutdown failed")
|
| 124 |
+
finally:
|
| 125 |
+
sys.exit(exit_code)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# SERVICE ENTRY POINT
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
signal.signal(signal.SIGINT, signal_handler)
|
| 131 |
+
signal.signal(signal.SIGTERM, signal_handler)
|
| 132 |
+
asyncio.run(main()) # no need of sys.exit(0) at last, asyncio will do that
|
react-frontend/dist/assets/index-BKJNczi-.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
html,body,#root{background-color:#212121;width:100vw;height:100vh;margin:0;padding:0}body{display:block}pre code.hljs{padding:1em;display:block;overflow-x:auto}code.hljs{padding:3px 5px}.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}._codeBlockContainer_l4365_3{background-color:#0f172a;border-radius:8px;margin:1rem 0;position:relative;overflow:hidden}._copyCodeBtn_l4365_21{z-index:10;color:#94a3b8;cursor:pointer;opacity:0;background-color:#1e293b;border:1px solid #334155;border-radius:4px;padding:4px 10px;font-family:sans-serif;font-size:12px;font-weight:500;transition:opacity .2s,background-color .2s,color .2s;position:absolute;top:12px;right:12px}._codeBlockContainer_l4365_3:hover ._copyCodeBtn_l4365_21{opacity:1}._copyCodeBtn_l4365_21:hover{color:#fff;background-color:#475569}._customPre_l4365_81{margin:0;padding:16px;overflow-x:auto;background:0 0!important}._spinnerContainer_1y0ia_1{width:var(--spinner-size);height:var(--spinner-size);justify-content:center;align-items:center;display:inline-flex}._spinner_1y0ia_1{width:100%;height:100%;position:relative}._tick_1y0ia_29{transform-origin:50% 200%;width:2px;height:25%;transform:translateX(-50%) rotate(calc(var(--tick-index) * 30deg));animation:1.2s linear infinite _fadePulse_1y0ia_1;animation-delay:calc(var(--tick-index) * -.1s);background-color:#6366f1;border-radius:2px;position:absolute;top:0;left:50%}@keyframes _fadePulse_1y0ia_1{0%{opacity:1}to{opacity:.15}}._appWrapper_1u7vo_1{color:#e3e3e3;background-color:#212121;width:100vw;height:100vh;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;display:flex;overflow:hidden}._sidebar_1u7vo_23{background-color:#171717;border-right:1px solid #2f2f2f;flex-direction:column;flex-shrink:0;width:260px;height:100%;transition:transform .25s cubic-bezier(.4,0,.2,1),width .25s;display:flex}._sidebarClosed_1u7vo_45{border-right:none;width:0;transform:translate(-260px)}._sidebarHeader_1u7vo_57{justify-content:space-between;align-items:center;gap:8px;padding:12px;display:flex}._newChatBtn_1u7vo_73{color:#fff;cursor:pointer;text-align:left;background:0 0;border:1px solid #424242;border-radius:6px;flex:1;align-items:center;gap:8px;padding:10px 14px;font-size:14px;transition:background .2s;display:flex}._newChatBtn_1u7vo_73:hover{background-color:#2a2a2a}._toggleCollapseBtn_1u7vo_113,._menuExpandBtn_1u7vo_113{color:#b4b4b4;cursor:pointer;background:0 0;border:none;border-radius:6px;padding:8px;font-size:18px}._toggleCollapseBtn_1u7vo_113:hover,._menuExpandBtn_1u7vo_113:hover{color:#fff;background-color:#2a2a2a}._historyList_1u7vo_143{flex:1;padding:0 12px;overflow-y:auto}._historySectionTitle_1u7vo_155{color:#8e8e8e;padding:12px 8px 6px;font-size:12px;font-weight:600}._historyItem_1u7vo_169{cursor:pointer;border-radius:6px;align-items:center;gap:10px;padding:10px 8px;font-size:14px;transition:background .15s;display:flex}._historyItem_1u7vo_169:hover{background-color:#2a2a2a}._chatTitle_1u7vo_199{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}._mainCanvas_1u7vo_213{background-color:#212121;flex-direction:column;flex:1;height:100%;display:flex;position:relative}._topBar_1u7vo_231{justify-content:space-between;align-items:center;height:56px;padding:0 16px;display:flex}._modelBadge_1u7vo_247{color:#b4b4b4;font-size:15px;font-weight:600}._scrollContainer_1u7vo_261{flex:1;padding-bottom:24px;overflow-y:auto}._contentConstrain_1u7vo_273{max-width:720px;margin:0 auto;padding:0 16px}._messageRow_1u7vo_285{flex-direction:column;gap:8px;padding:20px 0;animation:.3s ease-out forwards _fadeIn_1u7vo_1;display:flex}@keyframes _fadeIn_1u7vo_1{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}._identityGroup_1u7vo_311{align-items:center;gap:12px;display:flex}._avatarIcon_1u7vo_323{border-radius:50%;justify-content:center;align-items:center;width:28px;height:28px;font-size:12px;font-weight:700;display:flex}._userAvatar_1u7vo_345{color:#fff;background-color:#543fd7}._assistantAvatar_1u7vo_355{color:#fff;background-color:#10a37f}._senderLabel_1u7vo_365{font-size:14px;font-weight:600}._bubblePayload_1u7vo_375{color:#d1d1d1;padding-left:40px;font-size:16px;line-height:1.6}._bubblePayload_1u7vo_375 p{margin:0}._typingIndicator_1u7vo_399{align-items:center;gap:4px;height:24px;padding-left:40px;display:flex}._typingIndicator_1u7vo_399 span{background-color:#b4b4b4;border-radius:50%;width:6px;height:6px;animation:1.4s ease-in-out infinite both _bounce_1u7vo_1}._typingIndicator_1u7vo_399 span:first-child{animation-delay:-.32s}._typingIndicator_1u7vo_399 span:nth-child(2){animation-delay:-.16s}@keyframes _bounce_1u7vo_1{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}._dockFooter_1u7vo_449{background-color:#212121;padding:0 16px 24px}._dockConstrain_1u7vo_459{flex-direction:column;gap:12px;max-width:720px;margin:0 auto;display:flex}._inputFormBox_1u7vo_475{background-color:#2f2f2f;border:1px solid #424242;border-radius:16px;align-items:flex-end;padding:10px 14px;display:flex;position:relative}._spinnerCont_1u7vo_495{text-align:center}._textField_1u7vo_503{color:#fff;resize:none;background:0 0;border:none;outline:none;flex:1;max-height:200px;padding-right:40px;font-family:inherit;font-size:16px;line-height:1.5}._textField_1u7vo_503::placeholder{color:#7d7d7d}._actionSendBtn_1u7vo_539{color:#000;cursor:pointer;background-color:#fff;border:none;border-radius:8px;justify-content:center;align-items:center;width:32px;height:32px;font-size:12px;transition:background .2s,opacity .2s;display:flex;position:absolute;bottom:10px;right:10px}._actionSendBtn_1u7vo_539:disabled{color:#171717;cursor:not-allowed;background-color:#424242}._disclaimerText_1u7vo_587{color:#7d7d7d;text-align:center;margin:0;font-size:12px}.counter{color:var(--accent);background:var(--accent-bg);border:2px solid #0000;border-radius:5px;margin-bottom:24px;padding:5px 10px;font-size:16px;transition:border-color .3s}.counter:hover{border-color:var(--accent-border)}.counter:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.hero{position:relative}.hero .base,.hero .framework,.hero .vite{margin:0 auto;inset-inline:0}.hero .base{z-index:0;width:170px;position:relative}.hero .framework,.hero .vite{position:absolute}.hero .framework{z-index:1;height:28px;top:34px;transform:perspective(2000px)rotate(300deg)rotateX(44deg)rotateY(39deg)scale(1.4)}.hero .vite{z-index:0;width:auto;height:26px;top:107px;transform:perspective(2000px)rotate(300deg)rotateX(40deg)rotateY(39deg)scale(.8)}#center{flex-direction:column;flex-grow:1;place-content:center;place-items:center;gap:25px;display:flex}@media (width<=1024px){#center{gap:18px;padding:32px 20px 24px}}#next-steps{border-top:1px solid var(--border);text-align:left;display:flex}#next-steps>div{flex:1 1 0;padding:32px}@media (width<=1024px){#next-steps>div{padding:24px 20px}}#next-steps .icon{width:22px;height:22px;margin-bottom:16px}@media (width<=1024px){#next-steps{text-align:center;flex-direction:column}}#docs{border-right:1px solid var(--border)}@media (width<=1024px){#docs{border-right:none;border-bottom:1px solid var(--border)}}#next-steps ul{gap:8px;margin:32px 0 0;padding:0;list-style:none;display:flex}#next-steps ul .logo{height:18px}#next-steps ul a{color:var(--text-h);background:var(--social-bg);border-radius:6px;align-items:center;gap:8px;padding:6px 12px;font-size:16px;text-decoration:none;transition:box-shadow .3s;display:flex}#next-steps ul a:hover{box-shadow:var(--shadow)}#next-steps ul a .button-icon{width:18px;height:18px}@media (width<=1024px){#next-steps ul{flex-wrap:wrap;justify-content:center;margin-top:20px}#next-steps ul li{flex:calc(50% - 8px)}#next-steps ul a{box-sizing:border-box;justify-content:center;width:100%}}#spacer{border-top:1px solid var(--border);height:88px}@media (width<=1024px){#spacer{height:48px}}.ticks{width:100%;position:relative}.ticks:before,.ticks:after{content:"";border:5px solid #0000;position:absolute;top:-4.5px}.ticks:before{border-left-color:var(--border);left:0}.ticks:after{border-right-color:var(--border);right:0}
|
react-frontend/dist/assets/index-BKxufNR1.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
react-frontend/dist/favicon.svg
ADDED
|
|
react-frontend/dist/icons.svg
ADDED
|
|
react-frontend/dist/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>code-for-me</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-BKxufNR1.js"></script>
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BKJNczi-.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- AI & Model Inference (CPU optimized) ---
|
| 2 |
+
llama-cpp-python>=0.2.55
|
| 3 |
+
huggingface_hub>=0.21.0
|
| 4 |
+
|
| 5 |
+
# --- Queue & Asynchronous Processing ---
|
| 6 |
+
redis>=5.0.1
|
| 7 |
+
|
| 8 |
+
# --- Utilities ---
|
| 9 |
+
pydantic>=2.6.0
|
| 10 |
+
typing-extensions>=4.10.0
|
start.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
|
| 3 |
+
echo "Starting Redis..."
|
| 4 |
+
redis-server --bind 127.0.0.1 --protected-mode no &
|
| 5 |
+
|
| 6 |
+
echo "Waiting for Redis..."
|
| 7 |
+
|
| 8 |
+
until redis-cli ping >/dev/null 2>&1
|
| 9 |
+
do
|
| 10 |
+
sleep 1
|
| 11 |
+
done
|
| 12 |
+
|
| 13 |
+
echo "Redis is ready."
|
| 14 |
+
|
| 15 |
+
exec pm2-runtime ecosystem.config.js
|