Spaces:
Running
Running
| """ | |
| scheduler.py β APScheduler cron jobs | |
| All time-based triggers. Runs inside the HuggingFace Space process. | |
| """ | |
| import os | |
| from datetime import date | |
| from apscheduler.schedulers.background import BackgroundScheduler | |
| from slack_sdk import WebClient | |
| import sheets | |
| import handlers | |
| import prompts | |
| slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) | |
| CHANNEL = os.environ["SLACK_CHANNEL_ID"] | |
| def _post(text: str, channel: str = None) -> None: | |
| slack.chat_postMessage(channel=channel or CHANNEL, text=text) | |
| def _dm(slack_user_id: str, text: str) -> None: | |
| resp = slack.conversations_open(users=slack_user_id) | |
| dm_channel = resp["channel"]["id"] | |
| slack.chat_postMessage(channel=dm_channel, text=text) | |
| def _today() -> str: | |
| """Returns today's date in IST.""" | |
| from datetime import datetime, timezone, timedelta | |
| IST = timezone(timedelta(hours=5, minutes=30)) | |
| return datetime.now(IST).date().isoformat() | |
| def _get_current_day(candidate: dict) -> int: | |
| """Count working days (Mon-Sat) since start date. Sundays skipped.""" | |
| stage = candidate["stage"] | |
| if stage == "week0": | |
| start = candidate.get("week0_start_date", "") | |
| else: | |
| start = candidate.get("probation_start_date", "") | |
| if not start: | |
| return 1 | |
| start_date = date.fromisoformat(start) | |
| today = date.today() | |
| working_days = 0 | |
| current = start_date | |
| while current <= today: | |
| if current.weekday() != 6: # skip Sundays | |
| working_days += 1 | |
| current = current.replace(day=current.day + 1) if current.day < 28 else ( | |
| current.replace(month=current.month + 1, day=1) if current.month < 12 | |
| else current.replace(year=current.year + 1, month=1, day=1) | |
| ) | |
| return max(1, working_days) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOB 1 β MORNING FT BRIEF (9:00 AM daily) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Day-by-day FT focus messages β keyed by probation day number | |
| FT_DAILY_BRIEFS = { | |
| 1: ( | |
| "*Probation Day 1 β First contact with real work*\n\n" | |
| "Assign the problem today. Watch for:\n" | |
| "β’ Quality of clarifying questions β vague = shallow thinking\n" | |
| "β’ Whether they ask at all before diving in\n" | |
| "β’ End-of-day problem breakdown: does it exist? Does it have structure?\n\n" | |
| "_Do NOT suggest how to break the problem down. Wait and read._" | |
| ), | |
| 2: ( | |
| "*Probation Day 2 β Architecture before code*\n\n" | |
| "A proposed approach should arrive without you asking.\n" | |
| "Watch for:\n" | |
| "β’ Did it arrive proactively?\n" | |
| "β’ Are trade-offs visible β or did they pick the first thing that came to mind?\n" | |
| "β’ Did they name a failure case?\n\n" | |
| "_If no approach by EOD: send exactly one message β " | |
| "'What is your approach? Come back with it before you write code.'_" | |
| ), | |
| 3: ( | |
| "*Probation Day 3 β First real build, debugging posture*\n\n" | |
| "They are building now. Watch from a distance.\n" | |
| "Watch for:\n" | |
| "β’ Is the blocker in their report specific or vague?\n" | |
| "β’ Did they try anything before reporting it?\n" | |
| "β’ Are they building in the direction of the plan or drifting?\n\n" | |
| "_Do NOT check in. Wait and see what surfaces._" | |
| ), | |
| 4: ( | |
| "*Probation Day 4 β Depth check (your only scheduled touch point this week)*\n\n" | |
| "Run a 15-minute check-in today. Prepare three specific questions based on " | |
| "the architecture they submitted on Day 2.\n\n" | |
| "Ask each question, wait for a full answer, then push back on one.\n" | |
| "Log the quality of answers after.\n\n" | |
| "_Keep to 15 minutes. Do not fill silences they should be filling._" | |
| ), | |
| 5: ( | |
| "*Probation Day 5 β Week 1 checkpoint*\n\n" | |
| "The candidate should submit a checkpoint document by EOD:\n" | |
| "what was built, what works, what doesn't, decisions made, Week 2 plan.\n\n" | |
| "When it arrives:\n" | |
| "β’ Name one specific thing done well\n" | |
| "β’ Name one specific thing that must change in Week 2\n" | |
| "β’ Confirm or push back on the Week 2 plan\n\n" | |
| "_Complete the Week 1 evaluation β the agent will prompt you for signal answers._" | |
| ), | |
| 6: ( | |
| "*Probation Day 6 β Open Week 2 explicitly*\n\n" | |
| "Start with a brief, direct conversation today:\n" | |
| "β’ Name one thing they did well in Week 1 (observation, not praise)\n" | |
| "β’ Name the 1-2 things that need to change\n" | |
| "β’ Set deliverables with *specific dates* β not 'by end of week'\n" | |
| "β’ Get verbal confirmation of each deliverable\n\n" | |
| "_The agent will log commitments once you confirm them._" | |
| ), | |
| 7: ( | |
| "*Probation Day 7 β Building against commitments*\n\n" | |
| "Do NOT check in. The intern should surface updates proactively.\n\n" | |
| "Watch for:\n" | |
| "β’ Did an update arrive without you asking?\n" | |
| "β’ Is it specific enough to act on?\n" | |
| "β’ If there is risk β was it surfaced with a proposed solution?\n\n" | |
| "_If no update by mid-day: note it. Do not ask. Read the daily report._" | |
| ), | |
| 8: ( | |
| "*Probation Day 8 β Wednesday deliverable (first hard deadline)*\n\n" | |
| "The Wednesday component is due EOD today.\n\n" | |
| "If it arrives on time: acknowledge the specific thing done well. " | |
| "Ask one probe question about a design decision.\n\n" | |
| "If it is late but was flagged proactively before deadline: " | |
| "use the accountability structure.\n\n" | |
| "If it is late with no prior flag: the agent will issue a formal warning. " | |
| "Log the outcome immediately.\n\n" | |
| "_The agent will check at EOD and act accordingly._" | |
| ), | |
| 9: ( | |
| "*Probation Day 9 β Pushing through the hard part*\n\n" | |
| "Day 9 is typically where the work gets hardest.\n\n" | |
| "Watch for:\n" | |
| "β’ Is the intern working on the hard part or polishing what is already done?\n" | |
| "β’ Are blockers specific and come with attempted solutions?\n" | |
| "β’ If scope adjustment is proposed β is it reasoned or retreat?\n\n" | |
| "_If they are visibly avoiding the hard part: " | |
| "'The [component] is the harder piece. What is your current thinking on it?'_" | |
| ), | |
| 10: ( | |
| "*Probation Day 10 β Final delivery and review*\n\n" | |
| "Final component due today at the agreed time.\n" | |
| "Run the 20-minute final review:\n" | |
| "β’ 3-5 direct questions about the work\n" | |
| "β’ Ask where the system breaks\n" | |
| "β’ Ask about a trade-off they made\n\n" | |
| "Complete the final evaluation after the review.\n" | |
| "Communicate the decision to the intern *today* β do not leave it open.\n\n" | |
| "_The agent will generate the evaluation and send it to HR for confirmation._" | |
| ), | |
| } | |
| def _is_working_day() -> bool: | |
| """Returns False on Sundays in IST β no reports expected, no warnings fired.""" | |
| from datetime import datetime, timezone, timedelta | |
| IST = timezone(timedelta(hours=5, minutes=30)) | |
| return datetime.now(IST).weekday() != 6 # 6 = Sunday | |
| def job_morning_ft_brief() -> None: | |
| """Send FT the day's focus brief at 9:00 AM.""" | |
| if not _is_working_day(): return | |
| active = sheets.get_active_candidates() | |
| for candidate in active: | |
| stage = candidate["stage"] | |
| if stage not in ("probation_w1", "probation_w2"): | |
| continue | |
| day = _get_current_day(candidate) | |
| brief = FT_DAILY_BRIEFS.get(day) | |
| if not brief: | |
| continue | |
| ft_id = candidate.get("ft_slack_id", "") | |
| if ft_id: | |
| _dm(ft_id, f"*Re: {candidate['name']}*\n\n{brief}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOB 2 β MISSED REPORT CHECK (runs after each report window) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def job_missed_report_check() -> None: | |
| """Check for missed reports 30 min after each window closes.""" | |
| if not _is_working_day(): return | |
| handlers.handle_missed_report_check() | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOB 3 β EOD SIGNAL QUESTIONS TO FT (6:00 PM daily) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Signal questions keyed by day | |
| FT_SIGNAL_QUESTIONS = { | |
| 1: "Did {name} ask at least one specific clarifying question about the problem β scope, constraints, or success criteria? Reply: yes / no / partial", | |
| 2: "Did {name}'s architecture approach arrive without you asking? Did it name a failure case? Reply: yes / no / partial", | |
| 3: "When {name} hit a blocker today β was it reported with specific detail and prior attempts, or was it vague / passive? Reply: specific / vague / not reported", | |
| 4: "In today's depth check: could {name} explain their design decisions clearly and hold reasoning under pushback? Reply: yes / no / collapsed", | |
| 5: "Did {name} submit the Week 1 checkpoint on time, with real structure? Reply: yes / late / not submitted", | |
| 6: "How did {name} receive the raised bar today β seriously engaged or performed enthusiasm? Did they confirm deliverables with specific dates? Reply: engaged / vague / defensive", | |
| 7: "Did {name} surface a proactive update on their Wednesday deliverable without you asking? Reply: yes / no / only after prompted", | |
| 8: "Did {name}'s Wednesday deliverable arrive on time with a proper handover note? Reply: on_time / late_flagged / late_no_flag / not_delivered", | |
| 9: "Is {name} actively working on the harder Friday deliverable β or polishing already-completed work? Reply: pushing / avoiding / unclear", | |
| 10: "In the Day 10 final review: could {name} explain all major decisions and identify where the system fails? Reply: yes / partial / no", | |
| } | |
| def job_eod_signal_questions() -> None: | |
| """Send FT the signal question for today at 6:00 PM.""" | |
| if not _is_working_day(): return | |
| active = sheets.get_active_candidates() | |
| for candidate in active: | |
| stage = candidate["stage"] | |
| if stage not in ("probation_w1", "probation_w2"): | |
| continue | |
| day = _get_current_day(candidate) | |
| question_template = FT_SIGNAL_QUESTIONS.get(day) | |
| if not question_template: | |
| continue | |
| question = question_template.format(name=candidate["name"]) | |
| ft_id = candidate.get("ft_slack_id", "") | |
| if ft_id: | |
| _dm(ft_id, f"*End-of-day signal check β {candidate['name']} (Day {day})*\n\n{question}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOB 4 β DEADLINE CHECK (Wed EOD + Fri noon) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def job_deadline_check() -> None: | |
| """ | |
| Runs at Wednesday EOD (6:30 PM) and Friday noon (12:00 PM). | |
| Checks all W2 commitments due today. | |
| """ | |
| today = _today() | |
| due_today = sheets.get_due_commitments(today) | |
| for commitment in due_today: | |
| cid = commitment["candidate_id"] | |
| candidate = next( | |
| (c for c in sheets.get_active_candidates() if c["candidate_id"] == cid), | |
| None, | |
| ) | |
| if not candidate: | |
| continue | |
| # Check if candidate submitted deliverable (any report with matching day) | |
| day = _get_current_day(candidate) | |
| reports = sheets.get_reports_for_candidate(cid, "probation_w2") | |
| submitted_today = [ | |
| r for r in reports | |
| if r.get("submitted_at", "").startswith(today) | |
| and r.get("format_valid") == "True" | |
| ] | |
| if submitted_today: | |
| # Delivered β update commitment | |
| sheets.update_commitment_outcome( | |
| commitment_id=commitment["commitment_id"], | |
| delivered=True, | |
| delivered_at=submitted_today[-1]["submitted_at"], | |
| flagged_proactively=commitment.get("flagged_proactively", "") == "True", | |
| outcome="on_time", | |
| ) | |
| else: | |
| # Not delivered β check if they flagged proactively | |
| proactive = commitment.get("flagged_proactively", "") == "True" | |
| outcome = "late_flagged" if proactive else "late_no_flag" | |
| sheets.update_commitment_outcome( | |
| commitment_id=commitment["commitment_id"], | |
| delivered=False, | |
| delivered_at="", | |
| flagged_proactively=proactive, | |
| outcome=outcome, | |
| ) | |
| if not proactive: | |
| # Formal warning β no prior flag | |
| msg = ( | |
| f"<@{candidate['slack_user_id']}> The deliverable committed to for today " | |
| f"({commitment['description']}) has not arrived and was not flagged in advance.\n\n" | |
| f"This is a formal warning. Your FT and HR have been notified.\n\n" | |
| f"If you are still working on it, submit now with a handover note explaining " | |
| f"what is done and what is not." | |
| ) | |
| _post(msg) | |
| sheets.log_warning( | |
| candidate_id=cid, | |
| warning_type="formal_warning", | |
| stage="probation_w2", | |
| trigger_event=f"deadline_breach_{commitment['commitment_id']}", | |
| message_sent=msg, | |
| hr_notified=True, | |
| ) | |
| hr_id = candidate.get("hr_slack_id", "") | |
| if hr_id: | |
| _dm(hr_id, f"*Formal warning issued β {candidate['name']}*\n\nMissed deadline: {commitment['description']} due {today} with no prior flag.") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOB 5 β END-OF-STAGE EVALUATION (Day 5 + Day 10) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def job_end_of_stage_eval() -> None: | |
| """ | |
| Runs at EOD on Day 5 and Day 10 for candidates in probation. | |
| Also runs at EOD on Week 0 Day 5. | |
| Generates Claude evaluation and sends to HR for confirmation. | |
| """ | |
| active = sheets.get_active_candidates() | |
| for candidate in active: | |
| stage = candidate["stage"] | |
| day = _get_current_day(candidate) | |
| if stage == "week0" and day == 5: | |
| _run_week0_eval(candidate) | |
| elif stage == "probation_w1" and day == 5: | |
| _run_w1_eval(candidate) | |
| elif stage == "probation_w2" and day == 10: | |
| _run_w2_eval(candidate) | |
| def _run_week0_eval(candidate: dict) -> None: | |
| cid = candidate["candidate_id"] | |
| reports = sheets.get_reports_for_candidate(cid, "week0") | |
| warnings = sheets.get_warnings_for_candidate(cid) | |
| result = prompts.evaluate_week0(candidate, reports, warnings) | |
| eval_id = sheets.log_evaluation( | |
| candidate_id=cid, | |
| eval_type="week0", | |
| signals=result["signals"], | |
| summary_text=result.get("reasoning", ""), | |
| recommendation=result["recommendation"], | |
| recommendation_reasoning=result.get("reasoning", ""), | |
| ) | |
| hr_id = candidate.get("hr_slack_id", "") | |
| if hr_id: | |
| signal_lines = "\n".join( | |
| f"β’ {k}: *{v['value']}* β {v['evidence']}" | |
| for k, v in result["signals"].items() | |
| ) | |
| _dm( | |
| hr_id, | |
| f"*Week 0 Evaluation β {candidate['name']}*\n\n" | |
| f"{signal_lines}\n\n" | |
| f"*Recommendation:* {result['recommendation']}\n" | |
| f"*Reasoning:* {result['reasoning']}\n" | |
| f"*Watch in probation:* {result.get('gaps_to_watch', '')}\n\n" | |
| f"Reply `approve` to confirm or `reject` to override.\n" | |
| f"Eval ID: `{eval_id}`" | |
| ) | |
| def _run_w1_eval(candidate: dict) -> None: | |
| cid = candidate["candidate_id"] | |
| reports = sheets.get_reports_for_candidate(cid, "probation_w1") | |
| signals = sheets.get_signals_for_candidate(cid, week="w1") | |
| week0_eval = sheets.get_latest_evaluation(cid, "week0") or {} | |
| # Checkpoint is the Day 5 report with current_status field | |
| day5_reports = [r for r in reports if str(r.get("day_number")) == "5"] | |
| checkpoint = day5_reports[-1] if day5_reports else None | |
| ft_name = os.environ.get("FT_NAME", "FT") | |
| result = prompts.evaluate_probation_w1( | |
| candidate, ft_name, reports, signals, checkpoint, week0_eval | |
| ) | |
| eval_id = sheets.log_evaluation( | |
| candidate_id=cid, | |
| eval_type="probation_w1", | |
| signals=result["signals"], | |
| summary_text=result.get("reasoning", ""), | |
| recommendation=result["w1_path"], | |
| recommendation_reasoning=result.get("reasoning", ""), | |
| ) | |
| hr_id = candidate.get("hr_slack_id", "") | |
| if hr_id: | |
| signal_lines = "\n".join( | |
| f"β’ {k}: *{v['value']}* β {v['evidence']}" | |
| for k, v in result["signals"].items() | |
| ) | |
| _dm( | |
| hr_id, | |
| f"*Week 1 Evaluation β {candidate['name']}*\n\n" | |
| f"{signal_lines}\n\n" | |
| f"*W1 path:* {result['w1_path']}\n" | |
| f"*Reasoning:* {result['reasoning']}\n" | |
| f"*Watch in W2:* {result.get('w2_watch', '')}\n" | |
| f"*Bar raise for W2:* {result.get('w2_bar_raise', '')}\n\n" | |
| f"Reply `approve` to confirm or `reject` to override.\n" | |
| f"Eval ID: `{eval_id}`" | |
| ) | |
| def _run_w2_eval(candidate: dict) -> None: | |
| cid = candidate["candidate_id"] | |
| reports = sheets.get_reports_for_candidate(cid, "probation_w2") | |
| signals = sheets.get_signals_for_candidate(cid, week="w2") | |
| commitments = sheets.get_commitments_for_candidate(cid) | |
| warnings = sheets.get_warnings_for_candidate(cid) | |
| week0_eval = sheets.get_latest_evaluation(cid, "week0") or {} | |
| w1_eval = sheets.get_latest_evaluation(cid, "probation_w1") or {} | |
| # EOP document is the Day 10 report | |
| day10_reports = [r for r in reports if str(r.get("day_number")) == "10"] | |
| eop_doc = day10_reports[-1] if day10_reports else None | |
| ft_name = os.environ.get("FT_NAME", "FT") | |
| result = prompts.evaluate_probation_w2( | |
| candidate, ft_name, commitments, reports, signals, | |
| eop_doc, warnings, week0_eval, w1_eval | |
| ) | |
| eval_id = sheets.log_evaluation( | |
| candidate_id=cid, | |
| eval_type="probation_w2", | |
| signals=result["w2_signals"], | |
| summary_text=result.get("reasoning", ""), | |
| recommendation=result["outcome"], | |
| recommendation_reasoning=result.get("reasoning", ""), | |
| ) | |
| hr_id = candidate.get("hr_slack_id", "") | |
| if hr_id: | |
| signal_lines = "\n".join( | |
| f"β’ {k}: *{v['value']}* β {v['evidence']}" | |
| for k, v in result["w2_signals"].items() | |
| ) | |
| _dm( | |
| hr_id, | |
| f"*Final Probation Evaluation β {candidate['name']}*\n\n" | |
| f"{signal_lines}\n\n" | |
| f"*Outcome:* {result['outcome']}\n" | |
| f"*Reasoning:* {result['reasoning']}\n\n" | |
| f"*HR talking points:*\n{result.get('hr_talking_points', '')}\n\n" | |
| f"Reply `approve` to confirm or `reject` to override.\n" | |
| f"Eval ID: `{eval_id}`" | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOB β MORNING AGENDA PROMPT (9:00 AM) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def job_morning_agenda_prompt(force: bool = False) -> None: | |
| """ | |
| At 11:00 AM β bot tags each intern in their channel asking for today's agenda. | |
| Interns reply in thread. | |
| """ | |
| if not force and not _is_working_day(): | |
| print("[agenda] Skipping β not a working day") | |
| return | |
| from datetime import datetime, timezone, timedelta | |
| IST = timezone(timedelta(hours=5, minutes=30)) | |
| today_str = datetime.now(IST).strftime("%d %b %Y") | |
| active = sheets.get_active_candidates() | |
| print(f"[agenda] Active candidates: {len(active)}") | |
| # Group by channel | |
| channels = {} | |
| for candidate in active: | |
| if candidate.get("status") in ("eliminated", "cleared"): | |
| continue | |
| ch = candidate.get("channel_id", "").strip() | |
| if not ch: | |
| print(f"[agenda] {candidate.get('name')} has no channel_id β skipping") | |
| continue | |
| channels.setdefault(ch, []).append(candidate) | |
| print(f"[agenda] Channels to post in: {list(channels.keys())}") | |
| for channel_id, candidates in channels.items(): | |
| print(f"[agenda] Posting in {channel_id} for {[c['name'] for c in candidates]}") | |
| tags = " ".join(f"<@{c['slack_user_id']}>" for c in candidates) | |
| msg = "\n".join([ | |
| "*Good morning!* β " + today_str, | |
| "", | |
| tags, | |
| "", | |
| "Share your agenda for today β reply in this thread:", | |
| "", | |
| "```", | |
| "AGENDA β [Your Name] β " + today_str, | |
| "", | |
| "TODAY'S FOCUS", | |
| "- [Most important thing you will complete today]", | |
| "", | |
| "PROBLEMS I ANTICIPATE", | |
| "- [Any blockers or challenges you expect today / None]", | |
| "```", | |
| ]) | |
| try: | |
| _post(msg, channel_id) | |
| print(f"[agenda] Posted morning prompt in {channel_id} for {len(candidates)} interns") | |
| except Exception as e: | |
| print(f"[agenda] Failed to post in {channel_id}: {e}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # SCHEDULER SETUP | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def start_scheduler() -> BackgroundScheduler: | |
| scheduler = BackgroundScheduler(timezone="Asia/Kolkata") | |
| # Morning agenda prompt to interns β 11:00 AM IST | |
| scheduler.add_job(job_morning_agenda_prompt, "cron", hour=11, minute=0) | |
| # Morning FT brief β 9:05 AM IST (staggered to avoid rate limits) | |
| scheduler.add_job(job_morning_ft_brief, "cron", hour=9, minute=5) | |
| # Missed report check β 11:30 PM IST, once per day | |
| scheduler.add_job(job_missed_report_check, "cron", hour=23, minute=30) | |
| # EOD signal questions to FT β 6:00 PM IST | |
| scheduler.add_job(job_eod_signal_questions, "cron", hour=18, minute=0) | |
| # Deadline checks β Wed EOD (18:30) and Fri noon (12:00) | |
| scheduler.add_job(job_deadline_check, "cron", day_of_week="wed", hour=18, minute=30) | |
| scheduler.add_job(job_deadline_check, "cron", day_of_week="fri", hour=12, minute=0) | |
| # End-of-stage evaluations β 8:00 PM IST (after EOD reports) | |
| scheduler.add_job(job_end_of_stage_eval, "cron", hour=20, minute=0) | |
| scheduler.start() | |
| return scheduler |