Skip to content
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
42 changes: 35 additions & 7 deletions .github/workflows/ci_be.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ on:
- 'be_repo/**'

permissions:
contents: read
contents: write
checks: write
pull-requests: write

jobs:
build:
live-test:
name: Live Test
runs-on: ubuntu-latest

defaults:
Expand All @@ -19,15 +22,15 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
- name: Set up Python 3.9
uses: actions/setup-python@v3
with:
python-version: "3.10"
python-version: "3.9"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install flake8 pytest pytest-cov
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Lint with flake8
Expand All @@ -37,6 +40,31 @@ jobs:
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Test with pytest
- name: Build coverage file
run: |
pytest --ignore=tests/test_e2e.py
pytest --ignore=tests/test_e2e.py --junitxml=pytest.xml \
--cov=app --cov=modules \
--cov-report=term-missing:skip-covered tests/ | tee pytest-coverage.txt

- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./be_repo/pytest-coverage.txt
junitxml-path: ./be_repo/pytest.xml

- uses: actions/checkout@v3
with:
persist-credentials: false
fetch-depth: 0

- name: Update Readme with Coverage Html
run: |
sed -i '/<!-- Pytest Coverage Comment:Begin -->/,/<!-- Pytest Coverage Comment:End -->/c\<!-- Pytest Coverage Comment:Begin -->\n\${{ steps.coverageComment.outputs.coverageHtml }}\n<!-- Pytest Coverage Comment:End -->' ../README.md

- name: Commit & Push changes to Readme
uses: actions-js/push@master
with:
branch: dev/backend
message: Update coverage on Readme
github_token: ${{ secrets.GITHUB_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/ci_e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ jobs:
run: sudo apt purge google-chrome-stable

- name: Installing all necessary packages
run: pip install chromedriver-autoinstaller selenium pyvirtualdisplay
run: pip install chromedriver-autoinstaller selenium pyvirtualdisplay pytest
- name: Install xvfb
run: sudo apt-get install xvfb


- name: Run E2E tests
run: |
python ./be_repo/tests/test_e2e.py
pytest ./be_repo/tests/test_e2e.py
105 changes: 24 additions & 81 deletions CHANGELOG.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
# CVCoach

[![CI for FE](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/ci_fe.yml/badge.svg)](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/ci_fe.yml)
[![CI for BE](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/ci_be.yml/badge.svg)](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/ci_be.yml)
[![CI for E2E](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/ci_e2e.yml/badge.svg)](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/ci_e2e.yml)

[![CD Pipeline](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/cd.yml/badge.svg)](https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/actions/workflows/cd.yml)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)

<!-- Pytest Coverage Comment:Begin -->
<a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/README.md"><img alt="Coverage" src="https://img.shields.io/badge/Coverage-69%25-yellow.svg" /></a><details><summary>Coverage Report </summary><table><tr><th>File</th><th>Stmts</th><th>Miss</th><th>Cover</th><th>Missing</th></tr><tbody><tr><td><a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py">app.py</a></td><td>85</td><td>36</td><td>58%</td><td><a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L34-L35">34&ndash;35</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L40-L48">40&ndash;48</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L52-L82">52&ndash;82</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L88">88</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L92">92</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L97">97</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L101">101</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L112">112</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L117">117</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L119">119</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L124">124</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L128">128</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/app.py#L136">136</a></td></tr><tr><td colspan="5"><b>modules</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/evaluator.py">evaluator.py</a></td><td>56</td><td>14</td><td>75%</td><td><a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/evaluator.py#L96-L97">96&ndash;97</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/evaluator.py#L104-L122">104&ndash;122</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/parser.py">parser.py</a></td><td>12</td><td>1</td><td>92%</td><td><a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/parser.py#L20">20</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/upload.py">upload.py</a></td><td>23</td><td>3</td><td>87%</td><td><a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/upload.py#L23">23</a>, <a href="https://github.com/BUMETCS673/seprojects-cs673a2f24_team5/blob/main/modules/upload.py#L39-L40">39&ndash;40</a></td></tr><tr><td><b>TOTAL</b></td><td><b>176</b></td><td><b>54</b></td><td><b>69%</b></td><td>&nbsp;</td></tr></tbody></table></details>
<!-- Pytest Coverage Comment:End -->

<p>
<!-- <img src="https://img.shields.io/github/license/BUMETCS673/seprojects-cs673a2f24_team5" alt="license"/> -->
<img src="https://img.shields.io/docker/pulls/adamma1024/cvcoach_web" alt="docker-pull-count" />
<img src="https://img.shields.io/docker/pulls/adamma1024/cvcoach_be" alt="be-docker-pull-count" />
<a href="https://img.shields.io/badge/price-free-ff69b4"><img alt="Price" src="https://img.shields.io/badge/price-free-ff69b4?style=flat-square" /></a>
</p>

This repository is a project for METCS673. This project focuses on using AI to automate and improve the process of resume evaluation and interview preparation. By incorporating Retrieval-Augmented Generation (RAG), we ensure that our application can provide more accurate, context-relevant reviews.

## Steps to start up and test (For TA and professor)

### If you'd like to clone our project (Recommended)

```bash
cd /rootPath
docker compose up --build -d # Detach from the terminal
```

Then you can open your browser, and navigate to the <http://localhost:8081>

### If you'd like to pull docker images

We have two images separated for BE and FE.

1. adamma1024/cvcoach_be
2. adamma1024/cvcoach_web

Please make sure you've pulled all them on latest version (check it on Docker Hub) and start them correctly.

## Task Management

As per github's task tracking flow is difficult to use, we decide to use [JIRA Boards](https://bu-cs673a2f24-team-5.atlassian.net/jira/software/projects/SCRUM/boards/1) to trace the progress and manage the risk. Click the hyperlink to check details.
Expand Down
4 changes: 2 additions & 2 deletions be_repo/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim
FROM python:3.9-slim

# Set the workdir to avoid the "pytest Collecting hang"
WORKDIR /app
Expand All @@ -12,7 +12,7 @@ RUN pip3 install -r requirements.txt

# Install pytest for lab3(Even though it's ugly, from my perspective we should not add test codes in docker.)
RUN pip3 install pytest
RUN pytest
RUN pytest --ignore=tests/test_e2e.py

# Run the command to start the application
CMD ["python3", "app.py"]
24 changes: 12 additions & 12 deletions be_repo/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
PERMANENT_SESSION_LIFETIME=timedelta(minutes=30),
)

GOOGLE_CLIENT_ID = ''
GOOGLE_CLIENT_ID = '120137358324-l62fq2hlj9r31evvitg55rcl4rf21udd.apps.googleusercontent.com'

# Test MongoDB connection
try:
Expand All @@ -36,11 +36,11 @@


@app.route('/upload', methods=['POST', 'OPTIONS'])
def upload_resume():
def upload_resume():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

user_id = request.form.get('user_id')
if not user_id:
return jsonify({"error": "No user ID provided."}), 400
Expand All @@ -52,15 +52,15 @@ def login():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

token = request.form.get('access_token')
if not token:
return jsonify({'status': 'error', 'message': 'ID token is missing'}), 400

try:
# Verify the token with Google
idinfo = id_token.verify_oauth2_token(token, google_requests.Request(), GOOGLE_CLIENT_ID)

if idinfo['aud'] != GOOGLE_CLIENT_ID:
return jsonify({'status': 'error', 'message': 'Token audience mismatch'}), 400

Expand All @@ -70,12 +70,12 @@ def login():
name = idinfo.get('name', 'No name available')

# Create a session or JWT for the user
session.permanent = True
session.permanent = True
session['user'] = {'userid': userid, 'email': email, 'name': name}

# Respond with success and optional user data
return jsonify({'status': 'success', 'email': email, 'name': name}), 200

except ValueError as e:
# Token verification failed
print(f"Error verifying token: {str(e)}")
Expand All @@ -86,11 +86,11 @@ def resume_evaluate():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

user_id = request.form.get('user_id')
if not user_id:
return jsonify({"error": "No user ID provided."}), 400

# Load resume from database
resume = resume_collection.find_one({"user_id": user_id})
if not resume:
Expand All @@ -110,7 +110,7 @@ def resume_evaluate_with_JD():
if request.method == 'OPTIONS':
# Allows the preflight request to succeed
return jsonify({'status': 'OK'}), 200

user_id = request.form.get('user_id')
jd_text = request.form.get('jd_text')
if not user_id:
Expand All @@ -133,4 +133,4 @@ def resume_evaluate_with_JD():


if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=5000)
app.run(host='0.0.0.0', debug=True, port=5000)
4 changes: 3 additions & 1 deletion be_repo/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ qdrant_client==1.12.0
tqdm==4.66.2
selenium==4.26.1
google-auth==2.36.0
google-auth-oauthlib==1.2.1
google-auth-oauthlib==1.2.1
chromedriver-autoinstaller==0.6.4
pyvirtualdisplay
92 changes: 45 additions & 47 deletions be_repo/tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,99 @@
import pytest
import os
import time
from selenium import webdriver
import chromedriver_autoinstaller
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support import expected_conditions as EC
import time
import os
from pyvirtualdisplay import Display

FRONTEND_URL = "http://localhost:3001"
keywords = [
"Analysis Result",
"Consistency and Chronology",
"Education",
"Project and Work Experience",
"Resume Structure and Presentation",
"Skills and Certifications",
"Soft Skills"
]

display = Display(visible=0, size=(800, 800))
display.start()

chromedriver_autoinstaller.install()
@pytest.fixture(scope="module")
def driver():
# Start display
display = Display(visible=0, size=(800, 800))
display.start()

chrome_options = webdriver.ChromeOptions()
options = [
"--window-size=1200,1200",
"--ignore-certificate-errors"
"--headless",
]
# Install and set up Chrome driver
chromedriver_autoinstaller.install()
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--window-size=1200,1200")
chrome_options.add_argument("--ignore-certificate-errors")
chrome_options.add_argument("--headless")

for option in options:
chrome_options.add_argument(option)
driver = webdriver.Chrome(options=chrome_options)
yield driver
driver.quit()
display.stop()

driver = webdriver.Chrome(options=chrome_options)

driver.get(FRONTEND_URL)
wait = WebDriverWait(driver, 20)
@pytest.fixture
def wait(driver):
return WebDriverWait(driver, 20)

try:
wait.until(
lambda driver: driver.execute_script("return document.readyState") == "complete"
)

# Google login
# google_login_button = driver.find_element(By.CSS_SELECTOR, '[aria-labelledby="button-label"]')
# google_login_button.click()
def test_resume_upload(driver, wait):
driver.get(FRONTEND_URL)
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")

# Upload Resume
# Upload resume
upload_div = driver.find_element(By.CSS_SELECTOR, "div[style*='cursor: pointer'][style*='display: flex']")
# upload_div.click()

file_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")))

file_path = os.path.join(os.path.dirname(__file__), 'test_resume.pdf')
file_input.send_keys(file_path)

# Check for alert after upload
alert = wait.until(EC.alert_is_present())
alert_text = alert.text
assert "Resume uploaded successfully" in alert_text
alert.accept()


def test_analyze_resume(driver, wait):
# Analyze resume
analyze_button = driver.find_element(By.CLASS_NAME, "cursor-pointer")
analyze_button.click()

submit_button = driver.find_element(By.CLASS_NAME, "mt-6")
submit_button.click()

time.sleep(20)

keywords = [
"Analysis Result",
"Consistency and Chronology",
"Education",
"Project and Work Experience",
"Resume Structure and Presentation",
"Skills and Certifications",
"Soft Skills"
]
time.sleep(20) # Ensure analysis completes

elements = driver.find_elements(By.CLASS_NAME, "font-bold")
content_text = " ".join([element.text for element in elements])
for keyword in keywords:
count = content_text.count(keyword)
assert count == 1, f"'{keyword}' does not appear exactly twice in the content (found {count} times)"
assert count == 1, f"'{keyword}' does not appear exactly once in the content (found {count} times)"


def test_analyze_resume_with_jd(driver, wait):
# Analyze resume with JD
analyze_button = driver.find_element(By.CLASS_NAME, "cursor-pointer")
analyze_button.click()
textarea = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.w-full.h-40.p-4.border-2"))
)
textarea = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.w-full.h-40.p-4.border-2")))
textarea.send_keys("Sample job description text")

submit_button = driver.find_element(By.CLASS_NAME, "mt-6")
submit_button.click()

time.sleep(20)
time.sleep(20) # Ensure analysis completes

elements = driver.find_elements(By.CLASS_NAME, "font-bold")
content_text = " ".join([element.text for element in elements])
for keyword in keywords:
count = content_text.count(keyword)
assert count == 2, f"'{keyword}' does not appear exactly twice in the content (found {count} times)"

finally:
driver.quit()
Binary file not shown.
Binary file not shown.
Binary file added doc/iteration_2/CS673_SDD of Team5.docx
Binary file not shown.
Binary file added doc/iteration_2/CS673_SPPP of Team5.docx
Binary file not shown.
Binary file not shown.
Binary file added doc/iteration_2/CS673_STD of Team5.docx
Binary file not shown.
Binary file added doc/iteration_2/CS673_userstory of Team5.docx
Binary file not shown.
Binary file not shown.
Binary file added doc/iteration_2/demo.mkv
Binary file not shown.
Loading
Loading