banao-tech commited on
Commit
574f86d
Β·
verified Β·
1 Parent(s): 6a9e490

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -126
app.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
  app.py β€” HuggingFace Spaces entry point
3
- Gradio provides the public UI + keeps the Space alive.
4
- FastAPI handles the Slack webhook at /slack/events.
5
- Scheduler runs in the background thread.
6
  """
7
 
8
  import os
@@ -13,8 +13,7 @@ import threading
13
  import json
14
  from contextlib import asynccontextmanager
15
 
16
- import gradio as gr
17
- from fastapi import FastAPI, Request, Response
18
  from fastapi.responses import JSONResponse
19
  from pydantic import BaseModel
20
 
@@ -22,16 +21,16 @@ import router
22
  import scheduler as sched
23
  import sheets
24
 
 
25
  # ─────────────────────────────────────────────
26
- # FASTAPI β€” Slack webhook
27
  # ─────────────────────────────────────────────
28
 
29
  @asynccontextmanager
30
- async def lifespan(app):
31
- # Run setup in background β€” don't block startup or hammer Sheets API
32
  import asyncio
33
  async def delayed_setup():
34
- await asyncio.sleep(5) # wait 5s for Space to fully start
35
  try:
36
  sheets.setup_sheets()
37
  except Exception as e:
@@ -40,13 +39,29 @@ async def lifespan(app):
40
  sched.start_scheduler()
41
  yield
42
 
43
- app = FastAPI(lifespan=lifespan)
 
 
 
 
 
 
 
 
 
 
44
 
45
  SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "")
46
  CHANNEL_ID = os.environ.get("SLACK_CHANNEL_ID", "")
47
 
48
 
 
 
 
 
49
  def verify_slack_signature(body: bytes, timestamp: str, signature: str) -> bool:
 
 
50
  if abs(time.time() - int(timestamp)) > 300:
51
  return False
52
  sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
@@ -58,31 +73,32 @@ def verify_slack_signature(body: bytes, timestamp: str, signature: str) -> bool:
58
  return hmac.compare_digest(computed, signature)
59
 
60
 
61
- @app.post("/slack/events")
 
 
 
 
62
  async def slack_events(request: Request):
63
  try:
64
  body = await request.body()
65
-
66
  if not body:
67
  return JSONResponse({"ok": True})
68
 
69
  payload = json.loads(body)
70
 
71
- # One-time URL verification β€” must respond before anything else
72
- # Slack sends this unsigned to verify the endpoint exists
73
  if payload.get("type") == "url_verification":
74
  return JSONResponse({"challenge": payload["challenge"]})
75
 
76
- # All other events β€” verify signature
77
  timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
78
  signature = request.headers.get("X-Slack-Signature", "")
79
-
80
  if not verify_slack_signature(body, timestamp, signature):
81
  return JSONResponse({"error": "Invalid signature"}, status_code=403)
82
 
83
  if payload.get("type") == "event_callback":
84
  event = payload.get("event", {})
85
- if event.get("type") == "message" and event.get("channel") == CHANNEL_ID:
86
  thread = threading.Thread(target=router.route_event, args=(event,))
87
  thread.daemon = True
88
  thread.start()
@@ -95,6 +111,10 @@ async def slack_events(request: Request):
95
  return JSONResponse({"error": str(e)}, status_code=500)
96
 
97
 
 
 
 
 
98
  class CandidateIn(BaseModel):
99
  slack_user_id: str
100
  name: str
@@ -102,9 +122,12 @@ class CandidateIn(BaseModel):
102
  ft_slack_id: str
103
  hr_slack_id: str
104
  week0_start_date: str
105
- stage: str = "week0" # week0 | intern | probation_w1 | probation_w2
 
 
 
106
 
107
- @app.post("/admin/add_candidate")
108
  async def add_candidate(data: CandidateIn):
109
  try:
110
  candidate = sheets.create_candidate(
@@ -115,130 +138,43 @@ async def add_candidate(data: CandidateIn):
115
  hr_slack_id=data.hr_slack_id,
116
  week0_start_date=data.week0_start_date,
117
  stage=data.stage,
 
 
118
  )
119
  return JSONResponse({"ok": True, "candidate_id": candidate["candidate_id"]})
120
  except Exception as e:
121
  return JSONResponse({"error": str(e)}, status_code=500)
122
 
123
 
 
 
 
 
124
  class CommitmentIn(BaseModel):
125
  candidate_id: str
126
  description: str
127
  due_date: str
128
  due_time: str
129
 
130
- @app.post("/admin/log_commitment")
131
- async def log_commitment(data: CommitmentIn):
132
- commitment_id = sheets.log_commitment(
133
- candidate_id=data.candidate_id,
134
- description=data.description,
135
- due_date=data.due_date,
136
- due_time=data.due_time,
137
- )
138
- return JSONResponse({"ok": True, "commitment_id": commitment_id})
139
-
140
-
141
- @app.get("/health")
142
- async def health():
143
- return JSONResponse({"status": "ok"})
144
-
145
-
146
- # ─────────────────────────────────────────────
147
- # GRADIO β€” Dashboard UI (keeps Space alive)
148
- # ─────────────────────────────────────────────
149
-
150
- def get_cohort_status():
151
- """Pull live candidate data from Sheets for the dashboard."""
152
- try:
153
- candidates = sheets.get_active_candidates()
154
- if not candidates:
155
- return "No active candidates."
156
- lines = ["**Active candidates**\n"]
157
- for c in candidates:
158
- lines.append(
159
- f"- **{c['name']}** β€” Stage: `{c['stage']}` | "
160
- f"Status: `{c['status']}` | Misses: `{c['miss_count']}`"
161
- )
162
- return "\n".join(lines)
163
- except Exception as e:
164
- return f"Error loading candidates: {e}"
165
-
166
-
167
- def get_recent_reports():
168
- """Show last 10 report submissions across all candidates."""
169
- try:
170
- candidates = sheets.get_active_candidates()
171
- all_reports = []
172
- for c in candidates:
173
- reports = sheets.get_reports_for_candidate(c["candidate_id"])
174
- for r in reports:
175
- r["candidate_name"] = c["name"]
176
- all_reports.append(r)
177
- all_reports.sort(key=lambda x: x.get("submitted_at", ""), reverse=True)
178
- recent = all_reports[:10]
179
- if not recent:
180
- return "No reports yet."
181
- lines = ["**Recent reports**\n"]
182
- for r in recent:
183
- flag = "βœ“" if r.get("format_valid") == "True" else "βœ—"
184
- lines.append(
185
- f"- {flag} **{r['candidate_name']}** β€” "
186
- f"Day {r.get('day_number')} | "
187
- f"Score: {r.get('quality_score', '-')} | "
188
- f"{r.get('submitted_at', '')[:16]}"
189
- )
190
- return "\n".join(lines)
191
- except Exception as e:
192
- return f"Error loading reports: {e}"
193
 
194
-
195
- def get_pending_evals():
196
- """Show evaluations awaiting HR confirmation."""
197
  try:
198
- candidates = sheets.get_active_candidates()
199
- pending = []
200
- for c in candidates:
201
- rec = sheets.get_pending_recommendation(c["candidate_id"])
202
- if rec:
203
- pending.append(
204
- f"- **{c['name']}** β€” `{rec.get('eval_type')}` β†’ "
205
- f"*{rec.get('recommendation')}* (awaiting HR)"
206
- )
207
- if not pending:
208
- return "No pending evaluations."
209
- return "**Pending HR confirmations**\n\n" + "\n".join(pending)
210
  except Exception as e:
211
- return f"Error loading evaluations: {e}"
212
-
213
-
214
- with gr.Blocks(title="ai_vidya Pipeline Agent") as gradio_app:
215
- gr.Markdown("## ai_vidya Pipeline Agent")
216
- gr.Markdown("Slack agent managing Week 0 β†’ Probation for the AI Department intern pipeline.")
217
-
218
- with gr.Row():
219
- refresh_btn = gr.Button("Refresh", variant="secondary")
220
-
221
- with gr.Row():
222
- with gr.Column():
223
- cohort_md = gr.Markdown(value=get_cohort_status())
224
- with gr.Column():
225
- pending_md = gr.Markdown(value=get_pending_evals())
226
-
227
- with gr.Row():
228
- reports_md = gr.Markdown(value=get_recent_reports())
229
-
230
- def refresh():
231
- return get_cohort_status(), get_pending_evals(), get_recent_reports()
232
-
233
- refresh_btn.click(
234
- fn=refresh,
235
- outputs=[cohort_md, pending_md, reports_md],
236
- )
237
 
238
 
239
  # ─────────────────────────────────────────────
240
- # MOUNT GRADIO ONTO FASTAPI + START SCHEDULER
241
  # ─────────────────────────────────────────────
242
 
243
- # Mount Gradio at root β€” FastAPI handles /slack/events and /admin/* separately
244
- app = gr.mount_gradio_app(app, gradio_app, path="/dashboard")
 
 
1
  """
2
  app.py β€” HuggingFace Spaces entry point
3
+ Pure FastAPI + uvicorn. No Gradio.
4
+ Slack webhook at /slack/events.
5
+ Admin endpoints at /admin/*.
6
  """
7
 
8
  import os
 
13
  import json
14
  from contextlib import asynccontextmanager
15
 
16
+ from fastapi import FastAPI, Request
 
17
  from fastapi.responses import JSONResponse
18
  from pydantic import BaseModel
19
 
 
21
  import scheduler as sched
22
  import sheets
23
 
24
+
25
  # ─────────────────────────────────────────────
26
+ # LIFESPAN
27
  # ─────────────────────────────────────────────
28
 
29
  @asynccontextmanager
30
+ async def lifespan(app: FastAPI):
 
31
  import asyncio
32
  async def delayed_setup():
33
+ await asyncio.sleep(5)
34
  try:
35
  sheets.setup_sheets()
36
  except Exception as e:
 
39
  sched.start_scheduler()
40
  yield
41
 
42
+
43
+ # ─────────────────────────────────────────────
44
+ # APP
45
+ # ─────────────────────────────────────────────
46
+
47
+ app = FastAPI(
48
+ title="Intern Management Agent",
49
+ description="Slack agent managing intern pipeline β€” Week 0 through Probation.",
50
+ version="1.0.0",
51
+ lifespan=lifespan,
52
+ )
53
 
54
  SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "")
55
  CHANNEL_ID = os.environ.get("SLACK_CHANNEL_ID", "")
56
 
57
 
58
+ # ─────────────────────────────────────────────
59
+ # SLACK SIGNATURE VERIFICATION
60
+ # ─────────────────────────────────────────────
61
+
62
  def verify_slack_signature(body: bytes, timestamp: str, signature: str) -> bool:
63
+ if not timestamp or not signature:
64
+ return False
65
  if abs(time.time() - int(timestamp)) > 300:
66
  return False
67
  sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
 
73
  return hmac.compare_digest(computed, signature)
74
 
75
 
76
+ # ─────────────────────────────────────────────
77
+ # SLACK EVENTS
78
+ # ─────────────────────────────────────────────
79
+
80
+ @app.post("/slack/events", summary="Slack event webhook")
81
  async def slack_events(request: Request):
82
  try:
83
  body = await request.body()
 
84
  if not body:
85
  return JSONResponse({"ok": True})
86
 
87
  payload = json.loads(body)
88
 
89
+ # URL verification challenge β€” must respond before signature check
 
90
  if payload.get("type") == "url_verification":
91
  return JSONResponse({"challenge": payload["challenge"]})
92
 
93
+ # Verify all other requests
94
  timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
95
  signature = request.headers.get("X-Slack-Signature", "")
 
96
  if not verify_slack_signature(body, timestamp, signature):
97
  return JSONResponse({"error": "Invalid signature"}, status_code=403)
98
 
99
  if payload.get("type") == "event_callback":
100
  event = payload.get("event", {})
101
+ if event.get("type") == "message":
102
  thread = threading.Thread(target=router.route_event, args=(event,))
103
  thread.daemon = True
104
  thread.start()
 
111
  return JSONResponse({"error": str(e)}, status_code=500)
112
 
113
 
114
+ # ─────────────────────────────────────────────
115
+ # ADMIN β€” ADD CANDIDATE
116
+ # ─────────────────────────────────────────────
117
+
118
  class CandidateIn(BaseModel):
119
  slack_user_id: str
120
  name: str
 
122
  ft_slack_id: str
123
  hr_slack_id: str
124
  week0_start_date: str
125
+ stage: str = "week0" # week0 | intern | probation_w1 | probation_w2
126
+ department: str = "general" # ai | hr | sales | product | general
127
+ channel_id: str = "" # Slack channel ID where intern posts reports
128
+
129
 
130
+ @app.post("/admin/add_candidate", summary="Register a new intern")
131
  async def add_candidate(data: CandidateIn):
132
  try:
133
  candidate = sheets.create_candidate(
 
138
  hr_slack_id=data.hr_slack_id,
139
  week0_start_date=data.week0_start_date,
140
  stage=data.stage,
141
+ department=data.department,
142
+ channel_id=data.channel_id,
143
  )
144
  return JSONResponse({"ok": True, "candidate_id": candidate["candidate_id"]})
145
  except Exception as e:
146
  return JSONResponse({"error": str(e)}, status_code=500)
147
 
148
 
149
+ # ─────────────────────────────────────────────
150
+ # ADMIN β€” LOG COMMITMENT
151
+ # ─────────────────────────────────────────────
152
+
153
  class CommitmentIn(BaseModel):
154
  candidate_id: str
155
  description: str
156
  due_date: str
157
  due_time: str
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ @app.post("/admin/log_commitment", summary="Log a Week 2 delivery commitment")
161
+ async def log_commitment(data: CommitmentIn):
 
162
  try:
163
+ commitment_id = sheets.log_commitment(
164
+ candidate_id=data.candidate_id,
165
+ description=data.description,
166
+ due_date=data.due_date,
167
+ due_time=data.due_time,
168
+ )
169
+ return JSONResponse({"ok": True, "commitment_id": commitment_id})
 
 
 
 
 
170
  except Exception as e:
171
+ return JSONResponse({"error": str(e)}, status_code=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
 
174
  # ─────────────────────────────────────────────
175
+ # HEALTH
176
  # ─────────────────────────────────────────────
177
 
178
+ @app.get("/health", summary="Health check")
179
+ async def health():
180
+ return JSONResponse({"status": "ok"})