-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathmain.py
More file actions
428 lines (363 loc) · 14.7 KB
/
Copy pathmain.py
File metadata and controls
428 lines (363 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
#!/usr/bin/env python
"""
Job Search CLI - Command line interface for searching jobs from multiple platforms.
This script provides a command-line interface to search for jobs on:
- LinkedIn
- Indeed
- Glassdoor
It uses Google Search with specialized site-specific queries to find job listings
without requiring paid API access to these platforms.
"""
import os
import json
import sys
import argparse
from typing import List, Any, Optional
from src.utils.file_utils import slugify
from src.agents.cv_writer import call_cv_agent
from src.agents.coverLetter_writer import call_cover_letter_agent
from src.agents.job_details_parser import call_job_parsr_agent
from src.utils.job_search_pipeline import run_job_search
from src.utils.document_database import DocumentStorage
def process_jobs(
json_path: str,
output_dir: str = "output",
generate_cv: bool = False,
generate_cover_letter: bool = False,
) -> None:
"""
Load jobs from a JSON file and generate CVs and/or cover letters for each job.
Args:
json_path: Path to the JSON file containing job listings
output_dir: Directory to save output files
generate_cv: Whether to generate custom CVs
generate_cover_letter: Whether to generate cover letters
"""
print("📋 Loading job postings from JSON file...")
with open(json_path, "r", encoding="utf-8") as f:
jobs = json.load(f)
# Create a list if jobs is a single job
if isinstance(jobs, dict):
jobs = [jobs]
print(f"📊 Found {len(jobs)} job postings to process")
for i, job in enumerate(jobs):
# Create a folder for this job
company = job.get("company_name", "")
job_title = job.get("job_title", "")
print(f"\n🔹 Processing job {i + 1}/{len(jobs)}: {job_title} at {company}")
# Slugify both company and job title for consistent file naming
company_slug = slugify(company)
job_title_slug = slugify(job_title)
folder_name = os.path.join(output_dir, slugify(f"{company}_{job_title}"))
os.makedirs(folder_name, exist_ok=True)
print(f"📁 Created folder: {folder_name}")
# Save job details
print(
f"💾 Saving job details to {folder_name}/{company_slug}_{job_title_slug}_metadata.json"
)
with open(
os.path.join(folder_name, f"{company_slug}_{job_title_slug}_metadata.json"),
"w",
encoding="utf-8",
) as detail_file:
json.dump(job, detail_file, indent=2)
job_details_str = json.dumps(job)
# Generate custom CV if requested
if generate_cv:
print(f"🤖 Starting CV generation for {job_title}...")
try:
cv_text, state_json, cv_path = call_cv_agent(job_details_str)
# Store CV in database
import time
process_id = f"process_{time.strftime('%Y%m%d_%H%M%S')}_{i}"
cv_id = DocumentStorage.store_cv(
content=cv_text,
job_posting=job,
process_id=process_id,
state_json=state_json,
template_used="cv_template.txt",
)
print(f"✅ CV generated and stored in database (ID: {cv_id})")
# Also save to file for backward compatibility
cv_file_path = os.path.join(
folder_name, f"{company_slug}_{job_title_slug}_cv.txt"
)
with open(cv_file_path, "w", encoding="utf-8") as cv_file:
cv_file.write(cv_text)
print(f"📄 CV also saved to file: {cv_file_path}")
except Exception as e:
print(f"❌ Error generating CV for {folder_name}: {e}")
# Generate cover letter if requested
if generate_cover_letter:
print(f"📝 Starting cover letter generation for {job_title}...")
try:
cover_letter_text, cl_state_json, cl_path = call_cover_letter_agent(
job_details_str
)
# Store cover letter in database
import time
process_id = f"process_{time.strftime('%Y%m%d_%H%M%S')}_{i}"
cl_id = DocumentStorage.store_cover_letter(
content=cover_letter_text,
job_posting=job,
process_id=process_id,
state_json=cl_state_json,
template_used="cover_letter_template.txt",
)
print(f"✅ Cover letter generated and stored in database (ID: {cl_id})")
# Also save to file for backward compatibility
cl_file_path = os.path.join(
folder_name, f"{company_slug}_{job_title_slug}_cover_letter.txt"
)
with open(cl_file_path, "w", encoding="utf-8") as cl_file:
cl_file.write(cover_letter_text)
print(f"📄 Cover letter also saved to file: {cl_file_path}")
except Exception as e:
print(f"❌ Error generating cover letter for {folder_name}: {e}")
# Fallback to placeholder if generation fails
cover_letter_text = (
f"Cover letter placeholder for {job_title} at {company}\n"
)
cl_file_path = os.path.join(
folder_name, f"{company_slug}_{job_title_slug}_cover_letter.txt"
)
with open(cl_file_path, "w", encoding="utf-8") as cl_file:
cl_file.write(cover_letter_text)
print(f"📄 Placeholder cover letter saved to: {cl_file_path}")
print("\n✅ All jobs processed successfully!")
print(f"📂 Metadata files available in '{output_dir}' directory")
print("💾 All documents stored in database - use API endpoints to access them")
def parse_job_postings(text: Optional[str] = None, **kwargs) -> None:
"""
Parse job postings from text and store them in the database.
Optionally save to file if output_file is specified.
"""
input_file = kwargs.get("input_file", None)
if input_file:
with open(input_file, "r", encoding="utf-8") as f:
text = f.read()
if not text:
print("❌ No text provided for parsing. Exiting.")
return
# Run the Job Parser agent
print("🔍 Parsing job postings...")
job_postings_json = call_job_parsr_agent(text)
try:
job_postings = json.loads(job_postings_json)
jobs = job_postings if isinstance(job_postings, list) else [job_postings]
# Store parsed jobs in database
from src.utils.job_database import JobDatabase
db = JobDatabase()
try:
stored_count = 0
for job in jobs:
if db.add_job(job):
stored_count += 1
print(f"✅ Successfully stored {stored_count} job(s) in database")
finally:
db.close()
# Save to file only if specifically requested
output_file = kwargs.get("output_file", None)
if output_file:
# if the output file specified is not in the output directory, create the directory
if not os.path.exists(os.path.dirname(output_file)):
os.makedirs(os.path.dirname(output_file), exist_ok=True)
print(f"📂 Created output directory: {os.path.dirname(output_file)}")
with open(output_file, "w", encoding="utf-8") as f:
json.dump(jobs, f, indent=2)
print(f"💾 Parsed job postings also saved to {output_file}")
else:
print("💾 Job postings stored in database only")
except json.JSONDecodeError as e:
print(f"❌ Error parsing job postings JSON: {e}")
print("📋 Raw output:")
print(job_postings_json)
def parse_arguments():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Search for jobs on LinkedIn, Indeed, and Glassdoor without using APIs",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
# Create subparsers for different commands
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Search command
search_parser = subparsers.add_parser("search", help="Search for jobs")
search_parser.add_argument(
"keywords",
help='Job title or keywords to search for (e.g., "Python Developer, Mechanical Engineer")',
)
search_parser.add_argument(
"-l",
"--locations",
nargs="+",
default=["Remote"],
help='Locations to search in (e.g., "New York" "London" "Remote")',
)
search_parser.add_argument(
"-t",
"--job-type",
default="full-time",
choices=["full-time", "part-time", "contract", "internship", "any"],
help="Type of job",
)
search_parser.add_argument(
"-e",
"--experience",
default="mid-level",
choices=["entry", "mid-level", "senior", "any"],
help="Experience level required",
)
search_parser.add_argument(
"-m",
"--max-jobs",
type=int,
default=3,
help="Maximum number of jobs to fetch per site and location",
)
search_parser.add_argument(
"-o", "--output-dir", default="jobs", help="Directory to save results"
)
search_parser.add_argument(
"-c",
"--generate-cv",
action="store_true",
help="Generate custom CVs for all found jobs",
)
search_parser.add_argument(
"-cl",
"--generate-cover-letter",
action="store_true",
help="Generate cover letters for all found jobs",
)
# Process command
process_parser = subparsers.add_parser(
"process", help="Process jobs from a JSON file"
)
process_parser.add_argument("json_file", help="JSON file containing job listings")
process_parser.add_argument(
"-o", "--output-dir", default="output", help="Directory to save processed jobs"
)
process_parser.add_argument(
"-c",
"--generate-cv",
action="store_true",
default=True,
help="Generate custom CVs (default: True)",
)
process_parser.add_argument(
"-cl",
"--generate-cover-letter",
action="store_true",
help="Generate cover letters for all jobs",
)
process_parser.add_argument(
"--no-cv", action="store_true", help="Skip CV generation"
)
# Parse command
parse_parser = subparsers.add_parser("parse", help="Parse job details from text")
parse_parser.add_argument(
"-i", "--input-file", help="Text file with job details to parse"
)
parse_parser.add_argument(
"-o", "--output-file", help="JSON file to save parsed job details"
)
parse_parser.add_argument(
"-t", "--text", help="Direct text to parse instead of input file"
)
return parser.parse_args()
def main():
"""Main function to run the job search CLI."""
args = parse_arguments()
if args.command == "search":
print(f"\n🔎 Searching for: {args.keywords}")
print(f"📍 Locations: {', '.join(args.locations)}")
print(f"💼 Job type: {args.job_type}")
print(f"📊 Experience level: {args.experience}")
print(f"🔢 Max jobs per site/location: {args.max_jobs}")
try:
# Run the job search pipeline
output_file = run_job_search(
keywords=args.keywords,
locations=args.locations,
job_type=args.job_type,
experience_level=args.experience,
max_jobs=args.max_jobs,
)
print("\n✅ Job search completed successfully!")
print(f"💾 Results saved to: {output_file}")
# Generate documents if requested
if args.generate_cv or args.generate_cover_letter:
print("\n🚀 Starting document generation for all found jobs...")
process_jobs(
output_file,
generate_cv=args.generate_cv,
generate_cover_letter=args.generate_cover_letter,
)
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return 1
elif args.command == "process":
try:
print(f"\n📋 Processing jobs from: {args.json_file}")
generate_cv = args.generate_cv and not args.no_cv
process_jobs(
args.json_file,
args.output_dir,
generate_cv=generate_cv,
generate_cover_letter=args.generate_cover_letter,
)
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return 1
elif args.command == "parse":
try:
if args.text:
parse_job_postings(text=args.text, output_file=args.output_file)
elif args.input_file:
parse_job_postings(
input_file=args.input_file, output_file=args.output_file
)
else:
print(
"❌ Error: Either text or input file must be provided for parsing"
)
return 1
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return 1
elif args.command == "single":
try:
generate_cv = args.generate_cv and not args.no_cv
# Create a job object from the single job parameters
job_info = {
"job_title": args.title,
"company_name": args.company,
"job_description": args.description,
}
# Create a temporary JSON file for the single job
temp_file = os.path.join("output", "temp_single_job.json")
os.makedirs("output", exist_ok=True)
with open(temp_file, "w", encoding="utf-8") as f:
json.dump(job_info, f, indent=2)
# Process the job using the process_jobs function
print(f"🔹 Processing single job: {args.title} at {args.company}")
process_jobs(
temp_file,
output_dir="output",
generate_cv=generate_cv,
generate_cover_letter=args.generate_cover_letter,
)
# Clean up the temporary file
try:
os.remove(temp_file)
except Exception:
pass
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return 1
else:
print("❌ No command specified. Use --help for usage information.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())