Spaces:
Running
Running
| """ | |
| app.py β HuggingFace Spaces entry point | |
| Pure FastAPI + uvicorn. No Gradio. | |
| Slack webhook at /slack/events. | |
| Admin endpoints at /admin/*. | |
| """ | |
| import os | |
| import hmac | |
| import hashlib | |
| import time | |
| import threading | |
| import json | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| import router | |
| import scheduler as sched | |
| import sheets | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # LIFESPAN | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| async def lifespan(app: FastAPI): | |
| import asyncio | |
| async def delayed_setup(): | |
| await asyncio.sleep(5) | |
| try: | |
| sheets.setup_sheets() | |
| except Exception as e: | |
| print(f"setup_sheets warning: {e}") | |
| asyncio.create_task(delayed_setup()) | |
| sched.start_scheduler() | |
| yield | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # APP | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI( | |
| title="Intern Management Agent", | |
| description="Slack agent managing intern pipeline β Week 0 through Probation.", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # restrict to your dashboard URL in production | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "") | |
| CHANNEL_ID = os.environ.get("SLACK_CHANNEL_ID", "") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # SLACK SIGNATURE VERIFICATION | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def verify_slack_signature(body: bytes, timestamp: str, signature: str) -> bool: | |
| if not timestamp or not signature: | |
| return False | |
| if abs(time.time() - int(timestamp)) > 300: | |
| return False | |
| sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}" | |
| computed = "v0=" + hmac.new( | |
| SLACK_SIGNING_SECRET.encode(), | |
| sig_basestring.encode(), | |
| hashlib.sha256, | |
| ).hexdigest() | |
| return hmac.compare_digest(computed, signature) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # SLACK EVENTS | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| async def slack_events(request: Request): | |
| try: | |
| body = await request.body() | |
| if not body: | |
| return JSONResponse({"ok": True}) | |
| payload = json.loads(body) | |
| # URL verification challenge β must respond before signature check | |
| if payload.get("type") == "url_verification": | |
| return JSONResponse({"challenge": payload["challenge"]}) | |
| # Verify all other requests | |
| timestamp = request.headers.get("X-Slack-Request-Timestamp", "") | |
| signature = request.headers.get("X-Slack-Signature", "") | |
| if not verify_slack_signature(body, timestamp, signature): | |
| return JSONResponse({"error": "Invalid signature"}, status_code=403) | |
| if payload.get("type") == "event_callback": | |
| event = payload.get("event", {}) | |
| if event.get("type") == "message": | |
| thread = threading.Thread(target=router.route_event, args=(event,)) | |
| thread.daemon = True | |
| thread.start() | |
| return JSONResponse({"ok": True}) | |
| except json.JSONDecodeError: | |
| return JSONResponse({"ok": True}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ADMIN β ADD CANDIDATE | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class CandidateIn(BaseModel): | |
| slack_user_id: str | |
| name: str | |
| cohort_id: str | |
| ft_slack_id: str | |
| hr_slack_id: str | |
| week0_start_date: str | |
| stage: str = "week0" # week0 | intern | probation_w1 | probation_w2 | |
| department: str = "general" # ai | hr | sales | product | general | |
| channel_id: str = "" # Slack channel ID where intern posts reports | |
| async def add_candidate(data: CandidateIn): | |
| try: | |
| candidate = sheets.create_candidate( | |
| slack_user_id=data.slack_user_id, | |
| name=data.name, | |
| cohort_id=data.cohort_id, | |
| ft_slack_id=data.ft_slack_id, | |
| hr_slack_id=data.hr_slack_id, | |
| week0_start_date=data.week0_start_date, | |
| stage=data.stage, | |
| department=data.department, | |
| channel_id=data.channel_id, | |
| ) | |
| return JSONResponse({"ok": True, "candidate_id": candidate["candidate_id"]}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ADMIN β LOG COMMITMENT | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class CommitmentIn(BaseModel): | |
| candidate_id: str | |
| description: str | |
| due_date: str | |
| due_time: str | |
| async def log_commitment(data: CommitmentIn): | |
| try: | |
| commitment_id = sheets.log_commitment( | |
| candidate_id=data.candidate_id, | |
| description=data.description, | |
| due_date=data.due_date, | |
| due_time=data.due_time, | |
| ) | |
| return JSONResponse({"ok": True, "commitment_id": commitment_id}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # HEALTH | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # INTERN MANAGEMENT β GET / UPDATE / DELETE | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class InternUpdate(BaseModel): | |
| name: str | None = None | |
| stage: str | None = None | |
| department: str | None = None | |
| channel_id: str | None = None | |
| ft_slack_id: str | None = None | |
| hr_slack_id: str | None = None | |
| week0_start_date: str | None = None | |
| status: str | None = None | |
| cohort_id: str | None = None | |
| async def get_interns(department: str = None): | |
| """ | |
| Returns all active interns. | |
| Optional query param: ?department=ai | |
| Example: /interns?department=ai | |
| """ | |
| try: | |
| candidates = sheets.get_active_candidates() | |
| if department: | |
| candidates = [ | |
| c for c in candidates | |
| if c.get("department", "").lower() == department.lower() | |
| ] | |
| return JSONResponse({"ok": True, "count": len(candidates), "interns": candidates}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def get_intern(candidate_id: str): | |
| try: | |
| candidate = sheets.get_candidate_by_id(candidate_id) | |
| if not candidate: | |
| return JSONResponse({"error": "Intern not found"}, status_code=404) | |
| return JSONResponse({"ok": True, "intern": candidate}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def update_intern(candidate_id: str, data: InternUpdate): | |
| """ | |
| Update one or more fields of an intern. | |
| Only pass the fields you want to change. | |
| """ | |
| try: | |
| candidate = sheets.get_candidate_by_id(candidate_id) | |
| if not candidate: | |
| return JSONResponse({"error": "Intern not found"}, status_code=404) | |
| updates = data.dict(exclude_none=True) | |
| if not updates: | |
| return JSONResponse({"error": "No fields to update"}, status_code=400) | |
| for field, value in updates.items(): | |
| sheets.update_candidate_field(candidate_id, field, value) | |
| return JSONResponse({"ok": True, "updated": updates}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def delete_intern(candidate_id: str): | |
| """ | |
| Marks the intern as eliminated and inactive. | |
| Does not delete the row β preserves audit trail. | |
| """ | |
| try: | |
| candidate = sheets.get_candidate_by_id(candidate_id) | |
| if not candidate: | |
| return JSONResponse({"error": "Intern not found"}, status_code=404) | |
| sheets.update_candidate_field(candidate_id, "status", "eliminated") | |
| return JSONResponse({ | |
| "ok": True, | |
| "message": f"{candidate.get('name')} removed from pipeline. Row preserved for audit." | |
| }) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def trigger_agenda(): | |
| """Trigger the agenda prompt manually β bypasses working day check.""" | |
| try: | |
| def run(): | |
| try: | |
| sched.job_morning_agenda_prompt(force=True) | |
| except Exception as e: | |
| print(f"[trigger_agenda] error: {e}") | |
| thread = threading.Thread(target=run) | |
| thread.daemon = True | |
| thread.start() | |
| return JSONResponse({"ok": True, "message": "Agenda prompt triggered"}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def trigger_summary(): | |
| """Trigger the 11:30 PM miss check and summary manually β for testing.""" | |
| try: | |
| import threading | |
| import handlers | |
| thread = threading.Thread(target=handlers.handle_missed_report_check) | |
| thread.daemon = True | |
| thread.start() | |
| return JSONResponse({"ok": True, "message": "Miss check and summary triggered"}) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def health(): | |
| return JSONResponse({"status": "ok"}) |