File size: 14,264 Bytes
ba72f62
10d2e39
ba72f62
10d2e39
ba72f62
 
 
 
e3fc741
 
9a7d4db
 
10d2e39
 
d8529bc
d8acd61
504df0f
 
 
 
d8acd61
504df0f
 
 
5d095b2
 
 
2445440
af02e64
 
 
 
 
 
5d095b2
ce04e48
 
 
d8529bc
 
fb236cf
d8529bc
f1fc29e
 
 
 
 
 
763e3e7
d8acd61
671ea59
 
68d1258
 
 
29cfacc
671ea59
10d2e39
27994de
e3fc741
3c4bd31
 
 
 
 
 
da815dd
2af664a
d8acd61
22b00f2
e3fc741
 
 
d8acd61
5d095b2
d8acd61
 
 
 
 
 
 
 
 
 
 
5d095b2
d8acd61
5d095b2
 
29cfacc
504df0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8acd61
504df0f
 
2ae57cb
 
2445440
 
 
 
2ae57cb
2445440
27994de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504df0f
 
2ae57cb
 
 
2445440
27994de
504df0f
2445440
504df0f
 
2445440
504df0f
 
2445440
2ae57cb
 
 
 
 
22b00f2
 
 
2ae57cb
 
d8529bc
 
 
29cfacc
d8529bc
29cfacc
 
 
 
 
 
f1fc29e
 
d8529bc
29cfacc
d8529bc
29cfacc
 
d8529bc
504df0f
 
af02e64
 
 
 
 
 
 
 
 
 
 
d8acd61
af02e64
 
2445440
af02e64
 
 
 
 
 
 
2445440
af02e64
 
 
 
 
 
 
 
 
2445440
af02e64
d8acd61
af02e64
 
 
 
d8acd61
af02e64
504df0f
2ae57cb
 
 
 
22b00f2
 
 
 
 
2ae57cb
 
 
22b00f2
2ae57cb
 
504df0f
ce04e48
 
 
 
 
 
 
 
 
 
 
 
 
49fbb3a
 
 
 
 
ce04e48
 
 
 
 
 
 
 
 
 
 
 
49fbb3a
 
 
cb47676
49fbb3a
 
 
 
 
 
 
 
ce04e48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49fbb3a
 
ce04e48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29cfacc
ce04e48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504df0f
 
29cfacc
 
 
 
 
 
 
 
 
 
 
 
d8acd61
 
fb236cf
29cfacc
 
f1fc29e
 
 
 
 
 
 
29cfacc
f1fc29e
 
 
22b00f2
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
import os
import sys

# Hugging Face safe cache
os.environ["HF_HOME"] = "/tmp/huggingface"
os.environ["TRANSFORMERS_CACHE"] = "/tmp/huggingface/transformers"
os.environ["HUGGINGFACE_HUB_CACHE"] = "/tmp/huggingface/hub"

# Force Flask instance path to a writable temporary folder
safe_instance_path = "/tmp/flask_instance"

# Create the safe instance path after imports
os.makedirs(safe_instance_path, exist_ok=True)

from flask import Flask, render_template, redirect, url_for, flash, request, jsonify
from flask_login import LoginManager, login_required, current_user
from werkzeug.utils import secure_filename
import sys
from datetime import datetime

# Adjust sys.path for import flexibility
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)

# Import and initialize DB
from backend.models.database import db, Job, Application, init_db
from backend.models.user import User
from backend.routes.auth import auth_bp, handle_resume_upload

# Import the resume parsing helper.  This module contains lightweight
# heuristics for extracting information from PDF and DOCX files without
# relying on heavy external libraries.  See
# ``codingo/backend/services/resume_parser.py`` for details.
from backend.services.resume_parser import parse_resume as _parse_resume_helper
from backend.routes.interview_api import interview_api
# Import additional utilities
import re
import json

# -----------------------------------------------------------------------------
# Chatbot integration
#
# Import the chatbot module functions
from backend.services.codingo_chatbot import (
    get_response as _codingo_get_response,
    init_embedder_and_db,
    init_llm
)

# Initialize Flask app
app = Flask(
    __name__,
    static_folder='backend/static',
    static_url_path='/static',
    template_folder='backend/templates',
    instance_path=safe_instance_path
)

app.config['SECRET_KEY'] = 'saadi'

# Cookie configuration for Hugging Face Spaces
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None'
app.config['REMEMBER_COOKIE_SECURE'] = True

# Configure the database connection
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Create necessary directories in writable locations
os.makedirs('/tmp/static/audio', exist_ok=True)
os.makedirs('/tmp/temp', exist_ok=True)

# Initialize DB with app
init_db(app)

# Flask-Login setup
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)

@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))

# Register blueprints
app.register_blueprint(auth_bp)
app.register_blueprint(interview_api, url_prefix="/api")

# Routes
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/jobs')
def jobs():
    all_jobs = Job.query.order_by(Job.date_posted.desc()).all()
    return render_template('jobs.html', jobs=all_jobs)

@app.route('/job/<int:job_id>')
def job_detail(job_id):
    job = Job.query.get_or_404(job_id)
    return render_template('job_detail.html', job=job)

@app.route('/apply/<int:job_id>', methods=['GET', 'POST'])
@login_required
def apply(job_id):
    job = Job.query.get_or_404(job_id)
    if request.method == 'POST':
        file = request.files.get('resume')
        features, error, filepath = handle_resume_upload(file)

        if error:
            flash("Resume upload failed. Please try again.", "danger")
            return render_template('apply.html', job=job)

        def parse_entries(raw_value: str):
            import re
            entries = []
            if raw_value:
                for item in re.split(r'[\n,;]+', raw_value):
                    item = item.strip()
                    if item:
                        entries.append(item)
            return entries

        skills_input = request.form.get('skills', '')
        experience_input = request.form.get('experience', '')
        education_input = request.form.get('education', '')

        manual_features = {
            "skills": parse_entries(skills_input),
            "experience": parse_entries(experience_input),
            "education": parse_entries(education_input)
        }

        application = Application(
            job_id=job_id,
            user_id=current_user.id,
            name=current_user.username,
            email=current_user.email,
            resume_path=filepath,
            extracted_features=json.dumps(manual_features)
        )

        db.session.add(application)
        db.session.commit()

        flash('Your application has been submitted successfully!', 'success')
        return redirect(url_for('jobs'))

    return render_template('apply.html', job=job)

@app.route('/my_applications')
@login_required
def my_applications():
    applications = Application.query.filter_by(
        user_id=current_user.id
    ).order_by(Application.date_applied.desc()).all()
    return render_template('my_applications.html', applications=applications)

# Chatbot API endpoint
@app.route('/chatbot', methods=['POST'])
def chatbot_endpoint():
    """Handle chatbot queries from the frontend."""
    try:
        data = request.get_json(silent=True) or {}
        user_input = str(data.get('message', '')).strip()
        
        if not user_input:
            return jsonify({"error": "Empty message"}), 400
        
        # Use the imported function from codingo_chatbot module
        reply = _codingo_get_response(user_input)
        return jsonify({"response": reply})
        
    except Exception as exc:
        print(f"Chatbot endpoint error: {exc}", file=sys.stderr)
        return jsonify({"error": "I'm having trouble right now. Please try again."}), 500

@app.route('/parse_resume', methods=['POST'])
def parse_resume():
    """
    Parse an uploaded resume (PDF or DOCX) and return extracted
    information in JSON format.

    This endpoint is separate from the main application flow.  It saves
    the uploaded file to a temporary location (via ``handle_resume_upload``)
    so that recruiters can review the original document later, then
    invokes a lightweight parser to extract the candidate's name,
    skills, education and experience.  Errors during upload or
    parsing are reported back to the client.
    """
    file = request.files.get('resume')
    if not file or file.filename == '':
        return jsonify({"error": "No file uploaded"}), 400

    # Save the file using the existing helper.  We ignore the
    # ``features`` return value because ``handle_resume_upload`` no
    # longer parses resumes itself; it simply stores the file and
    # returns the path on disk.
    features, error, filepath = handle_resume_upload(file)
    if error or not filepath:
        return jsonify({"error": "Error processing resume. Please try again."}), 400

    try:
        # Parse the stored file.  Pass both the path and the original
        # filename so that the parser can fall back to the filename
        # when inferring the candidate's name.
        parsed = _parse_resume_helper(filepath, file.filename)
    except Exception as exc:
        # Log to stderr for debugging
        print(f"Resume parsing error: {exc}", file=sys.stderr)
        return jsonify({"error": "Failed to parse resume"}), 500

    # Normalise the response to ensure string values for the form
    response = {
        'name': parsed.get('name', ''),
        'skills': parsed.get('skills', ''),
        'education': parsed.get('education', ''),
        'experience': parsed.get('experience', '')
    }
    return jsonify(response), 200

@app.route("/interview/<int:job_id>")
@login_required
def interview_page(job_id):
    job = Job.query.get_or_404(job_id)
    application = Application.query.filter_by(
        user_id=current_user.id, 
        job_id=job_id
    ).first()
    
    if not application or not application.extracted_features:
        flash("Please apply for this job and upload your resume first.", "warning")
        return redirect(url_for('job_detail', job_id=job_id))
    
    cv_data = json.loads(application.extracted_features)
    return render_template("interview.html", job=job, cv=cv_data)

@app.route('/post_job', methods=['GET', 'POST'])
@login_required
def post_job():
    if current_user.role not in ('recruiter', 'admin'):
        flash('You do not have permission to post jobs.', 'warning')
        return redirect(url_for('jobs'))

    if request.method == 'POST':
        role_title = request.form.get('role', '').strip()
        description = request.form.get('description', '').strip()
        seniority = request.form.get('seniority', '').strip()
        skills_input = request.form.get('skills', '').strip()
        company = request.form.get('company', '').strip()
        # New field: number of interview questions.  Recruiters can specify
        # how many questions the interview should contain.  Default to 3 if
        # the value is missing or invalid.  See templates/post_job.html for
        # the corresponding input element.
        num_questions_raw = request.form.get('num_questions', '').strip()

        errors = []
        if not role_title:
            errors.append('Job title is required.')
        if not description:
            errors.append('Job description is required.')
        if not seniority:
            errors.append('Seniority level is required.')
        if not skills_input:
            errors.append('Skills are required.')
        if not company:
            errors.append('Company name is required.')
        # Validate number of questions; must be a positive integer.  Store
        # errors if the input is provided but invalid.  Missing values will
        # fall back to the default of 3.
        num_questions = 4
        if num_questions_raw:
            try:
                parsed_nq = int(num_questions_raw)
                if parsed_nq <= 0:
                    raise ValueError()
                num_questions = parsed_nq
            except ValueError:
                errors.append('Number of interview questions must be a positive integer.')

        if errors:
            for err in errors:
                flash(err, 'danger')
            return render_template('post_job.html')

        skills_list = [s.strip() for s in re.split(r'[\n,;]+', skills_input) if s.strip()]
        skills_json = json.dumps(skills_list)

        new_job = Job(
            role=role_title,
            description=description,
            seniority=seniority,
            skills=skills_json,
            company=company,
            recruiter_id=current_user.id,
            num_questions=num_questions
        )
        db.session.add(new_job)
        db.session.commit()

        flash('Job posted successfully!', 'success')
        return redirect(url_for('jobs'))

    return render_template('post_job.html')

@app.route('/dashboard')
@login_required
def dashboard():
    if current_user.role not in ('recruiter', 'admin'):
        flash('You do not have permission to access the dashboard.', 'warning')
        return redirect(url_for('index'))

    posted_jobs = Job.query.filter_by(recruiter_id=current_user.id).all()
    job_ids = [job.id for job in posted_jobs]

    candidates_with_scores = []
    if job_ids:
        candidate_apps = Application.query.filter(Application.job_id.in_(job_ids)).all()

        def compute_score(application):
            try:
                candidate_features = json.loads(application.extracted_features) if application.extracted_features else {}
                candidate_skills = candidate_features.get('skills', [])
                job_skills = json.loads(application.job.skills) if application.job and application.job.skills else []
                if not job_skills:
                    return ('Medium', 2)

                candidate_set = {s.lower() for s in candidate_skills}
                job_set = {s.lower() for s in job_skills}
                common = candidate_set & job_set
                ratio = len(common) / len(job_set) if job_set else 0

                if ratio >= 0.75:
                    return ('Excellent', 4)
                elif ratio >= 0.5:
                    return ('Good', 3)
                elif ratio >= 0.25:
                    return ('Medium', 2)
                else:
                    return ('Poor', 1)
            except Exception:
                return ('Medium', 2)

        for app_record in candidate_apps:
            score_label, score_value = compute_score(app_record)
            candidates_with_scores.append({
                'application': app_record,
                'score_label': score_label,
                'score_value': score_value
            })

        candidates_with_scores.sort(key=lambda item: item['score_value'], reverse=True)

    return render_template('dashboard.html', candidates=candidates_with_scores)

if __name__ == '__main__':
    print("Starting Codingo application...")
    
    # Import torch to check GPU availability
    try:
        import torch
        if torch.cuda.is_available():
            print(f"GPU Available: {torch.cuda.get_device_name(0)}")
            print(f"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")
        else:
            print("No GPU available, using CPU")
    except ImportError:
        print("PyTorch not installed, chatbot will use CPU")
    
    with app.app_context():
        db.create_all()
        # Pre-initialize chatbot on startup for faster first response
        print("Initializing chatbot...")
        try:
            # Initialize the embedder and database
            init_embedder_and_db()
            print("Embedder and database initialized")
            
            # Initialize the LLM (this will download the model if needed)
            init_llm()
            print("LLM initialized successfully")
        except Exception as e:
            print(f"Chatbot initialization error: {e}")
            import traceback
            traceback.print_exc()
    
    # Use port from environment or default to 7860
    port = int(os.environ.get('PORT', 7860))
    app.run(debug=True, host='0.0.0.0', port=port)