File size: 10,198 Bytes
cb97851
 
 
 
 
 
 
146e731
 
10f94c1
cb97851
5d93a4f
91b2483
146e731
cb97851
f9a80bc
 
cb97851
 
 
 
 
146e731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edc4b6c
cb97851
 
bef6750
cb97851
146e731
bef6750
cb97851
edc4b6c
bef6750
cb97851
 
 
 
f8bff10
cb97851
b9464fb
cb97851
 
f9a80bc
146e731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9a80bc
146e731
 
 
b9464fb
cb97851
146e731
b9464fb
cb97851
 
 
 
 
 
b9464fb
cb97851
 
b9464fb
cb97851
 
 
b9464fb
cb97851
146e731
cb97851
146e731
 
 
 
 
 
 
 
 
 
 
 
 
b9464fb
cb97851
146e731
b9464fb
cb97851
 
b9464fb
 
 
 
 
cb97851
 
b9464fb
cb97851
 
 
b9464fb
cb97851
 
 
b9464fb
5d93a4f
 
 
 
0b01037
7e01b9e
 
 
 
 
 
 
 
5d93a4f
 
7e01b9e
 
 
 
 
 
 
 
 
 
 
 
 
 
cb97851
7e01b9e
 
cb97851
edc4b6c
 
 
b9464fb
146e731
edc4b6c
 
 
 
cb97851
91b2483
f8bff10
 
 
bef6750
b9464fb
f8bff10
b9464fb
f9a80bc
f8bff10
b9464fb
f8bff10
b9464fb
f8bff10
 
b9464fb
cb97851
 
 
b9464fb
cb97851
 
bef6750
cb97851
 
bef6750
cb97851
b9464fb
 
cb97851
 
 
b9464fb
 
 
 
5d93a4f
 
 
 
 
 
7e01b9e
0b01037
 
7e01b9e
5d93a4f
cb97851
 
 
b9464fb
 
cb97851
 
b9464fb
cb97851
 
146e731
 
 
 
b9464fb
cb97851
 
b9464fb
cb97851
 
b9464fb
cb97851
 
 
b9464fb
cb97851
 
b9464fb
cb97851
 
b9464fb
cb97851
10f94c1
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
gradio.py

Functions for handling Gradio UI interactions and processing user inputs.
"""

import logging
import shutil
from pathlib import Path
from functions.linkedin_resume import extract_text_from_linkedin_pdf, check_default_linkedin_pdf
from functions.github import get_github_repositories
from functions.job_call import load_default_job_call, summarize_job_call
from functions.writer_agent import write_resume
from configuration import DEFAULT_GITHUB_PROFILE

# pylint: disable=broad-exception-caught

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def process_with_default_option(
    use_default_pdf,
    linkedin_pdf,
    github_profile,
    job_post,
    user_instructions
):
    """Process inputs with consideration for default PDF option."""

    has_default, default_path = check_default_linkedin_pdf()

    # Determine which PDF file to use
    pdf_file = None

    if use_default_pdf and has_default:
        pdf_file = MockFile(default_path)

    elif linkedin_pdf is not None:
        pdf_file = linkedin_pdf

    return process_inputs(pdf_file, github_profile, job_post, user_instructions)


def process_inputs(linkedin_pdf, github_url, job_post_text, user_instructions):
    """
    Process the input files and URLs from the Gradio interface.

    Args:
        linkedin_pdf: Uploaded LinkedIn resume export PDF file or mock file object with path
        github_url (str): GitHub profile URL
        job_post_text (str): Job post text content
        user_instructions (str): Additional instructions from the user

    Returns:
        str: Formatted output with file and URL information
    """
    result = ""
    extraction_result = None
    logger.info("Processing user inputs from Gradio interface")

    # Process LinkedIn PDF file
    if linkedin_pdf is not None:

        # Handle both file objects and mock file objects with path strings
        file_path = linkedin_pdf.name
        file_display_name = Path(file_path).name

        result += "βœ… LinkedIn Resume PDF provided\n"
        logger.info("Processing LinkedIn PDF: %s", file_display_name)

        # Save uploaded file as new default (only if it's not already the default)
        project_root = Path(__file__).parent.parent
        default_pdf_path = project_root / "data" / "linkedin_profile.pdf"

        # Check if this is an uploaded file (not the default file)
        if not isinstance(linkedin_pdf, MockFile):
            try:
                # Create data directory if it doesn't exist
                default_pdf_path.parent.mkdir(exist_ok=True)

                # Copy uploaded file to default location
                shutil.copy2(file_path, default_pdf_path)
                result += " βœ… Saved as new default LinkedIn profile\n"
                logger.info("Saved uploaded LinkedIn PDF as new default: %s", default_pdf_path)

            except Exception as save_error:
                result += f" ⚠️  Could not save as default: {str(save_error)}\n"
                logger.warning("Failed to save LinkedIn PDF as default: %s", str(save_error))

        # Extract and structure text from the PDF
        extraction_result = extract_text_from_linkedin_pdf(file_path)

        if extraction_result["status"] == "success":
            result += " βœ… Text extraction successful\n\n"
            logger.info("LinkedIn PDF text extraction successful")

        elif extraction_result["status"] == "warning":
            result += f" ⚠️  Text extraction: {extraction_result['message']}\n\n"
            logger.warning("LinkedIn PDF extraction warning: %s", extraction_result['message'])
        else:
            result += f" ❌ Text extraction failed: {extraction_result['message']}\n\n"
            logger.error("LinkedIn PDF extraction failed: %s", extraction_result['message'])
    else:
        result += "❌ No LinkedIn resume PDF file uploaded\n\n"
        logger.info("No LinkedIn PDF file provided")

    # Process GitHub profile
    # Use default GitHub profile if none provided
    if github_url and github_url.strip():
        github_url_to_use = github_url.strip()

    else:
        github_url_to_use = DEFAULT_GITHUB_PROFILE

    if github_url_to_use:
        if github_url and github_url.strip():
            result += "βœ… GitHub Profile URL provided\n"

        else:
            result += "βœ… Using default GitHub Profile URL\n"

        logger.info("Processing GitHub URL: %s", github_url_to_use)

        # Retrieve repositories from GitHub
        github_result = get_github_repositories(github_url_to_use)

        if github_result["status"] == "success":
            result += "  βœ… GitHub list download successful\n\n"
            logger.info(
                "GitHub repositories retrieved successfully for %s",
                github_result['metadata']['username']
            )

        else:
            result += f"  ❌ GitHub extraction failed: {github_result['message']}\n\n"
            logger.error("GitHub extraction failed: %s", github_result['message'])
    else:
        result += "❌ No GitHub profile URL provided\n\n"
        logger.info("No GitHub URL provided")

    # Process job post text
    if job_post_text and job_post_text.strip():
        result += "βœ… Job post text provided\n"
        logger.info("Job post text provided (%d characters)", len(job_post_text))
        job_text_to_use = job_post_text.strip()
    else:
        result += "ℹ️ No job post provided, attempting to use default\n"
        logger.info("No job post text provided, trying default")

        # Try to load default job call
        default_job = load_default_job_call()
        if default_job:
            job_text_to_use = default_job
        else:
            result += "ℹ️ No default job post available, proceeding without job post\n"
            logger.info("No default job post available, proceeding without job analysis")
            job_text_to_use = None

    # Generate job summary (will use default if job_text_to_use is None)
    summary = None
    if job_text_to_use:
        summary = summarize_job_call(job_text_to_use)

        if summary:
            if job_post_text and job_post_text.strip():
                result += "  βœ… Job post summary generated\n"
            else:
                result += "βœ… Using default job post\n"
                result += "  βœ… Job post summary generated\n"
            logger.info("Job post summary generated (%d characters)", len(summary))
        else:
            result += "  ❌ Job post summary generation failed\n"
            logger.warning("Job post summary generation failed")
    else:
        result += "ℹ️ Proceeding without job post analysis\n"
        logger.info("No job post available for analysis")

    # Process user instructions
    if user_instructions and user_instructions.strip():
        result += "βœ… Additional instructions provided\n"
        logger.info("User instructions provided (%d characters)", len(user_instructions))

    else:
        result += "ℹ️ No additional instructions provided\n"
        logger.info("No additional instructions provided")

    logger.info("Input processing completed")

    # Generate resume only if we have valid extraction result
    if extraction_result and extraction_result.get("status") == "success":
        try:
            _ = write_resume(extraction_result, user_instructions, summary)
            result += "\nβœ… Resume generated successfully\n"
            logger.info("Resume generation completed successfully")

        except Exception as e:
            result += f"\n❌ Resume generation failed: {str(e)}\n"
            logger.error("Resume generation failed: %s", str(e))
    else:
        result += "\n❌ Cannot generate resume: No valid LinkedIn data extracted\n"
        result += "Please ensure you upload a valid LinkedIn PDF export file.\n"
        logger.warning("Resume generation skipped - no valid LinkedIn data available")

    return result


def get_processed_data(linkedin_pdf, github_url, job_post_text, instructions):
    """
    Get structured data from all inputs for further processing.

    Args:
        linkedin_pdf: Uploaded LinkedIn resume export PDF file
        github_url (str): GitHub profile URL
        job_post_text (str): Job post text content
        instructions (str): Additional instructions from the user

    Returns:
        dict: Structured data containing all processed information
    """

    job_post_text = job_post_text.strip() if job_post_text and job_post_text.strip() else None
    instructions = instructions.strip() if instructions and instructions.strip() else None

    # If no job post text provided, try to get default
    if not job_post_text:
        default_job = load_default_job_call()

        if default_job:
            job_post_text = default_job
        else:
            # No job post provided and no default available
            logger.info("No job post provided and no default available")
            job_post_text = None

    processed_data = {
        "linkedin": None,
        "github": None,
        "job_post": job_post_text,
        "user_instructions": instructions,
        "errors": []
    }

    # Process LinkedIn PDF
    if linkedin_pdf is not None:

        # Handle both file objects and mock file objects with path strings
        file_path = linkedin_pdf.name
        extraction_result = extract_text_from_linkedin_pdf(file_path)

        if extraction_result["status"] == "success":
            processed_data["linkedin"] = extraction_result

        else:
            processed_data["errors"].append(f"LinkedIn: {extraction_result['message']}")

    # Process GitHub profile
    if github_url and github_url.strip():
        github_result = get_github_repositories(github_url)

        if github_result["status"] == "success":
            processed_data["github"] = github_result

        else:
            processed_data["errors"].append(f"GitHub: {github_result['message']}")

    return processed_data


class MockFile:
    """Mock file object that mimics uploaded file interface with just a file path."""

    def __init__(self, path):
        self.name = path