File size: 19,218 Bytes
504df0f
 
2ae57cb
504df0f
 
 
9d4b413
 
 
504df0f
2ae57cb
 
504df0f
2ae57cb
504df0f
2ae57cb
9d4b413
2ae57cb
504df0f
 
49fbb3a
 
 
 
 
 
 
 
2ae57cb
 
504df0f
3c4bd31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504df0f
2ae57cb
504df0f
 
2ae57cb
 
504df0f
2ae57cb
 
 
504df0f
 
2ae57cb
 
 
 
504df0f
1e8e58c
504df0f
2ae57cb
 
3c4bd31
 
 
 
 
 
504df0f
 
 
2ae57cb
 
 
 
 
504df0f
 
 
 
49fbb3a
 
 
504df0f
49fbb3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f99f0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981366b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f99f0f
 
 
 
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
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import json

db = SQLAlchemy()

# ✅ Import User model here, before Job and Application use it
from backend.models.user import User

class Job(db.Model):
    __tablename__ = 'jobs'

    id = db.Column(db.Integer, primary_key=True)
    role = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text, nullable=False)
    seniority = db.Column(db.String(50), nullable=False)
    skills = db.Column(db.Text, nullable=False)
    company = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, default=datetime.utcnow)

    # Number of interview questions for this job. Recruiters can set this
    # value when posting a job. Defaults to 3 to preserve existing
    # behaviour where the interview consists of three questions. The
    # interview API uses this field to determine when to stop asking
    # follow‑up questions. See backend/routes/interview_api.py for
    # details.
    num_questions = db.Column(db.Integer, nullable=False, default=3)

    recruiter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
    recruiter = db.relationship('User', backref='posted_jobs')

    @property
    def skills_list(self):
        """Return a list of skills parsed from the JSON string stored in ``skills``.

        The ``skills`` column stores a JSON encoded list of skills (e.g. '["Python", "Flask"]').
        In templates it is convenient to work with a Python list so that skills can be joined
        or iterated over.  If parsing fails for any reason an empty list is returned.
        """
        try:
            # Import json lazily to avoid circular imports at module import time.
            import json as _json
            return _json.loads(self.skills) if self.skills else []
        except Exception:
            return []

    def __repr__(self):
        return f"<Job {self.role} at {self.company}>"

class Application(db.Model):
    __tablename__ = 'applications'

    id = db.Column(db.Integer, primary_key=True)
    job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), nullable=False)
    resume_path = db.Column(db.String(255), nullable=True)
    cover_letter = db.Column(db.Text, nullable=True)
    extracted_features = db.Column(db.Text, nullable=True)
    status = db.Column(db.String(50), default='applied')
    date_applied = db.Column(db.DateTime, default=datetime.utcnow)
    interview_log = db.Column(db.Text)

    user = db.relationship('User', backref='applications')

    # Set up a relationship back to the Job so that templates can access
    # ``application.job`` directly.  Without this relationship you'd need to
    # query the Job model manually in the route or template, which is less
    # convenient and can lead to additional database queries.
    job = db.relationship('Job', backref='applications', lazy='joined')

    def __repr__(self):
        return f"Application('{self.name}', '{self.email}', Job ID: {self.job_id})"

    def get_profile_data(self):
        try:
            return json.loads(self.extracted_features) if self.extracted_features else {}
        except:
            return {}

def init_db(app):
    db.init_app(app)
    with app.app_context():
        # Create any missing tables.  SQLAlchemy does not automatically add
        # columns to existing tables, so we call create_all() first to ensure
        # new tables (like ``applications`` or ``jobs``) are present.
        db.create_all()

        # Dynamically add the ``num_questions`` column to the ``jobs`` table
        # if it is missing.  When deploying an updated application against an
        # existing database, the new field will not appear until we run an
        # explicit ALTER TABLE.  Inspect the current table schema and add
        # ``num_questions`` with a default of 3 if it doesn't exist.  This
        # logic is idempotent: the ALTER TABLE statement runs only when
        # necessary.
        from sqlalchemy import inspect
        inspector = inspect(db.engine)
        try:
            columns = [col['name'] for col in inspector.get_columns('jobs')]
            if 'num_questions' not in columns:
                # SQLite supports adding new columns via ALTER TABLE.  The
                # default value of 3 matches the default declared in the
                # Job model.  If you are using a different database backend,
                # verify that this syntax is supported.
                db.session.execute('ALTER TABLE jobs ADD COLUMN num_questions INTEGER NOT NULL DEFAULT 3')
                db.session.commit()
        except Exception:
            # If inspection fails (e.g. the table does not exist yet), rely on
            # SQLAlchemy's create_all() to create a fresh schema with the
            # ``num_questions`` column.
            pass

        # Seed professional demo data (recruiter + applicant + a curated set of
        # jobs) so the platform always looks polished and the full demo flow
        # works after every restart.  On Hugging Face the SQLite DB lives in
        # the ephemeral /tmp directory and is wiped on each restart, so this
        # idempotent seeding repopulates a clean, consistent dataset every time.
        seed_demo_data()


# Known demo accounts used for the marketing demo.  Passwords are intentionally
# simple here because these are throwaway demo accounts on an ephemeral DB.
DEMO_RECRUITER_EMAIL = 'hr@codingo.ai'
DEMO_APPLICANT_EMAIL = 'candidate@codingo.ai'
DEMO_PASSWORD = 'codingo123'


def seed_demo_data():
    """Idempotently seed a demo recruiter, a demo applicant and a curated set
    of professional job listings.

    Safe to call on every startup: each entity is only created when missing,
    and jobs are only inserted when the ``jobs`` table is empty.  Any failure
    is rolled back and swallowed so seeding can never block app startup.
    """
    try:
        # --- Demo recruiter (owns the seeded jobs so they appear in the HR
        #     dashboard, which filters by recruiter_id) ---
        recruiter = User.query.filter_by(email=DEMO_RECRUITER_EMAIL).first()
        if recruiter is None:
            recruiter = User(
                username='Codingo HR',
                email=DEMO_RECRUITER_EMAIL,
                role='recruiter',
            )
            recruiter.set_password(DEMO_PASSWORD)
            db.session.add(recruiter)
            db.session.commit()

        # --- Demo applicant (job-seeker role is 'unemployed') ---
        applicant = User.query.filter_by(email=DEMO_APPLICANT_EMAIL).first()
        if applicant is None:
            applicant = User(
                username='Demo Candidate',
                email=DEMO_APPLICANT_EMAIL,
                role='unemployed',
            )
            applicant.set_password(DEMO_PASSWORD)
            db.session.add(applicant)
            db.session.commit()

        # --- Jobs: only seed when there are none, to avoid duplicates and to
        #     never clobber jobs a recruiter may have posted at runtime ---
        if Job.query.count() == 0:
            demo_jobs = [
                {
                    'role': 'Data Scientist',
                    'seniority': 'Mid-level',
                    'company': 'NorthBridge Analytics',
                    'skills': ['Python', 'SQL', 'Pandas', 'scikit-learn',
                               'Machine Learning', 'Statistics', 'Data Visualization'],
                    'description': (
                        "We are looking for a Data Scientist to turn raw data into "
                        "actionable insight. You will design and evaluate machine "
                        "learning models, run statistical analyses, and build clear "
                        "visualizations that guide product and business decisions. "
                        "You will collaborate closely with engineering and product "
                        "teams to take models from prototype to production."
                    ),
                },
                {
                    'role': 'Data Engineer',
                    'seniority': 'Senior',
                    'company': 'Cloudbyte Systems',
                    'skills': ['Python', 'SQL', 'Apache Spark', 'Airflow',
                               'ETL', 'AWS', 'Data Warehousing'],
                    'description': (
                        "Join us as a Senior Data Engineer to design, build and "
                        "maintain the data pipelines that power our analytics and "
                        "machine learning platforms. You will own scalable ETL "
                        "workflows, optimize our data warehouse, and ensure data "
                        "quality and reliability across the organization."
                    ),
                },
                {
                    'role': 'Machine Learning Engineer',
                    'seniority': 'Mid-level',
                    'company': 'Vantix AI',
                    'skills': ['Python', 'TensorFlow', 'PyTorch', 'Machine Learning',
                               'MLOps', 'Model Deployment', 'Docker'],
                    'description': (
                        "As a Machine Learning Engineer you will take models from "
                        "research into reliable, production-grade services. You will "
                        "train and fine-tune models, build robust deployment and "
                        "monitoring pipelines, and work with data scientists to "
                        "ship features that delight our users."
                    ),
                },
                {
                    'role': 'NLP Engineer',
                    'seniority': 'Senior',
                    'company': 'Lingua Labs',
                    'skills': ['Python', 'NLP', 'Transformers', 'spaCy',
                               'PyTorch', 'LLMs', 'Hugging Face'],
                    'description': (
                        "We are hiring a Senior NLP Engineer to build state-of-the-art "
                        "language understanding systems. You will develop models for "
                        "text classification, information extraction and conversational "
                        "AI, fine-tune large language models, and deploy them at scale "
                        "to power our products."
                    ),
                },
                {
                    'role': 'Computer Vision Engineer',
                    'seniority': 'Mid-level',
                    'company': 'VisionWorks AI',
                    'skills': ['Python', 'OpenCV', 'Deep Learning', 'PyTorch',
                               'Image Processing', 'CNNs', 'Model Optimization'],
                    'description': (
                        "Join our team as a Computer Vision Engineer to develop "
                        "image and video understanding systems. You will design and "
                        "train deep learning models for detection, segmentation and "
                        "recognition, optimize them for real-time performance, and "
                        "integrate them into our production pipeline."
                    ),
                },
            ]

            for spec in demo_jobs:
                db.session.add(Job(
                    role=spec['role'],
                    description=spec['description'],
                    seniority=spec['seniority'],
                    skills=json.dumps(spec['skills']),
                    company=spec['company'],
                    recruiter_id=recruiter.id,
                    num_questions=4,
                ))
            db.session.commit()

        # --- Pre-seed completed demo interviews for the demo candidate so the
        #     HR dashboard is always populated even after a restart (the SQLite
        #     DB is ephemeral on Hugging Face).  Idempotent per (candidate, job):
        #     a row is only created when one does not already exist. ---
        demo_interviews = [
            {
                'role': 'Data Scientist',
                'skills': ['Python', 'SQL', 'Pandas', 'scikit-learn',
                           'Machine Learning', 'Data Visualization'],
                'experience': [
                    'Data Analyst at BrightData (2 years) — built dashboards and predictive models',
                    'Machine Learning Intern at NorthBridge Analytics — customer churn prediction',
                ],
                'education': ['BSc in Computer Science, USAL'],
                'qa_log': [
                    {
                        'question': "Hi, I'm LUNA, your AI recruiter. Can you tell me about your background and what drew you to data science?",
                        'answer': "I have around three years working with data. I started as a data analyst building dashboards, then moved into machine learning where I built churn and forecasting models in Python with scikit-learn. I love data science because it turns messy data into decisions that actually move the business.",
                        'evaluation': {'score': 'Excellent', 'feedback': 'Clear, specific background with concrete tools and real impact on the business.'},
                    },
                    {
                        'question': "How do you prevent a model from overfitting?",
                        'answer': "I use cross-validation to get an honest estimate of performance, keep the model as simple as the data allows, and apply regularization like L1 or L2. I also use more training data when possible, early stopping for neural nets, and I always compare training versus validation error to catch overfitting early.",
                        'evaluation': {'score': 'Excellent', 'feedback': 'Covers cross-validation, regularization, and monitoring train/validation gap accurately.'},
                    },
                    {
                        'question': "Walk me through a data science project you are proud of and the impact it had.",
                        'answer': "At NorthBridge I built a customer churn model. I engineered features from usage logs, trained a gradient boosting model, and reached about 0.86 ROC-AUC. The retention team used the risk scores to target outreach and we reduced monthly churn by roughly 12 percent.",
                        'evaluation': {'score': 'Good', 'feedback': 'Solid end-to-end project with a measurable result; could add more on validation and deployment.'},
                    },
                    {
                        'question': "What are your salary expectations? Are you looking for a full-time or part-time role, and do you prefer remote or on-site work?",
                        'answer': "I'm looking for a full-time role and I'm flexible on salary within the market range for a mid-level data scientist. I'm happy with hybrid or on-site, and comfortable fully remote as well.",
                        'evaluation': {'score': 'Excellent', 'feedback': 'Clear, reasonable and flexible answer on logistics.'},
                    },
                ],
            },
            {
                'role': 'Data Engineer',
                'skills': ['Python', 'SQL', 'Apache Spark', 'Airflow', 'ETL', 'AWS'],
                'experience': [
                    'Data Engineer at Cloudbyte Systems (3 years) — built batch and streaming pipelines',
                    'Backend Developer — automated ETL jobs and data quality checks',
                ],
                'education': ['BSc in Software Engineering, USAL'],
                'qa_log': [
                    {
                        'question': "Hi, I'm LUNA, your AI recruiter. Tell me about your experience and why data engineering?",
                        'answer': "I've spent three years as a data engineer building pipelines in Python and Spark, orchestrated with Airflow on AWS. I enjoy data engineering because reliable, well-modeled data is what makes everything else — analytics and machine learning — actually work.",
                        'evaluation': {'score': 'Excellent', 'feedback': 'Strong, specific experience with the exact stack the role needs.'},
                    },
                    {
                        'question': "How do you design a reliable ETL pipeline?",
                        'answer': "I make each step idempotent so re-runs are safe, add schema validation and data quality checks early, and design for incremental loads instead of full reloads. I orchestrate with Airflow, add retries and alerting on failures, and keep raw data so I can reprocess if logic changes.",
                        'evaluation': {'score': 'Excellent', 'feedback': 'Idempotency, validation, incremental loads and observability — a complete, senior answer.'},
                    },
                    {
                        'question': "Describe a time you optimized a slow data pipeline.",
                        'answer': "A nightly Spark job was taking over four hours. I found it was shuffling too much data, so I repartitioned on the join key, cached a reused dataframe, and switched some wide transformations to broadcast joins. It dropped to about forty minutes.",
                        'evaluation': {'score': 'Good', 'feedback': 'Concrete optimization with a real result; could mention profiling and cost trade-offs.'},
                    },
                    {
                        'question': "What are your salary expectations? Are you looking for a full-time or part-time role, and do you prefer remote or on-site work?",
                        'answer': "Full-time, and I'm open on compensation within the senior data engineer range. I prefer hybrid but I'm fully comfortable working remotely.",
                        'evaluation': {'score': 'Excellent', 'feedback': 'Direct and flexible on role type, pay and location.'},
                    },
                ],
            },
        ]

        for spec in demo_interviews:
            job = Job.query.filter_by(role=spec['role'], recruiter_id=recruiter.id).first()
            if job is None:
                continue
            existing = Application.query.filter_by(user_id=applicant.id, job_id=job.id).first()
            if existing is not None:
                continue
            db.session.add(Application(
                job_id=job.id,
                user_id=applicant.id,
                name=applicant.username,
                email=applicant.email,
                status='interviewed',
                extracted_features=json.dumps({
                    'skills': spec['skills'],
                    'experience': spec['experience'],
                    'education': spec['education'],
                }),
                interview_log=json.dumps(spec['qa_log'], ensure_ascii=False),
            ))
        db.session.commit()
    except Exception as exc:
        # Never let seeding break startup.
        db.session.rollback()
        print(f"Demo data seeding skipped due to error: {exc}")