diff --git a/package-lock.json b/package-lock.json index 5192e44..ffda786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5677,117 +5677,6 @@ "node": ">=18" } }, - "node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gcp-metadata/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/gcp-metadata/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/gcp-metadata/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true, - "peer": true - }, - "node_modules/gcp-metadata/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/readme.md b/readme.md index 1ba0a5e..62cc9d1 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,19 @@ If your PR aligns with our development direction and passes checks we’ll merge --- +## GenAI Implementation Guide + +The API now includes a public guide endpoint for planning a LangChain-based GenAI feature. + +- `GET /api/v1/gen-ai-implementation` returns the full guide for single-agent, multi-agent, and LangGraph approaches. +- `GET /api/v1/gen-ai-implementation?mode=single` returns only the single-agent blueprint. +- `GET /api/v1/gen-ai-implementation?mode=multi` returns the multi-agent blueprint. +- `GET /api/v1/gen-ai-implementation?mode=langgraph` returns the LangGraph blueprint. + +The response is structured so a UI can render hero copy, theme data, comparison cards, and implementation steps. + +--- + ## Found a Bug or Issue? If you encounter any problems or have suggestions: diff --git a/src/__tests__/gen-ai-implementation.test.js b/src/__tests__/gen-ai-implementation.test.js new file mode 100644 index 0000000..b2cad1e --- /dev/null +++ b/src/__tests__/gen-ai-implementation.test.js @@ -0,0 +1,115 @@ +import request from 'supertest'; +import app from '../app.js'; + +describe('GenAI Implementation API Routes', () => { + describe('GET /api/v1/gen-ai-implementation', () => { + it('should return the full implementation guide by default', async () => { + const response = await request(app) + .get('/api/v1/gen-ai-implementation') + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe( + 'GenAI implementation guide retrieved successfully' + ); + expect(response.body.data).toHaveProperty('feature'); + expect(response.body.data).toHaveProperty('uiTheme'); + expect(response.body.data).toHaveProperty('overview'); + expect(response.body.data).toHaveProperty('implementationModes'); + expect(response.body.data.implementationModes).toHaveProperty('single'); + expect(response.body.data.implementationModes).toHaveProperty('multi'); + expect(response.body.data.implementationModes).toHaveProperty( + 'langgraph' + ); + }); + + it('should return a single-agent guide when requested', async () => { + const response = await request(app) + .get('/api/v1/gen-ai-implementation?mode=single') + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.implementation.slug).toBe('single'); + expect(response.body.data.implementation.title).toBe( + 'Single-Agent LangChain' + ); + }); + + it('should return a LangGraph guide when requested', async () => { + const response = await request(app) + .get('/api/v1/gen-ai-implementation?mode=langgraph') + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.implementation.slug).toBe('langgraph'); + expect(response.body.data.implementation.codeSnippet).toContain( + 'StateGraph' + ); + }); + + it('should return 400 for invalid mode values', async () => { + const response = await request(app) + .get('/api/v1/gen-ai-implementation?mode=unknown') + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toBe( + 'Invalid mode. Use single, multi, langgraph, or all.' + ); + }); + }); + + describe('POST /api/v1/gen-ai-implementation/*/ask', () => { + it('should return single-agent demo output', async () => { + const response = await request(app) + .post('/api/v1/gen-ai-implementation/single/ask') + .send({ question: 'How to start with LangChain?' }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe( + 'Single-agent demo response generated successfully' + ); + expect(response.body.data.mode).toBe('single'); + expect(response.body.data).toHaveProperty('response'); + }); + + it('should return multi-agent demo output', async () => { + const response = await request(app) + .post('/api/v1/gen-ai-implementation/multi/ask') + .send({ question: 'How to design multi-agent pipeline?' }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe( + 'Multi-agent demo response generated successfully' + ); + expect(response.body.data.mode).toBe('multi'); + expect(response.body.data).toHaveProperty('researchNotes'); + }); + + it('should return langgraph demo output', async () => { + const response = await request(app) + .post('/api/v1/gen-ai-implementation/langgraph/ask') + .send({ question: 'How to add retries in workflow?' }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe( + 'LangGraph demo response generated successfully' + ); + expect(response.body.data.mode).toBe('langgraph'); + expect(response.body.data).toHaveProperty('graphTrace'); + }); + + it('should return 400 when question is missing', async () => { + const response = await request(app) + .post('/api/v1/gen-ai-implementation/single/ask') + .send({ question: '' }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Question is required.'); + }); + }); +}); \ No newline at end of file diff --git a/src/app.js b/src/app.js index 40e4a4a..9b7209a 100644 --- a/src/app.js +++ b/src/app.js @@ -33,12 +33,14 @@ import userRoutes from './routes/user.routes.js'; import productRoutes from './routes/product.routes.js'; import cartRoutes from './routes/cart.routes.js'; import paymentRoutes from './routes/payment.routes.js'; +import genAiImplementationRoutes from './routes/gen-ai-implementation.routes.js'; app.use('/api/v1/auth', authRoutes); app.use('/api/v1/users', userRoutes); app.use('/api/v1/store/products', productRoutes); app.use('/api/v1/store/cart', cartRoutes); app.use('/api/v1/payments', paymentRoutes); +app.use('/api/v1/gen-ai-implementation', genAiImplementationRoutes); // // Simple route for checking server status app.get('/', (req, res) => { diff --git a/src/config/config.js b/src/config/config.js index c31f79c..b34acdf 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -27,10 +27,10 @@ const _config = { }; let finalConfig; -if (process.env.NODE_ENV === 'testing') { - finalConfig = _config; // Don't freeze in testing mode -} else { +if (process.env.NODE_ENV === 'production') { finalConfig = Object.freeze(_config); +} else { + finalConfig = _config; // Keep config mutable outside production for tests and local overrides } export default finalConfig; diff --git a/src/config/email.config.js b/src/config/email.config.js index 5ac452f..22ca0c5 100644 --- a/src/config/email.config.js +++ b/src/config/email.config.js @@ -1,18 +1,31 @@ import nodemailer from 'nodemailer'; import { google } from 'googleapis'; import config from './config.js'; +import AppError from '../utils/appError.js'; const OAuth2 = google.auth.OAuth2; -const oAuth2Client = new OAuth2( - config.GOOGLE_CLIENT_ID, - config.GOOGLE_CLIENT_SECRET, - 'https://developers.google.com/oauthplayground' -); +export const createTransporter = async () => { + if ( + !config.GOOGLE_CLIENT_ID || + !config.GOOGLE_CLIENT_SECRET || + !config.GOOGLE_REFRESH_TOKEN || + !config.GMAIL_USER + ) { + throw new AppError( + 'Email service is not configured. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN, and GMAIL_USER.', + 503 + ); + } -oAuth2Client.setCredentials({ refresh_token: config.GOOGLE_REFRESH_TOKEN }); + const oAuth2Client = new OAuth2( + config.GOOGLE_CLIENT_ID, + config.GOOGLE_CLIENT_SECRET, + 'https://developers.google.com/oauthplayground' + ); + + oAuth2Client.setCredentials({ refresh_token: config.GOOGLE_REFRESH_TOKEN }); -export const createTransporter = async () => { const { token } = await oAuth2Client.getAccessToken(); return nodemailer.createTransport({ diff --git a/src/config/passport.js b/src/config/passport.js index 46d3f5c..ef90687 100644 --- a/src/config/passport.js +++ b/src/config/passport.js @@ -5,50 +5,62 @@ import userDao from '../dao/user.dao.js'; import config from './config.js'; import logger from '../loggers/winston.logger.js'; -passport.use( - new GoogleStrategy( - { - clientID: config.GOOGLE_CLIENT_ID, - clientSecret: config.GOOGLE_CLIENT_SECRET, - callbackURL: config.GOOGLE_CALLBACK_URL, - }, - async (accessToken, refreshToken, profile, done) => { - try { - logger.debug('Google profile:', JSON.stringify(profile)); - - // Find user by googleId or email - let user = await userDao.findByGoogleId(profile.id); - - if (!user && profile.emails && profile.emails.length > 0) { - // If not found by googleId, try to find by email - user = await userDao.findByEmail(profile.emails[0].value); - - if (user) { - // If found by email, link Google account - user.googleId = profile.id; - await user.save(); +export const googleOAuthEnabled = Boolean( + config.GOOGLE_CLIENT_ID && + config.GOOGLE_CLIENT_SECRET && + config.GOOGLE_CALLBACK_URL +); + +if (googleOAuthEnabled) { + passport.use( + new GoogleStrategy( + { + clientID: config.GOOGLE_CLIENT_ID, + clientSecret: config.GOOGLE_CLIENT_SECRET, + callbackURL: config.GOOGLE_CALLBACK_URL, + }, + async (accessToken, refreshToken, profile, done) => { + try { + logger.debug('Google profile:', JSON.stringify(profile)); + + // Find user by googleId or email + let user = await userDao.findByGoogleId(profile.id); + + if (!user && profile.emails && profile.emails.length > 0) { + // If not found by googleId, try to find by email + user = await userDao.findByEmail(profile.emails[0].value); + + if (user) { + // If found by email, link Google account + user.googleId = profile.id; + await user.save(); + } } - } - if (!user) { - // Create a new user if not found - user = await userService.registerUser({ - googleId: profile.id, - email: profile.emails[0].value, - name: profile.displayName, - avatar: - profile.photos && profile.photos.length > 0 - ? profile.photos[0].value - : undefined, - }); - logger.info(`New user created via Google: ${user.email}`); - } + if (!user) { + // Create a new user if not found + user = await userService.registerUser({ + googleId: profile.id, + email: profile.emails[0].value, + name: profile.displayName, + avatar: + profile.photos && profile.photos.length > 0 + ? profile.photos[0].value + : undefined, + }); + logger.info(`New user created via Google: ${user.email}`); + } - return done(null, user); - } catch (err) { - logger.error('Google authentication error:', err); - return done(err, null); + return done(null, user); + } catch (err) { + logger.error('Google authentication error:', err); + return done(err, null); + } } - } - ) -); + ) + ); +} else { + logger.warn('Google OAuth is disabled because credentials are missing.'); +} + +export default passport; diff --git a/src/controllers/gen-ai-implementation.controller.js b/src/controllers/gen-ai-implementation.controller.js new file mode 100644 index 0000000..3bfde34 --- /dev/null +++ b/src/controllers/gen-ai-implementation.controller.js @@ -0,0 +1,70 @@ +import asyncHandler from '../utils/asyncHandler.js'; +import genAiImplementationService from '../services/gen-ai-implementation.service.js'; + +class GenAiImplementationController { + /** + * Get the GenAI implementation guide. + * @route GET /api/v1/gen-ai-implementation + * @access Public + */ + getGuide = asyncHandler(async (req, res) => { + const mode = req.query.mode || 'all'; + const guide = genAiImplementationService.buildGuide(mode); + + res.status(200).json({ + success: true, + message: 'GenAI implementation guide retrieved successfully', + data: guide, + }); + }); + + /** + * Run single-agent demo flow. + * @route POST /api/v1/gen-ai-implementation/single/ask + * @access Public + */ + askSingle = asyncHandler(async (req, res) => { + const { question } = req.body; + const result = genAiImplementationService.runSingleAgentDemo(question); + + res.status(200).json({ + success: true, + message: 'Single-agent demo response generated successfully', + data: result, + }); + }); + + /** + * Run multi-agent demo flow. + * @route POST /api/v1/gen-ai-implementation/multi/ask + * @access Public + */ + askMulti = asyncHandler(async (req, res) => { + const { question } = req.body; + const result = genAiImplementationService.runMultiAgentDemo(question); + + res.status(200).json({ + success: true, + message: 'Multi-agent demo response generated successfully', + data: result, + }); + }); + + /** + * Run LangGraph demo flow. + * @route POST /api/v1/gen-ai-implementation/langgraph/ask + * @access Public + */ + askLangGraph = asyncHandler(async (req, res) => { + const { question } = req.body; + const result = genAiImplementationService.runLangGraphDemo(question); + + res.status(200).json({ + success: true, + message: 'LangGraph demo response generated successfully', + data: result, + }); + }); +} + +export default new GenAiImplementationController(); diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index 6645460..3e51414 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -1,6 +1,8 @@ import express from 'express'; import passport from 'passport'; import authController from '../controllers/auth.controller.js'; +import AppError from '../utils/appError.js'; +import { googleOAuthEnabled } from '../config/passport.js'; import { protect } from '../middlewares/auth.middleware.js'; import { validate } from '../middlewares/validator.middleware.js'; import { @@ -42,18 +44,27 @@ router .get(validate(verifyEmailTokenValidator), authController.verifyEmailToken); // OAuth routes -router.route('/google').get( - passport.authenticate('google', { - scope: ['profile', 'email'], - }) -); - -router.route('/google/callback').get( - passport.authenticate('google', { - failureRedirect: '/login-failed', // Redirect to failure page if authentication fails - session: false, - }), - authController.googleCallback -); +if (googleOAuthEnabled) { + router.route('/google').get( + passport.authenticate('google', { + scope: ['profile', 'email'], + }) + ); + + router.route('/google/callback').get( + passport.authenticate('google', { + failureRedirect: '/login-failed', // Redirect to failure page if authentication fails + session: false, + }), + authController.googleCallback + ); +} else { + const oauthUnavailable = (req, res, next) => { + next(new AppError('Google OAuth is not configured on this server.', 503)); + }; + + router.route('/google').get(oauthUnavailable); + router.route('/google/callback').get(oauthUnavailable); +} export default router; diff --git a/src/routes/gen-ai-implementation.routes.js b/src/routes/gen-ai-implementation.routes.js new file mode 100644 index 0000000..4728410 --- /dev/null +++ b/src/routes/gen-ai-implementation.routes.js @@ -0,0 +1,13 @@ +import express from 'express'; +import genAiImplementationController from '../controllers/gen-ai-implementation.controller.js'; + +const router = express.Router(); + +router.route('/').get(genAiImplementationController.getGuide); +router.route('/single/ask').post(genAiImplementationController.askSingle); +router.route('/multi/ask').post(genAiImplementationController.askMulti); +router + .route('/langgraph/ask') + .post(genAiImplementationController.askLangGraph); + +export default router; \ No newline at end of file diff --git a/src/services/gen-ai-implementation.service.js b/src/services/gen-ai-implementation.service.js new file mode 100644 index 0000000..e5e8c46 --- /dev/null +++ b/src/services/gen-ai-implementation.service.js @@ -0,0 +1,452 @@ +import AppError from '../utils/appError.js'; + +const uiTheme = { + name: 'Midnight Graph', + mode: 'dark', + palette: { + background: '#08111f', + surface: '#111a2e', + surfaceAlt: '#16233d', + primary: '#7c9cff', + secondary: '#6be7c8', + text: '#e8eefc', + muted: '#9bb0d0', + border: '#243354', + }, + layout: 'card-grid', +}; + +const implementationProcess = { + prerequisites: [ + 'Node.js 18+ and npm installed', + 'Working Express server with controller/service pattern', + 'One LLM provider key (for example OpenAI)', + ], + installCommands: [ + 'npm i langchain @langchain/openai @langchain/langgraph dotenv', + ], + envSetup: [ + 'Add OPENAI_API_KEY in your .env file', + 'Load environment variables once in server bootstrap', + 'Never hardcode API keys inside services', + ], + folderIntegration: [ + 'Create src/services/gen-ai-single-agent.service.js for single-agent flow', + 'Create src/services/gen-ai-langgraph.service.js for graph orchestration flow', + 'Use existing src/controllers/gen-ai-implementation.controller.js to call service methods', + 'Expose POST endpoints from src/routes/gen-ai-implementation.routes.js for runnable demos', + 'Keep GET /api/v1/gen-ai-implementation as a documentation/guide endpoint', + ], + validationAndSafety: [ + 'Validate question input before calling model', + 'Use short system prompts with clear output format', + 'Add request timeout and retry policy at service layer', + 'Log only request metadata, do not log secrets', + ], + rolloutPlan: [ + 'Phase 1: Ship single-agent first', + 'Phase 2: Add specialist agents for research/writer split', + 'Phase 3: Move to LangGraph for branching, retries, and checkpoints', + ], +}; + +const implementationModes = { + single: { + slug: 'single', + title: 'Single-Agent LangChain', + summary: + 'Use one orchestrator when the task has a clear path, a limited toolset, and predictable handoffs.', + whenToUse: [ + 'FAQ answering and support automation', + 'Simple retrieval augmented generation flows', + 'One model plus a few tools or retrievers', + ], + architecture: [ + 'User request enters one agent loop', + 'Agent selects tools or retrievers', + 'Model synthesizes the final answer', + 'Response is formatted for the client UI', + ], + implementationSteps: [ + 'Install LangChain core packages and choose a model provider.', + 'Define a system prompt with the expected output format.', + 'Attach tools for search, database lookup, or API calls.', + 'Wrap the agent in a service method and expose it through a controller.', + 'Return structured JSON so the frontend can render cards, steps, and code snippets.', + ], + processFlow: [ + 'Receive user question in controller', + 'Validate and sanitize input', + 'Build prompt template and invoke one LLM', + 'Return final structured answer', + ], + codeSnippet: `import 'dotenv/config'; +import { ChatOpenAI } from '@langchain/openai'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; +import { StringOutputParser } from '@langchain/core/output_parsers'; + +const model = new ChatOpenAI({ + model: 'gpt-4o-mini', + temperature: 0.2, +}); + +const prompt = ChatPromptTemplate.fromMessages([ + ['system', 'You are a helpful assistant. Keep answer beginner-friendly.'], + ['human', '{question}'], +]); + +const chain = prompt.pipe(model).pipe(new StringOutputParser()); + +export async function runSingleAgent(question) { + if (!question || !question.trim()) { + throw new Error('Question is required'); + } + + const answer = await chain.invoke({ question }); + return { + mode: 'single-agent', + question, + answer, + }; +}`, + tradeoffs: [ + 'Fast to build and easy to debug', + 'Can become brittle if the workflow grows too complex', + 'Best for linear workflows', + ], + }, + multi: { + slug: 'multi', + title: 'Multi-Agent LangChain', + summary: + 'Use multiple specialized agents when the task needs role separation, parallel work, or explicit delegation.', + whenToUse: [ + 'Research, planning, and content generation pipelines', + 'Tasks where one agent should review another agent’s output', + 'Workflows that benefit from a supervisor or router agent', + ], + architecture: [ + 'A supervisor receives the task and chooses the next specialist', + 'Specialist agents handle narrow responsibilities', + 'Agents return intermediate outputs to the supervisor', + 'Supervisor merges the final response for the client', + ], + implementationSteps: [ + 'Define a shared task schema so every agent returns the same shape.', + 'Create specialist agents for research, drafting, and validation.', + 'Add a supervisor agent that routes based on task context.', + 'Persist conversation state so the agents can share progress.', + 'Keep outputs deterministic and expose trace metadata in the API response.', + ], + processFlow: [ + 'Supervisor receives user goal', + 'Supervisor chooses specialist agent', + 'Specialist returns intermediate output', + 'Supervisor combines and returns final answer', + ], + codeSnippet: `import 'dotenv/config'; +import { ChatOpenAI } from '@langchain/openai'; + +const llm = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0.3 }); + +async function researchAgent(task) { + const prompt = 'Research this topic in concise points: ' + task; + const response = await llm.invoke(prompt); + return response.content; +} + +async function writerAgent(task, researchNotes) { + const prompt = [ + 'Task: ' + task, + 'Notes: ' + researchNotes, + 'Write a clean beginner-friendly response.', + ].join('\n'); + const response = await llm.invoke(prompt); + return response.content; +} + +export async function runMultiAgent(task) { + if (!task || !task.trim()) { + throw new Error('Task is required'); + } + + const notes = await researchAgent(task); + const finalAnswer = await writerAgent(task, notes); + + return { + mode: 'multi-agent', + task, + notes, + finalAnswer, + }; +}`, + tradeoffs: [ + 'More flexible than a single-agent flow', + 'Requires stronger state management and tracing', + 'Useful when tasks can be decomposed into clear roles', + ], + }, + langgraph: { + slug: 'langgraph', + title: 'LangGraph Orchestration', + summary: + 'Use LangGraph when you need explicit graph-based control, branching, retries, or stateful loops.', + whenToUse: [ + 'Complex workflows with conditional routing', + 'Multi-step systems that need retries or checkpoints', + 'Agent workflows that must pause, resume, or branch', + ], + architecture: [ + 'Model the workflow as nodes and edges', + 'Store shared state in a graph state object', + 'Route between nodes based on state conditions', + 'Compile the graph and execute it like a reusable pipeline', + ], + implementationSteps: [ + 'Design a state schema for messages, tasks, and final output.', + 'Create nodes for planning, tool use, validation, and response formatting.', + 'Add conditional edges for branching logic and fallback paths.', + 'Compile the graph once and reuse it from your service layer.', + 'Expose the graph result through your API so the UI can visualize the path taken.', + ], + processFlow: [ + 'Define graph state object and defaults', + 'Run planner node to generate plan', + 'Run execution node to produce draft output', + 'Route to validator and decide finish/retry', + 'Return final answer with traceable state fields', + ], + codeSnippet: `import 'dotenv/config'; +import { ChatOpenAI } from '@langchain/openai'; +import { StateGraph, END } from '@langchain/langgraph'; + +const llm = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0.2 }); + +async function plannerNode(state) { + const res = await llm.invoke('Create a 3-step plan for: ' + state.question); + return { ...state, plan: res.content }; +} + +async function executorNode(state) { + const res = await llm.invoke( + 'Question: ' + state.question + '\nPlan: ' + state.plan + '\nGenerate draft answer.' + ); + return { ...state, draft: res.content }; +} + +async function validatorNode(state) { + const res = await llm.invoke('Validate this draft and improve it: ' + state.draft); + return { ...state, finalAnswer: res.content }; +} + +export async function runLangGraph(question) { + const initialState = { question, plan: '', draft: '', finalAnswer: '' }; + + const graph = new StateGraph({ + channels: { question: null, plan: null, draft: null, finalAnswer: null }, + }) + .addNode('planner', plannerNode) + .addNode('executor', executorNode) + .addNode('validator', validatorNode) + .setEntryPoint('planner') + .addEdge('planner', 'executor') + .addEdge('executor', 'validator') + .addEdge('validator', END); + + const app = graph.compile(); + const result = await app.invoke(initialState); + + return { + mode: 'langgraph', + question: result.question, + plan: result.plan, + answer: result.finalAnswer, + }; +}`, + tradeoffs: [ + 'Best control for complex workflows', + 'More setup than a single-agent implementation', + 'Excellent for production systems that need observability', + ], + }, +}; + +const comparisonRows = [ + { + label: 'Complexity', + single: 'Low', + multi: 'Medium', + langgraph: 'High', + }, + { + label: 'Best for', + single: 'Linear tasks', + multi: 'Delegated work', + langgraph: 'Stateful workflows', + }, + { + label: 'Control', + single: 'Simple prompt loop', + multi: 'Supervisor routing', + langgraph: 'Explicit node routing', + }, + { + label: 'Observability', + single: 'Basic traces', + multi: 'Per-agent traces', + langgraph: 'Strong graph-level tracing', + }, +]; + +class GenAiImplementationService { + validateQuestion(question) { + const normalizedQuestion = `${question || ''}`.trim(); + + if (!normalizedQuestion) { + throw new AppError('Question is required.', 400); + } + + if (normalizedQuestion.length > 2000) { + throw new AppError( + 'Question is too long. Keep it under 2000 characters.', + 400 + ); + } + + return normalizedQuestion; + } + + buildOverview() { + return { + title: 'GenAI implementation blueprint', + subtitle: + 'Pick the simplest architecture that fits the workflow, then scale into multi-agent or LangGraph only when the task demands it.', + recommendedPath: [ + 'Start with a single-agent LangChain flow.', + 'Split into specialist agents if the workflow needs delegation.', + 'Move to LangGraph when routing, retries, or state checkpoints are required.', + ], + }; + } + + buildFeatureMetadata() { + return { + name: 'gen-ai-implementation', + title: 'GenAI Implementation', + description: + 'A structured guide for implementing single-agent, multi-agent, and LangGraph flows with LangChain.', + status: 'public', + }; + } + + buildImplementationProcess() { + return implementationProcess; + } + + buildAllImplementationModes() { + return { + single: implementationModes.single, + multi: implementationModes.multi, + langgraph: implementationModes.langgraph, + }; + } + + runSingleAgentDemo(question) { + const normalizedQuestion = this.validateQuestion(question); + + return { + mode: 'single', + question: normalizedQuestion, + stages: [ + 'Input validated', + 'Single prompt prepared', + 'One assistant response generated', + ], + response: + `Single-agent demo response for: "${normalizedQuestion}". ` + + 'Use this mode when your workflow is linear and you need a fast first implementation.', + nextStep: + 'If output quality is unstable, improve system prompt and add strict response format.', + }; + } + + runMultiAgentDemo(question) { + const normalizedQuestion = this.validateQuestion(question); + + const researchNotes = [ + 'Identify key terms from user question', + 'Gather concise factual points', + 'Prepare summary notes for writer agent', + ]; + + return { + mode: 'multi', + question: normalizedQuestion, + supervisorDecision: 'Route task to research agent then writer agent', + researchNotes, + writerOutput: + `Multi-agent demo response for: "${normalizedQuestion}". ` + + 'Research notes were transformed into a beginner-friendly final answer.', + nextStep: + 'Add evaluator/reviewer agent if you want quality checks before final response.', + }; + } + + runLangGraphDemo(question) { + const normalizedQuestion = this.validateQuestion(question); + + const graphTrace = [ + { from: 'START', to: 'planner' }, + { from: 'planner', to: 'executor' }, + { from: 'executor', to: 'validator' }, + { from: 'validator', to: 'END' }, + ]; + + return { + mode: 'langgraph', + question: normalizedQuestion, + state: { + plan: 'Create a short step-by-step execution plan', + draft: 'Generate initial answer from plan and context', + finalAnswer: `LangGraph demo response for: "${normalizedQuestion}" with planner -> executor -> validator pipeline.`, + }, + graphTrace, + nextStep: + 'Add conditional edge from validator to executor when confidence is low.', + }; + } + + buildGuide(mode) { + const normalizedMode = `${mode || 'all'}`.toLowerCase().trim(); + + if (normalizedMode === 'all') { + return { + feature: this.buildFeatureMetadata(), + uiTheme, + overview: this.buildOverview(), + process: this.buildImplementationProcess(), + implementationModes: this.buildAllImplementationModes(), + comparison: comparisonRows, + }; + } + + const implementation = implementationModes[normalizedMode]; + if (!implementation) { + throw new AppError( + 'Invalid mode. Use single, multi, langgraph, or all.', + 400 + ); + } + + return { + feature: this.buildFeatureMetadata(), + uiTheme, + overview: this.buildOverview(), + process: this.buildImplementationProcess(), + implementation, + comparison: comparisonRows, + }; + } +} + +export default new GenAiImplementationService(); diff --git a/src/services/payment.service.js b/src/services/payment.service.js index c61178d..279b937 100644 --- a/src/services/payment.service.js +++ b/src/services/payment.service.js @@ -10,10 +10,26 @@ import { convertCurrency } from '../utils/currencyUtils.js'; class PaymentService { constructor() { - this.razorpay = new Razorpay({ - key_id: config.razorpay.keyId, - key_secret: config.razorpay.keySecret, - }); + this.razorpay = + config.razorpay.keyId && config.razorpay.keySecret + ? new Razorpay({ + key_id: config.razorpay.keyId, + key_secret: config.razorpay.keySecret, + }) + : null; + + if (!this.razorpay) { + logger.warn('Razorpay is disabled because credentials are missing.'); + } + } + + ensureRazorpayConfigured() { + if (!this.razorpay) { + throw new AppError( + 'Payment service is not configured. Set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET.', + 503 + ); + } } // Generate unique order ID @@ -24,6 +40,8 @@ class PaymentService { // Create order for cart payment async createCartOrder(userId, shippingAddress = null, notes = '') { try { + this.ensureRazorpayConfigured(); + // Get user's cart using DAO const cart = await cartDAO.findCartByUserId(userId); @@ -147,6 +165,8 @@ class PaymentService { notes = '' ) { try { + this.ensureRazorpayConfigured(); + // Get product details using DAO const product = await productDAO.findProductById(productId); if (!product) { @@ -241,6 +261,8 @@ class PaymentService { razorpayPaymentId, razorpaySignature ) { + this.ensureRazorpayConfigured(); + const body = razorpayOrderId + '|' + razorpayPaymentId; const expectedSignature = crypto .createHmac('sha256', config.razorpay.keySecret) @@ -253,6 +275,8 @@ class PaymentService { // Verify and confirm payment async verifyPayment(razorpayOrderId, razorpayPaymentId, razorpaySignature) { try { + this.ensureRazorpayConfigured(); + // Verify signature const isValidSignature = this.verifyPaymentSignature( razorpayOrderId, @@ -363,6 +387,8 @@ class PaymentService { // Refund payment async refundPayment(paymentId, refundAmount = null, reason = '') { try { + this.ensureRazorpayConfigured(); + const payment = await paymentDAO.findById(paymentId); if (!payment) { throw new AppError('Payment not found', 404); @@ -374,8 +400,8 @@ class PaymentService { const refundAmountInPaise = refundAmount && - refundAmount > 0 && - Math.round(refundAmount * 100) <= payment.amount + refundAmount > 0 && + Math.round(refundAmount * 100) <= payment.amount ? Math.round(refundAmount * 100) : payment.amount; @@ -414,6 +440,8 @@ class PaymentService { // Handle Razorpay webhooks async handleWebhook(signature, payload) { try { + this.ensureRazorpayConfigured(); + // Verify webhook signature const expectedSignature = crypto .createHmac('sha256', config.razorpay.keySecret)