> Full Neon documentation index: https://neon.com/docs/llms.txt

# Building a Full-Stack Portfolio Website with a RAG Powered Chatbot

Develop a modern React portfolio website featuring a chatbot powered by pgvector, Neon Postgres, FastAPI, and OpenAI.

In this guide, you will build a full-stack portfolio website using `React` for the frontend and `FastAPI` for the backend, featuring a Retrieval-Augmented Generation (RAG) chatbot that leverages `pgvector` on `Neon`'s serverless Postgres to store and retrieve embeddings created with `OpenAI`'s embedding model.

This project is perfect for showcasing technical skills through a portfolio that not only demonstrates front-end and back-end capabilities but also integrates AI to answer questions about your experience.

## Prerequisites

Before you start, ensure that you have the following tools and services ready:

- `pip`: This is required for installing and managing Python packages, including [uv](https://docs.astral.sh/uv/) for creating virtual environments. You can check if `pip` is installed by running the following command:
  ```bash
  pip --version
  ```
- Neon serverless Postgres : You will need a Neon account for provisioning and scaling your `PostgreSQL` database. If you don't have an account yet, [sign up here](https://console.neon.tech/signup).
- Node.js: Needed for developing the frontend using React, you can download it following the [official installation guide](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs).
- OpenAI API key: You need access to the OpenAI API for generating embeddings, you can [sign up here](https://platform.openai.com/signup).

## Setting up the Backend

Follow these steps to set up your backend for the full-stack portfolio website:

1. Create the project structure.

   Since this is a full-stack project, your backend and frontend will be in separate directories within a single parent folder. Begin by creating the parent folder and moving into it

   ```bash
   mkdir portfolio_project
   cd portfolio_project
   ```

2. Create a `uv` Python virtual environment.

   If you don't already have uv installed, you can install it with:

   ```bash
   pip install uv
   ```

   Once `uv` is installed, create a new project:

   ```bash
   uv init portfolio_backend
   ```

   This will create a new project directory called `portfolio_backend`. Open this directory in your code editor of your choice.

3. Set up the virtual environment.

   You will now create and activate a virtual environment in which your project's dependencies will be installed.

   Tab: Linux/macOS

   ```bash
   uv venv
   source .venv/bin/activate
   ```

   Tab: Windows

   ```bash
   uv venv
   .venv\Scripts\activate
   ```

   You should see `(portfolio_backend)` in your terminal now, this means that your virtual environment is activated.

4. Install dependencies.

   Next, add all the necessary dependencies for your project:

   ```bash
   uv add fastapi asyncpg uvicorn loguru python-dotenv openai pgvector
   ```

   where each package does the following:

   - `FastAPI` : A Web / API framework
   - `AsyncPG` : An asynchronous PostgreSQL client
   - `Uvicorn` : An ASGI server for our app
   - `Loguru` : A logging library
   - `Python-dotenv` : To load environment variables from a .env file
   - `openai` : The OpenAI API client for generating embeddings and chatbot responses
   - `pgvector` : A Python client for working with pgvector in PostgreSQL

5. Create the project structure.

   Create the following directory structure to organize your project files:

   ```md
   portfolio_backend
   ├── src/
   │ ├── database/
   │ │ └── postgres.py
   │ ├── models/
   │ │ └── product_models.py
   │ ├── routes/
   │ │ └── product_routes.py
   │ └── main.py
   ├── .env
   ├── .python-version
   ├── README.md
   ├── pyproject.toml
   └── uv.lock
   ```

## Setting up your Database

In this section, you will set up the `pgvector` extension using Neon's console, add the database's schema, and create the database connection pool and lifecycle management logic in FastAPI.

First, add pgvector to Postgres:

```sql
CREATE EXTENSION IF NOT EXISTS vector;
```

Next, add the schema to your database:

```sql
CREATE TABLE portfolio_embeddings (
    id SERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    embedding VECTOR(1536) NOT NULL
);
```

This table will store the embeddings generated by OpenAI for the chatbot responses. The `content` column will store the text for which the embedding was generated, and the `embedding` column will store the 1536-dimensional vector. An embedding is a representation of text in a high-dimensional space that captures the meaning of the text, allowing you to compare and search for similar text.

With your schema in place, you're now ready to connect to your database in the FastAPI application. To do this you must create a `.env` file in the root of the project to hold environment-specific variables, such as the connection string to your Neon PostgreSQL database, and API keys.

```bash
DATABASE_URL=postgres://user:password@your-neon-hostname.neon.tech/neondb?sslmode=require&channel_binding=require
OPENAI_API_KEY=your-api-key
OPENAI_ORG_ID=your-org-id
OPENAI_PROJECT_ID=your-project-id
```

Make sure to replace the placeholders (user, password, your-neon-hostname, etc.) with your actual Neon database credentials, which are available in the console, and to fetch the OpenAI key from the OpenAI console.

In your project, the `database.py` file manages the connection to `PostgreSQL` using `asyncpg` and its connection pool, which is a mechanism for managing and reusing database connections efficiently. With this, you can use asynchronous queries, allowing the application to handle multiple requests concurrently.

```python
import os
import asyncpg
from loguru import logger
from typing import Optional
from pgvector.asyncpg import register_vector

conn_pool: Optional[asyncpg.Pool] = None


async def init_postgres() -> None:
    """
    Initialize the PostgreSQL connection pool
    """
    global conn_pool
    try:
        logger.info("Initializing PostgreSQL connection pool...")

        async def initalize_vector(conn):
            await register_vector(conn)

        conn_pool = await asyncpg.create_pool(
            dsn=os.getenv("DATABASE_URL"), init=initalize_vector
        )
        logger.info("PostgreSQL connection pool created successfully.")

    except Exception as e:
        logger.error(f"Error initializing PostgreSQL connection pool: {e}")
        raise


async def get_postgres() -> asyncpg.Pool:
    """
    Get a reference to the PostgreSQL connection pool.

    Returns
    -------
    asyncpg.Pool
        The connection pool object to the PostgreSQL database.
    """
    global conn_pool
    if conn_pool is None:
        logger.error("Connection pool is not initialized.")
        raise ConnectionError("PostgreSQL connection pool is not initialized.")
    try:
        return conn_pool
    except Exception as e:
        logger.error(f"Failed to return PostgreSQL connection pool: {e}")
        raise


async def close_postgres() -> None:
    """
    Close the PostgreSQL connection pool.
    """
    global conn_pool
    if conn_pool is not None:
        try:
            logger.info("Closing PostgreSQL connection pool...")
            await conn_pool.close()
            logger.info("PostgreSQL connection pool closed successfully.")
        except Exception as e:
            logger.error(f"Error closing PostgreSQL connection pool: {e}")
            raise
    else:
        logger.warning("PostgreSQL connection pool was not initialized.")

```

`init_postgres` is responsible for opening the connection pool to the `PostgreSQL` database and `close_postgres` is responsible for gracefully closing all connections in the pool when the `FastAPI` app shuts down to properly manage the lifecycle of the database.

Throughout your API you will also need access to the pool to get connection instances and run queries. `get_postgres` returns the active connection pool. If the pool is not initialized, an error is raised.

## Defining the Pydantic Models

Now, you will create the models that represent the data structures used in your API. These models will be used to validate the request and response data in your API endpoints. The API will use these models to serialize and deserialize data between the client and the server. For your API, you will need to complete a chat request and add embeddings to the database.

```python
from pydantic import BaseModel
from typing import Optional


class PortfolioEntryCreate(BaseModel):
    content: str


class PortfolioEntryResponse(BaseModel):
    id: int
    content: str
    embedding: Optional[list[float]]


class QueryRequest(BaseModel):
    query: str


class QueryResponse(BaseModel):
    content: str
    similarity: float
```

Each of the models represent the following:

- `PortfolioEntryCreate`: Represents the input for creating a new embedding
- `PortfolioEntryResponse`: Represents the output of a created embedding
- `QueryRequest`: Represents a question to ask the chatbot
- `QueryResponse`: Represents the response from the chatbot

## Creating the API Endpoints

In this section, you will create the API routes for adding new portfolio entries and chatting with the chatbot. The `add-portfolio-entry` route will add a new portfolio entry to the database and store its embedding. The `chat` route will use the stored embeddings to generate a response to a user query.

A typical RAG chatbot workflow involves first creating an embedding for the user query, then finding the most similar embeddings in the database, and finally using the context of these embeddings to generate a response. Here, embeddings are generated using OpenAI's text-embedding-3-small model, and the similarity between embeddings is calculated using the `<=>` operator in PostgreSQL, which represents the cosine similarity between two vectors. The chatbot response is then generated using OpenAI's GPT-4o-mini model.

```python
from fastapi import APIRouter, HTTPException, Depends
from models.models import PortfolioEntryCreate, PortfolioEntryResponse, QueryRequest
from database.postgres import get_postgres
import asyncpg
from typing import List
import os
from openai import OpenAI
import numpy as np

router = APIRouter()

client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    organization=os.getenv("OPENAI_ORG_ID"),
    project=os.getenv("OPENAI_PROJECT_ID"),
)


async def generate_embedding(content: str) -> List[float]:
    """
    Generate an embedding for the given content using OpenAI API.
    """
    try:
        content = content.replace("\n", " ")
        response = client.embeddings.create(
            input=[content],
            model="text-embedding-3-small",
        )
        return response.data[0].embedding
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Error generating embedding: {e}")


@router.post("/add-entry/", response_model=PortfolioEntryResponse)
async def add_portfolio_entry(
    entry: PortfolioEntryCreate, pool: asyncpg.Pool = Depends(get_postgres)
):
    """
    Add a new portfolio entry and store its embedding in PostgreSQL.
    """
    try:
        embedding = await generate_embedding(entry.content)

        embedding_np = np.array(embedding)

        async with pool.acquire() as conn:
            row = await conn.fetchrow(
                """
                INSERT INTO portfolio_embeddings (content, embedding)
                VALUES ($1, $2)
                RETURNING id, content, embedding
                """,
                entry.content,
                embedding_np,
            )
            if row:
                return PortfolioEntryResponse(
                    id=row["id"], content=row["content"], embedding=row["embedding"]
                )
            else:
                raise HTTPException(
                    status_code=500, detail="Failed to insert entry into the database."
                )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to add entry: {e}")


@router.post("/chat/")
async def chat(query: QueryRequest, pool: asyncpg.Pool = Depends(get_postgres)):
    """
    Chat with the portfolio chatbot by retrieving relevant information from stored embeddings
    and using it as context to generate a response.
    """
    try:
        query_embedding = await generate_embedding(query.query)

        query_embedding_np = np.array(query_embedding)

        async with pool.acquire() as conn:
            rows = await conn.fetch(
                """
                SELECT content, embedding <=> $1 AS similarity
                FROM portfolio_embeddings
                ORDER BY similarity
                LIMIT 5
                """,
                query_embedding_np,
            )

            context = "\n".join([row["content"] for row in rows])

        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {
                    "role": "system",
                    "content": (
                        "You are YOUR NAME, and you are answering questions as yourself, using 'I' and 'my' in your responses. "
                        "You should respond to questions about your portfolio, skills, experience, and education. For example, you should answer questions about specific technologies you've worked with, such as Java, React, or other tools. "
                        "If you have relevant experience with a technology, describe it concisely. For example, if asked about Java, describe your experience using it. "
                        "You should also answer questions about your education, including your experience at school, and your work in relevant industries. "
                        "However, if a question is completely unrelated to your professional experience, such as questions about recipes, trivia, or non-technical personal matters, respond with: 'That question isn't relevant to my experience or skills.' "
                        "Focus on answering technical and career-related questions, but only reject questions that are clearly off-topic."
                        "If they ask you about a technology you havent used, you can say: 'I haven't worked with that technology yet, but I'm always eager to learn new things.'"
                        "Answer any personal questions that are related to technology, like 'What are our favorite languages?' or 'What technology/language/anything tech are you most excited about?'"
                    ),
                },
                {
                    "role": "user",
                    "content": f"Context: {context}\n\nQuestion: {query.query}",
                },
            ],
            max_tokens=200,
            stream=False,
        )

        return {"response": response.choices[0].message.content}

    except Exception as e:
        raise HTTPException(
            status_code=500, detail=f"Failed to process chat request: {e}"
        )

```

In the `chat` route, the chatbot is sent the text obtained from RAG and the user questions, but is also sent a system message that sets the context for the chatbot. You can customize this message to provide additional context or instructions to the chatbot, and to guide the chatbot's responses to your liking.

## Running the Application

After setting up the database, models, and API routes, the next step is to run the `FastAPI` application.

The `main.py` file defines the `FastAPI` application, manages the database lifecycle, and includes the routes you created above.

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from database.postgres import init_postgres, close_postgres
from routes.routes import router
import dotenv
import uvicorn


@asynccontextmanager
async def lifespan(app: FastAPI):
    dotenv.load_dotenv()
    await init_postgres()
    yield
    await close_postgres()


app: FastAPI = FastAPI(lifespan=lifespan, title="FastAPI Portfolio RAG ChatBot API")
app.include_router(router)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

```

Since you will be connecting your application from the frontend, you will need to allow CORS (Cross Origin Resource Sharing). The `CORSMiddleware` is added such that the API can accept requests from any origin, including your React app.

To run the application, use uvicorn CLI with the following command:

```bash
uvicorn main:app --host 0.0.0.0 --port 8000
```

## Adding Embeddings to the Database

You can now add embeddings to the database by sending a POST request to the `/add-entry` endpoint with the content you want to store. To do this, you can use a tool like `curl`,`Postman`, `httpie`, or any other HTTP client. For example, using `httpie`:

```bash
http POST http://localhost:8000/add-entry/ content="YOUR CONTENT"
```

For the best results, you should add a variety of portfolio entries that cover different aspects of your experience and skills. This will help the chatbot generate more accurate responses to user queries.

## Testing the Chatbot

Using `httpie`, you can test the chatbot by sending a POST request to the `/chat` endpoint with a user query. For example:

```bash
http POST http://localhost:8000/chat/ query="What is your favorite programming language?"
```

The chatbot will respond with a relevant answer based on the embeddings stored in the database, something like:

```json
{
  "response": "My favorite programming language right now is Rust, with Python coming in a close second. I love the performance and safety features of Rust, and the simplicity and readability of Python."
}
```

## Setting up the Frontend

Now that the backend is set up and running, it's time to set up the frontend using React. The frontend will be responsible for interacting with the FastAPI backend and displaying your portfolio, as well as allowing users to ask questions to the RAG-powered chatbot.

1. Clone the frontend repository.

   First, go back to the parent directory (portfolio_project) you created at the beginning of this guide and clone the frontend repository:

   ```bash
   cd ..
   git clone https://github.com/sam-harri/portfolio_frontend
   ```

   Then, open up that new directory in your code editor.

2. Install the dependencies.

   Once you have cloned the frontend repository, install the necessary dependencies using `npm`:

   ```bash
   npm install
   ```

   This will install all the packages specified in the `package.json` file, which are required for the React app to run.

3. Update the frontend content.

   Now, you will update the content of your portfolio, such as your bio, projects, and skills, and experience to match your personal details. Each of the section in the portfolio is a separate component that you can modify to include your own information.
   These include:

   - Landing: The landing page of the portfolio, which includes your name, bio, and a profile picture.
   - Experience: A section that lists your work experience, including the company name, logo, your position, and a brief description of your role.
   - Skills: A section that lists your technical skills, such as programming languages, frameworks, and tools you are proficient in. Find the logos of your technologies at [Devicon](https://devicon.dev/)
   - Projects: A section that lists your projects, including the project name, description, and a link to the project's GitHub repository or live demo.

   The chatbot component is responsible for sending user queries to the backend and displaying the chatbot responses, and can be found in the `Chatbot.tsx` file.

## Running the Frontend

Once you've updated the content, you can start the frontend development server to preview your portfolio website.

To start the development server, run the following command:

```bash
npm run dev
```

This will start the React development server at [http://localhost:3000](http://localhost:3000). Open your browser and navigate to that address to view your portfolio website.

Now, with the React app running, take a look at how the website appears. Ensure that the design, content, and overall presentation are what you expect. You can interact with the chatbot (which will not yet be functional until the backend is also running) to check the layout and form submission.

To test the chatbot functionality, you will need to have the backend running as well. To do this, open another console window, navigate to the `portfolio_backend` directory, and run the FastAPI application using the `uvicorn` command as shown earlier.
Once the API is running, you can interact with the chatbot on the frontend to test the chatbot functionality, try out some prompts, tweak the responses, and see how the chatbot performs.

## Optional: Running the Full-Stack Project Using Docker and Docker Compose

In this optional section, you will containerize both the frontend and backend of your full-stack portfolio website using Docker. By using Docker, you can ensure that your application runs consistently across different environments without worrying about dependencies and setups. Docker Compose will allow you to orchestrate running both services (frontend and backend) with a single command.

The only prerequisite for this section is having Docker installed on your machine. If you don't have it installed, you can follow the [Docker installation guide](https://docs.docker.com/engine/install/).

1. Dockerize the backend.

   To Dockerize the backend, you will create a `Dockerfile` in the `portfolio_backend` directory. This file will define the steps to build the Docker image for your FastAPI application.

   ```Dockerfile
   FROM python:3.10-slim

   ENV PYTHONDONTWRITEBYTECODE=1
   ENV PYTHONUNBUFFERED=1

   WORKDIR /app

   COPY . /app

   RUN pip install uv

   RUN uv pip install -r pyproject.toml --system

   WORKDIR /app/src

   EXPOSE 8000

   CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
   ```

   This Dockerfile:

   - Copies your FastAPI app into the container
   - Installs all necessary Python dependencies
   - Exposes port 8000, which is where FastAPI will run
   - Runs the FastAPI app using uvicorn

2. Dockerize the frontend.

   Dockerizing the frontend is similar to the backend. Create a `Dockerfile` in the root of the `portfolio_frontend` directory:

   ```Dockerfile
   FROM node:18-alpine AS base

   WORKDIR /app

   COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./

   RUN \
   if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
   elif [ -f package-lock.json ]; then npm ci; \
   elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
   else echo "Lockfile not found." && exit 1; \
   fi

   COPY . .

   RUN npm run build

   RUN npm install -g serve

   EXPOSE 3000

   CMD ["serve", "-s", "dist"]
   ```

   This Dockerfile:

   - Uses Node.js to install frontend dependencies
   - Builds the React application
   - Serves the static build using the serve package
   - Exposes port 3000 where the React app will be available

3. Set up Docker Compose.

   Docker Compose simplifies the process of running multiple containers together. You can define both the frontend and backend in a single configuration and run them together with a single command.

   Below is the `docker-compose.yml` file, placed at the root of the project, which sets up both the services:

   ```yaml
   services:
   api:
     build:
     context: portfolio_backend/
     dockerfile: Dockerfile
     ports:
       - '8000:8000'
     env_file:
       - portfolio_backend/.env
   nextjs-app:
     build:
       context: portfolio_frontend/
       dockerfile: Dockerfile
     ports:
       - '3000:3000'
     environment:
       - NODE_ENV=production
     depends_on:
       - api
   ```

4. Run the application with Docker Compose.

   Once your Dockerfiles and Docker Compose file are ready, you can bring up the entire stack using:

   ```bash
   docker-compose up --build
   ```

   This command will:

   - Build the Docker images for both the backend and frontend
   - Start both the FastAPI backend (on port 8000) and the React frontend (on port 3000)
   - Automatically manage the service dependencies (the frontend will wait until the backend is up before starting)

   Using Docker and Docker Compose to run your full-stack portfolio website simplifies the process of managing dependencies and ensures consistency across different environments. You can now run your entire application, both frontend and backend, in isolated containers with a single command. This setup is also beneficial if you plan to deploy your application to production in a cloud environment or if you want to share the project with others who can run it without manual installation steps.

## Conclusion

You have successfully built and deployed a full-stack portfolio website powered by React, FastAPI, pgvector, Neon Postgres, and OpenAI. By leveraging OpenAI embeddings and the RAG-powered chatbot, you added an AI-driven layer to your portfolio that can dynamically answer questions about your projects, skills, and experience.

The next steps in the project could include deploying the application to a cloud platform like AWS, Azure, or Google Cloud, adding more features to the chatbot, or customizing the frontend design to match your personal style. You can also extend the chatbot's capabilities by fine-tuning it on more data or using a different model for generating responses.
