interns_manager / scheduler.py
banao-tech's picture
Update scheduler.py
f90b4c0 verified
"""
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