Skip to content

Commit 08cff34

Browse files
added env variables to fastApi for hosting, formatting changes from black
1 parent aa4d924 commit 08cff34

13 files changed

Lines changed: 457 additions & 338 deletions

‎backend/clients/groq_client.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ async def optimize_with_groq(resume_text: str, job_description: str, api_key: st
3535
async with httpx.AsyncClient() as client:
3636
response = await client.post(
3737
"https://api.groq.com/openai/v1/chat/completions",
38-
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
38+
headers={
39+
"Authorization": f"Bearer {api_key}",
40+
"Content-Type": "application/json",
41+
},
3942
json={
4043
"model": GROQ_MODEL,
4144
"messages": [

‎backend/clients/ollama_client.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def get_ollama_client():
1515
Returns:
1616
Configured ollama client
1717
"""
18-
ollama_host = os.getenv('OLLAMA_HOST', 'http://localhost:11434')
19-
if ollama_host != 'http://localhost:11434':
18+
ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434")
19+
if ollama_host != "http://localhost:11434":
2020
# Configure ollama client for custom host
2121
ollama._client.base_url = ollama_host
2222
return ollama

‎backend/main.py‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23

34
from fastapi import FastAPI, File, HTTPException, UploadFile
45
from fastapi.middleware.cors import CORSMiddleware
@@ -17,12 +18,14 @@
1718
app = FastAPI(title="ATS-Buddy", description="AI-powered resume customizer for job applications")
1819

1920
# Configure CORS for Next.js frontend
21+
allowed_origins = [
22+
os.environ.get("FRONTEND_ORIGIN", "http://localhost:3000"),
23+
os.environ.get("NEXT_PUBLIC_API_URL", "http://localhost:8000"),
24+
]
25+
2026
app.add_middleware(
2127
CORSMiddleware,
22-
allow_origins=[
23-
"http://localhost:3000", # Next.js dev server
24-
"http://frontend:3000", # Docker container
25-
],
28+
allow_origins=allowed_origins,
2629
allow_credentials=True,
2730
allow_methods=["*"],
2831
allow_headers=["*"],
@@ -66,7 +69,11 @@ async def health_check():
6669

6770
# Test Ollama connection
6871
models = ollama_client.list()
69-
return {"status": "healthy", "ollama_available": True, "available_models": [model.model for model in models.models]}
72+
return {
73+
"status": "healthy",
74+
"ollama_available": True,
75+
"available_models": [model.model for model in models.models],
76+
}
7077
except Exception as e:
7178
logger.error(f"Health check failed: {e}")
7279
return {"status": "unhealthy", "ollama_available": False, "error": str(e)}
@@ -181,11 +188,17 @@ async def optimize_resume_endpoint(request: ResumeRequest):
181188
# Step 0: validation:
182189
# Validate that either job_url or job_description is provided
183190
if not request.job_url and not request.job_description:
184-
raise HTTPException(status_code=400, detail="Either job_url or job_description must be provided")
191+
raise HTTPException(
192+
status_code=400,
193+
detail="Either job_url or job_description must be provided",
194+
)
185195

186196
# Validate Groq API key if using Groq
187197
if request.use_groq and not request.groq_api_key:
188-
raise HTTPException(status_code=400, detail="Groq API key is required when use_groq is enabled")
198+
raise HTTPException(
199+
status_code=400,
200+
detail="Groq API key is required when use_groq is enabled",
201+
)
189202

190203
# Step 1: Get job description (either from URL or direct input)
191204
if request.job_description:
@@ -207,7 +220,9 @@ async def optimize_resume_endpoint(request: ResumeRequest):
207220
logger.info("Successfully optimized resume")
208221

209222
return ResumeResponse(
210-
optimized_resume=result["optimized_resume"], job_description=job_description, changes_made=result["changes_made"]
223+
optimized_resume=result["optimized_resume"],
224+
job_description=job_description,
225+
changes_made=result["changes_made"],
211226
)
212227

213228
except Exception as e:

‎backend/optimization/change_detector.py‎

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,31 @@ def detect_changes(original_text: str, optimized_text: str) -> list[str]:
3030

3131
try:
3232
# Split into lines for comparison
33-
original_lines = [line.strip() for line in original_text.split('\n') if line.strip()]
34-
optimized_lines = [line.strip() for line in optimized_text.split('\n') if line.strip()]
33+
original_lines = [line.strip() for line in original_text.split("\n") if line.strip()]
34+
optimized_lines = [line.strip() for line in optimized_text.split("\n") if line.strip()]
3535

3636
logger.debug(f"Detecting changes: {len(original_lines)} original lines vs {len(optimized_lines)} optimized lines")
3737

3838
# Use SequenceMatcher to find differences
3939
matcher = SequenceMatcher(None, original_lines, optimized_lines)
4040

4141
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
42-
if tag == 'equal':
42+
if tag == "equal":
4343
continue
4444

4545
# Find section context for this change
4646
current_section, current_subsection = _find_section_context(i1, original_lines)
4747
section_label = f"{current_section} - {current_subsection}" if current_subsection else current_section
4848

49-
if tag == 'replace':
49+
if tag == "replace":
5050
# Lines were changed
5151
original_chunk = original_lines[i1:i2]
5252
optimized_chunk = optimized_lines[j1:j2]
5353

5454
change_type = _analyze_change_type(original_chunk, optimized_chunk, current_section)
5555
changes.append(f"{section_label}: {change_type}")
5656

57-
elif tag == 'delete':
57+
elif tag == "delete":
5858
# Lines were removed
5959
deleted_chunk = original_lines[i1:i2]
6060

@@ -70,7 +70,7 @@ def detect_changes(original_text: str, optimized_text: str) -> list[str]:
7070
else:
7171
changes.append(f"{section_label}: Removed {len(deleted_chunk)} less relevant item(s)")
7272

73-
elif tag == 'insert':
73+
elif tag == "insert":
7474
# Lines were added
7575
added_chunk = optimized_lines[j1:j2]
7676

@@ -154,28 +154,28 @@ def _identify_section(line: str) -> str | None:
154154

155155
line_stripped = line.strip()
156156
line_lower = line_stripped.lower()
157-
line_clean = line_stripped.rstrip(':').replace('**', '').replace('*', '').replace('#', '').strip()
157+
line_clean = line_stripped.rstrip(":").replace("**", "").replace("*", "").replace("#", "").strip()
158158

159159
# Skip empty lines or lines that are too long to be headers
160160
if not line_clean or len(line_clean) > 60:
161161
return None
162162

163163
# Skip bullet points
164-
if line_stripped.startswith(('• ', '- ', '* ', '○ ', '▪ ', '― ')):
164+
if line_stripped.startswith(("• ", "- ", "* ", "○ ", "▪ ", "― ")):
165165
return None
166166

167167
# Skip lines with too many commas
168-
if line.count(',') > 2:
168+
if line.count(",") > 2:
169169
return None
170170

171171
# Check if it's likely a section header
172172
is_header = False
173173

174174
if line_stripped.isupper():
175175
is_header = True
176-
elif line_stripped.endswith(':'):
176+
elif line_stripped.endswith(":"):
177177
is_header = True
178-
elif '**' in line or line.startswith('#'):
178+
elif "**" in line or line.startswith("#"):
179179
is_header = True
180180
elif len(line_clean.split()) <= 4 and len(line_clean) < 40:
181181
if line_clean[0].isupper():
@@ -207,8 +207,8 @@ def _analyze_change_type(original: list[str], optimized: list[str], section: str
207207
Returns:
208208
Description of the change
209209
"""
210-
original_text = ' '.join(original).lower()
211-
optimized_text = ' '.join(optimized).lower()
210+
original_text = " ".join(original).lower()
211+
optimized_text = " ".join(optimized).lower()
212212

213213
# Extract words for comparison
214214
original_words = set(original_text.split())
@@ -220,7 +220,7 @@ def _analyze_change_type(original: list[str], optimized: list[str], section: str
220220
# Get significant additions (filter out common words)
221221
significant_additions = []
222222
for w in added_words:
223-
cleaned = w.strip('.,;:!?()[]{}"\'-')
223+
cleaned = w.strip(".,;:!?()[]{}\"'-")
224224
if cleaned and cleaned not in COMMON_WORDS and len(cleaned) > 2:
225225
significant_additions.append(cleaned)
226226

@@ -231,9 +231,9 @@ def _analyze_change_type(original: list[str], optimized: list[str], section: str
231231
added_verbs = [w for w in significant_additions if w.lower() in ACTION_VERBS]
232232

233233
# Provide specific feedback based on section and what was added
234-
if section.lower() in ['skills', 'technical skills', 'technologies']:
234+
if section.lower() in ["skills", "technical skills", "technologies"]:
235235
if significant_additions:
236-
sample_skills = ', '.join(sorted(list(set(significant_additions))[:5]))
236+
sample_skills = ", ".join(sorted(list(set(significant_additions))[:5]))
237237
return f"Added skills: {sample_skills}"
238238
elif len(added_words) > len(removed_words):
239239
return "Enhanced skill list with relevant technologies"
@@ -242,15 +242,15 @@ def _analyze_change_type(original: list[str], optimized: list[str], section: str
242242
display_limit = 8
243243
if added_tech:
244244
total = len(set(added_tech))
245-
sample = ', '.join(sorted(list(set(added_tech))[:display_limit]))
245+
sample = ", ".join(sorted(list(set(added_tech))[:display_limit]))
246246
if total > display_limit:
247247
return f"Added keywords: {sample}, +{total - display_limit} more"
248248
else:
249249
return f"Added keywords: {sample}"
250250

251251
elif added_testing:
252252
total = len(set(added_testing))
253-
sample = ', '.join(sorted(list(set(added_testing))[:display_limit]))
253+
sample = ", ".join(sorted(list(set(added_testing))[:display_limit]))
254254
if total > display_limit:
255255
return f"Added testing keywords: {sample}, +{total - display_limit} more"
256256
else:
@@ -260,17 +260,16 @@ def _analyze_change_type(original: list[str], optimized: list[str], section: str
260260
return "Added quantifiable metrics and impact"
261261

262262
elif added_verbs:
263-
sample = ', '.join(sorted(list(set(added_verbs))[:3]))
263+
sample = ", ".join(sorted(list(set(added_verbs))[:3]))
264264
return f"Strengthened with action verbs: {sample}"
265265

266266
elif len(significant_additions) > 0:
267-
268267
total = len(set(significant_additions))
269268
if total <= display_limit:
270-
sample = ', '.join(sorted(list(set(significant_additions))))
269+
sample = ", ".join(sorted(list(set(significant_additions))))
271270
return f"Added keywords: {sample}"
272271
else:
273-
sample = ', '.join(sorted(list(set(significant_additions))[:display_limit]))
272+
sample = ", ".join(sorted(list(set(significant_additions))[:display_limit]))
274273
return f"Added keywords: {sample}, +{total - display_limit} more"
275274
elif len(added_words) > len(removed_words):
276275
return "Enhanced with additional keywords"
@@ -293,17 +292,17 @@ def _consolidate_changes(changes: list[str]) -> list[str]:
293292
change_dict: dict[str, list[str]] = {}
294293

295294
for change in changes:
296-
if ':' in change:
297-
section_label = change.split(':', 1)[0].strip()
298-
description = change.split(':', 1)[1].strip()
295+
if ":" in change:
296+
section_label = change.split(":", 1)[0].strip()
297+
description = change.split(":", 1)[1].strip()
299298

300299
if section_label not in change_dict:
301300
change_dict[section_label] = []
302301
change_dict[section_label].append(description)
303302
else:
304-
if 'Other' not in change_dict:
305-
change_dict['Other'] = []
306-
change_dict['Other'].append(change)
303+
if "Other" not in change_dict:
304+
change_dict["Other"] = []
305+
change_dict["Other"].append(change)
307306

308307
# Format consolidated changes
309308
grouped_changes = []
@@ -324,17 +323,17 @@ def _consolidate_changes(changes: list[str]) -> list[str]:
324323
if any(
325324
keyword_type in desc
326325
for keyword_type in [
327-
'Added skills:',
328-
'Added keywords:',
329-
'Added tech keywords:',
330-
'Added testing keywords:',
326+
"Added skills:",
327+
"Added keywords:",
328+
"Added tech keywords:",
329+
"Added testing keywords:",
331330
]
332331
):
333332
# Extract keywords
334-
parts = desc.split(':', 1)
333+
parts = desc.split(":", 1)
335334
if len(parts) > 1:
336335
keyword_str = parts[1].strip()
337-
keyword_list = keyword_str.split(',')[:-1] if '+' in keyword_str else keyword_str.split(',')
336+
keyword_list = keyword_str.split(",")[:-1] if "+" in keyword_str else keyword_str.split(",")
338337
all_keywords.extend([k.strip() for k in keyword_list if k.strip()])
339338
else:
340339
other_changes.append(desc)

‎backend/optimization/response_parser.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def parse_llm_response(response_text: str, original_text: str | None = None) ->
2626
logger.debug(f"Raw response received (first 1000 chars): {response_text[:1000]}")
2727

2828
# Try to find JSON in the response
29-
json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
29+
json_match = re.search(r"\{.*\}", response_text, re.DOTALL)
3030

3131
if json_match:
3232
json_str = json_match.group(0)
@@ -42,7 +42,7 @@ def parse_llm_response(response_text: str, original_text: str | None = None) ->
4242

4343
# Check if the optimized_resume field contains embedded JSON
4444
if isinstance(resume_text, str):
45-
if resume_text.strip().startswith('{') and resume_text.strip().endswith('}'):
45+
if resume_text.strip().startswith("{") and resume_text.strip().endswith("}"):
4646
try:
4747
embedded_json = json.loads(resume_text)
4848
if "optimized_resume" in embedded_json:
@@ -126,8 +126,8 @@ def _trim_at_markers(text: str) -> str:
126126
"Modifications:",
127127
"Key changes:",
128128
"Updates made:",
129-
'},',
130-
']',
129+
"},",
130+
"]",
131131
]
132132

133133
earliest_pos = len(text)
@@ -153,7 +153,7 @@ def _try_regex_extraction(response_text: str, original_text: str | None = None)
153153
Returns:
154154
Parsed result or None if extraction fails
155155
"""
156-
if not (response_text.strip().startswith('{') and response_text.strip().endswith('}')):
156+
if not (response_text.strip().startswith("{") and response_text.strip().endswith("}")):
157157
return None
158158

159159
# Extract optimized_resume content
@@ -167,7 +167,7 @@ def _try_regex_extraction(response_text: str, original_text: str | None = None)
167167
return None
168168

169169
resume_content = resume_match.group(1)
170-
resume_content = resume_content.replace('\\"', '"').replace('\\n', '\n').replace('\\t', '\t')
170+
resume_content = resume_content.replace('\\"', '"').replace("\\n", "\n").replace("\\t", "\t")
171171
logger.info("Successfully extracted optimized resume using regex parsing")
172172

173173
# Extract changes_made if it exists

‎backend/optimization/text_cleaner.py‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,36 @@ def clean_resume_text(text: str) -> str:
2020
return text
2121

2222
# Remove markdown code block syntax (```json, ```, etc.)
23-
text = re.sub(r'^```[\w]*\n?', '', text, flags=re.MULTILINE)
24-
text = re.sub(r'\n?```$', '', text, flags=re.MULTILINE)
23+
text = re.sub(r"^```[\w]*\n?", "", text, flags=re.MULTILINE)
24+
text = re.sub(r"\n?```$", "", text, flags=re.MULTILINE)
2525

2626
# Remove triple quotes at start/end (common with Groq responses)
27-
text = re.sub(r'^"""\s*\n?', '', text, flags=re.MULTILINE)
28-
text = re.sub(r'\n?\s*"""$', '', text, flags=re.MULTILINE)
27+
text = re.sub(r'^"""\s*\n?', "", text, flags=re.MULTILINE)
28+
text = re.sub(r'\n?\s*"""$', "", text, flags=re.MULTILINE)
2929

3030
# Remove JSON artifacts that might be embedded in the text
3131
json_artifacts = [
3232
r'",?\s*\n?\s*"changes_made":\s*\[.*?\].*?}?$', # Remove changes_made array at end
3333
r'",?\s*\n?\s*"keyword_matches":\s*\[.*?\].*?}?$', # Remove keyword_matches array at end
3434
r'"\s*,\s*\n?\s*"changes_made".*$', # Remove changes_made key and following content
3535
r'"\s*,\s*\n?\s*"keyword_matches".*$', # Remove keyword_matches key and following content
36-
r'\]\s*}?\s*$', # Remove trailing array/object closures
37-
r'\n*}$', # Remove trailing curly brace at end of text
38-
r'\n*\}$', # Remove trailing curly brace with newline
39-
r'\n*\}\s*$', # Remove trailing curly brace with whitespace
36+
r"\]\s*}?\s*$", # Remove trailing array/object closures
37+
r"\n*}$", # Remove trailing curly brace at end of text
38+
r"\n*\}$", # Remove trailing curly brace with newline
39+
r"\n*\}\s*$", # Remove trailing curly brace with whitespace
4040
]
4141

4242
for pattern in json_artifacts:
43-
text = re.sub(pattern, '', text, flags=re.MULTILINE | re.DOTALL)
43+
text = re.sub(pattern, "", text, flags=re.MULTILINE | re.DOTALL)
4444

4545
# Remove any leading '{', quotes, or JSON artifacts at the start
46-
text = re.sub(r'^[{\s]*"?optimized_resume"?\s*:\s*"?', '', text)
46+
text = re.sub(r'^[{\s]*"?optimized_resume"?\s*:\s*"?', "", text)
4747
text = text.strip('`"} {')
4848

4949
# Remove extra leading/trailing whitespace while preserving internal formatting
5050
text = text.strip()
5151

5252
# Normalize line endings and remove excessive blank lines
53-
text = re.sub(r'\n{3,}', '\n\n', text)
53+
text = re.sub(r"\n{3,}", "\n\n", text)
5454

5555
return text

0 commit comments

Comments
 (0)