From b7c10d133b0a593d4b3beae83aa6d05eb4f5c6c2 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Thu, 5 Jun 2025 14:56:35 +0530 Subject: [PATCH 01/22] initial folder setup and architecture scaffolding completed --- apps/frontend/src/App.css | 42 ++++++++++++ apps/frontend/src/App.tsx | 13 ++++ apps/frontend/src/assets/react.svg | 1 + apps/frontend/src/components/PromptForm.tsx | 0 .../src/components/ResponseViewer.tsx | 0 apps/frontend/src/index.css | 68 +++++++++++++++++++ apps/frontend/src/main.tsx | 10 +++ apps/frontend/src/pages/index.tsx | 1 + apps/frontend/src/services/api.ts | 1 + .../src/styles/promtFromComponent.css | 1 + .../src/styles/responseViewerComponent.css | 1 + apps/frontend/src/utils/constants.ts | 1 + apps/frontend/src/utils/validator.ts | 1 + apps/frontend/src/vite-env.d.ts | 1 + 14 files changed, 141 insertions(+) create mode 100644 apps/frontend/src/App.css create mode 100644 apps/frontend/src/App.tsx create mode 100644 apps/frontend/src/assets/react.svg create mode 100644 apps/frontend/src/components/PromptForm.tsx create mode 100644 apps/frontend/src/components/ResponseViewer.tsx create mode 100644 apps/frontend/src/index.css create mode 100644 apps/frontend/src/main.tsx create mode 100644 apps/frontend/src/pages/index.tsx create mode 100644 apps/frontend/src/services/api.ts create mode 100644 apps/frontend/src/styles/promtFromComponent.css create mode 100644 apps/frontend/src/styles/responseViewerComponent.css create mode 100644 apps/frontend/src/utils/constants.ts create mode 100644 apps/frontend/src/utils/validator.ts create mode 100644 apps/frontend/src/vite-env.d.ts diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/apps/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx new file mode 100644 index 0000000..c920881 --- /dev/null +++ b/apps/frontend/src/App.tsx @@ -0,0 +1,13 @@ + +import './App.css' + +function App() { + + return ( + <> +

Initial Folder & Setup done

+ + ) +} + +export default App diff --git a/apps/frontend/src/assets/react.svg b/apps/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/components/PromptForm.tsx b/apps/frontend/src/components/PromptForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/components/ResponseViewer.tsx b/apps/frontend/src/components/ResponseViewer.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/apps/frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/apps/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/frontend/src/pages/index.tsx b/apps/frontend/src/pages/index.tsx new file mode 100644 index 0000000..75ccbd6 --- /dev/null +++ b/apps/frontend/src/pages/index.tsx @@ -0,0 +1 @@ +// Main Ui \ No newline at end of file diff --git a/apps/frontend/src/services/api.ts b/apps/frontend/src/services/api.ts new file mode 100644 index 0000000..454d7a3 --- /dev/null +++ b/apps/frontend/src/services/api.ts @@ -0,0 +1 @@ +// api calls \ No newline at end of file diff --git a/apps/frontend/src/styles/promtFromComponent.css b/apps/frontend/src/styles/promtFromComponent.css new file mode 100644 index 0000000..110aa41 --- /dev/null +++ b/apps/frontend/src/styles/promtFromComponent.css @@ -0,0 +1 @@ +/* css */ \ No newline at end of file diff --git a/apps/frontend/src/styles/responseViewerComponent.css b/apps/frontend/src/styles/responseViewerComponent.css new file mode 100644 index 0000000..110aa41 --- /dev/null +++ b/apps/frontend/src/styles/responseViewerComponent.css @@ -0,0 +1 @@ +/* css */ \ No newline at end of file diff --git a/apps/frontend/src/utils/constants.ts b/apps/frontend/src/utils/constants.ts new file mode 100644 index 0000000..5778a34 --- /dev/null +++ b/apps/frontend/src/utils/constants.ts @@ -0,0 +1 @@ +// All constants \ No newline at end of file diff --git a/apps/frontend/src/utils/validator.ts b/apps/frontend/src/utils/validator.ts new file mode 100644 index 0000000..106e2b7 --- /dev/null +++ b/apps/frontend/src/utils/validator.ts @@ -0,0 +1 @@ +// All validation related stuff \ No newline at end of file diff --git a/apps/frontend/src/vite-env.d.ts b/apps/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// From 47e7d68de8b076d19e9e459871af2fe066c62304 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Thu, 5 Jun 2025 14:57:14 +0530 Subject: [PATCH 02/22] initial folder setup and architecture scaffolding completed --- apps/backend/.gitignore | 26 +++++++++++++++ apps/backend/package.json | 27 ++++++++++++++++ apps/backend/src/api/routes.ts | 1 + apps/backend/src/db/postgres.ts | 1 + apps/backend/src/db/redis.ts | 1 + apps/backend/src/llm/geminiHelper.ts | 1 + apps/backend/src/models/log.ts | 1 + apps/backend/src/server.ts | 1 + apps/backend/src/services/calculator.ts | 1 + apps/backend/src/tests/run.test.ts | 1 + apps/backend/src/utils/contants.ts | 1 + apps/backend/src/validators/runSchema.ts | 1 + apps/backend/src/validators/webSearch.ts | 1 + apps/backend/tsconfig.json | 23 ++++++++++++++ apps/frontend/.gitignore | 25 +++++++++++++++ apps/frontend/eslint.config.js | 28 +++++++++++++++++ apps/frontend/index.html | 13 ++++++++ apps/frontend/package.json | 40 ++++++++++++++++++++++++ apps/frontend/public/vite.svg | 1 + apps/frontend/tailwind.config.js | 12 +++++++ apps/frontend/tsconfig.app.json | 27 ++++++++++++++++ apps/frontend/tsconfig.json | 7 +++++ apps/frontend/tsconfig.node.json | 25 +++++++++++++++ apps/frontend/vite.config.ts | 7 +++++ infra/docker-compose.yml | 1 + 25 files changed, 273 insertions(+) create mode 100644 apps/backend/.gitignore create mode 100644 apps/backend/package.json create mode 100644 apps/backend/src/api/routes.ts create mode 100644 apps/backend/src/db/postgres.ts create mode 100644 apps/backend/src/db/redis.ts create mode 100644 apps/backend/src/llm/geminiHelper.ts create mode 100644 apps/backend/src/models/log.ts create mode 100644 apps/backend/src/server.ts create mode 100644 apps/backend/src/services/calculator.ts create mode 100644 apps/backend/src/tests/run.test.ts create mode 100644 apps/backend/src/utils/contants.ts create mode 100644 apps/backend/src/validators/runSchema.ts create mode 100644 apps/backend/src/validators/webSearch.ts create mode 100644 apps/backend/tsconfig.json create mode 100644 apps/frontend/.gitignore create mode 100644 apps/frontend/eslint.config.js create mode 100644 apps/frontend/index.html create mode 100644 apps/frontend/package.json create mode 100644 apps/frontend/public/vite.svg create mode 100644 apps/frontend/tailwind.config.js create mode 100644 apps/frontend/tsconfig.app.json create mode 100644 apps/frontend/tsconfig.json create mode 100644 apps/frontend/tsconfig.node.json create mode 100644 apps/frontend/vite.config.ts create mode 100644 infra/docker-compose.yml diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore new file mode 100644 index 0000000..2f4d051 --- /dev/null +++ b/apps/backend/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +package-lock.json +.env +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..70bfd30 --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@types/pg": "^8.15.4", + "body-parser": "^2.2.0", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "pg": "^8.16.0", + "redis": "^5.5.5", + "zod": "^3.25.51" + }, + "devDependencies": { + "@types/express": "^5.0.2", + "@types/node": "^22.15.29", + "@types/redis": "^4.0.11", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/apps/backend/src/api/routes.ts b/apps/backend/src/api/routes.ts new file mode 100644 index 0000000..1c49ab3 --- /dev/null +++ b/apps/backend/src/api/routes.ts @@ -0,0 +1 @@ +// routes.ts \ No newline at end of file diff --git a/apps/backend/src/db/postgres.ts b/apps/backend/src/db/postgres.ts new file mode 100644 index 0000000..07d0a96 --- /dev/null +++ b/apps/backend/src/db/postgres.ts @@ -0,0 +1 @@ +// postgres.ts \ No newline at end of file diff --git a/apps/backend/src/db/redis.ts b/apps/backend/src/db/redis.ts new file mode 100644 index 0000000..0cc7e75 --- /dev/null +++ b/apps/backend/src/db/redis.ts @@ -0,0 +1 @@ +// redis.ts \ No newline at end of file diff --git a/apps/backend/src/llm/geminiHelper.ts b/apps/backend/src/llm/geminiHelper.ts new file mode 100644 index 0000000..5089dd5 --- /dev/null +++ b/apps/backend/src/llm/geminiHelper.ts @@ -0,0 +1 @@ +// Gemini Helper diff --git a/apps/backend/src/models/log.ts b/apps/backend/src/models/log.ts new file mode 100644 index 0000000..74a9413 --- /dev/null +++ b/apps/backend/src/models/log.ts @@ -0,0 +1 @@ +// log.ts \ No newline at end of file diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts new file mode 100644 index 0000000..be4c6e7 --- /dev/null +++ b/apps/backend/src/server.ts @@ -0,0 +1 @@ +// server.ts \ No newline at end of file diff --git a/apps/backend/src/services/calculator.ts b/apps/backend/src/services/calculator.ts new file mode 100644 index 0000000..e9cc85f --- /dev/null +++ b/apps/backend/src/services/calculator.ts @@ -0,0 +1 @@ +// calculator.ts \ No newline at end of file diff --git a/apps/backend/src/tests/run.test.ts b/apps/backend/src/tests/run.test.ts new file mode 100644 index 0000000..d04fad4 --- /dev/null +++ b/apps/backend/src/tests/run.test.ts @@ -0,0 +1 @@ +// run.test.ts \ No newline at end of file diff --git a/apps/backend/src/utils/contants.ts b/apps/backend/src/utils/contants.ts new file mode 100644 index 0000000..40cd9ec --- /dev/null +++ b/apps/backend/src/utils/contants.ts @@ -0,0 +1 @@ +// All Constants \ No newline at end of file diff --git a/apps/backend/src/validators/runSchema.ts b/apps/backend/src/validators/runSchema.ts new file mode 100644 index 0000000..67feb7f --- /dev/null +++ b/apps/backend/src/validators/runSchema.ts @@ -0,0 +1 @@ +// runSchema diff --git a/apps/backend/src/validators/webSearch.ts b/apps/backend/src/validators/webSearch.ts new file mode 100644 index 0000000..1043324 --- /dev/null +++ b/apps/backend/src/validators/webSearch.ts @@ -0,0 +1 @@ +// webSearch \ No newline at end of file diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 0000000..da6c7d3 --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", // JavaScript version output + "module": "CommonJS", // Module system (Node.js standard) + "lib": ["ES2020"], // Standard library to include + "outDir": "./dist", // Output folder for compiled JS + "rootDir": "./src", // Source folder for TS files + "strict": true, // Enable all strict type-checking options + "esModuleInterop": true, // Support for importing CommonJS modules + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "moduleResolution": "node", // Resolve modules like Node.js + "resolveJsonModule": true, // Allow importing JSON files + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "sourceMap": true, // Generate source maps for debugging + "allowJs": false, // Do not allow compiling JS files + "types": ["node"] // Includes Node.js types for type checking + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore new file mode 100644 index 0000000..2107e24 --- /dev/null +++ b/apps/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +package-lock.json +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/apps/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/apps/frontend/index.html b/apps/frontend/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/apps/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..1d992d6 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "axios": "^1.9.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "next": "^15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwind-variants": "^1.0.0", + "zod": "^3.25.51" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/node": "^22.15.29", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.4", + "tailwindcss": "^4.1.8", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } +} diff --git a/apps/frontend/public/vite.svg b/apps/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js new file mode 100644 index 0000000..36c3e52 --- /dev/null +++ b/apps/frontend/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx}', + './components/**/*.{js,ts,jsx,tsx}', + './app/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/frontend/tsconfig.app.json b/apps/frontend/tsconfig.app.json new file mode 100644 index 0000000..c9ccbd4 --- /dev/null +++ b/apps/frontend/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/frontend/tsconfig.node.json b/apps/frontend/tsconfig.node.json new file mode 100644 index 0000000..9728af2 --- /dev/null +++ b/apps/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/apps/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..1af9cdf --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1 @@ +# docker - compose we will initialize later \ No newline at end of file From b0a15293971421033cff06a9402881be7f3bc4d1 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Thu, 5 Jun 2025 16:14:05 +0530 Subject: [PATCH 03/22] Initialize PostgreSQL connection and ensure run_logs table setup --- apps/backend/package.json | 2 ++ apps/backend/src/db/postgres.ts | 33 +++++++++++++++++++++++++++++- apps/backend/src/utils/contants.ts | 16 ++++++++++++++- apps/backend/src/utils/types.ts | 12 +++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/utils/types.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 70bfd30..d652254 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "dev": "nodemon src/server.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", @@ -13,6 +14,7 @@ "body-parser": "^2.2.0", "dotenv": "^16.5.0", "express": "^5.1.0", + "nodemon": "^3.1.10", "pg": "^8.16.0", "redis": "^5.5.5", "zod": "^3.25.51" diff --git a/apps/backend/src/db/postgres.ts b/apps/backend/src/db/postgres.ts index 07d0a96..4e1c9a9 100644 --- a/apps/backend/src/db/postgres.ts +++ b/apps/backend/src/db/postgres.ts @@ -1 +1,32 @@ -// postgres.ts \ No newline at end of file +import { Pool } from 'pg'; +import { RunLog } from '../utils/types'; +import { CREATE_LOG_TABLE_QUERY, SAVE_LOG_QUERY } from '../utils/contants'; +import dotenv from 'dotenv'; + +dotenv.config(); +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +async function initDB() { + try { + await pool.query('SELECT NOW()'); + console.log('Postgres DB connected'); + await pool.query(CREATE_LOG_TABLE_QUERY); + console.log('run_logs table ensured'); + } catch (err) { + console.error('Postgres DB initialization failed:', err); + } +} + +initDB(); + +export async function saveRunLog(log: RunLog): Promise { + const values = [log.prompt, log.tool, log.response, log.timestamp, log.tokens]; + + try { + await pool.query(SAVE_LOG_QUERY, values); + } catch (err) { + console.error('Failed to save run log to Postgres:', err); + } +} diff --git a/apps/backend/src/utils/contants.ts b/apps/backend/src/utils/contants.ts index 40cd9ec..cf6d345 100644 --- a/apps/backend/src/utils/contants.ts +++ b/apps/backend/src/utils/contants.ts @@ -1 +1,15 @@ -// All Constants \ No newline at end of file +export const CREATE_LOG_TABLE_QUERY = ` + CREATE TABLE IF NOT EXISTS run_logs ( + id SERIAL PRIMARY KEY, + prompt TEXT NOT NULL, + tool TEXT NOT NULL, + response TEXT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + tokens INTEGER NOT NULL + ); + `; +export const SAVE_LOG_QUERY = ` + INSERT INTO run_logs (prompt, tool, response, timestamp, tokens) + VALUES ($1, $2, $3, $4, $5) + `; + diff --git a/apps/backend/src/utils/types.ts b/apps/backend/src/utils/types.ts new file mode 100644 index 0000000..d57a861 --- /dev/null +++ b/apps/backend/src/utils/types.ts @@ -0,0 +1,12 @@ +export type ToolType = 'web-search' | 'calculator'; + +export interface RunRequest { + prompt: string; + tool: ToolType; +} + +export interface RunLog extends RunRequest { + response: string; + timestamp?: Date; + tokens?: number; +} \ No newline at end of file From f2eca12080fa5e0659ac712135168b1b4b7f3ddb Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Thu, 5 Jun 2025 17:12:58 +0530 Subject: [PATCH 04/22] Implement redis client and limit recent runlogs sorted set to 10 entries --- apps/backend/src/db/redis.ts | 35 ++++++++++++++++++++++++++++++++++- apps/backend/tsconfig.json | 2 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/db/redis.ts b/apps/backend/src/db/redis.ts index 0cc7e75..b17969b 100644 --- a/apps/backend/src/db/redis.ts +++ b/apps/backend/src/db/redis.ts @@ -1 +1,34 @@ -// redis.ts \ No newline at end of file +// redis.ts +import { createClient } from 'redis'; +import { RunLog } from '../utils/types'; + +const redisClient = createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379', +}); + +redisClient.on('error', (err) => console.error('Redis Client Error', err)); + +await redisClient.connect(); + +export async function cacheRun(log: RunLog): Promise { + try { + const key = `runlog:${log.timestamp!.toISOString()}`; + await redisClient.set(key, JSON.stringify(log), { + EX: 3600, // expire in 1 hour + }); + + // Add the key to a sorted set with timestamp as score + await redisClient.zAdd('recent_runlogs', { + score: log.timestamp!.getTime(), + value: key, + }); + + + const totalCount = await redisClient.zCard('recent_runlogs'); + if (totalCount > 10) { + await redisClient.zRemRangeByRank('recent_runlogs', 0, totalCount - 11); // It will store upto 10 user requests + } + } catch (err) { + console.error('Failed to cache run log in Redis:', err); + } +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index da6c7d3..21a9c1c 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", // JavaScript version output - "module": "CommonJS", // Module system (Node.js standard) + "module": "es2022", // Module system (Node.js standard) "lib": ["ES2020"], // Standard library to include "outDir": "./dist", // Output folder for compiled JS "rootDir": "./src", // Source folder for TS files From b8b71fa382c359d0f995fc42622f8ab10ef694eb Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Thu, 5 Jun 2025 18:09:27 +0530 Subject: [PATCH 05/22] Integrate Gemini AI for content generation --- apps/backend/package.json | 1 + apps/backend/src/llm/geminiHelper.ts | 49 +++++++++++++++++++++++++++- apps/backend/src/utils/types.ts | 5 +++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index d652254..d375c32 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,6 +10,7 @@ "license": "ISC", "description": "", "dependencies": { + "@google/genai": "^1.4.0", "@types/pg": "^8.15.4", "body-parser": "^2.2.0", "dotenv": "^16.5.0", diff --git a/apps/backend/src/llm/geminiHelper.ts b/apps/backend/src/llm/geminiHelper.ts index 5089dd5..65f1f15 100644 --- a/apps/backend/src/llm/geminiHelper.ts +++ b/apps/backend/src/llm/geminiHelper.ts @@ -1 +1,48 @@ -// Gemini Helper +import { GoogleGenAI } from "@google/genai"; +import { GeminiResponse } from "../utils/types"; +import dotenv from "dotenv"; +dotenv.config(); + +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; +const GEMINI_MODEL = process.env.GEMINI_MODEL || ""; + +if (!GEMINI_API_KEY || GEMINI_MODEL) { + throw new Error("GEMINI_API_KEY or GEMINI_MODEL is not set in environment variables."); +} + +const genAI = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); + +export async function generateGeminiResponse( + prompt: string, + tool: string +): Promise { + try { + const fullPrompt = `Use the "${tool}" tool to help answer this: ${prompt}`; + + const result = await genAI.models.generateContent({ + model: GEMINI_MODEL, + contents: [{ role: "user", parts: [{ text: fullPrompt }] }], + }); + + const responseText = result.text ?? ""; + let tokensUsed = 0; + if (result.usageMetadata) { + tokensUsed = result.usageMetadata.totalTokenCount || 0; + } else { + console.warn( + "usageMetadata not found in Gemini API response. Token count may not be accurate." + ); + } + + console.log("Full Gemini API Response:", result); + console.log("Tokens Used:", tokensUsed); + + return { + response: responseText, + tokensUsed: tokensUsed, + }; + } catch (error) { + console.error("Error calling Gemini API:", error); + throw error; + } +} diff --git a/apps/backend/src/utils/types.ts b/apps/backend/src/utils/types.ts index d57a861..fb62192 100644 --- a/apps/backend/src/utils/types.ts +++ b/apps/backend/src/utils/types.ts @@ -9,4 +9,9 @@ export interface RunLog extends RunRequest { response: string; timestamp?: Date; tokens?: number; +} + +export interface GeminiResponse { + response: string; + tokensUsed: number; } \ No newline at end of file From 2c11779d45904ac4b1c7529350ea00a1f90a7598 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Fri, 6 Jun 2025 00:32:23 +0530 Subject: [PATCH 06/22] Create websearch & calculator services --- apps/backend/package.json | 7 ++++++- apps/backend/src/services/calculator.ts | 12 +++++++++++- apps/backend/src/services/webSearch.ts | 25 +++++++++++++++++++++++++ apps/backend/src/utils/contants.ts | 1 + 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/services/webSearch.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index d375c32..49f504e 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -2,8 +2,9 @@ "name": "backend", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { - "dev": "nodemon src/server.ts", + "dev": "nodemon --exec tsx src/server.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", @@ -13,8 +14,11 @@ "@google/genai": "^1.4.0", "@types/pg": "^8.15.4", "body-parser": "^2.2.0", + "cheerio": "^1.0.0", "dotenv": "^16.5.0", "express": "^5.1.0", + "mathjs": "^14.5.2", + "node-fetch": "^3.3.2", "nodemon": "^3.1.10", "pg": "^8.16.0", "redis": "^5.5.5", @@ -25,6 +29,7 @@ "@types/node": "^22.15.29", "@types/redis": "^4.0.11", "ts-node": "^10.9.2", + "tsx": "^4.19.4", "typescript": "^5.8.3" } } diff --git a/apps/backend/src/services/calculator.ts b/apps/backend/src/services/calculator.ts index e9cc85f..4706b82 100644 --- a/apps/backend/src/services/calculator.ts +++ b/apps/backend/src/services/calculator.ts @@ -1 +1,11 @@ -// calculator.ts \ No newline at end of file +import { evaluate } from "mathjs"; + +export function evaluateExpression(prompt: string): number { + try { + const result = evaluate(prompt); + if (typeof result !== 'number') throw new Error("Not a valid number result"); + return result; + } catch { + throw new Error("Invalid math expression"); + } +} \ No newline at end of file diff --git a/apps/backend/src/services/webSearch.ts b/apps/backend/src/services/webSearch.ts new file mode 100644 index 0000000..7e748e1 --- /dev/null +++ b/apps/backend/src/services/webSearch.ts @@ -0,0 +1,25 @@ +import fetch from 'node-fetch'; +import * as cheerio from 'cheerio'; +import { DUCK_DUCK_GO_BASE_URL } from '../utils/contants'; + +export async function performWebSearch(prompt: string): Promise<{ title: string; link: string }[]> { + const query = encodeURIComponent(prompt); + const res = await fetch(`${DUCK_DUCK_GO_BASE_URL}/html/?q=${query}`); + const html = await res.text(); + const $ = cheerio.load(html); + + const results: { title: string; link: string }[] = []; + + $('.result__a').each((i, el) => { + if (i >= 10) return false; + const title = $(el).text(); + const link = $(el).attr('href'); + if (title && link) results.push({ title, link }); + }); + + if (results.length === 0) { + throw new Error('No search results found.'); + } + + return results; +} diff --git a/apps/backend/src/utils/contants.ts b/apps/backend/src/utils/contants.ts index cf6d345..c85de80 100644 --- a/apps/backend/src/utils/contants.ts +++ b/apps/backend/src/utils/contants.ts @@ -12,4 +12,5 @@ export const SAVE_LOG_QUERY = ` INSERT INTO run_logs (prompt, tool, response, timestamp, tokens) VALUES ($1, $2, $3, $4, $5) `; +export const DUCK_DUCK_GO_BASE_URL = `https://html.duckduckgo.com`; From 05bfbb28641b3a67a2c11f4ae3dc216ceb378c73 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Fri, 6 Jun 2025 00:36:16 +0530 Subject: [PATCH 07/22] Create reqired route for get response of user query --- apps/backend/src/api/routes.ts | 47 +++++++++++++++++++++++- apps/backend/src/server.ts | 17 ++++++++- apps/backend/src/validators/runSchema.ts | 9 ++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/api/routes.ts b/apps/backend/src/api/routes.ts index 1c49ab3..a8bfb72 100644 --- a/apps/backend/src/api/routes.ts +++ b/apps/backend/src/api/routes.ts @@ -1 +1,46 @@ -// routes.ts \ No newline at end of file +import express from "express"; +import { RunRequestSchema } from "../validators/runSchema"; +import { performWebSearch } from "../services/webSearch"; +import { evaluateExpression } from "../services/calculator"; +import { generateFriendlyReply } from "../llm/geminiHelper"; + +const router = express.Router(); + +router.post("/", async (req: any, res: any) => { + const parseResult = RunRequestSchema.safeParse(req.body); + if (!parseResult.success) return res.status(400).json({ error: "Invalid input" }); + const { prompt, tool } = parseResult.data; + + try { + let responseData: any; + + if (tool === "web-search") { + const results = await performWebSearch(prompt); + const { text, totalTokenCount } = await generateFriendlyReply( + "search", + prompt, + results + ); + responseData = { + prompt, + tool, + results, + totalTokenCount, + summary: text, + }; + } else if (tool === "calculator") { + const result = evaluateExpression(prompt).toString(); + const { text, totalTokenCount } = await generateFriendlyReply( + "calc", + prompt, + result + ); + responseData = { prompt, tool, result, totalTokenCount, summary: text }; + } + res.json(responseData); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +export default router; diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index be4c6e7..70155b3 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -1 +1,16 @@ -// server.ts \ No newline at end of file +import express from 'express'; +import bodyParser from 'body-parser'; +import dotenv from 'dotenv'; +import router from './api/routes'; + +dotenv.config(); + +const app = express(); +app.use(bodyParser.json()); + +app.use('/api/v1/query', router); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/apps/backend/src/validators/runSchema.ts b/apps/backend/src/validators/runSchema.ts index 67feb7f..7630fff 100644 --- a/apps/backend/src/validators/runSchema.ts +++ b/apps/backend/src/validators/runSchema.ts @@ -1 +1,8 @@ -// runSchema +import { z } from "zod"; + +export const RunRequestSchema = z.object({ + prompt: z.string().max(500), + tool: z.enum(['web-search', 'calculator']), +}); + +export type RunRequest = z.infer; \ No newline at end of file From 79d61afe3f9ca381631ce998d6b2b3f6ae799305 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Fri, 6 Jun 2025 00:45:04 +0530 Subject: [PATCH 08/22] Update base prompt of LLM for get required output --- apps/backend/src/llm/geminiHelper.ts | 64 ++++++++++++---------------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/apps/backend/src/llm/geminiHelper.ts b/apps/backend/src/llm/geminiHelper.ts index 65f1f15..97f9199 100644 --- a/apps/backend/src/llm/geminiHelper.ts +++ b/apps/backend/src/llm/geminiHelper.ts @@ -1,48 +1,38 @@ import { GoogleGenAI } from "@google/genai"; -import { GeminiResponse } from "../utils/types"; import dotenv from "dotenv"; dotenv.config(); const GEMINI_API_KEY = process.env.GEMINI_API_KEY; const GEMINI_MODEL = process.env.GEMINI_MODEL || ""; -if (!GEMINI_API_KEY || GEMINI_MODEL) { - throw new Error("GEMINI_API_KEY or GEMINI_MODEL is not set in environment variables."); +if (!GEMINI_API_KEY || !GEMINI_MODEL) { + throw new Error( + "GEMINI_API_KEY or GEMINI_MODEL is not set in environment variables." + ); } -const genAI = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); - -export async function generateGeminiResponse( +const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! }); +export async function generateFriendlyReply( + type: "search" | "calc", prompt: string, - tool: string -): Promise { - try { - const fullPrompt = `Use the "${tool}" tool to help answer this: ${prompt}`; - - const result = await genAI.models.generateContent({ - model: GEMINI_MODEL, - contents: [{ role: "user", parts: [{ text: fullPrompt }] }], - }); - - const responseText = result.text ?? ""; - let tokensUsed = 0; - if (result.usageMetadata) { - tokensUsed = result.usageMetadata.totalTokenCount || 0; - } else { - console.warn( - "usageMetadata not found in Gemini API response. Token count may not be accurate." - ); - } - - console.log("Full Gemini API Response:", result); - console.log("Tokens Used:", tokensUsed); - - return { - response: responseText, - tokensUsed: tokensUsed, - }; - } catch (error) { - console.error("Error calling Gemini API:", error); - throw error; - } + result: string | { title: string; link: string }[] +): Promise<{ text: string; totalTokenCount: number }> { + const instruction = `You are a concise assistant. Respond only in the following format, and do not add anything else.\n`; + const promptMap = { + search: + `${instruction} Based on the prompt "${prompt}", here’s what I found: ` + + (Array.isArray(result) + ? result.map((r, i) => `${i + 1}. ${r.title} (${r.link})`).join("\n") + : result), + calc: `${instruction} Based on the prompt "${prompt}", the answer to your calculation is ${result}.`, + }; + + const geminiResponse = await ai.models.generateContent({ + model: GEMINI_MODEL, + contents: [{ role: "user", parts: [{ text: promptMap[type] }] }], + }); + const text = geminiResponse?.candidates?.[0]?.content?.parts?.[0]?.text || ""; + const totalTokenCount = geminiResponse?.usageMetadata?.totalTokenCount || 0; + // console.log(geminiResponse); + return { text, totalTokenCount }; } From 58972f28ef9cc828828387bc47e4a0aabe44fcec Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Fri, 6 Jun 2025 17:55:15 +0530 Subject: [PATCH 09/22] Implement fastify to create API --- apps/backend/package.json | 8 +- apps/backend/src/api/routes.ts | 201 +++++++++++++++++++++++++-------- apps/backend/src/server.ts | 56 +++++++-- 3 files changed, 209 insertions(+), 56 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 49f504e..00597b1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -11,12 +11,15 @@ "license": "ISC", "description": "", "dependencies": { + "@fastify/compress": "^6.5.0", + "@fastify/cors": "^8.4.2", + "@fastify/helmet": "^11.1.1", + "@fastify/rate-limit": "^9.1.0", "@google/genai": "^1.4.0", "@types/pg": "^8.15.4", - "body-parser": "^2.2.0", "cheerio": "^1.0.0", "dotenv": "^16.5.0", - "express": "^5.1.0", + "fastify": "^4.24.3", "mathjs": "^14.5.2", "node-fetch": "^3.3.2", "nodemon": "^3.1.10", @@ -28,6 +31,7 @@ "@types/express": "^5.0.2", "@types/node": "^22.15.29", "@types/redis": "^4.0.11", + "pino-pretty": "^13.0.0", "ts-node": "^10.9.2", "tsx": "^4.19.4", "typescript": "^5.8.3" diff --git a/apps/backend/src/api/routes.ts b/apps/backend/src/api/routes.ts index a8bfb72..e871505 100644 --- a/apps/backend/src/api/routes.ts +++ b/apps/backend/src/api/routes.ts @@ -1,46 +1,159 @@ -import express from "express"; -import { RunRequestSchema } from "../validators/runSchema"; -import { performWebSearch } from "../services/webSearch"; -import { evaluateExpression } from "../services/calculator"; -import { generateFriendlyReply } from "../llm/geminiHelper"; - -const router = express.Router(); - -router.post("/", async (req: any, res: any) => { - const parseResult = RunRequestSchema.safeParse(req.body); - if (!parseResult.success) return res.status(400).json({ error: "Invalid input" }); - const { prompt, tool } = parseResult.data; - - try { - let responseData: any; - - if (tool === "web-search") { - const results = await performWebSearch(prompt); - const { text, totalTokenCount } = await generateFriendlyReply( - "search", - prompt, - results - ); - responseData = { - prompt, - tool, - results, - totalTokenCount, - summary: text, - }; - } else if (tool === "calculator") { - const result = evaluateExpression(prompt).toString(); - const { text, totalTokenCount } = await generateFriendlyReply( - "calc", - prompt, - result - ); - responseData = { prompt, tool, result, totalTokenCount, summary: text }; - } - res.json(responseData); - } catch (err) { - res.status(500).json({ error: (err as Error).message }); +import { FastifyInstance } from 'fastify'; +import { RunRequestSchema } from '../validators/runSchema'; +import { performWebSearch } from '../services/webSearch'; +import { evaluateExpression } from '../services/calculator'; +import { generateFriendlyReply } from '../llm/geminiHelper'; + +// Convert Zod schema to JSON Schema for Fastify, no need zod + +const requestBodySchema = { + type: 'object', + required: ['prompt', 'tool'], + properties: { + prompt: { type: 'string', minLength: 1, maxLength: 5000 }, + tool: { type: 'string', enum: ['web-search', 'calculator'] } + }, + additionalProperties: false +}; + +const successResponseSchema = { + type: 'object', + properties: { + prompt: { type: 'string' }, + tool: { type: 'string' }, + results: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + link: { type: 'string' } + }, + required: ['title', 'link'] + } + }, + result: { type: 'string' }, + totalTokenCount: { type: 'number' }, + summary: { type: 'string' }, + timestamp: { type: 'string' } + } +}; + +const errorResponseSchema = { + type: 'object', + properties: { + error: { type: 'string' }, + code: { type: 'string' }, + timestamp: { type: 'string' } } -}); +}; + +interface RunRequest { + prompt: string; + tool: 'web-search' | 'calculator'; +} + +export default async function routes(fastify: FastifyInstance) { -export default router; + fastify.get('/health', { + schema: { + response: { + 200: { + type: 'object', + properties: { + status: { type: 'string' }, + timestamp: { type: 'string' }, + uptime: { type: 'number' } + } + } + } + } + }, async () => { + return { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }; + }); + + fastify.post<{ Body: RunRequest }>('/query', { + schema: { + body: requestBodySchema, + response: { + 200: successResponseSchema, + 400: errorResponseSchema, + 500: errorResponseSchema + } + }, + preValidation: async (request, reply) => { + const parseResult = RunRequestSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.code(400).send({ + error: "Invalid input", + code: "VALIDATION_ERROR", + timestamp: new Date().toISOString() + }); + } + } + }, async (request, reply) => { + const { prompt, tool } = request.body; + const startTime = Date.now(); + + try { + let responseData: any; + + if (tool === "web-search") { + const results = await performWebSearch(prompt); + const { text, totalTokenCount } = await generateFriendlyReply("search", prompt, results); + + responseData = { + prompt, + tool, + results, + totalTokenCount, + summary: text, + timestamp: new Date().toISOString() + }; + } else if (tool === "calculator") { + const result = evaluateExpression(prompt).toString(); + const { text, totalTokenCount } = await generateFriendlyReply("calc", prompt, result); + + responseData = { + prompt, + tool, + result, + totalTokenCount, + summary: text, + timestamp: new Date().toISOString() + }; + } + + fastify.log.info({ + method: request.method, + url: request.url, + tool, + responseTime: Date.now() - startTime, + tokenCount: responseData.totalTokenCount + }); + + return reply.code(200).send(responseData); + } catch (err) { + const error = err as Error; + + fastify.log.error({ + method: request.method, + url: request.url, + error: error.message, + stack: error.stack, + tool, + prompt: prompt.substring(0, 100) + }); + + return reply.code(500).send({ + error: error.message, + code: "INTERNAL_ERROR", + timestamp: new Date().toISOString() + }); + } + }); +} diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index 70155b3..e62741f 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -1,16 +1,52 @@ -import express from 'express'; -import bodyParser from 'body-parser'; import dotenv from 'dotenv'; import router from './api/routes'; - +import Fastify from 'fastify'; dotenv.config(); -const app = express(); -app.use(bodyParser.json()); -app.use('/api/v1/query', router); +const fastify = Fastify({ + logger: { + level: process.env.LOG_LEVEL || 'info', + transport: process.env.NODE_ENV === 'development' ? { + target: 'pino-pretty' + } : undefined + }, + bodyLimit: 1024 * 1024, + trustProxy: true, + keepAliveTimeout: 72000, + connectionTimeout: 10000 +}); + +async function registerRoutes() { + await fastify.register(router, { prefix: '/api/v1' }); +} +async function start() { + try { + await registerRoutes(); + + const PORT = parseInt(process.env.PORT || '3000', 10); + const HOST = process.env.HOST || '0.0.0.0'; + + await fastify.listen({ port: PORT, host: HOST }); + console.log(`Fastify server running at http://${HOST}:${PORT}`); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +} + +process.on('SIGINT', async () => { + console.log('Received SIGINT, shutting down gracefully...'); + await fastify.close(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('Received SIGTERM, shutting down gracefully...'); + await fastify.close(); + process.exit(0); +}); -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`Server running at http://localhost:${PORT}`); -}); \ No newline at end of file +if (import.meta.url === `file://${process.argv[1]}`) { + start(); +} From 55960e497dfaa56e346bcb70a401bb3f68409059 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Fri, 6 Jun 2025 18:44:49 +0530 Subject: [PATCH 10/22] Integrate DB & Redis for cache logs & faster retrivals --- apps/backend/src/api/routes.ts | 282 ++++++++++-------- apps/backend/src/db/postgres.ts | 2 +- apps/backend/src/db/redis.ts | 41 ++- apps/backend/src/services/webSearch.ts | 2 +- .../src/utils/{contants.ts => constants.ts} | 2 +- 5 files changed, 187 insertions(+), 142 deletions(-) rename apps/backend/src/utils/{contants.ts => constants.ts} (86%) diff --git a/apps/backend/src/api/routes.ts b/apps/backend/src/api/routes.ts index e871505..b957c2e 100644 --- a/apps/backend/src/api/routes.ts +++ b/apps/backend/src/api/routes.ts @@ -1,159 +1,195 @@ -import { FastifyInstance } from 'fastify'; -import { RunRequestSchema } from '../validators/runSchema'; -import { performWebSearch } from '../services/webSearch'; -import { evaluateExpression } from '../services/calculator'; -import { generateFriendlyReply } from '../llm/geminiHelper'; +import { FastifyInstance } from "fastify"; +import { RunRequestSchema } from "../validators/runSchema"; +import { performWebSearch } from "../services/webSearch"; +import { evaluateExpression } from "../services/calculator"; +import { generateFriendlyReply } from "../llm/geminiHelper"; +import { cacheRun, getCachedRun } from "../db/redis"; +import { saveRunLog } from "../db/postgres"; +import { RunLog } from "../utils/types"; // Convert Zod schema to JSON Schema for Fastify, no need zod const requestBodySchema = { - type: 'object', - required: ['prompt', 'tool'], + type: "object", + required: ["prompt", "tool"], properties: { - prompt: { type: 'string', minLength: 1, maxLength: 5000 }, - tool: { type: 'string', enum: ['web-search', 'calculator'] } + prompt: { type: "string", minLength: 1, maxLength: 5000 }, + tool: { type: "string", enum: ["web-search", "calculator"] }, }, - additionalProperties: false + additionalProperties: false, }; const successResponseSchema = { - type: 'object', + type: "object", properties: { - prompt: { type: 'string' }, - tool: { type: 'string' }, - results: { - type: 'array', + prompt: { type: "string" }, + tool: { type: "string" }, + results: { + type: "array", items: { - type: 'object', + type: "object", properties: { - title: { type: 'string' }, - link: { type: 'string' } + title: { type: "string" }, + link: { type: "string" }, }, - required: ['title', 'link'] - } + required: ["title", "link"], + }, }, - result: { type: 'string' }, - totalTokenCount: { type: 'number' }, - summary: { type: 'string' }, - timestamp: { type: 'string' } - } + result: { type: "string" }, + totalTokenCount: { type: "number" }, + summary: { type: "string" }, + timestamp: { type: "string" }, + }, }; const errorResponseSchema = { - type: 'object', + type: "object", properties: { - error: { type: 'string' }, - code: { type: 'string' }, - timestamp: { type: 'string' } - } + error: { type: "string" }, + code: { type: "string" }, + timestamp: { type: "string" }, + }, }; interface RunRequest { prompt: string; - tool: 'web-search' | 'calculator'; + tool: "web-search" | "calculator"; } export default async function routes(fastify: FastifyInstance) { - - fastify.get('/health', { - schema: { - response: { - 200: { - type: 'object', - properties: { - status: { type: 'string' }, - timestamp: { type: 'string' }, - uptime: { type: 'number' } - } - } - } - } - }, async () => { - return { - status: 'healthy', - timestamp: new Date().toISOString(), - uptime: process.uptime() - }; - }); - - fastify.post<{ Body: RunRequest }>('/query', { - schema: { - body: requestBodySchema, - response: { - 200: successResponseSchema, - 400: errorResponseSchema, - 500: errorResponseSchema - } + // routes for get API server health + fastify.get( + "/health", + { + schema: { + response: { + 200: { + type: "object", + properties: { + status: { type: "string" }, + timestamp: { type: "string" }, + uptime: { type: "number" }, + }, + }, + }, + }, }, - preValidation: async (request, reply) => { - const parseResult = RunRequestSchema.safeParse(request.body); - if (!parseResult.success) { - return reply.code(400).send({ - error: "Invalid input", - code: "VALIDATION_ERROR", - timestamp: new Date().toISOString() - }); - } + async () => { + return { + status: "healthy", + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }; } - }, async (request, reply) => { - const { prompt, tool } = request.body; - const startTime = Date.now(); - - try { - let responseData: any; - - if (tool === "web-search") { - const results = await performWebSearch(prompt); - const { text, totalTokenCount } = await generateFriendlyReply("search", prompt, results); + ); +// routes for Ai response based on user query + fastify.post<{ Body: RunRequest }>( + "/query", + { + schema: { + body: requestBodySchema, + response: { + 200: successResponseSchema, + 400: errorResponseSchema, + 500: errorResponseSchema, + }, + }, + preValidation: async (request, reply) => { + const parseResult = RunRequestSchema.safeParse(request.body); + if (!parseResult.success) { + return reply.code(400).send({ + error: "Invalid input", + code: "VALIDATION_ERROR", + timestamp: new Date().toISOString(), + }); + } + }, + }, + async (request, reply) => { + const { prompt, tool } = request.body; + const startTime = Date.now(); + + try { + const cachedUserQueryAndAiResponse = await getCachedRun(tool, prompt); + + if (cachedUserQueryAndAiResponse) { + fastify.log.info({ msg: "Cache hit", tool, prompt }); + return reply + .code(200) + .send(JSON.parse(cachedUserQueryAndAiResponse.response)); + } + let responseData: any; + + if (tool === "web-search") { + const results = await performWebSearch(prompt); + const { text, totalTokenCount } = await generateFriendlyReply( + "search", + prompt, + results + ); + + responseData = { + prompt, + tool, + results, + totalTokenCount, + summary: text, + timestamp: new Date().toISOString(), + }; + } else if (tool === "calculator") { + const result = evaluateExpression(prompt).toString(); + const { text, totalTokenCount } = await generateFriendlyReply( + "calc", + prompt, + result + ); + + responseData = { + prompt, + tool, + result, + totalTokenCount, + summary: text, + timestamp: new Date().toISOString(), + }; + } - responseData = { + fastify.log.info({ + method: request.method, + url: request.url, + tool, + responseTime: Date.now() - startTime, + tokenCount: responseData.totalTokenCount, + }); + const log: RunLog = { prompt, tool, - results, - totalTokenCount, - summary: text, - timestamp: new Date().toISOString() + response: JSON.stringify(responseData), + timestamp: new Date(responseData.timestamp), + tokens: responseData.totalTokenCount, }; - } else if (tool === "calculator") { - const result = evaluateExpression(prompt).toString(); - const { text, totalTokenCount } = await generateFriendlyReply("calc", prompt, result); - responseData = { - prompt, + await Promise.all([cacheRun(log), saveRunLog(log)]); + + return reply.code(200).send(responseData); + } catch (err) { + const error = err as Error; + + fastify.log.error({ + method: request.method, + url: request.url, + error: error.message, + stack: error.stack, tool, - result, - totalTokenCount, - summary: text, - timestamp: new Date().toISOString() - }; - } + prompt: prompt.substring(0, 100), + }); - fastify.log.info({ - method: request.method, - url: request.url, - tool, - responseTime: Date.now() - startTime, - tokenCount: responseData.totalTokenCount - }); - - return reply.code(200).send(responseData); - } catch (err) { - const error = err as Error; - - fastify.log.error({ - method: request.method, - url: request.url, - error: error.message, - stack: error.stack, - tool, - prompt: prompt.substring(0, 100) - }); - - return reply.code(500).send({ - error: error.message, - code: "INTERNAL_ERROR", - timestamp: new Date().toISOString() - }); + return reply.code(500).send({ + error: error.message, + code: "INTERNAL_ERROR", + timestamp: new Date().toISOString(), + }); + } } - }); + ); } diff --git a/apps/backend/src/db/postgres.ts b/apps/backend/src/db/postgres.ts index 4e1c9a9..dd8ca8d 100644 --- a/apps/backend/src/db/postgres.ts +++ b/apps/backend/src/db/postgres.ts @@ -1,6 +1,6 @@ import { Pool } from 'pg'; import { RunLog } from '../utils/types'; -import { CREATE_LOG_TABLE_QUERY, SAVE_LOG_QUERY } from '../utils/contants'; +import { CREATE_LOG_TABLE_QUERY, SAVE_LOG_QUERY } from '../utils/constants'; import dotenv from 'dotenv'; dotenv.config(); diff --git a/apps/backend/src/db/redis.ts b/apps/backend/src/db/redis.ts index b17969b..528191e 100644 --- a/apps/backend/src/db/redis.ts +++ b/apps/backend/src/db/redis.ts @@ -1,34 +1,43 @@ -// redis.ts -import { createClient } from 'redis'; -import { RunLog } from '../utils/types'; +import { createClient } from "redis"; +import crypto from "crypto"; +import { RunLog } from "../utils/types"; const redisClient = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379', + url: process.env.REDIS_URL || "redis://localhost:6379", }); -redisClient.on('error', (err) => console.error('Redis Client Error', err)); - +redisClient.on("error", (err) => console.error("Redis Client Error", err)); await redisClient.connect(); +function getRedisKey(tool: string, prompt: string): string { + const hash = crypto.createHash("sha256").update(prompt).digest("hex"); + return `runlog:${tool}:${hash}`; +} + +export async function getCachedRun( + tool: string, + prompt: string +): Promise { + const key = getRedisKey(tool, prompt); + const data = await redisClient.get(key); + return data ? JSON.parse(data) : null; +} + export async function cacheRun(log: RunLog): Promise { try { - const key = `runlog:${log.timestamp!.toISOString()}`; - await redisClient.set(key, JSON.stringify(log), { - EX: 3600, // expire in 1 hour - }); + const key = getRedisKey(log.tool, log.prompt); + await redisClient.set(key, JSON.stringify(log), { EX: 3600 }); // expires after 1 hour - // Add the key to a sorted set with timestamp as score - await redisClient.zAdd('recent_runlogs', { + await redisClient.zAdd("recent_runlogs", { score: log.timestamp!.getTime(), value: key, }); - - const totalCount = await redisClient.zCard('recent_runlogs'); + const totalCount = await redisClient.zCard("recent_runlogs"); if (totalCount > 10) { - await redisClient.zRemRangeByRank('recent_runlogs', 0, totalCount - 11); // It will store upto 10 user requests + await redisClient.zRemRangeByRank("recent_runlogs", 0, totalCount - 11); } } catch (err) { - console.error('Failed to cache run log in Redis:', err); + console.error("Failed to cache run log in Redis:", err); } } diff --git a/apps/backend/src/services/webSearch.ts b/apps/backend/src/services/webSearch.ts index 7e748e1..19a61ce 100644 --- a/apps/backend/src/services/webSearch.ts +++ b/apps/backend/src/services/webSearch.ts @@ -1,6 +1,6 @@ import fetch from 'node-fetch'; import * as cheerio from 'cheerio'; -import { DUCK_DUCK_GO_BASE_URL } from '../utils/contants'; +import { DUCK_DUCK_GO_BASE_URL } from '../utils/constants'; export async function performWebSearch(prompt: string): Promise<{ title: string; link: string }[]> { const query = encodeURIComponent(prompt); diff --git a/apps/backend/src/utils/contants.ts b/apps/backend/src/utils/constants.ts similarity index 86% rename from apps/backend/src/utils/contants.ts rename to apps/backend/src/utils/constants.ts index c85de80..a67613c 100644 --- a/apps/backend/src/utils/contants.ts +++ b/apps/backend/src/utils/constants.ts @@ -12,5 +12,5 @@ export const SAVE_LOG_QUERY = ` INSERT INTO run_logs (prompt, tool, response, timestamp, tokens) VALUES ($1, $2, $3, $4, $5) `; -export const DUCK_DUCK_GO_BASE_URL = `https://html.duckduckgo.com`; +export const DUCK_DUCK_GO_BASE_URL = "https://html.duckduckgo.com"; From fd72d8f16c2a8d88a1cee212bdeb0cf6f95f22c5 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Sat, 7 Jun 2025 13:14:40 +0530 Subject: [PATCH 11/22] Add user ID support for Redis caching and Postgres logging --- apps/backend/src/api/routes.ts | 12 ++++++++---- apps/backend/src/db/postgres.ts | 5 +++-- apps/backend/src/db/redis.ts | 18 ++++++++++-------- apps/backend/src/utils/constants.ts | 5 +++-- apps/backend/src/utils/types.ts | 1 + apps/backend/src/validators/runSchema.ts | 1 + 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/api/routes.ts b/apps/backend/src/api/routes.ts index b957c2e..333220d 100644 --- a/apps/backend/src/api/routes.ts +++ b/apps/backend/src/api/routes.ts @@ -7,14 +7,15 @@ import { cacheRun, getCachedRun } from "../db/redis"; import { saveRunLog } from "../db/postgres"; import { RunLog } from "../utils/types"; -// Convert Zod schema to JSON Schema for Fastify, no need zod +// Convert Zod schema to JSON Schema for Fastify, const requestBodySchema = { type: "object", - required: ["prompt", "tool"], + required: ["prompt", "tool", "userId"], properties: { prompt: { type: "string", minLength: 1, maxLength: 5000 }, tool: { type: "string", enum: ["web-search", "calculator"] }, + userId: { type: "string", minLength: 1, maxLength: 16 }, }, additionalProperties: false, }; @@ -54,6 +55,7 @@ const errorResponseSchema = { interface RunRequest { prompt: string; tool: "web-search" | "calculator"; + userId: string; } export default async function routes(fastify: FastifyInstance) { @@ -106,11 +108,11 @@ export default async function routes(fastify: FastifyInstance) { }, }, async (request, reply) => { - const { prompt, tool } = request.body; + const { prompt, tool, userId } = request.body; const startTime = Date.now(); try { - const cachedUserQueryAndAiResponse = await getCachedRun(tool, prompt); + const cachedUserQueryAndAiResponse = await getCachedRun(userId, tool, prompt); if (cachedUserQueryAndAiResponse) { fastify.log.info({ msg: "Cache hit", tool, prompt }); @@ -161,7 +163,9 @@ export default async function routes(fastify: FastifyInstance) { responseTime: Date.now() - startTime, tokenCount: responseData.totalTokenCount, }); + const log: RunLog = { + userId, prompt, tool, response: JSON.stringify(responseData), diff --git a/apps/backend/src/db/postgres.ts b/apps/backend/src/db/postgres.ts index dd8ca8d..5b73bdd 100644 --- a/apps/backend/src/db/postgres.ts +++ b/apps/backend/src/db/postgres.ts @@ -10,7 +10,8 @@ const pool = new Pool({ async function initDB() { try { - await pool.query('SELECT NOW()'); + await pool.query("SET TIME ZONE 'UTC';"); + // await pool.query('SELECT NOW()'); console.log('Postgres DB connected'); await pool.query(CREATE_LOG_TABLE_QUERY); console.log('run_logs table ensured'); @@ -22,7 +23,7 @@ async function initDB() { initDB(); export async function saveRunLog(log: RunLog): Promise { - const values = [log.prompt, log.tool, log.response, log.timestamp, log.tokens]; + const values = [log.userId,log.prompt, log.tool, log.response, log.timestamp, log.tokens]; try { await pool.query(SAVE_LOG_QUERY, values); diff --git a/apps/backend/src/db/redis.ts b/apps/backend/src/db/redis.ts index 528191e..a60b021 100644 --- a/apps/backend/src/db/redis.ts +++ b/apps/backend/src/db/redis.ts @@ -9,33 +9,35 @@ const redisClient = createClient({ redisClient.on("error", (err) => console.error("Redis Client Error", err)); await redisClient.connect(); -function getRedisKey(tool: string, prompt: string): string { +function getRedisKey(userId:string, tool: string, prompt: string): string { const hash = crypto.createHash("sha256").update(prompt).digest("hex"); - return `runlog:${tool}:${hash}`; + return `runlog:${userId}:${tool}:${hash}`; } export async function getCachedRun( + userId: string, tool: string, prompt: string ): Promise { - const key = getRedisKey(tool, prompt); + const key = getRedisKey(userId, tool, prompt); const data = await redisClient.get(key); return data ? JSON.parse(data) : null; } export async function cacheRun(log: RunLog): Promise { try { - const key = getRedisKey(log.tool, log.prompt); - await redisClient.set(key, JSON.stringify(log), { EX: 3600 }); // expires after 1 hour + const key = getRedisKey(log.userId, log.tool, log.prompt); + await redisClient.set(key, JSON.stringify(log), { EX: 60 * 60 * 12 }); // expires after 12 hours - await redisClient.zAdd("recent_runlogs", { + const userZSet = `recent_runlogs:${log.userId}`; // scoped with user uniq id + await redisClient.zAdd(userZSet, { score: log.timestamp!.getTime(), value: key, }); - const totalCount = await redisClient.zCard("recent_runlogs"); + const totalCount = await redisClient.zCard(userZSet); if (totalCount > 10) { - await redisClient.zRemRangeByRank("recent_runlogs", 0, totalCount - 11); + await redisClient.zRemRangeByRank(userZSet, 0, totalCount - 11); } } catch (err) { console.error("Failed to cache run log in Redis:", err); diff --git a/apps/backend/src/utils/constants.ts b/apps/backend/src/utils/constants.ts index a67613c..30e4bd1 100644 --- a/apps/backend/src/utils/constants.ts +++ b/apps/backend/src/utils/constants.ts @@ -1,6 +1,7 @@ export const CREATE_LOG_TABLE_QUERY = ` CREATE TABLE IF NOT EXISTS run_logs ( id SERIAL PRIMARY KEY, + userId TEXT NOT NULL, prompt TEXT NOT NULL, tool TEXT NOT NULL, response TEXT NOT NULL, @@ -9,8 +10,8 @@ export const CREATE_LOG_TABLE_QUERY = ` ); `; export const SAVE_LOG_QUERY = ` - INSERT INTO run_logs (prompt, tool, response, timestamp, tokens) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO run_logs (userId, prompt, tool, response, timestamp, tokens) + VALUES ($1, $2, $3, $4, $5, $6) `; export const DUCK_DUCK_GO_BASE_URL = "https://html.duckduckgo.com"; diff --git a/apps/backend/src/utils/types.ts b/apps/backend/src/utils/types.ts index fb62192..2cb2940 100644 --- a/apps/backend/src/utils/types.ts +++ b/apps/backend/src/utils/types.ts @@ -6,6 +6,7 @@ export interface RunRequest { } export interface RunLog extends RunRequest { + userId:string, response: string; timestamp?: Date; tokens?: number; diff --git a/apps/backend/src/validators/runSchema.ts b/apps/backend/src/validators/runSchema.ts index 7630fff..964b9b8 100644 --- a/apps/backend/src/validators/runSchema.ts +++ b/apps/backend/src/validators/runSchema.ts @@ -3,6 +3,7 @@ import { z } from "zod"; export const RunRequestSchema = z.object({ prompt: z.string().max(500), tool: z.enum(['web-search', 'calculator']), + userId:z.string().max(16) }); export type RunRequest = z.infer; \ No newline at end of file From ace3f0bbfcfcd10fcb60493cd75cb6d211a7a3c8 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Sat, 7 Jun 2025 15:52:51 +0530 Subject: [PATCH 12/22] Add environment based server config with HTTPS, Helmet, CSP, and rate limiting for production --- apps/backend/src/server.ts | 84 ++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index e62741f..a5ab1e7 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -1,48 +1,90 @@ -import dotenv from 'dotenv'; -import router from './api/routes'; -import Fastify from 'fastify'; +import dotenv from "dotenv"; +import router from "./api/routes"; +import Fastify from "fastify"; +import fs from "fs"; +import path from "path"; +import fastifyHelmet from "@fastify/helmet"; +import fastifyRateLimit from "@fastify/rate-limit"; + dotenv.config(); +const isDev = process.env.NODE_ENV === "development"; -const fastify = Fastify({ +const fastifyOptions:any = { logger: { - level: process.env.LOG_LEVEL || 'info', - transport: process.env.NODE_ENV === 'development' ? { - target: 'pino-pretty' - } : undefined + level: process.env.LOG_LEVEL || "info", + transport: isDev ? { target: "pino-pretty" } : undefined, }, - bodyLimit: 1024 * 1024, + bodyLimit: 1024 * 1024, // 1MB trustProxy: true, keepAliveTimeout: 72000, - connectionTimeout: 10000 -}); + connectionTimeout: 10000, +}; + +if (!isDev) { + try { + fastifyOptions.https = { + key: fs.readFileSync(path.resolve(process.env.SSL_KEY_PATH || " ")), + cert: fs.readFileSync(path.resolve(process.env.SSL_CERT_PATH || " ")), + }; + } catch (err) { + const error = err as Error; + console.error("Failed to load SSL certificates:", error.message); + process.exit(1); + } +} + +const fastify = Fastify(fastifyOptions); + +async function registerPlugins() { + if (!isDev) { + await fastify.register(fastifyHelmet, { + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + objectSrc: ["'none'"], + upgradeInsecureRequests: [], + }, + }, + }); + + await fastify.register(fastifyRateLimit, { + max: parseInt(process.env.RATE_LIMIT_MAX || "100", 10), + timeWindow: process.env.RATE_LIMIT_WINDOW || "1 minute", + }); + } +} async function registerRoutes() { - await fastify.register(router, { prefix: '/api/v1' }); + await fastify.register(router, { prefix: "/api/v1" }); } async function start() { try { + await registerPlugins(); await registerRoutes(); - - const PORT = parseInt(process.env.PORT || '3000', 10); - const HOST = process.env.HOST || '0.0.0.0'; - + + const PORT = parseInt(process.env.PORT || "8082", 10); + const HOST = process.env.HOST || "0.0.0.0"; + await fastify.listen({ port: PORT, host: HOST }); - console.log(`Fastify server running at http://${HOST}:${PORT}`); + console.log( + `Server running at ${isDev ? "http" : "https"}://${HOST}:${PORT}` + ); } catch (err) { fastify.log.error(err); process.exit(1); } } -process.on('SIGINT', async () => { - console.log('Received SIGINT, shutting down gracefully...'); +process.on("SIGINT", async () => { + console.log("Received SIGINT, shutting down gracefully..."); await fastify.close(); process.exit(0); }); -process.on('SIGTERM', async () => { - console.log('Received SIGTERM, shutting down gracefully...'); +process.on("SIGTERM", async () => { + console.log("Received SIGTERM, shutting down gracefully..."); await fastify.close(); process.exit(0); }); From 8750e7f406f1ddf2525d7acbc68f52e18fa9edbd Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Sat, 7 Jun 2025 19:31:19 +0530 Subject: [PATCH 13/22] Implement cors & other securities --- apps/backend/src/server.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts index a5ab1e7..86d7b44 100644 --- a/apps/backend/src/server.ts +++ b/apps/backend/src/server.ts @@ -5,12 +5,13 @@ import fs from "fs"; import path from "path"; import fastifyHelmet from "@fastify/helmet"; import fastifyRateLimit from "@fastify/rate-limit"; +import fastifyCors from "@fastify/cors"; dotenv.config(); const isDev = process.env.NODE_ENV === "development"; -const fastifyOptions:any = { +const fastifyOptions: any = { logger: { level: process.env.LOG_LEVEL || "info", transport: isDev ? { target: "pino-pretty" } : undefined, @@ -37,6 +38,12 @@ if (!isDev) { const fastify = Fastify(fastifyOptions); async function registerPlugins() { + await fastify.register(fastifyCors, { + origin: process.env.CLIENT_ORIGIN || (isDev ? "*" : false), + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"], + credentials: true, + }); if (!isDev) { await fastify.register(fastifyHelmet, { contentSecurityPolicy: { @@ -44,14 +51,23 @@ async function registerPlugins() { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], objectSrc: ["'none'"], - upgradeInsecureRequests: [], + upgradeInsecureRequests: isDev ? [] : ["https:"], }, }, + hsts: { maxAge: 31536000, includeSubDomains: true }, // Strict-Transport-Security in prod + xFrameOptions: { action: "deny" }, // Prevent clickjacking + xContentTypeOptions: true, // Prevent MIME-type sniffing + xXssProtection: true, // Enable XSS filtering }); await fastify.register(fastifyRateLimit, { max: parseInt(process.env.RATE_LIMIT_MAX || "100", 10), timeWindow: process.env.RATE_LIMIT_WINDOW || "1 minute", + errorResponseBuilder: () => ({ + statusCode: 429, + error: 'Too Many Requests', + message: 'Rate limit exceeded. Please try again later.', + }) }); } } From 207edc4f87571cdc81f217387fb7523e97d4bcb6 Mon Sep 17 00:00:00 2001 From: Sayan Ghosh Date: Sun, 8 Jun 2025 01:49:59 +0530 Subject: [PATCH 14/22] Create UI & integrate API successfully --- apps/frontend/package.json | 5 +- apps/frontend/src/App.css | 6 +- apps/frontend/src/App.tsx | 3 +- apps/frontend/src/assets/send.svg | 5 + apps/frontend/src/components/PromptForm.tsx | 335 ++++++++++++++++++ apps/frontend/src/components/Spinner.tsx | 29 ++ apps/frontend/src/index.css | 4 +- apps/frontend/src/pages/index.tsx | 10 +- apps/frontend/src/services/api.ts | 36 +- .../src/utils/commonHelperFunctions.ts | 7 + apps/frontend/src/utils/constants.ts | 1 - apps/frontend/tailwind.config.js | 12 - apps/frontend/vite.config.ts | 10 +- 13 files changed, 438 insertions(+), 25 deletions(-) create mode 100644 apps/frontend/src/assets/send.svg create mode 100644 apps/frontend/src/components/Spinner.tsx create mode 100644 apps/frontend/src/utils/commonHelperFunctions.ts delete mode 100644 apps/frontend/tailwind.config.js diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 1d992d6..5e77fc1 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/vite": "^4.1.8", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -18,6 +19,8 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.1.8", + "uuid": "^11.1.0", "zod": "^3.25.51" }, "devDependencies": { @@ -31,8 +34,6 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", - "postcss": "^8.5.4", - "tailwindcss": "^4.1.8", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5" diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css index b9d355d..32bd75a 100644 --- a/apps/frontend/src/App.css +++ b/apps/frontend/src/App.css @@ -1,4 +1,6 @@ -#root { +@import "tailwindcss"; + +/* #root { max-width: 1280px; margin: 0 auto; padding: 2rem; @@ -39,4 +41,4 @@ .read-the-docs { color: #888; -} +} */ diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index c920881..c93ce97 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,11 +1,12 @@ import './App.css' +import HomePage from './pages' function App() { return ( <> -

Initial Folder & Setup done

+ ) } diff --git a/apps/frontend/src/assets/send.svg b/apps/frontend/src/assets/send.svg new file mode 100644 index 0000000..6650482 --- /dev/null +++ b/apps/frontend/src/assets/send.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/frontend/src/components/PromptForm.tsx b/apps/frontend/src/components/PromptForm.tsx index e69de29..f03f79d 100644 --- a/apps/frontend/src/components/PromptForm.tsx +++ b/apps/frontend/src/components/PromptForm.tsx @@ -0,0 +1,335 @@ +import { useEffect, useRef, useState } from "react"; +import Spinner from "./Spinner"; +import { v4 as uuidv4 } from "uuid"; +import { runPrompt } from "../services/api"; +import { parseSearchResults } from "../utils/commonHelperFunctions"; +import sendIcon from '../assets/send.svg'; + + +export default function PromptForm() { + const [prompt, setPrompt] = useState(""); + const [tool, setTool] = useState("web-search"); + const [loading, setLoading] = useState(false); + const [history, setHistory] = useState([]); + const [error, setError] = useState(null); + const [stopTyping, setStopTyping] = useState(false); + const [timings, setTimings] = useState({ render: 0, query: 0, response: 0 }); + const responseContainerRef = useRef(null); + + interface HistoryEntry { + question: string; + tool: "web-search" | "calculator"; + response: string; + displayedResponse: string; + tokens: number | null; + loading: boolean; + responseTimeStamp: string; + } + + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search); + const paramId = searchParams.get("userId"); + const existingId = sessionStorage.getItem("userId"); + const idToStore = paramId || existingId || uuidv4(); + sessionStorage.setItem("userId", idToStore); + }, []); + + const isTyping: boolean = history.some( + (entry) => + entry.response && entry.displayedResponse.length < entry.response.length + ); + + // Handle typing animation for each response + + useEffect(() => { + const timers: NodeJS.Timeout[] = []; + history.forEach((entry, index) => { + if (stopTyping) return; + if (entry.response && entry.displayedResponse !== entry.response) { + const timer = setInterval(() => { + setHistory((prev) => + prev.map((item, i) => { + if (i === index) { + const nextCharIndex = item.displayedResponse.length + 1; + if (nextCharIndex <= item.response.length) { + return { + ...item, + displayedResponse: item.response.slice(0, nextCharIndex), + }; + } + clearInterval(timer); + } + return item; + }) + ); + }, 30); + timers.push(timer); + } + }); + return () => timers.forEach(clearInterval); + }, [history, stopTyping]); + + useEffect(() => { + if (responseContainerRef.current) { + responseContainerRef.current.scrollTop = + responseContainerRef.current.scrollHeight; + } + }, [history]); + + const handleSubmit = async () => { + if (!prompt) return; + const start = performance.now(); + setLoading(true); + setError(null); + setStopTyping(false); + + const newEntry = { + question: prompt, + tool, + response: null, + displayedResponse: "", + tokens: null, + loading: true, + responseTimeStamp: "", + }; + + setHistory((prev) => [...prev, newEntry]); + + const userId = sessionStorage.getItem("userId") || uuidv4(); + sessionStorage.setItem("userId", userId); + + try { + const queryStart = performance.now(); + const result = await runPrompt({ + prompt, + tool: tool as "web-search" | "calculator", + userId, + }); + const queryEnd = performance.now(); + + const parsedResponse = + tool === "calculator" + ? result.summary.replace(/\\?[".,\n]/g, "").trim() + : parseSearchResults(result.summary); + + const finalResponse = Array.isArray(parsedResponse) + ? `Based on the prompt ${prompt} , here’s what I found:
    ` + + parsedResponse + .map( + (item, i) => + `
  • ${i + 1}. ${ + item.title + }
  • ` + ) + .join("") + + `
` + : parsedResponse; + + const end = performance.now(); + + setTimings({ + render: end - start, + query: queryEnd - queryStart, + response: end - queryEnd, + }); + + setHistory((prev) => + prev.map((entry, index) => + index === prev.length - 1 + ? { + ...entry, + response: finalResponse, + displayedResponse: "", + tokens: result.totalTokenCount, + loading: false, + responseTimeStamp: result.timestamp, + } + : entry + ) + ); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "An error occurred"); + setHistory((prev) => + prev.map((entry, index) => + index === prev.length - 1 ? { ...entry, loading: false } : entry + ) + ); + } finally { + setLoading(false); + setPrompt(""); + } + }; + + return ( +
+

+ Mini Agent Forge +

+ +
+ {history.length > 0 ? ( +
+ {history.map((entry, index) => ( +
+

+ Prompt: {index + 1} +

+

+ + {entry.question}{" "} + + ({entry.tool}) + + + + + {new Date().toLocaleString()} + +

+ + {entry.loading ? ( +
+ + + + + Processing... +
+ ) : entry.response ? ( + <> +

+ Response: +

+
+ + {entry.displayedResponse && !isTyping && ( + +

+ Total Token : {entry.tokens} +

+ + {new Date(entry.responseTimeStamp).toLocaleString()} + +
+ )} + + ) : null} +
+ ))} +
+ ) : ( +

+ No questions submitted yet. Enter a query to see results. +

+ )} +
+ + {/* Error Container */} + {error && ( +
+ Error: {error} +
+ )} + + {/* Input Container (Fixed at Bottom) */} +
+
+