Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): structure base backend code #11

Merged
merged 7 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ verify_ssl = true
name = "pypi"

[packages]
fastapi = "==0.103.2"
uvicorn = "==0.23.2"
psycopg2-binary = "==2.9.10"
sqlalchemy = "==1.4.54"

[dev-packages]
pre-commit = "*"
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# chatbot
# EzHr-Chatbot LLM-based Assistant

LLM-powerd Chat Platform for teams. Answer related human resourcing questions.
This is the LLM-based assistant for the EzHr-Chatbot project. It makes use of RAG and LlamaIndex to provide a conversational interface for the users.

Hahaha.
## Authors

- Meow

## Components

### API Server

This is the API server for the EzHr-Chatbot LLM-based assistant. It is built using FastAPI and LlamaIndex for LLM framework coding.

For developing and testing purposes, we can use the Docker compose to run the API server.

```bash
# Move to the docker_compose directory
cd deployment/docker_compose

# Create the environment file
cp .env.example .env

# Build the docker image
docker-compose build

# Run the docker container
docker-compose up -d
```
Empty file added backend/.dockerignore
Empty file.
Empty file added backend/.gitignore
Empty file.
21 changes: 21 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Use the official Python base image
FROM python:3.11.10-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy the requirements file to the working directory
COPY requirements.txt .

# Install the Python dependencies
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code to the working directory
COPY . .

# Expose the port on which the application will run
EXPOSE 5000

# Run the application
CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000" ]
103 changes: 103 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# EzHr-Chatbot API Server

This API server powers the EzHr-Chatbot, a Large Language Model (LLM)-based assistant, built with FastAPI and LlamaIndex.

## Prerequisites

A PostgreSQL database is required to store data. For testing purposes, you can set up a PostgreSQL instance using Docker:

```bash
docker run --name postgres -e POSTGRES_PASSWORD=123 -e POSTGRES_USER=root -e POSTGRES_DB=ezhr_chatbot -p 5432:5432 -d postgres:15.2-alpine

# Create the required table
docker exec -it postgres psql -U root -d ezhr_chatbot -c "CREATE TABLE embedding_model (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, provider VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL);"
```

### Environment Variables

Set the following environment variables for database connectivity:

```bash
export POSTGRES_USER=root
export POSTGRES_PASSWORD=123
export POSTGRES_DB=ezhr_chatbot
export POSTGRES_HOST=localhost
export POSTGRES_PORT=5432
```

### Installing Dependencies and Running the Server

Ensure Python 3.10 or later is installed. Install dependencies and start the server as follows:

```bash
# Install dependencies
pip install -r requirements.txt

# Run the server
uvicorn main:app --reload --port 5000
```

## Project Structure

The project follows a structured flow:

1. **Define Endpoints**: `routers/v1/`
2. **Service Logic**: `services/`
3. **Database Operations**: `repositories/`
4. **Model Definitions**: `models/`

### Workflow Breakdown

- **Endpoints**: Define API routes in `routers/v1/`. For example, an endpoint for embedding models might be defined in `routers/v1/embedding_model.py`.

```python
router = APIRouter(prefix="/embedding_model", tags=["embedding_model"])

@router.get("/", response_model=APIResponse)
async def get_embedding_models(db_session: Session = Depends(get_session)):
pass
```

- **Service Layer**: Extract parameters in the controller and delegate logic to the services, located in the `services/` directory. Each method in the service class corresponds to a controller endpoint. Example:

```python
class EmbeddingModelService:
def __init__(self, db_session: Session):
self.db_session = db_session

def get_embedding_models(self) -> Tuple[List[EmbeddingModel], APIError | None]:
return EmbeddingModelRepository(self.db_session).get_embedding_models()
```

- **Repository Layer**: Perform database operations within repositories, found in the `repositories/` directory. Here’s an example repository method:

```python
class EmbeddingModelRepository:
def __init__(self, db_session: Session):
self.db_session = db_session

def get_embedding_models(self) -> Tuple[List[EmbeddingModel], APIError | None]:
try:
embedding_models = self.db_session.query(EmbeddingModel).all()
return embedding_models, None
except Exception as e:
logger.error(f"Error getting embedding models: {e}")
return [], APIError(err_code=20001)
```

- **Model Definitions**: Define database entities in the `models/` directory. Example of a model definition:

```python
class EmbeddingModel(Base):
__tablename__ = "embedding_model"

id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String, index=True)
description = Column(String)
provider = Column(String)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now)
deleted_at = Column(DateTime, default=None, nullable=True)
```

This structured approach maintains a clean separation of concerns, making the API server more maintainable and scalable.
Empty file added backend/databases/__init__.py
Empty file.
Empty file added backend/databases/minio.py
Empty file.
71 changes: 71 additions & 0 deletions backend/databases/postgres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session

from utils.logger import LoggerFactory
from settings import Constants, Secrets

logger = LoggerFactory().get_logger(__name__)


class PostgresConnector:
lelouvincx marked this conversation as resolved.
Show resolved Hide resolved
"""
Pattern: Singleton
Purpose: Create a single instance of the database connection
"""

_instance = None

def __init__(self):
if PostgresConnector._instance is None:
PostgresConnector._instance = self.__create_engine()

@classmethod
def get_instance(cls):
"""
Get the instance of the database connection
"""
if cls._instance is None:
cls._instance = cls().__create_engine()
return cls._instance

@classmethod
def __create_engine(cls):
"""
Create the database connection if there is no any existing connection
"""
try:
uri = Constants.POSTGRES_CONNECTOR_URI.format(
user=Secrets.POSTGRES_USER,
password=Secrets.POSTGRES_PASSWORD,
host=Secrets.POSTGRES_HOST,
port=Secrets.POSTGRES_PORT,
name=Secrets.POSTGRES_NAME,
)
return create_engine(
uri,
pool_size=Constants.POSTGRES_POOL_SIZE,
max_overflow=Constants.POSTGRES_MAX_OVERFLOW,
pool_timeout=Constants.POSTGRES_POOL_TIMEOUT,
pool_recycle=Constants.POSTGRES_POOL_RECYCLE,
)
except Exception as e:
logger.error(f"Error initializing database: {e}")
raise


# Get the engine from the PostgresConnector singleton
engine = PostgresConnector.get_instance()

# Create a session maker
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_session() -> Session:
"""
Generate a database session for the applications
"""
session = SessionLocal()
try:
yield session
finally:
session.close()
Empty file added backend/databases/redis.py
Empty file.
33 changes: 33 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from fastapi import FastAPI

from utils.logger import LoggerFactory
from routers import base
from routers.v1 import embedding_model
from settings import Constants

logger = LoggerFactory().get_logger(__name__)


def create_app() -> FastAPI:
"""
Construct and configure the FastAPI application
"""
# Initialize FastAPI application
app = FastAPI(
title=Constants.FASTAPI_NAME,
version=Constants.FASTAPI_VERSION,
description=Constants.FASTAPI_DESCRIPTION,
)

logger.info(
f"API {Constants.FASTAPI_NAME} {Constants.FASTAPI_VERSION} started successfully"
)

# Include application routers
app.include_router(router=base.router)
app.include_router(router=embedding_model.router, prefix=Constants.FASTAPI_PREFIX)

return app


app = create_app()
Empty file added backend/middlewares/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions backend/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
14 changes: 14 additions & 0 deletions backend/models/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import BaseModel
from typing import Optional, Any

from utils.error_handler import ErrorCodesMappingNumber


class APIResponse(BaseModel):
message: str = "Success"
headers: Optional[Any] = None
data: Optional[Any] = None


class APIError(BaseModel):
kind: Any
37 changes: 37 additions & 0 deletions backend/models/embedding_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from pydantic import BaseModel, Field
from sqlalchemy import Column, Integer, String, DateTime
from datetime import datetime
from typing import Optional

from models import Base


class EmbeddingModel(Base):
__tablename__ = "embedding_model"

id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String, index=True)
description = Column(String)
provider = Column(String)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now)
deleted_at = Column(DateTime, default=None, nullable=True)


class EmbeddingModelRequest(BaseModel):
name: str
description: str
provider: str


class EmbeddingModelResponse(BaseModel):
id: int
name: str
description: str
provider: str
created_at: datetime
updated_at: datetime
deleted_at: Optional[datetime]

class Config:
from_attributes = True
Loading