Skip to content
Open
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
7 changes: 3 additions & 4 deletions .cursor/rules/general-rule.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ alwaysApply: true
---
## Rules to Follow

- You must always commit your changes whenever you update code.
- You must always try and write code that is well documented. (self or commented is fine)
- You must only work on a single feature at a time.
- You must explain your decisions thouroughly to the user.
You always prefer to use branch development. Before writing any code - you create a feature branch to hold those changes.

After you are done - provide instructions in a "MERGE.md" file that explains how to merge the changes back to main with both a GitHub PR route and a GitHub CLI route.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,11 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.vercel

# Node.js
node_modules
.next

# PDF filess
*.pdf

17 changes: 17 additions & 0 deletions .vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Ignore development files
*.log
*.tmp
.DS_Store
.vscode/
.idea/

# Ignore large files that aren't needed for deployment
*.ipynb
*.md
.git/
.gitignore

# Keep only essential files for deployment
!api/
!frontend/
!vercel.json
132 changes: 132 additions & 0 deletions MERGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# PDF RAG System - Merge Instructions

This document provides instructions for merging the PDF RAG functionality back to the main branch and deploying the application.

## Changes Made

### Backend (API)
- Added PDF upload endpoint (`/api/upload-pdf`)
- Added PDF text extraction using PyPDF2
- Implemented simple RAG system with keyword-based search
- Updated chat endpoint to use RAG context when PDF is uploaded
- Added PyPDF2 dependency to requirements.txt

### Frontend
- Added PDF upload UI in settings panel
- Added file upload handling with drag-and-drop interface
- Added upload status indicators
- Updated welcome message to mention PDF functionality
- Added new icons for file operations

### Dependencies Added
- PyPDF2==3.0.1 (PDF text extraction)

## Deployment Instructions

### Option 1: GitHub Pull Request (Recommended)

1. **Push the feature branch to GitHub:**
```bash
git push origin feature/pdf-rag-system
```

2. **Create a Pull Request:**
- Go to your GitHub repository
- Click "Compare & pull request" for the `feature/pdf-rag-system` branch
- Add a descriptive title: "Add PDF Upload and RAG Chat Functionality"
- Add description of the changes
- Assign reviewers if needed
- Click "Create pull request"

3. **Merge the Pull Request:**
- Review the changes
- Click "Merge pull request"
- Delete the feature branch after merging

4. **Deploy to Vercel:**
- Vercel will automatically detect the changes and redeploy
- Monitor the deployment in your Vercel dashboard
- **Set Environment Variable**: Add `OPENAI_API_KEY` for the backend

### Option 2: GitHub CLI

1. **Push the feature branch:**
```bash
git push origin feature/pdf-rag-system
```

2. **Create and merge PR using GitHub CLI:**
```bash
# Create pull request
gh pr create --title "Add PDF Upload and RAG Chat Functionality" --body "Implements PDF upload and RAG-based chat using simple keyword matching"

# Merge the pull request
gh pr merge --squash
```

3. **Switch back to main and pull changes:**
```bash
git checkout main
git pull origin main
```

4. **Deploy to Vercel:**
- Vercel will automatically redeploy from the main branch

## Testing the Deployment

After deployment, test the following features:

1. **Basic Chat:**
- Set OpenAI API key in settings
- Send a message without uploading PDF
- Verify normal chat functionality

2. **PDF Upload:**
- Upload a PDF file in settings
- Verify successful upload message
- Check that file appears in the UI

3. **RAG Chat:**
- Ask questions about the uploaded PDF content
- Verify responses are based on PDF content
- Test with questions not in the PDF (should get "I don't have enough information" response)

## Environment Variables

### For Vercel (Both Frontend and Backend):
- `OPENAI_API_KEY`: Your OpenAI API key (for production use)

## File Structure

```
The-AI-Engineer-Challenge/
├── api/
│ ├── app.py (updated with RAG functionality)
│ └── requirements.txt (added PyPDF2)
├── frontend/
│ └── app/page.tsx (updated with PDF upload UI)
└── vercel.json (unchanged - original configuration)
```

## Local Development

For local testing, run:
```bash
# Backend
cd api
pip install -r requirements.txt
python app.py

# Frontend (in another terminal)
cd frontend
npm install
npm run dev
```

## Notes

- Uses simple keyword-based RAG instead of vector embeddings for Vercel compatibility
- PDF processing happens server-side for security
- RAG system uses in-memory storage (resets on server restart)
- **Frontend and Backend deploy together on Vercel** - this is the original working configuration
Empty file added aimakerspace/__init__.py
Empty file.
Empty file.
66 changes: 66 additions & 0 deletions aimakerspace/openai_utils/chatmodel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import os
from typing import Any, AsyncIterator, Iterable, List, MutableMapping

from dotenv import load_dotenv
from openai import AsyncOpenAI, OpenAI

load_dotenv()

ChatMessage = MutableMapping[str, Any]


class ChatOpenAI:
"""Thin wrapper around the OpenAI chat completion APIs."""

def __init__(self, model_name: str = "gpt-4o-mini"):
self.model_name = model_name
self.openai_api_key = os.getenv("OPENAI_API_KEY")
if self.openai_api_key is None:
raise ValueError("OPENAI_API_KEY is not set")

self._client = OpenAI()
self._async_client = AsyncOpenAI()

def run(
self,
messages: Iterable[ChatMessage],
text_only: bool = True,
**kwargs: Any,
) -> Any:
"""Execute a chat completion request.

``messages`` must be an iterable of ``{"role": ..., "content": ...}``
dictionaries. When ``text_only`` is ``True`` (the default) only the
completion text is returned; otherwise the full response object is
provided.
"""

message_list = self._coerce_messages(messages)
response = self._client.chat.completions.create(
model=self.model_name, messages=message_list, **kwargs
)

if text_only:
return response.choices[0].message.content

return response

async def astream(
self, messages: Iterable[ChatMessage], **kwargs: Any
) -> AsyncIterator[str]:
"""Yield streaming completion chunks as they arrive from the API."""

message_list = self._coerce_messages(messages)
stream = await self._async_client.chat.completions.create(
model=self.model_name, messages=message_list, stream=True, **kwargs
)

async for chunk in stream:
content = chunk.choices[0].delta.content
if content is not None:
yield content

def _coerce_messages(self, messages: Iterable[ChatMessage]) -> List[ChatMessage]:
if isinstance(messages, list):
return messages
return list(messages)
69 changes: 69 additions & 0 deletions aimakerspace/openai_utils/embedding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import asyncio
import os
from typing import Iterable, List

from dotenv import load_dotenv
from openai import AsyncOpenAI, OpenAI


class EmbeddingModel:
"""Helper for generating embeddings via the OpenAI API."""

def __init__(self, embeddings_model_name: str = "text-embedding-3-small"):
load_dotenv()
self.openai_api_key = os.getenv("OPENAI_API_KEY")
if self.openai_api_key is None:
raise ValueError(
"OPENAI_API_KEY environment variable is not set. "
"Please configure it with your OpenAI API key."
)

self.embeddings_model_name = embeddings_model_name
self.async_client = AsyncOpenAI()
self.client = OpenAI()

async def async_get_embeddings(self, list_of_text: Iterable[str]) -> List[List[float]]:
"""Return embeddings for ``list_of_text`` using the async client."""

embedding_response = await self.async_client.embeddings.create(
input=list(list_of_text), model=self.embeddings_model_name
)

return [item.embedding for item in embedding_response.data]

async def async_get_embedding(self, text: str) -> List[float]:
"""Return an embedding for a single text using the async client."""

embedding = await self.async_client.embeddings.create(
input=text, model=self.embeddings_model_name
)

return embedding.data[0].embedding

def get_embeddings(self, list_of_text: Iterable[str]) -> List[List[float]]:
"""Return embeddings for ``list_of_text`` using the sync client."""

embedding_response = self.client.embeddings.create(
input=list(list_of_text), model=self.embeddings_model_name
)

return [item.embedding for item in embedding_response.data]

def get_embedding(self, text: str) -> List[float]:
"""Return an embedding for a single text using the sync client."""

embedding = self.client.embeddings.create(
input=text, model=self.embeddings_model_name
)

return embedding.data[0].embedding


if __name__ == "__main__":
embedding_model = EmbeddingModel()
print(asyncio.run(embedding_model.async_get_embedding("Hello, world!")))
print(
asyncio.run(
embedding_model.async_get_embeddings(["Hello, world!", "Goodbye, world!"])
)
)
60 changes: 60 additions & 0 deletions aimakerspace/openai_utils/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import re
from typing import Any, Dict, List


class BasePrompt:
"""Simple string template helper used to format prompt text."""

def __init__(self, prompt: str):
self.prompt = prompt
self._pattern = re.compile(r"\{([^}]+)\}")

def format_prompt(self, **kwargs: Any) -> str:
"""Return the prompt with ``kwargs`` substituted for placeholders."""

matches = self._pattern.findall(self.prompt)
replacements = {match: kwargs.get(match, "") for match in matches}
return self.prompt.format(**replacements)

def get_input_variables(self) -> List[str]:
"""Return the placeholder names used by this prompt."""

return self._pattern.findall(self.prompt)


class RolePrompt(BasePrompt):
"""Prompt template that also captures an accompanying chat role."""

def __init__(self, prompt: str, role: str):
super().__init__(prompt)
self.role = role

def create_message(self, apply_format: bool = True, **kwargs: Any) -> Dict[str, str]:
"""Build an OpenAI chat message dictionary for this prompt."""

content = self.format_prompt(**kwargs) if apply_format else self.prompt
return {"role": self.role, "content": content}


class SystemRolePrompt(RolePrompt):
def __init__(self, prompt: str):
super().__init__(prompt, "system")


class UserRolePrompt(RolePrompt):
def __init__(self, prompt: str):
super().__init__(prompt, "user")


class AssistantRolePrompt(RolePrompt):
def __init__(self, prompt: str):
super().__init__(prompt, "assistant")


if __name__ == "__main__":
prompt = BasePrompt("Hello {name}, you are {age} years old")
print(prompt.format_prompt(name="John", age=30))

prompt = SystemRolePrompt("Hello {name}, you are {age} years old")
print(prompt.create_message(name="John", age=30))
print(prompt.get_input_variables())
Loading