omgy commited on
Commit
2d9120a
·
verified ·
1 Parent(s): aa4596d

Upload 3 files

Browse files
utils/docx_generator.py ADDED
@@ -0,0 +1,879 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DOCX Generator Utility
3
+ Handles creation of Word documents (.docx) for all template types
4
+ """
5
+
6
+ import io
7
+ import sys
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from docx import Document
12
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
13
+ from docx.oxml import OxmlElement
14
+ from docx.oxml.ns import qn
15
+ from docx.shared import Inches, Pt, RGBColor
16
+
17
+
18
+ class DocxGenerator:
19
+ """Generate professional Word documents"""
20
+
21
+ def __init__(self):
22
+ """Initialize DOCX generator"""
23
+ self.default_font = "Calibri"
24
+ self.heading_font = "Arial"
25
+
26
+ def _set_cell_border(self, cell, **kwargs):
27
+ """
28
+ Set cell border
29
+
30
+ Args:
31
+ cell: Table cell
32
+ kwargs: Border properties (top, bottom, left, right)
33
+ """
34
+ tc = cell._tc
35
+ tcPr = tc.get_or_add_tcPr()
36
+
37
+ tcBorders = OxmlElement("w:tcBorders")
38
+ for edge in ("left", "top", "right", "bottom"):
39
+ if edge in kwargs:
40
+ edge_data = kwargs.get(edge)
41
+ edge_el = OxmlElement(f"w:{edge}")
42
+ edge_el.set(qn("w:val"), "single")
43
+ edge_el.set(qn("w:sz"), "4")
44
+ edge_el.set(qn("w:space"), "0")
45
+ edge_el.set(qn("w:color"), edge_data.get("color", "000000"))
46
+ tcBorders.append(edge_el)
47
+
48
+ tcPr.append(tcBorders)
49
+
50
+ def _add_heading(self, doc: Document, text: str, level: int = 1):
51
+ """Add styled heading to document"""
52
+ heading = doc.add_heading(text, level=level)
53
+ heading.style.font.name = self.heading_font
54
+ heading.style.font.color.rgb = RGBColor(0, 51, 102) # Dark blue
55
+ return heading
56
+
57
+ def _add_paragraph(
58
+ self,
59
+ doc: Document,
60
+ text: str,
61
+ bold: bool = False,
62
+ italic: bool = False,
63
+ size: int = 11,
64
+ ):
65
+ """Add styled paragraph to document"""
66
+ para = doc.add_paragraph()
67
+ run = para.add_run(text)
68
+ run.font.name = self.default_font
69
+ run.font.size = Pt(size)
70
+ run.bold = bold
71
+ run.italic = italic
72
+ return para
73
+
74
+ def generate_resume(self, data: Dict[str, Any]) -> io.BytesIO:
75
+ """
76
+ Generate resume document
77
+
78
+ Args:
79
+ data: Resume data containing:
80
+ - personal_info: name, email, phone, location, linkedin, website
81
+ - summary: Professional summary
82
+ - experience: List of work experiences
83
+ - education: List of education entries
84
+ - skills: List of skills
85
+ - certifications: List of certifications (optional)
86
+ - projects: List of projects (optional)
87
+
88
+ Returns:
89
+ BytesIO buffer containing the .docx file
90
+ """
91
+ doc = Document()
92
+
93
+ # Set margins
94
+ sections = doc.sections
95
+ for section in sections:
96
+ section.top_margin = Inches(0.5)
97
+ section.bottom_margin = Inches(0.5)
98
+ section.left_margin = Inches(0.75)
99
+ section.right_margin = Inches(0.75)
100
+
101
+ # Personal Information (Header)
102
+ personal = data.get("personal_info", {})
103
+
104
+ # Name (Large and centered)
105
+ name_para = doc.add_paragraph()
106
+ name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
107
+ name_run = name_para.add_run(personal.get("name", "Your Name"))
108
+ name_run.font.name = self.heading_font
109
+ name_run.font.size = Pt(24)
110
+ name_run.bold = True
111
+ name_run.font.color.rgb = RGBColor(0, 51, 102)
112
+
113
+ # Contact Info (Centered)
114
+ contact_para = doc.add_paragraph()
115
+ contact_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
116
+ contact_parts = []
117
+ if personal.get("email"):
118
+ contact_parts.append(personal["email"])
119
+ if personal.get("phone"):
120
+ contact_parts.append(personal["phone"])
121
+ if personal.get("location"):
122
+ contact_parts.append(personal["location"])
123
+
124
+ contact_run = contact_para.add_run(" | ".join(contact_parts))
125
+ contact_run.font.name = self.default_font
126
+ contact_run.font.size = Pt(10)
127
+
128
+ # Links (Centered)
129
+ if personal.get("linkedin") or personal.get("website"):
130
+ links_para = doc.add_paragraph()
131
+ links_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
132
+ links_parts = []
133
+ if personal.get("linkedin"):
134
+ links_parts.append(f"LinkedIn: {personal['linkedin']}")
135
+ if personal.get("website"):
136
+ links_parts.append(f"Portfolio: {personal['website']}")
137
+
138
+ links_run = links_para.add_run(" | ".join(links_parts))
139
+ links_run.font.name = self.default_font
140
+ links_run.font.size = Pt(10)
141
+ links_run.font.color.rgb = RGBColor(0, 102, 204)
142
+
143
+ doc.add_paragraph() # Spacing
144
+
145
+ # Professional Summary
146
+ if data.get("summary"):
147
+ self._add_heading(doc, "PROFESSIONAL SUMMARY", level=1)
148
+ self._add_paragraph(doc, data["summary"])
149
+ doc.add_paragraph() # Spacing
150
+
151
+ # Work Experience
152
+ if data.get("experience"):
153
+ self._add_heading(doc, "WORK EXPERIENCE", level=1)
154
+
155
+ for exp in data["experience"]:
156
+ # Job Title and Company
157
+ title_para = doc.add_paragraph()
158
+ title_run = title_para.add_run(exp.get("title", "Position"))
159
+ title_run.font.name = self.default_font
160
+ title_run.font.size = Pt(12)
161
+ title_run.bold = True
162
+
163
+ # Company and Dates
164
+ company_para = doc.add_paragraph()
165
+ company_run = company_para.add_run(
166
+ f"{exp.get('company', 'Company')} | "
167
+ f"{exp.get('start_date', 'Start')} - {exp.get('end_date', 'End')}"
168
+ )
169
+ company_run.font.name = self.default_font
170
+ company_run.font.size = Pt(11)
171
+ company_run.italic = True
172
+
173
+ # Location
174
+ if exp.get("location"):
175
+ location_run = company_para.add_run(f" | {exp['location']}")
176
+ location_run.font.name = self.default_font
177
+ location_run.font.size = Pt(11)
178
+ location_run.italic = True
179
+
180
+ # Responsibilities/Achievements
181
+ if exp.get("responsibilities"):
182
+ for resp in exp["responsibilities"]:
183
+ bullet_para = doc.add_paragraph(resp, style="List Bullet")
184
+ bullet_para.paragraph_format.left_indent = Inches(0.25)
185
+
186
+ doc.add_paragraph() # Spacing between jobs
187
+
188
+ # Education
189
+ if data.get("education"):
190
+ self._add_heading(doc, "EDUCATION", level=1)
191
+
192
+ for edu in data["education"]:
193
+ # Degree
194
+ degree_para = doc.add_paragraph()
195
+ degree_run = degree_para.add_run(
196
+ f"{edu.get('degree', 'Degree')} in {edu.get('field', 'Field')}"
197
+ )
198
+ degree_run.font.name = self.default_font
199
+ degree_run.font.size = Pt(12)
200
+ degree_run.bold = True
201
+
202
+ # School and Dates
203
+ school_para = doc.add_paragraph()
204
+ school_run = school_para.add_run(
205
+ f"{edu.get('school', 'School')} | "
206
+ f"{edu.get('graduation_date', 'Graduation Date')}"
207
+ )
208
+ school_run.font.name = self.default_font
209
+ school_run.font.size = Pt(11)
210
+ school_run.italic = True
211
+
212
+ # GPA or Honors
213
+ if edu.get("gpa") or edu.get("honors"):
214
+ details = []
215
+ if edu.get("gpa"):
216
+ details.append(f"GPA: {edu['gpa']}")
217
+ if edu.get("honors"):
218
+ details.append(edu["honors"])
219
+
220
+ details_para = doc.add_paragraph(" | ".join(details))
221
+ details_para.paragraph_format.left_indent = Inches(0.25)
222
+
223
+ doc.add_paragraph() # Spacing
224
+
225
+ # Skills
226
+ if data.get("skills"):
227
+ self._add_heading(doc, "SKILLS", level=1)
228
+
229
+ # Group skills by category if provided
230
+ if isinstance(data["skills"], dict):
231
+ for category, skills_list in data["skills"].items():
232
+ skills_para = doc.add_paragraph()
233
+ category_run = skills_para.add_run(f"{category}: ")
234
+ category_run.font.name = self.default_font
235
+ category_run.font.size = Pt(11)
236
+ category_run.bold = True
237
+
238
+ skills_run = skills_para.add_run(", ".join(skills_list))
239
+ skills_run.font.name = self.default_font
240
+ skills_run.font.size = Pt(11)
241
+ else:
242
+ # Simple list of skills
243
+ skills_para = doc.add_paragraph(", ".join(data["skills"]))
244
+ skills_para.paragraph_format.left_indent = Inches(0.25)
245
+
246
+ doc.add_paragraph() # Spacing
247
+
248
+ # Certifications
249
+ if data.get("certifications"):
250
+ self._add_heading(doc, "CERTIFICATIONS", level=1)
251
+
252
+ for cert in data["certifications"]:
253
+ cert_para = doc.add_paragraph(style="List Bullet")
254
+ cert_run = cert_para.add_run(
255
+ f"{cert.get('name', 'Certification')} - "
256
+ f"{cert.get('issuer', 'Issuer')}"
257
+ )
258
+ cert_run.font.name = self.default_font
259
+ cert_run.font.size = Pt(11)
260
+
261
+ if cert.get("date"):
262
+ date_run = cert_para.add_run(f" ({cert['date']})")
263
+ date_run.font.name = self.default_font
264
+ date_run.font.size = Pt(10)
265
+ date_run.italic = True
266
+
267
+ # Projects
268
+ if data.get("projects"):
269
+ self._add_heading(doc, "PROJECTS", level=1)
270
+
271
+ for proj in data["projects"]:
272
+ # Project Title
273
+ proj_para = doc.add_paragraph()
274
+ proj_run = proj_para.add_run(proj.get("name", "Project"))
275
+ proj_run.font.name = self.default_font
276
+ proj_run.font.size = Pt(12)
277
+ proj_run.bold = True
278
+
279
+ # Description
280
+ if proj.get("description"):
281
+ desc_para = doc.add_paragraph(proj["description"])
282
+ desc_para.paragraph_format.left_indent = Inches(0.25)
283
+
284
+ # Technologies
285
+ if proj.get("technologies"):
286
+ tech_para = doc.add_paragraph()
287
+ tech_para.paragraph_format.left_indent = Inches(0.25)
288
+ tech_label = tech_para.add_run("Technologies: ")
289
+ tech_label.font.size = Pt(10)
290
+ tech_label.italic = True
291
+ tech_list = tech_para.add_run(", ".join(proj["technologies"]))
292
+ tech_list.font.size = Pt(10)
293
+
294
+ doc.add_paragraph() # Spacing
295
+
296
+ # Save to BytesIO
297
+ buffer = io.BytesIO()
298
+ doc.save(buffer)
299
+ buffer.seek(0)
300
+
301
+ return buffer
302
+
303
+ def generate_cover_letter(self, data: Dict[str, Any]) -> io.BytesIO:
304
+ """
305
+ Generate cover letter document
306
+
307
+ Args:
308
+ data: Cover letter data containing:
309
+ - name: Applicant name
310
+ - address: Applicant address
311
+ - email: Email
312
+ - phone: Phone
313
+ - date: Letter date
314
+ - company: Company name
315
+ - hiring_manager: Hiring manager name (optional)
316
+ - position: Position applied for
317
+ - content: Letter content (paragraphs)
318
+
319
+ Returns:
320
+ BytesIO buffer containing the .docx file
321
+ """
322
+ doc = Document()
323
+
324
+ # Set margins
325
+ sections = doc.sections
326
+ for section in sections:
327
+ section.top_margin = Inches(1)
328
+ section.bottom_margin = Inches(1)
329
+ section.left_margin = Inches(1)
330
+ section.right_margin = Inches(1)
331
+
332
+ # Applicant Info
333
+ self._add_paragraph(doc, data.get("name", "Your Name"), bold=True, size=12)
334
+ if data.get("address"):
335
+ self._add_paragraph(doc, data["address"], size=10)
336
+
337
+ contact_line = []
338
+ if data.get("email"):
339
+ contact_line.append(data["email"])
340
+ if data.get("phone"):
341
+ contact_line.append(data["phone"])
342
+ if contact_line:
343
+ self._add_paragraph(doc, " | ".join(contact_line), size=10)
344
+
345
+ doc.add_paragraph() # Spacing
346
+
347
+ # Date
348
+ date_str = data.get("date", datetime.now().strftime("%B %d, %Y"))
349
+ self._add_paragraph(doc, date_str, size=11)
350
+
351
+ doc.add_paragraph() # Spacing
352
+
353
+ # Recipient Info
354
+ if data.get("hiring_manager"):
355
+ self._add_paragraph(doc, data["hiring_manager"], size=11)
356
+ self._add_paragraph(
357
+ doc, data.get("company", "Company Name"), bold=True, size=11
358
+ )
359
+
360
+ doc.add_paragraph() # Spacing
361
+
362
+ # Salutation
363
+ salutation = (
364
+ f"Dear {data.get('hiring_manager', 'Hiring Manager')},"
365
+ if data.get("hiring_manager")
366
+ else "Dear Hiring Manager,"
367
+ )
368
+ self._add_paragraph(doc, salutation, size=11)
369
+
370
+ doc.add_paragraph() # Spacing
371
+
372
+ # Letter Content
373
+ content = data.get("content", "")
374
+
375
+ # Split content into paragraphs
376
+ paragraphs = content.split("\n\n") if "\n\n" in content else [content]
377
+
378
+ for para_text in paragraphs:
379
+ if para_text.strip():
380
+ para = doc.add_paragraph()
381
+ para.alignment = WD_ALIGN_PARAGRAPH.LEFT
382
+ run = para.add_run(para_text.strip())
383
+ run.font.name = self.default_font
384
+ run.font.size = Pt(11)
385
+ para.paragraph_format.space_after = Pt(12)
386
+
387
+ doc.add_paragraph() # Spacing
388
+
389
+ # Closing
390
+ self._add_paragraph(doc, "Sincerely,", size=11)
391
+ doc.add_paragraph()
392
+ doc.add_paragraph()
393
+ self._add_paragraph(doc, data.get("name", "Your Name"), bold=True, size=11)
394
+
395
+ # Save to BytesIO
396
+ buffer = io.BytesIO()
397
+ doc.save(buffer)
398
+ buffer.seek(0)
399
+
400
+ return buffer
401
+
402
+ def generate_proposal(self, data: Dict[str, Any]) -> io.BytesIO:
403
+ """
404
+ Generate business proposal document
405
+
406
+ Args:
407
+ data: Proposal data containing:
408
+ - title: Proposal title
409
+ - client_name: Client name
410
+ - prepared_by: Your name/company
411
+ - date: Proposal date
412
+ - content: Proposal content (can be structured or plain text)
413
+ - project_overview: Project description
414
+ - scope: Scope of work
415
+ - deliverables: List of deliverables
416
+ - timeline: Project timeline
417
+ - budget: Budget information
418
+
419
+ Returns:
420
+ BytesIO buffer containing the .docx file
421
+ """
422
+ doc = Document()
423
+
424
+ # Set margins
425
+ sections = doc.sections
426
+ for section in sections:
427
+ section.top_margin = Inches(1)
428
+ section.bottom_margin = Inches(1)
429
+ section.left_margin = Inches(1)
430
+ section.right_margin = Inches(1)
431
+
432
+ # Title Page
433
+ title_para = doc.add_paragraph()
434
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
435
+ title_run = title_para.add_run(data.get("title", "Business Proposal"))
436
+ title_run.font.name = self.heading_font
437
+ title_run.font.size = Pt(28)
438
+ title_run.bold = True
439
+ title_run.font.color.rgb = RGBColor(0, 51, 102)
440
+
441
+ doc.add_paragraph()
442
+ doc.add_paragraph()
443
+
444
+ # Prepared For
445
+ for_para = doc.add_paragraph()
446
+ for_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
447
+ for_run = for_para.add_run(
448
+ f"Prepared for:\n{data.get('client_name', 'Client Name')}"
449
+ )
450
+ for_run.font.name = self.default_font
451
+ for_run.font.size = Pt(14)
452
+
453
+ doc.add_paragraph()
454
+
455
+ # Prepared By
456
+ by_para = doc.add_paragraph()
457
+ by_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
458
+ by_run = by_para.add_run(
459
+ f"Prepared by:\n{data.get('prepared_by', 'Your Company')}"
460
+ )
461
+ by_run.font.name = self.default_font
462
+ by_run.font.size = Pt(14)
463
+
464
+ doc.add_paragraph()
465
+
466
+ # Date
467
+ date_para = doc.add_paragraph()
468
+ date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
469
+ date_run = date_para.add_run(
470
+ data.get("date", datetime.now().strftime("%B %d, %Y"))
471
+ )
472
+ date_run.font.name = self.default_font
473
+ date_run.font.size = Pt(12)
474
+
475
+ # Page Break
476
+ doc.add_page_break()
477
+
478
+ # If structured content is provided
479
+ if data.get("content") and isinstance(data["content"], str):
480
+ # Parse and format the content
481
+ sections_text = data["content"].split("\n\n")
482
+ for section in sections_text:
483
+ if section.strip():
484
+ lines = section.strip().split("\n")
485
+ # First line as heading if it looks like a heading
486
+ if len(lines[0]) < 50 and not lines[0].endswith("."):
487
+ self._add_heading(doc, lines[0], level=1)
488
+ for line in lines[1:]:
489
+ if line.strip():
490
+ self._add_paragraph(doc, line.strip())
491
+ else:
492
+ for line in lines:
493
+ if line.strip():
494
+ self._add_paragraph(doc, line.strip())
495
+ doc.add_paragraph()
496
+ else:
497
+ # Manual structure
498
+ # Executive Summary
499
+ if data.get("executive_summary"):
500
+ self._add_heading(doc, "Executive Summary", level=1)
501
+ self._add_paragraph(doc, data["executive_summary"])
502
+ doc.add_paragraph()
503
+
504
+ # Project Overview
505
+ if data.get("project_overview"):
506
+ self._add_heading(doc, "Project Overview", level=1)
507
+ self._add_paragraph(doc, data["project_overview"])
508
+ doc.add_paragraph()
509
+
510
+ # Scope of Work
511
+ if data.get("scope"):
512
+ self._add_heading(doc, "Scope of Work", level=1)
513
+ self._add_paragraph(doc, data["scope"])
514
+ doc.add_paragraph()
515
+
516
+ # Deliverables
517
+ if data.get("deliverables"):
518
+ self._add_heading(doc, "Deliverables", level=1)
519
+ for deliverable in data["deliverables"]:
520
+ doc.add_paragraph(deliverable, style="List Bullet")
521
+ doc.add_paragraph()
522
+
523
+ # Timeline
524
+ if data.get("timeline"):
525
+ self._add_heading(doc, "Timeline", level=1)
526
+ self._add_paragraph(doc, data["timeline"])
527
+ doc.add_paragraph()
528
+
529
+ # Budget
530
+ if data.get("budget"):
531
+ self._add_heading(doc, "Investment", level=1)
532
+ self._add_paragraph(doc, data["budget"])
533
+ doc.add_paragraph()
534
+
535
+ # Next Steps
536
+ self._add_heading(doc, "Next Steps", level=1)
537
+ next_steps = data.get(
538
+ "next_steps",
539
+ [
540
+ "Review this proposal",
541
+ "Schedule a meeting to discuss details",
542
+ "Sign agreement and begin work",
543
+ ],
544
+ )
545
+ for step in next_steps:
546
+ doc.add_paragraph(step, style="List Number")
547
+
548
+ # Save to BytesIO
549
+ buffer = io.BytesIO()
550
+ doc.save(buffer)
551
+ buffer.seek(0)
552
+
553
+ return buffer
554
+
555
+ def generate_invoice(self, data: Dict[str, Any]) -> io.BytesIO:
556
+ """
557
+ Generate invoice document
558
+
559
+ Args:
560
+ data: Invoice data containing:
561
+ - invoice_number: Invoice number
562
+ - invoice_date: Invoice date
563
+ - due_date: Payment due date
564
+ - from_info: Your business info (name, address, email, phone)
565
+ - to_info: Client info (name, address, email)
566
+ - items: List of line items (description, quantity, rate, amount)
567
+ - notes: Additional notes (optional)
568
+ - tax_rate: Tax percentage (optional)
569
+
570
+ Returns:
571
+ BytesIO buffer containing the .docx file
572
+ """
573
+ doc = Document()
574
+
575
+ # Set margins
576
+ sections = doc.sections
577
+ for section in sections:
578
+ section.top_margin = Inches(0.75)
579
+ section.bottom_margin = Inches(0.75)
580
+ section.left_margin = Inches(0.75)
581
+ section.right_margin = Inches(0.75)
582
+
583
+ # Title
584
+ title_para = doc.add_paragraph()
585
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
586
+ title_run = title_para.add_run("INVOICE")
587
+ title_run.font.name = self.heading_font
588
+ title_run.font.size = Pt(24)
589
+ title_run.bold = True
590
+ title_run.font.color.rgb = RGBColor(0, 51, 102)
591
+
592
+ doc.add_paragraph()
593
+
594
+ # Invoice Info Table
595
+ info_table = doc.add_table(rows=2, cols=2)
596
+ info_table.style = "Light Grid Accent 1"
597
+
598
+ # From Info (Left)
599
+ from_cell = info_table.cell(0, 0)
600
+ from_info = data.get("from_info", {})
601
+ from_text = "FROM:\n"
602
+ from_text += f"{from_info.get('name', 'Your Business')}\n"
603
+ if from_info.get("address"):
604
+ from_text += f"{from_info['address']}\n"
605
+ if from_info.get("email"):
606
+ from_text += f"{from_info['email']}\n"
607
+ if from_info.get("phone"):
608
+ from_text += f"{from_info['phone']}"
609
+ from_cell.text = from_text
610
+
611
+ # Invoice Details (Right)
612
+ details_cell = info_table.cell(0, 1)
613
+ details_text = f"Invoice #: {data.get('invoice_number', 'INV-001')}\n"
614
+ details_text += (
615
+ f"Date: {data.get('invoice_date', datetime.now().strftime('%Y-%m-%d'))}\n"
616
+ )
617
+ details_text += f"Due Date: {data.get('due_date', 'Upon Receipt')}"
618
+ details_cell.text = details_text
619
+
620
+ # Bill To (Left)
621
+ to_cell = info_table.cell(1, 0)
622
+ to_info = data.get("to_info", {})
623
+ to_text = "BILL TO:\n"
624
+ to_text += f"{to_info.get('name', 'Client Name')}\n"
625
+ if to_info.get("address"):
626
+ to_text += f"{to_info['address']}\n"
627
+ if to_info.get("email"):
628
+ to_text += f"{to_info['email']}"
629
+ to_cell.text = to_text
630
+
631
+ doc.add_paragraph()
632
+ doc.add_paragraph()
633
+
634
+ # Items Table
635
+ items = data.get("items", [])
636
+ if items:
637
+ items_table = doc.add_table(rows=len(items) + 1, cols=4)
638
+ items_table.style = "Light Grid Accent 1"
639
+
640
+ # Header
641
+ header_cells = items_table.rows[0].cells
642
+ header_cells[0].text = "Description"
643
+ header_cells[1].text = "Quantity"
644
+ header_cells[2].text = "Rate"
645
+ header_cells[3].text = "Amount"
646
+
647
+ # Make header bold
648
+ for cell in header_cells:
649
+ for paragraph in cell.paragraphs:
650
+ for run in paragraph.runs:
651
+ run.font.bold = True
652
+
653
+ # Items
654
+ subtotal = 0
655
+ for idx, item in enumerate(items, 1):
656
+ row_cells = items_table.rows[idx].cells
657
+ row_cells[0].text = item.get("description", "")
658
+ row_cells[1].text = str(item.get("quantity", 1))
659
+ row_cells[2].text = f"${item.get('rate', 0):.2f}"
660
+
661
+ amount = item.get(
662
+ "amount", item.get("quantity", 1) * item.get("rate", 0)
663
+ )
664
+ row_cells[3].text = f"${amount:.2f}"
665
+ subtotal += amount
666
+
667
+ doc.add_paragraph()
668
+
669
+ # Totals
670
+ totals_table = doc.add_table(rows=4, cols=2)
671
+ totals_table.alignment = WD_ALIGN_PARAGRAPH.RIGHT
672
+
673
+ # Subtotal
674
+ totals_table.cell(0, 0).text = "Subtotal:"
675
+ totals_table.cell(0, 1).text = f"${subtotal:.2f}"
676
+
677
+ # Tax
678
+ tax_rate = data.get("tax_rate", 0)
679
+ tax_amount = subtotal * (tax_rate / 100) if tax_rate > 0 else 0
680
+ totals_table.cell(1, 0).text = f"Tax ({tax_rate}%):"
681
+ totals_table.cell(1, 1).text = f"${tax_amount:.2f}"
682
+
683
+ # Discount
684
+ discount = data.get("discount", 0)
685
+ totals_table.cell(2, 0).text = "Discount:"
686
+ totals_table.cell(2, 1).text = f"-${discount:.2f}"
687
+
688
+ # Total
689
+ total = subtotal + tax_amount - discount
690
+ totals_table.cell(3, 0).text = "TOTAL:"
691
+ totals_table.cell(3, 1).text = f"${total:.2f}"
692
+
693
+ # Make total row bold
694
+ for cell in [totals_table.cell(3, 0), totals_table.cell(3, 1)]:
695
+ for paragraph in cell.paragraphs:
696
+ for run in paragraph.runs:
697
+ run.font.bold = True
698
+ run.font.size = Pt(14)
699
+
700
+ doc.add_paragraph()
701
+ doc.add_paragraph()
702
+
703
+ # Notes
704
+ if data.get("notes"):
705
+ self._add_heading(doc, "Notes:", level=2)
706
+ self._add_paragraph(doc, data["notes"])
707
+
708
+ # Payment Instructions
709
+ if data.get("payment_instructions"):
710
+ doc.add_paragraph()
711
+ self._add_heading(doc, "Payment Instructions:", level=2)
712
+ self._add_paragraph(doc, data["payment_instructions"])
713
+
714
+ # Save to BytesIO
715
+ buffer = io.BytesIO()
716
+ doc.save(buffer)
717
+ buffer.seek(0)
718
+
719
+ return buffer
720
+
721
+ def generate_contract(self, data: Dict[str, Any]) -> io.BytesIO:
722
+ """
723
+ Generate contract document
724
+
725
+ Args:
726
+ data: Contract data containing:
727
+ - contract_type: Type of contract
728
+ - date: Contract date
729
+ - party1: First party info (name, address)
730
+ - party2: Second party info (name, address)
731
+ - terms: Contract terms/content
732
+ - effective_date: When contract takes effect
733
+ - expiration_date: When contract expires (optional)
734
+
735
+ Returns:
736
+ BytesIO buffer containing the .docx file
737
+ """
738
+ doc = Document()
739
+
740
+ # Set margins
741
+ sections = doc.sections
742
+ for section in sections:
743
+ section.top_margin = Inches(1)
744
+ section.bottom_margin = Inches(1)
745
+ section.left_margin = Inches(1.25)
746
+ section.right_margin = Inches(1.25)
747
+
748
+ # Title
749
+ contract_type = data.get("contract_type", "Service Agreement")
750
+ title_para = doc.add_paragraph()
751
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
752
+ title_run = title_para.add_run(contract_type.upper())
753
+ title_run.font.name = self.heading_font
754
+ title_run.font.size = Pt(18)
755
+ title_run.bold = True
756
+
757
+ doc.add_paragraph()
758
+
759
+ # Date
760
+ date_para = doc.add_paragraph()
761
+ date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
762
+ date_run = date_para.add_run(
763
+ f"Date: {data.get('date', datetime.now().strftime('%B %d, %Y'))}"
764
+ )
765
+ date_run.font.name = self.default_font
766
+ date_run.font.size = Pt(11)
767
+
768
+ doc.add_paragraph()
769
+
770
+ # Parties
771
+ self._add_heading(doc, "PARTIES", level=1)
772
+
773
+ party1 = data.get("party1", {})
774
+ party2 = data.get("party2", {})
775
+
776
+ parties_text = f"This Agreement is entered into between:\n\n"
777
+ parties_text += f'Party 1 ("Provider"): {party1.get("name", "Party 1 Name")}'
778
+ if party1.get("address"):
779
+ parties_text += f"\nAddress: {party1['address']}"
780
+
781
+ parties_text += f"\n\nAND\n\n"
782
+ parties_text += f'Party 2 ("Client"): {party2.get("name", "Party 2 Name")}'
783
+ if party2.get("address"):
784
+ parties_text += f"\nAddress: {party2['address']}"
785
+
786
+ self._add_paragraph(doc, parties_text)
787
+ doc.add_paragraph()
788
+
789
+ # Effective Date
790
+ if data.get("effective_date"):
791
+ effective_para = doc.add_paragraph()
792
+ effective_run = effective_para.add_run(
793
+ f"Effective Date: {data['effective_date']}"
794
+ )
795
+ effective_run.font.bold = True
796
+ doc.add_paragraph()
797
+
798
+ # Terms
799
+ terms_content = data.get("terms", "")
800
+
801
+ if terms_content:
802
+ # Parse terms into sections
803
+ sections_text = terms_content.split("\n\n")
804
+ for section in sections_text:
805
+ if section.strip():
806
+ lines = section.strip().split("\n")
807
+ # Check if first line is a heading
808
+ if len(lines[0]) < 60 and (
809
+ lines[0].endswith(":") or not lines[0].endswith(".")
810
+ ):
811
+ self._add_heading(
812
+ doc, lines[0].replace(":", "").strip(), level=2
813
+ )
814
+ for line in lines[1:]:
815
+ if line.strip():
816
+ self._add_paragraph(doc, line.strip())
817
+ else:
818
+ for line in lines:
819
+ if line.strip():
820
+ self._add_paragraph(doc, line.strip())
821
+ doc.add_paragraph()
822
+
823
+ # Disclaimer
824
+ doc.add_page_break()
825
+ self._add_heading(doc, "LEGAL DISCLAIMER", level=1)
826
+ disclaimer = (
827
+ "This document is provided as a template only and should be reviewed by a "
828
+ "qualified legal professional before use. The parties acknowledge that this "
829
+ "agreement may not be suitable for all situations and that legal advice "
830
+ "should be sought for specific circumstances."
831
+ )
832
+ self._add_paragraph(doc, disclaimer, italic=True, size=10)
833
+
834
+ doc.add_paragraph()
835
+ doc.add_paragraph()
836
+
837
+ # Signature Section
838
+ self._add_heading(doc, "SIGNATURES", level=1)
839
+
840
+ # Party 1 Signature
841
+ doc.add_paragraph()
842
+ self._add_paragraph(doc, "Party 1 (Provider):", bold=True)
843
+ doc.add_paragraph()
844
+ doc.add_paragraph("_" * 50)
845
+ self._add_paragraph(doc, f"Signature: {party1.get('name', '')}")
846
+ doc.add_paragraph()
847
+ doc.add_paragraph("_" * 50)
848
+ self._add_paragraph(doc, "Date:")
849
+
850
+ doc.add_paragraph()
851
+ doc.add_paragraph()
852
+
853
+ # Party 2 Signature
854
+ self._add_paragraph(doc, "Party 2 (Client):", bold=True)
855
+ doc.add_paragraph()
856
+ doc.add_paragraph("_" * 50)
857
+ self._add_paragraph(doc, f"Signature: {party2.get('name', '')}")
858
+ doc.add_paragraph()
859
+ doc.add_paragraph("_" * 50)
860
+ self._add_paragraph(doc, "Date:")
861
+
862
+ # Save to BytesIO
863
+ buffer = io.BytesIO()
864
+ doc.save(buffer)
865
+ buffer.seek(0)
866
+
867
+ return buffer
868
+
869
+
870
+ # Singleton instance
871
+ _docx_generator = None
872
+
873
+
874
+ def get_docx_generator() -> DocxGenerator:
875
+ """Get or create DocxGenerator singleton"""
876
+ global _docx_generator
877
+ if _docx_generator is None:
878
+ _docx_generator = DocxGenerator()
879
+ return _docx_generator
utils/gemini_client.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini AI Client
3
+ Handles all interactions with Google's Gemini 2.5 Flash API
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import google.generativeai as genai
12
+
13
+
14
+ class GeminiClient:
15
+ """Client for Google Gemini AI API"""
16
+
17
+ def __init__(self, api_key: Optional[str] = None):
18
+ """
19
+ Initialize Gemini client
20
+
21
+ Args:
22
+ api_key: Google Gemini API key (optional, reads from env if not provided)
23
+ """
24
+ self.api_key = api_key or os.getenv("GEMINI_API_KEY")
25
+
26
+ if not self.api_key:
27
+ raise ValueError("GEMINI_API_KEY not found in environment variables")
28
+
29
+ # Configure Gemini
30
+ genai.configure(api_key=self.api_key)
31
+
32
+ # Initialize model (Gemini 2.5 Flash)
33
+ self.model = genai.GenerativeModel("gemini-2.0-flash-exp")
34
+
35
+ # Safety settings
36
+ self.safety_settings = [
37
+ {
38
+ "category": "HARM_CATEGORY_HARASSMENT",
39
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE",
40
+ },
41
+ {
42
+ "category": "HARM_CATEGORY_HATE_SPEECH",
43
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE",
44
+ },
45
+ {
46
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
47
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE",
48
+ },
49
+ {
50
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
51
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE",
52
+ },
53
+ ]
54
+
55
+ print(f"✓ Gemini AI client initialized successfully", file=sys.stderr)
56
+
57
+ def generate_text(
58
+ self, prompt: str, temperature: float = 0.7, max_tokens: int = 2048
59
+ ) -> str:
60
+ """
61
+ Generate text using Gemini
62
+
63
+ Args:
64
+ prompt: Input prompt
65
+ temperature: Creativity level (0.0 to 1.0)
66
+ max_tokens: Maximum response length
67
+
68
+ Returns:
69
+ Generated text
70
+ """
71
+ try:
72
+ generation_config = {
73
+ "temperature": temperature,
74
+ "max_output_tokens": max_tokens,
75
+ "top_p": 0.95,
76
+ "top_k": 40,
77
+ }
78
+
79
+ response = self.model.generate_content(
80
+ prompt,
81
+ generation_config=generation_config,
82
+ safety_settings=self.safety_settings,
83
+ )
84
+
85
+ return response.text
86
+
87
+ except Exception as e:
88
+ print(f"Error generating text: {str(e)}", file=sys.stderr)
89
+ raise
90
+
91
+ def enhance_resume_description(self, description: str, role: str = "") -> str:
92
+ """
93
+ Enhance a resume job description
94
+
95
+ Args:
96
+ description: Original description
97
+ role: Job role/title for context
98
+
99
+ Returns:
100
+ Enhanced description
101
+ """
102
+ prompt = f"""You are a professional resume writer. Enhance the following job description to be more impactful and achievement-focused.
103
+
104
+ Role: {role}
105
+ Original Description: {description}
106
+
107
+ Requirements:
108
+ - Use strong action verbs
109
+ - Quantify achievements where possible
110
+ - Focus on impact and results
111
+ - Keep it concise (2-3 sentences)
112
+ - Professional tone
113
+ - Do not add fake numbers or achievements
114
+
115
+ Enhanced Description:"""
116
+
117
+ return self.generate_text(prompt, temperature=0.5)
118
+
119
+ def generate_cover_letter(self, data: Dict[str, Any]) -> str:
120
+ """
121
+ Generate a personalized cover letter
122
+
123
+ Args:
124
+ data: Dictionary containing:
125
+ - name: Applicant name
126
+ - company: Company name
127
+ - position: Job position
128
+ - skills: List of relevant skills
129
+ - experience: Brief experience summary
130
+ - tone: Tone (formal, creative, technical)
131
+
132
+ Returns:
133
+ Complete cover letter text
134
+ """
135
+ tone_guides = {
136
+ "formal": "Professional and formal business style",
137
+ "creative": "Engaging and creative while remaining professional",
138
+ "technical": "Technical and detail-oriented style",
139
+ }
140
+
141
+ tone = data.get("tone", "formal")
142
+ tone_guide = tone_guides.get(tone, tone_guides["formal"])
143
+
144
+ prompt = f"""Write a compelling cover letter with the following details:
145
+
146
+ Applicant Name: {data.get("name", "Applicant")}
147
+ Company: {data.get("company", "the company")}
148
+ Position: {data.get("position", "the position")}
149
+ Relevant Skills: {", ".join(data.get("skills", []))}
150
+ Experience Summary: {data.get("experience", "No experience provided")}
151
+
152
+ Tone: {tone_guide}
153
+
154
+ Structure:
155
+ 1. Opening paragraph: Show enthusiasm and mention how you learned about the position
156
+ 2. Body paragraphs (2-3): Highlight relevant skills and experiences
157
+ 3. Closing paragraph: Express interest in an interview and thank them
158
+
159
+ Requirements:
160
+ - Personalized and specific to the role
161
+ - Highlight relevant achievements
162
+ - Professional formatting
163
+ - 3-4 paragraphs
164
+ - Do not include [Date] or address placeholders
165
+
166
+ Cover Letter:"""
167
+
168
+ return self.generate_text(prompt, temperature=0.7, max_tokens=1500)
169
+
170
+ def generate_proposal(self, data: Dict[str, Any]) -> str:
171
+ """
172
+ Generate a business proposal
173
+
174
+ Args:
175
+ data: Dictionary containing:
176
+ - client_name: Client name
177
+ - project_title: Project title
178
+ - scope: Project scope
179
+ - timeline: Expected timeline
180
+ - budget: Budget estimate (optional)
181
+ - deliverables: List of deliverables
182
+
183
+ Returns:
184
+ Complete proposal text
185
+ """
186
+ prompt = f"""Create a professional business proposal with the following details:
187
+
188
+ Client: {data.get("client_name", "Client")}
189
+ Project: {data.get("project_title", "Project")}
190
+ Scope: {data.get("scope", "Not specified")}
191
+ Timeline: {data.get("timeline", "To be determined")}
192
+ Budget: {data.get("budget", "To be discussed")}
193
+ Deliverables: {", ".join(data.get("deliverables", []))}
194
+
195
+ Structure:
196
+ 1. Executive Summary
197
+ 2. Project Overview
198
+ 3. Scope of Work
199
+ 4. Deliverables
200
+ 5. Timeline
201
+ 6. Investment (if budget provided)
202
+ 7. Next Steps
203
+
204
+ Requirements:
205
+ - Professional and persuasive
206
+ - Clear and specific
207
+ - Well-structured with sections
208
+ - Professional tone
209
+
210
+ Proposal:"""
211
+
212
+ return self.generate_text(prompt, temperature=0.6, max_tokens=2048)
213
+
214
+ def enhance_contract_terms(self, contract_type: str, custom_terms: str = "") -> str:
215
+ """
216
+ Generate or enhance contract terms
217
+
218
+ Args:
219
+ contract_type: Type of contract (freelance, service, nda, etc.)
220
+ custom_terms: Custom requirements or terms
221
+
222
+ Returns:
223
+ Contract terms text
224
+ """
225
+ prompt = f"""Generate professional contract terms for a {contract_type} agreement.
226
+
227
+ Custom Requirements: {custom_terms if custom_terms else "Standard terms"}
228
+
229
+ Include:
230
+ 1. Scope of Services
231
+ 2. Payment Terms
232
+ 3. Timeline and Deadlines
233
+ 4. Intellectual Property Rights
234
+ 5. Confidentiality
235
+ 6. Termination Clause
236
+ 7. Liability and Warranties
237
+
238
+ Requirements:
239
+ - Professional legal language
240
+ - Clear and specific
241
+ - Balanced for both parties
242
+ - Industry-standard terms
243
+ - Add disclaimer that this should be reviewed by legal counsel
244
+
245
+ Contract Terms:"""
246
+
247
+ return self.generate_text(prompt, temperature=0.4, max_tokens=2048)
248
+
249
+ def enhance_portfolio_description(self, project_data: Dict[str, Any]) -> str:
250
+ """
251
+ Enhance portfolio project description
252
+
253
+ Args:
254
+ project_data: Dictionary containing:
255
+ - title: Project title
256
+ - description: Current description
257
+ - technologies: List of technologies used
258
+ - role: Your role in the project
259
+
260
+ Returns:
261
+ Enhanced project description
262
+ """
263
+ prompt = f"""Enhance this portfolio project description to be more compelling and professional:
264
+
265
+ Project: {project_data.get("title", "Project")}
266
+ Current Description: {project_data.get("description", "")}
267
+ Technologies: {", ".join(project_data.get("technologies", []))}
268
+ Role: {project_data.get("role", "Developer")}
269
+
270
+ Requirements:
271
+ - Start with impact/achievement
272
+ - Highlight technical skills
273
+ - Mention problem solved
274
+ - Keep concise (3-4 sentences)
275
+ - Professional and engaging
276
+
277
+ Enhanced Description:"""
278
+
279
+ return self.generate_text(prompt, temperature=0.6)
280
+
281
+ def generate_skills_summary(
282
+ self, skills: List[str], experience_years: int = 0
283
+ ) -> str:
284
+ """
285
+ Generate a professional skills summary
286
+
287
+ Args:
288
+ skills: List of skills
289
+ experience_years: Years of experience
290
+
291
+ Returns:
292
+ Skills summary paragraph
293
+ """
294
+ prompt = f"""Create a compelling professional summary for someone with:
295
+
296
+ Skills: {", ".join(skills)}
297
+ Years of Experience: {experience_years if experience_years > 0 else "Entry-level"}
298
+
299
+ Requirements:
300
+ - 2-3 sentences
301
+ - Highlight key strengths
302
+ - Professional tone
303
+ - Focus on value proposition
304
+ - Do not exaggerate
305
+
306
+ Professional Summary:"""
307
+
308
+ return self.generate_text(prompt, temperature=0.6, max_tokens=300)
309
+
310
+ def improve_text_quality(self, text: str, style: str = "professional") -> str:
311
+ """
312
+ General purpose text improvement
313
+
314
+ Args:
315
+ text: Text to improve
316
+ style: Desired style (professional, casual, technical, creative)
317
+
318
+ Returns:
319
+ Improved text
320
+ """
321
+ prompt = f"""Improve the following text in a {style} style:
322
+
323
+ Original: {text}
324
+
325
+ Requirements:
326
+ - Fix grammar and spelling
327
+ - Improve clarity and flow
328
+ - Maintain original meaning
329
+ - Use appropriate vocabulary
330
+ - Keep similar length
331
+
332
+ Improved Text:"""
333
+
334
+ return self.generate_text(prompt, temperature=0.5)
335
+
336
+ def generate_json_structured(
337
+ self, prompt: str, schema: Dict[str, Any]
338
+ ) -> Dict[str, Any]:
339
+ """
340
+ Generate structured JSON response
341
+
342
+ Args:
343
+ prompt: Prompt for generation
344
+ schema: Expected JSON schema
345
+
346
+ Returns:
347
+ Dictionary with generated data
348
+ """
349
+ full_prompt = f"""{prompt}
350
+
351
+ Respond with valid JSON matching this structure:
352
+ {json.dumps(schema, indent=2)}
353
+
354
+ JSON Response:"""
355
+
356
+ response_text = self.generate_text(full_prompt, temperature=0.5)
357
+
358
+ try:
359
+ # Extract JSON from response
360
+ if "```json" in response_text:
361
+ json_str = response_text.split("```json")[1].split("```")[0].strip()
362
+ elif "```" in response_text:
363
+ json_str = response_text.split("```")[1].split("```")[0].strip()
364
+ else:
365
+ json_str = response_text.strip()
366
+
367
+ return json.loads(json_str)
368
+
369
+ except Exception as e:
370
+ print(f"Error parsing JSON: {str(e)}", file=sys.stderr)
371
+ return {}
372
+
373
+
374
+ # Singleton instance
375
+ _gemini_client = None
376
+
377
+
378
+ def get_gemini_client() -> GeminiClient:
379
+ """Get or create Gemini client singleton"""
380
+ global _gemini_client
381
+ if _gemini_client is None:
382
+ _gemini_client = GeminiClient()
383
+ return _gemini_client
utils/pdf_generator.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PDF Generator Utility
3
+ Handles creation of PDF documents for portfolio exports
4
+ """
5
+
6
+ import io
7
+ import sys
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from reportlab.lib import colors
12
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT, TA_RIGHT
13
+ from reportlab.lib.pagesizes import A4, letter
14
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
15
+ from reportlab.lib.units import inch
16
+ from reportlab.platypus import (
17
+ HRFlowable,
18
+ Image,
19
+ PageBreak,
20
+ Paragraph,
21
+ SimpleDocTemplate,
22
+ Spacer,
23
+ Table,
24
+ TableStyle,
25
+ )
26
+
27
+
28
+ class PDFGenerator:
29
+ """Generate professional PDF documents"""
30
+
31
+ def __init__(self):
32
+ """Initialize PDF generator"""
33
+ self.page_size = letter
34
+ self.styles = getSampleStyleSheet()
35
+ self._setup_custom_styles()
36
+
37
+ def _setup_custom_styles(self):
38
+ """Setup custom paragraph styles"""
39
+ # Title style
40
+ self.styles.add(
41
+ ParagraphStyle(
42
+ name="CustomTitle",
43
+ parent=self.styles["Heading1"],
44
+ fontSize=24,
45
+ textColor=colors.HexColor("#003366"),
46
+ spaceAfter=30,
47
+ alignment=TA_CENTER,
48
+ fontName="Helvetica-Bold",
49
+ )
50
+ )
51
+
52
+ # Subtitle style
53
+ self.styles.add(
54
+ ParagraphStyle(
55
+ name="CustomSubtitle",
56
+ parent=self.styles["Normal"],
57
+ fontSize=14,
58
+ textColor=colors.HexColor("#666666"),
59
+ spaceAfter=20,
60
+ alignment=TA_CENTER,
61
+ fontName="Helvetica",
62
+ )
63
+ )
64
+
65
+ # Section heading
66
+ self.styles.add(
67
+ ParagraphStyle(
68
+ name="SectionHeading",
69
+ parent=self.styles["Heading2"],
70
+ fontSize=16,
71
+ textColor=colors.HexColor("#003366"),
72
+ spaceAfter=12,
73
+ spaceBefore=20,
74
+ fontName="Helvetica-Bold",
75
+ borderWidth=1,
76
+ borderColor=colors.HexColor("#003366"),
77
+ borderPadding=5,
78
+ )
79
+ )
80
+
81
+ # Body text
82
+ self.styles.add(
83
+ ParagraphStyle(
84
+ name="CustomBody",
85
+ parent=self.styles["Normal"],
86
+ fontSize=11,
87
+ textColor=colors.HexColor("#333333"),
88
+ spaceAfter=10,
89
+ alignment=TA_JUSTIFY,
90
+ fontName="Helvetica",
91
+ )
92
+ )
93
+
94
+ # Contact info
95
+ self.styles.add(
96
+ ParagraphStyle(
97
+ name="ContactInfo",
98
+ parent=self.styles["Normal"],
99
+ fontSize=10,
100
+ textColor=colors.HexColor("#666666"),
101
+ alignment=TA_CENTER,
102
+ fontName="Helvetica",
103
+ )
104
+ )
105
+
106
+ def generate_portfolio_pdf(self, data: Dict[str, Any]) -> io.BytesIO:
107
+ """
108
+ Generate portfolio PDF
109
+
110
+ Args:
111
+ data: Portfolio data containing:
112
+ - name: Full name
113
+ - title: Professional title
114
+ - bio: Biography
115
+ - contact: Contact info (email, phone, website, linkedin)
116
+ - skills: List or dict of skills
117
+ - experience: List of work experiences
118
+ - education: List of education entries
119
+ - projects: List of projects
120
+ - certifications: List of certifications (optional)
121
+
122
+ Returns:
123
+ BytesIO buffer containing the PDF file
124
+ """
125
+ buffer = io.BytesIO()
126
+ doc = SimpleDocTemplate(
127
+ buffer,
128
+ pagesize=self.page_size,
129
+ rightMargin=0.75 * inch,
130
+ leftMargin=0.75 * inch,
131
+ topMargin=0.75 * inch,
132
+ bottomMargin=0.75 * inch,
133
+ )
134
+
135
+ # Container for the 'Flowable' objects
136
+ elements = []
137
+
138
+ # Header - Name and Title
139
+ name = data.get("name", "Your Name")
140
+ elements.append(Paragraph(name, self.styles["CustomTitle"]))
141
+
142
+ title = data.get("title", "Professional Title")
143
+ elements.append(Paragraph(title, self.styles["CustomSubtitle"]))
144
+
145
+ # Contact Information
146
+ contact = data.get("contact", {})
147
+ contact_parts = []
148
+ if contact.get("email"):
149
+ contact_parts.append(contact["email"])
150
+ if contact.get("phone"):
151
+ contact_parts.append(contact["phone"])
152
+ if contact.get("website"):
153
+ contact_parts.append(
154
+ f'<link href="{contact["website"]}">{contact["website"]}</link>'
155
+ )
156
+ if contact.get("linkedin"):
157
+ contact_parts.append(f"LinkedIn: {contact['linkedin']}")
158
+
159
+ if contact_parts:
160
+ contact_text = " | ".join(contact_parts)
161
+ elements.append(Paragraph(contact_text, self.styles["ContactInfo"]))
162
+ elements.append(Spacer(1, 0.2 * inch))
163
+
164
+ # Horizontal line
165
+ elements.append(
166
+ HRFlowable(width="100%", thickness=2, color=colors.HexColor("#003366"))
167
+ )
168
+ elements.append(Spacer(1, 0.2 * inch))
169
+
170
+ # Bio/Summary
171
+ if data.get("bio"):
172
+ elements.append(
173
+ Paragraph("PROFESSIONAL SUMMARY", self.styles["SectionHeading"])
174
+ )
175
+ elements.append(Paragraph(data["bio"], self.styles["CustomBody"]))
176
+ elements.append(Spacer(1, 0.2 * inch))
177
+
178
+ # Skills
179
+ if data.get("skills"):
180
+ elements.append(Paragraph("SKILLS", self.styles["SectionHeading"]))
181
+
182
+ skills = data["skills"]
183
+ if isinstance(skills, dict):
184
+ # Categorized skills
185
+ for category, skill_list in skills.items():
186
+ skills_text = f"<b>{category}:</b> {', '.join(skill_list)}"
187
+ elements.append(Paragraph(skills_text, self.styles["CustomBody"]))
188
+ else:
189
+ # Simple list
190
+ skills_text = ", ".join(skills)
191
+ elements.append(Paragraph(skills_text, self.styles["CustomBody"]))
192
+
193
+ elements.append(Spacer(1, 0.2 * inch))
194
+
195
+ # Work Experience
196
+ if data.get("experience"):
197
+ elements.append(Paragraph("WORK EXPERIENCE", self.styles["SectionHeading"]))
198
+
199
+ for exp in data["experience"]:
200
+ # Job title and company
201
+ job_title = exp.get("title", "Position")
202
+ company = exp.get("company", "Company")
203
+ dates = f"{exp.get('start_date', 'Start')} - {exp.get('end_date', 'Present')}"
204
+
205
+ title_text = f"<b>{job_title}</b> at {company}"
206
+ elements.append(Paragraph(title_text, self.styles["CustomBody"]))
207
+
208
+ # Dates and location
209
+ location_text = dates
210
+ if exp.get("location"):
211
+ location_text += f" | {exp['location']}"
212
+ elements.append(
213
+ Paragraph(f"<i>{location_text}</i>", self.styles["CustomBody"])
214
+ )
215
+
216
+ # Responsibilities
217
+ if exp.get("responsibilities"):
218
+ for resp in exp["responsibilities"]:
219
+ elements.append(
220
+ Paragraph(f"• {resp}", self.styles["CustomBody"])
221
+ )
222
+
223
+ elements.append(Spacer(1, 0.15 * inch))
224
+
225
+ # Projects
226
+ if data.get("projects"):
227
+ elements.append(Paragraph("PROJECTS", self.styles["SectionHeading"]))
228
+
229
+ for proj in data["projects"]:
230
+ # Project name
231
+ proj_name = proj.get("name", "Project")
232
+ elements.append(
233
+ Paragraph(f"<b>{proj_name}</b>", self.styles["CustomBody"])
234
+ )
235
+
236
+ # Description
237
+ if proj.get("description"):
238
+ elements.append(
239
+ Paragraph(proj["description"], self.styles["CustomBody"])
240
+ )
241
+
242
+ # Technologies
243
+ if proj.get("technologies"):
244
+ tech_text = (
245
+ f"<i>Technologies: {', '.join(proj['technologies'])}</i>"
246
+ )
247
+ elements.append(Paragraph(tech_text, self.styles["CustomBody"]))
248
+
249
+ # URL
250
+ if proj.get("url"):
251
+ url_text = f'<link href="{proj["url"]}">{proj["url"]}</link>'
252
+ elements.append(Paragraph(url_text, self.styles["CustomBody"]))
253
+
254
+ elements.append(Spacer(1, 0.15 * inch))
255
+
256
+ # Education
257
+ if data.get("education"):
258
+ elements.append(Paragraph("EDUCATION", self.styles["SectionHeading"]))
259
+
260
+ for edu in data["education"]:
261
+ degree = edu.get("degree", "Degree")
262
+ field = edu.get("field", "Field")
263
+ school = edu.get("school", "School")
264
+ grad_date = edu.get("graduation_date", "Graduation Date")
265
+
266
+ edu_text = f"<b>{degree} in {field}</b>"
267
+ elements.append(Paragraph(edu_text, self.styles["CustomBody"]))
268
+
269
+ school_text = f"{school} | {grad_date}"
270
+ elements.append(
271
+ Paragraph(f"<i>{school_text}</i>", self.styles["CustomBody"])
272
+ )
273
+
274
+ # GPA or honors
275
+ if edu.get("gpa"):
276
+ elements.append(
277
+ Paragraph(f"GPA: {edu['gpa']}", self.styles["CustomBody"])
278
+ )
279
+ if edu.get("honors"):
280
+ elements.append(Paragraph(edu["honors"], self.styles["CustomBody"]))
281
+
282
+ elements.append(Spacer(1, 0.15 * inch))
283
+
284
+ # Certifications
285
+ if data.get("certifications"):
286
+ elements.append(Paragraph("CERTIFICATIONS", self.styles["SectionHeading"]))
287
+
288
+ for cert in data["certifications"]:
289
+ cert_name = cert.get("name", "Certification")
290
+ issuer = cert.get("issuer", "Issuer")
291
+ cert_date = cert.get("date", "")
292
+
293
+ cert_text = f"• <b>{cert_name}</b> - {issuer}"
294
+ if cert_date:
295
+ cert_text += f" ({cert_date})"
296
+
297
+ elements.append(Paragraph(cert_text, self.styles["CustomBody"]))
298
+
299
+ # Footer
300
+ elements.append(Spacer(1, 0.5 * inch))
301
+ elements.append(
302
+ HRFlowable(width="100%", thickness=1, color=colors.HexColor("#CCCCCC"))
303
+ )
304
+ footer_text = f"Generated on {datetime.now().strftime('%B %d, %Y')}"
305
+ elements.append(Paragraph(footer_text, self.styles["ContactInfo"]))
306
+
307
+ # Build PDF
308
+ doc.build(elements)
309
+ buffer.seek(0)
310
+
311
+ return buffer
312
+
313
+ def generate_simple_pdf(self, content: str, title: str = "Document") -> io.BytesIO:
314
+ """
315
+ Generate a simple PDF from text content
316
+
317
+ Args:
318
+ content: Text content
319
+ title: Document title
320
+
321
+ Returns:
322
+ BytesIO buffer containing the PDF file
323
+ """
324
+ buffer = io.BytesIO()
325
+ doc = SimpleDocTemplate(
326
+ buffer,
327
+ pagesize=self.page_size,
328
+ rightMargin=inch,
329
+ leftMargin=inch,
330
+ topMargin=inch,
331
+ bottomMargin=inch,
332
+ )
333
+
334
+ elements = []
335
+
336
+ # Title
337
+ elements.append(Paragraph(title, self.styles["CustomTitle"]))
338
+ elements.append(Spacer(1, 0.3 * inch))
339
+
340
+ # Content
341
+ paragraphs = content.split("\n\n")
342
+ for para in paragraphs:
343
+ if para.strip():
344
+ elements.append(Paragraph(para.strip(), self.styles["CustomBody"]))
345
+ elements.append(Spacer(1, 0.1 * inch))
346
+
347
+ # Build PDF
348
+ doc.build(elements)
349
+ buffer.seek(0)
350
+
351
+ return buffer
352
+
353
+
354
+ # Singleton instance
355
+ _pdf_generator = None
356
+
357
+
358
+ def get_pdf_generator() -> PDFGenerator:
359
+ """Get or create PDFGenerator singleton"""
360
+ global _pdf_generator
361
+ if _pdf_generator is None:
362
+ _pdf_generator = PDFGenerator()
363
+ return _pdf_generator