[
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance, race,\nreligion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n- Using welcoming and inclusive language\n- Being respectful of differing viewpoints and experiences\n- Gracefully accepting constructive criticism\n- Focusing on what is best for the community\n- Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n- The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n- Trolling, insulting/derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at . All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Issues Reporting Guidelines\n\nWelcome to the AI Hawk Contributing Guide and Issues Tracker! To keep things organized and ensure issues are resolved quickly, please follow the guidelines below when submitting a bug report, feature request, or any other issue.\n\nIf you have a general question, are curious about how something in Python works, please remember that [Google](https://google.com) is your friend and it can answer many questions.\n\nThis is a work in progress and you may encounter bugs.\n\nThe employers who you are applying to are not looking for candidates who need someone to hold their hand and do everything for them, they are not your parents, they are your potential boses; they will be expecting you to be able to solve simple problems on your own, the AI Hawk mods and devs expect the same of you.\n\nPlease do not beg in the issues tracker, discussions or chat.  We are not here to give you a job, we are here to provide you with a tool for you to go out and find a job on your own.  We will try to have instructions for all steps of the process, but you must read the docs, learn on your own, and understand that this is an open-source project run by volunteers.  It will require you to do some work of your own.\n\nIf you see something that needs to be documented, or some documentation which could be improved, submit a documentation request or document it yourself and submit a PR to help others understand how that part of the software functions and how to use it.\n\n## Before You Submit an Issue\n\n### 1. Search Existing Issues\n\nPlease search through the existing open issues and closed issues to ensure your issue hasn’t already been reported. This helps avoid duplicates and allows us to focus on unresolved problems.\n\n### 2. Check Documentation\n\nReview the README and any available documentation to see if your issue is covered.\n\nWatch this [Intro to AI Hawk video on YouTube](https://www.youtube.com/watch?v=gdW9wogHEUM)\n\nJoin us on [Telegram](https://t.me/AIhawkCommunity) to check with the community about issues and ask for help with issues.  If a dev, mod, contributor or other community member is available, a live conversation will likely resolve your small issues and configuration problems faster than using this issues tracker would.\n\n### 3. Provide Detailed Information\n\nIf you are reporting a bug, make sure you include enough details to reproduce the issue. The more information you provide, the faster we can diagnose and fix the problem.\n\n## Issue Types\n\n### 1. Bug Reports\n\nPlease include the following information:\n\n- **Description:** A clear and concise description of the problem.\n- **Steps to Reproduce:** Provide detailed steps to reproduce the bug.\n- **Expected Behavior:** What should have happened.\n- **Actual Behavior:** What actually happened.\n- **Environment Details:** Include your OS, browser version (if applicable), which LLM you are using and any other relevant environment details.\n- **Logs/Screenshots:** If applicable, attach screenshots or log outputs.\n\n### 2. Feature Requests\n\nFor new features or improvements:\n\n- Clearly describe the feature you would like to see.\n- Explain the problem this feature would solve or the benefit it would bring.\n- If possible, provide examples or references to similar features in other tools or platforms.\n\n### 3. Questions/Discussions\n\n- If you’re unsure whether something is a bug or if you’re seeking clarification on functionality, you can ask a question. The best place to ask a question is on [Telegram](https://t.me/AIhawkCommunity). If you are asking a question on GitHub, please make sure to label your issue as a question.\n\n## Issue Labeling and Response Time\n\nWe use the following labels to categorize issues:\n\n- **bug:** An issue where something isn't functioning as expected.\n- **documentation:** Improvements or additions to project documentation.\n- **duplicate:** This issue or pull request already exists elsewhere.\n- **enhancement:** A request for a new feature or improvement.\n- **good first issue:** A simple issue suitable for newcomers.\n- **help wanted:** The issue needs extra attention or assistance.\n- **invalid:** The issue is not valid or doesn't seem correct.\n- **question:** Additional information or clarification is needed.\n- **wontfix:** The issue will not be fixed or addressed.\n- We aim to respond to issues as early as possible. Please be patient, as maintainers may have limited availability.\n\n## Contributing Fixes\n\nIf you’re able to contribute a fix for an issue:\n\n1. Fork the repository and create a new branch for your fix.\n2. Reference the issue number in your branch and pull request.\n3. Submit a pull request with a detailed description of the changes and how they resolve the issue.\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: feder-cr\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-issue.yml",
    "content": "name: Bug report\ndescription: Report a bug or an issue that isn't working as expected.\ntitle: \"[BUG]: <Provide a clear, descriptive title>\"\nlabels: [\"bug\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please fill out the following information to help us resolve the issue.\n\n  - type: input\n    id: description\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is.\n      placeholder: \"Describe the bug in detail...\"\n\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      description: |\n        Steps to reproduce the behavior:\n        1. Use branch named '...'\n        2. Go to file '...'\n        3. Find property named '...'\n        4. Change '...'\n        5. Run program using command '...'\n        6. See error\n      placeholder: \"List the steps to reproduce the bug...\"\n\n  - type: input\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: What you expected to happen.\n      placeholder: \"What was the expected result?\"\n\n  - type: input\n    id: actual\n    attributes:\n      label: Actual behavior\n      description: What actually happened instead.\n      placeholder: \"What happened instead?\"\n\n  - type: dropdown\n    id: branch\n    attributes:\n      label: Branch\n      description: Specify the branch you were using when the bug occurred.\n      options:\n        - main\n        - other\n\n  - type: input\n    id: otherBranch\n    attributes:\n      label: Branch name\n      description: If you selected ```other``` branch for the previous question, what is the branch name?\n      placeholder: \"what-is-the-name-of-the-branch-you-were-using\"\n\n  - type: input\n    id: pythonVersion\n    attributes:\n      label: Python version\n      description: Specify the version of Python you were using when the bug occurred.\n      placeholder: \"e.g., 3.12.5(64b)\"\n\n  - type: input\n    id: llm\n    attributes:\n      label: LLM Used\n      description: Specify the LLM provider you were using when the bug occurred.\n      placeholder: \"e.g., ChatGPT\"\n\n  - type: input\n    id: model\n    attributes:\n      label: Model used\n      description: Specify the LLM model you were using when the bug occurred.\n      placeholder: \"e.g., GPT-4o-mini\"\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional context\n      description: Add any other context about the problem here.\n      placeholder: \"Any additional information...\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Questions\n    url: t.me/AIhawkCommunity\n    about: You can join the discussions on Telegram.\n  - name: New issue\n    url: >-\n      https://github.com/feder-cr/Auto_Jobs_Applier_AIHawk/blob/v3/.github/CONTRIBUTING.md\n    about: \"Before opening a new issue, please make sure to read CONTRIBUTING.md\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation-issue.yml",
    "content": "name: Documentation request\ndescription: Suggest improvements or additions to the project's documentation.\ntitle: \"[DOCS]: <Provide a short title>\"\nlabels: [\"documentation\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for helping to improve the project's documentation! Please provide the following details to ensure your request is clear.\n\n  - type: input\n    id: doc_section\n    attributes:\n      label: Affected documentation section\n      description: Specify which part of the documentation needs improvement or addition.\n      placeholder: \"e.g., Installation Guide, API Reference...\"\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Documentation improvement description\n      description: Describe the specific improvements or additions you suggest.\n      placeholder: \"Explain what changes you propose and why...\"\n\n  - type: input\n    id: reason\n    attributes:\n      label: Why is this change necessary?\n      description: Explain why the documentation needs to be updated or expanded.\n      placeholder: \"Describe the issue or gap in the documentation...\"\n\n  - type: input\n    id: additional\n    attributes:\n      label: Additional context\n      description: Add any other context, such as related documentation, external resources, or screenshots.\n      placeholder: \"Add any other supporting information...\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement-issue.yml",
    "content": "name: Feature request\ndescription: Suggest a new feature or improvement for the project.\ntitle: \"[FEATURE]: <Provide a descriptive title>\"\nlabels: [\"enhancement\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for suggesting a feature! Please fill out the form below to help us understand your idea.\n\n  - type: input\n    id: summary\n    attributes:\n      label: Feature summary\n      description: Provide a short summary of the feature you're requesting.\n      placeholder: \"Summarize the feature in a few words...\"\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Feature description\n      description: A detailed description of the feature or improvement.\n      placeholder: \"Describe the feature in detail...\"\n\n  - type: input\n    id: motivation\n    attributes:\n      label: Motivation\n      description: Explain why this feature would be beneficial and how it solves a problem.\n      placeholder: \"Why do you need this feature?\"\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives considered\n      description: List any alternative solutions or features you've considered.\n      placeholder: \"Are there any alternative features or solutions you’ve considered?\"\n\n  - type: input\n    id: additional\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots to support your feature request.\n      placeholder: \"Any additional information...\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Python CI\n\non:\n  push:\n  pull_request:\n    types: [opened, synchronize, reopened]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v3\n        with:\n          python-version: '3.x'\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run tests\n        run: pytest "
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Mark and Close Stale Issues\n\non:\n  # Schedule the workflow to run periodically (e.g., daily at 1:30 AM UTC)\n  schedule:\n    - cron: \"30 1 * * *\"\n  workflow_dispatch:\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n\n    steps:\n      - name: Run Stale Action\n        uses: actions/stale@v9\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          days-before-issue-stale: 10 # Days of inactivity before marking an issue as stale\n          days-before-issue-close: 5 # Days after being marked stale before closing the issue\n          stale-issue-label: \"stale\" # Label to apply to stale issues\n          exempt-issue-labels: \"pinned,important\" # Labels to exclude from being marked as stale\n          exempt-issue-assignees: true # Exempt issues with assignees from being marked as stale\n          stale-issue-message: \"This issue has been marked as stale due to inactivity. Please comment or update if this is still relevant.\"\n          close-issue-message: \"This issue was closed due to prolonged inactivity.\"\n          days-before-pr-stale: 10 # Days of inactivity before marking a PR as stale\n          days-before-pr-close: 2 # Days after being marked stale before closing the PR\n          stale-pr-label: \"stale\" # Label to apply to stale PRs\n          exempt-pr-labels: \"pinned,important\" # Labels to exclude from being marked as stale\n          stale-pr-message: >\n            \"This pull request has been marked as stale due to inactivity. \n            To keep it open, you can:\n            - Show progress by updating the PR with new commits.\n            - Continue the conversation by adding comments or requesting clarification on any blockers.\n            - Resolve pending feedback by replying to unresolved comments or implementing suggested changes.\n            - Indicate readiness for review by explicitly requesting a review from maintainers or reviewers.\n            If no action is taken within 7 days, this pull request will be closed.\"\n          close-pr-message: \"This PR was closed due to prolonged inactivity.\"\n          remove-stale-when-updated: true # Remove the stale label if there is new activity\n          operations-per-run: 20 # Number of issues to process per run (default is 30)\n"
  },
  {
    "path": ".gitignore",
    "content": "# application files and logs\n/generated_cv\n/log/*\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\nchrome_profile/*\ndata_folder/output/*\nanswers.json\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n.venv\n.pytest_cache\nvirtual\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv’s dependency resolution may lead to different\n#   Pipfile.lock files generated on each colleague’s machine.\n#   Thus, uncomment the following line if the pipenv environment is expected to be identical\n#   across all environments.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# MacOS\n.DS_Store\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n# PyCharm and all JetBrains IDEs\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n.idea/\n*.iml\n\n# Visual Studio Code\n.vscode/\n\n# Visual Studio 2015/2017/2019/2022\n.vs/\n*.opendb\n*.VC.db\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Mono Auto Generated Files\nmono_crash.*\n\njob_applications/"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Auto_Jobs_Applier_AIHawk\n\n## Table of Contents\n\n- [Issue Labels](#issue-labels)\n- [Bug Reports](#bug-reports)\n- [Feature Requests](#feature-requests)\n- [Branch Rules](#branch-rules)\n- [Version Control](#version-control)\n- [Release Process](#release-process)\n- [Roles](#roles) \n- [Pull Request Process](#pull-request-process)\n- [Code Style Guidelines](#code-style-guidelines)\n- [Development Setup](#development-setup)\n- [Testing](#testing)\n- [Communication](#communication)\n- [Development Diagrams](./docs/development_diagrams.md)\n\nThank you for your interest in contributing to Auto_Jobs_Applier_AIHawk. This document provides guidelines for contributing to the project.\n\n## Issue Labels\n\nThe project uses the following labels:\n\n- **bug**: Something isn't working correctly\n- **enhancement**: New feature requests\n- **good first issue**: Good for newcomers\n- **help wanted**: Extra attention needed\n- **documentation**: Documentation improvements\n\n## Bug Reports\n\nWhen submitting a bug report, please include:\n\n- A clear, descriptive title prefixed with [BUG]\n- Steps to reproduce the issue\n- Expected behavior\n- Actual behavior\n- Any error messages or screenshots\n- Your environment details (OS, Python version, etc.)\n\n## Feature Requests\n\nFor feature requests, please:\n\n- Prefix the title with [FEATURE]\n- Include a feature summary\n- Provide detailed feature description\n- Explain your motivation for the feature\n- List any alternatives you've considered\n\n## Branch Rules\n\n- `main` - Production-ready code, protected branch\n- `develop` - Integration branch for features\n- `feature/*` - New features\n- `release/*` - Release preparation\n- `bugfix/*` - Bug fixes for development\n- `hotfix/*` - Emergency production fixes\n\n## Version Control\n\n- Semantic versioning: `vMAJOR.MINOR.PATCH`\n- Release tags on `main` branch only\n- Package versions match git tags\n\n## Release Process\n\nweek one for `release/v4.1.0`\n\n- Planning meeting for `release/v4.1.0` with release scope and milestone objectives set by the maintainers. Release and maintainer meeting agendas and schedules are posted on the project repository [wiki](https://github.com/AIHawk/AIHawk/wiki) and shared in the `#releases` channel on Discord.\n- `release/v4.0.0` release candidate ready for release\n- `release/v4.0.0` merge into `develop`, `main`\n- tag `main` as `release/v4.0.0`\n- `release/v4.0.0` published to AIHawk/releases and PyPI as a package with release documentation\n- delete `release/v4.0.0` branch\n\nrelease/v4.1.0 release weeks\n\n- Contributers work on issues and PRs, prioritizing next milestone\n- Maintainers review PRs from `feature/*`, `bugfix/*` branches and issues, merging into `develop`\n- Maintainers review PRs from `hotfix/*` branches and issues, merged into `main` and `develop`, `main` tagged and merged into `4.0.1` package and `release/v4.0.1` and `release/v4.1.0`, documentation is updated\n\nlast week, release candidate\n\n- `develop` is frozen, only bug fixes\n- create release branch `release/v4.1.0` from `develop`\n- only bug fixes are merged into `release/v4.1.0`\n- additional testing and release candidate review\n\nweek one is repeated for `release/v4.2.0`\n\n```mermaid\ngantt\n    title Release Cycle Process\n    dateFormat  YYYY-MM-DD\n    section Retro/Plan\n    Planning release/v4.1.0    : 2025-01-01, 2d\n    Publish release/v4.0.0     :milestone, m1, 2025-01-01, 1d\n    \n    section Dev Cycle\n    Feature Development        :2025-01-03, 27d\n    PR Reviews                 :2025-01-03, 27d\n    \n    section Release\n    Freeze develop              :milestone, m3, 2025-01-30, 1d\n    Create release/v4.1.0   :milestone, m4, 2025-01-30, 1d\n    Bug Fixes Only         :2025-01-30, 2d\n    RC Testing             :2025-01-30, 2d\n    \n    section Next Cycle\n    Skip Weekend             :2025-02-01, 2d\n    Planning release/v4.2.0      :2025-02-03, 2d\n    Publish release/v4.1.0     :milestone, m4, 2025-02-03, 1d\n```\n\n## Roles\n\n### Organization Owner\n\n- Has full access to all repositories\n- Controls organization-wide settings and permissions\n- Can set base permissions for all members\n- Manages repository settings and collaborator access\n\n### Release Manager\n\n- Creates and manages release branch from develop\n- Coordinates release cycles and versioning\n- Merges release into main\n\n### Maintainer\n\n- Reviews and approves develop, feature PRs\n- Triage issues, bugs, PRs\n- Manages feature, bugfix PRs merge into develop\n- Leads feature development, bug prioritization\n- Manages README, CONTRIBUTING, and other documentation\n\n### Moderator\n\n- Moderates Telegram, Discord channels\n- Manages project wiki\n- Contributes to README, CONTRIBUTING, and other documentation\n\n### Contributor\n\n- Creates feature branches from develop\n- Implements new features, bug fixes, and other changes\n- creates PRs on features\n- Collaborates with other developers on features\n\n## Pull Request Process\n\n1. Fork the repository\n2. Create a new branch for your feature or bug\n3. Write clear commit messages\n4. Update documentation as needed\n5. Add tests for new functionality\n6. Ensure tests pass\n7. Submit a pull request with a clear description\n\n## Merging Pull Requests\n\n- All PRs are reviewed by maintainers\n- At least 2 Maintainers approve PRs for merge\n- PRs are merged into `develop`\n- PRs are tested and verified to work as expected\n\n## Code Style Guidelines\n\n- Follow PEP 8 standards for Python code\n- Include docstrings for new functions and classes\n- Add comments for complex logic\n- Maintain consistent naming conventions\n- Security best practices\n- Any performance considerations\n\n## Development Setup\n\n1. Clone the repository\n2. Install dependencies from requirements.txt\n3. Set up necessary API keys and configurations\n\n## Testing\n\nBefore submitting a PR:\n\n- Test your changes thoroughly\n- Ensure existing tests pass\n- Add new tests for new functionality\n- Verify functionality with different configurations\n\n## Communication\n\n- Be respectful and constructive in discussions\n- Use clear and concise language\n- Reference relevant issues in commits and PRs\n- Ask for help when needed\n\nThe project maintainers reserve the right to reject any contribution that doesn't meet these guidelines or align with the project's goals.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2024 AI Hawk FOSS <https://github.com/AIHawk-FOSS/Auto_Jobs_Applier_AI_Agent/graphs/contributors> </br>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  1. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "README.md",
    "content": "\n<div align=\"center\">\n\n\n# AIHawk: The first Jobs Applier AI Web Agent\n\n\nAIHawk's core architecture remains **open source**, allowing developers to inspect and extend the codebase. However, due to copyright considerations, we have removed all third‑party provider plugins from this repository.\n\n\n\n---\n\n\nAIHawk has been featured by major media outlets for revolutionizing how job seekers interact with the job market:\n\n[**Business Insider**](https://www.businessinsider.com/aihawk-applies-jobs-for-you-linkedin-risks-inaccuracies-mistakes-2024-11)\n[**TechCrunch**](https://techcrunch.com/2024/10/10/a-reporter-used-ai-to-apply-to-2843-jobs/)\n[**Semafor**](https://www.semafor.com/article/09/12/2024/linkedins-have-nots-and-have-bots)\n[**Dev.by**](https://devby.io/news/ya-razoslal-rezume-na-2843-vakansii-po-17-v-chas-kak-ii-boty-vytesnyaut-ludei-iz-protsessa-naima.amp)\n[**Wired**](https://www.wired.it/article/aihawk-come-automatizzare-ricerca-lavoro/)\n[**The Verge**](https://www.theverge.com/2024/10/10/24266898/ai-is-enabling-job-seekers-to-think-like-spammers)\n[**Vanity Fair**](https://www.vanityfair.it/article/intelligenza-artificiale-candidature-di-lavoro)\n[**404 Media**](https://www.404media.co/i-applied-to-2-843-roles-the-rise-of-ai-powered-job-application-bots/)\n\n"
  },
  {
    "path": "assets/resume_schema.yaml",
    "content": "# YAML Schema for plain_text_resume.yaml\n\npersonal_information:\n  type: object\n  properties:\n    name: {type: string}\n    surname: {type: string}\n    date_of_birth: {type: string, format: date}\n    country: {type: string}\n    zip_code: {type: string, pattern: \"^[0-9]{5,10}$\"}\n    city: {type: string}\n    address: {type: string}\n    phone_prefix: {type: string, format: phone_prefix}\n    phone: {type: string, format: phone}\n    email: {type: string, format: email}\n    github: {type: string, format: uri}\n    linkedin: {type: string, format: uri}\n  required: [name, surname, date_of_birth, country, city, address, zip_code, phone_prefix, phone, email]\n\neducation_details:\n  type: array\n  items:\n    type: object\n    properties:\n      degree: {type: string}\n      university: {type: string}\n      gpa: {type: string}\n      graduation_year: {type: string}\n      field_of_study: {type: string}\n      exam:\n        type: object\n        additionalProperties: {type: string}\n    required: [degree, university, gpa, graduation_year, field_of_study]\n\nexperience_details:\n  type: array\n  items:\n    type: object\n    properties:\n      position: {type: string}\n      company: {type: string}\n      employment_period: {type: string}\n      location: {type: string}\n      industry: {type: string}\n      key_responsibilities:\n        type: object\n        additionalProperties: {type: string}\n      skills_acquired:\n        type: array\n        items: {type: string}\n    required: [position, company, employment_period, location, industry, key_responsibilities, skills_acquired]\n\nprojects:\n  type: array\n  items:\n    type: object\n    properties:\n      name: {type: string}\n      description: {type: string}\n      link: {type: string, format: uri}\n    required: [name, description]\n\nachievements:\n  type: array\n  items:\n    type: object\n    properties:\n      name: {type: string}\n      description: {type: string}\n    required: [name, description]\n\ncertifications:\n  type: array\n  items: {type: string}\n\nlanguages:\n  type: array\n  items:\n    type: object\n    properties:\n      language: {type: string}\n      proficiency: {type: string, enum: [Native, Fluent, Intermediate, Beginner]}\n    required: [language, proficiency]\n\ninterests:\n  type: array\n  items: {type: string}\n\navailability:\n  type: object\n  properties:\n    notice_period: {type: string}\n  required: [notice_period]\n\nsalary_expectations:\n  type: object\n  properties:\n    salary_range_usd: {type: string}\n  required: [salary_range_usd]\n\nself_identification:\n  type: object\n  properties:\n    gender: {type: string}\n    pronouns: {type: string}\n    veteran: {type: string, enum: [Yes, No]}\n    disability: {type: string, enum: [Yes, No]}\n    ethnicity: {type: string}\n  required: [gender, pronouns, veteran, disability, ethnicity]\n\nlegal_authorization:\n  type: object\n  properties:\n    eu_work_authorization: {type: string, enum: [Yes, No]}\n    us_work_authorization: {type: string, enum: [Yes, No]}\n    requires_us_visa: {type: string, enum: [Yes, No]}\n    requires_us_sponsorship: {type: string, enum: [Yes, No]}\n    requires_eu_visa: {type: string, enum: [Yes, No]}\n    legally_allowed_to_work_in_eu: {type: string, enum: [Yes, No]}\n    legally_allowed_to_work_in_us: {type: string, enum: [Yes, No]}\n    requires_eu_sponsorship: {type: string, enum: [Yes, No]}\n  required: [eu_work_authorization, us_work_authorization, requires_us_visa, requires_us_sponsorship, requires_eu_visa, legally_allowed_to_work_in_eu, legally_allowed_to_work_in_us, requires_eu_sponsorship]\n\nwork_preferences:\n  type: object\n  properties:\n    remote_work: {type: string, enum: [Yes, No]}\n    in_person_work: {type: string, enum: [Yes, No]}\n    open_to_relocation: {type: string, enum: [Yes, No]}\n    willing_to_complete_assessments: {type: string, enum: [Yes, No]}\n    willing_to_undergo_drug_tests: {type: string, enum: [Yes, No]}\n    willing_to_undergo_background_checks: {type: string, enum: [Yes, No]}\n  required: [remote_work, in_person_work, open_to_relocation, willing_to_complete_assessments, willing_to_undergo_drug_tests, willing_to_undergo_background_checks]\n"
  },
  {
    "path": "config.py",
    "content": "# In this file, you can set the configurations of the app.\n\nfrom src.utils.constants import DEBUG, ERROR, LLM_MODEL, OPENAI\n\n#config related to logging must have prefix LOG_\nLOG_LEVEL = 'ERROR'\nLOG_SELENIUM_LEVEL = ERROR\nLOG_TO_FILE = False\nLOG_TO_CONSOLE = False\n\nMINIMUM_WAIT_TIME_IN_SECONDS = 60\n\nJOB_APPLICATIONS_DIR = \"job_applications\"\nJOB_SUITABILITY_SCORE = 7\n\nJOB_MAX_APPLICATIONS = 5\nJOB_MIN_APPLICATIONS = 1\n\nLLM_MODEL_TYPE = 'openai'\nLLM_MODEL = 'gpt-4o-mini'\n# Only required for OLLAMA models\nLLM_API_URL = ''\n"
  },
  {
    "path": "data_folder/plain_text_resume.yaml",
    "content": "personal_information:\n  name: \"[Your Name]\"\n  surname: \"[Your Surname]\"\n  date_of_birth: \"[Your Date of Birth]\"\n  country: \"[Your Country]\"\n  city: \"[Your City]\"\n  address: \"[Your Address]\"\n  zip_code: \"[Your zip code]\"\n  phone_prefix: \"[Your Phone Prefix]\"\n  phone: \"[Your Phone Number]\"\n  email: \"[Your Email Address]\"\n  github: \"[Your GitHub Profile URL]\"\n  linkedin: \"[Your LinkedIn Profile URL]\"\n\neducation_details:\n  - education_level: \"[Your Education Level]\"\n    institution: \"[Your Institution]\"\n    field_of_study: \"[Your Field of Study]\"\n    final_evaluation_grade: \"[Your Final Evaluation Grade]\"\n    start_date: \"[Start Date]\"\n    year_of_completion: \"[Year of Completion]\"\n    exam:\n      exam_name_1: \"[Grade]\"\n      exam_name_2: \"[Grade]\"\n      exam_name_3: \"[Grade]\"\n      exam_name_4: \"[Grade]\"\n      exam_name_5: \"[Grade]\"\n      exam_name_6: \"[Grade]\"\n\nexperience_details:\n  - position: \"[Your Position]\"\n    company: \"[Company Name]\"\n    employment_period: \"[Employment Period]\"\n    location: \"[Location]\"\n    industry: \"[Industry]\"\n    key_responsibilities:\n      - responsibility_1: \"[Responsibility Description]\"\n      - responsibility_2: \"[Responsibility Description]\"\n      - responsibility_3: \"[Responsibility Description]\"\n    skills_acquired:\n      - \"[Skill]\"\n      - \"[Skill]\"\n      - \"[Skill]\"\n\n  - position: \"[Your Position]\"\n    company: \"[Company Name]\"\n    employment_period: \"[Employment Period]\"\n    location: \"[Location]\"\n    industry: \"[Industry]\"\n    key_responsibilities:\n      - responsibility_1: \"[Responsibility Description]\"\n      - responsibility_2: \"[Responsibility Description]\"\n      - responsibility_3: \"[Responsibility Description]\"\n    skills_acquired:\n      - \"[Skill]\"\n      - \"[Skill]\"\n      - \"[Skill]\"\n\nprojects:\n  - name: \"[Project Name]\"\n    description: \"[Project Description]\"\n    link: \"[Project Link]\"\n\n  - name: \"[Project Name]\"\n    description: \"[Project Description]\"\n    link: \"[Project Link]\"\n\nachievements:\n  - name: \"[Achievement Name]\"\n    description: \"[Achievement Description]\"\n  - name: \"[Achievement Name]\"\n    description: \"[Achievement Description]\"\n\ncertifications:\n  - name: \"[Certification Name]\"\n    description: \"[Certification Description]\"\n  - name: \"[Certification Name]\"\n    description: \"[Certification Description]\"\n\nlanguages:\n  - language: \"[Language]\"\n    proficiency: \"[Proficiency Level]\"\n  - language: \"[Language]\"\n    proficiency: \"[Proficiency Level]\"\n\ninterests:\n  - \"[Interest]\"\n  - \"[Interest]\"\n  - \"[Interest]\"\n\navailability:\n  notice_period: \"[Notice Period]\"\n\nsalary_expectations:\n  salary_range_usd: \"[Salary Range]\"\n\nself_identification:\n  gender: \"[Gender]\"\n  pronouns: \"[Pronouns]\"\n  veteran: \"[Yes/No]\"\n  disability: \"[Yes/No]\"\n  ethnicity: \"[Ethnicity]\"\n\n\nlegal_authorization:\n  eu_work_authorization: \"[Yes/No]\"\n  us_work_authorization: \"[Yes/No]\"\n  requires_us_visa: \"[Yes/No]\"\n  requires_us_sponsorship: \"[Yes/No]\"\n  requires_eu_visa: \"[Yes/No]\"\n  legally_allowed_to_work_in_eu: \"[Yes/No]\"\n  legally_allowed_to_work_in_us: \"[Yes/No]\"\n  requires_eu_sponsorship: \"[Yes/No]\"\n  canada_work_authorization: \"[Yes/No]\"\n  requires_canada_visa: \"[Yes/No]\"\n  legally_allowed_to_work_in_canada: \"[Yes/No]\"\n  requires_canada_sponsorship: \"[Yes/No]\"\n  uk_work_authorization: \"[Yes/No]\"\n  requires_uk_visa: \"[Yes/No]\"\n  legally_allowed_to_work_in_uk: \"[Yes/No]\"\n  requires_uk_sponsorship: \"[Yes/No]\"\n\n\nwork_preferences:\n  remote_work: \"[Yes/No]\"\n  in_person_work: \"[Yes/No]\"\n  open_to_relocation: \"[Yes/No]\"\n  willing_to_complete_assessments: \"[Yes/No]\"\n  willing_to_undergo_drug_tests: \"[Yes/No]\"\n  willing_to_undergo_background_checks: \"[Yes/No]\"\n"
  },
  {
    "path": "data_folder/secrets.yaml",
    "content": "llm_api_key: 'sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR'\n"
  },
  {
    "path": "data_folder/work_preferences.yaml",
    "content": "remote: true\nhybrid: true\nonsite: true\n\nexperience_level:\n  internship: false\n  entry: true\n  associate: true\n  mid_senior_level: true\n  director: false\n  executive: false\n\njob_types:\n  full_time: true\n  contract: false\n  part_time: false\n  temporary: true\n  internship: false\n  other: false\n  volunteer: true\n\ndate:\n  all_time: false\n  month: false\n  week: false\n  24_hours: true\n\npositions:\n  - Software engineer\n\nlocations:\n  - Germany\n\napply_once_at_company: true\n\ndistance: 100\n\ncompany_blacklist:\n  - wayfair\n  - Crossover\n\ntitle_blacklist:\n  - word1\n  - word2\n\nlocation_blacklist:\n  - Brazil"
  },
  {
    "path": "data_folder_example/plain_text_resume.yaml",
    "content": "personal_information:\n  name: \"solid\"\n  surname: \"snake\"\n  date_of_birth: \"12/01/1861\"\n  country: \"Ireland\"\n  city: \"Dublin\"\n  zip_code: \"520123\"\n  address: \"12 Fox road\"\n  phone_prefix: \"+1\"\n  phone: \"7819117091\"\n  email: \"hi@gmail.com\"\n  github: \"https://github.com/lol\"\n  linkedin: \"https://www.linkedin.com/in/thezucc/\"\n  \n\neducation_details:\n  - education_level: \"Master's Degree\"\n    institution: \"Bob academy\"\n    field_of_study: \"Bobs Engineering\"\n    final_evaluation_grade: \"4.0\"\n    year_of_completion: \"2023\"\n    start_date: \"2022\"\n    additional_info:\n      exam:\n        Algorithms: \"A\"\n        Linear Algebra: \"A\"\n        Database Systems: \"A\"\n        Operating Systems: \"A-\"\n        Web Development: \"A\"\n\nexperience_details:\n  - position: \"X\"\n    company: \"Y.\"\n    employment_period: \"06/2019 - Present\"\n    location: \"San Francisco, CA\"\n    industry: \"Technology\"\n    key_responsibilities:\n      - responsibility: \"Developed web applications using React and Node.js\"\n      - responsibility: \"Collaborated with cross-functional teams to design and implement new features\"\n      - responsibility: \"Troubleshot and resolved complex software issues\"\n    skills_acquired:\n      - \"React\"\n      - \"Node.js\"\n      - \"Software Troubleshooting\"\n  - position: \"Software Developer\"\n    company: \"Innovatech\"\n    employment_period: \"06/2015 - 12/2017\"\n    location: \"Milan, Italy\"\n    industry: \"Technology\"\n    key_responsibilities:\n      - responsibility: \"Developed and maintained web applications using modern technologies\"\n      - responsibility: \"Collaborated with UX/UI designers to enhance user experience\"\n      - responsibility: \"Implemented automated testing procedures to ensure code quality\"\n    skills_acquired:\n      - \"Web development\"\n      - \"User experience design\"\n      - \"Automated testing\"\n  - position: \"Junior Developer\"\n    company: \"StartUp Hub\"\n    employment_period: \"01/2014 - 05/2015\"\n    location: \"Florence, Italy\"\n    industry: \"Startups\"\n    key_responsibilities:\n      - responsibility: \"Assisted in the development of mobile applications and web platforms\"\n      - responsibility: \"Participated in code reviews and contributed to software design discussions\"\n      - responsibility: \"Resolved bugs and implemented feature enhancements\"\n    skills_acquired:\n      - \"Mobile app development\"\n      - \"Code reviews\"\n      - \"Bug fixing\"\nprojects:\n  - name: \"X\"\n    description: \"Y blah blah blah \"\n    link: \"https://github.com/haveagoodday\"\n\n\n\nachievements:\n  - name: \"Employee of the Month\"\n    description: \"Recognized for exceptional performance and contributions to the team.\"\n  - name: \"Hackathon Winner\"\n    description: \"Won first place in a national hackathon competition.\"\n\ncertifications:\n  - name: \"Certified Scrum Master\"\n    description: \"Recognized certification for proficiency in Agile methodologies and Scrum framework.\"\n  - name: \"AWS Certified Solutions Architect\"\n    description: \"Certification demonstrating expertise in designing, deploying, and managing applications on AWS.\"\n\nlanguages:\n  - language: \"English\"\n    proficiency: \"Fluent\"\n  - language: \"Spanish\"\n    proficiency: \"Intermediate\"\n\ninterests:\n  - \"Machine Learning\"\n  - \"Cybersecurity\"\n  - \"Open Source Projects\"\n  - \"Digital Marketing\"\n  - \"Entrepreneurship\"\n\navailability:\n  notice_period: \"2 weeks\"\n\nsalary_expectations:\n  salary_range_usd: \"90000 - 110000\"\n\nself_identification:\n  gender: \"Female\"\n  pronouns: \"She/Her\"\n  veteran: \"No\"\n  disability: \"No\"\n  ethnicity: \"Asian\"\n\nlegal_authorization:\n  eu_work_authorization: \"Yes\"\n  us_work_authorization: \"Yes\"\n  requires_us_visa: \"No\"\n  requires_us_sponsorship: \"Yes\"\n  requires_eu_visa: \"No\"\n  legally_allowed_to_work_in_eu: \"Yes\"\n  legally_allowed_to_work_in_us: \"Yes\"\n  requires_eu_sponsorship: \"No\"\n  canada_work_authorization: \"Yes\"\n  requires_canada_visa: \"No\"\n  legally_allowed_to_work_in_canada: \"Yes\"\n  requires_canada_sponsorship: \"No\"\n  uk_work_authorization: \"Yes\"\n  requires_uk_visa: \"No\"\n  legally_allowed_to_work_in_uk: \"Yes\"\n  requires_uk_sponsorship: \"No\"\n\n\nwork_preferences:\n  remote_work: \"Yes\"\n  in_person_work: \"Yes\"\n  open_to_relocation: \"Yes\"\n  willing_to_complete_assessments: \"Yes\"\n  willing_to_undergo_drug_tests: \"Yes\"\n  willing_to_undergo_background_checks: \"Yes\"\n"
  },
  {
    "path": "data_folder_example/resume_liam_murphy.txt",
    "content": "Liam Murphy\nGalway, Ireland\nEmail: liam.murphy@gmail.com | AIHawk: liam-murphy\nGitHub: liam-murphy | Phone: +353 871234567\n\nEducation\nBachelor's Degree in Computer Science\nNational University of Ireland, Galway (GPA: 4/4)\nGraduation Year: 2020\n\nExperience\nCo-Founder & Software Engineer\nCryptoWave Solutions (03/2021 - Present)\nLocation: Ireland | Industry: Blockchain Technology\n\nCo-founded and led a startup specializing in app and software development with a focus on blockchain technology\nProvided blockchain consultations for 10+ companies, enhancing their software capabilities with secure, decentralized solutions\nDeveloped blockchain applications, integrated cutting-edge technology to meet client needs and drive industry innovation\nResearch Intern\nNational University of Ireland, Galway (11/2022 - 03/2023)\nLocation: Galway, Ireland | Industry: IoT Security Research\n\nConducted in-depth research on IoT security, focusing on binary instrumentation and runtime monitoring\nPerformed in-depth study of the MQTT protocol and Falco\nDeveloped multiple software components including MQTT packet analysis library, Falco adapter, and RML monitor in Prolog\nAuthored thesis \"Binary Instrumentation for Runtime Monitoring of Internet of Things Systems Using Falco\"\nSoftware Engineer\nUniversity Hospital Galway (05/2022 - 11/2022)\nLocation: Galway, Ireland | Industry: Healthcare IT\n\nIntegrated and enforced robust security protocols\nDeveloped and maintained a critical software tool for password validation used by over 1,600 employees\nPlayed an integral role in the hospital's cybersecurity team\nProjects\nJobBot\nAI-driven tool to automate and personalize job applications on AIHawk, gained over 3000 stars on GitHub, improving efficiency and reducing application time\nLink: JobBot\n\nmqtt-packet-parser\nDeveloped a Node.js module for parsing MQTT packets, improved parsing efficiency by 40%\nLink: mqtt-packet-parser\n\nAchievements\nWinner of an Irish public competition - Won first place in a public competition with a perfect score of 70/70, securing a Software Developer position at University Hospital Galway\nGalway Merit Scholarship - Awarded annually from 2018 to 2020 in recognition of academic excellence and contribution\nGitHub Recognition - Gained over 3000 stars on GitHub with JobBot project\nCertifications\nC1\n\nLanguages\nEnglish - Native\nSpanish - Professional\nInterests\nFull-Stack Development, Software Architecture, IoT system design and development, Artificial Intelligence, Cloud Technologies\n\n"
  },
  {
    "path": "data_folder_example/secrets.yaml",
    "content": "llm_api_key: 'sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR'"
  },
  {
    "path": "data_folder_example/work_preferences.yaml",
    "content": "remote: true\nhybrid: true\nonsite: true\n\nexperience_level:\n  internship: false\n  entry: true\n  associate: true\n  mid_senior_level: true\n  director: false\n  executive: false\n\njob_types:\n  full_time: true\n  contract: false\n  part_time: false\n  temporary: true\n  internship: false\n  other: false\n  volunteer: true\n\ndate:\n  all_time: false\n  month: false\n  week: false\n  24_hours: true\n\npositions:\n  - Software engineer\n\nlocations:\n  - Germany\n\napply_once_at_company: true\n\ndistance: 100\n\ncompany_blacklist:\n  - wayfair\n  - Crossover\n\ntitle_blacklist:\n  - word1\n  - word2\n\nlocation_blacklist:\n  - Brazil\n\n"
  },
  {
    "path": "main.py",
    "content": "import base64\nimport sys\nfrom pathlib import Path\nimport traceback\nfrom typing import List, Optional, Tuple, Dict\n\nimport click\nimport inquirer\nimport yaml\nfrom selenium import webdriver\nfrom selenium.common.exceptions import WebDriverException\nfrom selenium.webdriver.chrome.service import Service as ChromeService\nfrom webdriver_manager.chrome import ChromeDriverManager\nimport re\nfrom src.libs.resume_and_cover_builder import ResumeFacade, ResumeGenerator, StyleManager\nfrom src.resume_schemas.job_application_profile import JobApplicationProfile\nfrom src.resume_schemas.resume import Resume\nfrom src.logging import logger\nfrom src.utils.chrome_utils import init_browser\nfrom src.utils.constants import (\n    PLAIN_TEXT_RESUME_YAML,\n    SECRETS_YAML,\n    WORK_PREFERENCES_YAML,\n)\n# from ai_hawk.bot_facade import AIHawkBotFacade\n# from ai_hawk.job_manager import AIHawkJobManager\n# from ai_hawk.llm.llm_manager import GPTAnswerer\n\n\nclass ConfigError(Exception):\n    \"\"\"Custom exception for configuration-related errors.\"\"\"\n    pass\n\n\nclass ConfigValidator:\n    \"\"\"Validates configuration and secrets YAML files.\"\"\"\n\n    EMAIL_REGEX = re.compile(r\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$\")\n    REQUIRED_CONFIG_KEYS = {\n        \"remote\": bool,\n        \"experience_level\": dict,\n        \"job_types\": dict,\n        \"date\": dict,\n        \"positions\": list,\n        \"locations\": list,\n        \"location_blacklist\": list,\n        \"distance\": int,\n        \"company_blacklist\": list,\n        \"title_blacklist\": list,\n    }\n    EXPERIENCE_LEVELS = [\n        \"internship\",\n        \"entry\",\n        \"associate\",\n        \"mid_senior_level\",\n        \"director\",\n        \"executive\",\n    ]\n    JOB_TYPES = [\n        \"full_time\",\n        \"contract\",\n        \"part_time\",\n        \"temporary\",\n        \"internship\",\n        \"other\",\n        \"volunteer\",\n    ]\n    DATE_FILTERS = [\"all_time\", \"month\", \"week\", \"24_hours\"]\n    APPROVED_DISTANCES = {0, 5, 10, 25, 50, 100}\n\n    @staticmethod\n    def validate_email(email: str) -> bool:\n        \"\"\"Validate the format of an email address.\"\"\"\n        return bool(ConfigValidator.EMAIL_REGEX.match(email))\n\n    @staticmethod\n    def load_yaml(yaml_path: Path) -> dict:\n        \"\"\"Load and parse a YAML file.\"\"\"\n        try:\n            with open(yaml_path, \"r\") as stream:\n                return yaml.safe_load(stream)\n        except yaml.YAMLError as exc:\n            raise ConfigError(f\"Error reading YAML file {yaml_path}: {exc}\")\n        except FileNotFoundError:\n            raise ConfigError(f\"YAML file not found: {yaml_path}\")\n\n    @classmethod\n    def validate_config(cls, config_yaml_path: Path) -> dict:\n        \"\"\"Validate the main configuration YAML file.\"\"\"\n        parameters = cls.load_yaml(config_yaml_path)\n        # Check for required keys and their types\n        for key, expected_type in cls.REQUIRED_CONFIG_KEYS.items():\n            if key not in parameters:\n                if key in [\"company_blacklist\", \"title_blacklist\", \"location_blacklist\"]:\n                    parameters[key] = []\n                else:\n                    raise ConfigError(f\"Missing required key '{key}' in {config_yaml_path}\")\n            elif not isinstance(parameters[key], expected_type):\n                if key in [\"company_blacklist\", \"title_blacklist\", \"location_blacklist\"] and parameters[key] is None:\n                    parameters[key] = []\n                else:\n                    raise ConfigError(\n                        f\"Invalid type for key '{key}' in {config_yaml_path}. Expected {expected_type.__name__}.\"\n                    )\n        cls._validate_experience_levels(parameters[\"experience_level\"], config_yaml_path)\n        cls._validate_job_types(parameters[\"job_types\"], config_yaml_path)\n        cls._validate_date_filters(parameters[\"date\"], config_yaml_path)\n        cls._validate_list_of_strings(parameters, [\"positions\", \"locations\"], config_yaml_path)\n        cls._validate_distance(parameters[\"distance\"], config_yaml_path)\n        cls._validate_blacklists(parameters, config_yaml_path)\n        return parameters\n\n    @classmethod\n    def _validate_experience_levels(cls, experience_levels: dict, config_path: Path):\n        \"\"\"Ensure experience levels are booleans.\"\"\"\n        for level in cls.EXPERIENCE_LEVELS:\n            if not isinstance(experience_levels.get(level), bool):\n                raise ConfigError(\n                    f\"Experience level '{level}' must be a boolean in {config_path}\"\n                )\n\n    @classmethod\n    def _validate_job_types(cls, job_types: dict, config_path: Path):\n        \"\"\"Ensure job types are booleans.\"\"\"\n        for job_type in cls.JOB_TYPES:\n            if not isinstance(job_types.get(job_type), bool):\n                raise ConfigError(\n                    f\"Job type '{job_type}' must be a boolean in {config_path}\"\n                )\n\n    @classmethod\n    def _validate_date_filters(cls, date_filters: dict, config_path: Path):\n        \"\"\"Ensure date filters are booleans.\"\"\"\n        for date_filter in cls.DATE_FILTERS:\n            if not isinstance(date_filters.get(date_filter), bool):\n                raise ConfigError(\n                    f\"Date filter '{date_filter}' must be a boolean in {config_path}\"\n                )\n\n    @classmethod\n    def _validate_list_of_strings(cls, parameters: dict, keys: list, config_path: Path):\n        \"\"\"Ensure specified keys are lists of strings.\"\"\"\n        for key in keys:\n            if not all(isinstance(item, str) for item in parameters[key]):\n                raise ConfigError(\n                    f\"'{key}' must be a list of strings in {config_path}\"\n                )\n\n    @classmethod\n    def _validate_distance(cls, distance: int, config_path: Path):\n        \"\"\"Validate the distance value.\"\"\"\n        if distance not in cls.APPROVED_DISTANCES:\n            raise ConfigError(\n                f\"Invalid distance value '{distance}' in {config_path}. Must be one of: {cls.APPROVED_DISTANCES}\"\n            )\n\n    @classmethod\n    def _validate_blacklists(cls, parameters: dict, config_path: Path):\n        \"\"\"Ensure blacklists are lists.\"\"\"\n        for blacklist in [\"company_blacklist\", \"title_blacklist\", \"location_blacklist\"]:\n            if not isinstance(parameters.get(blacklist), list):\n                raise ConfigError(\n                    f\"'{blacklist}' must be a list in {config_path}\"\n                )\n            if parameters[blacklist] is None:\n                parameters[blacklist] = []\n\n    @staticmethod\n    def validate_secrets(secrets_yaml_path: Path) -> str:\n        \"\"\"Validate the secrets YAML file and retrieve the LLM API key.\"\"\"\n        secrets = ConfigValidator.load_yaml(secrets_yaml_path)\n        mandatory_secrets = [\"llm_api_key\"]\n\n        for secret in mandatory_secrets:\n            if secret not in secrets:\n                raise ConfigError(f\"Missing secret '{secret}' in {secrets_yaml_path}\")\n\n            if not secrets[secret]:\n                raise ConfigError(f\"Secret '{secret}' cannot be empty in {secrets_yaml_path}\")\n\n        return secrets[\"llm_api_key\"]\n\n\nclass FileManager:\n    \"\"\"Handles file system operations and validations.\"\"\"\n\n    REQUIRED_FILES = [SECRETS_YAML, WORK_PREFERENCES_YAML, PLAIN_TEXT_RESUME_YAML]\n\n    @staticmethod\n    def validate_data_folder(app_data_folder: Path) -> Tuple[Path, Path, Path, Path]:\n        \"\"\"Validate the existence of the data folder and required files.\"\"\"\n        if not app_data_folder.is_dir():\n            raise FileNotFoundError(f\"Data folder not found: {app_data_folder}\")\n\n        missing_files = [file for file in FileManager.REQUIRED_FILES if not (app_data_folder / file).exists()]\n        if missing_files:\n            raise FileNotFoundError(f\"Missing files in data folder: {', '.join(missing_files)}\")\n\n        output_folder = app_data_folder / \"output\"\n        output_folder.mkdir(exist_ok=True)\n\n        return (\n            app_data_folder / SECRETS_YAML,\n            app_data_folder / WORK_PREFERENCES_YAML,\n            app_data_folder / PLAIN_TEXT_RESUME_YAML,\n            output_folder,\n        )\n\n    @staticmethod\n    def get_uploads(plain_text_resume_file: Path) -> Dict[str, Path]:\n        \"\"\"Convert resume file paths to a dictionary.\"\"\"\n        if not plain_text_resume_file.exists():\n            raise FileNotFoundError(f\"Plain text resume file not found: {plain_text_resume_file}\")\n\n        uploads = {\"plainTextResume\": plain_text_resume_file}\n\n        return uploads\n\n\ndef create_cover_letter(parameters: dict, llm_api_key: str):\n    \"\"\"\n    Logic to create a CV.\n    \"\"\"\n    try:\n        logger.info(\"Generating a CV based on provided parameters.\")\n\n        # Carica il resume in testo semplice\n        with open(parameters[\"uploads\"][\"plainTextResume\"], \"r\", encoding=\"utf-8\") as file:\n            plain_text_resume = file.read()\n\n        style_manager = StyleManager()\n        available_styles = style_manager.get_styles()\n\n        if not available_styles:\n            logger.warning(\"No styles available. Proceeding without style selection.\")\n        else:\n            # Present style choices to the user\n            choices = style_manager.format_choices(available_styles)\n            questions = [\n                inquirer.List(\n                    \"style\",\n                    message=\"Select a style for the resume:\",\n                    choices=choices,\n                )\n            ]\n            style_answer = inquirer.prompt(questions)\n            if style_answer and \"style\" in style_answer:\n                selected_choice = style_answer[\"style\"]\n                for style_name, (file_name, author_link) in available_styles.items():\n                    if selected_choice.startswith(style_name):\n                        style_manager.set_selected_style(style_name)\n                        logger.info(f\"Selected style: {style_name}\")\n                        break\n            else:\n                logger.warning(\"No style selected. Proceeding with default style.\")\n        questions = [\n    inquirer.Text('job_url', message=\"Please enter the URL of the job description:\")\n        ]\n        answers = inquirer.prompt(questions)\n        job_url = answers.get('job_url')\n        resume_generator = ResumeGenerator()\n        resume_object = Resume(plain_text_resume)\n        driver = init_browser()\n        resume_generator.set_resume_object(resume_object)\n        resume_facade = ResumeFacade(            \n            api_key=llm_api_key,\n            style_manager=style_manager,\n            resume_generator=resume_generator,\n            resume_object=resume_object,\n            output_path=Path(\"data_folder/output\"),\n        )\n        resume_facade.set_driver(driver)\n        resume_facade.link_to_job(job_url)\n        result_base64, suggested_name = resume_facade.create_cover_letter()         \n\n        # Decodifica Base64 in dati binari\n        try:\n            pdf_data = base64.b64decode(result_base64)\n        except base64.binascii.Error as e:\n            logger.error(\"Error decoding Base64: %s\", e)\n            raise\n\n        # Definisci il percorso della cartella di output utilizzando `suggested_name`\n        output_dir = Path(parameters[\"outputFileDirectory\"]) / suggested_name\n\n        # Crea la cartella se non esiste\n        try:\n            output_dir.mkdir(parents=True, exist_ok=True)\n            logger.info(f\"Cartella di output creata o già esistente: {output_dir}\")\n        except IOError as e:\n            logger.error(\"Error creating output directory: %s\", e)\n            raise\n        \n        output_path = output_dir / \"cover_letter_tailored.pdf\"\n        try:\n            with open(output_path, \"wb\") as file:\n                file.write(pdf_data)\n            logger.info(f\"CV salvato in: {output_path}\")\n        except IOError as e:\n            logger.error(\"Error writing file: %s\", e)\n            raise\n    except Exception as e:\n        logger.exception(f\"An error occurred while creating the CV: {e}\")\n        raise\n\n\ndef create_resume_pdf_job_tailored(parameters: dict, llm_api_key: str):\n    \"\"\"\n    Logic to create a CV.\n    \"\"\"\n    try:\n        logger.info(\"Generating a CV based on provided parameters.\")\n\n        # Carica il resume in testo semplice\n        with open(parameters[\"uploads\"][\"plainTextResume\"], \"r\", encoding=\"utf-8\") as file:\n            plain_text_resume = file.read()\n\n        style_manager = StyleManager()\n        available_styles = style_manager.get_styles()\n\n        if not available_styles:\n            logger.warning(\"No styles available. Proceeding without style selection.\")\n        else:\n            # Present style choices to the user\n            choices = style_manager.format_choices(available_styles)\n            questions = [\n                inquirer.List(\n                    \"style\",\n                    message=\"Select a style for the resume:\",\n                    choices=choices,\n                )\n            ]\n            style_answer = inquirer.prompt(questions)\n            if style_answer and \"style\" in style_answer:\n                selected_choice = style_answer[\"style\"]\n                for style_name, (file_name, author_link) in available_styles.items():\n                    if selected_choice.startswith(style_name):\n                        style_manager.set_selected_style(style_name)\n                        logger.info(f\"Selected style: {style_name}\")\n                        break\n            else:\n                logger.warning(\"No style selected. Proceeding with default style.\")\n        questions = [inquirer.Text('job_url', message=\"Please enter the URL of the job description:\")]\n        answers = inquirer.prompt(questions)\n        job_url = answers.get('job_url')\n        resume_generator = ResumeGenerator()\n        resume_object = Resume(plain_text_resume)\n        driver = init_browser()\n        resume_generator.set_resume_object(resume_object)\n        resume_facade = ResumeFacade(            \n            api_key=llm_api_key,\n            style_manager=style_manager,\n            resume_generator=resume_generator,\n            resume_object=resume_object,\n            output_path=Path(\"data_folder/output\"),\n        )\n        resume_facade.set_driver(driver)\n        resume_facade.link_to_job(job_url)\n        result_base64, suggested_name = resume_facade.create_resume_pdf_job_tailored()         \n\n        # Decodifica Base64 in dati binari\n        try:\n            pdf_data = base64.b64decode(result_base64)\n        except base64.binascii.Error as e:\n            logger.error(\"Error decoding Base64: %s\", e)\n            raise\n\n        # Definisci il percorso della cartella di output utilizzando `suggested_name`\n        output_dir = Path(parameters[\"outputFileDirectory\"]) / suggested_name\n\n        # Crea la cartella se non esiste\n        try:\n            output_dir.mkdir(parents=True, exist_ok=True)\n            logger.info(f\"Cartella di output creata o già esistente: {output_dir}\")\n        except IOError as e:\n            logger.error(\"Error creating output directory: %s\", e)\n            raise\n        \n        output_path = output_dir / \"resume_tailored.pdf\"\n        try:\n            with open(output_path, \"wb\") as file:\n                file.write(pdf_data)\n            logger.info(f\"CV salvato in: {output_path}\")\n        except IOError as e:\n            logger.error(\"Error writing file: %s\", e)\n            raise\n    except Exception as e:\n        logger.exception(f\"An error occurred while creating the CV: {e}\")\n        raise\n\n\ndef create_resume_pdf(parameters: dict, llm_api_key: str):\n    \"\"\"\n    Logic to create a CV.\n    \"\"\"\n    try:\n        logger.info(\"Generating a CV based on provided parameters.\")\n\n        # Load the plain text resume\n        with open(parameters[\"uploads\"][\"plainTextResume\"], \"r\", encoding=\"utf-8\") as file:\n            plain_text_resume = file.read()\n\n        # Initialize StyleManager\n        style_manager = StyleManager()\n        available_styles = style_manager.get_styles()\n\n        if not available_styles:\n            logger.warning(\"No styles available. Proceeding without style selection.\")\n        else:\n            # Present style choices to the user\n            choices = style_manager.format_choices(available_styles)\n            questions = [\n                inquirer.List(\n                    \"style\",\n                    message=\"Select a style for the resume:\",\n                    choices=choices,\n                )\n            ]\n            style_answer = inquirer.prompt(questions)\n            if style_answer and \"style\" in style_answer:\n                selected_choice = style_answer[\"style\"]\n                for style_name, (file_name, author_link) in available_styles.items():\n                    if selected_choice.startswith(style_name):\n                        style_manager.set_selected_style(style_name)\n                        logger.info(f\"Selected style: {style_name}\")\n                        break\n            else:\n                logger.warning(\"No style selected. Proceeding with default style.\")\n\n        # Initialize the Resume Generator\n        resume_generator = ResumeGenerator()\n        resume_object = Resume(plain_text_resume)\n        driver = init_browser()\n        resume_generator.set_resume_object(resume_object)\n\n        # Create the ResumeFacade\n        resume_facade = ResumeFacade(\n            api_key=llm_api_key,\n            style_manager=style_manager,\n            resume_generator=resume_generator,\n            resume_object=resume_object,\n            output_path=Path(\"data_folder/output\"),\n        )\n        resume_facade.set_driver(driver)\n        result_base64 = resume_facade.create_resume_pdf()\n\n        # Decode Base64 to binary data\n        try:\n            pdf_data = base64.b64decode(result_base64)\n        except base64.binascii.Error as e:\n            logger.error(\"Error decoding Base64: %s\", e)\n            raise\n\n        # Define the output directory using `suggested_name`\n        output_dir = Path(parameters[\"outputFileDirectory\"])\n\n        # Write the PDF file\n        output_path = output_dir / \"resume_base.pdf\"\n        try:\n            with open(output_path, \"wb\") as file:\n                file.write(pdf_data)\n            logger.info(f\"Resume saved at: {output_path}\")\n        except IOError as e:\n            logger.error(\"Error writing file: %s\", e)\n            raise\n    except Exception as e:\n        logger.exception(f\"An error occurred while creating the CV: {e}\")\n        raise\n\n        \ndef handle_inquiries(selected_actions: List[str], parameters: dict, llm_api_key: str):\n    \"\"\"\n    Decide which function to call based on the selected user actions.\n\n    :param selected_actions: List of actions selected by the user.\n    :param parameters: Configuration parameters dictionary.\n    :param llm_api_key: API key for the language model.\n    \"\"\"\n    try:\n        if selected_actions:\n            if \"Generate Resume\" == selected_actions:\n                logger.info(\"Crafting a standout professional resume...\")\n                create_resume_pdf(parameters, llm_api_key)\n                \n            if \"Generate Resume Tailored for Job Description\" == selected_actions:\n                logger.info(\"Customizing your resume to enhance your job application...\")\n                create_resume_pdf_job_tailored(parameters, llm_api_key)\n                \n            if \"Generate Tailored Cover Letter for Job Description\" == selected_actions:\n                logger.info(\"Designing a personalized cover letter to enhance your job application...\")\n                create_cover_letter(parameters, llm_api_key)\n\n        else:\n            logger.warning(\"No actions selected. Nothing to execute.\")\n    except Exception as e:\n        logger.exception(f\"An error occurred while handling inquiries: {e}\")\n        raise\n\ndef prompt_user_action() -> str:\n    \"\"\"\n    Use inquirer to ask the user which action they want to perform.\n\n    :return: Selected action.\n    \"\"\"\n    try:\n        questions = [\n            inquirer.List(\n                'action',\n                message=\"Select the action you want to perform:\",\n                choices=[\n                    \"Generate Resume\",\n                    \"Generate Resume Tailored for Job Description\",\n                    \"Generate Tailored Cover Letter for Job Description\",\n                ],\n            ),\n        ]\n        answer = inquirer.prompt(questions)\n        if answer is None:\n            print(\"No answer provided. The user may have interrupted.\")\n            return \"\"\n        return answer.get('action', \"\")\n    except Exception as e:\n        print(f\"An error occurred: {e}\")\n        return \"\"\n\n\ndef main():\n    \"\"\"Main entry point for the AIHawk Job Application Bot.\"\"\"\n    try:\n        # Define and validate the data folder\n        data_folder = Path(\"data_folder\")\n        secrets_file, config_file, plain_text_resume_file, output_folder = FileManager.validate_data_folder(data_folder)\n\n        # Validate configuration and secrets\n        config = ConfigValidator.validate_config(config_file)\n        llm_api_key = ConfigValidator.validate_secrets(secrets_file)\n\n        # Prepare parameters\n        config[\"uploads\"] = FileManager.get_uploads(plain_text_resume_file)\n        config[\"outputFileDirectory\"] = output_folder\n\n        # Interactive prompt for user to select actions\n        selected_actions = prompt_user_action()\n\n        # Handle selected actions and execute them\n        handle_inquiries(selected_actions, config, llm_api_key)\n\n    except ConfigError as ce:\n        logger.error(f\"Configuration error: {ce}\")\n        logger.error(\n            \"Refer to the configuration guide for troubleshooting: \"\n            \"https://github.com/feder-cr/Auto_Jobs_Applier_AIHawk?tab=readme-ov-file#configuration\"\n        )\n    except FileNotFoundError as fnf:\n        logger.error(f\"File not found: {fnf}\")\n        logger.error(\"Ensure all required files are present in the data folder.\")\n    except RuntimeError as re:\n        logger.error(f\"Runtime error: {re}\")\n        logger.debug(traceback.format_exc())\n    except Exception as e:\n        logger.exception(f\"An unexpected error occurred: {e}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "requirements.txt",
    "content": "click\ngit+https://github.com/feder-cr/lib_resume_builder_AIHawk.git\nhttpx~=0.27.2\ninputimeout==1.0.4\njsonschema==4.23.0\njsonschema-specifications==2023.12.1\nlangchain==0.2.11\nlangchain-anthropic\nlangchain-huggingface\nlangchain-community==0.2.10\nlangchain-core==0.2.36\nlangchain-google-genai==1.0.10\nlangchain-ollama==0.1.3\nlangchain-openai==0.1.17\nlangchain-text-splitters==0.2.2\nlangsmith==0.1.93\nLevenshtein==0.25.1\nloguru==0.7.2\nopenai==1.37.1\npdfminer.six==20221105\npytest>=8.3.3\npython-dotenv~=1.0.1\nPyYAML~=6.0.2\nregex==2024.7.24\nreportlab==4.2.2\nselenium==4.9.1\nwebdriver-manager==4.0.2\npytest\npytest-mock\npytest-cov\nundetected-chromedriver==3.5.5"
  },
  {
    "path": "src/__init__.py",
    "content": ""
  },
  {
    "path": "src/job.py",
    "content": "from dataclasses import dataclass\nfrom src.logging import logger\n\n@dataclass\nclass Job:\n    role: str = \"\"\n    company: str = \"\"\n    location: str = \"\"\n    link: str = \"\"\n    apply_method: str = \"\"\n    description: str = \"\"\n    summarize_job_description: str = \"\"\n    recruiter_link: str = \"\"\n    resume_path: str = \"\"\n    cover_letter_path: str = \"\"\n\n    def formatted_job_information(self):\n        \"\"\"\n        Formats the job information as a markdown string.\n        \"\"\"\n        logger.debug(f\"Formatting job information for job: {self.role} at {self.company}\")\n        job_information = f\"\"\"\n        # Job Description\n        ## Job Information \n        - Position: {self.role}\n        - At: {self.company}\n        - Location: {self.location}\n        - Recruiter Profile: {self.recruiter_link or 'Not available'}\n        \n        ## Description\n        {self.description or 'No description provided.'}\n        \"\"\"\n        formatted_information = job_information.strip()\n        logger.debug(f\"Formatted job information: {formatted_information}\")\n        return formatted_information"
  },
  {
    "path": "src/jobContext.py",
    "content": "from src.job import Job\nfrom src.job_application import JobApplication\n\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass JobContext:\n    job: Job = None\n    job_application: JobApplication = None"
  },
  {
    "path": "src/job_application_saver.py",
    "content": "from src.logging import logger\nimport os\nimport json\nimport shutil\n\nfrom dataclasses import asdict\n\nfrom config import JOB_APPLICATIONS_DIR\nfrom job import Job\nfrom job_application import JobApplication\n\n# Base directory where all applications will be saved\nBASE_DIR = JOB_APPLICATIONS_DIR\n\n\nclass ApplicationSaver:\n\n    def __init__(self, job_application: JobApplication):\n        self.job_application = job_application\n        self.job_application_files_path = None\n\n    # Function to create a directory for each job application\n    def create_application_directory(self):\n        job = self.job_application.job\n\n        # Create a unique directory name using the application ID and company name\n        dir_name = f\"{job.id} - {job.company} {job.title}\"\n        dir_path = os.path.join(BASE_DIR, dir_name)\n\n        # Create the directory if it doesn't exist\n        os.makedirs(dir_path, exist_ok=True)\n        self.job_application_files_path = dir_path\n        return dir_path\n\n    # Function to save the job application details as a JSON file\n    def save_application_details(self):\n\n        if self.job_application_files_path is None:\n            raise ValueError(\n                \"Job application file path is not set. Please create the application directory first.\"\n            )\n\n        json_file_path = os.path.join(\n            self.job_application_files_path, \"job_application.json\"\n        )\n        with open(json_file_path, \"w\") as json_file:\n            json.dump(self.job_application.application, json_file, indent=4)\n\n    # Function to save files like Resume and CV\n    def save_file(self, dir_path, file_path, new_filename):\n        if dir_path is None:\n            raise ValueError(\"dir path cannot be None\")\n\n        # Copy the file to the application directory with a new name\n        destination = os.path.join(dir_path, new_filename)\n        shutil.copy(file_path, destination)\n\n    # Function to save job description as a text file\n    def save_job_description(self):\n        if self.job_application_files_path is None:\n            raise ValueError(\n                \"Job application file path is not set. Please create the application directory first.\"\n            )\n\n        job: Job = self.job_application.job\n\n        json_file_path = os.path.join(\n            self.job_application_files_path, \"job_description.json\"\n        )\n        with open(json_file_path, \"w\") as json_file:\n            json.dump(asdict(job), json_file, indent=4)\n\n    @staticmethod\n    def save(job_application: JobApplication):\n        saver = ApplicationSaver(job_application)\n        saver.create_application_directory()\n        saver.save_application_details()\n        saver.save_job_description()\n        # todo: tempory fix, to rely on resume and cv path from job object instead of job application object\n        if job_application.resume_path:\n            saver.save_file(\n                saver.job_application_files_path,\n                job_application.job.resume_path,\n                \"resume.pdf\",\n            )\n        logger.debug(f\"Saving cover letter to path: {job_application.cover_letter_path}\")\n        if job_application.cover_letter_path:\n            saver.save_file(\n                saver.job_application_files_path,\n                job_application.job.cover_letter_path,\n                \"cover_letter.pdf\"\n            )\n"
  },
  {
    "path": "src/libs/llm_manager.py",
    "content": "import json\nimport os\nimport re\nimport textwrap\nimport time\nfrom abc import ABC, abstractmethod\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Union\n\nimport httpx\nfrom dotenv import load_dotenv\nfrom langchain_core.messages import BaseMessage\nfrom langchain_core.messages.ai import AIMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompt_values import StringPromptValue\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom Levenshtein import distance\n\nimport ai_hawk.llm.prompts as prompts\nfrom config import JOB_SUITABILITY_SCORE\nfrom src.utils.constants import (\n    AVAILABILITY,\n    CERTIFICATIONS,\n    CLAUDE,\n    COMPANY,\n    CONTENT,\n    COVER_LETTER,\n    EDUCATION_DETAILS,\n    EXPERIENCE_DETAILS,\n    FINISH_REASON,\n    GEMINI,\n    HUGGINGFACE,\n    ID,\n    INPUT_TOKENS,\n    INTERESTS,\n    JOB_APPLICATION_PROFILE,\n    JOB_DESCRIPTION,\n    LANGUAGES,\n    LEGAL_AUTHORIZATION,\n    LLM_MODEL_TYPE,\n    LOGPROBS,\n    MODEL,\n    MODEL_NAME,\n    OLLAMA,\n    OPENAI,\n    PERPLEXITY,\n    OPTIONS,\n    OUTPUT_TOKENS,\n    PERSONAL_INFORMATION,\n    PHRASE,\n    PROJECTS,\n    PROMPTS,\n    QUESTION,\n    REPLIES,\n    RESPONSE_METADATA,\n    RESUME,\n    RESUME_EDUCATIONS,\n    RESUME_JOBS,\n    RESUME_PROJECTS,\n    RESUME_SECTION,\n    SALARY_EXPECTATIONS,\n    SELF_IDENTIFICATION,\n    SYSTEM_FINGERPRINT,\n    TEXT,\n    TIME,\n    TOKEN_USAGE,\n    TOTAL_COST,\n    TOTAL_TOKENS,\n    USAGE_METADATA,\n    WORK_PREFERENCES,\n)\nfrom src.job import Job\nfrom src.logging import logger\nimport config as cfg\n\nload_dotenv()\n\n\nclass AIModel(ABC):\n    @abstractmethod\n    def invoke(self, prompt: str) -> str:\n        pass\n\n\nclass OpenAIModel(AIModel):\n    def __init__(self, api_key: str, llm_model: str):\n        from langchain_openai import ChatOpenAI\n\n        self.model = ChatOpenAI(\n            model_name=llm_model, openai_api_key=api_key, temperature=0.4\n        )\n\n    def invoke(self, prompt: str) -> BaseMessage:\n        logger.debug(\"Invoking OpenAI API\")\n        response = self.model.invoke(prompt)\n        return response\n\n\nclass ClaudeModel(AIModel):\n    def __init__(self, api_key: str, llm_model: str):\n        from langchain_anthropic import ChatAnthropic\n\n        self.model = ChatAnthropic(model=llm_model, api_key=api_key, temperature=0.4)\n\n    def invoke(self, prompt: str) -> BaseMessage:\n        response = self.model.invoke(prompt)\n        logger.debug(\"Invoking Claude API\")\n        return response\n\n\nclass OllamaModel(AIModel):\n    def __init__(self, llm_model: str, llm_api_url: str):\n        from langchain_ollama import ChatOllama\n\n        if len(llm_api_url) > 0:\n            logger.debug(f\"Using Ollama with API URL: {llm_api_url}\")\n            self.model = ChatOllama(model=llm_model, base_url=llm_api_url)\n        else:\n            self.model = ChatOllama(model=llm_model)\n\n    def invoke(self, prompt: str) -> BaseMessage:\n        response = self.model.invoke(prompt)\n        return response\n\nclass PerplexityModel(AIModel):\n    def __init__(self, api_key: str, llm_model: str):\n        from langchain_community.chat_models import ChatPerplexity\n        self.model = ChatPerplexity(model=llm_model, api_key=api_key, temperature=0.4)\n\n    def invoke(self, prompt: str) -> BaseMessage:\n        response = self.model.invoke(prompt)\n        return response\n\n# gemini doesn't seem to work because API doesn't rstitute answers for questions that involve answers that are too short\nclass GeminiModel(AIModel):\n    def __init__(self, api_key: str, llm_model: str):\n        from langchain_google_genai import (\n            ChatGoogleGenerativeAI,\n            HarmBlockThreshold,\n            HarmCategory,\n        )\n\n        self.model = ChatGoogleGenerativeAI(\n            model=llm_model,\n            google_api_key=api_key,\n            safety_settings={\n                HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_DEROGATORY: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_TOXICITY: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_VIOLENCE: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_SEXUAL: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_MEDICAL: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_DANGEROUS: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,\n                HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,\n            },\n        )\n\n    def invoke(self, prompt: str) -> BaseMessage:\n        response = self.model.invoke(prompt)\n        return response\n\n\nclass HuggingFaceModel(AIModel):\n    def __init__(self, api_key: str, llm_model: str):\n        from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint\n\n        self.model = HuggingFaceEndpoint(\n            repo_id=llm_model, huggingfacehub_api_token=api_key, temperature=0.4\n        )\n        self.chatmodel = ChatHuggingFace(llm=self.model)\n\n    def invoke(self, prompt: str) -> BaseMessage:\n        response = self.chatmodel.invoke(prompt)\n        logger.debug(\n            f\"Invoking Model from Hugging Face API. Response: {response}, Type: {type(response)}\"\n        )\n        return response\n\n\nclass AIAdapter:\n    def __init__(self, config: dict, api_key: str):\n        self.model = self._create_model(config, api_key)\n\n    def _create_model(self, config: dict, api_key: str) -> AIModel:\n        llm_model_type = cfg.LLM_MODEL_TYPE\n        llm_model = cfg.LLM_MODEL\n\n        llm_api_url = cfg.LLM_API_URL\n\n        logger.debug(f\"Using {llm_model_type} with {llm_model}\")\n\n        if llm_model_type == OPENAI:\n            return OpenAIModel(api_key, llm_model)\n        elif llm_model_type == CLAUDE:\n            return ClaudeModel(api_key, llm_model)\n        elif llm_model_type == OLLAMA:\n            return OllamaModel(llm_model, llm_api_url)\n        elif llm_model_type == GEMINI:\n            return GeminiModel(api_key, llm_model)\n        elif llm_model_type == HUGGINGFACE:\n            return HuggingFaceModel(api_key, llm_model)\n        elif llm_model_type == PERPLEXITY:\n            return PerplexityModel(api_key, llm_model)\n        else:\n            raise ValueError(f\"Unsupported model type: {llm_model_type}\")\n\n    def invoke(self, prompt: str) -> str:\n        return self.model.invoke(prompt)\n\n\nclass LLMLogger:\n    def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel]):\n        self.llm = llm\n        logger.debug(f\"LLMLogger successfully initialized with LLM: {llm}\")\n\n    @staticmethod\n    def log_request(prompts, parsed_reply: Dict[str, Dict]):\n        logger.debug(\"Starting log_request method\")\n        logger.debug(f\"Prompts received: {prompts}\")\n        logger.debug(f\"Parsed reply received: {parsed_reply}\")\n\n        try:\n            calls_log = os.path.join(Path(\"data_folder/output\"), \"open_ai_calls.json\")\n            logger.debug(f\"Logging path determined: {calls_log}\")\n        except Exception as e:\n            logger.error(f\"Error determining the log path: {str(e)}\")\n            raise\n\n        if isinstance(prompts, StringPromptValue):\n            logger.debug(\"Prompts are of type StringPromptValue\")\n            prompts = prompts.text\n            logger.debug(f\"Prompts converted to text: {prompts}\")\n        elif isinstance(prompts, Dict):\n            logger.debug(\"Prompts are of type Dict\")\n            try:\n                prompts = {\n                    f\"prompt_{i + 1}\": prompt.content\n                    for i, prompt in enumerate(prompts.messages)\n                }\n                logger.debug(f\"Prompts converted to dictionary: {prompts}\")\n            except Exception as e:\n                logger.error(f\"Error converting prompts to dictionary: {str(e)}\")\n                raise\n        else:\n            logger.debug(\"Prompts are of unknown type, attempting default conversion\")\n            try:\n                prompts = {\n                    f\"prompt_{i + 1}\": prompt.content\n                    for i, prompt in enumerate(prompts.messages)\n                }\n                logger.debug(\n                    f\"Prompts converted to dictionary using default method: {prompts}\"\n                )\n            except Exception as e:\n                logger.error(f\"Error converting prompts using default method: {str(e)}\")\n                raise\n\n        try:\n            current_time = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n            logger.debug(f\"Current time obtained: {current_time}\")\n        except Exception as e:\n            logger.error(f\"Error obtaining current time: {str(e)}\")\n            raise\n\n        try:\n            token_usage = parsed_reply[USAGE_METADATA]\n            output_tokens = token_usage[OUTPUT_TOKENS]\n            input_tokens = token_usage[INPUT_TOKENS]\n            total_tokens = token_usage[TOTAL_TOKENS]\n            logger.debug(\n                f\"Token usage - Input: {input_tokens}, Output: {output_tokens}, Total: {total_tokens}\"\n            )\n        except KeyError as e:\n            logger.error(f\"KeyError in parsed_reply structure: {str(e)}\")\n            raise\n\n        try:\n            model_name = parsed_reply[RESPONSE_METADATA][MODEL_NAME]\n            logger.debug(f\"Model name: {model_name}\")\n        except KeyError as e:\n            logger.error(f\"KeyError in response_metadata: {str(e)}\")\n            raise\n\n        try:\n            prompt_price_per_token = 0.00000015\n            completion_price_per_token = 0.0000006\n            total_cost = (input_tokens * prompt_price_per_token) + (\n                output_tokens * completion_price_per_token\n            )\n            logger.debug(f\"Total cost calculated: {total_cost}\")\n        except Exception as e:\n            logger.error(f\"Error calculating total cost: {str(e)}\")\n            raise\n\n        try:\n            log_entry = {\n                MODEL: model_name,\n                TIME: current_time,\n                PROMPTS: prompts,\n                REPLIES: parsed_reply[CONTENT],\n                TOTAL_TOKENS: total_tokens,\n                INPUT_TOKENS: input_tokens,\n                OUTPUT_TOKENS: output_tokens,\n                TOTAL_COST: total_cost,\n            }\n            logger.debug(f\"Log entry created: {log_entry}\")\n        except KeyError as e:\n            logger.error(\n                f\"Error creating log entry: missing key {str(e)} in parsed_reply\"\n            )\n            raise\n\n        try:\n            with open(calls_log, \"a\", encoding=\"utf-8\") as f:\n                json_string = json.dumps(log_entry, ensure_ascii=False, indent=4)\n                f.write(json_string + \"\\n\")\n                logger.debug(f\"Log entry written to file: {calls_log}\")\n        except Exception as e:\n            logger.error(f\"Error writing log entry to file: {str(e)}\")\n            raise\n\n\nclass LoggerChatModel:\n    def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel]):\n        self.llm = llm\n        logger.debug(f\"LoggerChatModel successfully initialized with LLM: {llm}\")\n\n    def __call__(self, messages: List[Dict[str, str]]) -> str:\n        logger.debug(f\"Entering __call__ method with messages: {messages}\")\n        while True:\n            try:\n                logger.debug(\"Attempting to call the LLM with messages\")\n\n                reply = self.llm.invoke(messages)\n                logger.debug(f\"LLM response received: {reply}\")\n\n                parsed_reply = self.parse_llmresult(reply)\n                logger.debug(f\"Parsed LLM reply: {parsed_reply}\")\n\n                LLMLogger.log_request(prompts=messages, parsed_reply=parsed_reply)\n                logger.debug(\"Request successfully logged\")\n\n                return reply\n\n            except httpx.HTTPStatusError as e:\n                logger.error(f\"HTTPStatusError encountered: {str(e)}\")\n                if e.response.status_code == 429:\n                    retry_after = e.response.headers.get(\"retry-after\")\n                    retry_after_ms = e.response.headers.get(\"retry-after-ms\")\n\n                    if retry_after:\n                        wait_time = int(retry_after)\n                        logger.warning(\n                            f\"Rate limit exceeded. Waiting for {wait_time} seconds before retrying (extracted from 'retry-after' header)...\"\n                        )\n                        time.sleep(wait_time)\n                    elif retry_after_ms:\n                        wait_time = int(retry_after_ms) / 1000.0\n                        logger.warning(\n                            f\"Rate limit exceeded. Waiting for {wait_time} seconds before retrying (extracted from 'retry-after-ms' header)...\"\n                        )\n                        time.sleep(wait_time)\n                    else:\n                        wait_time = 30\n                        logger.warning(\n                            f\"'retry-after' header not found. Waiting for {wait_time} seconds before retrying (default)...\"\n                        )\n                        time.sleep(wait_time)\n                else:\n                    logger.error(\n                        f\"HTTP error occurred with status code: {e.response.status_code}, waiting 30 seconds before retrying\"\n                    )\n                    time.sleep(30)\n\n            except Exception as e:\n                logger.error(f\"Unexpected error occurred: {str(e)}\")\n                logger.info(\n                    \"Waiting for 30 seconds before retrying due to an unexpected error.\"\n                )\n                time.sleep(30)\n                continue\n\n    def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]:\n        logger.debug(f\"Parsing LLM result: {llmresult}\")\n\n        try:\n            if hasattr(llmresult, USAGE_METADATA):\n                content = llmresult.content\n                response_metadata = llmresult.response_metadata\n                id_ = llmresult.id\n                usage_metadata = llmresult.usage_metadata\n\n                parsed_result = {\n                    CONTENT: content,\n                    RESPONSE_METADATA: {\n                        MODEL_NAME: response_metadata.get(\n                            MODEL_NAME, \"\"\n                        ),\n                        SYSTEM_FINGERPRINT: response_metadata.get(\n                            SYSTEM_FINGERPRINT, \"\"\n                        ),\n                        FINISH_REASON: response_metadata.get(\n                            FINISH_REASON, \"\"\n                        ),\n                        LOGPROBS: response_metadata.get(\n                            LOGPROBS, None\n                        ),\n                    },\n                    ID: id_,\n                    USAGE_METADATA: {\n                        INPUT_TOKENS: usage_metadata.get(\n                            INPUT_TOKENS, 0\n                        ),\n                        OUTPUT_TOKENS: usage_metadata.get(\n                            OUTPUT_TOKENS, 0\n                        ),\n                        TOTAL_TOKENS: usage_metadata.get(\n                            TOTAL_TOKENS, 0\n                        ),\n                    },\n                }\n            else:\n                content = llmresult.content\n                response_metadata = llmresult.response_metadata\n                id_ = llmresult.id\n                token_usage = response_metadata[TOKEN_USAGE]\n\n                parsed_result = {\n                    CONTENT: content,\n                    RESPONSE_METADATA: {\n                        MODEL_NAME: response_metadata.get(\n                            MODEL, \"\"\n                        ),\n                        FINISH_REASON: response_metadata.get(\n                            FINISH_REASON, \"\"\n                        ),\n                    },\n                    ID: id_,\n                    USAGE_METADATA: {\n                        INPUT_TOKENS: token_usage.prompt_tokens,\n                        OUTPUT_TOKENS: token_usage.completion_tokens,\n                        TOTAL_TOKENS: token_usage.total_tokens,\n                    },\n                }\n            logger.debug(f\"Parsed LLM result successfully: {parsed_result}\")\n            return parsed_result\n\n        except KeyError as e:\n            logger.error(f\"KeyError while parsing LLM result: missing key {str(e)}\")\n            raise\n\n        except Exception as e:\n            logger.error(f\"Unexpected error while parsing LLM result: {str(e)}\")\n            raise\n\n\nclass GPTAnswerer:\n    def __init__(self, config, llm_api_key):\n        self.ai_adapter = AIAdapter(config, llm_api_key)\n        self.llm_cheap = LoggerChatModel(self.ai_adapter)\n\n    @property\n    def job_description(self):\n        return self.job.description\n\n    @staticmethod\n    def find_best_match(text: str, options: list[str]) -> str:\n        logger.debug(f\"Finding best match for text: '{text}' in options: {options}\")\n        distances = [\n            (option, distance(text.lower(), option.lower())) for option in options\n        ]\n        best_option = min(distances, key=lambda x: x[1])[0]\n        logger.debug(f\"Best match found: {best_option}\")\n        return best_option\n\n    @staticmethod\n    def _remove_placeholders(text: str) -> str:\n        logger.debug(f\"Removing placeholders from text: {text}\")\n        text = text.replace(\"PLACEHOLDER\", \"\")\n        return text.strip()\n\n    @staticmethod\n    def _preprocess_template_string(template: str) -> str:\n        logger.debug(\"Preprocessing template string\")\n        return textwrap.dedent(template)\n\n    def set_resume(self, resume):\n        logger.debug(f\"Setting resume: {resume}\")\n        self.resume = resume\n\n    def set_job(self, job: Job):\n        logger.debug(f\"Setting job: {job}\")\n        self.job = job\n        self.job.set_summarize_job_description(\n            self.summarize_job_description(self.job.description)\n        )\n\n    def set_job_application_profile(self, job_application_profile):\n        logger.debug(f\"Setting job application profile: {job_application_profile}\")\n        self.job_application_profile = job_application_profile\n\n    def _clean_llm_output(self, output: str) -> str:\n        return output.replace(\"*\", \"\").replace(\"#\", \"\").strip()\n    \n    def summarize_job_description(self, text: str) -> str:\n        logger.debug(f\"Summarizing job description: {text}\")\n        prompts.summarize_prompt_template = self._preprocess_template_string(\n            prompts.summarize_prompt_template\n        )\n        prompt = ChatPromptTemplate.from_template(prompts.summarize_prompt_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        raw_output = chain.invoke({TEXT: text})\n        output = self._clean_llm_output(raw_output)\n        logger.debug(f\"Summary generated: {output}\")\n        return output\n\n    def _create_chain(self, template: str):\n        logger.debug(f\"Creating chain with template: {template}\")\n        prompt = ChatPromptTemplate.from_template(template)\n        return prompt | self.llm_cheap | StrOutputParser()\n\n    def answer_question_textual_wide_range(self, question: str) -> str:\n        logger.debug(f\"Answering textual question: {question}\")\n        chains = {\n            PERSONAL_INFORMATION: self._create_chain(\n                prompts.personal_information_template\n            ),\n            SELF_IDENTIFICATION: self._create_chain(\n                prompts.self_identification_template\n            ),\n            LEGAL_AUTHORIZATION: self._create_chain(\n                prompts.legal_authorization_template\n            ),\n            WORK_PREFERENCES: self._create_chain(\n                prompts.work_preferences_template\n            ),\n            EDUCATION_DETAILS: self._create_chain(\n                prompts.education_details_template\n            ),\n            EXPERIENCE_DETAILS: self._create_chain(\n                prompts.experience_details_template\n            ),\n            PROJECTS: self._create_chain(prompts.projects_template),\n            AVAILABILITY: self._create_chain(prompts.availability_template),\n            SALARY_EXPECTATIONS: self._create_chain(\n                prompts.salary_expectations_template\n            ),\n            CERTIFICATIONS: self._create_chain(\n                prompts.certifications_template\n            ),\n            LANGUAGES: self._create_chain(prompts.languages_template),\n            INTERESTS: self._create_chain(prompts.interests_template),\n            COVER_LETTER: self._create_chain(prompts.coverletter_template),\n        }\n\n        prompt = ChatPromptTemplate.from_template(prompts.determine_section_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        raw_output = chain.invoke({QUESTION: question})\n        output = self._clean_llm_output(raw_output)\n\n        match = re.search(\n            r\"(Personal information|Self Identification|Legal Authorization|Work Preferences|Education \"\n            r\"Details|Experience Details|Projects|Availability|Salary \"\n            r\"Expectations|Certifications|Languages|Interests|Cover letter)\",\n            output,\n            re.IGNORECASE,\n        )\n        if not match:\n            raise ValueError(\"Could not extract section name from the response.\")\n\n        section_name = match.group(1).lower().replace(\" \", \"_\")\n\n        if section_name == \"cover_letter\":\n            chain = chains.get(section_name)\n            raw_output = chain.invoke(\n                {\n                    RESUME: self.resume,\n                    JOB_DESCRIPTION: self.job_description,\n                    COMPANY: self.job.company,\n                }\n            )\n            output = self._clean_llm_output(raw_output)\n            logger.debug(f\"Cover letter generated: {output}\")\n            return output\n        resume_section = getattr(self.resume, section_name, None) or getattr(\n            self.job_application_profile, section_name, None\n        )\n        if resume_section is None:\n            logger.error(\n                f\"Section '{section_name}' not found in either resume or job_application_profile.\"\n            )\n            raise ValueError(\n                f\"Section '{section_name}' not found in either resume or job_application_profile.\"\n            )\n        chain = chains.get(section_name)\n        if chain is None:\n            logger.error(f\"Chain not defined for section '{section_name}'\")\n            raise ValueError(f\"Chain not defined for section '{section_name}'\")\n        raw_output = chain.invoke(\n            {RESUME_SECTION: resume_section, QUESTION: question}\n        )\n        output = self._clean_llm_output(raw_output)\n        logger.debug(f\"Question answered: {output}\")\n        return output\n\n    def answer_question_numeric(\n        self, question: str, default_experience: str = 3\n    ) -> str:\n        logger.debug(f\"Answering numeric question: {question}\")\n        func_template = self._preprocess_template_string(\n            prompts.numeric_question_template\n        )\n        prompt = ChatPromptTemplate.from_template(func_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        raw_output_str = chain.invoke(\n            {\n                RESUME_EDUCATIONS: self.resume.education_details,\n                RESUME_JOBS: self.resume.experience_details,\n                RESUME_PROJECTS: self.resume.projects,\n                QUESTION: question,\n            }\n        )\n        output_str = self._clean_llm_output(raw_output_str)\n        logger.debug(f\"Raw output for numeric question: {output_str}\")\n        try:\n            output = self.extract_number_from_string(output_str)\n            logger.debug(f\"Extracted number: {output}\")\n        except ValueError:\n            logger.warning(\n                f\"Failed to extract number, using default experience: {default_experience}\"\n            )\n            output = default_experience\n        return output\n\n    def extract_number_from_string(self, output_str):\n        logger.debug(f\"Extracting number from string: {output_str}\")\n        numbers = re.findall(r\"\\d+\", output_str)\n        if numbers:\n            logger.debug(f\"Numbers found: {numbers}\")\n            return str(numbers[0])\n        else:\n            logger.error(\"No numbers found in the string\")\n            raise ValueError(\"No numbers found in the string\")\n\n    def answer_question_from_options(self, question: str, options: list[str]) -> str:\n        logger.debug(f\"Answering question from options: {question}\")\n        func_template = self._preprocess_template_string(prompts.options_template)\n        prompt = ChatPromptTemplate.from_template(func_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        raw_output_str = chain.invoke(\n            {\n                RESUME: self.resume,\n                JOB_APPLICATION_PROFILE: self.job_application_profile,\n                QUESTION: question,\n                OPTIONS: options,\n            }\n        )\n        output_str = self._clean_llm_output(raw_output_str)\n        logger.debug(f\"Raw output for options question: {output_str}\")\n        best_option = self.find_best_match(output_str, options)\n        logger.debug(f\"Best option determined: {best_option}\")\n        return best_option\n\n    def determine_resume_or_cover(self, phrase: str) -> str:\n        logger.debug(\n            f\"Determining if phrase refers to resume or cover letter: {phrase}\"\n        )\n        prompt = ChatPromptTemplate.from_template(\n            prompts.resume_or_cover_letter_template\n        )\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        raw_response = chain.invoke({PHRASE: phrase})\n        response = self._clean_llm_output(raw_response)\n        logger.debug(f\"Response for resume_or_cover: {response}\")\n        if \"resume\" in response:\n            return \"resume\"\n        elif \"cover\" in response:\n            return \"cover\"\n        else:\n            return \"resume\"\n\n    def is_job_suitable(self):\n        logger.info(\"Checking if job is suitable\")\n        prompt = ChatPromptTemplate.from_template(prompts.is_relavant_position_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        raw_output = chain.invoke(\n            {\n                RESUME: self.resume,\n                JOB_DESCRIPTION: self.job_description,\n            }\n        )\n        output = self._clean_llm_output(raw_output)\n        logger.debug(f\"Job suitability output: {output}\")\n\n        try:\n            score = re.search(r\"Score:\\s*(\\d+)\", output, re.IGNORECASE).group(1)\n            reasoning = re.search(r\"Reasoning:\\s*(.+)\", output, re.IGNORECASE | re.DOTALL).group(1)\n        except AttributeError:\n            logger.warning(\"Failed to extract score or reasoning from LLM. Proceeding with application, but job may or may not be suitable.\")\n            return True\n\n        logger.info(f\"Job suitability score: {score}\")\n        if int(score) < JOB_SUITABILITY_SCORE:\n            logger.debug(f\"Job is not suitable: {reasoning}\")\n        return int(score) >= JOB_SUITABILITY_SCORE\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/__init__.py",
    "content": "__version__ = '0.1'\n\n# Import all the necessary classes and functions, called when the package is imported\nfrom .resume_generator import ResumeGenerator\nfrom .style_manager import StyleManager\nfrom .resume_facade import ResumeFacade"
  },
  {
    "path": "src/libs/resume_and_cover_builder/config.py",
    "content": "\"\"\"\nThis module is used to store the global configuration of the application.\n\"\"\"\n# app/libs/resume_and_cover_builder/config.py\nfrom pathlib import Path\n\nclass GlobalConfig:\n    def __init__(self):\n        self.STRINGS_MODULE_RESUME_PATH: Path = None\n        self.STRINGS_MODULE_RESUME_JOB_DESCRIPTION_PATH: Path = None\n        self.STRINGS_MODULE_COVER_LETTER_JOB_DESCRIPTION_PATH: Path = None\n        self.STRINGS_MODULE_NAME: str = None\n        self.STYLES_DIRECTORY: Path = None\n        self.LOG_OUTPUT_FILE_PATH: Path = None\n        self.API_KEY: str = None\n        self.html_template = \"\"\"\n                            <!DOCTYPE html>\n                            <html lang=\"en\">\n                            <head>\n                                <meta charset=\"UTF-8\">\n                                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n                                <title>Resume</title>\n                                <link href=\"https://fonts.googleapis.com/css2?family=Barlow:wght@400;600&display=swap\" rel=\"stylesheet\" />\n                                <link href=\"https://fonts.googleapis.com/css2?family=Barlow:wght@400;600&display=swap\" rel=\"stylesheet\" /> \n                                <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css\" /> \n                                    <style>\n                                        $style_css\n                                    </style>\n                            </head>\n                            <body>\n                            $body\n                            </body>\n                            </html>\n                            \"\"\"\n\nglobal_config = GlobalConfig()\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/cover_letter_prompt/__init__.py",
    "content": ""
  },
  {
    "path": "src/libs/resume_and_cover_builder/cover_letter_prompt/strings_feder-cr.py",
    "content": "from src.libs.resume_and_cover_builder.template_base import prompt_cover_letter_template\n\n\ncover_letter_template = \"\"\"\nCompose a brief and impactful cover letter based on the provided job description and resume. The letter should be no longer than three paragraphs and should be written in a professional, yet conversational tone. Avoid using any placeholders, and ensure that the letter flows naturally and is tailored to the job.\n\nAnalyze the job description to identify key qualifications and requirements. Introduce the candidate succinctly, aligning their career objectives with the role. Highlight relevant skills and experiences from the resume that directly match the job’s demands, using specific examples to illustrate these qualifications. Reference notable aspects of the company, such as its mission or values, that resonate with the candidate’s professional goals. Conclude with a strong statement of why the candidate is a good fit for the position, expressing a desire to discuss further.\n\nPlease write the cover letter in a way that directly addresses the job role and the company’s characteristics, ensuring it remains concise and engaging without unnecessary embellishments. The letter should be formatted into paragraphs and should not include a greeting or signature.\n\n## Rules:\n- Do not include any introductions, explanations, or additional information.\n\n## Details :\n- **Job Description:**\n```\n{job_description}\n```\n- **My resume:**\n```\n{resume}\n```\n\"\"\"+ prompt_cover_letter_template\n\n\nsummarize_prompt_template = \"\"\"\nAs a seasoned HR expert, your task is to identify and outline the key skills and requirements necessary for the position of this job. Use the provided job description as input to extract all relevant information. This will involve conducting a thorough analysis of the job's responsibilities and the industry standards. You should consider both the technical and soft skills needed to excel in this role. Additionally, specify any educational qualifications, certifications, or experiences that are essential. Your analysis should also reflect on the evolving nature of this role, considering future trends and how they might affect the required competencies.\n\nRules:\nRemove boilerplate text\nInclude only relevant information to match the job description against the resume\n\n# Analysis Requirements\nYour analysis should include the following sections:\nTechnical Skills: List all the specific technical skills required for the role based on the responsibilities described in the job description.\nSoft Skills: Identify the necessary soft skills, such as communication abilities, problem-solving, time management, etc.\nEducational Qualifications and Certifications: Specify the essential educational qualifications and certifications for the role.\nProfessional Experience: Describe the relevant work experiences that are required or preferred.\nRole Evolution: Analyze how the role might evolve in the future, considering industry trends and how these might influence the required skills.\n\n# Final Result:\nYour analysis should be structured in a clear and organized document with distinct sections for each of the points listed above. Each section should contain:\nThis comprehensive overview will serve as a guideline for the recruitment process, ensuring the identification of the most qualified candidates.\n\n# Job Description:\n```\n{text}\n```\n\n---\n\n# Job Description Summary\"\"\""
  },
  {
    "path": "src/libs/resume_and_cover_builder/llm/llm_generate_cover_letter_from_job.py",
    "content": "\"\"\"\nThis creates the cover letter (in html, utils will then convert in PDF) matching with job description and plain-text resume\n\"\"\"\n# app/libs/resume_and_cover_builder/llm_generate_cover_letter_from_job.py\nimport os\nimport textwrap\nfrom ..utils import LoggerChatModel\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_openai import ChatOpenAI, OpenAIEmbeddings\nfrom pathlib import Path\nfrom dotenv import load_dotenv\nfrom requests.exceptions import HTTPError as HTTPStatusError\nfrom pathlib import Path\nfrom loguru import logger\n\n# Load environment variables from .env file\nload_dotenv()\n\n# Configure log file\nlog_folder = 'log/cover_letter/gpt_cover_letter_job_descr'\nif not os.path.exists(log_folder):\n    os.makedirs(log_folder)\nlog_path = Path(log_folder).resolve()\nlogger.add(log_path / \"gpt_cover_letter_job_descr.log\", rotation=\"1 day\", compression=\"zip\", retention=\"7 days\", level=\"DEBUG\")\n\nclass LLMCoverLetterJobDescription:\n    def __init__(self, openai_api_key, strings):\n        self.llm_cheap = LoggerChatModel(ChatOpenAI(model_name=\"gpt-4o-mini\", openai_api_key=openai_api_key, temperature=0.4))\n        self.llm_embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)\n        self.strings = strings\n\n    @staticmethod\n    def _preprocess_template_string(template: str) -> str:\n        \"\"\"\n        Preprocess the template string by removing leading whitespace and indentation.\n        Args:\n            template (str): The template string to preprocess.\n        Returns:\n            str: The preprocessed template string.\n        \"\"\"\n        return textwrap.dedent(template)\n\n    def set_resume(self, resume) -> None:\n        \"\"\"\n        Set the resume text to be used for generating the cover letter.\n        Args:\n            resume (str): The plain text resume to be used.\n        \"\"\"\n        self.resume = resume\n\n    def set_job_description_from_text(self, job_description_text) -> None:\n        \"\"\"\n        Set the job description text to be used for generating the cover letter.\n        Args:\n            job_description_text (str): The plain text job description to be used.\n        \"\"\"\n        logger.debug(\"Starting job description summarization...\")\n        prompt = ChatPromptTemplate.from_template(self.strings.summarize_prompt_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        output = chain.invoke({\"text\": job_description_text})\n        self.job_description = output\n        logger.debug(f\"Job description summarization complete: {self.job_description}\")\n\n    def generate_cover_letter(self) -> str:\n        \"\"\"\n        Generate the cover letter based on the job description and resume.\n        Returns:\n            str: The generated cover letter\n        \"\"\"\n        logger.debug(\"Starting cover letter generation...\")\n        prompt_template = self._preprocess_template_string(self.strings.cover_letter_template)\n        logger.debug(f\"Cover letter template after preprocessing: {prompt_template}\")\n\n        prompt = ChatPromptTemplate.from_template(prompt_template)\n        logger.debug(f\"Prompt created: {prompt}\")\n\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        logger.debug(f\"Chain created: {chain}\")\n\n        input_data = {\n            \"job_description\": self.job_description,\n            \"resume\": self.resume\n        }\n        logger.debug(f\"Input data: {input_data}\")\n\n        output = chain.invoke(input_data)\n        logger.debug(f\"Cover letter generation result: {output}\")\n\n        logger.debug(\"Cover letter generation completed\")\n        return output\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/llm/llm_generate_resume.py",
    "content": "\"\"\"\nCreate a class that generates a resume based on a resume and a resume template.\n\"\"\"\n# app/libs/resume_and_cover_builder/gpt_resume.py\nimport os\nimport textwrap\nfrom src.libs.resume_and_cover_builder.utils import LoggerChatModel\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_openai import ChatOpenAI\nfrom dotenv import load_dotenv\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom loguru import logger\nfrom pathlib import Path\n\n# Load environment variables from .env file\nload_dotenv()\n\n# Configure log file\nlog_folder = 'log/resume/gpt_resume'\nif not os.path.exists(log_folder):\n    os.makedirs(log_folder)\nlog_path = Path(log_folder).resolve()\nlogger.add(log_path / \"gpt_resume.log\", rotation=\"1 day\", compression=\"zip\", retention=\"7 days\", level=\"DEBUG\")\n\nclass LLMResumer:\n    def __init__(self, openai_api_key, strings):\n        self.llm_cheap = LoggerChatModel(\n            ChatOpenAI(\n                model_name=\"gpt-4o-mini\", openai_api_key=openai_api_key, temperature=0.4\n            )\n        )\n        self.strings = strings\n\n    @staticmethod\n    def _preprocess_template_string(template: str) -> str:\n        \"\"\"\n        Preprocess the template string by removing leading whitespace and indentation.\n        Args:\n            template (str): The template string to preprocess.\n        Returns:\n            str: The preprocessed template string.\n        \"\"\"\n        return textwrap.dedent(template)\n\n    def set_resume(self, resume) -> None:\n        \"\"\"\n        Set the resume object to be used for generating the resume.\n        Args:\n            resume (Resume): The resume object to be used.\n        \"\"\"\n        self.resume = resume\n\n    def generate_header(self, data = None) -> str:\n        \"\"\"\n        Generate the header section of the resume.\n        Args:\n            data (dict): The personal information to use for generating the header.\n        Returns:\n            str: The generated header section.\n        \"\"\"\n        header_prompt_template = self._preprocess_template_string(\n            self.strings.prompt_header\n        )\n        prompt = ChatPromptTemplate.from_template(header_prompt_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        input_data = {\n            \"personal_information\": self.resume.personal_information\n        } if data is None else data\n        output = chain.invoke(input_data)\n        return output\n    \n    def generate_education_section(self, data = None) -> str:\n        \"\"\"\n        Generate the education section of the resume.\n        Args:\n            data (dict): The education details to use for generating the education section.\n        Returns:\n            str: The generated education section.\n        \"\"\"\n        logger.debug(\"Starting education section generation\")\n\n        education_prompt_template = self._preprocess_template_string(self.strings.prompt_education)\n        logger.debug(f\"Education template: {education_prompt_template}\")\n\n        prompt = ChatPromptTemplate.from_template(education_prompt_template)\n        logger.debug(f\"Prompt: {prompt}\")\n        \n        chain = prompt | self.llm_cheap | StrOutputParser()\n        logger.debug(f\"Chain created: {chain}\")\n        \n        input_data = {\n            \"education_details\": self.resume.education_details\n        } if data is None else data\n        output = chain.invoke(input_data)\n        logger.debug(f\"Chain invocation result: {output}\")\n\n        logger.debug(\"Education section generation completed\")\n        return output\n\n    def generate_work_experience_section(self, data = None) -> str:\n        \"\"\"\n        Generate the work experience section of the resume.\n        Args:\n            data (dict): The work experience details to use for generating the work experience section.\n        Returns:\n            str: The generated work experience section.\n        \"\"\"\n        logger.debug(\"Starting work experience section generation\")\n\n        work_experience_prompt_template = self._preprocess_template_string(self.strings.prompt_working_experience)\n        logger.debug(f\"Work experience template: {work_experience_prompt_template}\")\n\n        prompt = ChatPromptTemplate.from_template(work_experience_prompt_template)\n        logger.debug(f\"Prompt: {prompt}\")\n        \n        chain = prompt | self.llm_cheap | StrOutputParser()\n        logger.debug(f\"Chain created: {chain}\")\n        \n        input_data = {\n            \"experience_details\": self.resume.experience_details\n        } if data is None else data\n        output = chain.invoke(input_data)\n        logger.debug(f\"Chain invocation result: {output}\")\n\n        logger.debug(\"Work experience section generation completed\")\n        return output\n\n    def generate_projects_section(self, data = None) -> str:\n        \"\"\"\n        Generate the side projects section of the resume.\n        Args:\n            data (dict): The side projects to use for generating the side projects section.\n        Returns:\n            str: The generated side projects section.\n        \"\"\"\n        logger.debug(\"Starting side projects section generation\")\n\n        projects_prompt_template = self._preprocess_template_string(self.strings.prompt_projects)\n        logger.debug(f\"Side projects template: {projects_prompt_template}\")\n\n        prompt = ChatPromptTemplate.from_template(projects_prompt_template)\n        logger.debug(f\"Prompt: {prompt}\")\n        \n        chain = prompt | self.llm_cheap | StrOutputParser()\n        logger.debug(f\"Chain created: {chain}\")\n        \n        input_data = {\n            \"projects\": self.resume.projects\n        } if data is None else data\n        output = chain.invoke(input_data)\n        logger.debug(f\"Chain invocation result: {output}\")\n\n        logger.debug(\"Side projects section generation completed\")\n        return output\n\n    def generate_achievements_section(self, data = None) -> str:\n        \"\"\"\n        Generate the achievements section of the resume.\n        Args:\n            data (dict): The achievements to use for generating the achievements section.\n        Returns:\n            str: The generated achievements section.\n        \"\"\"\n        logger.debug(\"Starting achievements section generation\")\n\n        achievements_prompt_template = self._preprocess_template_string(self.strings.prompt_achievements)\n        logger.debug(f\"Achievements template: {achievements_prompt_template}\")\n\n        prompt = ChatPromptTemplate.from_template(achievements_prompt_template)\n        logger.debug(f\"Prompt: {prompt}\")\n\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        logger.debug(f\"Chain created: {chain}\")\n\n        input_data = {\n            \"achievements\": self.resume.achievements,\n            \"certifications\": self.resume.certifications,\n        } if data is None else data\n        logger.debug(f\"Input data for the chain: {input_data}\")\n\n        output = chain.invoke(input_data)\n        logger.debug(f\"Chain invocation result: {output}\")\n\n        logger.debug(\"Achievements section generation completed\")\n        return output\n\n    def generate_certifications_section(self, data = None) -> str:\n        \"\"\"\n        Generate the certifications section of the resume.\n        Returns:\n            str: The generated certifications section.\n        \"\"\"\n        logger.debug(\"Starting Certifications section generation\")\n\n        certifications_prompt_template = self._preprocess_template_string(self.strings.prompt_certifications)\n        logger.debug(f\"Certifications template: {certifications_prompt_template}\")\n\n        prompt = ChatPromptTemplate.from_template(certifications_prompt_template)\n        logger.debug(f\"Prompt: {prompt}\")\n\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        logger.debug(f\"Chain created: {chain}\")\n\n        input_data = {\n            \"certifications\": self.resume.certifications\n        } if data is None else data\n        logger.debug(f\"Input data for the chain: {input_data}\")\n\n        output = chain.invoke(input_data)\n        logger.debug(f\"Chain invocation result: {output}\")\n\n        logger.debug(\"Certifications section generation completed\")\n        return output\n    \n    def generate_additional_skills_section(self, data = None) -> str:\n        \"\"\"\n        Generate the additional skills section of the resume.\n        Returns:\n            str: The generated additional skills section.\n        \"\"\"\n        additional_skills_prompt_template = self._preprocess_template_string(self.strings.prompt_additional_skills)\n        \n        skills = set()\n        if self.resume.experience_details:\n            for exp in self.resume.experience_details:\n                if exp.skills_acquired:\n                    skills.update(exp.skills_acquired)\n\n        if self.resume.education_details:\n            for edu in self.resume.education_details:\n                if edu.exam:\n                    for exam in edu.exam:\n                        skills.update(exam.keys())\n        prompt = ChatPromptTemplate.from_template(additional_skills_prompt_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        input_data = {\n            \"languages\": self.resume.languages,\n            \"interests\": self.resume.interests,\n            \"skills\": skills,\n        } if data is None else data\n        output = chain.invoke(input_data)\n        \n        return output\n\n    def generate_html_resume(self) -> str:\n        \"\"\"\n        Generate the full HTML resume based on the resume object.\n        Returns:\n            str: The generated HTML resume.\n        \"\"\"\n        def header_fn():\n            if self.resume.personal_information:\n                return self.generate_header()\n            return \"\"\n\n        def education_fn():\n            if self.resume.education_details:\n                return self.generate_education_section()\n            return \"\"\n\n        def work_experience_fn():\n            if self.resume.experience_details:\n                return self.generate_work_experience_section()\n            return \"\"\n\n        def projects_fn():\n            if self.resume.projects:\n                return self.generate_projects_section()\n            return \"\"\n\n        def achievements_fn():\n            if self.resume.achievements:\n                return self.generate_achievements_section()\n            return \"\"\n        \n        def certifications_fn():\n            if self.resume.certifications:\n                return self.generate_certifications_section()\n            return \"\"\n\n        def additional_skills_fn():\n            if (self.resume.experience_details or self.resume.education_details or\n                self.resume.languages or self.resume.interests):\n                return self.generate_additional_skills_section()\n            return \"\"\n\n        # Create a dictionary to map the function names to their respective callables\n        functions = {\n            \"header\": header_fn,\n            \"education\": education_fn,\n            \"work_experience\": work_experience_fn,\n            \"projects\": projects_fn,\n            \"achievements\": achievements_fn,\n            \"certifications\": certifications_fn,\n            \"additional_skills\": additional_skills_fn,\n        }\n\n        # Use ThreadPoolExecutor to run the functions in parallel\n        with ThreadPoolExecutor() as executor:\n            future_to_section = {executor.submit(fn): section for section, fn in functions.items()}\n            results = {}\n            for future in as_completed(future_to_section):\n                section = future_to_section[future]\n                try:\n                    result = future.result()\n                    if result:\n                        results[section] = result\n                except Exception as exc:\n                    logger.error(f'{section} raised an exception: {exc}')\n        full_resume = \"<body>\\n\"\n        full_resume += f\"  {results.get('header', '')}\\n\"\n        full_resume += \"  <main>\\n\"\n        full_resume += f\"    {results.get('education', '')}\\n\"\n        full_resume += f\"    {results.get('work_experience', '')}\\n\"\n        full_resume += f\"    {results.get('projects', '')}\\n\"\n        full_resume += f\"    {results.get('achievements', '')}\\n\"\n        full_resume += f\"    {results.get('certifications', '')}\\n\"\n        full_resume += f\"    {results.get('additional_skills', '')}\\n\"\n        full_resume += \"  </main>\\n\"\n        full_resume += \"</body>\"\n        return full_resume\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/llm/llm_generate_resume_from_job.py",
    "content": "\"\"\"\nCreate a class that generates a job description based on a resume and a job description template.\n\"\"\"\n# app/libs/resume_and_cover_builder/llm_generate_resume_from_job.py\nimport os\nfrom src.libs.resume_and_cover_builder.llm.llm_generate_resume import LLMResumer\nfrom src.libs.resume_and_cover_builder.utils import LoggerChatModel\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_openai import ChatOpenAI\nfrom dotenv import load_dotenv\nfrom loguru import logger\nfrom pathlib import Path\n\n# Load environment variables from .env file\nload_dotenv()\n\nlog_folder = 'log/resume/gpt_resum_job_descr'\nif not os.path.exists(log_folder):\n    os.makedirs(log_folder)\nlog_path = Path(log_folder).resolve()\nlogger.add(log_path / \"gpt_resum_job_descr.log\", rotation=\"1 day\", compression=\"zip\", retention=\"7 days\", level=\"DEBUG\")\n\nclass LLMResumeJobDescription(LLMResumer):\n    def __init__(self, openai_api_key, strings):\n        super().__init__(openai_api_key, strings)\n\n    def set_job_description_from_text(self, job_description_text) -> None:\n        \"\"\"\n        Set the job description text to be used for generating the resume.\n        Args:\n            job_description_text (str): The plain text job description to be used.\n        \"\"\"\n        prompt = ChatPromptTemplate.from_template(self.strings.summarize_prompt_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        output = chain.invoke({\"text\": job_description_text})\n        self.job_description = output\n    \n    def generate_header(self) -> str:\n        \"\"\"\n        Generate the header section of the resume.\n        Returns:\n            str: The generated header section.\n        \"\"\"\n        return super().generate_header(data={\n            \"personal_information\": self.resume.personal_information,\n            \"job_description\": self.job_description\n        })\n\n    def generate_education_section(self) -> str:\n        \"\"\"\n        Generate the education section of the resume.\n        Returns:\n            str: The generated education section.\n        \"\"\"\n        return super().generate_education_section(data={\n            \"education_details\": self.resume.education_details,\n            \"job_description\": self.job_description\n        })\n\n    def generate_work_experience_section(self) -> str:\n        \"\"\"\n        Generate the work experience section of the resume.\n        Returns:\n            str: The generated work experience section.\n        \"\"\"\n        return super().generate_work_experience_section(data={\n            \"experience_details\": self.resume.experience_details,\n            \"job_description\": self.job_description\n        })\n\n    def generate_projects_section(self) -> str:\n        \"\"\"\n        Generate the side projects section of the resume.\n        Returns:\n            str: The generated side projects section.\n        \"\"\"\n        return super().generate_projects_section(data={\n            \"projects\": self.resume.projects,\n            \"job_description\": self.job_description\n        })\n\n    def generate_achievements_section(self) -> str:\n        \"\"\"\n        Generate the achievements section of the resume.\n        Returns:\n            str: The generated achievements section.\n        \"\"\"\n        return super().generate_achievements_section(data={\n            \"achievements\": self.resume.achievements,\n            \"job_description\": self.job_description\n        })\n\n\n    def generate_certifications_section(self) -> str:\n        \"\"\"\n        Generate the certifications section of the resume.\n        Returns:\n            str: The generated certifications section.\n        \"\"\"\n        return super().generate_certifications_section(data={\n            \"certifications\": self.resume.certifications,\n            \"job_description\": self.job_description\n        })\n\n    def generate_additional_skills_section(self) -> str:\n        \"\"\"\n        Generate the additional skills section of the resume.\n        Returns:\n            str: The generated additional skills section.\n        \"\"\"\n        additional_skills_prompt_template = self._preprocess_template_string(\n            self.strings.prompt_additional_skills\n        )\n        skills = set()\n        if self.resume.experience_details:\n            for exp in self.resume.experience_details:\n                if exp.skills_acquired:\n                    skills.update(exp.skills_acquired)\n\n        if self.resume.education_details:\n            for edu in self.resume.education_details:\n                if edu.exam:\n                    for exam in edu.exam:\n                        skills.update(exam.keys())\n        prompt = ChatPromptTemplate.from_template(additional_skills_prompt_template)\n        chain = prompt | self.llm_cheap | StrOutputParser()\n        output = chain.invoke({\n            \"languages\": self.resume.languages,\n            \"interests\": self.resume.interests,\n            \"skills\": skills,\n            \"job_description\": self.job_description\n        })\n        return output\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/llm/llm_job_parser.py",
    "content": "import os\nimport tempfile\nimport textwrap\nimport time\nimport re  # For email validation\nfrom src.libs.resume_and_cover_builder.utils import LoggerChatModel\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate, PromptTemplate\nfrom langchain_openai import ChatOpenAI\nfrom dotenv import load_dotenv\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom loguru import logger\nfrom pathlib import Path\nfrom langchain_core.prompt_values import StringPromptValue\nfrom langchain_core.runnables import RunnablePassthrough\nfrom langchain_text_splitters import TokenTextSplitter\nfrom langchain_community.embeddings import OpenAIEmbeddings\nfrom langchain_community.vectorstores import FAISS\nfrom lib_resume_builder_AIHawk.config import global_config\nfrom langchain_community.document_loaders import TextLoader\nfrom requests.exceptions import HTTPError as HTTPStatusError  # HTTP error handling\nimport openai\n\n# Load environment variables from the .env file\nload_dotenv()\n\n# Configure the log file\nlog_folder = 'log/resume/gpt_resume'\nif not os.path.exists(log_folder):\n    os.makedirs(log_folder)\nlog_path = Path(log_folder).resolve()\nlogger.add(log_path / \"gpt_resume.log\", rotation=\"1 day\", compression=\"zip\", retention=\"7 days\", level=\"DEBUG\")\n\n\nclass LLMParser:\n    def __init__(self, openai_api_key):\n        self.llm = LoggerChatModel(\n            ChatOpenAI(\n                model_name=\"gpt-4o-mini\", openai_api_key=openai_api_key, temperature=0.4\n            )\n        )\n        self.llm_embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)  # Initialize embeddings\n        self.vectorstore = None  # Will be initialized after document loading\n\n    @staticmethod\n    def _preprocess_template_string(template: str) -> str:\n        \"\"\"\n        Preprocess the template string by removing leading whitespaces and indentation.\n        Args:\n            template (str): The template string to preprocess.\n        Returns:\n            str: The preprocessed template string.\n        \"\"\"\n        return textwrap.dedent(template)\n    \n    def set_body_html(self, body_html):\n        \"\"\"\n        Retrieves the job description from HTML, processes it, and initializes the vectorstore.\n        Args:\n            body_html (str): The HTML content to process.\n        \"\"\"\n\n        # Save the HTML content to a temporary file\n        with tempfile.NamedTemporaryFile(delete=False, suffix=\".html\", mode=\"w\", encoding=\"utf-8\") as temp_file:\n            temp_file.write(body_html)\n            temp_file_path = temp_file.name \n        try:\n            loader = TextLoader(temp_file_path, encoding=\"utf-8\", autodetect_encoding=True)\n            document = loader.load()\n            logger.debug(\"Document successfully loaded.\")\n        except Exception as e:\n            logger.error(f\"Error during document loading: {e}\")\n            raise\n        finally:\n            os.remove(temp_file_path)\n            logger.debug(f\"Temporary file removed: {temp_file_path}\")\n        \n        # Split the text into chunks\n        text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)\n        all_splits = text_splitter.split_documents(document)\n        logger.debug(f\"Text split into {len(all_splits)} fragments.\")\n        \n        # Create the vectorstore using FAISS\n        try:\n            self.vectorstore = FAISS.from_documents(documents=all_splits, embedding=self.llm_embeddings)\n            logger.debug(\"Vectorstore successfully initialized.\")\n        except Exception as e:\n            logger.error(f\"Error during vectorstore creation: {e}\")\n            raise\n\n    def _retrieve_context(self, query: str, top_k: int = 3) -> str:\n        \"\"\"\n        Retrieves the most relevant text fragments using the retriever.\n        Args:\n            query (str): The search query.\n            top_k (int): Number of fragments to retrieve.\n        Returns:\n            str: Concatenated text fragments.\n        \"\"\"\n        if not self.vectorstore:\n            raise ValueError(\"Vectorstore not initialized. Run extract_job_description first.\")\n        \n        retriever = self.vectorstore.as_retriever()\n        retrieved_docs = retriever.get_relevant_documents(query)[:top_k]\n        context = \"\\n\\n\".join(doc.page_content for doc in retrieved_docs)\n        logger.debug(f\"Context retrieved for query '{query}': {context[:200]}...\")  # Log the first 200 characters\n        return context\n    \n    def _extract_information(self, question: str, retrieval_query: str) -> str:\n        \"\"\"\n        Generic method to extract specific information using the retriever and LLM.\n        Args:\n            question (str): The question to ask the LLM for extraction.\n            retrieval_query (str): The query to use for retrieving relevant context.\n        Returns:\n            str: The extracted information.\n        \"\"\"\n        context = self._retrieve_context(retrieval_query)\n        \n        prompt = ChatPromptTemplate.from_template(\n            template=\"\"\"\n            You are an expert in extracting specific information from job descriptions. \n            Carefully read the job description context below and provide a clear and concise answer to the question.\n\n            Context: {context}\n\n            Question: {question}\n            Answer:\n            \"\"\"\n        )\n        \n        formatted_prompt = prompt.format(context=context, question=question)\n        logger.debug(f\"Formatted prompt for extraction: {formatted_prompt[:200]}...\")  # Log the first 200 characters\n        \n        try:\n            chain = prompt | self.llm | StrOutputParser()\n            result = chain.invoke({\"context\": context, \"question\": question})\n            extracted_info = result.strip()\n            logger.debug(f\"Extracted information: {extracted_info}\")\n            return extracted_info\n        except Exception as e:  \n            logger.error(f\"Error during information extraction: {e}\")\n            return \"\"\n    \n    def extract_job_description(self) -> str:\n        \"\"\"\n        Extracts the company name from the job description.\n        Returns:\n            str: The extracted job description.\n        \"\"\"\n        question = \"What is the job description of the company?\"\n        retrieval_query = \"Job description\"\n        logger.debug(\"Starting job description extraction.\")\n        return self._extract_information(question, retrieval_query)\n    \n    def extract_company_name(self) -> str:\n        \"\"\"\n        Extracts the company name from the job description.\n        Returns:\n            str: The extracted company name.\n        \"\"\"\n        question = \"What is the company's name?\"\n        retrieval_query = \"Company name\"\n        logger.debug(\"Starting company name extraction.\")\n        return self._extract_information(question, retrieval_query)\n    \n    def extract_role(self) -> str:\n        \"\"\"\n        Extracts the sought role/title from the job description.\n        Returns:\n            str: The extracted role/title.\n        \"\"\"\n        question = \"What is the role or title sought in this job description?\"\n        retrieval_query = \"Job title\"\n        logger.debug(\"Starting role/title extraction.\")\n        return self._extract_information(question, retrieval_query)\n    \n    def extract_location(self) -> str:\n        \"\"\"\n        Extracts the location from the job description.\n        Returns:\n            str: The extracted location.\n        \"\"\"\n        question = \"What is the location mentioned in this job description?\"\n        retrieval_query = \"Location\"\n        logger.debug(\"Starting location extraction.\")\n        return self._extract_information(question, retrieval_query)\n    \n    def extract_recruiter_email(self) -> str:\n        \"\"\"\n        Extracts the recruiter's email from the job description.\n        Returns:\n            str: The extracted recruiter's email.\n        \"\"\"\n        question = \"What is the recruiter's email address in this job description?\"\n        retrieval_query = \"Recruiter email\"\n        logger.debug(\"Starting recruiter email extraction.\")\n        email = self._extract_information(question, retrieval_query)\n        \n        # Validate the extracted email using regex\n        email_regex = r'[\\w\\.-]+@[\\w\\.-]+\\.\\w+'\n        if re.match(email_regex, email):\n            logger.debug(\"Valid recruiter's email.\")\n            return email\n        else:\n            logger.warning(\"Invalid or not found recruiter's email.\")\n            return \"\"\n \n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/module_loader.py",
    "content": "\"\"\"\nThis module is used to store the global configuration of the application.\n\"\"\"\n# app/libs/resume_and_cover_builder/module_loader.py\nimport importlib\nimport sys\n\ndef load_module(module_path: str, module_name: str):\n    spec = importlib.util.spec_from_file_location(module_name, module_path)\n    module = importlib.util.module_from_spec(spec)\n    sys.modules[module_name] = module\n    spec.loader.exec_module(module)\n    return module"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_facade.py",
    "content": "\"\"\"\nThis module contains the FacadeManager class, which is responsible for managing the interaction between the user and other components of the application.\n\"\"\"\n# app/libs/resume_and_cover_builder/manager_facade.py\nimport hashlib\nimport inquirer\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom src.libs.resume_and_cover_builder.llm.llm_job_parser import LLMParser\nfrom src.job import Job\nfrom src.utils.chrome_utils import HTML_to_PDF\nfrom .config import global_config\n\nclass ResumeFacade:\n    def __init__(self, api_key, style_manager, resume_generator, resume_object, output_path):\n        \"\"\"\n        Initialize the FacadeManager with the given API key, style manager, resume generator, resume object, and log path.\n        Args:\n            api_key (str): The OpenAI API key to be used for generating text.\n            style_manager (StyleManager): The StyleManager instance to manage the styles.\n            resume_generator (ResumeGenerator): The ResumeGenerator instance to generate resumes and cover letters.\n            resume_object (str): The resume object to be used for generating resumes and cover letters.\n            output_path (str): The path to the log file.\n        \"\"\"\n        lib_directory = Path(__file__).resolve().parent\n        global_config.STRINGS_MODULE_RESUME_PATH = lib_directory / \"resume_prompt/strings_feder-cr.py\"\n        global_config.STRINGS_MODULE_RESUME_JOB_DESCRIPTION_PATH = lib_directory / \"resume_job_description_prompt/strings_feder-cr.py\"\n        global_config.STRINGS_MODULE_COVER_LETTER_JOB_DESCRIPTION_PATH = lib_directory / \"cover_letter_prompt/strings_feder-cr.py\"\n        global_config.STRINGS_MODULE_NAME = \"strings_feder_cr\"\n        global_config.STYLES_DIRECTORY = lib_directory / \"resume_style\"\n        global_config.LOG_OUTPUT_FILE_PATH = output_path\n        global_config.API_KEY = api_key\n        self.style_manager = style_manager\n        self.resume_generator = resume_generator\n        self.resume_generator.set_resume_object(resume_object)\n        self.selected_style = None  # Property to store the selected style\n    \n    def set_driver(self, driver):\n         self.driver = driver\n\n    def prompt_user(self, choices: list[str], message: str) -> str:\n        \"\"\"\n        Prompt the user with the given message and choices.\n        Args:\n            choices (list[str]): The list of choices to present to the user.\n            message (str): The message to display to the user.\n        Returns:\n            str: The choice selected by the user.\n        \"\"\"\n        questions = [\n            inquirer.List('selection', message=message, choices=choices),\n        ]\n        return inquirer.prompt(questions)['selection']\n\n    def prompt_for_text(self, message: str) -> str:\n        \"\"\"\n        Prompt the user to enter text with the given message.\n        Args:\n            message (str): The message to display to the user.\n        Returns:\n            str: The text entered by the user.\n        \"\"\"\n        questions = [\n            inquirer.Text('text', message=message),\n        ]\n        return inquirer.prompt(questions)['text']\n\n        \n    def link_to_job(self, job_url):\n        self.driver.get(job_url)\n        self.driver.implicitly_wait(10)\n        body_element = self.driver.find_element(\"tag name\", \"body\")\n        body_element = body_element.get_attribute(\"outerHTML\")\n        self.llm_job_parser = LLMParser(openai_api_key=global_config.API_KEY)\n        self.llm_job_parser.set_body_html(body_element)\n\n        self.job = Job()\n        self.job.role = self.llm_job_parser.extract_role()\n        self.job.company = self.llm_job_parser.extract_company_name()\n        self.job.description = self.llm_job_parser.extract_job_description()\n        self.job.location = self.llm_job_parser.extract_location()\n        self.job.link = job_url\n        logger.info(f\"Extracting job details from URL: {job_url}\")\n\n\n    def create_resume_pdf_job_tailored(self) -> tuple[bytes, str]:\n        \"\"\"\n        Create a resume PDF using the selected style and the given job description text.\n        Args:\n            job_url (str): The job URL to generate the hash for.\n            job_description_text (str): The job description text to include in the resume.\n        Returns:\n            tuple: A tuple containing the PDF content as bytes and the unique filename.\n        \"\"\"\n        style_path = self.style_manager.get_style_path()\n        if style_path is None:\n            raise ValueError(\"You must choose a style before generating the PDF.\")\n\n\n        html_resume = self.resume_generator.create_resume_job_description_text(style_path, self.job.description)\n\n        # Generate a unique name using the job URL hash\n        suggested_name = hashlib.md5(self.job.link.encode()).hexdigest()[:10]\n        \n        result = HTML_to_PDF(html_resume, self.driver)\n        self.driver.quit()\n        return result, suggested_name\n    \n    \n    \n    def create_resume_pdf(self) -> tuple[bytes, str]:\n        \"\"\"\n        Create a resume PDF using the selected style and the given job description text.\n        Args:\n            job_url (str): The job URL to generate the hash for.\n            job_description_text (str): The job description text to include in the resume.\n        Returns:\n            tuple: A tuple containing the PDF content as bytes and the unique filename.\n        \"\"\"\n        style_path = self.style_manager.get_style_path()\n        if style_path is None:\n            raise ValueError(\"You must choose a style before generating the PDF.\")\n        \n        html_resume = self.resume_generator.create_resume(style_path)\n        result = HTML_to_PDF(html_resume, self.driver)\n        self.driver.quit()\n        return result\n\n    def create_cover_letter(self) -> tuple[bytes, str]:\n        \"\"\"\n        Create a cover letter based on the given job description text and job URL.\n        Args:\n            job_url (str): The job URL to generate the hash for.\n            job_description_text (str): The job description text to include in the cover letter.\n        Returns:\n            tuple: A tuple containing the PDF content as bytes and the unique filename.\n        \"\"\"\n        style_path = self.style_manager.get_style_path()\n        if style_path is None:\n            raise ValueError(\"You must choose a style before generating the PDF.\")\n        \n        \n        cover_letter_html = self.resume_generator.create_cover_letter_job_description(style_path, self.job.description)\n\n        # Generate a unique name using the job URL hash\n        suggested_name = hashlib.md5(self.job.link.encode()).hexdigest()[:10]\n\n        \n        result = HTML_to_PDF(cover_letter_html, self.driver)\n        self.driver.quit()\n        return result, suggested_name"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_generator.py",
    "content": "\"\"\"\nThis module is responsible for generating resumes and cover letters using the LLM model.\n\"\"\"\n# app/libs/resume_and_cover_builder/resume_generator.py\nfrom string import Template\nfrom typing import Any\nfrom src.libs.resume_and_cover_builder.llm.llm_generate_resume import LLMResumer\nfrom src.libs.resume_and_cover_builder.llm.llm_generate_resume_from_job import LLMResumeJobDescription\nfrom src.libs.resume_and_cover_builder.llm.llm_generate_cover_letter_from_job import LLMCoverLetterJobDescription\nfrom .module_loader import load_module\nfrom .config import global_config\n\nclass ResumeGenerator:\n    def __init__(self):\n        pass\n    \n    def set_resume_object(self, resume_object):\n         self.resume_object = resume_object\n         \n\n    def _create_resume(self, gpt_answerer: Any, style_path):\n        # Imposta il resume nell'oggetto gpt_answerer\n        gpt_answerer.set_resume(self.resume_object)\n        \n        # Leggi il template HTML\n        template = Template(global_config.html_template)\n        \n        try:\n            with open(style_path, \"r\") as f:\n                style_css = f.read()  # Correzione: chiama il metodo `read` con le parentesi\n        except FileNotFoundError:\n            raise ValueError(f\"Il file di stile non è stato trovato nel percorso: {style_path}\")\n        except Exception as e:\n            raise RuntimeError(f\"Errore durante la lettura del file CSS: {e}\")\n        \n        # Genera l'HTML del resume\n        body_html = gpt_answerer.generate_html_resume()\n        \n        # Applica i contenuti al template\n        return template.substitute(body=body_html, style_css=style_css)\n\n    def create_resume(self, style_path):\n        strings = load_module(global_config.STRINGS_MODULE_RESUME_PATH, global_config.STRINGS_MODULE_NAME)\n        gpt_answerer = LLMResumer(global_config.API_KEY, strings)\n        return self._create_resume(gpt_answerer, style_path)\n\n    def create_resume_job_description_text(self, style_path: str, job_description_text: str):\n        strings = load_module(global_config.STRINGS_MODULE_RESUME_JOB_DESCRIPTION_PATH, global_config.STRINGS_MODULE_NAME)\n        gpt_answerer = LLMResumeJobDescription(global_config.API_KEY, strings)\n        gpt_answerer.set_job_description_from_text(job_description_text)\n        return self._create_resume(gpt_answerer, style_path)\n\n    def create_cover_letter_job_description(self, style_path: str, job_description_text: str):\n        strings = load_module(global_config.STRINGS_MODULE_COVER_LETTER_JOB_DESCRIPTION_PATH, global_config.STRINGS_MODULE_NAME)\n        gpt_answerer = LLMCoverLetterJobDescription(global_config.API_KEY, strings)\n        gpt_answerer.set_resume(self.resume_object)\n        gpt_answerer.set_job_description_from_text(job_description_text)\n        cover_letter_html = gpt_answerer.generate_cover_letter()\n        template = Template(global_config.html_template)\n        with open(style_path, \"r\") as f:\n            style_css = f.read()\n        return template.substitute(body=cover_letter_html, style_css=style_css)\n    \n    \n    "
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_job_description_prompt/__init__.py",
    "content": ""
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_job_description_prompt/strings_feder-cr.py",
    "content": "from src.libs.resume_and_cover_builder.template_base import prompt_header_template, prompt_education_template, prompt_working_experience_template, prompt_projects_template, prompt_additional_skills_template, prompt_certifications_template, prompt_achievements_template\n\nprompt_header = \"\"\"\nAct as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is to create a professional and polished header for the resume. The header should:\n\n1. **Contact Information**: Include your full name, city and country, phone number, email address, LinkedIn profile, and GitHub profile.\n2. **Formatting**: Ensure the contact details are presented clearly and are easy to read.\n\nTo implement this:\n- If any of the contact information fields (e.g., LinkedIn profile, GitHub profile) are not provided (i.e., `None`), omit them from the header.\n\n- **My information:**  \n  {personal_information}\n\"\"\" + prompt_header_template\n\nprompt_education = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to articulate the educational background for a resume, ensuring it aligns with the provided job description. For each educational entry, ensure you include:\n\n1. **Institution Name and Location**: Specify the university or educational institution’s name and location.\n2. **Degree and Field of Study**: Clearly indicate the degree earned and the field of study.\n3. **Grade**: Include your Grade if it is strong and relevant.\n4. **Relevant Coursework**: List key courses with their grades to showcase your academic strengths. If no coursework is provided, omit this section from the template.\n\nTo implement this, follow these steps:\n- If the exam details are not provided (i.e., `None`), skip the coursework section when filling out the template.\n- If the exam details are available, fill out the coursework section accordingly.\n\n\n- **My information:**  \n  {education_details}\n\n- **Job Description:**  \n  {job_description}\n\"\"\"+ prompt_education_template\n\n\nprompt_working_experience = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to detail the work experience for a resume, ensuring it aligns with the provided job description. For each job entry, ensure you include:\n\n1. **Company Name and Location**: Provide the name of the company and its location.\n2. **Job Title**: Clearly state your job title.\n3. **Dates of Employment**: Include the start and end dates of your employment.\n4. **Responsibilities and Achievements**: Describe your key responsibilities and notable achievements, emphasizing measurable results and specific contributions.\n\nEnsure that the descriptions highlight relevant experience and align with the job description.\n\nTo implement this:\n- If any of the work experience details (e.g., responsibilities, achievements) are not provided (i.e., `None`), omit those sections when filling out the template.\n\n\n- **My information:**  \n  {experience_details}\n\n- **Job Description:**  \n  {job_description}\n\"\"\"+ prompt_working_experience_template\n\n\nprompt_projects = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to highlight notable side projects based on the provided job description. For each project, ensure you include:\n\n1. **Project Name and Link**: Provide the name of the project and include a link to the GitHub repository or project page.\n2. **Project Details**: Describe any notable recognition or achievements related to the project, such as GitHub stars or community feedback.\n3. **Technical Contributions**: Highlight your specific contributions and the technologies used in the project.\n\nEnsure that the project descriptions demonstrate your skills and achievements relevant to the job description.\n\nTo implement this:\n- If any of the project details (e.g., link, achievements) are not provided (i.e., `None`), omit those sections when filling out the template.\n\n\n- **My information:**  \n  {projects}\n\n- **Job Description:**  \n  {job_description}\n\"\"\"+ prompt_projects_template\n\n\nprompt_achievements = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to list significant achievements based on the provided job description. For each achievement, ensure you include:\n\n1. **Award or Recognition**: Clearly state the name of the award, recognition, scholarship, or honor.\n2. **Description**: Provide a brief description of the achievement and its relevance to your career or academic journey.\n\nEnsure that the achievements are clearly presented and effectively highlight your accomplishments.\n\nTo implement this:\n- If any of the achievement details (e.g., certifications, descriptions) are not provided (i.e., `None`), omit those sections when filling out the template.\n\n\n- **My information:**  \n  {achievements}\n\n- **Job Description:**  \n  {job_description}\n\"\"\"+ prompt_achievements_template\n\n\nprompt_certifications = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to list significant certifications based on the provided details. For each certification, ensure you include:\n\n1. **Certification Name**: Clearly state the name of the certification.\n2. **Description**: Provide a brief description of the certification and its relevance to your professional or academic career.\n\nEnsure that the certifications are clearly presented and effectively highlight your qualifications.\n\nTo implement this:\n\nIf any of the certification details (e.g., descriptions) are not provided (i.e., None), omit those sections when filling out the template.\n\n- **My information:**  \n  {certifications}\n\n- **Job Description:**  \n  {job_description}\n\"\"\"+ prompt_certifications_template\n\n\nprompt_additional_skills = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to list additional skills relevant to the job. For each skill, ensure you include:\nDo not add any information beyond what is listed in the provided data fields. Only use the information provided in the 'languages', 'interests', and 'skills' fields to formulate your responses. Avoid extrapolating or incorporating details from the job description or other external sources.\n\n1. **Skill Category**: Clearly state the category or type of skill.\n2. **Specific Skills**: List the specific skills or technologies within each category.\n3. **Proficiency and Experience**: Briefly describe your experience and proficiency level.\n\nEnsure that the skills listed are relevant and accurately reflect your expertise in the field.\n\nTo implement this:\n- If any of the skill details (e.g., languages, interests, skills) are not provided (i.e., `None`), omit those sections when filling out the template.\n\n\n- **My information:**  \n  {languages}\n  {interests}\n  {skills}\n\n- **Job Description:**  \n  {job_description}\n\"\"\"+ prompt_additional_skills_template\n\nsummarize_prompt_template = \"\"\"\nAs a seasoned HR expert, your task is to identify and outline the key skills and requirements necessary for the position of this job. Use the provided job description as input to extract all relevant information. This will involve conducting a thorough analysis of the job's responsibilities and the industry standards. You should consider both the technical and soft skills needed to excel in this role. Additionally, specify any educational qualifications, certifications, or experiences that are essential. Your analysis should also reflect on the evolving nature of this role, considering future trends and how they might affect the required competencies.\n\nRules:\nRemove boilerplate text\nInclude only relevant information to match the job description against the resume\n\n# Analysis Requirements\nYour analysis should include the following sections:\nTechnical Skills: List all the specific technical skills required for the role based on the responsibilities described in the job description.\nSoft Skills: Identify the necessary soft skills, such as communication abilities, problem-solving, time management, etc.\nEducational Qualifications and Certifications: Specify the essential educational qualifications and certifications for the role.\nProfessional Experience: Describe the relevant work experiences that are required or preferred.\nRole Evolution: Analyze how the role might evolve in the future, considering industry trends and how these might influence the required skills.\n\n# Final Result:\nYour analysis should be structured in a clear and organized document with distinct sections for each of the points listed above. Each section should contain:\nThis comprehensive overview will serve as a guideline for the recruitment process, ensuring the identification of the most qualified candidates.\n\n# Job Description:\n```\n{text}\n```\n\n---\n\n# Job Description Summary\"\"\"\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_prompt/__init__.py",
    "content": ""
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_prompt/strings_feder-cr.py",
    "content": "from src.libs.resume_and_cover_builder.template_base import prompt_header_template, prompt_education_template, prompt_working_experience_template, prompt_projects_template, prompt_achievements_template, prompt_certifications_template, prompt_additional_skills_template\n\nprompt_header = \"\"\"\nAct as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is to create a professional and polished header for the resume. The header should:\n\n1. **Contact Information**: Include your full name, city and country, phone number, email address, LinkedIn profile, and GitHub profile. Exclude any information that is not provided.\n2. **Formatting**: Ensure the contact details are presented clearly and are easy to read.\n\n- **My information:**  \n  {personal_information}\n\"\"\" + prompt_header_template\n\n\nprompt_education = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to articulate the educational background for a resume. For each educational entry, ensure you include:\n\n1. **Institution Name and Location**: Specify the university or educational institution’s name and location.\n2. **Degree and Field of Study**: Clearly indicate the degree earned and the field of study.\n3. **Grade**: Include your Grade if it is strong and relevant.\n4. **Relevant Coursework**: List key courses with their grades to showcase your academic strengths.\n\n- **My information:**  \n  {education_details}\n\"\"\"+ prompt_education_template\n\n\nprompt_working_experience = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to detail the work experience for a resume. For each job entry, ensure you include:\n\n1. **Company Name and Location**: Provide the name of the company and its location.\n2. **Job Title**: Clearly state your job title.\n3. **Dates of Employment**: Include the start and end dates of your employment.\n4. **Responsibilities and Achievements**: Describe your key responsibilities and notable achievements, emphasizing measurable results and specific contributions.\n\n- **My information:**  \n  {experience_details}\n\"\"\"+ prompt_working_experience_template\n\n\nprompt_projects = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to highlight notable side projects. For each project, ensure you include:\n\n1. **Project Name and Link**: Provide the name of the project and include a link to the GitHub repository or project page.\n2. **Project Details**: Describe any notable recognition or achievements related to the project, such as GitHub stars or community feedback.\n3. **Technical Contributions**: Highlight your specific contributions and the technologies used in the project. \n\n- **My information:**  \n  {projects}\n\"\"\"+ prompt_projects_template\n\n\nprompt_achievements = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to list significant achievements. For each achievement, ensure you include:\n\n1. **Award or Recognition**: Clearly state the name of the award, recognition, scholarship, or honor.\n2. **Description**: Provide a brief description of the achievement and its relevance to your career or academic journey.\n\n- **My information:**  \n  {achievements}\n\"\"\"+ prompt_achievements_template\n\n\nprompt_certifications = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to list significant certifications based on the provided details. For each certification, ensure you include:\n\n1. **Certification Name**: Clearly state the name of the certification.\n2. **Description**: Provide a brief description of the certification and its relevance to your professional or academic career.\n\nEnsure that the certifications are clearly presented and effectively highlight your qualifications.\n\nTo implement this:\n\nIf any of the certification details (e.g., descriptions) are not provided (i.e., None), omit those sections when filling out the template.\n\n- **My information:**  \n  {certifications}\n\n\"\"\"+ prompt_certifications_template\n\n\nprompt_additional_skills = \"\"\"\nAct as an HR expert and resume writer with a specialization in creating ATS-friendly resumes. Your task is to list additional skills relevant to the job. For each skill, ensure you include:\n\n1. **Skill Category**: Clearly state the category or type of skill.\n2. **Specific Skills**: List the specific skills or technologies within each category.\n3. **Proficiency and Experience**: Briefly describe your experience and proficiency level.\n\n- **My information:**  \n  {languages}\n  {interests}\n  {skills}\n\"\"\"+ prompt_additional_skills_template\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_style/__init__.py",
    "content": ""
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_style/style_cloyola.css",
    "content": "/*Cloyola Grey $https://github.com/cloyola*/\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap');\n@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');\n\nbody {\n  font-family: 'Roboto', sans-serif;\n  line-height: 1.4;\n  color: #333;\n  max-width: 700px;\n  margin: 0 auto;\n  padding: 10px;\n  font-size: 9pt;\n}\n\nheader {\n    text-align: left;\n    margin-bottom: 20px;\n    background-color: #7c7c7c40;\n    padding: 20px;\n    border-radius: 8px;\n    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\nh1 {\n  font-size: 18pt;\n  font-weight: 700;\n  margin: 0 0 5px 0;\n}\n\n.contact-info {\n  display: flex;\n  justify-content: left;\n  flex-wrap: wrap;\n  gap: 10px;\n  font-size: 9pt;\n  font-weight: normal;\n}\n\n.contact-info p {\n  margin: 0;\n}\n\n.contact-info a {\n  color: #0077b5;\n  text-decoration: none;\n}\n\n.fab,\n.fas {\n  margin-right: 3px;\n}\n\nspan.entry-location {\n  font-weight: normal;\n}\n\nh2 {\n  font-size: 14pt;\n  font-weight: 600;\n  border-bottom: 1px dotted #4c4c4c;\n  padding-bottom: 2px;\n  margin: 10px 0 5px 0;\n  text-align: left;\n}\n\n.entry {\n    margin-bottom: 15px; /*margin-bottom: 8px;*/\n    background-color: #fff;\n    padding: 15px;\n    border-radius: 8px;\n    box-shadow: 3px 3px 5px 2px rgba(0, 0, 0, 0.2);\n}\n\n.entry-header {\n  display: flex;\n  justify-content: space-between;\n  font-weight: 600;\n  font-size: 10pt;\n}\n\n.entry-details {\n  display: flex;\n  justify-content: space-between;\n  font-style: italic;\n  margin-bottom: 2px;\n  font-size: 9pt;\n}\n\n.compact-list {\n  margin: 2px 0;\n  padding-left: 15px;\n}\n\n.compact-list li {\n  margin-bottom: 2px;\n}\n\n.two-column {\n  display: flex;\n  justify-content: space-between;\n}\n\n.two-column ul {\n  width: 48%;\n  margin: 0;\n  padding-left: 15px;\n  list-style-type: circle;\n}\n\na {\n  color: #0077b5;\n  text-decoration: none;\n}\n\na:hover {\n  text-decoration: underline;\n}\n\n@media print {\n  body {\n    padding: 0;\n    margin: 0;\n    font-size: 9pt;\n  }\n\n  @page {\n    margin: 0.5cm;\n  }\n\n  h1 {\n    font-size: 18pt;\n  }\n\n  h2 {\n    font-size: 11pt;\n  }\n\n  .contact-info {\n    font-size: 8pt;\n  }\n\n  .entry-details {\n    font-size: 7pt;\n  }\n\n  .compact-list {\n    padding-left: 12px;\n  }\n}\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_style/style_josylad_blue.css",
    "content": "/*Modern Blue$https://github.com/josylad*/\n\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap');\n\nbody {\n  font-family: 'Poppins', sans-serif;\n  line-height: 1.6;\n  color: #2c3e50;\n  max-width: 850px;\n  margin: 0 auto;\n  padding: 20px;\n  font-size: 10pt;\n  background-color: #f9f9f9;\n}\n\nheader {\n  text-align: center;\n  margin-bottom: 20px;\n  background-color: #3498db;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\nh1 {\n  font-size: 28pt;\n  font-weight: 700;\n  margin: 0 0 10px 0;\n  color: #fff;\n}\n\n.contact-info {\n  display: flex;\n  justify-content: center;\n  flex-wrap: wrap;\n  gap: 15px;\n  font-size: 10pt;\n  font-weight: 300;\n  color: #ecf0f1;\n}\n\n.contact-info p {\n  margin: 0;\n}\n\n.contact-info a {\n  color: #ecf0f1;\n  text-decoration: none;\n  transition: color 0.3s ease;\n}\n\n.contact-info a:hover {\n  color: #2c3e50;\n}\n\n.fab,\n.fas {\n  margin-right: 5px;\n}\n\nh2 {\n  font-size: 18pt;\n  font-weight: 600;\n  border-bottom: 2px solid #3498db;\n  padding-bottom: 5px;\n  margin: 20px 0 15px 0;\n  color: #2c3e50;\n}\n\n.entry {\n  margin-bottom: 15px;\n  background-color: #fff;\n  padding: 15px;\n  border-radius: 8px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.entry-header {\n  display: flex;\n  justify-content: space-between;\n  font-weight: 600;\n  color: #3498db;\n}\n\n.entry-details {\n  display: flex;\n  justify-content: space-between;\n  font-style: italic;\n  margin-bottom: 8px;\n  font-size: 9pt;\n  color: #7f8c8d;\n}\n\n.compact-list {\n  margin: 5px 0;\n  padding-left: 20px;\n}\n\n.compact-list li {\n  margin-bottom: 5px;\n}\n\n.two-column {\n  display: flex;\n  justify-content: space-between;\n  flex-wrap: wrap;\n}\n\n.two-column ul {\n  width: 48%;\n  margin: 0;\n  padding-left: 20px;\n}\n\na {\n  color: #3498db;\n  text-decoration: none;\n  transition: color 0.3s ease;\n}\n\na:hover {\n  color: #2980b9;\n  text-decoration: underline;\n}\n\n@media print {\n  body {\n    padding: 0;\n    margin: 0;\n    font-size: 9pt;\n    background-color: #fff;\n  }\n\n  @page {\n    margin: 1cm;\n  }\n\n  h1 {\n    font-size: 24pt;\n  }\n\n  h2 {\n    font-size: 16pt;\n  }\n\n  .contact-info {\n    font-size: 9pt;\n  }\n\n  .entry-details {\n    font-size: 8pt;\n  }\n\n  .compact-list {\n    padding-left: 15px;\n  }\n\n  header {\n    box-shadow: none;\n  }\n\n  .entry {\n    box-shadow: none;\n    padding: 10px 0;\n  }\n}"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_style/style_josylad_grey.css",
    "content": "/*Modern Grey$https://github.com/josylad*/\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap');\n\nbody {\n  font-family: 'Poppins', sans-serif;\n  line-height: 1.6;\n  color: #333;\n  max-width: 850px;\n  margin: 0 auto;\n  padding: 20px;\n  font-size: 10pt;\n  background-color: #f9f9f9;\n}\n\nheader {\n  text-align: center;\n  margin-bottom: 20px;\n  background-color: #4a4a4a;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\nh1 {\n  font-size: 28pt;\n  font-weight: 700;\n  margin: 0 0 10px 0;\n  color: #fff;\n}\n\n.contact-info {\n  display: flex;\n  justify-content: center;\n  flex-wrap: wrap;\n  gap: 15px;\n  font-size: 10pt;\n  font-weight: 300;\n  color: #e0e0e0;\n}\n\n.contact-info p {\n  margin: 0;\n}\n\n.contact-info a {\n  color: #e0e0e0;\n  text-decoration: none;\n  transition: color 0.3s ease;\n}\n\n.contact-info a:hover {\n  color: #fff;\n}\n\n.fab,\n.fas {\n  margin-right: 5px;\n}\n\nh2 {\n  font-size: 18pt;\n  font-weight: 600;\n  border-bottom: 2px solid #4a4a4a;\n  padding-bottom: 5px;\n  margin: 20px 0 15px 0;\n  color: #333;\n}\n\n.entry {\n  margin-bottom: 15px;\n  background-color: #fff;\n  padding: 15px;\n  border-radius: 8px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.entry-header {\n  display: flex;\n  justify-content: space-between;\n  font-weight: 600;\n  color: #4a4a4a;\n}\n\n.entry-details {\n  display: flex;\n  justify-content: space-between;\n  font-style: italic;\n  margin-bottom: 8px;\n  font-size: 9pt;\n  color: #777;\n}\n\n.compact-list {\n  margin: 5px 0;\n  padding-left: 20px;\n}\n\n.compact-list li {\n  margin-bottom: 5px;\n}\n\n.skills-section {\n  margin-top: 20px;\n}\n\n.skills-section h2 {\n  font-size: 18pt;\n  font-weight: 600;\n  border-bottom: 2px solid #4a4a4a;\n  padding-bottom: 5px;\n  margin: 0 0 15px 0;\n  color: #333;\n  text-align: center;\n}\n\n.skills-container {\n  display: flex;\n  justify-content: space-between;\n}\n\n.skills-column {\n  width: 48%;\n}\n\n.skills-list {\n  list-style-type: none;\n  padding: 0;\n  margin: 0;\n}\n\n.skills-list li {\n  margin-bottom: 8px;\n  display: flex;\n  align-items: center;\n}\n\n.skills-list li::before {\n  content: \"•\";\n  color: #4a4a4a;\n  font-weight: bold;\n  display: inline-block;\n  width: 1em;\n  margin-right: 0.5em;\n}\n\na {\n  color: #4a4a4a;\n  text-decoration: none;\n  transition: color 0.3s ease;\n}\n\na:hover {\n  color: #333;\n  text-decoration: underline;\n}\n\n@media print {\n  body {\n    padding: 0;\n    margin: 0;\n    font-size: 9pt;\n    background-color: #fff;\n  }\n\n  @page {\n    margin: 1cm;\n  }\n\n  h1 {\n    font-size: 24pt;\n  }\n\n  h2 {\n    font-size: 16pt;\n  }\n\n  .contact-info {\n    font-size: 9pt;\n  }\n\n  .entry-details {\n    font-size: 8pt;\n  }\n\n  .compact-list,\n  .skills-list {\n    padding-left: 15px;\n  }\n\n  header {\n    box-shadow: none;\n  }\n\n  .entry {\n    box-shadow: none;\n    padding: 10px 0;\n  }\n}\n\n@media (max-width: 600px) {\n  .skills-container {\n    flex-direction: column;\n  }\n  \n  .skills-column {\n    width: 100%;\n  }\n}"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_style/style_krishnavalliappan.css",
    "content": "/*Default$https://github.com/krishnavalliappan*/\nbody {\n  font-family: \"Barlow\", Arial, sans-serif;\n  line-height: 1.2;\n  color: #333;\n  max-width: 700px;\n  margin: 0 auto;\n  padding: 10px;\n  font-size: 9pt;\n}\n\nheader {\n  text-align: center;\n  margin-bottom: 10px;\n}\n\nh1 {\n  font-size: 24pt;\n  font-weight: 700;\n  margin: 0 0 5px 0;\n}\n\n.contact-info {\n  display: flex;\n  justify-content: center;\n  flex-wrap: wrap;\n  gap: 10px;\n  font-size: 9pt;\n  font-weight: normal;\n}\n\n.contact-info p {\n  margin: 0;\n}\n\n.contact-info a {\n  color: #0077b5;\n  text-decoration: none;\n}\n\n.fab,\n.fas {\n  margin-right: 3px;\n}\n\nspan {\n  font-weight: normal;\n}\n\nh2 {\n  font-size: 16pt;\n  font-weight: 600;\n  border-bottom: 1px solid #333;\n  padding-bottom: 2px;\n  margin: 10px 0 5px 0;\n  text-align: center;\n}\n\n.entry {\n  margin-bottom: 8px;\n}\n\n.entry-header {\n  display: flex;\n  justify-content: space-between;\n  font-weight: 600;\n}\n\n.entry-details {\n  display: flex;\n  justify-content: space-between;\n  font-style: italic;\n  margin-bottom: 2px;\n  font-size: 8pt;\n}\n\n.compact-list {\n  margin: 2px 0;\n  padding-left: 15px;\n}\n\n.compact-list li {\n  margin-bottom: 2px;\n}\n\n.two-column {\n  display: flex;\n  justify-content: space-between;\n}\n\n.two-column ul {\n  width: 48%;\n  margin: 0;\n  padding-left: 15px;\n}\n\na {\n  color: #0077b5;\n  text-decoration: none;\n}\n\na:hover {\n  text-decoration: underline;\n}\n\n@media print {\n  body {\n    padding: 0;\n    margin: 0;\n    font-size: 9pt;\n  }\n\n  @page {\n    margin: 0.5cm;\n  }\n\n  h1 {\n    font-size: 18pt;\n  }\n\n  h2 {\n    font-size: 11pt;\n  }\n\n  .contact-info {\n    font-size: 8pt;\n  }\n\n  .entry-details {\n    font-size: 7pt;\n  }\n\n  .compact-list {\n    padding-left: 12px;\n  }\n}\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/resume_style/style_samodum_bold.css",
    "content": "/*Clean Blue$https://github.com/samodum*/\n@import url(\"https://fonts.googleapis.com/css2?family=Josefin+Sans&family=Kaisei+HarunoUmi&family=Open+Sans:ital,wght@0,400;0,600;1,400&display=swap\");\n\n:root {\n  --pageWidth: 49.62rem;\n  --textColor: #383838;\n  --lineColorA: #b8b8b8;\n  --accentColor: blue;\n  --HFont: \"Josefin Sans\", sans-serif;\n  --PFont: \"Open Sans\", sans-serif;\n  --BText: \"Kaisei HarunoUmi\", serif;\n  --sectionSpacing: 1.5rem;\n  --bodyFontSize: 0.875rem;\n  --KeyColumn: 9.375rem;\n}\n\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n  color: var(--textColor);\n  font-size: var(--bodyFontSize);\n}\n\nbody {\n  /* border: 1px solid var(--accentColor); page guidelines*/\n  max-width: var(--pageWidth);\n  padding: 3.375rem 1.5rem;\n  display: flex;\n  font-family: var(--PFont);\n  flex-direction: column;\n  gap: 1.5rem;\n  margin: 0 auto;\n}\n\nmain {\n  display: flex;\n  flex-direction: column;\n  gap: 1.5rem;\n  order: 2;\n}\n\na {\n  text-decoration: none;\n}\n\na:hover {\n  color: var(--accentColor);\n  transition: color 0.3s ease;\n}\n\nheader {\n  order: 0;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  align-items: start;\n  gap: 1.5rem;\n}\n\nh1 {\n  font-family: var(--HFont);\n  font-size: 1.5rem;\n  font-weight: 400;\n  margin-bottom: -0.125rem;\n  color: var(--accentColor);\n}\n\n.contact-info {\n  display: flex;\n  flex-direction: column;\n  gap: 0.125rem;\n}\n\n.contact-info p {\n  font-family: var(--PFont);\n}\n\n.contact-info p::before {\n  margin-right: 0.25rem;\n  text-transform: capitalize;\n  font-family: var(--HFont);\n  font-weight: 600;\n}\n\n.contact-info p:nth-child(1)::before {\n  content: \"address:\";\n}\n.contact-info p:nth-child(2)::before {\n  content: \"phone:\";\n}\n.contact-info p:nth-child(3)::before {\n  content: \"email:\";\n}\n.contact-info p:nth-child(4)::before {\n  content: \"linkedin:\";\n}\n.contact-info p:nth-child(5)::before {\n  content: \"github:\";\n}\n\nsection h2 {\n  font-family: var(--HFont);\n  font-size: 1.125rem;\n  font-weight: bold;\n  color: var(--accentColor);\n  padding-bottom: 0.25rem;\n  margin-bottom: 0.5rem;\n  border-bottom: 1px solid var(--lineColorA);\n}\n\n.entry {\n  padding-top: 1rem;\n  display: grid;\n  grid-template-columns: 1fr 4fr;\n  column-gap: 10px;\n}\n\n.entry:first-of-type {\n  padding-top: 0.5rem;\n}\n\n.entry-header {\n  grid-column: 1;\n  font-family: var(--HFont);\n  font-weight: 600;\n  display: flex;\n  flex-direction: column;\n  gap: 0.25rem;\n}\n\n.entry-details,\n#side-projects .compact-list {\n  margin-top: -4px;\n}\n\n.entry-details,\n.compact-list {\n  grid-column: 2;\n}\n\n.entry-title {\n  font-family: var(--HFont);\n  font-weight: 600;\n  margin-right: 0.25rem;\n}\n\n.entry-year {\n  font-style: italic;\n}\n\n.compact-list {\n  padding-left: 10px;\n  list-style-type: circle;\n  margin: 0;\n}\n\n.compact-list li {\n  margin-left: 5px;\n}\n\n#achievements .compact-list {\n  padding-top: 0.25rem;\n}\n\n.two-column {\n  padding-top: 0.25rem;\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  column-gap: 10px;\n}\n\n.two-column .compact-list:first-child {\n  grid-column: 1;\n}\n\n#work-experience {\n  order: 1;\n}\n#education {\n  order: 2;\n}\n#achievements {\n  order: 3;\n}\n#side-projects {\n  order: 4;\n}\n#skills-languages {\n  order: 5;\n}\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/style_manager.py",
    "content": "import os\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple, Optional\nimport logging\n\n# Configure logging\nlogging.basicConfig(level=logging.DEBUG, format=\"%(asctime)s - %(levelname)s - %(message)s\")\n\n\nclass StyleManager:\n    def __init__(self):\n        self.selected_style: Optional[str] = None\n        current_file = Path(__file__).resolve()\n        project_root = current_file.parent.parent.parent.parent\n        self.styles_directory = project_root / \"src\" / \"libs\" / \"resume_and_cover_builder\" / \"resume_style\"\n\n        logging.debug(f\"Project root determined as: {project_root}\")\n        logging.debug(f\"Styles directory set to: {self.styles_directory}\")\n\n    def get_styles(self) -> Dict[str, Tuple[str, str]]:\n        \"\"\"\n        Retrieve the available styles from the styles directory.\n        Returns:\n            Dict[str, Tuple[str, str]]: A dictionary mapping style names to their file names and author links.\n        \"\"\"\n        styles_to_files = {}\n        if not self.styles_directory:\n            logging.warning(\"Styles directory is not set.\")\n            return styles_to_files\n        logging.debug(f\"Reading styles directory: {self.styles_directory}\")\n        try:\n            files = [f for f in self.styles_directory.iterdir() if f.is_file()]\n            logging.debug(f\"Files found: {[f.name for f in files]}\")\n            for file_path in files:\n                logging.debug(f\"Processing file: {file_path}\")\n                with file_path.open(\"r\", encoding=\"utf-8\") as file:\n                    first_line = file.readline().strip()\n                    logging.debug(f\"First line of file {file_path.name}: {first_line}\")\n                    if first_line.startswith(\"/*\") and first_line.endswith(\"*/\"):\n                        content = first_line[2:-2].strip()\n                        if \"$\" in content:\n                            style_name, author_link = content.split(\"$\", 1)\n                            style_name = style_name.strip()\n                            author_link = author_link.strip()\n                            styles_to_files[style_name] = (file_path.name, author_link)\n                            logging.info(f\"Added style: {style_name} by {author_link}\")\n        except FileNotFoundError:\n            logging.error(f\"Directory {self.styles_directory} not found.\")\n        except PermissionError:\n            logging.error(f\"Permission denied for accessing {self.styles_directory}.\")\n        except Exception as e:\n            logging.error(f\"Unexpected error while reading styles: {e}\")\n        return styles_to_files\n\n    def format_choices(self, styles_to_files: Dict[str, Tuple[str, str]]) -> List[str]:\n        \"\"\"\n        Format the style choices for user presentation.\n        Args:\n            styles_to_files (Dict[str, Tuple[str, str]]): A dictionary mapping style names to their file names and author links.\n        Returns:\n            List[str]: A list of formatted style choices.\n        \"\"\"\n        return [f\"{style_name} (style author -> {author_link})\" for style_name, (file_name, author_link) in styles_to_files.items()]\n\n    def set_selected_style(self, selected_style: str):\n        \"\"\"\n        Directly set the selected style.\n        Args:\n            selected_style (str): The name of the style to select.\n        \"\"\"\n        self.selected_style = selected_style\n        logging.info(f\"Selected style set to: {self.selected_style}\")\n\n    def get_style_path(self) -> Optional[Path]:\n        \"\"\"\n        Get the path to the selected style.\n        Returns:\n            Path: A Path object representing the path to the selected style file, or None if not found.\n        \"\"\"\n        try:\n            styles = self.get_styles()\n            if self.selected_style not in styles:\n                raise ValueError(f\"Style '{self.selected_style}' not found.\")\n            file_name, _ = styles[self.selected_style]\n            return self.styles_directory / file_name\n        except Exception as e:\n            logging.error(f\"Error retrieving selected style: {e}\")\n            return None\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/template_base.py",
    "content": "\"\"\"\nThis module is used to store the global configuration of the application.\n\"\"\"\n# app/libs/resume_and_cover_builder/template_base.py\n\n\n\nprompt_cover_letter_template = \"\"\"\n- **Template to Use**\n```\n<section id=\"cover-letter\">\n    <div style=\"display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;\">\n        <div>\n            <p>[Your Name]</p>\n            <p>[Your Address]</p>\n            <p>[City, State ZIP]</p>\n            <p>[Your Email]</p>\n            <p>[Your Phone Number]</p>\n        </div>\n        <div style=\"text-align: right;\">\n            <p>[Company Name]</p>\n        </div>\n    </div>\n    <div>\n    <p>Dear [Recipient Team],</p>\n    <p>[Opening paragraph: Introduce yourself and state the position you are applying for.]</p>\n    <p>[Body paragraphs: Highlight your qualifications, experiences, and how they align with the job requirements.]</p>\n    <p>[Closing paragraph: Express your enthusiasm for the position and thank the recipient for their consideration.]</p>\n    <p>Sincerely,</p>\n    <p>[Your Name]</p>\n    <p>[Date]</p>\n    </div>\n</section>\n```\nThe results should be provided in html format, Provide only the html code for the cover letter, without any explanations or additional text and also without ```html ```\n\"\"\"\nprompt_header_template = \"\"\"\n- **Template to Use**\n```\n<header>\n  <h1>[Name and Surname]</h1>\n  <div class=\"contact-info\"> \n    <p class=\"fas fa-map-marker-alt\">\n      <span>[Your City, Your Country]</span>\n    </p> \n    <p class=\"fas fa-phone\">\n      <span>[Your Prefix Phone number]</span>\n    </p> \n    <p class=\"fas fa-envelope\">\n      <span>[Your Email]</span>\n    </p> \n    <p class=\"fab fa-linkedin\">\n      <a href=\"[Link LinkedIn account]\">LinkedIn</a>\n    </p> \n    <p class=\"fab fa-github\">\n      <a href=\"[Link GitHub account]\">GitHub</a>\n    </p> \n  </div>\n</header>\n```\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\n\"\"\"\n\nprompt_education_template = \"\"\"\n- **Template to Use**\n```\n<section id=\"education\">\n    <h2>Education</h2>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\">[University Name]</span>\n          <span class=\"entry-location\">[Location] </span>\n      </div>\n      <div class=\"entry-details\">\n          <span class=\"entry-title\">[Degree] in [Field of Study] | Grade: [Your Grade]</span>\n          <span class=\"entry-year\">[Start Year] – [End Year]  </span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Course Name] → Grade: [Grade]</li>\n          <li>[Course Name] → Grade: [Grade]</li>\n          <li>[Course Name] → Grade: [Grade]</li>\n          <li>[Course Name] → Grade: [Grade]</li>\n          <li>[Course Name] → Grade: [Grade]</li>\n      </ul>\n    </div>\n</section>\n```\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\"\"\"\n\n\nprompt_working_experience_template = \"\"\"\n- **Template to Use**\n```\n<section id=\"work-experience\">\n    <h2>Work Experience</h2>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\">[Company Name]</span>\n          <span class=\"entry-location\">[Location]</span>\n      </div>\n      <div class=\"entry-details\">\n          <span class=\"entry-title\">[Your Job Title]</span>\n          <span class=\"entry-year\">[Start Date] – [End Date] </span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Describe your responsibilities and achievements in this role] </li>\n          <li>[Describe any key projects or technologies you worked with]  </li>\n          <li>[Mention any notable accomplishments or results]</li>\n      </ul>\n    </div>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\">[Company Name]</span>\n          <span class=\"entry-location\">[Location]</span>\n      </div>\n      <div class=\"entry-details\">\n          <span class=\"entry-title\">[Your Job Title]</span>\n          <span class=\"entry-year\">[Start Date] – [End Date] </span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Describe your responsibilities and achievements in this role] </li>\n          <li>[Describe any key projects or technologies you worked with]  </li>\n          <li>[Mention any notable accomplishments or results]</li>\n      </ul>\n    </div>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\">[Company Name]</span>\n          <span class=\"entry-location\">[Location]</span>\n      </div>\n      <div class=\"entry-details\">\n          <span class=\"entry-title\">[Your Job Title]</span>\n          <span class=\"entry-year\">[Start Date] – [End Date] </span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Describe your responsibilities and achievements in this role] </li>\n          <li>[Describe any key projects or technologies you worked with]  </li>\n          <li>[Mention any notable accomplishments or results]</li>\n      </ul>\n    </div>\n</section>\n```\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\"\"\"\n\n\nprompt_projects_template = \"\"\"\n- **Template to Use**\n```\n<section id=\"side-projects\">\n    <h2>Side Projects</h2>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\"><i class=\"fab fa-github\"></i> <a href=\"[Github Repo or Link]\">[Project Name]</a></span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Describe any notable recognition or reception]</li>\n          <li>[Describe any notable recognition or reception]</li>\n      </ul>\n    </div>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\"><i class=\"fab fa-github\"></i> <a href=\"[Github Repo or Link]\">[Project Name]</a></span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Describe any notable recognition or reception]</li>\n          <li>[Describe any notable recognition or reception]</li>\n      </ul>\n    </div>\n    <div class=\"entry\">\n      <div class=\"entry-header\">\n          <span class=\"entry-name\"><i class=\"fab fa-github\"></i> <a href=\"[Github Repo or Link]\">[Project Name]</a></span>\n      </div>\n      <ul class=\"compact-list\">\n          <li>[Describe any notable recognition or reception]</li>\n          <li>[Describe any notable recognition or reception]</li>\n      </ul>\n    </div>\n</section>\n```\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\n\"\"\"\n\n\nprompt_achievements_template = \"\"\"\n- **Template to Use**\n```\n<section id=\"achievements\">\n    <h2>Achievements</h2>\n    <ul class=\"compact-list\">\n      <li><strong>[Award or Recognition or Scholarship or Honor]:</strong> [Describe]</li>\n      <li><strong>[Award or Recognition or Scholarship or Honor]:</strong> [Describe]</li>\n      <li><strong>[Award or Recognition or Scholarship or Honor]:</strong> [Describe]</li>\n    </ul>\n</section>\n```\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\n\"\"\"\n\nprompt_certifications_template = \"\"\"\n- **Template to Use**\n```\n<section id=\"certifications\">\n    <h2>Certifications</h2>\n    <ul class=\"compact-list\">\n      <li><strong>[Certification Name]:</strong> [Describe]</li>\n      <li><strong>[Certification Name]:</strong> [Describe]</li>\n    </ul>\n</section>\n```\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\n\"\"\"\n\nprompt_additional_skills_template = \"\"\"\n- **Template to Use**\n'''\n<section id=\"skills-languages\">\n    <h2>Additional Skills</h2>\n    <div class=\"two-column\">\n      <ul class=\"compact-list\">\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n      </ul>\n      <ul class=\"compact-list\">\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li>[Specific Skill or Technology]</li>\n          <li><strong>Languages:</strong> </li>\n      </ul>\n    </div>\n</section>\n'''\nThe results should be provided in html format, Provide only the html code for the resume, without any explanations or additional text and also without ```html ```\n\"\"\"\n"
  },
  {
    "path": "src/libs/resume_and_cover_builder/utils.py",
    "content": "\"\"\"\nThis module contains utility functions for the Resume and Cover Letter Builder service.\n\"\"\"\n\n# app/libs/resume_and_cover_builder/utils.py\nimport json\nimport openai\nimport time\nfrom datetime import datetime\nfrom typing import Dict, List\nfrom langchain_core.messages.ai import AIMessage\nfrom langchain_core.prompt_values import StringPromptValue\nfrom langchain_openai import ChatOpenAI\nfrom .config import global_config\nfrom loguru import logger\nfrom requests.exceptions import HTTPError as HTTPStatusError\n\n\nclass LLMLogger:\n\n    def __init__(self, llm: ChatOpenAI):\n        self.llm = llm\n\n    @staticmethod\n    def log_request(prompts, parsed_reply: Dict[str, Dict]):\n        calls_log = global_config.LOG_OUTPUT_FILE_PATH / \"open_ai_calls.json\"\n        if isinstance(prompts, StringPromptValue):\n            prompts = prompts.text\n        elif isinstance(prompts, Dict):\n            # Convert prompts to a dictionary if they are not in the expected format\n            prompts = {\n                f\"prompt_{i+1}\": prompt.content\n                for i, prompt in enumerate(prompts.messages)\n            }\n        else:\n            prompts = {\n                f\"prompt_{i+1}\": prompt.content\n                for i, prompt in enumerate(prompts.messages)\n            }\n\n        current_time = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n\n        # Extract token usage details from the response\n        token_usage = parsed_reply[\"usage_metadata\"]\n        output_tokens = token_usage[\"output_tokens\"]\n        input_tokens = token_usage[\"input_tokens\"]\n        total_tokens = token_usage[\"total_tokens\"]\n\n        # Extract model details from the response\n        model_name = parsed_reply[\"response_metadata\"][\"model_name\"]\n        prompt_price_per_token = 0.00000015\n        completion_price_per_token = 0.0000006\n\n        # Calculate the total cost of the API call\n        total_cost = (input_tokens * prompt_price_per_token) + (\n            output_tokens * completion_price_per_token\n        )\n\n        # Create a log entry with all relevant information\n        log_entry = {\n            \"model\": model_name,\n            \"time\": current_time,\n            \"prompts\": prompts,\n            \"replies\": parsed_reply[\"content\"],  # Response content\n            \"total_tokens\": total_tokens,\n            \"input_tokens\": input_tokens,\n            \"output_tokens\": output_tokens,\n            \"total_cost\": total_cost,\n        }\n\n        # Write the log entry to the log file in JSON format\n        with open(calls_log, \"a\", encoding=\"utf-8\") as f:\n            json_string = json.dumps(log_entry, ensure_ascii=False, indent=4)\n            f.write(json_string + \"\\n\")\n\n\nclass LoggerChatModel:\n\n    def __init__(self, llm: ChatOpenAI):\n        self.llm = llm\n\n    def __call__(self, messages: List[Dict[str, str]]) -> str:\n        max_retries = 15\n        retry_delay = 10\n\n        for attempt in range(max_retries):\n            try:\n                reply = self.llm.invoke(messages)\n                parsed_reply = self.parse_llmresult(reply)\n                LLMLogger.log_request(prompts=messages, parsed_reply=parsed_reply)\n                return reply\n            except (openai.RateLimitError, HTTPStatusError) as err:\n                if isinstance(err, HTTPStatusError) and err.response.status_code == 429:\n                    logger.warning(f\"HTTP 429 Too Many Requests: Waiting for {retry_delay} seconds before retrying (Attempt {attempt + 1}/{max_retries})...\")\n                    time.sleep(retry_delay)\n                    retry_delay *= 2\n                else:\n                    wait_time = self.parse_wait_time_from_error_message(str(err))\n                    logger.warning(f\"Rate limit exceeded or API error. Waiting for {wait_time} seconds before retrying (Attempt {attempt + 1}/{max_retries})...\")\n                    time.sleep(wait_time)\n            except Exception as e:\n                logger.error(f\"Unexpected error occurred: {str(e)}, retrying in {retry_delay} seconds... (Attempt {attempt + 1}/{max_retries})\")\n                time.sleep(retry_delay)\n                retry_delay *= 2\n\n        logger.critical(\"Failed to get a response from the model after multiple attempts.\")\n        raise Exception(\"Failed to get a response from the model after multiple attempts.\")\n\n    def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]:\n        # Parse the LLM result into a structured format.\n        content = llmresult.content\n        response_metadata = llmresult.response_metadata\n        id_ = llmresult.id\n        usage_metadata = llmresult.usage_metadata\n\n        parsed_result = {\n            \"content\": content,\n            \"response_metadata\": {\n                \"model_name\": response_metadata.get(\"model_name\", \"\"),\n                \"system_fingerprint\": response_metadata.get(\"system_fingerprint\", \"\"),\n                \"finish_reason\": response_metadata.get(\"finish_reason\", \"\"),\n                \"logprobs\": response_metadata.get(\"logprobs\", None),\n            },\n            \"id\": id_,\n            \"usage_metadata\": {\n                \"input_tokens\": usage_metadata.get(\"input_tokens\", 0),\n                \"output_tokens\": usage_metadata.get(\"output_tokens\", 0),\n                \"total_tokens\": usage_metadata.get(\"total_tokens\", 0),\n            },\n        }\n        return parsed_result\n"
  },
  {
    "path": "src/logging.py",
    "content": "import logging.handlers\nimport os\nimport sys\nimport logging\nfrom loguru import logger\nfrom selenium.webdriver.remote.remote_connection import LOGGER as selenium_logger\n\nfrom config import LOG_LEVEL, LOG_SELENIUM_LEVEL, LOG_TO_CONSOLE, LOG_TO_FILE\n\n\ndef remove_default_loggers():\n    \"\"\"Remove default loggers from root logger.\"\"\"\n    root_logger = logging.getLogger()\n    if root_logger.hasHandlers():\n        root_logger.handlers.clear()\n    if os.path.exists(\"log/app.log\"):\n        os.remove(\"log/app.log\")\n\ndef init_loguru_logger():\n    \"\"\"Initialize and configure loguru logger.\"\"\"\n\n    def get_log_filename():\n        return f\"log/app.log\"\n\n    log_file = get_log_filename()\n\n    os.makedirs(os.path.dirname(log_file), exist_ok=True)\n\n    logger.remove()\n\n    # Add file logger if LOG_TO_FILE is True\n    if LOG_TO_FILE:\n        logger.add(\n            log_file,\n            level=LOG_LEVEL,\n            rotation=\"10 MB\",\n            retention=\"1 week\",\n            compression=\"zip\",\n            format=\"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>\",\n            backtrace=True,\n            diagnose=True,\n        )\n\n    # Add console logger if LOG_TO_CONSOLE is True\n    if LOG_TO_CONSOLE:\n        logger.add(\n            sys.stderr,\n            level=LOG_LEVEL,\n            format=\"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>\",\n            backtrace=True,\n            diagnose=True,\n        )\n\n\ndef init_selenium_logger():\n    \"\"\"Initialize and configure selenium logger to write to selenium.log.\"\"\"\n    log_file = \"log/selenium.log\"\n    os.makedirs(os.path.dirname(log_file), exist_ok=True)\n\n    selenium_logger.handlers.clear()\n\n    selenium_logger.setLevel(LOG_SELENIUM_LEVEL)\n\n    # Create file handler for selenium logger\n    file_handler = logging.handlers.TimedRotatingFileHandler(\n        log_file, when=\"D\", interval=1, backupCount=5\n    )\n    file_handler.setLevel(LOG_SELENIUM_LEVEL)\n\n    # Define a simplified format for selenium logger entries\n    formatter = logging.Formatter(\"%(asctime)s - %(levelname)s - %(message)s\")\n    file_handler.setFormatter(formatter)\n\n    # Add the file handler to selenium_logger\n    selenium_logger.addHandler(file_handler)\n\n\nremove_default_loggers()\ninit_loguru_logger()\ninit_selenium_logger()\n"
  },
  {
    "path": "src/resume_schemas/job_application_profile.py",
    "content": "from dataclasses import dataclass\n\nimport yaml\n\nfrom src.logging import logger\n\n\n@dataclass\nclass SelfIdentification:\n    gender: str\n    pronouns: str\n    veteran: str\n    disability: str\n    ethnicity: str\n\n\n@dataclass\nclass LegalAuthorization:\n    eu_work_authorization: str\n    us_work_authorization: str\n    requires_us_visa: str\n    legally_allowed_to_work_in_us: str\n    requires_us_sponsorship: str\n    requires_eu_visa: str\n    legally_allowed_to_work_in_eu: str\n    requires_eu_sponsorship: str\n    canada_work_authorization: str\n    requires_canada_visa: str\n    legally_allowed_to_work_in_canada: str\n    requires_canada_sponsorship: str\n    uk_work_authorization: str\n    requires_uk_visa: str \n    legally_allowed_to_work_in_uk: str\n    requires_uk_sponsorship: str\n\n\n\n@dataclass\nclass WorkPreferences:\n    remote_work: str\n    in_person_work: str\n    open_to_relocation: str\n    willing_to_complete_assessments: str\n    willing_to_undergo_drug_tests: str\n    willing_to_undergo_background_checks: str\n\n\n@dataclass\nclass Availability:\n    notice_period: str\n\n\n@dataclass\nclass SalaryExpectations:\n    salary_range_usd: str\n\n\n@dataclass\nclass JobApplicationProfile:\n    self_identification: SelfIdentification\n    legal_authorization: LegalAuthorization\n    work_preferences: WorkPreferences\n    availability: Availability\n    salary_expectations: SalaryExpectations\n\n    def __init__(self, yaml_str: str):\n        logger.debug(\"Initializing JobApplicationProfile with provided YAML string\")\n        try:\n            data = yaml.safe_load(yaml_str)\n            logger.debug(f\"YAML data successfully parsed: {data}\")\n        except yaml.YAMLError as e:\n            logger.error(f\"Error parsing YAML file: {e}\")\n            raise ValueError(\"Error parsing YAML file.\") from e\n        except Exception as e:\n            logger.error(f\"Unexpected error occurred while parsing the YAML file: {e}\")\n            raise RuntimeError(\"An unexpected error occurred while parsing the YAML file.\") from e\n\n        if not isinstance(data, dict):\n            logger.error(f\"YAML data must be a dictionary, received: {type(data)}\")\n            raise TypeError(\"YAML data must be a dictionary.\")\n\n        # Process self_identification\n        try:\n            logger.debug(\"Processing self_identification\")\n            self.self_identification = SelfIdentification(**data['self_identification'])\n            logger.debug(f\"self_identification processed: {self.self_identification}\")\n        except KeyError as e:\n            logger.error(f\"Required field {e} is missing in self_identification data.\")\n            raise KeyError(f\"Required field {e} is missing in self_identification data.\") from e\n        except TypeError as e:\n            logger.error(f\"Error in self_identification data: {e}\")\n            raise TypeError(f\"Error in self_identification data: {e}\") from e\n        except AttributeError as e:\n            logger.error(f\"Attribute error in self_identification processing: {e}\")\n            raise AttributeError(\"Attribute error in self_identification processing.\") from e\n        except Exception as e:\n            logger.error(f\"An unexpected error occurred while processing self_identification: {e}\")\n            raise RuntimeError(\"An unexpected error occurred while processing self_identification.\") from e\n\n        # Process legal_authorization\n        try:\n            logger.debug(\"Processing legal_authorization\")\n            self.legal_authorization = LegalAuthorization(**data['legal_authorization'])\n            logger.debug(f\"legal_authorization processed: {self.legal_authorization}\")\n        except KeyError as e:\n            logger.error(f\"Required field {e} is missing in legal_authorization data.\")\n            raise KeyError(f\"Required field {e} is missing in legal_authorization data.\") from e\n        except TypeError as e:\n            logger.error(f\"Error in legal_authorization data: {e}\")\n            raise TypeError(f\"Error in legal_authorization data: {e}\") from e\n        except AttributeError as e:\n            logger.error(f\"Attribute error in legal_authorization processing: {e}\")\n            raise AttributeError(\"Attribute error in legal_authorization processing.\") from e\n        except Exception as e:\n            logger.error(f\"An unexpected error occurred while processing legal_authorization: {e}\")\n            raise RuntimeError(\"An unexpected error occurred while processing legal_authorization.\") from e\n\n        # Process work_preferences\n        try:\n            logger.debug(\"Processing work_preferences\")\n            self.work_preferences = WorkPreferences(**data['work_preferences'])\n            logger.debug(f\"Work_preferences processed: {self.work_preferences}\")\n        except KeyError as e:\n            logger.error(f\"Required field {e} is missing in work_preferences data.\")\n            raise KeyError(f\"Required field {e} is missing in work_preferences data.\") from e\n        except TypeError as e:\n            logger.error(f\"Error in work_preferences data: {e}\")\n            raise TypeError(f\"Error in work_preferences data: {e}\") from e\n        except AttributeError as e:\n            logger.error(f\"Attribute error in work_preferences processing: {e}\")\n            raise AttributeError(\"Attribute error in work_preferences processing.\") from e\n        except Exception as e:\n            logger.error(f\"An unexpected error occurred while processing work_preferences: {e}\")\n            raise RuntimeError(\"An unexpected error occurred while processing work_preferences.\") from e\n\n        # Process availability\n        try:\n            logger.debug(\"Processing availability\")\n            self.availability = Availability(**data['availability'])\n            logger.debug(f\"Availability processed: {self.availability}\")\n        except KeyError as e:\n            logger.error(f\"Required field {e} is missing in availability data.\")\n            raise KeyError(f\"Required field {e} is missing in availability data.\") from e\n        except TypeError as e:\n            logger.error(f\"Error in availability data: {e}\")\n            raise TypeError(f\"Error in availability data: {e}\") from e\n        except AttributeError as e:\n            logger.error(f\"Attribute error in availability processing: {e}\")\n            raise AttributeError(\"Attribute error in availability processing.\") from e\n        except Exception as e:\n            logger.error(f\"An unexpected error occurred while processing availability: {e}\")\n            raise RuntimeError(\"An unexpected error occurred while processing availability.\") from e\n\n        # Process salary_expectations\n        try:\n            logger.debug(\"Processing salary_expectations\")\n            self.salary_expectations = SalaryExpectations(**data['salary_expectations'])\n            logger.debug(f\"salary_expectations processed: {self.salary_expectations}\")\n        except KeyError as e:\n            logger.error(f\"Required field {e} is missing in salary_expectations data.\")\n            raise KeyError(f\"Required field {e} is missing in salary_expectations data.\") from e\n        except TypeError as e:\n            logger.error(f\"Error in salary_expectations data: {e}\")\n            raise TypeError(f\"Error in salary_expectations data: {e}\") from e\n        except AttributeError as e:\n            logger.error(f\"Attribute error in salary_expectations processing: {e}\")\n            raise AttributeError(\"Attribute error in salary_expectations processing.\") from e\n        except Exception as e:\n            logger.error(f\"An unexpected error occurred while processing salary_expectations: {e}\")\n            raise RuntimeError(\"An unexpected error occurred while processing salary_expectations.\") from e\n\n        logger.debug(\"JobApplicationProfile initialization completed successfully.\")\n\n    def __str__(self):\n        logger.debug(\"Generating string representation of JobApplicationProfile\")\n\n        def format_dataclass(obj):\n            return \"\\n\".join(f\"{field.name}: {getattr(obj, field.name)}\" for field in obj.__dataclass_fields__.values())\n\n        formatted_str = (f\"Self Identification:\\n{format_dataclass(self.self_identification)}\\n\\n\"\n                         f\"Legal Authorization:\\n{format_dataclass(self.legal_authorization)}\\n\\n\"\n                         f\"Work Preferences:\\n{format_dataclass(self.work_preferences)}\\n\\n\"\n                         f\"Availability: {self.availability.notice_period}\\n\\n\"\n                         f\"Salary Expectations: {self.salary_expectations.salary_range_usd}\\n\\n\")\n        logger.debug(f\"String representation generated: {formatted_str}\")\n        return formatted_str\n"
  },
  {
    "path": "src/resume_schemas/resume.py",
    "content": "from dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional, Union\nimport yaml\nfrom pydantic import BaseModel, EmailStr, HttpUrl, Field\n\n\n\nclass PersonalInformation(BaseModel):\n    name: Optional[str]\n    surname: Optional[str]\n    date_of_birth: Optional[str]\n    country: Optional[str]\n    city: Optional[str]\n    address: Optional[str]\n    zip_code: Optional[str] = Field(None, min_length=5, max_length=10)\n    phone_prefix: Optional[str]\n    phone: Optional[str]\n    email: Optional[EmailStr]\n    github: Optional[HttpUrl] = None\n    linkedin: Optional[HttpUrl] = None\n\n\nclass EducationDetails(BaseModel):\n    education_level: Optional[str]\n    institution: Optional[str]\n    field_of_study: Optional[str]\n    final_evaluation_grade: Optional[str]\n    start_date: Optional[str]\n    year_of_completion: Optional[int]\n    exam: Optional[Union[List[Dict[str, str]], Dict[str, str]]] = None\n\n\nclass ExperienceDetails(BaseModel):\n    position: Optional[str]\n    company: Optional[str]\n    employment_period: Optional[str]\n    location: Optional[str]\n    industry: Optional[str]\n    key_responsibilities: Optional[List[Dict[str, str]]] = None\n    skills_acquired: Optional[List[str]] = None\n\n\nclass Project(BaseModel):\n    name: Optional[str]\n    description: Optional[str]\n    link: Optional[HttpUrl] = None\n\n\nclass Achievement(BaseModel):\n    name: Optional[str]\n    description: Optional[str]\n\n\nclass Certifications(BaseModel):\n    name: Optional[str]\n    description: Optional[str]\n\n\nclass Language(BaseModel):\n    language: Optional[str]\n    proficiency: Optional[str]\n\n\nclass Availability(BaseModel):\n    notice_period: Optional[str]\n\n\nclass SalaryExpectations(BaseModel):\n    salary_range_usd: Optional[str]\n\n\nclass SelfIdentification(BaseModel):\n    gender: Optional[str]\n    pronouns: Optional[str]\n    veteran: Optional[str]\n    disability: Optional[str]\n    ethnicity: Optional[str]\n\n\nclass LegalAuthorization(BaseModel):\n    eu_work_authorization: Optional[str]\n    us_work_authorization: Optional[str]\n    requires_us_visa: Optional[str]\n    requires_us_sponsorship: Optional[str]\n    requires_eu_visa: Optional[str]\n    legally_allowed_to_work_in_eu: Optional[str]\n    legally_allowed_to_work_in_us: Optional[str]\n    requires_eu_sponsorship: Optional[str]\n\n\nclass Resume(BaseModel):\n    personal_information: Optional[PersonalInformation]\n    education_details: Optional[List[EducationDetails]] = None\n    experience_details: Optional[List[ExperienceDetails]] = None\n    projects: Optional[List[Project]] = None\n    achievements: Optional[List[Achievement]] = None\n    certifications: Optional[List[Certifications]] = None\n    languages: Optional[List[Language]] = None\n    interests: Optional[List[str]] = None\n\n    @staticmethod\n    def normalize_exam_format(exam):\n        if isinstance(exam, dict):\n            return [{k: v} for k, v in exam.items()]\n        return exam\n\n    def __init__(self, yaml_str: str):\n        try:\n            # Parse the YAML string\n            data = yaml.safe_load(yaml_str)\n\n            if 'education_details' in data:\n                for ed in data['education_details']:\n                    if 'exam' in ed:\n                        ed['exam'] = self.normalize_exam_format(ed['exam'])\n\n            # Create an instance of Resume from the parsed data\n            super().__init__(**data)\n        except yaml.YAMLError as e:\n            raise ValueError(\"Error parsing YAML file.\") from e\n        except Exception as e:\n            raise Exception(f\"Unexpected error while parsing YAML: {e}\") from e\n\n\n    def _process_personal_information(self, data: Dict[str, Any]) -> PersonalInformation:\n        try:\n            return PersonalInformation(**data)\n        except TypeError as e:\n            raise TypeError(f\"Invalid data for PersonalInformation: {e}\") from e\n        except AttributeError as e:\n            raise AttributeError(f\"AttributeError in PersonalInformation: {e}\") from e\n        except Exception as e:\n            raise Exception(f\"Unexpected error in PersonalInformation processing: {e}\") from e\n\n    def _process_education_details(self, data: List[Dict[str, Any]]) -> List[EducationDetails]:\n        education_list = []\n        for edu in data:\n            try:\n                exams = [Exam(name=k, grade=v) for k, v in edu.get('exam', {}).items()]\n                education = EducationDetails(\n                    education_level=edu.get('education_level'),\n                    institution=edu.get('institution'),\n                    field_of_study=edu.get('field_of_study'),\n                    final_evaluation_grade=edu.get('final_evaluation_grade'),\n                    start_date=edu.get('start_date'),\n                    year_of_completion=edu.get('year_of_completion'),\n                    exam=exams\n                )\n                education_list.append(education)\n            except KeyError as e:\n                raise KeyError(f\"Missing field in education details: {e}\") from e\n            except TypeError as e:\n                raise TypeError(f\"Invalid data for Education: {e}\") from e\n            except AttributeError as e:\n                raise AttributeError(f\"AttributeError in Education: {e}\") from e\n            except Exception as e:\n                raise Exception(f\"Unexpected error in Education processing: {e}\") from e\n        return education_list\n\n    def _process_experience_details(self, data: List[Dict[str, Any]]) -> List[ExperienceDetails]:\n        experience_list = []\n        for exp in data:\n            try:\n                key_responsibilities = [\n                    Responsibility(description=list(resp.values())[0])\n                    for resp in exp.get('key_responsibilities', [])\n                ]\n                skills_acquired = [str(skill) for skill in exp.get('skills_acquired', [])]\n                experience = ExperienceDetails(\n                    position=exp['position'],\n                    company=exp['company'],\n                    employment_period=exp['employment_period'],\n                    location=exp['location'],\n                    industry=exp['industry'],\n                    key_responsibilities=key_responsibilities,\n                    skills_acquired=skills_acquired\n                )\n                experience_list.append(experience)\n            except KeyError as e:\n                raise KeyError(f\"Missing field in experience details: {e}\") from e\n            except TypeError as e:\n                raise TypeError(f\"Invalid data for Experience: {e}\") from e\n            except AttributeError as e:\n                raise AttributeError(f\"AttributeError in Experience: {e}\") from e\n            except Exception as e:\n                raise Exception(f\"Unexpected error in Experience processing: {e}\") from e\n        return experience_list\n\n\n@dataclass\nclass Exam:\n    name: str\n    grade: str\n\n@dataclass\nclass Responsibility:\n    description: str"
  },
  {
    "path": "src/utils/__init__.py",
    "content": ""
  },
  {
    "path": "src/utils/chrome_utils.py",
    "content": "import os\nimport time\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service as ChromeService\nfrom selenium.webdriver.chrome.options import Options\nfrom webdriver_manager.chrome import ChromeDriverManager  # Import webdriver_manager\nimport urllib\nfrom src.logging import logger\n\ndef chrome_browser_options():\n    logger.debug(\"Setting Chrome browser options\")\n    options = Options()\n    options.add_argument(\"--start-maximized\")\n    options.add_argument(\"--no-sandbox\")\n    options.add_argument(\"--disable-dev-shm-usage\")\n    options.add_argument(\"--ignore-certificate-errors\")\n    options.add_argument(\"--disable-extensions\")\n    options.add_argument(\"--disable-gpu\")  # Opzionale, utile in alcuni ambienti\n    options.add_argument(\"window-size=1200x800\")\n    options.add_argument(\"--disable-background-timer-throttling\")\n    options.add_argument(\"--disable-backgrounding-occluded-windows\")\n    options.add_argument(\"--disable-translate\")\n    options.add_argument(\"--disable-popup-blocking\")\n    options.add_argument(\"--no-first-run\")\n    options.add_argument(\"--no-default-browser-check\")\n    options.add_argument(\"--disable-logging\")\n    options.add_argument(\"--disable-autofill\")\n    options.add_argument(\"--disable-plugins\")\n    options.add_argument(\"--disable-animations\")\n    options.add_argument(\"--disable-cache\")\n    options.add_argument(\"--incognito\")\n    options.add_argument(\"--allow-file-access-from-files\")  # Consente l'accesso ai file locali\n    options.add_argument(\"--disable-web-security\")         # Disabilita la sicurezza web\n    logger.debug(\"Using Chrome in incognito mode\")\n    \n    return options\n\ndef init_browser() -> webdriver.Chrome:\n    try:\n        options = chrome_browser_options()\n        # Use webdriver_manager to handle ChromeDriver\n        driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=options)\n        logger.debug(\"Chrome browser initialized successfully.\")\n        return driver\n    except Exception as e:\n        logger.error(f\"Failed to initialize browser: {str(e)}\")\n        raise RuntimeError(f\"Failed to initialize browser: {str(e)}\")\n\n\n\ndef HTML_to_PDF(html_content, driver):\n    \"\"\"\n    Converte una stringa HTML in un PDF e restituisce il PDF come stringa base64.\n\n    :param html_content: Stringa contenente il codice HTML da convertire.\n    :param driver: Istanza del WebDriver di Selenium.\n    :return: Stringa base64 del PDF generato.\n    :raises ValueError: Se l'input HTML non è una stringa valida.\n    :raises RuntimeError: Se si verifica un'eccezione nel WebDriver.\n    \"\"\"\n    # Validazione del contenuto HTML\n    if not isinstance(html_content, str) or not html_content.strip():\n        raise ValueError(\"Il contenuto HTML deve essere una stringa non vuota.\")\n\n    # Codifica l'HTML in un URL di tipo data\n    encoded_html = urllib.parse.quote(html_content)\n    data_url = f\"data:text/html;charset=utf-8,{encoded_html}\"\n\n    try:\n        driver.get(data_url)\n        # Attendi che la pagina si carichi completamente\n        time.sleep(2)  # Potrebbe essere necessario aumentare questo tempo per HTML complessi\n\n        # Esegue il comando CDP per stampare la pagina in PDF\n        pdf_base64 = driver.execute_cdp_cmd(\"Page.printToPDF\", {\n            \"printBackground\": True,          # Includi lo sfondo nella stampa\n            \"landscape\": False,               # Stampa in verticale (False per ritratto)\n            \"paperWidth\": 8.27,               # Larghezza del foglio in pollici (A4)\n            \"paperHeight\": 11.69,             # Altezza del foglio in pollici (A4)\n            \"marginTop\": 0.8,                  # Margine superiore in pollici (circa 2 cm)\n            \"marginBottom\": 0.8,               # Margine inferiore in pollici (circa 2 cm)\n            \"marginLeft\": 0.5,                 # Margine sinistro in pollici (circa 1.27 cm)\n            \"marginRight\": 0.5,                # Margine destro in pollici (circa 1.27 cm)\n            \"displayHeaderFooter\": False,      # Non visualizzare intestazioni e piè di pagina\n            \"preferCSSPageSize\": True,         # Preferire le dimensioni della pagina CSS\n            \"generateDocumentOutline\": False,  # Non generare un sommario del documento\n            \"generateTaggedPDF\": False,        # Non generare PDF taggato\n            \"transferMode\": \"ReturnAsBase64\"   # Restituire il PDF come stringa base64\n        })\n        return pdf_base64['data']\n    except Exception as e:\n        logger.error(f\"Si è verificata un'eccezione WebDriver: {e}\")\n        raise RuntimeError(f\"Si è verificata un'eccezione WebDriver: {e}\")\n"
  },
  {
    "path": "src/utils/constants.py",
    "content": "DATE_ALL_TIME = \"all_time\"\nDATE_MONTH = \"month\"\nDATE_WEEK = \"week\"\nDATE_24_HOURS = \"24_hours\"\n\n\n# constants used in application\nSECRETS_YAML = \"secrets.yaml\"\nWORK_PREFERENCES_YAML = \"work_preferences.yaml\"\nPLAIN_TEXT_RESUME_YAML = \"plain_text_resume.yaml\"\n\n\n# String constants used in the application\nDEBUG = \"DEBUG\"\nINFO = \"INFO\"\nWARNING = \"WARNING\"\nERROR = \"ERROR\"\nCRITICAL = \"CRITICAL\"\n\nMINIMUM_LOG_LEVEL = \"MINIMUM_LOG_LEVEL\"\n\n# Constants in llm_manager.py\nUSAGE_METADATA = \"usage_metadata\"\nOUTPUT_TOKENS = \"output_tokens\"\nINPUT_TOKENS = \"input_tokens\"\nTOTAL_TOKENS = \"total_tokens\"\nTOKEN_USAGE = \"token_usage\"\n\nMODEL = \"model\"\nTIME = \"time\"\nPROMPTS = \"prompts\"\nREPLIES = \"replies\"\nCONTENT = \"content\"\nTOTAL_COST = \"total_cost\"\n\nRESPONSE_METADATA = \"response_metadata\"\nMODEL_NAME = \"model_name\"\nSYSTEM_FINGERPRINT = \"system_fingerprint\"\nFINISH_REASON = \"finish_reason\"\nLOGPROBS = \"logprobs\"\nID = \"id\"\nTEXT = \"text\"\nPHRASE = \"phrase\"\nQUESTION = \"question\"\nOPTIONS = \"options\"\nRESUME = \"resume\"\nRESUME_SECTION = \"resume_section\"\nJOB_DESCRIPTION = \"job_description\"\nCOMPANY = \"company\"\nJOB_APPLICATION_PROFILE = \"job_application_profile\"\nRESUME_EDUCATIONS = \"resume_educations\"\nRESUME_JOBS = \"resume_jobs\"\nRESUME_PROJECTS = \"resume_projects\"\n\nPERSONAL_INFORMATION = \"personal_information\"\nSELF_IDENTIFICATION = \"self_identification\"\nLEGAL_AUTHORIZATION = \"legal_authorization\"\nWORK_PREFERENCES = \"work_preferences\"\nEDUCATION_DETAILS = \"education_details\"\nEXPERIENCE_DETAILS = \"experience_details\"\nPROJECTS = \"projects\"\nAVAILABILITY = \"availability\"\nSALARY_EXPECTATIONS = \"salary_expectations\"\nCERTIFICATIONS = \"certifications\"\nLANGUAGES = \"languages\"\nINTERESTS = \"interests\"\nCOVER_LETTER = \"cover_letter\"\n\nLLM_MODEL_TYPE = \"llm_model_type\"\nLLM_API_URL = \"llm_api_url\"\nLLM_MODEL = \"llm_model\"\nOPENAI = \"openai\"\nCLAUDE = \"claude\"\nOLLAMA = \"ollama\"\nGEMINI = \"gemini\"\nHUGGINGFACE = \"huggingface\"\nPERPLEXITY = \"perplexity\"\n"
  }
]