Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b7c10d1
initial folder setup and architecture scaffolding completed
Sayan97Ghosh Jun 5, 2025
47e7d68
initial folder setup and architecture scaffolding completed
Sayan97Ghosh Jun 5, 2025
b0a1529
Initialize PostgreSQL connection and ensure run_logs table setup
Sayan97Ghosh Jun 5, 2025
f2eca12
Implement redis client and limit recent runlogs sorted set to 10 entries
Sayan97Ghosh Jun 5, 2025
b8b71fa
Integrate Gemini AI for content generation
Sayan97Ghosh Jun 5, 2025
2c11779
Create websearch & calculator services
Sayan97Ghosh Jun 5, 2025
05bfbb2
Create reqired route for get response of user query
Sayan97Ghosh Jun 5, 2025
79d61af
Update base prompt of LLM for get required output
Sayan97Ghosh Jun 5, 2025
58972f2
Implement fastify to create API
Sayan97Ghosh Jun 6, 2025
55960e4
Integrate DB & Redis for cache logs & faster retrivals
Sayan97Ghosh Jun 6, 2025
fd72d8f
Add user ID support for Redis caching and Postgres logging
Sayan97Ghosh Jun 7, 2025
ace3f0b
Add environment based server config with HTTPS, Helmet, CSP, and rate…
Sayan97Ghosh Jun 7, 2025
8750e7f
Implement cors & other securities
Sayan97Ghosh Jun 7, 2025
207edc4
Create UI & integrate API successfully
Sayan97Ghosh Jun 7, 2025
4065c0c
Merge branch 'main' of ssh://github.com/Sayan97Ghosh/mini-agent-forge…
Sayan97Ghosh Jun 8, 2025
ac114b8
Refactor frontend & backend code
Sayan97Ghosh Jun 8, 2025
2acfe36
Create containers in docker for test frontend and backend locally
Sayan97Ghosh Jun 8, 2025
71f7472
Add GitHub Actions workflow for test and Docker build
Sayan97Ghosh Jun 8, 2025
5c6a713
Merge branch 'feature/refactor' into develop
Sayan97Ghosh Jun 8, 2025
bf228c3
Fix indentation under the jobs: key in git action setup file
Sayan97Ghosh Jun 8, 2025
0f9ecc3
Adding docker cli suuport because Github by default not intall docker…
Sayan97Ghosh Jun 8, 2025
f1ae296
Modified action.yml code for action deployment
Sayan97Ghosh Jun 8, 2025
d7b62c1
Add unit tests for Prompt Input component with Vitest and React Testi…
Sayan97Ghosh Jun 8, 2025
d0438e7
Update README.md
Sayan97Ghosh Jun 8, 2025
77e3bdf
Merge pull request #1 from Sayan97Ghosh/Sayan97Ghosh-patch-1
Sayan97Ghosh Jun 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/test-and-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Test and Docker Build

on:
push:
branches: [develop]
pull_request:
branches: [develop]

jobs:
test-and-build:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
ports: ['5432:5432']
env:
POSTGRES_DB: mini_agent
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 2536
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5

redis:
image: redis:alpine
ports: ['6379:6379']
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install backend dependencies
working-directory: ./apps/backend
run: npm install

- name: Set backend env variables
run: |
echo "DATABASE_URL=${{ secrets.BACKEND_DATABASE_URL }}" >> .env
echo "REDIS_URL=${{ secrets.BACKEND_REDIS_URL }}" >> .env
echo "GEMINI_API_KEY=${{ secrets.BACKEND_GEMINI_API_KEY }}" >> .env
echo "GEMINI_MODEL=${{ secrets.BACKEND_GEMINI_MODEL }}" >> .env
echo "NODE_ENV=${{ secrets.BACKEND_NODE_ENV }}" >> .env
echo "LOG_LEVEL=${{ secrets.BACKEND_LOG_LEVEL }}" >> .env
echo "RATE_LIMIT_MAX=${{ secrets.BACKEND_RATE_LIMIT_MAX }}" >> .env
echo "RATE_LIMIT_WINDOW=${{ secrets.BACKEND_RATE_LIMIT_WINDOW }}" >> .env
echo "PORT=${{ secrets.BACKEND_PORT }}" >> .env
working-directory: ./apps/backend

- name: Install frontend dependencies
working-directory: ./apps/frontend
run: npm install

- name: Set frontend env variables
run: |
echo "VITE_MODE=${{ secrets.FRONTEND_VITE_MODE }}" >> .env
echo "VITE_PORT=${{ secrets.FRONTEND_VITE_PORT }}" >> .env
working-directory: ./apps/frontend

- name: Run backend tests
working-directory: ./apps/backend
run: npm run test || echo "No tests defined"

- name: Install docker-compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

- name: Build Docker images
working-directory: ./apps
run: docker-compose build
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,33 @@ POST /api/v1/query
"tool": "calculator"
}
```
**🐳 Docker Setup**

Step 1: Go to the root directory
```bash
cd mini-agent-forge/
```
Step 2: Run Docker Compose
``` bash
docker-compose up --build
```
This will spin up:

Backend

Frontend

PostgreSQL

Redis

Once started:

Backend: http://localhost:8082

Frontend: http://localhost:4173



🧰 Local Development Setup
✅ Prerequisites
Expand Down
26 changes: 26 additions & 0 deletions apps/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -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?
6 changes: 6 additions & 0 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 8082
CMD ["npm", "run", "start"]
39 changes: 39 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon --exec tsx src/server.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"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",
"cheerio": "^1.0.0",
"dotenv": "^16.5.0",
"fastify": "^4.24.3",
"mathjs": "^14.5.2",
"node-fetch": "^3.3.2",
"nodemon": "^3.1.10",
"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",
"pino-pretty": "^13.0.0",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
}
}
199 changes: 199 additions & 0 deletions apps/backend/src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
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,

const requestBodySchema = {
type: "object",
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: 100 },
},
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";
userId: string;
}

export default async function routes(fastify: FastifyInstance) {
// routes for get API server health
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(),
};
}
);
// 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, userId } = request.body;
const startTime = Date.now();

try {
const cachedUserQueryAndAiResponse = await getCachedRun(userId, 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(),
};
}

fastify.log.info({
method: request.method,
url: request.url,
tool,
responseTime: Date.now() - startTime,
tokenCount: responseData.totalTokenCount,
});

const log: RunLog = {
userId,
prompt,
tool,
response: JSON.stringify(responseData),
timestamp: new Date(responseData.timestamp),
tokens: responseData.totalTokenCount,
};

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,
prompt: prompt.substring(0, 100),
});

return reply.code(500).send({
error: error.message,
code: "INTERNAL_ERROR",
timestamp: new Date().toISOString(),
});
}
}
);
}
Loading