import os import gradio as gr import arabic_reshaper from bidi.algorithm import get_display from fpdf import FPDF from llama_index.llms.groq import Groq import asyncio import traceback print("[startup] CV Agent initializing...") GROQ_KEY = os.environ.get("GROQ_API_KEY") print(f"[startup] GROQ_API_KEY set: {bool(GROQ_KEY)}") llm = Groq(model="llama-3.3-70b-versatile", api_key=GROQ_KEY or "") FONT_DIR = "Fonts" AR_FONTS = {"Arial": {"regular": f"{FONT_DIR}/ arfonts-arial.ttf", "bold": f"{FONT_DIR}/ arfonts-arial-bold.ttf"}} EN_FONTS = {"Arial": {"regular": f"{FONT_DIR}/ arial.ttf", "bold": f"{FONT_DIR}/ G_ari_bd.TTF"}} if not os.path.exists(FONT_DIR): try: os.makedirs(FONT_DIR, exist_ok=True) print(f"[fonts] Created font directory: {FONT_DIR}") except Exception as e: print(f"[fonts] Failed to create font dir: {e}") RESUME_STYLES = [ "Europass - 🇪🇺 Formal European format", "Harvard - 🎓 Academic with impact", "US Resume - 🇺🇸 One-page concise resume", "UK CV - 🇬🇧 Two-page traditional format", "Functional Resume - 🧠 Skills-focused layout", "Academic CV - 📚 For researchers and scholars" ] RESUME_STYLE_GUIDELINES = { "Europass - 🇪🇺 Formal European format": "الأقسام : الخبرة العملية، التعليم، المهارات، اللغات، معلومات إضافية.\n" "الخبرة العملية: لكل وظيفة، اذكر الشركة، المسمى الوظيفي، الفترة، المهام الرئيسية والإنجازات.\n" "التعليم: لكل مؤهل، اذكر المؤسسة، الدرجة، التخصص، سنة التخرج.\n" "المهارات: قسمين (التقنية والشخصية) مع النقاط التفصيلية.\n" "اللغات: مع تحديد المستوى حسب الإطار الأوروبي (A1-C2).\n" "معلومات إضافية: الشهادات، المشاريع، المنشورات، الاهتمامات.", "Harvard - 🎓 Academic with impact": "الأقسام : ملخص مهني، الخبرة العملية، التعليم، المهارات، الإنجازات، المنشورات.\n" "ملخص مهني: 3-4 جمل مركزة تلخص الخبرات والإنجازات الرئيسية.\n" "الخبرة العملية: ركز على الإنجازات والأثر مع استخدام أفعال الحركة وأرقام قابلة للقياس.\n" "التعليم: شامل مع ذكر المشاريع البحثية أو الرسائل إذا كانت ذات صلة.\n" "الإنجازات: قسم منفصل للإنجازات الأكاديمية والمهنية البارزة.\n" "المنشورات: قائمة كاملة بالمنشورات الأكاديمية أو المقالات.", "US Resume - 🇺🇸 One-page concise resume": "الأقسام : ملخص، الخبرة العملية، التعليم، المهارات.\n" "ملخص: 2-3 جمل قصيرة في الأعلى تلخص المؤهلات الرئيسية.\n" "الخبرة العملية: فقط الوظائف الأكثر صلة بالوظيفة المستهدفة، مع التركيز على الإنجازات.\n" "التعليم: مختصر مع ذكر المؤهل والمؤسسة وسنة التخرج فقط.\n" "المهارات: قائمة نقطية مختصرة بالمهارات الأساسية فقط.\n" "المبدأ الأساسي: التركيز والاختصار في صفحة واحدة.", "UK CV - 🇬🇧 Two-page traditional format": "الأقسام المطلوبة: الملف الشخصي، التاريخ الوظيفي، التعليم، المهارات، الاهتمامات، المراجع.\n" "الملف الشخصي: فقرة قصيرة تقدم المرشح وأهدافه المهنية.\n" "التاريخ الوظيفي: مفصل مع ذكر جميع الوظائف والمهام والإنجازات.\n" "التعليم: شامل مع ذكر المواد أو المشاريع البارزة.\n" "المراجع: أسماء ووظائف الأشخاص الذين يمكن التواصل معهم للتوصية.\n" "المبدأ الأساسي: الشمولية والتفصيل في صفحتين كحد أقصى.", "Functional Resume - 🧠 Skills-focused layout": "الأقسام المطلوبة: ملخص المهارات، الإنجازات الرئيسية، الخبرة العملية، التعليم.\n" "ملخص المهارات: قسم مفصل ينقسم إلى مهارات تقنية وشخصية وقيادية.\n" "الإنجازات الرئيسية: قائمة بالإنجازات البارزة عبر الخبرات المختلفة.\n" "الخبرة العملية: مختصر مع ذكر الشركات والفترات فقط دون تفاصيل.\n" "التعليم: مختصر مع ذكر المؤهل الأساسي فقط.\n" "المبدأ الأساسي: التركيز على المهارات والقدرات بدلاً من التسلسل الزمني.", "Academic CV - 📚 For researchers and scholars": "الأقسام المطلوبة: التعليم، الخبرة البحثية، المنشورات، المؤتمرات، الجوائز، المهارات.\n" "التعليم: شامل مع ذكر المشرفين والرسائل والتقديرات.\n" "الخبرة البحثية: المشاريع البحثية، المنح، وفرق البحث.\n" "المنشورات: مفصلة مع ذكر جميع المنشورات الأكاديمية مرتبة زمنياً.\n" "المؤتمرات: المشاركات في المؤتمرات العلمية والعروض التقديمية.\n" "الجوائز: جميع الجوائز الأكاديمية والمنح الدراسية.\n" "المبدأ الأساسي: الشمولية بدون حدود للصفحات مع التركيز على الإنجازات الأكاديمية." } RESUME_QUESTIONS_AR = [ "ما اسمك الكامل؟", "ما رقم هاتفك للتواصل؟", "ما عنوان بريدك الإلكتروني؟", "في أي مدينة ودولة تقيم حاليًا؟", "ما هو تاريخ ميلادك؟ (اختياري حسب الدولة)", "ما جنسيتك؟ (اختياري حسب الدولة)", "ما حالتك الاجتماعية؟ (متزوج، أعزب... إلخ) (اختياري حسب الدولة)", "هل لديك حسابات مهنية مثل LinkedIn أو GitHub أو موقع شخصي؟ (اكتب الروابط إن وجدت)", "ما نوع الوظيفة أو المجال الذي تستهدفه حاليًا؟", "صف نفسك أو هدفك المهني في سطرين إلى ثلاثة أسطر", "ما هي الوظائف أو الخبرات السابقة التي لديك؟", "لكل وظيفة سابقة:\n\t\t- ما اسم الشركة؟\n\t\t- ما المسمى الوظيفي؟\n\t\t- ما الفترة الزمنية (من – إلى)؟\n\t\t- ما هي المهام التي كنت تقوم بها؟\n\t\t- ما هي أهم إنجازاتك في هذه الوظيفة؟", "هل سبق لك العمل بشكل حر (Freelance) أو تطوعي؟ اذكر التفاصيل", "ما هي مؤهلاتك الدراسية؟", "لكل مؤهل دراسي:\n\t\t- اسم المؤسسة التعليمية\n\t\t- الدرجة (بكالوريوس، دبلوم...)\n\t\t- التخصص\n\t\t- سنة التخرج\n\t\t- التقدير العام (إن وجد)", "ما هي المهارات التقنية التي تتقنها؟ (برامج، أدوات، أنظمة...)", "ما هي المهارات الشخصية التي تميزك؟ (مثل: القيادة، العمل الجماعي، التواصل...)", "ما هي اللغات التي تتقنها؟", "ما مستواك في كل لغة؟ (أساسي، متوسط، جيد، ممتاز – أو A1 إلى C2)", "هل حصلت على أي شهادات أو دورات تدريبية؟", "لكل دورة أو شهادة:\n\t\t- اسم الدورة\n\t\t- الجهة المانحة\n\t\t- تاريخ الحصول عليها", "هل لديك مشاريع عملية أو شخصية مهمة شاركت بها؟ اذكر التفاصيل", "هل لديك جوائز، تكريمات، أو إنجازات خاصة تود ذكرها؟", "هل لديك عضويات في جمعيات مهنية أو علمية؟", "هل سبق أن شاركت في مؤتمرات أو أبحاث أو منشورات؟ اذكر التفاصيل", "هل ترغب بإدراج مراجع (أشخاص يمكن التواصل معهم لتوصية وظيفية)؟ اذكر الاسم، الوظيفة، جهة العمل، ورقم التواصل أو البريد (اختياري)", "ما هي اهتماماتك أو هواياتك المرتبطة بالمجال المهني أو العام؟", "هل لديك اي معلومات او ملاحظات اضافية تود اضافتها الى السيرة الذاتية" ] RESUME_QUESTIONS_EN = [ "Full Name?", "Phone Number?", "Email Address?", "Current City and Country?", "Date of Birth? (Optional depending on country)", "Nationality? (Optional depending on country)", "Marital Status? (Married, Single, etc.) (Optional depending on country)", "Do you have professional accounts (LinkedIn, GitHub, personal website)? (Provide links if available)", "What type of job or field are you currently targeting?", "Describe yourself or your career objective in 2-3 lines", "What previous jobs or work experiences do you have?", "For each previous job:\n\t\t- Company name\n\t\t- Job title\n\t\t- Time period (from - to)\n\t\t- What were your responsibilities?\n\t\t- Key achievements in this role?", "Have you done freelance or volunteer work? Provide details", "What are your educational qualifications?", "For each qualification:\n\t\t- Institution name\n\t\t- Degree (Bachelor, Diploma, etc.)\n\t\t- Major\n\t\t- Graduation year\n\t\t- Grade (if available)", "What technical skills do you master? (Software, tools, systems...)", "What soft skills distinguish you? (e.g., leadership, teamwork, communication...)", "What languages do you speak?", "What is your proficiency level in each language? (Basic, Intermediate, Good, Excellent - or A1 to C2)", "Have you obtained any certificates or training courses?", "For each course or certificate:\n\t\t- Course name\n\t\t- Issuing authority\n\t\t- Date obtained", "Do you have any important practical or personal projects you participated in? Provide details", "Do you have any awards, honors, or special achievements you want to mention?", "Are you a member of any professional or scientific associations?", "Have you participated in conferences, research, or publications? Provide details", "Do you wish to include references? (Name, job title, company, and contact number or email - optional)", "What are your interests or hobbies related to the professional field or in general?", "Do you have any additional information or notes for the resume?" ] COVER_QUESTIONS_AR = [ "ما اسم الشركة أو الجهة التي تستهدفها برسالة التغطية؟", "ما اسم الوظيفة التي تتقدم لها برسالة التغطية؟", "هل تعرف اسم الشخص المسؤول عن التوظيف؟ (إن وجد)", "لماذا ترغب في العمل بهذه الشركة تحديدًا؟", "ما الذي يجعلك مرشحًا مثاليًا لهذه الوظيفة؟", "ما القيمة التي يمكنك إضافتها إلى الفريق أو الشركة؟", "هل هناك تجربة أو مشروع سابق ترى أنه مناسب لذكره في رسالة التغطية؟ وضّح السبب.", "هل سبق لك التقديم لهذه الشركة أو الوظيفة سابقًا؟ هل خضعت لمقابلة؟ (إن وجدت)", "هل تفضل أن تكون نغمة رسالة التغطية رسمية، مهنية مرنة، أم بأسلوب شخصي قوي؟", "هل ترغب في تضمين سبب تركك لوظيفتك السابقة (إن وجد) ضمن الرسالة؟ (اختياري)" ] COVER_QUESTIONS_EN = [ "What is the name of the company or organization you are targeting with the cover letter?", "What is the job title you are applying for?", "Do you know the name of the hiring manager? (if available)", "Why do you want to work for this company specifically?", "What makes you an ideal candidate for this job?", "What value can you add to the team or company?", "Is there a previous experience or project you think is relevant to mention in the cover letter? Explain why.", "Have you applied to this company or job before? Did you have an interview? (if applicable)", "Do you prefer the tone of the cover letter to be formal, flexible professional, or strong personal style?", "Do you wish to include the reason for leaving your previous job (if any) in the letter? (optional)" ] def reshape_ar(text): try: return get_display(arabic_reshaper.reshape(text)) except Exception as e: print(f"[reshape_ar] error: {e}") return text def parse_resume_sections(text): sections = {} if not text: print("[parse_resume_sections] empty text") return sections lines = text.strip().splitlines() current = None buffer = [] i = 0 while i < len(lines): line = lines[i].strip() if line == "[SECTION_TITLE]": i += 1 if i < len(lines): current = lines[i].strip() buffer = [] elif line == "[SECTION_BODY]": i += 1 while i < len(lines) and lines[i].strip() != "[SECTION_TITLE]": buffer.append(lines[i]) i += 1 if current: sections[current] = "\n".join(buffer).strip() continue else: i += 1 print(f"[parse_resume_sections] found sections: {list(sections.keys())}") return sections def split_text(text, max_width, pdf, font_name, font_size): try: pdf.set_font(font_name, "", font_size) except Exception: pass lines = [] words = text.split() current_line = [] current_width = 0 for word in words: word_width = pdf.get_string_width(word + ' ') if current_width + word_width < max_width: current_line.append(word) current_width += word_width else: lines.append(' '.join(current_line)) current_line = [word] current_width = word_width if current_line: lines.append(' '.join(current_line)) return lines class PDF(FPDF): def __init__(self, font_name, font_paths, lang): super().__init__() self.lang = lang self.font_name = font_name try: if font_paths.get("regular") and os.path.exists(font_paths["regular"]): self.add_font(font_name, "", font_paths["regular"], uni=True) if font_paths.get("bold") and os.path.exists(font_paths["bold"]): self.add_font(font_name, "B", font_paths["bold"], uni=True) print(f"[PDF] loaded fonts for {font_name}") except Exception as e: print(f"[PDF] font load error: {e}") self.font_name = "Arial" self.set_margins(left=5, top=15, right=5) self.set_auto_page_break(auto=True, margin=15) def header_custom(self, text): try: # استخدم الخط العربي إذا كانت اللغة عربية if self.lang == 'ar': self.set_font(self.font_name, 'B', 24) else: self.set_font('Helvetica', 'B', 24) except Exception as e: print(f"[PDF.header_custom] font error: {e}") self.set_font('Helvetica', 'B', 24) self.set_text_color(0, 0, 0) align = 'R' if self.lang == 'ar' else 'C' display = reshape_ar(text) if self.lang == 'ar' else (text or "") try: self.cell(0, 10, display, ln=1, align=align) except Exception as e: print(f"[PDF.header_custom] cell error: {e}") self.ln(3) def contact_info(self, lines): try: self.set_font(self.font_name, '', 10) except Exception: self.set_font('Arial', '', 10) self.set_text_color(0, 0, 0) align = 'R' if self.lang == 'ar' else 'C' for line in lines: line = (line or "").strip() if not line: continue try: display = reshape_ar(line) if self.lang == 'ar' else line self.cell(0, 5, display, ln=1, align=align) except Exception as e: print(f"[PDF.contact_info] error: {e}") self.ln(5) def section_title(self, title): try: if self.lang == 'ar': self.set_font(self.font_name, 'B', 14) else: self.set_font('Helvetica', 'B', 14) except Exception as e: print(f"[PDF.section_title] font error: {e}") self.set_font('Helvetica', 'B', 14) self.set_text_color(0, 0, 0) align = 'R' if self.lang == 'ar' else 'L' display = reshape_ar(title) if self.lang == 'ar' else (title or "") self.cell(0, 8, display, ln=1, align=align) y = self.get_y() self.set_draw_color(0, 0, 0) self.set_line_width(0.2) self.line(10, y, self.w - 10, y) self.ln(5) def section_body(self, body): try: if self.lang == 'ar': self.set_font(self.font_name, '', 10) else: self.set_font('Helvetica', '', 10) except Exception as e: print(f"[PDF.section_body] font error: {e}") self.set_font('Helvetica', '', 10) self.set_text_color(0, 0, 0) align = 'R' if self.lang == 'ar' else 'L' if self.lang == 'ar': body = reshape_ar(body) self.multi_cell(0, 5, body, align=align) self.ln(4) def _process_arabic(self, text): try: reshaped = arabic_reshaper.reshape(text) return get_display(reshaped) except Exception as e: print(f"[PDF._process_arabic] error: {e}") return text def generate_pdf_simple(text, filename, font_name, font_paths, lang, user_data): print(f"[debug] generate_pdf_simple called with filename={filename}, lang={lang}") pdf = PDF(font_name, font_paths, lang) pdf.add_page() sections = parse_resume_sections(text) print(f"[debug] Total sections parsed: {len(sections)}") # حذف الأقسام الفارغة clean_sections = {} for title, body in sections.items(): if body and body.strip() not in ["", "(No information provided)", "N/A", "لا توجد معلومات", "Please provide"]: clean_sections[title] = body.strip() else: print(f"[debug] Skipping empty section: {title}") # العنوان الرئيسي name = "" name_keys = ["الاسم", "Name", "Personal Information", "الاسم الكامل"] for key in name_keys: if key in clean_sections: name_lines = clean_sections[key].splitlines() if name_lines: name = name_lines[0].strip() del clean_sections[key] break if not name and user_data: name = user_data[0] if len(user_data) > 0 else "" # رأس الصفحة pdf.header_custom(name) # طباعة الأقسام الباقية فقط for title, body in clean_sections.items(): if not body or body.strip() in ["", "(No information provided)"]: print(f"[debug] Skipping section: {title}") continue pdf.section_title(title) pdf.section_body(body) try: pdf.output(filename) print(f"[debug] PDF successfully saved: {filename}") except Exception as e: print(f"[error] PDF generation failed: {e}") return filename async def generate_content(prompt, max_tokens=2048, temperature=0.3): print(f"[generate_content] prompt length={len(prompt)}") try: resp = await llm.acomplete(prompt=prompt) text = getattr(resp, "text", "") print(f"[generate_content] received text length={len(text)}") return text.strip() except Exception as e: print(f"[generate_content] LLM call error: {e}") print(traceback.format_exc()) return "" with gr.Blocks(theme=gr.themes.Soft()) as demo: gr.Markdown(" CV Agent/منشئ السيرة الذاتية الذكي") lang_dd = gr.Dropdown(["العربية", "English"], label="🌐 اختر اللغة/ choose a language") start_btn = gr.Button("ابدأ / Start") resume_boxes = [gr.Textbox(label=f"Q{i+1}", visible=False) for i in range(len(RESUME_QUESTIONS_AR))] with gr.Row(visible=False) as main_interface: with gr.Column(scale=2): for tb in resume_boxes: tb.visible = True output_lang_choice_dd = gr.Dropdown(choices=["العربية", "English"], label="لغة السيرة الذاتية/output language") resume_style_dd = gr.Dropdown(choices=RESUME_STYLES, label="📚 نمط السيرة الذاتية/ CV format ") submit_btn = gr.Button("توليد السيرة الذاتية✨/✨generate CV") with gr.Row(visible=False) as output_row: file_output = gr.File(label="📄 السيرة الذاتية/ your CV") cv_preview = gr.Textbox(label="📄 المعاينة", lines=10) cover_preview = gr.Textbox(label="📄 معاينة رسالة التغطية", lines=10, visible=False) def update_questions(lang): print(f"[update_questions] lang={lang}") questions = RESUME_QUESTIONS_AR if lang == "العربية" else RESUME_QUESTIONS_EN updates = [gr.update(label=q, visible=True) for q in questions] return updates lang_dd.change(fn=update_questions, inputs=[lang_dd], outputs=resume_boxes) def show_questions(lang): print(f"[show_questions] lang={lang}") questions = RESUME_QUESTIONS_AR if lang == "العربية" else RESUME_QUESTIONS_EN updates = [gr.update(visible=True)] updates += [gr.update(label=q, visible=True) for q in questions] updates += [gr.update(value=lang), gr.update(value=RESUME_STYLES[0]), gr.update(value="توليد السيرة الذاتية✨/✨generate CV")] return updates start_btn.click(fn=show_questions, inputs=[lang_dd], outputs=[main_interface, *resume_boxes, output_lang_choice_dd, resume_style_dd, submit_btn]) async def process_all(ui_lang, output_lang, resume_style, *answers): print(f"[process_all] called ui_lang={ui_lang} output_lang={output_lang} resume_style={resume_style}") try: questions = RESUME_QUESTIONS_AR if ui_lang == "العربية" else RESUME_QUESTIONS_EN data = "\n".join(f"- {q}: {a}" for q, a in zip(questions, answers) if a and str(a).strip()) style_guideline = RESUME_STYLE_GUIDELINES.get(resume_style, "") prompt = f""" You are a professional resume writer. Write a clean, well-structured resume in {output_lang.upper()}. add [SECTION_TITLE] Before each section title. Then write the section title (e.g., Education, Work Experience...). Then on a new line, write [SECTION_BODY] and the content. Include a section for "Name" with the full name only. Include a section for "Contact Information" with phone, email, location. Do not include notes or advices at the end of resume IMPORTANT: - ignore any section that has no information provided by the user and add to the body "(No information provided)" - Do NOT include any extra information. Return only the resume by the provided informations - Rewrite any poorly written sentences in a professional manner. - Do not copy the user's words exactly; improve and rephrase them professionally. - Follow these style guidelines: {style_guideline} - the technical skill and personal skill must be in two different sections. - when the chosen output language is English, translate the information into English. - write understandable and readable sentences. Client Information: {data} """ print(f"[process_all] prompt length={len(prompt)}") cv_text = await generate_content(prompt) print(f"[process_all] cv_text length={len(cv_text)}") fonts = AR_FONTS if output_lang == "العربية" else EN_FONTS pdf_path = generate_pdf_simple(cv_text, "resume.pdf", "Arial", fonts["Arial"], "ar" if output_lang == "العربية" else "en", answers) print(f"[process_all] returning pdf_path={pdf_path}") return gr.update(visible=True), pdf_path except Exception as e: print(f"[process_all] exception: {e}") print(traceback.format_exc()) return gr.update(visible=True), None submit_btn.click(fn=process_all, inputs=[lang_dd, output_lang_choice_dd, resume_style_dd] + resume_boxes, outputs=[output_row, file_output]) print("[startup] Launching Gradio demo...") demo.launch(debug=True, share=True)