Skip to content

Commit bd82bdb

Browse files
jeremymanningclaude
andcommitted
Improve onboarding bot: dynamic form, robust user lookup, local run docs
- Dynamic form: conditionally show grad program fields based on role selection using Slack views.update (hide when not relevant) - Fix slash command user parsing: handle raw @username text from Slack (not just <@U...> format) by looking up users by name - Add openpyxl to requirements (missing dependency for website service) - Add local running instructions to README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7ddbe08 commit bd82bdb

4 files changed

Lines changed: 210 additions & 127 deletions

File tree

lab_manual.pdf

0 Bytes
Binary file not shown.

scripts/onboarding/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,24 @@ pip install -r requirements.txt
114114

115115
### 7. Run the Bot
116116

117+
The bot runs locally on your machine using Slack's Socket Mode (no public URL needed).
118+
Run it from the **repository root** directory:
119+
117120
```bash
121+
# Create and activate a virtual environment (first time only)
122+
python -m venv venv
123+
source venv/bin/activate # macOS/Linux
124+
pip install -r scripts/onboarding/requirements.txt
125+
126+
# Load environment variables and start the bot
127+
set -a && source scripts/onboarding/.env && set +a
118128
python -m scripts.onboarding.bot
119129
```
120130

131+
The bot will connect to Slack and log `⚡️ Bolt app is running!` when ready.
132+
Press `Ctrl+C` to stop. The bot must be running for slash commands and
133+
Workflow Builder integrations to respond.
134+
121135
## Usage
122136

123137
### Starting Onboarding

scripts/onboarding/handlers/onboard.py

Lines changed: 193 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,44 @@ def handle_onboard_command(ack, command, client: WebClient, respond):
4747

4848
user_id = command["user_id"]
4949
text = command.get("text", "").strip()
50+
logger.info(f"Onboard command from {user_id}, text: '{text}'")
5051

5152
# Check if user is admin
5253
if user_id != config.slack.admin_user_id:
5354
respond("Only the lab admin can initiate onboarding.")
5455
return
5556

56-
# Parse mentioned user if provided
57+
# Parse mentioned user from text
58+
# Slack may send mentions as <@U12345|username>, <@U12345>,
59+
# or just @username (common in slash commands where Slack doesn't
60+
# auto-convert mentions to the <@...> format)
61+
import re
5762
target_user_id = None
58-
if text.startswith("<@") and ">" in text:
59-
# Extract user ID from mention like <@U12345|username>
60-
target_user_id = text.split("<@")[1].split("|")[0].split(">")[0]
63+
mention_match = re.search(r'<@([A-Z0-9]+)(?:\|[^>]*)?>', text)
64+
if mention_match:
65+
target_user_id = mention_match.group(1)
66+
elif text.startswith("@") or text:
67+
# Try to look up user by name/display name
68+
search_name = text.lstrip("@").strip()
69+
if search_name:
70+
try:
71+
users_resp = client.users_list()
72+
for member in users_resp["members"]:
73+
if member.get("deleted") or member.get("is_bot"):
74+
continue
75+
name = member.get("name", "")
76+
display_name = member.get("profile", {}).get("display_name", "")
77+
real_name = member.get("real_name", "")
78+
if search_name.lower() in (
79+
name.lower(),
80+
display_name.lower(),
81+
real_name.lower(),
82+
):
83+
target_user_id = member["id"]
84+
logger.info(f"Resolved '{search_name}' to user {target_user_id} ({real_name})")
85+
break
86+
except SlackApiError as e:
87+
logger.error(f"Error looking up user: {e}")
6188

6289
if not target_user_id:
6390
respond(
@@ -114,6 +141,43 @@ def handle_onboard_command(ack, command, client: WebClient, respond):
114141

115142
respond(f"Started onboarding for <@{target_user_id}>. They've been sent the welcome message.")
116143

144+
# Dynamic form updates — show/hide grad fields based on role selection
145+
@app.action("role_select")
146+
def handle_role_select(ack, body, client: WebClient):
147+
"""Update the form when role is selected to show/hide grad fields."""
148+
ack()
149+
selected_role = body["actions"][0]["selected_option"]["value"]
150+
logger.info(f"Role selected: {selected_role}")
151+
client.views_update(
152+
view_id=body["view"]["id"],
153+
view={
154+
"type": "modal",
155+
"callback_id": "onboarding_form",
156+
"title": {"type": "plain_text", "text": "CDL Onboarding"},
157+
"submit": {"type": "plain_text", "text": "Submit"},
158+
"close": {"type": "plain_text", "text": "Cancel"},
159+
"blocks": _build_form_blocks(role=selected_role),
160+
},
161+
)
162+
163+
@app.action("grad_type_select")
164+
def handle_grad_type_select(ack, body, client: WebClient):
165+
"""Update the form when grad type is selected to show/hide masters field."""
166+
ack()
167+
selected_grad_type = body["actions"][0]["selected_option"]["value"]
168+
logger.info(f"Grad type selected: {selected_grad_type}")
169+
client.views_update(
170+
view_id=body["view"]["id"],
171+
view={
172+
"type": "modal",
173+
"callback_id": "onboarding_form",
174+
"title": {"type": "plain_text", "text": "CDL Onboarding"},
175+
"submit": {"type": "plain_text", "text": "Submit"},
176+
"close": {"type": "plain_text", "text": "Cancel"},
177+
"blocks": _build_form_blocks(role="Graduate Student", grad_type=selected_grad_type),
178+
},
179+
)
180+
117181
# Handle the onboarding form submission
118182
@app.view("onboarding_form")
119183
def handle_onboarding_form(ack, body, client: WebClient, view):
@@ -436,6 +500,130 @@ def _build_welcome_message(user_name: str) -> list:
436500
]
437501

438502

503+
def _build_form_blocks(role=None, grad_type=None):
504+
"""Build the onboarding form blocks, conditionally showing grad fields."""
505+
role_options = [
506+
{"text": {"type": "plain_text", "text": "Graduate Student"}, "value": "Graduate Student"},
507+
{"text": {"type": "plain_text", "text": "Undergraduate"}, "value": "Undergraduate"},
508+
{"text": {"type": "plain_text", "text": "Postdoctoral Researcher"}, "value": "Postdoctoral Researcher"},
509+
{"text": {"type": "plain_text", "text": "Lab Manager"}, "value": "Lab Manager"},
510+
{"text": {"type": "plain_text", "text": "Research Scientist"}, "value": "Research Scientist"},
511+
]
512+
513+
role_element = {
514+
"type": "static_select",
515+
"action_id": "role_select",
516+
"placeholder": {"type": "plain_text", "text": "Select your role"},
517+
"options": role_options,
518+
}
519+
if role:
520+
role_element["initial_option"] = next(
521+
(o for o in role_options if o["value"] == role), None
522+
)
523+
524+
blocks = [
525+
{
526+
"type": "section",
527+
"text": {
528+
"type": "mrkdwn",
529+
"text": "Please provide the following information for your CDL profile.",
530+
},
531+
},
532+
{
533+
"type": "input",
534+
"block_id": "role_block",
535+
"dispatch_action": True,
536+
"element": role_element,
537+
"label": {"type": "plain_text", "text": "Your Role in the Lab"},
538+
},
539+
]
540+
541+
# Show grad type only for Graduate Students
542+
if role == "Graduate Student":
543+
grad_type_options = [
544+
{"text": {"type": "plain_text", "text": "Doctoral"}, "value": "Doctoral"},
545+
{"text": {"type": "plain_text", "text": "Masters"}, "value": "Masters"},
546+
]
547+
grad_type_element = {
548+
"type": "static_select",
549+
"action_id": "grad_type_select",
550+
"placeholder": {"type": "plain_text", "text": "Select program type"},
551+
"options": grad_type_options,
552+
}
553+
if grad_type:
554+
grad_type_element["initial_option"] = next(
555+
(o for o in grad_type_options if o["value"] == grad_type), None
556+
)
557+
blocks.append({
558+
"type": "input",
559+
"block_id": "grad_type_block",
560+
"dispatch_action": True,
561+
"element": grad_type_element,
562+
"label": {"type": "plain_text", "text": "Graduate Program Type"},
563+
})
564+
565+
# Show masters field only for Masters students
566+
if grad_type == "Masters":
567+
blocks.append({
568+
"type": "input",
569+
"block_id": "grad_field_block",
570+
"element": {
571+
"type": "plain_text_input",
572+
"action_id": "grad_field_input",
573+
"placeholder": {"type": "plain_text", "text": "e.g., Quantitative Biomedical Sciences"},
574+
},
575+
"label": {"type": "plain_text", "text": "Masters Program/Field"},
576+
})
577+
578+
# Common fields (always shown)
579+
blocks.extend([
580+
{
581+
"type": "input",
582+
"block_id": "github_block",
583+
"element": {
584+
"type": "plain_text_input",
585+
"action_id": "github_input",
586+
"placeholder": {"type": "plain_text", "text": "e.g., octocat"},
587+
},
588+
"label": {"type": "plain_text", "text": "GitHub Username"},
589+
"hint": {"type": "plain_text", "text": "Your GitHub username (not email). We'll invite you to the ContextLab organization."},
590+
},
591+
{
592+
"type": "input",
593+
"block_id": "bio_block",
594+
"element": {
595+
"type": "plain_text_input",
596+
"action_id": "bio_input",
597+
"multiline": True,
598+
"placeholder": {"type": "plain_text", "text": "Tell us about yourself and your research interests..."},
599+
},
600+
"label": {"type": "plain_text", "text": "Short Bio"},
601+
"hint": {"type": "plain_text", "text": "3-4 sentences about you. We'll edit it for style consistency."},
602+
},
603+
{
604+
"type": "input",
605+
"block_id": "website_block",
606+
"optional": True,
607+
"element": {
608+
"type": "plain_text_input",
609+
"action_id": "website_input",
610+
"placeholder": {"type": "plain_text", "text": "https://your-website.com"},
611+
},
612+
"label": {"type": "plain_text", "text": "Personal Website (optional)"},
613+
"hint": {"type": "plain_text", "text": "If you have a personal website, we'll link to it from your profile."},
614+
},
615+
{
616+
"type": "section",
617+
"text": {
618+
"type": "mrkdwn",
619+
"text": ":camera: *Photo:* After submitting this form, please upload a profile photo by sending it as a message in this conversation.",
620+
},
621+
},
622+
])
623+
624+
return blocks
625+
626+
439627
def _open_onboarding_form(client: WebClient, trigger_id: str, request: OnboardingRequest):
440628
"""Open the onboarding information form modal."""
441629
try:
@@ -447,129 +635,7 @@ def _open_onboarding_form(client: WebClient, trigger_id: str, request: Onboardin
447635
"title": {"type": "plain_text", "text": "CDL Onboarding"},
448636
"submit": {"type": "plain_text", "text": "Submit"},
449637
"close": {"type": "plain_text", "text": "Cancel"},
450-
"blocks": [
451-
{
452-
"type": "section",
453-
"text": {
454-
"type": "mrkdwn",
455-
"text": "Please provide the following information for your CDL profile.",
456-
},
457-
},
458-
{
459-
"type": "input",
460-
"block_id": "role_block",
461-
"element": {
462-
"type": "static_select",
463-
"action_id": "role_select",
464-
"placeholder": {"type": "plain_text", "text": "Select your role"},
465-
"options": [
466-
{"text": {"type": "plain_text", "text": "Graduate Student"}, "value": "Graduate Student"},
467-
{"text": {"type": "plain_text", "text": "Undergraduate"}, "value": "Undergraduate"},
468-
{"text": {"type": "plain_text", "text": "Postdoctoral Researcher"}, "value": "Postdoctoral Researcher"},
469-
{"text": {"type": "plain_text", "text": "Lab Manager"}, "value": "Lab Manager"},
470-
{"text": {"type": "plain_text", "text": "Research Scientist"}, "value": "Research Scientist"},
471-
],
472-
},
473-
"label": {"type": "plain_text", "text": "Your Role in the Lab"},
474-
},
475-
{
476-
"type": "input",
477-
"block_id": "grad_type_block",
478-
"optional": True,
479-
"element": {
480-
"type": "static_select",
481-
"action_id": "grad_type_select",
482-
"placeholder": {"type": "plain_text", "text": "Select program type"},
483-
"options": [
484-
{"text": {"type": "plain_text", "text": "Doctoral"}, "value": "Doctoral"},
485-
{"text": {"type": "plain_text", "text": "Masters"}, "value": "Masters"},
486-
],
487-
},
488-
"label": {"type": "plain_text", "text": "Graduate Program Type"},
489-
"hint": {"type": "plain_text", "text": "Required for Graduate Students only"},
490-
},
491-
{
492-
"type": "input",
493-
"block_id": "grad_field_block",
494-
"optional": True,
495-
"element": {
496-
"type": "plain_text_input",
497-
"action_id": "grad_field_input",
498-
"placeholder": {"type": "plain_text", "text": "e.g., Quantitative Biomedical Sciences"},
499-
},
500-
"label": {"type": "plain_text", "text": "Masters Program/Field"},
501-
"hint": {"type": "plain_text", "text": "Required for Masters students only"},
502-
},
503-
{
504-
"type": "input",
505-
"block_id": "github_block",
506-
"element": {
507-
"type": "plain_text_input",
508-
"action_id": "github_input",
509-
"placeholder": {
510-
"type": "plain_text",
511-
"text": "e.g., octocat",
512-
},
513-
},
514-
"label": {
515-
"type": "plain_text",
516-
"text": "GitHub Username",
517-
},
518-
"hint": {
519-
"type": "plain_text",
520-
"text": "Your GitHub username (not email). We'll invite you to the ContextLab organization.",
521-
},
522-
},
523-
{
524-
"type": "input",
525-
"block_id": "bio_block",
526-
"element": {
527-
"type": "plain_text_input",
528-
"action_id": "bio_input",
529-
"multiline": True,
530-
"placeholder": {
531-
"type": "plain_text",
532-
"text": "Tell us about yourself and your research interests...",
533-
},
534-
},
535-
"label": {
536-
"type": "plain_text",
537-
"text": "Short Bio",
538-
},
539-
"hint": {
540-
"type": "plain_text",
541-
"text": "3-4 sentences about you. We'll edit it for style consistency.",
542-
},
543-
},
544-
{
545-
"type": "input",
546-
"block_id": "website_block",
547-
"optional": True,
548-
"element": {
549-
"type": "plain_text_input",
550-
"action_id": "website_input",
551-
"placeholder": {
552-
"type": "plain_text",
553-
"text": "https://your-website.com",
554-
},
555-
},
556-
"label": {
557-
"type": "plain_text",
558-
"text": "Personal Website (optional)",
559-
},
560-
"hint": {
561-
"type": "plain_text",
562-
"text": "If you have a personal website, we'll link to it from your profile.",
563-
},
564-
},
565-
{
566-
"type": "section",
567-
"text": {
568-
"type": "mrkdwn",
569-
"text": ":camera: *Photo:* After submitting this form, please upload a profile photo by sending it as a message in this conversation.",
570-
},
571-
},
572-
],
638+
"blocks": _build_form_blocks(),
573639
},
574640
)
575641
except SlackApiError as e:

scripts/onboarding/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ google-auth-httplib2>=0.1.1
1616
# Image processing
1717
Pillow>=10.0.0
1818

19+
# Website integration (Excel file handling)
20+
openpyxl>=3.1.0
21+
1922
# Bio editing (Claude API)
2023
anthropic>=0.7.0
2124

0 commit comments

Comments
 (0)