Skip to content

Add resume seniority override#157

Merged
michaelmwu merged 4 commits intomainfrom
michaelmwu/resume-seniority-override
Mar 5, 2026
Merged

Add resume seniority override#157
michaelmwu merged 4 commits intomainfrom
michaelmwu/resume-seniority-override

Conversation

@michaelmwu
Copy link
Member

@michaelmwu michaelmwu commented Mar 5, 2026

Description

  • add a seniority override dropdown to resume update confirmations and show parsed seniority
  • allow overrides to populate cSeniority and guard empty apply requests
  • add unit coverage for seniority helpers and view wiring

Related Issue

  • N/A

How Has This Been Tested?

  • ruff (pre-commit)
  • ruff format (pre-commit)
  • mypy (pre-commit)

Summary by CodeRabbit

  • New Features
    • Resume preview now displays extracted seniority information.
    • Users can manually override the detected seniority level during resume updates.
    • Enhanced validation to ensure meaningful changes are made before confirming resume updates.

Copilot AI review requested due to automatic review settings March 5, 2026 01:26
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

Warning

Rate limit exceeded

@michaelmwu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92a9c0b4-2c4c-41d7-bea3-a9dd7771e636

📥 Commits

Reviewing files that changed from the base of the PR and between 60e7d07 and 35876a3.

📒 Files selected for processing (1)
  • apps/discord_bot/src/five08/discord_bot/cogs/crm.py
📝 Walkthrough

Walkthrough

This change extends the resume CRM workflow with seniority-level parsing and override functionality. It introduces helper functions to normalize seniority labels and extract seniority from extracted profiles, adds a UI select component for seniority override, and enhances the resume confirmation view to display parsed seniority and apply user-selected overrides to update payloads.

Changes

Cohort / File(s) Summary
Seniority Parsing & Override Core
apps/discord_bot/src/five08/discord_bot/cogs/crm.py
Added helper functions _format_seniority_label() and _extract_parsed_seniority() to normalize and extract seniority data. Introduced ResumeSeniorityOverrideSelect UI component for user override selection. Extended ResumeUpdateConfirmationView to accept, display, and apply seniority overrides via new _set_seniority_override() method and conditional component addition. Modified _build_resume_preview_embed() to include Parsed Seniority field. Propagated seniority through _run_resume_extract_and_preview() and added update guard in confirm_updates().
Seniority Feature Tests
tests/unit/test_crm.py
Added comprehensive tests validating seniority label formatting, extraction from dict/object payloads, UI component composition with seniority select, and seniority override application to proposed updates.

Sequence Diagram

sequenceDiagram
    participant User as User/Discord
    participant Extract as Resume Extraction
    participant Parse as _extract_parsed_seniority()
    participant View as ResumeUpdateConfirmationView
    participant Select as ResumeSeniorityOverrideSelect
    participant Updates as proposed_updates

    User->>Extract: Submit resume for extraction
    Extract-->>Parse: Return extracted_profile
    Parse-->>View: Compute parsed_seniority
    View->>View: Display embed with Parsed Seniority
    View->>View: Initialize seniority_override state
    View->>Select: Add select component if parsed_seniority present
    User->>Select: Choose seniority override (optional)
    Select->>View: Trigger _set_seniority_override()
    View->>Updates: Apply override to proposed_updates
    View-->>User: Confirm with updated payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A seniority tale so fine,
From parsing profiles in a line,
Junior, mid, or staff so grand,
Override buttons, oh so planned!
The rabbit hops through levels deep, 🎓✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add resume seniority override' accurately and concisely summarizes the main change: introducing a seniority override feature for resume updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch michaelmwu/resume-seniority-override

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a seniority override feature to the resume update confirmation workflow in the Discord CRM bot. When a resume is parsed and a seniority level is extracted, users now see it in the preview embed and can override it via a new dropdown select menu before confirming CRM updates.

Changes:

  • New helper functions _format_seniority_label and _extract_parsed_seniority for normalizing and extracting seniority data from parsed resume profiles.
  • New ResumeSeniorityOverrideSelect Discord UI component and corresponding wiring in ResumeUpdateConfirmationView (including _set_seniority_override) plus a guard against empty apply requests.
  • Unit tests covering label formatting, seniority extraction, and view wiring.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
apps/discord_bot/src/five08/discord_bot/cogs/crm.py Adds seniority helper functions, new ResumeSeniorityOverrideSelect UI select, updates ResumeUpdateConfirmationView with override support, adds parsed seniority to the preview embed, and guards against empty confirm_updates calls.
tests/unit/test_crm.py Adds parametrized tests for _format_seniority_label and _extract_parsed_seniority, plus tests for view wiring and the seniority override setter.

One issue found:

In _format_seniority_label, lines 86–87 are unreachable dead code:

    if normalized in labels:
        return labels[normalized]
    if normalized == "midlevel":   # ← dead code: "midlevel" is already a key in `labels`
        normalized = "mid-level"
    return normalized.title()

Since "midlevel" is already a key in the labels dict above (mapping to "Mid-level"), the return labels[normalized] call on line 85 will always be reached before line 86 when normalized == "midlevel". The block at lines 86–87 can never execute.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +86 to +87
if normalized == "midlevel":
normalized = "mid-level"
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 86–87 are dead code. normalized == "midlevel" is already covered by the labels dictionary lookup immediately above (line 84–85), which returns "Mid-level" for "midlevel" and exits the function. The reassignment on line 87 can never be reached.

Suggested change
if normalized == "midlevel":
normalized = "mid-level"

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/crm.py`:
- Around line 768-773: The new ResumeSeniorityOverrideSelect (the
discord.ui.Select added via self.add_item) remains enabled after the
confirm/cancel final actions, allowing stale interactions; update the confirm
and cancel handlers (the methods that currently disable only buttons) to also
disable the ResumeSeniorityOverrideSelect instance before editing the message or
stopping the view—locate the select by its class name
ResumeSeniorityOverrideSelect on the current View, set its disabled property to
True (or call view.remove_item/clear_items as appropriate), then perform the
same interaction.response.edit_message/update that you use to disable buttons so
the dropdown cannot be changed after finalization.
- Around line 693-703: The placeholder constructed in __init__ uses parsed_label
= _format_seniority_label(parsed_seniority) without length checks, which can
make the final placeholder exceed Discord's 150-char limit and break rendering;
fix by truncating or ellipsizing parsed_label to a safe length before building
the placeholder (ensure f"Override seniority (parsed: {parsed_label})" is <=150
chars), then pass the truncated label into super().__init__; update the logic in
the __init__ method where parsed_label is used and keep the truncation
centralized (e.g., a small helper or inline truncation) so all placeholder text
stays within the limit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0ffd168-f4bc-4e5c-aa4e-36fa92eca37a

📥 Commits

Reviewing files that changed from the base of the PR and between 67fb07d and 60e7d07.

📒 Files selected for processing (2)
  • apps/discord_bot/src/five08/discord_bot/cogs/crm.py
  • tests/unit/test_crm.py

Copilot AI review requested due to automatic review settings March 5, 2026 03:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

apps/discord_bot/src/five08/discord_bot/cogs/crm.py:2482

  • _extract_parsed_seniority is called twice on the same underlying data within a single request flow: once at line 2246 inside _build_resume_preview_embed (to add the "Parsed Seniority" embed field) and again at line 2482 in _run_resume_extract_and_preview (to decide whether to show the view and which seniority to pass in). Because _build_resume_preview_embed doesn't include the parsed seniority in its return value, the caller has to recompute it. This means the embed and the view are powered by two separate calls, creating a subtle maintenance risk: if one call site diverges (e.g., one passes a different value or there's a bug in one path), the displayed embed seniority and the view's dropdown could get out of sync. Consider extending the return type of _build_resume_preview_embed to tuple[discord.Embed, dict[str, Any], str | None] to include the already-computed parsed_seniority, and drop the extra call at line 2482.

        if isinstance(extracted_profile, dict):
            confidence = extracted_profile.get("confidence")
            source = extracted_profile.get("source")
            if confidence is not None or source:
                embed.add_field(
                    name="Extraction",
                    value=f"Source: `{source or 'unknown'}` | Confidence: `{confidence}`",
                    inline=False,
                )

        parsed_seniority = _extract_parsed_seniority(extracted_profile)
        if parsed_seniority:
            embed.add_field(
                name="Parsed Seniority",
                value=f"`{_format_seniority_label(parsed_seniority)}`",
                inline=True,
            )

        if link_member:
            embed.add_field(
                name="Discord Link",
                value=f"Will link contact to {link_member.mention}",
                inline=False,
            )

        profile_url = f"{self.base_url}/#Contact/view/{contact_id}"
        embed.add_field(name="🔗 CRM Profile", value=f"[View in CRM]({profile_url})")
        return embed, proposed_updates

    def _build_role_suggestions_embed(
        self,
        *,
        contact_name: str,
        extracted_profile: dict[str, Any],
        current_discord_roles: list[str] | None = None,
    ) -> discord.Embed | None:
        """Build a separate embed suggesting Discord roles to add based on resume data.

        Only ever suggests additions — roles are never removed.
        Never suggests roles in DISCORD_ROLES_NEVER_SUGGEST.
        """
        skills: list[str] = extracted_profile.get("skills") or []
        primary_roles: list[str] = extracted_profile.get("primary_roles") or []
        country: str | None = extracted_profile.get("address_country")

        technical = suggest_technical_discord_roles(skills, primary_roles)
        locality = suggest_locality_discord_roles(country)

        # Filter roles that should never be suggested
        technical = [r for r in technical if r not in DISCORD_ROLES_NEVER_SUGGEST]
        locality = [r for r in locality if r not in DISCORD_ROLES_NEVER_SUGGEST]

        # If we know the member's current roles, only show missing ones
        if current_discord_roles is not None:
            existing = set(current_discord_roles)
            technical = [r for r in technical if r not in existing]
            locality = [r for r in locality if r not in existing]

        if not technical and not locality:
            return None

        embed = discord.Embed(
            title="🏷️ Suggested Discord Roles",
            description=f"Roles to **add** for **{contact_name}** based on resume — never remove existing roles.",
            color=0x57F287,
        )

        if technical:
            embed.add_field(
                name="Technical",
                value=" ".join(f"`{r}`" for r in technical),
                inline=False,
            )
        if locality:
            embed.add_field(
                name="Locality",
                value=" ".join(f"`{r}`" for r in locality),
                inline=False,
            )

        return embed

    async def _run_resume_extract_and_preview(
        self,
        interaction: discord.Interaction,
        contact_id: str,
        contact_name: str,
        attachment_id: str,
        filename: str,
        link_member: discord.Member | None,
        *,
        action: str = "crm.upload_resume",
        status_message: str | None = None,
    ) -> None:
        """Kick off worker extraction and show confirmation preview."""
        action_name = action
        status_text = (
            status_message or "📥 Resume uploaded. Extracting profile fields now..."
        )
        try:
            job_id = await self._enqueue_resume_extract_job(
                contact_id=contact_id,
                attachment_id=attachment_id,
                filename=filename,
            )
        except Exception as exc:
            logger.error("Failed to enqueue resume extract job: %s", exc)
            self._audit_command(
                interaction=interaction,
                action=action_name,
                result="error",
                metadata={
                    "filename": filename,
                    "attachment_id": attachment_id,
                    "stage": "extract_enqueue",
                    "error": str(exc),
                },
                resource_type="crm_contact",
                resource_id=str(contact_id),
            )
            await interaction.followup.send(
                "⚠️ Resume uploaded, but extraction job could not be enqueued.",
                ephemeral=True,
            )
            return

        await interaction.followup.send(
            status_text,
            ephemeral=True,
        )

        try:
            job = await self._wait_for_backend_job_result(job_id)
        except Exception as exc:
            logger.error("Worker polling failed for job_id=%s error=%s", job_id, exc)
            self._audit_command(
                interaction=interaction,
                action=action_name,
                result="error",
                metadata={
                    "filename": filename,
                    "attachment_id": attachment_id,
                    "job_id": job_id,
                    "stage": "extract_polling",
                    "error": str(exc),
                },
                resource_type="crm_contact",
                resource_id=str(contact_id),
            )
            await interaction.followup.send(
                "⚠️ Resume uploaded, but extraction polling failed.",
                ephemeral=True,
            )
            return
        if not job:
            self._audit_command(
                interaction=interaction,
                action=action_name,
                result="error",
                metadata={
                    "filename": filename,
                    "attachment_id": attachment_id,
                    "job_id": job_id,
                    "stage": "extract_timeout",
                },
                resource_type="crm_contact",
                resource_id=str(contact_id),
            )
            await interaction.followup.send(
                "⚠️ Timed out waiting for extraction result. Try again in a moment.",
                ephemeral=True,
            )
            return

        status = str(job.get("status", "unknown"))
        if status != "succeeded":
            self._audit_command(
                interaction=interaction,
                action=action_name,
                result="error",
                metadata={
                    "filename": filename,
                    "attachment_id": attachment_id,
                    "job_id": job_id,
                    "stage": "extract_failed",
                    "job_status": status,
                    "last_error": str(job.get("last_error", "")),
                },
                resource_type="crm_contact",
                resource_id=str(contact_id),
            )
            await interaction.followup.send(
                f"❌ Extraction job failed (status: {status}). "
                f"Error: {job.get('last_error') or 'Unknown error'}",
                ephemeral=True,
            )
            return

        result = job.get("result")
        if not isinstance(result, dict):
            self._audit_command(
                interaction=interaction,
                action=action_name,
                result="error",
                metadata={
                    "filename": filename,
                    "attachment_id": attachment_id,
                    "job_id": job_id,
                    "stage": "extract_malformed_result",
                },
                resource_type="crm_contact",
                resource_id=str(contact_id),
            )
            await interaction.followup.send(
                "❌ Extraction result was empty or malformed.",
                ephemeral=True,
            )
            return

        if not result.get("success", False):
            self._audit_command(
                interaction=interaction,
                action=action_name,
                result="error",
                metadata={
                    "filename": filename,
                    "attachment_id": attachment_id,
                    "job_id": job_id,
                    "stage": "extract_unsuccessful",
                    "error": str(result.get("error", "")),
                },
                resource_type="crm_contact",
                resource_id=str(contact_id),
            )
            await interaction.followup.send(
                f"❌ Resume extraction failed: {result.get('error') or 'Unknown error'}",

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 776 to +786
@@ -699,6 +783,20 @@ def __init__(
self.contact_name = contact_name
self.proposed_updates = proposed_updates
self.link_discord = link_discord
self.parsed_seniority = parsed_seniority
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The seniority_override instance attribute is set in __init__ and updated in _set_seniority_override, but it is never read anywhere in the class or codebase. The actual effect of the override is applied directly through self.proposed_updates["cSeniority"] = value, making the seniority_override field redundant dead state. It should either be removed (since proposed_updates["cSeniority"] already tracks the override) or actively used — for example, to conditionally skip the confirm_updates guard or for audit logging purposes.

Copilot uses AI. Check for mistakes.
Comment on lines +719 to +734
discord.SelectOption(label="Senior", value="senior"),
discord.SelectOption(label="Staff", value="staff"),
]
super().__init__(
placeholder=placeholder,
min_values=1,
max_values=1,
options=options,
custom_id="resume_seniority_override",
)

async def callback(self, interaction: discord.Interaction) -> None:
view = self.view
if not isinstance(view, ResumeUpdateConfirmationView):
await interaction.response.send_message(
"❌ Unable to update seniority override.",
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ResumeSeniorityOverrideSelect.callback method has no test coverage. In particular, the error-handling branch at lines 721–726 (when self.view is not a ResumeUpdateConfirmationView) and the success path (interaction sends the formatted override message) are untested. Given that this file has comprehensive test coverage for other callbacks and view interactions, covering this callback would be consistent with the existing testing conventions.

Copilot uses AI. Check for mistakes.
@michaelmwu michaelmwu merged commit 44db77b into main Mar 5, 2026
5 checks passed
@michaelmwu michaelmwu deleted the michaelmwu/resume-seniority-override branch March 5, 2026 03:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants