Johnny commited on
Commit
edfcf73
·
1 Parent(s): 19ea0c5

added config.toml, updated requirements.txt, UI update

Browse files
Files changed (5) hide show
  1. .streamlit/config.toml +6 -0
  2. config.py +67 -10
  3. main.py +15 -6
  4. requirements.txt +214 -10
  5. utils.py +86 -42
.streamlit/config.toml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor="#F63366"
3
+ backgroundColor="#FFFFFF"
4
+ secondaryBackgroundColor="#F0F2F6"
5
+ textColor="#262730"
6
+ font="sans serif"
config.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  from dotenv import load_dotenv
3
  from supabase import create_client
 
4
 
5
  # Load environment variables from .env file
6
  load_dotenv()
@@ -12,18 +13,74 @@ if not SUPABASE_KEY:
12
  raise ValueError("SUPABASE_KEY is not set in the environment variables.")
13
  supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
14
 
 
 
 
 
 
15
  # Hugging Face API Config
16
- HF_API_URL = "https://router.huggingface.co/hf-inference/models/google/gemma-7b"
17
  HF_API_TOKEN = os.getenv("HF_API_TOKEN")
18
  HF_HEADERS = {"Authorization": f"Bearer HF_API_TOKEN"}
19
 
20
- def query(payload):
21
- """Sends request to Hugging Face inference API."""
22
- import requests
23
- response = requests.post(HF_API_URL, headers=HF_HEADERS, json=payload)
24
-
25
- if response.status_code != 200:
26
- print(f"Error: {response.status_code}, {response.text}") # Debugging
27
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- return response.json()
 
 
 
1
  import os
2
  from dotenv import load_dotenv
3
  from supabase import create_client
4
+ import requests
5
 
6
  # Load environment variables from .env file
7
  load_dotenv()
 
13
  raise ValueError("SUPABASE_KEY is not set in the environment variables.")
14
  supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
15
 
16
+ HF_MODELS = {
17
+ "gemma": "https://api-inference.huggingface.co/models/google/gemma-7b",
18
+ "bart": "https://api-inference.huggingface.co/models/facebook/bart-large-cnn"
19
+ }
20
+
21
  # Hugging Face API Config
22
+ #HF_API_URL = "https://router.huggingface.co/hf-inference/models/google/gemma-7b"
23
  HF_API_TOKEN = os.getenv("HF_API_TOKEN")
24
  HF_HEADERS = {"Authorization": f"Bearer HF_API_TOKEN"}
25
 
26
+ # Ensure the API key is loaded
27
+ if not HF_API_TOKEN:
28
+ raise ValueError("Missing Hugging Face API key. Check your .env file.")
29
+
30
+ #
31
+ def query(payload, model="gemma"):
32
+ """
33
+ Sends a request to the selected Hugging Face model API.
34
+
35
+ :param payload: The input data for inference.
36
+ :param model: Choose either 'gemma' (for google/gemma-7b) or 'bart' (for facebook/bart-large-cnn).
37
+ :return: The model's response in JSON format, or None if the request fails.
38
+ """
39
+ if model not in HF_MODELS:
40
+ raise ValueError("Invalid model name. Choose 'gemma' or 'bart'.")
41
+
42
+ api_url = f"https://api-inference.huggingface.co/models/{HF_MODELS[model]}"
43
+
44
+ try:
45
+ response = requests.post(api_url, headers=HF_HEADERS, json=payload)
46
+
47
+ if response.status_code == 401:
48
+ print(f"Error querying Hugging Face model '{model}': 401 Unauthorized. Check API key.")
49
+ return None # Handle authentication failure
50
+
51
+ response.raise_for_status() # Raise an error for failed requests (e.g., 500 errors)
52
+
53
+ return response.json() # Return the parsed JSON response
54
+
55
+ except requests.exceptions.RequestException as e:
56
+ print(f"Error querying Hugging Face model '{model}': {e}")
57
+ return None # Return None if API call fails
58
+
59
+ # Bart query
60
+ def query(payload, model="bart"):
61
+ """
62
+ Sends a request to the selected Hugging Face model API.
63
+
64
+ :param payload: The input data for inference.
65
+ :param model: Choose either 'gemma' (for google/gemma-7b) or 'bart' (for facebook/bart-large-cnn).
66
+ :return: The model's response in JSON format, or None if the request fails.
67
+ """
68
+ if model not in HF_MODELS:
69
+ raise ValueError("Invalid model name. Choose 'gemma' or 'bart'.")
70
+
71
+ api_url = f"https://api-inference.huggingface.co/models/{HF_MODELS[model]}"
72
+
73
+ try:
74
+ response = requests.post(api_url, headers=HF_HEADERS, json=payload)
75
+
76
+ if response.status_code == 401:
77
+ print(f"Error querying Hugging Face model '{model}': 401 Unauthorized. Check API key.")
78
+ return None # Handle authentication failure
79
+
80
+ response.raise_for_status() # Raise an error for failed requests (e.g., 500 errors)
81
+
82
+ return response.json() # Return the parsed JSON response
83
 
84
+ except requests.exceptions.RequestException as e:
85
+ print(f"Error querying Hugging Face model '{model}': {e}")
86
+ return None # Return None if API call fails
main.py CHANGED
@@ -1,7 +1,7 @@
1
  import streamlit as st
2
- from utils import process_resumes, generate_pdf_report, store_in_supabase, score_candidate, extract_email, parse_resume
3
  from config import supabase
4
- from config import HF_API_TOKEN, HF_API_URL, HF_HEADERS
5
  import fitz # PyMuPDF
6
  from io import BytesIO
7
  from dotenv import load_dotenv
@@ -9,12 +9,21 @@ import os
9
  import requests
10
 
11
  def main():
12
- st.title("AI Candidate Screening App")
 
 
 
 
 
 
 
 
 
 
13
  job_description = st.text_area("Enter Job Description")
14
- uploaded_files = st.file_uploader("Upload Resumes (PDF)", accept_multiple_files=True, type=["pdf"])
15
 
16
- if st.button("Process Resumes"):
17
- shortlisted = process_resumes(uploaded_files, job_description)
18
  for candidate in shortlisted:
19
  st.write(f"**{candidate['name']}** - Score: {candidate['score']}")
20
 
 
1
  import streamlit as st
2
+ from utils import evaluate_resumes, generate_pdf_report, store_in_supabase, score_candidate, extract_email, parse_resume
3
  from config import supabase
4
+ from config import HF_API_TOKEN, HF_HEADERS, HF_MODELS
5
  import fitz # PyMuPDF
6
  from io import BytesIO
7
  from dotenv import load_dotenv
 
9
  import requests
10
 
11
  def main():
12
+ st.set_page_config(page_title="TalentLens.AI", layout="centered")
13
+ st.markdown(
14
+ "<h1 style='text-align: center;'>TalentLens.AI</h1>",
15
+ unsafe_allow_html=True
16
+ )
17
+ st.divider()
18
+ st.markdown(
19
+ "<h3 style='text-align: center;'>AI-Powered Intelligent Resume Screening</h3>",
20
+ unsafe_allow_html=True
21
+ )
22
+ uploaded_files = st.file_uploader("Upload Resumes (PDF Only)", accept_multiple_files=True, type=["pdf"])
23
  job_description = st.text_area("Enter Job Description")
 
24
 
25
+ if st.button("Evaluate Resumes"):
26
+ shortlisted = evaluate_resumes(uploaded_files, job_description)
27
  for candidate in shortlisted:
28
  st.write(f"**{candidate['name']}** - Score: {candidate['score']}")
29
 
requirements.txt CHANGED
@@ -1,10 +1,214 @@
1
- streamlit
2
- langchain
3
- psycopg2
4
- sqlalchemy
5
- PyPDF2
6
- resume-parser
7
- python-dotenv
8
- fitz
9
- requests
10
- reportlab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.11.13
3
+ aiosignal==1.3.2
4
+ altair==5.5.0
5
+ annotated-types==0.7.0
6
+ anyio==4.8.0
7
+ appdirs==1.4.4
8
+ asgiref==3.8.1
9
+ asttokens==3.0.0
10
+ attrs==25.2.0
11
+ auth0-python==4.8.1
12
+ backoff==2.2.1
13
+ bcrypt==4.3.0
14
+ blinker==1.9.0
15
+ blis==1.2.0
16
+ build==1.2.2.post1
17
+ cachetools==5.5.2
18
+ catalogue==2.0.10
19
+ certifi==2025.1.31
20
+ cffi==1.17.1
21
+ charset-normalizer==3.4.1
22
+ chroma-hnswlib==0.7.6
23
+ chromadb==0.6.3
24
+ click==8.1.8
25
+ cloudpathlib==0.21.0
26
+ coloredlogs==15.0.1
27
+ confection==0.1.5
28
+ crewai==0.105.0
29
+ cryptography==44.0.2
30
+ cymem==2.0.11
31
+ decorator==5.2.1
32
+ Deprecated==1.2.18
33
+ deprecation==2.1.0
34
+ distro==1.9.0
35
+ docstring_parser==0.16
36
+ docx2txt==0.8
37
+ durationpy==0.9
38
+ et_xmlfile==2.0.0
39
+ executing==2.2.0
40
+ fastapi==0.115.11
41
+ filelock==3.17.0
42
+ flatbuffers==25.2.10
43
+ frozenlist==1.5.0
44
+ fsspec==2025.3.0
45
+ gitdb==4.0.12
46
+ GitPython==3.1.44
47
+ google-auth==2.38.0
48
+ googleapis-common-protos==1.69.1
49
+ gotrue==2.11.4
50
+ greenlet==3.1.1
51
+ grpcio==1.71.0
52
+ h11==0.14.0
53
+ h2==4.2.0
54
+ hpack==4.1.0
55
+ httpcore==1.0.7
56
+ httptools==0.6.4
57
+ httpx==0.27.2
58
+ huggingface-hub==0.29.3
59
+ humanfriendly==10.0
60
+ hyperframe==6.1.0
61
+ idna==3.10
62
+ importlib_metadata==8.6.1
63
+ importlib_resources==6.5.2
64
+ iniconfig==2.1.0
65
+ instructor==1.7.4
66
+ ipython==9.0.2
67
+ ipython_pygments_lexers==1.1.1
68
+ jedi==0.19.2
69
+ Jinja2==3.1.6
70
+ jiter==0.8.2
71
+ joblib==1.4.2
72
+ json5==0.10.0
73
+ json_repair==0.39.1
74
+ jsonpatch==1.33
75
+ jsonpickle==4.0.2
76
+ jsonpointer==3.0.0
77
+ jsonref==1.1.0
78
+ jsonschema==4.23.0
79
+ jsonschema-specifications==2024.10.1
80
+ kubernetes==32.0.1
81
+ langchain==0.3.20
82
+ langchain-core==0.3.45
83
+ langchain-text-splitters==0.3.6
84
+ langcodes==3.5.0
85
+ langsmith==0.3.15
86
+ language_data==1.3.0
87
+ litellm==1.60.2
88
+ marisa-trie==1.2.1
89
+ markdown-it-py==3.0.0
90
+ MarkupSafe==3.0.2
91
+ matplotlib-inline==0.1.7
92
+ mdurl==0.1.2
93
+ mmh3==5.1.0
94
+ monotonic==1.6
95
+ mpmath==1.3.0
96
+ multidict==6.1.0
97
+ murmurhash==1.0.12
98
+ narwhals==1.30.0
99
+ networkx==3.4.2
100
+ nltk==3.9.1
101
+ numpy==2.2.3
102
+ oauthlib==3.2.2
103
+ onnxruntime==1.16.3
104
+ openai==1.66.3
105
+ openpyxl==3.1.5
106
+ opentelemetry-api==1.31.0
107
+ opentelemetry-exporter-otlp-proto-common==1.31.0
108
+ opentelemetry-exporter-otlp-proto-grpc==1.31.0
109
+ opentelemetry-exporter-otlp-proto-http==1.31.0
110
+ opentelemetry-instrumentation==0.52b0
111
+ opentelemetry-instrumentation-asgi==0.52b0
112
+ opentelemetry-instrumentation-fastapi==0.52b0
113
+ opentelemetry-proto==1.31.0
114
+ opentelemetry-sdk==1.31.0
115
+ opentelemetry-semantic-conventions==0.52b0
116
+ opentelemetry-util-http==0.52b0
117
+ orjson==3.10.15
118
+ overrides==7.7.0
119
+ packaging==24.2
120
+ pandas==2.2.3
121
+ parso==0.8.4
122
+ pdfminer.six==20231228
123
+ pdfplumber==0.11.5
124
+ pexpect==4.9.0
125
+ phonenumbers==9.0.1
126
+ pillow==11.1.0
127
+ pluggy==1.5.0
128
+ postgrest==0.19.3
129
+ posthog==3.19.1
130
+ preshed==3.0.9
131
+ prompt_toolkit==3.0.50
132
+ propcache==0.3.0
133
+ protobuf==5.29.3
134
+ psycopg2==2.9.10
135
+ ptyprocess==0.7.0
136
+ pure_eval==0.2.3
137
+ pyarrow==14.0.0
138
+ pyasn1==0.6.1
139
+ pyasn1_modules==0.4.1
140
+ pycparser==2.22
141
+ pydantic==2.10.6
142
+ pydantic_core==2.27.2
143
+ pydeck==0.9.1
144
+ Pygments==2.19.1
145
+ PyJWT==2.10.1
146
+ PyMuPDF==1.25.4
147
+ PyPDF2==3.0.1
148
+ pypdfium2==4.30.1
149
+ PyPika==0.48.9
150
+ pyproject_hooks==1.2.0
151
+ pytest==8.3.5
152
+ python-dateutil==2.9.0.post0
153
+ python-dotenv==1.0.1
154
+ pytz==2025.1
155
+ pyvis==0.3.2
156
+ PyYAML==6.0.2
157
+ realtime==2.4.1
158
+ referencing==0.36.2
159
+ regex==2024.11.6
160
+ requests==2.32.3
161
+ requests-oauthlib==2.0.0
162
+ requests-toolbelt==1.0.0
163
+ resume-parser==0.8.4
164
+ rich==13.9.4
165
+ rpds-py==0.23.1
166
+ rsa==4.9
167
+ shellingham==1.5.4
168
+ six==1.17.0
169
+ smart-open==7.1.0
170
+ smmap==5.0.2
171
+ sniffio==1.3.1
172
+ spacy==3.8.4
173
+ spacy-legacy==3.0.12
174
+ spacy-loggers==1.0.5
175
+ SQLAlchemy==2.0.39
176
+ srsly==2.5.1
177
+ stack-data==0.6.3
178
+ starlette==0.46.1
179
+ stemming==1.0.1
180
+ storage3==0.11.3
181
+ streamlit==1.43.2
182
+ StrEnum==0.4.15
183
+ supabase==2.13.0
184
+ supafunc==0.9.3
185
+ sympy==1.13.3
186
+ tenacity==9.0.0
187
+ thinc==8.3.4
188
+ tika==2.6.0
189
+ tiktoken==0.9.0
190
+ tokenizers==0.21.0
191
+ toml==0.10.2
192
+ tomli==2.2.1
193
+ tomli_w==1.2.0
194
+ tornado==6.4.2
195
+ tqdm==4.67.1
196
+ traitlets==5.14.3
197
+ typer==0.15.2
198
+ typing_extensions==4.12.2
199
+ tzdata==2025.1
200
+ urllib3==2.3.0
201
+ uv==0.6.6
202
+ uvicorn==0.34.0
203
+ uvloop==0.21.0
204
+ wasabi==1.1.3
205
+ watchdog==6.0.0
206
+ watchfiles==1.0.4
207
+ wcwidth==0.2.13
208
+ weasel==0.4.1
209
+ websocket-client==1.8.0
210
+ websockets==14.2
211
+ wrapt==1.17.2
212
+ yarl==1.18.3
213
+ zipp==3.21.0
214
+ zstandard==0.23.0
utils.py CHANGED
@@ -4,11 +4,34 @@ import json
4
  import re
5
  from io import BytesIO
6
  import supabase
7
- from config import SUPABASE_URL, SUPABASE_KEY, HF_API_TOKEN, HF_API_URL, HF_HEADERS, supabase
8
- #from config import supabase
9
 
10
  # These functions will be called in the main.py file
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  def parse_resume(pdf_file):
13
  """Extracts text from a resume PDF."""
14
  doc = fitz.open(stream=pdf_file.read(), filetype="pdf")
@@ -20,70 +43,91 @@ def extract_email(resume_text):
20
  match = re.search(r"[\w\.-]+@[\w\.-]+", resume_text)
21
  return match.group(0) if match else None
22
 
 
 
23
  def score_candidate(resume_text, job_description):
24
- """Sends resume and job description to Hugging Face for scoring."""
 
 
 
 
 
 
25
  payload = {"inputs": f"Resume: {resume_text}\nJob Description: {job_description}"}
26
- response = requests.post(HF_API_URL, headers=HF_HEADERS, json=payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # Debugging: Print response
29
- if response.status_code != 200:
30
- print(f"Error: {response.status_code}, {response.text}") # Log any errors
31
- return 0 # Return default score if API fails
32
 
33
  try:
34
- return response.json().get("score", 0)
35
- except requests.exceptions.JSONDecodeError:
36
- print("Failed to decode JSON response:", response.text) # Debugging output
37
- return 0 # Return default score if JSON decoding fails
38
 
39
  def store_in_supabase(resume_text, score, candidate_name, email, summary):
40
- """Stores candidate data in Supabase"""
 
 
 
 
 
 
 
 
 
 
 
41
  data = {
42
  "name": candidate_name,
43
  "resume": resume_text,
44
  "score": score,
45
  "email": email,
46
- "summary": summary # Store summary in Supabase
47
  }
48
 
49
  response = supabase.table("candidates").insert(data).execute()
50
- print("Inserted into Supabase:", response) # Debugging output
51
 
 
52
  def generate_pdf_report(shortlisted_candidates):
53
  """Generates a PDF summary of shortlisted candidates."""
54
  pdf = BytesIO()
55
  doc = fitz.open()
 
56
  for candidate in shortlisted_candidates:
57
  page = doc.new_page()
58
- summary = candidate.get("summary", "No summary available") # Avoid KeyError
 
 
 
59
  page.insert_text(
60
  (50, 50),
61
- f"Candidate: {candidate['name']}\nEmail: {candidate['email']}\nScore: {candidate['score']}\nSummary: {summary}"
 
 
 
62
  )
 
63
  doc.save(pdf)
64
  pdf.seek(0)
65
- return pdf
66
-
67
- def process_resumes(uploaded_files, job_description):
68
- """Processes uploaded resumes and returns shortlisted candidates."""
69
- candidates = []
70
- for pdf_file in uploaded_files:
71
- resume_text = parse_resume(pdf_file)
72
- score = score_candidate(resume_text, job_description)
73
- email = extract_email(resume_text)
74
-
75
- # Generate summary (replace with actual summarization logic later)
76
- summary = f"{pdf_file.name} has a score of {score} for this job."
77
-
78
- candidates.append({
79
- "name": pdf_file.name,
80
- "resume": resume_text,
81
- "score": score,
82
- "email": email,
83
- "summary": summary
84
- })
85
-
86
- # Store all details including summary in Supabase
87
- store_in_supabase(resume_text, score, pdf_file.name, email, summary)
88
-
89
- return sorted(candidates, key=lambda x: x["score"], reverse=True)[:5] # Return top 5 candidates
 
4
  import re
5
  from io import BytesIO
6
  import supabase
7
+ from config import SUPABASE_URL, SUPABASE_KEY, HF_API_TOKEN, HF_HEADERS, supabase, HF_MODELS, query
 
8
 
9
  # These functions will be called in the main.py file
10
 
11
+ def evaluate_resumes(uploaded_files, job_description):
12
+ """Evaluates uploaded resumes and returns shortlisted candidates."""
13
+ candidates = []
14
+ for pdf_file in uploaded_files:
15
+ resume_text = parse_resume(pdf_file)
16
+ score = score_candidate(resume_text, job_description)
17
+ email = extract_email(resume_text)
18
+
19
+ # Generate a summary of the resume
20
+ summary = summarize_resume(resume_text)
21
+
22
+ candidates.append({
23
+ "name": pdf_file.name,
24
+ "resume": resume_text,
25
+ "score": score,
26
+ "email": email,
27
+ "summary": summary
28
+ })
29
+
30
+ # Store all details including summary in Supabase
31
+ store_in_supabase(resume_text, score, pdf_file.name, email, summary)
32
+
33
+ return sorted(candidates, key=lambda x: x["score"], reverse=True)[:5] # Return top 5 candidates
34
+
35
  def parse_resume(pdf_file):
36
  """Extracts text from a resume PDF."""
37
  doc = fitz.open(stream=pdf_file.read(), filetype="pdf")
 
43
  match = re.search(r"[\w\.-]+@[\w\.-]+", resume_text)
44
  return match.group(0) if match else None
45
 
46
+ # Test on why score 0 is returned even though resume matches key words
47
+ # score_candidate function will use HuggingFace gemini model
48
  def score_candidate(resume_text, job_description):
49
+ """
50
+ Scores the candidate's resume based on the job description using the Hugging Face API.
51
+
52
+ :param resume_text: The extracted resume text.
53
+ :param job_description: The job description for comparison.
54
+ :return: A numerical score (default 0 if scoring fails).
55
+ """
56
  payload = {"inputs": f"Resume: {resume_text}\nJob Description: {job_description}"}
57
+ response_gemma = query(payload, model="gemma") # Use Google Gemma Model for scoring
58
+
59
+ if response_gemma is None:
60
+ return 0 # Return 0 if API call fails
61
+
62
+ try:
63
+ return float(response_gemma.get("score", 0)) # Ensure score is always a float
64
+ except (TypeError, ValueError):
65
+ return 0 # Return 0 if score parsing fails
66
+
67
+ # summarize_resume function will use HuggingFace BART model
68
+ def summarize_resume(resume_text):
69
+ """
70
+ Summarizes the resume using Facebook's BART-Large-CNN model.
71
+
72
+ :param resume_text: The extracted resume text.
73
+ :return: A summarized version of the resume or an error message.
74
+ """
75
+ payload = {"inputs": resume_text}
76
+ response_bart = query(payload, model="bart")
77
 
78
+ if response_bart is None:
79
+ return "Summary could not be generated." # Handle API failures gracefully
 
 
80
 
81
  try:
82
+ summary = response_bart[0].get("summary_text", "Summary not available.")
83
+ return summary
84
+ except (IndexError, KeyError):
85
+ return "Summary not available."
86
 
87
  def store_in_supabase(resume_text, score, candidate_name, email, summary):
88
+ """
89
+ Stores resume data in Supabase.
90
+
91
+ :param resume_text: The extracted resume text.
92
+ :param score: The candidate's score (must be a valid number).
93
+ :param candidate_name: The candidate's name.
94
+ :param email: Candidate's email address.
95
+ :param summary: A summarized version of the resume.
96
+ """
97
+ if score is None:
98
+ score = 0 # Ensure score is never NULL
99
+
100
  data = {
101
  "name": candidate_name,
102
  "resume": resume_text,
103
  "score": score,
104
  "email": email,
105
+ "summary": summary
106
  }
107
 
108
  response = supabase.table("candidates").insert(data).execute()
109
+ return response
110
 
111
+ # Test with 10 resumes, if they will be shortlisted
112
  def generate_pdf_report(shortlisted_candidates):
113
  """Generates a PDF summary of shortlisted candidates."""
114
  pdf = BytesIO()
115
  doc = fitz.open()
116
+
117
  for candidate in shortlisted_candidates:
118
  page = doc.new_page()
119
+
120
+ # Use the stored summary, or provide a fallback
121
+ summary = candidate.get("summary", "No summary available")
122
+
123
  page.insert_text(
124
  (50, 50),
125
+ f"Candidate: {candidate['name']}\n"
126
+ f"Email: {candidate['email']}\n"
127
+ f"Score: {candidate['score']}\n"
128
+ f"Summary: {summary}"
129
  )
130
+
131
  doc.save(pdf)
132
  pdf.seek(0)
133
+ return pdf