dhanvanth183 commited on
Commit
c341754
Β·
verified Β·
1 Parent(s): 7196672

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +606 -587
app.py CHANGED
@@ -1,587 +1,606 @@
1
- import streamlit as st
2
- from openai import OpenAI
3
- import os
4
- from dotenv import load_dotenv
5
- from datetime import datetime
6
- import pytz
7
- from reportlab.lib.pagesizes import letter
8
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
9
- from reportlab.lib.units import inch
10
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
11
- from reportlab.lib.enums import TA_LEFT, TA_JUSTIFY
12
- import io
13
-
14
- # Load environment variables
15
- load_dotenv()
16
-
17
- # Page configuration
18
- st.set_page_config(page_title="Assistant Tiya", layout="wide")
19
- st.title("πŸ€– Assistant Tiya")
20
-
21
- # Load API keys from environment variables
22
- openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
23
- openai_api_key = os.getenv("OPENAI_API_KEY")
24
-
25
- # Check if API keys are available
26
- if not openrouter_api_key or not openai_api_key:
27
- st.error("❌ API keys not found. Please set OPENROUTER_API_KEY and OPENAI_API_KEY in your environment variables (.env file).")
28
- st.stop()
29
-
30
- def get_est_timestamp():
31
- """Get current timestamp in EST timezone with format dd-mm-yyyy-HH-MM"""
32
- est = pytz.timezone('US/Eastern')
33
- now = datetime.now(est)
34
- return now.strftime("%d-%m-%Y-%H-%M")
35
-
36
-
37
- def generate_pdf(content, filename):
38
- """Generate PDF from content and return as bytes"""
39
- try:
40
- pdf_buffer = io.BytesIO()
41
- doc = SimpleDocTemplate(
42
- pdf_buffer,
43
- pagesize=letter,
44
- rightMargin=0.75*inch,
45
- leftMargin=0.75*inch,
46
- topMargin=0.75*inch,
47
- bottomMargin=0.75*inch
48
- )
49
-
50
- story = []
51
- styles = getSampleStyleSheet()
52
-
53
- # Custom style for body text
54
- body_style = ParagraphStyle(
55
- 'CustomBody',
56
- parent=styles['Normal'],
57
- fontSize=11,
58
- leading=14,
59
- alignment=TA_JUSTIFY,
60
- spaceAfter=12
61
- )
62
-
63
- # Add content only (no preamble)
64
- # Split content into paragraphs for better formatting
65
- paragraphs = content.split('\n\n')
66
- for para in paragraphs:
67
- if para.strip():
68
- # Replace line breaks with spaces within paragraphs
69
- clean_para = para.replace('\n', ' ').strip()
70
- story.append(Paragraph(clean_para, body_style))
71
-
72
- # Build PDF
73
- doc.build(story)
74
- pdf_buffer.seek(0)
75
- return pdf_buffer.getvalue()
76
-
77
- except Exception as e:
78
- st.error(f"Error generating PDF: {str(e)}")
79
- return None
80
-
81
-
82
- def categorize_input(resume_finder, cover_letter, select_resume, entry_query):
83
- """
84
- Categorize input into one of 4 groups:
85
- - resume_finder: T, F, No Select
86
- - cover_letter: F, T, not No Select
87
- - general_query: F, F, not No Select
88
- - retry: any other combination
89
- """
90
-
91
- if resume_finder and not cover_letter and select_resume == "No Select":
92
- return "resume_finder", None
93
-
94
- elif not resume_finder and cover_letter and select_resume != "No Select":
95
- return "cover_letter", None
96
-
97
- elif not resume_finder and not cover_letter and select_resume != "No Select":
98
- if not entry_query.strip():
99
- return "retry", "Please enter a query for General Query mode."
100
- return "general_query", None
101
-
102
- else:
103
- return "retry", "Please check your entries and try again"
104
-
105
-
106
- def load_portfolio(file_path):
107
- """Load portfolio markdown file"""
108
- try:
109
- full_path = os.path.join(os.path.dirname(__file__), file_path)
110
- with open(full_path, 'r', encoding='utf-8') as f:
111
- return f.read()
112
- except FileNotFoundError:
113
- st.error(f"Portfolio file {file_path} not found!")
114
- return None
115
-
116
-
117
- def handle_resume_finder(job_description, ai_portfolio, ds_portfolio, api_key):
118
- """Handle Resume Finder category using OpenRouter"""
119
-
120
- prompt = f"""You are an expert resume matcher. Analyze the following job description and two portfolios to determine which is the best match.
121
-
122
- IMPORTANT MAPPING:
123
- - If AI_portfolio is most relevant β†’ Resume = Resume_P
124
- - If DS_portfolio is most relevant β†’ Resume = Resume_Dss
125
-
126
- Job Description:
127
- {job_description}
128
-
129
- AI_portfolio (Maps to: Resume_P):
130
- {ai_portfolio}
131
-
132
- DS_portfolio (Maps to: Resume_Dss):
133
- {ds_portfolio}
134
-
135
- Respond ONLY with:
136
- Resume: [Resume_P or Resume_Dss]
137
- Reasoning: [25-30 words explaining the match]
138
-
139
- NO PREAMBLE."""
140
-
141
- try:
142
- client = OpenAI(
143
- base_url="https://openrouter.ai/api/v1",
144
- api_key=api_key,
145
- )
146
-
147
- completion = client.chat.completions.create(
148
- model="openai/gpt-oss-safeguard-20b",
149
- messages=[
150
- {
151
- "role": "user",
152
- "content": prompt
153
- }
154
- ]
155
- )
156
-
157
- response = completion.choices[0].message.content
158
- if response:
159
- return response
160
- else:
161
- st.error("❌ No response received from OpenRouter API")
162
- return None
163
-
164
- except Exception as e:
165
- st.error(f"❌ Error calling OpenRouter API: {str(e)}")
166
- return None
167
-
168
-
169
- def generate_cover_letter_context(job_description, portfolio, api_key):
170
- """Generate company motivation and achievement section using web search via Perplexity Sonar
171
-
172
- Args:
173
- job_description: The job posting
174
- portfolio: Candidate's resume/portfolio
175
- api_key: OpenRouter API key (used for Perplexity Sonar with web search)
176
-
177
- Returns:
178
- dict: {"company_motivation": str, "achievement_section": str}
179
- """
180
-
181
- prompt = f"""You are an expert career strategist. Your task is to generate two specific, personalized inputs for a cover letter.
182
-
183
- Use your web search capability to find relevant company information. Given the job description and candidate's portfolio, you will:
184
- 1. Search for relevant company information (mission, values, recent projects, culture)
185
- 2. Identify the best achievement from the portfolio that matches the role
186
- 3. Generate targeted content for cover letter generation
187
-
188
- Job Description:
189
- {job_description}
190
-
191
- Candidate's Portfolio:
192
- {portfolio}
193
-
194
- Generate a JSON response with exactly this format (no additional text):
195
- {{
196
- "company_motivation": "1-2 sentences showing specific interest in THIS company based on their mission/values/recent work. Use your web search to find specific details about the company. Should feel genuine and specific, not generic.",
197
- "achievement_section": "One specific, quantified achievement from the portfolio that directly supports the job requirements. Format: 'achievement that resulted in specific outcome'."
198
- }}
199
-
200
- Requirements for company_motivation:
201
- - Must reference something specific from the job description OR from web search about the company (company needs, projects, values, recent news)
202
- - Should show research and genuine interest using real company information
203
- - 1-2 sentences maximum
204
- - Sound natural and authentic
205
-
206
- Requirements for achievement_section:
207
- - Must be concrete and specific
208
- - Should include numbers/metrics when possible
209
- - Must be relevant to the job requirements
210
- - Maximum 1 sentence
211
-
212
- Return ONLY the JSON object, no other text."""
213
-
214
- # Use Perplexity Sonar via OpenRouter (has built-in web search)
215
- client = OpenAI(
216
- base_url="https://openrouter.ai/api/v1",
217
- api_key=api_key,
218
- )
219
-
220
- completion = client.chat.completions.create(
221
- model="perplexity/sonar",
222
- messages=[
223
- {
224
- "role": "user",
225
- "content": prompt
226
- }
227
- ]
228
- )
229
-
230
- response_text = completion.choices[0].message.content
231
-
232
- # Parse JSON response
233
- import json
234
- try:
235
- result = json.loads(response_text)
236
- return {
237
- "company_motivation": result.get("company_motivation", ""),
238
- "achievement_section": result.get("achievement_section", "")
239
- }
240
- except json.JSONDecodeError:
241
- # Fallback if JSON parsing fails
242
- return {
243
- "company_motivation": "",
244
- "achievement_section": ""
245
- }
246
-
247
-
248
- def handle_cover_letter(job_description, portfolio, api_key, company_motivation="", specific_achievement=""):
249
- """Handle Cover Letter category using OpenAI
250
-
251
- Args:
252
- job_description: The job posting
253
- portfolio: Candidate's resume/portfolio
254
- api_key: OpenAI API key
255
- company_motivation: Why candidate is interested in THIS company/role (auto-generated if empty)
256
- specific_achievement: One concrete achievement to leverage (auto-generated if empty)
257
- """
258
-
259
- # Build context about company motivation if provided
260
- motivation_section = ""
261
- if company_motivation.strip():
262
- motivation_section = f"\nCandidate's Interest in This Role:\n{company_motivation}"
263
-
264
- achievement_section = ""
265
- if specific_achievement.strip():
266
- achievement_section = f"\nKey Achievement to Reference:\n{specific_achievement}"
267
-
268
- prompt = f"""You are an expert career coach writing authentic, human cover letters that stand outβ€”not generic templates.
269
-
270
- Your goal: Write a cover letter that feels like it was written by the actual candidate, showing genuine interest and proof of capability.
271
-
272
- Cover Letter Structure (follow this order):
273
- 1. Opening (2-3 sentences): Hook with specific reason for interest in THIS company, not generic
274
- 2. Middle (4-5 sentences):
275
- - Show you researched them (reference job description specifics)
276
- - Connect 1-2 resume achievements directly to their needs
277
- - Briefly mention the achievement below to prove capability
278
- 3. Closing (1-2 sentences): Express enthusiasm and leave door open
279
-
280
- Critical Requirements for Authenticity:
281
- - Write like a real person, NOT a template (varied sentence length, conversational where appropriate)
282
- - Show personality through word choiceβ€”confident but humble, professional but warm
283
- - Every claim must link to either the job description or the achievement below
284
- - Use specific details from the resume and job posting (shows real attention)
285
- - NO fluff, NO corporate jargon, NO redundancy
286
- - If something doesn't connect, don't force it
287
- - Sound like someone who actually wants this job, not just applying to any job
288
- - Do NOT mention salary expectations or benefits negotiations
289
-
290
- Formatting Requirements:
291
- - Start with "Dear Hiring Manager,"
292
- - End with: "Best,\nDhanvanth Voona" (Best on one line, name on next line)
293
- - Maximum 250 words (tight constraint = only include essential points)
294
- - NO PREAMBLE (begin directly with opening)
295
- - STRICTLY NO em dashes (use commas or separate sentences instead)
296
- - Single paragraphs are fine; multiple short paragraphs OK
297
-
298
- Context for Authentic Writing:
299
- Resume:
300
- {portfolio}
301
-
302
- Job Description:
303
- {job_description}{motivation_section}{achievement_section}
304
-
305
- Response (Max 250 words, genuine tone):"""
306
-
307
- client = OpenAI(api_key=api_key)
308
-
309
- completion = client.chat.completions.create(
310
- model="gpt-5-mini-2025-08-07",
311
- messages=[
312
- {
313
- "role": "user",
314
- "content": prompt
315
- }
316
- ]
317
- )
318
-
319
- response = completion.choices[0].message.content
320
- return response
321
-
322
-
323
- def handle_general_query(job_description, portfolio, query, length, api_key):
324
- """Handle General Query category using OpenAI"""
325
-
326
- word_count_map = {
327
- "short": "40-60",
328
- "medium": "80-100",
329
- "long": "120-150"
330
- }
331
-
332
- word_count = word_count_map.get(length, "40-60")
333
-
334
- prompt = f"""You are an expert career consultant helping a candidate answer application questions with authentic, tailored responses.
335
-
336
- Your task: Answer the query authentically using ONLY genuine connections between the candidate's experience and the job context.
337
-
338
- Word Count Strategy (Important - Read Carefully):
339
- - Target: {word_count} words MAXIMUM
340
- - Adaptive: Use fewer words if the query can be answered completely and convincingly with fewer words
341
- - Examples: "What is your greatest strength?" might need only 45 words. "Why our company?" needs 85-100 words to show genuine research
342
- - NEVER force content to hit word count targets - prioritize authentic connection over word count
343
-
344
- Connection Quality Guidelines:
345
- - Extract key company values/needs from job description
346
- - Find 1-2 direct experiences from resume that align with these
347
- - Show cause-and-effect: "Because you need X, my experience with Y makes me valuable"
348
- - If connection is weak or forced, acknowledge limitations honestly
349
- - Avoid generic statements - every sentence should reference either the job, company, or specific experience
350
-
351
- Requirements:
352
- - Answer naturally as if written by the candidate
353
- - Start directly with the answer (NO PREAMBLE or "Let me tell you...")
354
- - Response must be directly usable in an application
355
- - Make it engaging and personalized, not templated
356
- - STRICTLY NO EM DASHES
357
- - One authentic connection beats three forced ones
358
-
359
- Resume:
360
- {portfolio}
361
-
362
- Job Description:
363
- {job_description}
364
-
365
- Query:
366
- {query}
367
-
368
- Response (Max {word_count} words, use fewer if appropriate):"""
369
-
370
- client = OpenAI(api_key=api_key)
371
-
372
- completion = client.chat.completions.create(
373
- model="gpt-5-mini-2025-08-07",
374
- messages=[
375
- {
376
- "role": "user",
377
- "content": prompt
378
- }
379
- ]
380
- )
381
-
382
- response = completion.choices[0].message.content
383
- return response
384
-
385
-
386
- # Main input section
387
- st.header("πŸ“‹ Input Form")
388
-
389
- # Create columns for better layout
390
- col1, col2 = st.columns(2)
391
-
392
- with col1:
393
- job_description = st.text_area(
394
- "Job Description (Required)*",
395
- placeholder="Paste the job description here...",
396
- height=150
397
- )
398
-
399
- with col2:
400
- st.subheader("Options")
401
- resume_finder = st.checkbox("Resume Finder", value=False)
402
- cover_letter = st.checkbox("Cover Letter", value=False)
403
-
404
- # Length of Resume
405
- length_options = {
406
- "Short (40-60 words)": "short",
407
- "Medium (80-100 words)": "medium",
408
- "Long (120-150 words)": "long"
409
- }
410
- length_of_resume = st.selectbox(
411
- "Length of Response",
412
- options=list(length_options.keys()),
413
- index=0
414
- )
415
- length_value = length_options[length_of_resume]
416
-
417
- # Select Resume dropdown
418
- resume_options = ["No Select", "Resume_P", "Resume_Dss"]
419
- select_resume = st.selectbox(
420
- "Select Resume",
421
- options=resume_options,
422
- index=0
423
- )
424
-
425
- # Entry Query
426
- entry_query = st.text_area(
427
- "Entry Query (Optional)",
428
- placeholder="Ask any question related to your application...",
429
- max_chars=5000,
430
- height=100
431
- )
432
-
433
- # Submit button
434
- if st.button("πŸš€ Generate", type="primary", use_container_width=True):
435
- # Validate job description
436
- if not job_description.strip():
437
- st.error("❌ Job Description is required!")
438
- st.stop()
439
-
440
- # Categorize input
441
- category, error_message = categorize_input(
442
- resume_finder, cover_letter, select_resume, entry_query
443
- )
444
-
445
- if category == "retry":
446
- st.warning(f"⚠️ {error_message}")
447
- else:
448
- st.header("πŸ“€ Response")
449
-
450
- # Debug info (can be removed later)
451
- with st.expander("πŸ“Š Debug Info"):
452
- st.write(f"**Category:** {category}")
453
- st.write(f"**Resume Finder:** {resume_finder}")
454
- st.write(f"**Cover Letter:** {cover_letter}")
455
- st.write(f"**Select Resume:** {select_resume}")
456
- st.write(f"**Has Query:** {bool(entry_query.strip())}")
457
- st.write(f"**OpenAI API Key Set:** {'βœ… Yes' if openai_api_key else '❌ No'}")
458
- st.write(f"**OpenRouter API Key Set:** {'βœ… Yes' if openrouter_api_key else '❌ No'}")
459
- st.write(f"**OpenAI Key First 10 chars:** {openai_api_key[:10] + '...' if openai_api_key else 'N/A'}")
460
- st.write(f"**OpenRouter Key First 10 chars:** {openrouter_api_key[:10] + '...' if openrouter_api_key else 'N/A'}")
461
-
462
- # Load portfolios
463
- ai_portfolio = load_portfolio("AI_portfolio.md")
464
- ds_portfolio = load_portfolio("DS_portfolio.md")
465
-
466
- if ai_portfolio is None or ds_portfolio is None:
467
- st.stop()
468
-
469
- response = None
470
- error_occurred = None
471
-
472
- if category == "resume_finder":
473
- with st.spinner("πŸ” Finding the best resume for you..."):
474
- try:
475
- response = handle_resume_finder(
476
- job_description, ai_portfolio, ds_portfolio, openrouter_api_key
477
- )
478
- except Exception as e:
479
- error_occurred = f"Resume Finder Error: {str(e)}"
480
-
481
- elif category == "cover_letter":
482
- selected_portfolio = ai_portfolio if select_resume == "Resume_P" else ds_portfolio
483
-
484
- # Generate company motivation and achievement section
485
- st.info("πŸ” Analyzing company and generating personalized context with web search...")
486
- context_placeholder = st.empty()
487
-
488
- try:
489
- context_placeholder.info("πŸ“Š Generating company motivation and achievement section (with web search)...")
490
- context = generate_cover_letter_context(job_description, selected_portfolio, openrouter_api_key)
491
- company_motivation = context.get("company_motivation", "")
492
- specific_achievement = context.get("achievement_section", "")
493
- context_placeholder.success("βœ… Context generated successfully with web search!")
494
- except Exception as e:
495
- error_occurred = f"Context Generation Error: {str(e)}"
496
- context_placeholder.error(f"❌ Failed to generate context: {str(e)}")
497
- st.info("πŸ’‘ Proceeding with cover letter generation without auto-generated context...")
498
- company_motivation = ""
499
- specific_achievement = ""
500
-
501
- # Now generate the cover letter
502
- with st.spinner("✍️ Generating your cover letter..."):
503
- try:
504
- response = handle_cover_letter(
505
- job_description, selected_portfolio, openai_api_key,
506
- company_motivation=company_motivation,
507
- specific_achievement=specific_achievement
508
- )
509
- except Exception as e:
510
- error_occurred = f"Cover Letter Error: {str(e)}"
511
-
512
- elif category == "general_query":
513
- selected_portfolio = ai_portfolio if select_resume == "Resume_P" else ds_portfolio
514
- with st.spinner("πŸ’­ Crafting your response..."):
515
- try:
516
- response = handle_general_query(
517
- job_description, selected_portfolio, entry_query,
518
- length_value, openai_api_key
519
- )
520
- except Exception as e:
521
- error_occurred = f"General Query Error: {str(e)}"
522
-
523
- # Display error if one occurred
524
- if error_occurred:
525
- st.error(f"❌ {error_occurred}")
526
- st.info("πŸ’‘ **Troubleshooting Tips:**\n- Check your API keys in the .env file\n- Verify your API key has sufficient credits/permissions\n- Ensure the model name is correct for your API tier")
527
-
528
- # Store response in session state only if new response generated
529
- if response:
530
- st.session_state.edited_response = response
531
- st.session_state.editing = False
532
- elif not error_occurred:
533
- st.error("❌ Failed to generate response. Please check the error messages above and try again.")
534
-
535
- # Display stored response if available (persists across button clicks)
536
- if "edited_response" in st.session_state and st.session_state.edited_response:
537
- st.header("πŸ“€ Response")
538
-
539
- # Toggle edit mode
540
- col_response, col_buttons = st.columns([3, 1])
541
-
542
- with col_buttons:
543
- if st.button("✏️ Edit", key="edit_btn", use_container_width=True):
544
- st.session_state.editing = not st.session_state.editing
545
-
546
- # Display response or edit area
547
- if st.session_state.editing:
548
- st.session_state.edited_response = st.text_area(
549
- "Edit your response:",
550
- value=st.session_state.edited_response,
551
- height=250,
552
- key="response_editor"
553
- )
554
-
555
- col_save, col_cancel = st.columns(2)
556
- with col_save:
557
- if st.button("πŸ’Ύ Save Changes", use_container_width=True):
558
- st.session_state.editing = False
559
- st.success("βœ… Response updated!")
560
- st.rerun()
561
-
562
- with col_cancel:
563
- if st.button("❌ Cancel", use_container_width=True):
564
- st.session_state.editing = False
565
- st.rerun()
566
- else:
567
- # Display the response
568
- st.success(st.session_state.edited_response)
569
-
570
- # Download PDF button
571
- timestamp = get_est_timestamp()
572
- pdf_filename = f"Dhanvanth_{timestamp}.pdf"
573
-
574
- pdf_content = generate_pdf(st.session_state.edited_response, pdf_filename)
575
- if pdf_content:
576
- st.download_button(
577
- label="πŸ“₯ Download as PDF",
578
- data=pdf_content,
579
- file_name=pdf_filename,
580
- mime="application/pdf",
581
- use_container_width=True
582
- )
583
-
584
- st.markdown("---")
585
- st.markdown(
586
- "Say Hi to Griva thalli from her mama ❀️"
587
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from openai import OpenAI
3
+ import os
4
+ from dotenv import load_dotenv
5
+ from datetime import datetime
6
+ import pytz
7
+ from reportlab.lib.pagesizes import letter
8
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
9
+ from reportlab.lib.units import inch
10
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
11
+ from reportlab.lib.enums import TA_LEFT, TA_JUSTIFY
12
+ import io
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+
17
+ # Page configuration
18
+ st.set_page_config(page_title="AI Resume Assistant", layout="wide")
19
+ st.title("πŸ€– AI Resume Assistant")
20
+
21
+ # Load API keys from environment variables
22
+ openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
23
+ openai_api_key = os.getenv("OPENAI_API_KEY")
24
+
25
+ # Check if API keys are available
26
+ if not openrouter_api_key or not openai_api_key:
27
+ st.error("❌ API keys not found. Please set OPENROUTER_API_KEY and OPENAI_API_KEY in your environment variables (.env file).")
28
+ st.stop()
29
+
30
+ def get_est_timestamp():
31
+ """Get current timestamp in EST timezone with format dd-mm-yyyy-HH-MM"""
32
+ est = pytz.timezone('US/Eastern')
33
+ now = datetime.now(est)
34
+ return now.strftime("%d-%m-%Y-%H-%M")
35
+
36
+
37
+ def generate_pdf(content, filename):
38
+ """Generate PDF from content and return as bytes"""
39
+ try:
40
+ pdf_buffer = io.BytesIO()
41
+ doc = SimpleDocTemplate(
42
+ pdf_buffer,
43
+ pagesize=letter,
44
+ rightMargin=0.75*inch,
45
+ leftMargin=0.75*inch,
46
+ topMargin=0.75*inch,
47
+ bottomMargin=0.75*inch
48
+ )
49
+
50
+ story = []
51
+ styles = getSampleStyleSheet()
52
+
53
+ # Custom style for body text
54
+ body_style = ParagraphStyle(
55
+ 'CustomBody',
56
+ parent=styles['Normal'],
57
+ fontSize=11,
58
+ leading=14,
59
+ alignment=TA_JUSTIFY,
60
+ spaceAfter=12
61
+ )
62
+
63
+ # Add content only (no preamble)
64
+ # Split content into paragraphs for better formatting
65
+ paragraphs = content.split('\n\n')
66
+ for para in paragraphs:
67
+ if para.strip():
68
+ # Replace line breaks with spaces within paragraphs
69
+ clean_para = para.replace('\n', ' ').strip()
70
+ story.append(Paragraph(clean_para, body_style))
71
+
72
+ # Build PDF
73
+ doc.build(story)
74
+ pdf_buffer.seek(0)
75
+ return pdf_buffer.getvalue()
76
+
77
+ except Exception as e:
78
+ st.error(f"Error generating PDF: {str(e)}")
79
+ return None
80
+
81
+
82
+ def categorize_input(resume_finder, cover_letter, select_resume, entry_query):
83
+ """
84
+ Categorize input into one of 4 groups:
85
+ - resume_finder: T, F, No Select
86
+ - cover_letter: F, T, not No Select
87
+ - general_query: F, F, not No Select
88
+ - retry: any other combination
89
+ """
90
+
91
+ if resume_finder and not cover_letter and select_resume == "No Select":
92
+ return "resume_finder", None
93
+
94
+ elif not resume_finder and cover_letter and select_resume != "No Select":
95
+ return "cover_letter", None
96
+
97
+ elif not resume_finder and not cover_letter and select_resume != "No Select":
98
+ if not entry_query.strip():
99
+ return "retry", "Please enter a query for General Query mode."
100
+ return "general_query", None
101
+
102
+ else:
103
+ return "retry", "Please check your entries and try again"
104
+
105
+
106
+ def load_portfolio(file_path):
107
+ """Load portfolio markdown file"""
108
+ try:
109
+ full_path = os.path.join(os.path.dirname(__file__), file_path)
110
+ with open(full_path, 'r', encoding='utf-8') as f:
111
+ return f.read()
112
+ except FileNotFoundError:
113
+ st.error(f"Portfolio file {file_path} not found!")
114
+ return None
115
+
116
+
117
+ def handle_resume_finder(job_description, ai_portfolio, ds_portfolio, api_key):
118
+ """Handle Resume Finder category using OpenRouter"""
119
+
120
+ prompt = f"""You are an expert resume matcher. Analyze the following job description and two portfolios to determine which is the best match.
121
+
122
+ IMPORTANT MAPPING:
123
+ - If AI_portfolio is most relevant β†’ Resume = Resume_P
124
+ - If DS_portfolio is most relevant β†’ Resume = Resume_Dss
125
+
126
+ Job Description:
127
+ {job_description}
128
+
129
+ AI_portfolio (Maps to: Resume_P):
130
+ {ai_portfolio}
131
+
132
+ DS_portfolio (Maps to: Resume_Dss):
133
+ {ds_portfolio}
134
+
135
+ Respond ONLY with:
136
+ Resume: [Resume_P or Resume_Dss]
137
+ Reasoning: [25-30 words explaining the match]
138
+
139
+ NO PREAMBLE."""
140
+
141
+ try:
142
+ client = OpenAI(
143
+ base_url="https://openrouter.ai/api/v1",
144
+ api_key=api_key,
145
+ )
146
+
147
+ completion = client.chat.completions.create(
148
+ model="openai/gpt-oss-safeguard-20b",
149
+ messages=[
150
+ {
151
+ "role": "user",
152
+ "content": prompt
153
+ }
154
+ ]
155
+ )
156
+
157
+ response = completion.choices[0].message.content
158
+ if response:
159
+ return response
160
+ else:
161
+ st.error("❌ No response received from OpenRouter API")
162
+ return None
163
+
164
+ except Exception as e:
165
+ st.error(f"❌ Error calling OpenRouter API: {str(e)}")
166
+ return None
167
+
168
+
169
+ def generate_cover_letter_context(job_description, portfolio, api_key):
170
+ """Generate company research, role problem analysis, and achievement matching using web search via Perplexity Sonar
171
+
172
+ Args:
173
+ job_description: The job posting
174
+ portfolio: Candidate's resume/portfolio
175
+ api_key: OpenRouter API key (used for Perplexity Sonar with web search)
176
+
177
+ Returns:
178
+ dict: {"company_motivation": str, "role_problem": str, "achievement_section": str}
179
+ """
180
+
181
+ prompt = f"""You are an expert career strategist researching a company and role to craft authentic, researched-backed cover letter context.
182
+
183
+ Your task: Use web search to find SPECIFIC, RECENT company intelligence, then match it to the candidate's achievements.
184
+
185
+ REQUIRED WEB SEARCHES:
186
+ 1. Recent company moves (funding rounds, product launches, acquisitions, market expansion, hiring momentum)
187
+ 2. Current company challenges (what problem are they actively solving?)
188
+ 3. Company tech stack / tools they use
189
+ 4. Why they're hiring NOW (growth? new product? team expansion?)
190
+ 5. Company market position and strategy
191
+
192
+ Job Description:
193
+ {job_description}
194
+
195
+ Candidate's Portfolio:
196
+ {portfolio}
197
+
198
+ Generate a JSON response with this format (no additional text):
199
+ {{
200
+ "company_motivation": "2-3 sentences showing specific, researched interest. Reference recent company moves (funding, product launches, market position) OR specific challenge. Format: '[Company name] recently [specific move/challenge], and your focus on [specific need] aligns with my experience building [domain].' Avoid forced connectionsβ€”if authenticity is low, keep motivation minimal.",
201
+ "role_problem": "1 sentence defining CORE PROBLEM this role solves for company. Example: 'Improving demand forecasting accuracy for franchisee decision-making' OR 'Building production vision models under real-time latency constraints.'",
202
+ "achievement_section": "ONE specific achievement from portfolio solving role_problem (not just relevant). Format: 'Built [X] to solve [problem/constraint], achieving [metric] across [scale].' Example: 'Built self-serve ML agents (FastAPI+LangChain) to reduce business team dependency on Data Engineering by 60% across 150k+ samples.' This must map directly to role_problem."
203
+ }}
204
+
205
+ REQUIREMENTS FOR AUTHENTICITY:
206
+ - company_motivation: Must reference verifiable findings from web search (recent news, funding, product launch, specific challenge)
207
+ - role_problem: Explicitly state the core problem extracted from job description + company research
208
+ - achievement_section: Must map directly to role_problem with clear cause-effect (not just "relevant to job")
209
+ - NO FORCED CONNECTIONS: If no genuine connection exists between candidate achievement and role problem, return empty string rather than forcing weak match
210
+ - AUTHENTICITY PRIORITY: A short, genuine motivation beats a longer forced one. Minimize if needed to avoid template-feel.
211
+
212
+ Return ONLY the JSON object, no other text."""
213
+
214
+ # Use Perplexity Sonar via OpenRouter (has built-in web search)
215
+ client = OpenAI(
216
+ base_url="https://openrouter.ai/api/v1",
217
+ api_key=api_key,
218
+ )
219
+
220
+ completion = client.chat.completions.create(
221
+ model="perplexity/sonar",
222
+ messages=[
223
+ {
224
+ "role": "user",
225
+ "content": prompt
226
+ }
227
+ ]
228
+ )
229
+
230
+ response_text = completion.choices[0].message.content
231
+
232
+ # Parse JSON response
233
+ import json
234
+ try:
235
+ result = json.loads(response_text)
236
+ return {
237
+ "company_motivation": result.get("company_motivation", ""),
238
+ "role_problem": result.get("role_problem", ""),
239
+ "achievement_section": result.get("achievement_section", "")
240
+ }
241
+ except json.JSONDecodeError:
242
+ # Fallback if JSON parsing fails
243
+ return {
244
+ "company_motivation": "",
245
+ "role_problem": "",
246
+ "achievement_section": ""
247
+ }
248
+
249
+
250
+ def handle_cover_letter(job_description, portfolio, api_key, company_motivation="", role_problem="", specific_achievement=""):
251
+ """Handle Cover Letter category using OpenAI
252
+
253
+ Args:
254
+ job_description: The job posting
255
+ portfolio: Candidate's resume/portfolio
256
+ api_key: OpenAI API key
257
+ company_motivation: Researched company interest with recent moves/challenges (auto-generated if empty)
258
+ role_problem: The core problem this role solves for the company (auto-generated if empty)
259
+ specific_achievement: One concrete achievement that solves role_problem (auto-generated if empty)
260
+ """
261
+
262
+ # Build context sections if provided
263
+ motivation_section = ""
264
+ if company_motivation.strip():
265
+ motivation_section = f"\nCompany Research (Recent Moves/Challenges):\n{company_motivation}"
266
+
267
+ problem_section = ""
268
+ if role_problem.strip():
269
+ problem_section = f"\nRole's Core Problem:\n{role_problem}"
270
+
271
+ achievement_section = ""
272
+ if specific_achievement.strip():
273
+ achievement_section = f"\nAchievement That Solves This Problem:\n{specific_achievement}"
274
+
275
+ prompt = f"""You are an expert career coach writing authentic, researched cover letters that prove specific company knowledge and solve real problems.
276
+
277
+ Your goal: Write a letter showing you researched THIS company (not a template) and authentically connect your achievements to THEIR specific problem.
278
+
279
+ CRITICAL FOUNDATION:
280
+ You have three inputs: company research (recent moves/challenges), role problem (what they're hiring to solve), and one matching achievement.
281
+ Construct narrative: "Because you [company context] need to [role problem], my experience with [achievement] makes me valuable."
282
+
283
+ Cover Letter Structure:
284
+ 1. Opening (2-3 sentences): Hook with SPECIFIC company research (recent move, funding, product, market challenge)
285
+ - NOT: "I'm interested in your company"
286
+ - YES: "Your recent expansion to [X markets] and focus on [tech] align with my experience"
287
+
288
+ 2. Middle (4-5 sentences):
289
+ - State role's core problem (what you understand they're hiring to solve)
290
+ - Connect achievement DIRECTLY to that problem (show cause-effect)
291
+ - Reference job description specifics your achievement addresses
292
+ - Show understanding of their constraint/challenge
293
+
294
+ 3. Closing (1-2 sentences): Express genuine enthusiasm about solving THIS specific problem
295
+
296
+ CRITICAL REQUIREMENTS:
297
+ - RESEARCH PROOF: Opening must show specific company knowledge (recent news, not generic mission)
298
+ - PROBLEM CLARITY: Explicitly state what problem you're solving for them
299
+ - SPECIFIC MAPPING: Achievement β†’ Role Problem β†’ Company Need (clear cause-effect chain)
300
+ - NO TEMPLATE: Varied sentence length, conversational tone, human voice
301
+ - NO FORCED CONNECTIONS: If something doesn't link cleanly, leave it out
302
+ - NO FLUFF: Every sentence serves a purpose (authentic < complete)
303
+ - NO SALARY TALK: Omit expectations or negotiations
304
+ - NO CORPORATE JARGON: Write like a real human
305
+ - NO EM DASHES: Use commas or separate sentences
306
+
307
+ Formatting:
308
+ - Start: "Dear Hiring Manager,"
309
+ - End: "Best,\nDhanvanth Voona" (on separate lines)
310
+ - Max 250 words
311
+ - NO PREAMBLE (start directly)
312
+ - Multiple short paragraphs OK
313
+
314
+ Context for Writing:
315
+ Resume:
316
+ {portfolio}
317
+
318
+ Job Description:
319
+ {job_description}{motivation_section}{problem_section}{achievement_section}
320
+
321
+ Response (Max 250 words, researched + authentic tone):"""
322
+
323
+ client = OpenAI(api_key=api_key)
324
+
325
+ completion = client.chat.completions.create(
326
+ model="gpt-5-mini-2025-08-07",
327
+ messages=[
328
+ {
329
+ "role": "user",
330
+ "content": prompt
331
+ }
332
+ ]
333
+ )
334
+
335
+ response = completion.choices[0].message.content
336
+ return response
337
+
338
+
339
+ def handle_general_query(job_description, portfolio, query, length, api_key):
340
+ """Handle General Query category using OpenAI"""
341
+
342
+ word_count_map = {
343
+ "short": "40-60",
344
+ "medium": "80-100",
345
+ "long": "120-150"
346
+ }
347
+
348
+ word_count = word_count_map.get(length, "40-60")
349
+
350
+ prompt = f"""You are an expert career consultant helping a candidate answer application questions with authentic, tailored responses.
351
+
352
+ Your task: Answer the query authentically using ONLY genuine connections between the candidate's experience and the job context.
353
+
354
+ Word Count Strategy (Important - Read Carefully):
355
+ - Target: {word_count} words MAXIMUM
356
+ - Adaptive: Use fewer words if the query can be answered completely and convincingly with fewer words
357
+ - Examples: "What is your greatest strength?" might need only 45 words. "Why our company?" needs 85-100 words to show genuine research
358
+ - NEVER force content to hit word count targets - prioritize authentic connection over word count
359
+
360
+ Connection Quality Guidelines:
361
+ - Extract key company values/needs from job description
362
+ - Find 1-2 direct experiences from resume that align with these
363
+ - Show cause-and-effect: "Because you need X, my experience with Y makes me valuable"
364
+ - If connection is weak or forced, acknowledge limitations honestly
365
+ - Avoid generic statements - every sentence should reference either the job, company, or specific experience
366
+
367
+ Requirements:
368
+ - Answer naturally as if written by the candidate
369
+ - Start directly with the answer (NO PREAMBLE or "Let me tell you...")
370
+ - Response must be directly usable in an application
371
+ - Make it engaging and personalized, not templated
372
+ - STRICTLY NO EM DASHES
373
+ - One authentic connection beats three forced ones
374
+
375
+ Resume:
376
+ {portfolio}
377
+
378
+ Job Description:
379
+ {job_description}
380
+
381
+ Query:
382
+ {query}
383
+
384
+ Response (Max {word_count} words, use fewer if appropriate):"""
385
+
386
+ client = OpenAI(api_key=api_key)
387
+
388
+ completion = client.chat.completions.create(
389
+ model="gpt-5-mini-2025-08-07",
390
+ messages=[
391
+ {
392
+ "role": "user",
393
+ "content": prompt
394
+ }
395
+ ]
396
+ )
397
+
398
+ response = completion.choices[0].message.content
399
+ return response
400
+
401
+
402
+ # Main input section
403
+ st.header("πŸ“‹ Input Form")
404
+
405
+ # Create columns for better layout
406
+ col1, col2 = st.columns(2)
407
+
408
+ with col1:
409
+ job_description = st.text_area(
410
+ "Job Description (Required)*",
411
+ placeholder="Paste the job description here...",
412
+ height=150
413
+ )
414
+
415
+ with col2:
416
+ st.subheader("Options")
417
+ resume_finder = st.checkbox("Resume Finder", value=False)
418
+ cover_letter = st.checkbox("Cover Letter", value=False)
419
+
420
+ # Length of Resume
421
+ length_options = {
422
+ "Short (40-60 words)": "short",
423
+ "Medium (80-100 words)": "medium",
424
+ "Long (120-150 words)": "long"
425
+ }
426
+ length_of_resume = st.selectbox(
427
+ "Length of Resume",
428
+ options=list(length_options.keys()),
429
+ index=0
430
+ )
431
+ length_value = length_options[length_of_resume]
432
+
433
+ # Select Resume dropdown
434
+ resume_options = ["No Select", "Resume_P", "Resume_Dss"]
435
+ select_resume = st.selectbox(
436
+ "Select Resume",
437
+ options=resume_options,
438
+ index=0
439
+ )
440
+
441
+ # Entry Query
442
+ entry_query = st.text_area(
443
+ "Entry Query (Optional)",
444
+ placeholder="Ask any question related to your application...",
445
+ max_chars=5000,
446
+ height=100
447
+ )
448
+
449
+ # Submit button
450
+ if st.button("πŸš€ Generate", type="primary", use_container_width=True):
451
+ # Validate job description
452
+ if not job_description.strip():
453
+ st.error("❌ Job Description is required!")
454
+ st.stop()
455
+
456
+ # Categorize input
457
+ category, error_message = categorize_input(
458
+ resume_finder, cover_letter, select_resume, entry_query
459
+ )
460
+
461
+ if category == "retry":
462
+ st.warning(f"⚠️ {error_message}")
463
+ else:
464
+ st.header("πŸ“€ Response")
465
+
466
+ # Debug info (can be removed later)
467
+ with st.expander("πŸ“Š Debug Info"):
468
+ st.write(f"**Category:** {category}")
469
+ st.write(f"**Resume Finder:** {resume_finder}")
470
+ st.write(f"**Cover Letter:** {cover_letter}")
471
+ st.write(f"**Select Resume:** {select_resume}")
472
+ st.write(f"**Has Query:** {bool(entry_query.strip())}")
473
+ st.write(f"**OpenAI API Key Set:** {'βœ… Yes' if openai_api_key else '❌ No'}")
474
+ st.write(f"**OpenRouter API Key Set:** {'βœ… Yes' if openrouter_api_key else '❌ No'}")
475
+ st.write(f"**OpenAI Key First 10 chars:** {openai_api_key[:10] + '...' if openai_api_key else 'N/A'}")
476
+ st.write(f"**OpenRouter Key First 10 chars:** {openrouter_api_key[:10] + '...' if openrouter_api_key else 'N/A'}")
477
+
478
+ # Load portfolios
479
+ ai_portfolio = load_portfolio("AI_portfolio.md")
480
+ ds_portfolio = load_portfolio("DS_portfolio.md")
481
+
482
+ if ai_portfolio is None or ds_portfolio is None:
483
+ st.stop()
484
+
485
+ response = None
486
+ error_occurred = None
487
+
488
+ if category == "resume_finder":
489
+ with st.spinner("πŸ” Finding the best resume for you..."):
490
+ try:
491
+ response = handle_resume_finder(
492
+ job_description, ai_portfolio, ds_portfolio, openrouter_api_key
493
+ )
494
+ except Exception as e:
495
+ error_occurred = f"Resume Finder Error: {str(e)}"
496
+
497
+ elif category == "cover_letter":
498
+ selected_portfolio = ai_portfolio if select_resume == "Resume_P" else ds_portfolio
499
+
500
+ # Generate company motivation and achievement section
501
+ st.info("πŸ” Analyzing company and generating personalized context with web search...")
502
+ context_placeholder = st.empty()
503
+
504
+ try:
505
+ context_placeholder.info("πŸ“Š Researching company, analyzing role, and matching achievements (with web search)...")
506
+ context = generate_cover_letter_context(job_description, selected_portfolio, openrouter_api_key)
507
+ company_motivation = context.get("company_motivation", "")
508
+ role_problem = context.get("role_problem", "")
509
+ specific_achievement = context.get("achievement_section", "")
510
+ context_placeholder.success("βœ… Company research and achievement matching complete!")
511
+ except Exception as e:
512
+ error_occurred = f"Context Generation Error: {str(e)}"
513
+ context_placeholder.error(f"❌ Failed to generate context: {str(e)}")
514
+ st.info("πŸ’‘ Proceeding with cover letter generation without auto-generated context...")
515
+ company_motivation = ""
516
+ role_problem = ""
517
+ specific_achievement = ""
518
+
519
+ # Now generate the cover letter
520
+ with st.spinner("✍️ Generating your cover letter..."):
521
+ try:
522
+ response = handle_cover_letter(
523
+ job_description, selected_portfolio, openai_api_key,
524
+ company_motivation=company_motivation,
525
+ role_problem=role_problem,
526
+ specific_achievement=specific_achievement
527
+ )
528
+ except Exception as e:
529
+ error_occurred = f"Cover Letter Error: {str(e)}"
530
+
531
+ elif category == "general_query":
532
+ selected_portfolio = ai_portfolio if select_resume == "Resume_P" else ds_portfolio
533
+ with st.spinner("πŸ’­ Crafting your response..."):
534
+ try:
535
+ response = handle_general_query(
536
+ job_description, selected_portfolio, entry_query,
537
+ length_value, openai_api_key
538
+ )
539
+ except Exception as e:
540
+ error_occurred = f"General Query Error: {str(e)}"
541
+
542
+ # Display error if one occurred
543
+ if error_occurred:
544
+ st.error(f"❌ {error_occurred}")
545
+ st.info("πŸ’‘ **Troubleshooting Tips:**\n- Check your API keys in the .env file\n- Verify your API key has sufficient credits/permissions\n- Ensure the model name is correct for your API tier")
546
+
547
+ # Store response in session state only if new response generated
548
+ if response:
549
+ st.session_state.edited_response = response
550
+ st.session_state.editing = False
551
+ elif not error_occurred:
552
+ st.error("❌ Failed to generate response. Please check the error messages above and try again.")
553
+
554
+ # Display stored response if available (persists across button clicks)
555
+ if "edited_response" in st.session_state and st.session_state.edited_response:
556
+ st.header("πŸ“€ Response")
557
+
558
+ # Toggle edit mode
559
+ col_response, col_buttons = st.columns([3, 1])
560
+
561
+ with col_buttons:
562
+ if st.button("✏️ Edit", key="edit_btn", use_container_width=True):
563
+ st.session_state.editing = not st.session_state.editing
564
+
565
+ # Display response or edit area
566
+ if st.session_state.editing:
567
+ st.session_state.edited_response = st.text_area(
568
+ "Edit your response:",
569
+ value=st.session_state.edited_response,
570
+ height=250,
571
+ key="response_editor"
572
+ )
573
+
574
+ col_save, col_cancel = st.columns(2)
575
+ with col_save:
576
+ if st.button("πŸ’Ύ Save Changes", use_container_width=True):
577
+ st.session_state.editing = False
578
+ st.success("βœ… Response updated!")
579
+ st.rerun()
580
+
581
+ with col_cancel:
582
+ if st.button("❌ Cancel", use_container_width=True):
583
+ st.session_state.editing = False
584
+ st.rerun()
585
+ else:
586
+ # Display the response
587
+ st.success(st.session_state.edited_response)
588
+
589
+ # Download PDF button
590
+ timestamp = get_est_timestamp()
591
+ pdf_filename = f"Dhanvanth_{timestamp}.pdf"
592
+
593
+ pdf_content = generate_pdf(st.session_state.edited_response, pdf_filename)
594
+ if pdf_content:
595
+ st.download_button(
596
+ label="πŸ“₯ Download as PDF",
597
+ data=pdf_content,
598
+ file_name=pdf_filename,
599
+ mime="application/pdf",
600
+ use_container_width=True
601
+ )
602
+
603
+ st.markdown("---")
604
+ st.markdown(
605
+ "Say Hi to Griva thalli from her mama ❀️"
606
+ )