subi333 commited on
Commit
e8a6607
·
0 Parent(s):

Initial commit for AI coding assistant

Browse files
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