Skip to content

Api reference

components.support.public.blueprint

support_blueprint module-attribute

support_blueprint = create_blueprint(
    name="support",
    import_name=__name__,
    template_folder=join(
        dirname(__file__), "..", "templates"
    ),
    cli_group="support",
)

components.support.public.commands

fix_fr_contact_request_data_2025_09_15

fix_fr_contact_request_data_2025_09_15

fix_fr_contact_request_data_2025_09_15(*, dry_run=True)

Following a run of import_fr_legacy_data that included a bug that set the US conversation id on contact_requests instead of the EU conversation id (but with the EU workspace id) when updating existing contact_requests, around 16000 contact_requests were affected (most of them from Sept 2024, around the time of the migration from the US workspace to the EU workspace, and when we had a bunch of conversations with both EU and US conversation ids).

This command fixes the data by setting the correct conversation id (the EU one) on the contact_requests.

Source code in components/support/public/commands/fix_fr_contact_request_data_2025_09_15.py
@support.command(requires_authentication=False)
@command_with_dry_run
def fix_fr_contact_request_data_2025_09_15(*, dry_run: bool = True) -> None:
    """
    Following a run of import_fr_legacy_data that included a bug that set the US conversation id
    on contact_requests instead of the EU conversation id (but with the EU workspace id) when updating
    existing contact_requests, around 16000 contact_requests were affected (most of them from Sept 2024,
    around the time of the migration from the US workspace to the EU workspace, and when we had a bunch
    of conversations with both EU and US conversation ids).

    This command fixes the data by setting the correct conversation id (the EU one) on the contact_requests.
    """
    from components.support.internal.business_logic.actions.fix_fr_contact_request_data import (
        fix_fr_contact_request_data,
    )

    try:
        fix_fr_contact_request_data()
        current_logger.info(
            "Successfully fixed contact requests" + (" (dry run)" if dry_run else "")
        )
    except Exception as e:
        current_logger.error(f"Failed to import intercom admins: {str(e)}")
        raise

import_fr_intercom_admins

import_fr_intercom_admins

import_fr_intercom_admins(
    *, dry_run=True, only_intercom_admin_id=None
)

Import intercom admins into support agents.

This command imports data from the intercom_admin model into support_agent and support_agent_company models. It maps the relevant fields and creates the necessary relationships.

Parameters:

Name Type Description Default
dry_run bool

Whether to run in dry-run mode (no database changes). Defaults to True.

True
only_intercom_admin_id str

If provided, only update the specified intercom admin. Defaults to None.

None
Source code in components/support/public/commands/import_fr_intercom_admins.py
@support.command(requires_authentication=False)
@click.option(
    "--only-intercom-admin-id",
    help="Optional intercom admin ID to only update a specific admin",
    required=False,
    type=str,
)
@command_with_dry_run
def import_fr_intercom_admins(
    *, dry_run: bool = True, only_intercom_admin_id: str | None = None
) -> None:
    """Import intercom admins into support agents.

    This command imports data from the intercom_admin model into support_agent and support_agent_company models.
    It maps the relevant fields and creates the necessary relationships.

    Args:
        dry_run (bool, optional): Whether to run in dry-run mode (no database changes). Defaults to True.
        only_intercom_admin_id (str, optional): If provided, only update the specified intercom admin. Defaults to None.
    """
    from components.support.internal.business_logic.actions.import_fr_intercom_admins.import_fr_intercom_admins import (
        import_fr_intercom_admins_to_support_agents,
    )

    try:
        import_fr_intercom_admins_to_support_agents(
            save=not dry_run, only_for_intercom_admin_id=only_intercom_admin_id
        )
        current_logger.info(
            "Successfully imported intercom admins" + (" (dry run)" if dry_run else "")
        )
    except Exception as e:
        current_logger.error(f"Failed to import intercom admins: {str(e)}")
        raise

import_fr_intercom_spe_inboxes

import_fr_intercom_spe_inboxes

import_fr_intercom_spe_inboxes(*, dry_run=True)

Import all existing intercom_spe_inbox entries into support.support_specialization.

Source code in components/support/public/commands/import_fr_intercom_spe_inboxes.py
@support.command(requires_authentication=False)
@command_with_dry_run
def import_fr_intercom_spe_inboxes(*, dry_run: bool = True) -> None:
    """Import all existing intercom_spe_inbox entries into support.support_specialization."""
    from components.support.internal.business_logic.actions.import_fr_intercom_admins.import_fr_intercom_spe_inboxes_to_support_specializations import (
        import_fr_intercom_spe_inboxes_to_support_specializations,
    )

    try:
        import_fr_intercom_spe_inboxes_to_support_specializations(save=not dry_run)
        current_logger.info(
            "Successfully imported intercom_spe_inboxes into support.support_specialization"
            + (" (dry run)" if dry_run else "")
        )
    except Exception as e:
        current_logger.error(f"Failed to import intercom_spe_inboxes: {str(e)}")
        raise

import_fr_legacy_quick_callback_data

run_import_fr_legacy_quick_callback_data

run_import_fr_legacy_quick_callback_data(
    dry_run, from_date, to_date, bulk_size
)

Import legacy quick callback data from CareCallback and CareConversationContext to create ContactRequest, QuickCallbackRequest and PhoneCall records.

Only imports CareCallback records where is_immediate_callback=True. This command should be run to migrate existing quick callback data to the new schema.

Source code in components/support/public/commands/import_fr_legacy_quick_callback_data.py
@support.command(requires_authentication=False)
@command_with_dry_run
@click.option(
    "--from-date",
    type=DateCli(),
    required=True,
    help="Get quick callback data from specified date",
)
@click.option(
    "--to-date",
    type=DateCli(),
    required=False,
    help="Get quick callback data to specified date, if not provided will default to today",
)
@click.option(
    "--bulk-size",
    type=int,
    required=False,
    help="Should we update/insert/delete entries progressively",
    default=2000,
)
def run_import_fr_legacy_quick_callback_data(
    dry_run: bool, from_date: date, to_date: date, bulk_size: int
) -> None:
    """
    Import legacy quick callback data from CareCallback and CareConversationContext
    to create ContactRequest, QuickCallbackRequest and PhoneCall records.

    Only imports CareCallback records where is_immediate_callback=True.
    This command should be run to migrate existing quick callback data to the new schema.
    """
    if not to_date:
        to_date = utctoday()
    import_fr_legacy_quick_callback_data(
        from_date=from_date,
        to_date=to_date,
        dry_run=dry_run,
        bulk_size=bulk_size,
    )

import_fr_support_agents_from_turing

import_fr_support_agents_from_turing

import_fr_support_agents_from_turing(dry_run, verbose)

Import support agents from a Turing table that's filled with data coming from a Google Sheet. In the future we want to replace this with support endpoint calls from eu_tools directly (which contains the source for Alan employees & external agents)

Source code in components/support/public/commands/import_fr_support_agents_from_turing.py
@support.command(requires_authentication=False)
@command_with_dry_run
@click.option(
    "--verbose",
    is_flag=True,
    help="Show verbose output. If not set, a progress bar will be shown.",
)
def import_fr_support_agents_from_turing(dry_run: bool, verbose: bool) -> None:
    """
    Import support agents from a Turing table that's filled with data coming
    from a Google Sheet. In the future we want to replace this with support endpoint calls
    from eu_tools directly (which contains the source for Alan employees & external agents)
    """
    from components.support.internal.business_logic.actions.import_fr_support_agents_from_turing import (
        import_fr_support_agents_from_turing,
    )

    import_fr_support_agents_from_turing(commit=not dry_run, verbose=verbose)

import_intercom_inboxes_command

import_intercom_inboxes

import_intercom_inboxes(
    *, dry_run=True, intercom_workspace_id
)

Import Intercom inboxes from a given Intercom workspace.

Parameters:

Name Type Description Default
dry_run bool

Whether to run in dry-run mode (no database changes). Defaults to True.

True
intercom_workspace_id str

The Intercom workspace ID to import inboxes from.

required
Source code in components/support/public/commands/import_intercom_inboxes_command.py
@support.command(requires_authentication=False)
@click.option(
    "--intercom-workspace-id",
    help="The Intercom workspace ID to import inboxes from.",
    required=True,
    type=str,
)
@command_with_dry_run
def import_intercom_inboxes(
    *, dry_run: bool = True, intercom_workspace_id: str
) -> None:
    """Import Intercom inboxes from a given Intercom workspace.

    Args:
        dry_run (bool, optional): Whether to run in dry-run mode (no database changes). Defaults to True.
        intercom_workspace_id (str): The Intercom workspace ID to import inboxes from.
    """
    from components.support.internal.business_logic.actions.import_intercom_inboxes import (
        import_support_agent_spes_from_workspace_inboxes,
    )

    try:
        import_support_agent_spes_from_workspace_inboxes(
            intercom_workspace_id=intercom_workspace_id, commit=not dry_run
        )
        current_logger.info(
            f"Successfully imported Intercom inboxes from workspace {intercom_workspace_id}"
            + (" (dry run)" if dry_run else "")
        )
    except Exception as e:
        current_logger.error(f"Failed to import Intercom inboxes: {str(e)}")
        raise

import_legacy_contact_request_data

run_import_be_legacy_contact_request_data

run_import_be_legacy_contact_request_data(
    dry_run, from_date, to_date
)

To globalize the Assigner, we need to import all existing contact requests from Belgium. We already are creating new contact requests for new convos. This command should be run regularly automatically until we stop needing it.

Source code in components/support/public/commands/import_legacy_contact_request_data.py
@support.command(requires_authentication=False)
@command_with_dry_run
@click.option(
    "--from-date",
    type=DateCli(),
    required=True,
    help="Get data from specified date",
)
@click.option(
    "--to-date",
    type=DateCli(),
    required=False,
    help="Get data to specified date, if not provided will default to today",
)
def run_import_be_legacy_contact_request_data(
    dry_run: bool,
    from_date: date,
    to_date: date,
) -> None:
    """
    To globalize the Assigner, we need to import all existing contact requests from Belgium.
    We already are creating new contact requests for new convos.
    This command should be run regularly automatically until we stop needing it.
    """
    if not to_date:
        to_date = utctoday()
    import_be_legacy_contact_request_data(
        from_date=from_date,
        to_date=to_date,
        dry_run=dry_run,
    )

run_import_fr_legacy_contact_request_data

run_import_fr_legacy_contact_request_data(
    dry_run, from_date, to_date, bulk_size
)

To globalize the Assigner, we need to import all existing contact requests from France. We already are creating new contact requests for new convos. This command should be run regularly automatically until we stop needing it.

Source code in components/support/public/commands/import_legacy_contact_request_data.py
@support.command(requires_authentication=False)
@command_with_dry_run
@click.option(
    "--from-date",
    type=DateCli(),
    required=True,
    help="Get data from specified date",
)
@click.option(
    "--to-date",
    type=DateCli(),
    required=False,
    help="Get data to specified date, if not provided will default to today",
)
@click.option(
    "--bulk-size",
    type=int,
    required=False,
    help="Should we update/insert/delete entries progressively",
    default=2000,
)
def run_import_fr_legacy_contact_request_data(
    dry_run: bool, from_date: date, to_date: date, bulk_size: int
) -> None:
    """
    To globalize the Assigner, we need to import all existing contact requests from France.
    We already are creating new contact requests for new convos.
    This command should be run regularly automatically until we stop needing it.
    """
    if not to_date:
        to_date = utctoday()
    import_fr_legacy_contact_request_data(
        from_date=from_date,
        to_date=to_date,
        dry_run=dry_run,
        bulk_size=bulk_size,
    )

import_support_agents_from_employees_command

import_support_agents_from_employees

import_support_agents_from_employees(dry_run, verbose)

Import support agents from employees into SupportAgent and SupportAgentWorkspaceAffectation. Creates or updates SupportAgent entries and their associated workspace affectations.

Source code in components/support/public/commands/import_support_agents_from_employees_command.py
@support.command(requires_authentication=False)
@command_with_dry_run
@click.option(
    "--verbose",
    is_flag=True,
    help="Show verbose output. If not set, a progress bar will be shown.",
)
def import_support_agents_from_employees(dry_run: bool, verbose: bool) -> None:
    """
    Import support agents from employees into SupportAgent and SupportAgentWorkspaceAffectation.
    Creates or updates SupportAgent entries and their associated workspace affectations.
    """
    from components.support.internal.business_logic.actions.import_support_agents_from_employees import (
        import_support_agents_from_employees,
    )

    if not _is_employee_import_command_active(get_current_app_name()):
        current_logger.info(
            "Not running import_support_agents_from_employees - feature flag for country is disabled"
        )
        return

    try:
        import_support_agents_from_employees(commit=not dry_run, verbose=verbose)
        current_logger.info(
            "Successfully imported support agents from employees"
            + (" (dry run)" if dry_run else "")
        )
    except Exception as e:
        current_logger.error(f"Failed to import support agents: {str(e)}")
        raise

intercom_fallback

intercom_new_conversation_fallback

process_new_conversations_not_sent_by_intercom
process_new_conversations_not_sent_by_intercom(
    dry_run, lookback_minutes=10
)

Process new conversations not sent by Intercom.

This command gets a list of conversations from Intercom that have been created in the past {lookback-minutes} and processes them to make sure we haven't missed any in case of Intercom errors (webhook not fired, Alan service temporarily unavailable, etc)

Parameters:

Name Type Description Default
dry_run bool

Whether to run in dry-run mode (no database changes). Defaults to True.

required
lookback_minutes int

If provided, get conversations from Intercom created in the last X minutes. Defaults to 10.

10
Source code in components/support/public/commands/intercom_fallback/intercom_new_conversation_fallback.py
@support.command(requires_authentication=False)
@click.option(
    "-l",
    "--lookback-minutes",
    default=10,
    type=int,
    help="Lookback x minutes from now. Defaults to 10min.",
)
@command_with_dry_run
def process_new_conversations_not_sent_by_intercom(
    dry_run: bool,
    lookback_minutes: int = 10,
) -> None:
    """Process new conversations not sent by Intercom.

    This command gets a list of conversations from Intercom that have been created in the past
    {lookback-minutes} and processes them to make sure we haven't missed any in case of
    Intercom errors (webhook not fired, Alan service temporarily unavailable, etc)

    Args:
        dry_run (bool, optional): Whether to run in dry-run mode (no database changes). Defaults to True.
        lookback_minutes (int, optional): If provided, get conversations from Intercom created in the last X minutes. Defaults to 10.
    """
    import datetime
    import time

    from components.support.internal.business_logic.actions.intercom_fallback.process_new_conversation_fallback import (
        find_and_process_new_intercom_conversations_not_sent_by_intercom,
        is_fallback_command_active,
    )

    current_app_name = get_current_app_name()
    if not is_fallback_command_active(current_app_name):
        return
    # give ourselves a buffer of 1 min to process new conversations "normally"
    buffer = 60  # seconds
    end_unix_time = int(time.mktime(datetime.datetime.now().timetuple())) - buffer
    start_unix_time = end_unix_time - (lookback_minutes * 60)
    intercom_client = get_care_intercom_client(current_app_name)
    find_and_process_new_intercom_conversations_not_sent_by_intercom(
        start_unix_time=start_unix_time,
        end_unix_time=end_unix_time,
        intercom_client=intercom_client,
        dry_run=dry_run,
    )

intercom_update_fallback

sync_conversation_updates_not_sent_by_intercom
sync_conversation_updates_not_sent_by_intercom(
    dry_run, end_offset_seconds=0, lookback_hours=6
)

Process conversation updates not sent by Intercom.

This command gets a list of conversations from Intercom that have been updated in the past {lookback-hours} and processes them (syncs the DB) to make sure we haven't missed any in case of Intercom errors (webhook not fired, Alan service temporarily unavailable, etc)

Parameters:

Name Type Description Default
dry_run bool

Whether to run in dry-run mode (no database changes). Defaults to True.

required
lookback_hours int

If provided, get conversations from Intercom updated in the last X hours. Defaults to 6.

6
end_offset_seconds int

start at datetime.now() - end-offset-seconds. This allows running the command from cron, disregarding recent conversations. Defaults to 0.

0
Source code in components/support/public/commands/intercom_fallback/intercom_update_fallback.py
@support.command(requires_authentication=False)
@click.option(
    "-o",
    "--end-offset-seconds",
    default=0,
    type=int,
    help="when no end-unix-time is given, start at datetime.now() - end-offset-seconds. This allows running the command from cron, disregarding recent conversations. Defaults to 0.",
)
@click.option(
    "-l",
    "--lookback-hours",
    default=6,
    type=int,
    help="Lookback x hours from computed end time. Defaults to 6h.",
)
@command_with_dry_run
def sync_conversation_updates_not_sent_by_intercom(
    dry_run: bool,
    end_offset_seconds: int = 0,
    lookback_hours: int = 6,
) -> None:
    """Process conversation updates not sent by Intercom.

    This command gets a list of conversations from Intercom that have been updated in the past
    {lookback-hours} and processes them (syncs the DB) to make sure we haven't missed any in case of
    Intercom errors (webhook not fired, Alan service temporarily unavailable, etc)

    Args:
        dry_run (bool, optional): Whether to run in dry-run mode (no database changes). Defaults to True.
        lookback_hours (int, optional): If provided, get conversations from Intercom updated in the last X hours. Defaults to 6.
        end_offset_seconds (int, optional): start at datetime.now() - end-offset-seconds. This allows running the command from cron, disregarding recent conversations. Defaults to 0.
    """
    import datetime
    import time

    from components.support.internal.business_logic.actions.intercom_fallback.update_conversation_fallback import (
        find_and_sync_db_for_intercom_conversation_updates_not_sent_by_intercom,
    )

    current_app_name = get_current_app_name()
    if not is_fallback_command_active(current_app_name):
        return

    end_unix_time = (
        int(time.mktime(datetime.datetime.now().timetuple())) - end_offset_seconds
    )
    start_unix_time = end_unix_time - (lookback_hours * 3600)
    intercom_client = get_care_intercom_client(current_app_name)
    find_and_sync_db_for_intercom_conversation_updates_not_sent_by_intercom(
        start_unix_time=start_unix_time,
        end_unix_time=end_unix_time,
        intercom_client=intercom_client,
        dry_run=dry_run,
    )

synchronize_support_specializations

synchronize_support_specializations

synchronize_support_specializations(dry_run)

Synchronize support specializations from Intercom to our backend.

Source code in components/support/public/commands/synchronize_support_specializations.py
@support.command(requires_authentication=False)
@command_with_dry_run
@do_not_run_in_prod
def synchronize_support_specializations(dry_run: bool) -> None:
    """
    Synchronize support specializations from Intercom to our backend.
    """
    from components.support.internal.business_logic.actions.synchronize_support_specializations_actions import (
        synchronize_support_specializations,
    )

    synchronize_support_specializations(commit=not dry_run)

components.support.public.data

assigner_sql_queries

BE_ASSIGNER_SQL_QUERY module-attribute

BE_ASSIGNER_SQL_QUERY = "\n    WITH support_agents_for_computation AS (\n        SELECT\n            sa.id as support_agent_id,\n            sa.intercom_admin_id,\n            sawa.level,\n            sawa.intercom_workspace_id,\n            --Roles are now stored in an array, so we check for the presence of a role in the array.\n            'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '17:00:00'\n                THEN 'eligible_for_async' = ANY(sawa.roles)\n                ELSE True\n            END as is_eligible_for_async,\n            NOT sa.is_external_admin AS has_role_uce,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '17:00:00'\n                THEN 'eligible_for_callback' = ANY(sawa.roles)\n                ELSE False\n            END as has_role_callback,\n            'en' = ANY(sa.spoken_languages) as speaks_english,\n            'fr' = ANY(sa.spoken_languages) as speaks_french,\n            'nl' = ANY(sa.spoken_languages) as speaks_dutch\n        FROM support.support_agent as sa\n        JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n        WHERE sa.id = (:support_agent_id)  -- the admin requesting an assignment\n    ),\n    uce_specialisations_mapping AS (\n      SELECT sa.intercom_admin_id\n      , ss.intercom_inbox_id\n      , sasms.score\n      FROM support_agents_for_computation sa\n      LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n      LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n      LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n      WHERE COALESCE(sasms.score, 0.2) != 0\n    ),\n    prefiltered_contact_request_backlog AS (\n        SELECT\n            DISTINCT ON (cr.intercom_conversation_id)\n            cr.id as contact_request_id,\n            cr.intercom_conversation_id,\n            cris.assigned_intercom_inbox_id as inbox_id,\n            cris.last_inbox_assignment_by,\n            cris.waiting_since,\n            greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n            cris.last_admin_reply_at,\n            cr.source_type as type,\n            cram.recommended_admin_id,\n            cris.started_at,\n            cris.state,\n            cris.assigned_intercom_admin_id as admin_id,\n            cr.created_at,\n            -- using tags to compute properties of the conversations\n            CASE\n                WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n                WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n                WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n                WHEN cr.source_type in ('async_conversation_request', 'unknown') THEN 'SLA[12h]'\n                ELSE 'SLA[5min]'\n            END AS priority_level,\n            CASE\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n                ELSE 1\n            END AS conversation_workforce_level,\n            tags.*\n        FROM support.contact_request cr\n        JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n        LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n        LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n        CROSS JOIN LATERAL (\n            SELECT\n                MAX(CASE WHEN crt.name in ('ROLE[CALLBACK]', '#OTHER - CARE TEAM - Eligible call-back request') THEN crt.name END) AS callback_tag,\n                MAX(CASE WHEN crt.name = 'App language Dutch' THEN crt.name END) AS dutch_tag,\n                MAX(CASE WHEN crt.name = 'App language English' THEN crt.name END) AS english_tag,\n                MAX(CASE WHEN crt.name = 'App language French' THEN crt.name END) AS french_tag,\n                MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n                MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag,\n                MAX(CASE WHEN crt.name = 'SENSITIVE' THEN crt.name END) AS uce_reserved_tag\n            FROM support.contact_request_tag crt\n            WHERE crt.contact_request_id = cr.id\n        ) tags\n        WHERE\n            cr.intercom_workspace_id = (:workspace_id)\n            AND cris.state = 'open'\n            AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n        ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n    ),\n\n    eligible_contact_request_backlog_with_priority_level AS (\n        SELECT\n            pcrb.contact_request_id,\n            pcrb.intercom_conversation_id,\n            pcrb.inbox_id,\n            pcrb.last_inbox_assignment_by,\n            pcrb.time_reference_for_priority_score,\n            pcrb.priority_level,\n            pcrb.conversation_workforce_level,\n            sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n            CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n            pcrb.sla_tag,\n            pcrb.started_at\n        FROM prefiltered_contact_request_backlog pcrb\n        CROSS JOIN support_agents_for_computation sa\n        WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n            -- reduce scope for agents depending on their sync/async eligibility configuration\n            AND (\n                    (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                    OR\n                    (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                    OR\n                    (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n            )\n            -- reduce scope for agents not speaking English when not ASYNC\n            AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n            -- reduce scope for agents not speaking Dutch when not ASYNC\n            AND (COALESCE(sa.speaks_dutch, True) OR pcrb.dutch_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n            -- reduce scope for agents not speaking French when not ASYNC\n            AND (COALESCE(sa.speaks_french, True) OR pcrb.french_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n    ),\n    eligible_contact_request_backlog_with_context AS (\n        SELECT\n            ecrb.contact_request_id,\n            ecrb.intercom_conversation_id,\n            ecrb.priority_level,\n            sa.intercom_admin_id,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (5 - 2)\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (30 - 2)\n                ELSE --  priority_level = 'SLA[12h]'\n                        (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (12 * 60 - 2)\n            END AS sla_score,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n                ELSE\n                    count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n            END AS sla_breached,\n            distance_workforce_level_admin_conversation,\n            CASE\n                WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n                WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n                ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n            END AS workforce_level_matching_score,\n            ecrb.inbox_id,\n            ss.intercom_inbox_name as inbox_name,\n            ecrb.last_inbox_assignment_by,\n            ecrb.uce_member_matching_score,\n            ecrb.time_reference_for_priority_score,\n            ecrb.sla_tag,\n            ecrb.conversation_workforce_level,\n            CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_priority_level ecrb\n        INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n        CROSS JOIN support_agents_for_computation sa\n        WHERE\n            sa.level - ecrb.conversation_workforce_level >= 0\n            AND CASE\n                    WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n                    ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n                END\n    ),\n    eligible_convos_with_scores AS (\n        SELECT\n            ec.contact_request_id,\n            ec.intercom_conversation_id,\n            ec.inbox_id,\n            ec.inbox_name,\n            NULL as reserved_for_external_agent_platform,\n            ec.time_reference_for_priority_score,\n            m.score AS expertise_matching_score,\n            m.score AS raw_expertise_matching_score,\n            POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n            POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n            uce_member_matching_score,\n            COALESCE(\n                    power(m.score, (:power_weight_a))\n                    * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n                    * POWER(workforce_level_matching_score, (:power_weight_d))\n                    * uce_member_matching_score\n                , 0) AS assignment_score,\n            sla_score,\n            sla_breached,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n            priority_level,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n            null as n_rejections,\n            ec.sla_tag,\n            ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n            ec.additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_context ec\n        LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n        WHERE crae.contact_request_id IS NULL\n          AND COALESCE(m.score, 0.2) >= 0\n    ),\n    backlog_stats AS MATERIALIZED (\n        SELECT\n            count(*) as n_assignable_convos,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n        FROM eligible_convos_with_scores\n    )\n    SELECT *, 1.0 AS shift_matching_score\n    FROM eligible_convos_with_scores\n    CROSS JOIN backlog_stats stats\n    ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score\n    LIMIT 10;\n    "

BE_RANKED_SQL_QUERY module-attribute

BE_RANKED_SQL_QUERY = "\n    WITH support_agents_for_computation AS (\n        SELECT\n            sa.id as support_agent_id,\n            sa.intercom_admin_id,\n            sawa.level,\n            sawa.intercom_workspace_id,\n            --Roles are now stored in an array, so we check for the presence of a role in the array.\n            'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '17:00:00'\n                THEN 'eligible_for_async' = ANY(sawa.roles)\n                ELSE True\n            END as is_eligible_for_async,\n            NOT sa.is_external_admin AS has_role_uce,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '17:00:00'\n                THEN 'eligible_for_callback' = ANY(sawa.roles)\n                ELSE False\n            END as has_role_callback,\n            'en' = ANY(sa.spoken_languages) as speaks_english,\n            'fr' = ANY(sa.spoken_languages) as speaks_french,\n            'nl' = ANY(sa.spoken_languages) as speaks_dutch\n        FROM support.support_agent as sa\n        JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n        WHERE sa.id = (:support_agent_id)  -- the admin requesting an assignment\n    ),\n    uce_specialisations_mapping AS (\n      SELECT sa.intercom_admin_id\n      , ss.intercom_inbox_id\n      , sasms.score\n      FROM support_agents_for_computation sa\n      LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n      LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n      LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n      WHERE COALESCE(sasms.score, 0.2) != 0\n    ),\n    prefiltered_contact_request_backlog AS (\n        SELECT\n            DISTINCT ON (cr.intercom_conversation_id)\n            cr.id as contact_request_id,\n            cr.intercom_conversation_id,\n            cris.assigned_intercom_inbox_id as inbox_id,\n            cris.last_inbox_assignment_by,\n            cris.waiting_since,\n            greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n            cris.last_admin_reply_at,\n            cr.source_type as type,\n            cram.recommended_admin_id,\n            cris.started_at,\n            cris.state,\n            cris.assigned_intercom_admin_id as admin_id,\n            cr.created_at,\n            -- using tags to compute properties of the conversations\n            CASE\n                WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n                WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n                WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n                WHEN cr.source_type in ('async_conversation_request', 'unknown') THEN 'SLA[12h]'\n                ELSE 'SLA[5min]'\n            END AS priority_level,\n            CASE\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n                ELSE 1\n            END AS conversation_workforce_level,\n            tags.*\n        FROM support.contact_request cr\n        JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n        LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n        LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n        CROSS JOIN LATERAL (\n            SELECT\n                MAX(CASE WHEN crt.name in ('ROLE[CALLBACK]', '#OTHER - CARE TEAM - Eligible call-back request') THEN crt.name END) AS callback_tag,\n                MAX(CASE WHEN crt.name = 'App language Dutch' THEN crt.name END) AS dutch_tag,\n                MAX(CASE WHEN crt.name = 'App language English' THEN crt.name END) AS english_tag,\n                MAX(CASE WHEN crt.name = 'App language French' THEN crt.name END) AS french_tag,\n                MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n                MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag,\n                MAX(CASE WHEN crt.name = 'SENSITIVE' THEN crt.name END) AS uce_reserved_tag\n            FROM support.contact_request_tag crt\n            WHERE crt.contact_request_id = cr.id\n        ) tags\n        WHERE\n            cr.intercom_workspace_id = (:workspace_id)\n            AND cris.state = 'open'\n            AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n        ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n    ),\n\n    eligible_contact_request_backlog_with_priority_level AS (\n        SELECT\n            pcrb.contact_request_id,\n            pcrb.intercom_conversation_id,\n            pcrb.inbox_id,\n            pcrb.last_inbox_assignment_by,\n            pcrb.time_reference_for_priority_score,\n            pcrb.priority_level,\n            pcrb.conversation_workforce_level,\n            sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n            CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n            pcrb.sla_tag,\n            pcrb.started_at\n        FROM prefiltered_contact_request_backlog pcrb\n        CROSS JOIN support_agents_for_computation sa\n        WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n            -- reduce scope for agents depending on their sync/async eligibility configuration\n            AND (\n                    (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                    OR\n                    (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                    OR\n                    (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n            )\n            -- reduce scope for agents not speaking English when not ASYNC\n            AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n            -- reduce scope for agents not speaking Dutch when not ASYNC\n            AND (COALESCE(sa.speaks_dutch, True) OR pcrb.dutch_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n            -- reduce scope for agents not speaking French when not ASYNC\n            AND (COALESCE(sa.speaks_french, True) OR pcrb.french_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n    ),\n    eligible_contact_request_backlog_with_context AS (\n        SELECT\n            ecrb.contact_request_id,\n            ecrb.intercom_conversation_id,\n            ecrb.priority_level,\n            sa.intercom_admin_id,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (5 - 2)\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (30 - 2)\n                ELSE --  priority_level = 'SLA[12h]'\n                        (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (12 * 60 - 2)\n            END AS sla_score,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n                ELSE\n                    count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n            END AS sla_breached,\n            distance_workforce_level_admin_conversation,\n            CASE\n                WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n                WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n                ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n            END AS workforce_level_matching_score,\n            ecrb.inbox_id,\n            ss.intercom_inbox_name as inbox_name,\n            ecrb.last_inbox_assignment_by,\n            ecrb.uce_member_matching_score,\n            ecrb.time_reference_for_priority_score,\n            ecrb.sla_tag,\n            ecrb.conversation_workforce_level,\n            CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_priority_level ecrb\n        INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n        CROSS JOIN support_agents_for_computation sa\n        WHERE\n            sa.level - ecrb.conversation_workforce_level >= 0\n            AND CASE\n                    WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n                    ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n                END\n    ),\n    eligible_convos_with_scores AS (\n        SELECT\n            ec.contact_request_id,\n            ec.intercom_conversation_id,\n            ec.inbox_id,\n            ec.inbox_name,\n            NULL as reserved_for_external_agent_platform,\n            ec.time_reference_for_priority_score,\n            m.score AS expertise_matching_score,\n            m.score AS raw_expertise_matching_score,\n            POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n            POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n            uce_member_matching_score,\n            COALESCE(\n                    power(m.score, (:power_weight_a))\n                    * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n                    * POWER(workforce_level_matching_score, (:power_weight_d))\n                    * uce_member_matching_score\n                , 0) AS assignment_score,\n            sla_score,\n            sla_breached,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n            priority_level,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n            null as n_rejections,\n            ec.sla_tag,\n            ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n            ec.additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_context ec\n        LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n        WHERE crae.contact_request_id IS NULL\n          AND COALESCE(m.score, 0.2) >= 0\n    ),\n    backlog_stats AS MATERIALIZED (\n        SELECT\n            count(*) as n_assignable_convos,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n        FROM eligible_convos_with_scores\n    )\n    SELECT\n        row_number() OVER (ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS assignment_rank\n        , (:support_agent_id)\n        , *\n        , row_number() OVER (ORDER BY intercom_conversation_id = (:conversation_id) DESC, assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS row_nb\n    FROM eligible_convos_with_scores\n    CROSS JOIN backlog_stats stats\n    ORDER BY row_nb, assignment_score DESC NULLS LAST, time_reference_for_priority_score\n    LIMIT 10;\n    "

ES_ASSIGNER_SQL_QUERY module-attribute

ES_ASSIGNER_SQL_QUERY = "\n    WITH support_agents_for_computation AS (\n        SELECT\n            sa.id as support_agent_id,\n            sa.intercom_admin_id,\n            sawa.level,\n            sawa.intercom_workspace_id,\n            --Roles are now stored in an array, so we check for the presence of a role in the array.\n            'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n                THEN 'eligible_for_async' = ANY(sawa.roles)\n                ELSE True\n            END as is_eligible_for_async,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n                THEN 'eligible_for_callback' = ANY(sawa.roles)\n                ELSE False\n            END as has_role_callback, -- not used yet in SP yet but keep it for consistency\n            NOT sa.is_external_admin AS has_role_uce,\n            'en' = ANY(sa.spoken_languages) as speaks_english,\n            'es' = ANY(sa.spoken_languages) as speaks_spanish,\n            'ca' = ANY(sa.spoken_languages) as speaks_catalan\n        FROM support.support_agent as sa\n        JOIN support.support_agent_workspace_affectation sawa on sa.id = sawa.support_agent_id\n        WHERE sa.id = (:support_agent_id)  -- the admin requesting an assignment\n            AND sawa.intercom_workspace_id = (:workspace_id)\n    ),\n    uce_specialisations_mapping AS (\n        SELECT sa.intercom_admin_id\n        , ss.intercom_inbox_id\n        , sasms.score\n        FROM support_agents_for_computation sa\n        LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n        LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n        LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n        WHERE COALESCE(sasms.score, 0.2) != 0\n    ),\n    prefiltered_contact_request_backlog AS (\n        SELECT\n            DISTINCT ON (cr.intercom_conversation_id)\n            cr.id as contact_request_id,\n            cr.intercom_conversation_id,\n            cris.assigned_intercom_inbox_id as inbox_id,\n            cris.last_inbox_assignment_by,\n            cris.waiting_since,\n            greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n            cris.last_admin_reply_at,\n            cr.source_type as type,\n            cram.recommended_admin_id,\n            cris.started_at,\n            cris.state,\n            cris.assigned_intercom_admin_id as admin_id,\n            cr.created_at,\n            -- using tags to compute properties of the conversations\n            CASE\n                WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n                WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n                WHEN tags.sla_tag = 'SLA[30min]' THEN 'SLA[30min]'\n                WHEN cr.source_type = 'async_conversation_request' THEN 'SLA[12h]'\n                ELSE 'SLA[5min]'\n            END AS priority_level,\n            CASE\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' THEN 3\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n                ELSE 1\n            END AS conversation_workforce_level,\n            tags.*\n        FROM support.contact_request cr\n        JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n        LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n        LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n        CROSS JOIN LATERAL (\n            SELECT\n                MAX(CASE WHEN crt.name = 'ROLE[SPANISH]' THEN crt.name END) AS spanish_tag,\n                MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag,\n                MAX(CASE WHEN crt.name = 'ROLE[CATALAN]' THEN crt.name END) AS catalan_tag,\n                MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n                MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n            FROM support.contact_request_tag crt\n            WHERE crt.contact_request_id = cr.id\n        ) tags\n        WHERE\n            cr.intercom_workspace_id = (:workspace_id)\n            AND cris.state = 'open'\n            AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n        ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n    ),\n    eligible_contact_request_backlog_with_priority_level AS (\n        SELECT\n            pcrb.contact_request_id,\n            pcrb.intercom_conversation_id,\n            pcrb.inbox_id,\n            pcrb.last_inbox_assignment_by,\n            pcrb.time_reference_for_priority_score,\n            pcrb.priority_level,\n            pcrb.conversation_workforce_level,\n            sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n            CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n            pcrb.sla_tag,\n            pcrb.started_at\n        FROM prefiltered_contact_request_backlog pcrb\n        CROSS JOIN support_agents_for_computation sa\n        WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n            -- reduce scope for agents depending on their sync/async eligibility configuration\n            AND (\n                    (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]')\n                    OR\n                    (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]')\n            )\n            -- reduce scope for agents not speaking English\n            AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n            -- reduce scope for agents not speaking Spanish\n            AND (COALESCE(sa.speaks_spanish, True) OR pcrb.spanish_tag IS NULL)\n            -- reduce scope for agents not speaking Catalan\n            AND (COALESCE(sa.speaks_catalan, True) OR pcrb.catalan_tag IS NULL)\n    ),\n    eligible_contact_request_backlog_with_context AS (\n        SELECT\n            ecrb.contact_request_id,\n            ecrb.intercom_conversation_id,\n            ecrb.priority_level,\n            sa.intercom_admin_id,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (5 - 2)\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (30 - 2)\n                ELSE --  priority_level = 'SLA[12h]'\n                        (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (12 * 60 - 2)\n            END AS sla_score,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n                ELSE\n                    count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n            END AS sla_breached,\n            distance_workforce_level_admin_conversation,\n            CASE\n                WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n                WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n                ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n            END AS workforce_level_matching_score,\n            ecrb.inbox_id,\n            ss.intercom_inbox_name as inbox_name,\n            ecrb.last_inbox_assignment_by,\n            ecrb.uce_member_matching_score,\n            ecrb.time_reference_for_priority_score,\n            ecrb.sla_tag,\n            ecrb.conversation_workforce_level,\n            CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_priority_level ecrb\n        INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n        CROSS JOIN support_agents_for_computation sa\n        WHERE\n            sa.level - ecrb.conversation_workforce_level >= 0\n            AND CASE\n                    WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n                    ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n                END\n    ),\n    eligible_convos_with_scores AS (\n        SELECT\n            ec.contact_request_id,\n            ec.intercom_conversation_id,\n            ec.inbox_id,\n            ec.inbox_name,\n            NULL AS reserved_for_external_agent_platform,\n            ec.time_reference_for_priority_score,\n            m.score AS expertise_matching_score,\n            m.score AS raw_expertise_matching_score,\n            POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n            POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n            uce_member_matching_score,\n            COALESCE(\n                    power(m.score, ((:power_weight_a)))\n                    * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n                    * POWER(workforce_level_matching_score, (:power_weight_d))\n                    * uce_member_matching_score\n                , 0) AS assignment_score,\n            sla_score,\n            sla_breached,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n            priority_level,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n            null as n_rejections,\n            ec.sla_tag,\n            ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n            ec.additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_context ec\n        LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n        WHERE crae.contact_request_id IS NULL\n        AND COALESCE(m.score, 0.2) >= 0\n    ),\n    backlog_stats AS MATERIALIZED (\n        SELECT\n            count(*) as n_assignable_convos,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n        FROM eligible_convos_with_scores\n    )\n    SELECT *, 1.0 AS shift_matching_score\n    FROM eligible_convos_with_scores\n    CROSS JOIN backlog_stats stats\n    ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score\n    LIMIT 10;\n    "

ES_RANKED_SQL_QUERY module-attribute

ES_RANKED_SQL_QUERY = "\n    WITH support_agents_for_computation AS (\n        SELECT\n            sa.id as support_agent_id,\n            sa.intercom_admin_id,\n            sawa.level,\n            sawa.intercom_workspace_id,\n            --Roles are now stored in an array, so we check for the presence of a role in the array.\n            'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n                THEN 'eligible_for_async' = ANY(sawa.roles)\n                ELSE True\n            END as is_eligible_for_async,\n            CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n                THEN 'eligible_for_callback' = ANY(sawa.roles)\n                ELSE False\n            END as has_role_callback, -- not used yet in SP yet but keep it for consistency\n            NOT sa.is_external_admin AS has_role_uce,\n            'en' = ANY(sa.spoken_languages) as speaks_english,\n            'es' = ANY(sa.spoken_languages) as speaks_spanish,\n            'ca' = ANY(sa.spoken_languages) as speaks_catalan\n        FROM support.support_agent as sa\n        JOIN support.support_agent_workspace_affectation sawa on sa.id = sawa.support_agent_id\n        WHERE sa.id = (:support_agent_id)  -- the admin requesting an assignment\n            AND sawa.intercom_workspace_id = (:workspace_id)\n    ),\n    uce_specialisations_mapping AS (\n        SELECT sa.intercom_admin_id\n        , ss.intercom_inbox_id\n        , sasms.score\n        FROM support_agents_for_computation sa\n        LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n        LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n        LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n        WHERE COALESCE(sasms.score, 0.2) != 0\n    ),\n    prefiltered_contact_request_backlog AS (\n        SELECT\n            DISTINCT ON (cr.intercom_conversation_id)\n            cr.id as contact_request_id,\n            cr.intercom_conversation_id,\n            cris.assigned_intercom_inbox_id as inbox_id,\n            cris.last_inbox_assignment_by,\n            cris.waiting_since,\n            greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n            cris.last_admin_reply_at,\n            cr.source_type as type,\n            cram.recommended_admin_id,\n            cris.started_at,\n            cris.state,\n            cris.assigned_intercom_admin_id as admin_id,\n            cr.created_at,\n            -- using tags to compute properties of the conversations\n            CASE\n                WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n                WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n                WHEN tags.sla_tag = 'SLA[30min]' THEN 'SLA[30min]'\n                WHEN cr.source_type = 'async_conversation_request' THEN 'SLA[12h]'\n                ELSE 'SLA[5min]'\n            END AS priority_level,\n            CASE\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' THEN 3\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n                ELSE 1\n            END AS conversation_workforce_level,\n            tags.*\n        FROM support.contact_request cr\n        JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n        LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n        LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n        CROSS JOIN LATERAL (\n            SELECT\n                MAX(CASE WHEN crt.name = 'ROLE[SPANISH]' THEN crt.name END) AS spanish_tag,\n                MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag,\n                MAX(CASE WHEN crt.name = 'ROLE[CATALAN]' THEN crt.name END) AS catalan_tag,\n                MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n                MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n            FROM support.contact_request_tag crt\n            WHERE crt.contact_request_id = cr.id\n        ) tags\n        WHERE\n            cr.intercom_workspace_id = (:workspace_id)\n            AND cris.state = 'open'\n            AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n        ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n    ),\n    eligible_contact_request_backlog_with_priority_level AS (\n        SELECT\n            pcrb.contact_request_id,\n            pcrb.intercom_conversation_id,\n            pcrb.inbox_id,\n            pcrb.last_inbox_assignment_by,\n            pcrb.time_reference_for_priority_score,\n            pcrb.priority_level,\n            pcrb.conversation_workforce_level,\n            sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n            CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n            pcrb.sla_tag,\n            pcrb.started_at\n        FROM prefiltered_contact_request_backlog pcrb\n        CROSS JOIN support_agents_for_computation sa\n        WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n            -- reduce scope for agents depending on their sync/async eligibility configuration\n            AND (\n                    (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]')\n                    OR\n                    (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]')\n            )\n            -- reduce scope for agents not speaking English\n            AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n            -- reduce scope for agents not speaking Spanish\n            AND (COALESCE(sa.speaks_spanish, True) OR pcrb.spanish_tag IS NULL)\n            -- reduce scope for agents not speaking Catalan\n            AND (COALESCE(sa.speaks_catalan, True) OR pcrb.catalan_tag IS NULL)\n    ),\n    eligible_contact_request_backlog_with_context AS (\n        SELECT\n            ecrb.contact_request_id,\n            ecrb.intercom_conversation_id,\n            ecrb.priority_level,\n            sa.intercom_admin_id,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (5 - 2)\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (30 - 2)\n                ELSE --  priority_level = 'SLA[12h]'\n                        (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (12 * 60 - 2)\n            END AS sla_score,\n            CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n                ELSE\n                    count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n            END AS sla_breached,\n            distance_workforce_level_admin_conversation,\n            CASE\n                WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n                WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n                ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n            END AS workforce_level_matching_score,\n            ecrb.inbox_id,\n            ss.intercom_inbox_name as inbox_name,\n            ecrb.last_inbox_assignment_by,\n            ecrb.uce_member_matching_score,\n            ecrb.time_reference_for_priority_score,\n            ecrb.sla_tag,\n            ecrb.conversation_workforce_level,\n            CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_priority_level ecrb\n        INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n        CROSS JOIN support_agents_for_computation sa\n        WHERE\n            sa.level - ecrb.conversation_workforce_level >= 0\n            AND CASE\n                    WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n                    ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n                END\n    ),\n    eligible_convos_with_scores AS (\n        SELECT\n            ec.contact_request_id,\n            ec.intercom_conversation_id,\n            ec.inbox_id,\n            ec.inbox_name,\n            NULL AS reserved_for_external_agent_platform,\n            ec.time_reference_for_priority_score,\n            m.score AS expertise_matching_score,\n            m.score AS raw_expertise_matching_score,\n            POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n            POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n            uce_member_matching_score,\n            COALESCE(\n                    power(m.score, ((:power_weight_a)))\n                    * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n                    * POWER(workforce_level_matching_score, (:power_weight_d))\n                    * uce_member_matching_score\n                , 0) AS assignment_score,\n            sla_score,\n            sla_breached,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n            priority_level,\n            MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n            null as n_rejections,\n            ec.sla_tag,\n            ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n            ec.additional_power_weight_c\n        FROM eligible_contact_request_backlog_with_context ec\n        LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n        WHERE crae.contact_request_id IS NULL\n        AND COALESCE(m.score, 0.2) >= 0\n    ),\n    backlog_stats AS MATERIALIZED (\n        SELECT\n            count(*) as n_assignable_convos,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n            count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n            count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n            percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n        FROM eligible_convos_with_scores\n    )\n    SELECT\n        row_number() OVER (ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS assignment_rank\n        , (:support_agent_id)\n        , *\n        , row_number() OVER (ORDER BY intercom_conversation_id = (:conversation_id) DESC, assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS row_nb\n    FROM eligible_convos_with_scores\n    CROSS JOIN backlog_stats stats\n    ORDER BY row_nb, assignment_score DESC NULLS LAST, time_reference_for_priority_score\n    LIMIT 10;\n    "

FR_ASSIGNER_SQL_QUERY module-attribute

FR_ASSIGNER_SQL_QUERY = "\n    -- This query is a translation of the original GET_CONVERSATION_BY_ASSIGNMENT_SCORE_EU_SQL\n    -- from the FR resolution_platform to the global support component.\n    -- Major changes include:\n    -- - Replacing FR tables with global support tables (e.g., care_conversation_backlog -> contact_request_intercom_state).\n    -- - Adapting to new schemas (e.g., support_agent roles are now in an array).\n    -- - Removing the old_spe_new_spe_mapping CTE as it is no longer needed.\n    WITH recalibrated_support_agents AS (\n        SELECT\n            sa.id\n        FROM support.assigner_recalibration ar\n        JOIN support.support_agent sa ON ar.alan_email = sa.alan_email\n        JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND ar.intercom_workspace_id = sawa.intercom_workspace_id\n        WHERE ar.intercom_workspace_id = (:workspace_id)\n    ),\n    support_agents_for_computation AS (\n        SELECT\n            sa.id as support_agent_id\n            , sa.intercom_admin_id\n            , sawa.level\n            , sawa.intercom_workspace_id\n            --Roles are now stored in an array, so we check for the presence of a role in the array.\n            , 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync\n            , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n                THEN 'eligible_for_async' = ANY(sawa.roles)\n                ELSE True\n                END as is_eligible_for_async\n            , sa.platform_name as platform\n            , NOT sa.is_external_admin AS has_role_uce\n            , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n                THEN 'eligible_for_callback' = ANY(sawa.roles)\n                ELSE False\n                END as has_role_callback\n            , 'automated_answer_reviewer' = ANY(sawa.roles) as has_reviewer_role\n            , 'en' = ANY(sa.spoken_languages) as speaks_english\n            , sa.work_location != 'fr' as is_offshore\n        FROM support.support_agent as sa\n        JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n        WHERE (sa.id = ANY((select array(SELECT id FROM recalibrated_support_agents))::uuid[])\n            OR sa.id = (:support_agent_id))  -- the admin requesting an assignment\n    ),\n    uce_specialisations_mapping AS (\n        SELECT sa.intercom_admin_id\n            , ss.intercom_inbox_id\n            , sasms.score\n        FROM support_agents_for_computation sa\n        LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id\n        LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n        LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n        WHERE sasms.score != 0\n    ),\n    prefiltered_contact_request_backlog AS (\n        SELECT\n            DISTINCT ON (cr.intercom_conversation_id)\n            cr.id as contact_request_id\n            , cr.intercom_conversation_id\n            , cris.assigned_intercom_inbox_id as inbox_id\n            , cris.last_inbox_assignment_by\n            , cris.waiting_since\n            , greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score\n            , cris.last_admin_reply_at\n            , cram.recommended_admin_id as recommended_admin_id\n            , cris.started_at\n            , COALESCE(cram.reserved_for_automated_answers_review, FALSE) as reserved_for_review\n            , cris.state\n            , cris.assigned_intercom_admin_id as admin_id\n            , cr.created_at\n            -- using tags to compute properties of the conversations\n            , SUBSTRING(tags.platform_tag, 'ROLE\\[(.*)\\]') AS reserved_for_external_agent_platform\n            -- priority level is defined by the sync/async nature of the convo but can be overwritten during a handover\n            , CASE\n                WHEN tags.sla_tag = 'SLA[12h]' THEN 'SLA[12h]'\n                WHEN tags.sla_tag = 'SLA[5min]' THEN 'SLA[5min]'\n                WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n                -- if not sync tagged or if one of the hotline source type → mark async.\n                WHEN tags.type_tag != 'SYNC' or cr.source_type in ('immediate_callback_request', 'hotline_request')THEN 'SLA[12h]'\n                ELSE 'SLA[5min]'\n                END AS priority_level\n            -- workforce level of the conversation\n            , CASE\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n                ELSE 1\n                END AS conversation_workforce_level\n            , tags.*\n        FROM support.contact_request cr\n        JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n        LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n        LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id -- to define priority level\n        CROSS JOIN LATERAL (\n            SELECT\n                MAX(CASE WHEN crt.name IN ('ROLE[UCE]', 'COMPLEX CLAIM', 'SENSITIVE') THEN crt.name END) AS uce_reserved_tag\n                , MAX(CASE WHEN crt.name = 'ROLE[CALLBACK]' THEN crt.name END) AS callback_tag\n                , MAX(CASE WHEN crt.name IN ('ROLE[WEBHELP]', 'ROLE[ONEPILOT]') THEN crt.name END) AS platform_tag\n                , MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag\n                , MAX(CASE WHEN crt.name = 'ROLE[GEORESTRICTED]' THEN crt.name END) AS geo_tag\n                , MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n                , MAX(CASE WHEN crt.name IN ('bypass-assigner', 'Abusive Conversation') THEN crt.name END) AS bypass_tag\n                , MAX(CASE WHEN crt.name IN ('SLA[12h]', 'SLA[5min]', 'SLA[30min]') THEN crt.name END) as sla_tag\n                , MAX(CASE WHEN crt.name IN ('SYNC', 'ASYNC') THEN crt.name END) as type_tag\n                , MAX(CASE WHEN crt.name IN ('bypass-classification') THEN crt.name END) AS classification_bypass_tag\n            FROM support.contact_request_tag crt\n            WHERE crt.contact_request_id = cr.id\n        ) tags\n        WHERE\n            cr.intercom_workspace_id = (:workspace_id)\n            AND cris.state = 'open'\n            AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n            AND cr.created_at > '2021-11-26' -- until data is cleared in prod\n            AND tags.bypass_tag IS NULL -- exclude convos explicitly flagged as bypassing the Assigner\n        ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n    ),\n    eligible_contact_request_backlog_with_priority_level AS (\n        SELECT\n            pcrb.contact_request_id\n            , pcrb.intercom_conversation_id\n            , pcrb.inbox_id\n            , pcrb.last_inbox_assignment_by\n            , pcrb.time_reference_for_priority_score\n            , pcrb.priority_level\n            , pcrb.conversation_workforce_level\n            , sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation\n            , CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id\n                THEN (:member_matching_score_boost)\n                ELSE 1\n                END AS uce_member_matching_score\n            , pcrb.started_at\n            , pcrb.reserved_for_external_agent_platform\n            , pcrb.reserved_for_review\n            , pcrb.classification_bypass_tag IS NOT NULL as bypassed_classifier\n        FROM prefiltered_contact_request_backlog pcrb\n        CROSS JOIN support_agents_for_computation sa\n        WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n            -- reduce scope for agents depending on their sync/async eligibility configuration\n            AND (\n                -- Admin is ELIGIBLE_FOR_SYNC and conversation is NOT ASYNC and NOT CALLBACK\n                (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                OR\n                -- Admin is ELIGIBLE_FOR_ASYNC and conversation is NOT SLA[5min] and NOT CALLBACK\n                (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                OR\n                -- Admin has CALLBACK role and conversation is CALLBACK\n                (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n            )\n            -- assign automated answer reviews for reviewers\n            AND (COALESCE(sa.has_reviewer_role, False) OR pcrb.reserved_for_review IS NOT TRUE)\n            -- reduce scope for agents not speaking english\n            AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n            -- reduce scope of georestricted convos for offshore agents\n            AND (COALESCE(NOT sa.is_offshore, False) OR pcrb.geo_tag IS NULL)\n            AND CASE\n                    WHEN NOT pcrb.reserved_for_review THEN sa.level - pcrb.conversation_workforce_level >= 0\n                    ELSE TRUE\n                END\n                -- handle conversations reserved for specific external agent platforms\n                AND CASE\n                        WHEN pcrb.reserved_for_external_agent_platform IS NOT NULL\n                        THEN UPPER(sa.platform) = UPPER(pcrb.reserved_for_external_agent_platform)\n                        ELSE TRUE\n                END\n    ),\n\n    contact_request_context_digest AS (\n        SELECT\n            cr.id as contact_request_id\n            , cr.duplicated_from_id as original_contact_request_id\n            , COALESCE(cr.classification_result->'raw_prediction_classes', cr.classification_result->'prediction'->'raw_prediction_classes' ) as raw_prediction_classes\n            , COALESCE(cr.classification_result->'raw_prediction', cr.classification_result->'prediction'->'raw_prediction') as raw_prediction\n            , created_at\n        FROM support.contact_request cr\n        INNER JOIN eligible_contact_request_backlog_with_priority_level e ON e.contact_request_id = cr.id\n        WHERE cr.intercom_workspace_id = (:workspace_id)\n    ),\n    classifier_classes AS (\n        SELECT\n            raw_prediction_classes AS classes\n        FROM contact_request_context_digest\n        WHERE jsonb_typeof(raw_prediction) = 'array'\n        ORDER BY created_at DESC\n        LIMIT 1\n    ),\n    classifier_classes_as_array AS (\n        SELECT ARRAY_AGG(cls) classes_array\n        FROM classifier_classes, JSONB_ARRAY_ELEMENTS_TEXT(classifier_classes.classes) cls\n    ),\n\n    -- # 1. define the eligible backlog for the UCE requesting an assignment\n    eligible_contact_request_backlog_with_context AS (\n        SELECT\n            ecrb.contact_request_id\n            , ecrb.intercom_conversation_id\n            , ecrb.priority_level\n            -- 1. SLA score = (1 min + waiting_time in min) / (1 min + SLA in min)\n            , CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (5 - 2)\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (30 - 2)\n                ELSE --  priority_level = 'SLA[12h]'\n                        (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (12 * 60 - 2)\n                END AS sla_score\n            , CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n                ELSE\n                    count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n                END AS sla_breached\n            -- 2. workforce level matching score\n            , distance_workforce_level_admin_conversation\n            -- the workforce_level_score_boost is here to give a higher assignment score when the level of the conversation matches the level of the admin\n            -- workforce_level_matching_score doesn't matter the escalation reviews of automated answers\n            , CASE\n                WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n                WHEN ecrb.reserved_for_review THEN 1.0\n                WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n                ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n                END AS workforce_level_matching_score\n            -- 3. parsing of classification result (to be used later for the expertise matching score computation)\n            , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n                THEN COALESCE(crcd.raw_prediction, crcd2.raw_prediction)\n                ELSE array_to_json(array_fill(1.0 / jsonb_array_length(cls.classes) , ARRAY[jsonb_array_length(cls.classes)]))::jsonb\n                END AS predicted_probabilities\n            , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)) = 'array'\n                THEN COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)\n                ELSE cls.classes\n                END AS predicted_classes\n            , ecrb.inbox_id\n            , ss.intercom_inbox_name as inbox_name\n            , ecrb.last_inbox_assignment_by\n            , ecrb.uce_member_matching_score\n            , ecrb.time_reference_for_priority_score\n            , ecrb.reserved_for_external_agent_platform\n            , ecrb.conversation_workforce_level\n            , ecrb.reserved_for_review\n            , ecrb.bypassed_classifier\n        FROM eligible_contact_request_backlog_with_priority_level ecrb\n        INNER JOIN support.support_specialization ss ON ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner AND ss.intercom_workspace_id = (:workspace_id) -- only consider inboxes recognized by the assigner\n        LEFT JOIN contact_request_context_digest crcd ON crcd.contact_request_id = ecrb.contact_request_id\n        LEFT JOIN contact_request_context_digest crcd2 ON crcd.original_contact_request_id = crcd2.contact_request_id  -- inherit context from parent convo when relevant\n        CROSS JOIN classifier_classes cls\n        -- we want to prevent assignments of tickets not yet classified, but don't want to wait more than 30 seconds for sync and 2 minutes for the rest\n        WHERE jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n            OR CASE\n                    WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n                    ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n                END\n    ),\n    -- # 2. compute expertise matching scores for all (eligible convo X UCE) pairs\n    convos_x_probabilities AS (\n        -- expand the eligible convos on all predicted classes\n        SELECT\n            s.*\n            , s._predicted_class AS predicted_class\n        FROM (\n            SELECT\n                contact_request_id\n                , intercom_conversation_id\n                , priority_level\n                , sla_score\n                , sla_breached\n                , conversation_workforce_level\n                , workforce_level_matching_score\n                , uce_member_matching_score\n                , jsonb_array_elements(predicted_probabilities)::numeric AS predicted_probability\n                , trim(jsonb_array_elements(predicted_classes)::text, '\"') AS _predicted_class\n                , last_inbox_assignment_by\n                , inbox_id\n                , inbox_name\n                , time_reference_for_priority_score\n                , reserved_for_external_agent_platform\n                , reserved_for_review\n                , bypassed_classifier\n            FROM eligible_contact_request_backlog_with_context\n        ) s\n        UNION ALL\n        -- expand for classes unknown by the classifier as well\n        SELECT\n            contact_request_id\n            , intercom_conversation_id\n            , priority_level\n            , sla_score\n            , sla_breached\n            , conversation_workforce_level\n            , workforce_level_matching_score\n            , uce_member_matching_score\n            , 0 AS predicted_probability\n            , t.inbox_unknown_by_classifier AS _predicted_class\n            , last_inbox_assignment_by\n            , inbox_id\n            , inbox_name\n            , time_reference_for_priority_score\n            , reserved_for_external_agent_platform\n            , reserved_for_review\n            , bypassed_classifier\n            , t.inbox_unknown_by_classifier AS predicted_class\n        FROM eligible_contact_request_backlog_with_context\n        CROSS JOIN (\n                SELECT intercom_inbox_name AS inbox_unknown_by_classifier FROM support.support_specialization\n                EXCEPT\n                SELECT JSONB_ARRAY_ELEMENTS_TEXT(classes) FROM classifier_classes\n        ) t\n    ),\n    -- Calibration pass 1: Compute min/max across ALL agents for calibration (lightweight aggregation)\n    calibration_min_max_per_agent AS (\n        SELECT\n            sa.support_agent_id\n            , cxp.contact_request_id\n            , sum((CASE\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n                    ELSE cxp.predicted_probability::numeric\n                END)\n                * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n        FROM convos_x_probabilities cxp\n        JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n        CROSS JOIN support_agents_for_computation sa\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n        CROSS JOIN classifier_classes_as_array cls\n        WHERE\n            CASE\n                WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n                ELSE TRUE\n            END\n            AND CASE\n                    WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n                    THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n                    ELSE TRUE\n            END\n        GROUP BY sa.support_agent_id, cxp.contact_request_id\n    ),\n    min_max_expertise_matching_score_per_convo AS (\n        SELECT\n            contact_request_id\n            , min(power(raw_expertise_matching_score, (:power_weight_a))) AS min_ems\n            , max(power(raw_expertise_matching_score, (:power_weight_a))) AS max_ems\n        FROM calibration_min_max_per_agent\n        GROUP BY contact_request_id\n    ),\n    -- calibration pass 2: Compute detailed scores ONLY for the requesting agent which is less costly\n    eligible_convos_with_expertise_matching_score_per_uce AS (\n        SELECT\n            sa.intercom_admin_id\n            , sa.support_agent_id\n            , cxp.intercom_conversation_id\n            , cxp.contact_request_id\n            , cxp.inbox_id\n            , cxp.inbox_name\n            , cxp.priority_level\n            , cxp.workforce_level_matching_score\n            , cxp.sla_score\n            , cxp.sla_breached\n            , cxp.uce_member_matching_score\n            , cxp.time_reference_for_priority_score\n            , cxp.reserved_for_external_agent_platform\n            , cxp.reserved_for_review\n            , sum((CASE\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n                    ELSE cxp.predicted_probability::numeric\n                END)\n                * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n            -- L1 & L2 agents have an extra boost on priority_level for sync conversations\n            , case when cxp.priority_level = 'SLA[5min]' and sa.level < 3 then 2 else 0 end as additional_power_weight_c\n        FROM convos_x_probabilities cxp\n        JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n        CROSS JOIN support_agents_for_computation sa\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n        CROSS JOIN classifier_classes_as_array cls\n        -- additional exclusion conditions for both the care expert asking for the assignment and the calibration cohort\n        WHERE\n            sa.support_agent_id = (:support_agent_id)::uuid  -- ONLY requesting agent\n            AND CASE\n                WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n                ELSE TRUE\n            END\n            -- handle conversations reserved for specific external agent platforms\n            AND CASE\n                    WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n                    THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n                    ELSE TRUE\n            END\n        GROUP BY sa.intercom_admin_id, sa.support_agent_id, cxp.contact_request_id, cxp.inbox_id, cxp.inbox_name, cxp.priority_level, cxp.workforce_level_matching_score, cxp.sla_score, cxp.sla_breached, cxp.uce_member_matching_score,\n    cxp.time_reference_for_priority_score, cxp.reserved_for_external_agent_platform, cxp.reserved_for_review, cxp.intercom_conversation_id, sa.level\n    ),\n    eligible_convos_with_scores AS (\n        SELECT\n            ec.contact_request_id\n            , ec.intercom_conversation_id\n            , ec.inbox_id\n            , ec.inbox_name\n            , ec.time_reference_for_priority_score\n            -- normalization of the expertise matching score (distributed over [20%, 100%])\n            , CASE WHEN max_ems > min_ems\n                    THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n                ELSE 1\n                END AS expertise_matching_score\n            , POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score\n            , CASE\n                WHEN NOT ec.reserved_for_review THEN POWER(workforce_level_matching_score, (:power_weight_d)) ELSE 1\n                END AS workforce_level_matching_score\n            , uce_member_matching_score\n            , COALESCE(\n                    CASE\n                        WHEN max_ems > min_ems\n                        THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n                        ELSE 1\n                    END\n                    * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n                    * POWER(workforce_level_matching_score, (:power_weight_d))\n                    * uce_member_matching_score\n                , 0) AS assignment_score\n            , raw_expertise_matching_score\n            , sla_score\n            , sla_breached\n            , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score\n            , priority_level\n            , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level\n            , m.max_ems\n            , ec.reserved_for_external_agent_platform\n            , ec.reserved_for_review\n            , null as n_rejections\n            , ec.priority_level = 'SLA[5min]' as is_sync_conversation\n            , ec.additional_power_weight_c\n        FROM eligible_convos_with_expertise_matching_score_per_uce ec\n        JOIN min_max_expertise_matching_score_per_convo m USING(contact_request_id)\n        LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n        WHERE\n            crae.contact_request_id IS NULL\n            AND ec.raw_expertise_matching_score > 0  -- exclude conversations with negative expertise matching score for current UCE\n    ),\n    backlog_stats AS MATERIALIZED (\n        SELECT\n            count(*) as n_assignable_convos\n            , count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async\n            , count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync\n            , count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback\n            , count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached\n            , count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached\n            , count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n        FROM eligible_convos_with_scores\n    )\n    SELECT\n        *\n        , 1.0 AS shift_matching_score\n    FROM eligible_convos_with_scores\n    CROSS JOIN backlog_stats stats\n    ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score\n    LIMIT 10;\n    "

FR_RANKED_SQL_QUERY module-attribute

FR_RANKED_SQL_QUERY = "\n    -- This query is a translation of the original GET_CONVERSATION_BY_ASSIGNMENT_SCORE_EU_SQL\n    -- from the FR resolution_platform to the global support component.\n    -- Major changes include:\n    -- - Replacing FR tables with global support tables (e.g., care_conversation_backlog -> contact_request_intercom_state).\n    -- - Adapting to new schemas (e.g., support_agent roles are now in an array).\n    -- - Removing the old_spe_new_spe_mapping CTE as it is no longer needed.\n    WITH recalibrated_support_agents AS (\n        SELECT\n            sa.id\n        FROM support.assigner_recalibration ar\n        JOIN support.support_agent sa ON ar.alan_email = sa.alan_email\n        JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND ar.intercom_workspace_id = sawa.intercom_workspace_id\n        WHERE ar.intercom_workspace_id = (:workspace_id)\n    ),\n    support_agents_for_computation AS (\n        SELECT\n            sa.id as support_agent_id\n            , sa.intercom_admin_id\n            , sawa.level\n            , sawa.intercom_workspace_id\n            --Roles are now stored in an array, so we check for the presence of a role in the array.\n            , 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync\n            , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n                THEN 'eligible_for_async' = ANY(sawa.roles)\n                ELSE True\n                END as is_eligible_for_async\n            , sa.platform_name as platform\n            , NOT sa.is_external_admin AS has_role_uce\n            , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n                THEN 'eligible_for_callback' = ANY(sawa.roles)\n                ELSE False\n                END as has_role_callback\n            , 'automated_answer_reviewer' = ANY(sawa.roles) as has_reviewer_role\n            , 'en' = ANY(sa.spoken_languages) as speaks_english\n            , sa.work_location != 'fr' as is_offshore\n        FROM support.support_agent as sa\n        JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n        WHERE (sa.id = ANY((select array(SELECT id FROM recalibrated_support_agents))::uuid[])\n            OR sa.id = (:support_agent_id))  -- the admin requesting an assignment\n    ),\n    uce_specialisations_mapping AS (\n        SELECT sa.intercom_admin_id\n            , ss.intercom_inbox_id\n            , sasms.score\n        FROM support_agents_for_computation sa\n        LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id\n        LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n        LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n        WHERE sasms.score != 0\n    ),\n    prefiltered_contact_request_backlog AS (\n        SELECT\n            DISTINCT ON (cr.intercom_conversation_id)\n            cr.id as contact_request_id\n            , cr.intercom_conversation_id\n            , cris.assigned_intercom_inbox_id as inbox_id\n            , cris.last_inbox_assignment_by\n            , cris.waiting_since\n            , greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score\n            , cris.last_admin_reply_at\n            , cram.recommended_admin_id as recommended_admin_id\n            , cris.started_at\n            , COALESCE(cram.reserved_for_automated_answers_review, FALSE) as reserved_for_review\n            , cris.state\n            , cris.assigned_intercom_admin_id as admin_id\n            , cr.created_at\n            -- using tags to compute properties of the conversations\n            , SUBSTRING(tags.platform_tag, 'ROLE\\[(.*)\\]') AS reserved_for_external_agent_platform\n            -- priority level is defined by the sync/async nature of the convo but can be overwritten during a handover\n            , CASE\n                WHEN tags.sla_tag = 'SLA[12h]' THEN 'SLA[12h]'\n                WHEN tags.sla_tag = 'SLA[5min]' THEN 'SLA[5min]'\n                WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n                -- if not sync tagged or if one of the hotline source type → mark async.\n                WHEN tags.type_tag != 'SYNC' or cr.source_type in ('immediate_callback_request', 'hotline_request')THEN 'SLA[12h]'\n                ELSE 'SLA[5min]'\n                END AS priority_level\n            -- workforce level of the conversation\n            , CASE\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n                WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n                ELSE 1\n                END AS conversation_workforce_level\n            , tags.*\n        FROM support.contact_request cr\n        JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n        LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n        LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id -- to define priority level\n        CROSS JOIN LATERAL (\n            SELECT\n                MAX(CASE WHEN crt.name IN ('ROLE[UCE]', 'COMPLEX CLAIM', 'SENSITIVE') THEN crt.name END) AS uce_reserved_tag\n                , MAX(CASE WHEN crt.name = 'ROLE[CALLBACK]' THEN crt.name END) AS callback_tag\n                , MAX(CASE WHEN crt.name IN ('ROLE[WEBHELP]', 'ROLE[ONEPILOT]') THEN crt.name END) AS platform_tag\n                , MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag\n                , MAX(CASE WHEN crt.name = 'ROLE[GEORESTRICTED]' THEN crt.name END) AS geo_tag\n                , MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n                , MAX(CASE WHEN crt.name IN ('bypass-assigner', 'Abusive Conversation') THEN crt.name END) AS bypass_tag\n                , MAX(CASE WHEN crt.name IN ('SLA[12h]', 'SLA[5min]', 'SLA[30min]') THEN crt.name END) as sla_tag\n                , MAX(CASE WHEN crt.name IN ('SYNC', 'ASYNC') THEN crt.name END) as type_tag\n                , MAX(CASE WHEN crt.name IN ('bypass-classification') THEN crt.name END) AS classification_bypass_tag\n            FROM support.contact_request_tag crt\n            WHERE crt.contact_request_id = cr.id\n        ) tags\n        WHERE\n            cr.intercom_workspace_id = (:workspace_id)\n            AND cris.state = 'open'\n            AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n            AND cr.created_at > '2021-11-26' -- until data is cleared in prod\n            AND tags.bypass_tag IS NULL -- exclude convos explicitly flagged as bypassing the Assigner\n        ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n    ),\n    eligible_contact_request_backlog_with_priority_level AS (\n        SELECT\n            pcrb.contact_request_id\n            , pcrb.intercom_conversation_id\n            , pcrb.inbox_id\n            , pcrb.last_inbox_assignment_by\n            , pcrb.time_reference_for_priority_score\n            , pcrb.priority_level\n            , pcrb.conversation_workforce_level\n            , sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation\n            , CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id\n                THEN (:member_matching_score_boost)\n                ELSE 1\n                END AS uce_member_matching_score\n            , pcrb.started_at\n            , pcrb.reserved_for_external_agent_platform\n            , pcrb.reserved_for_review\n            , pcrb.classification_bypass_tag IS NOT NULL as bypassed_classifier\n        FROM prefiltered_contact_request_backlog pcrb\n        CROSS JOIN support_agents_for_computation sa\n        WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n            -- reduce scope for agents depending on their sync/async eligibility configuration\n            AND (\n                -- Admin is ELIGIBLE_FOR_SYNC and conversation is NOT ASYNC and NOT CALLBACK\n                (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                OR\n                -- Admin is ELIGIBLE_FOR_ASYNC and conversation is NOT SLA[5min] and NOT CALLBACK\n                (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n                OR\n                -- Admin has CALLBACK role and conversation is CALLBACK\n                (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n            )\n            -- assign automated answer reviews for reviewers\n            AND (COALESCE(sa.has_reviewer_role, False) OR pcrb.reserved_for_review IS NOT TRUE)\n            -- reduce scope for agents not speaking english\n            AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n            -- reduce scope of georestricted convos for offshore agents\n            AND (COALESCE(NOT sa.is_offshore, False) OR pcrb.geo_tag IS NULL)\n            AND CASE\n                    WHEN NOT pcrb.reserved_for_review THEN sa.level - pcrb.conversation_workforce_level >= 0\n                    ELSE TRUE\n                END\n                -- handle conversations reserved for specific external agent platforms\n                AND CASE\n                        WHEN pcrb.reserved_for_external_agent_platform IS NOT NULL\n                        THEN UPPER(sa.platform) = UPPER(pcrb.reserved_for_external_agent_platform)\n                        ELSE TRUE\n                END\n    ),\n\n    contact_request_context_digest AS (\n        SELECT\n            cr.id as contact_request_id\n            , cr.duplicated_from_id as original_contact_request_id\n            , COALESCE(cr.classification_result->'raw_prediction_classes', cr.classification_result->'prediction'->'raw_prediction_classes' ) as raw_prediction_classes\n            , COALESCE(cr.classification_result->'raw_prediction', cr.classification_result->'prediction'->'raw_prediction') as raw_prediction\n            , created_at\n        FROM support.contact_request cr\n        INNER JOIN eligible_contact_request_backlog_with_priority_level e ON e.contact_request_id = cr.id\n        WHERE cr.intercom_workspace_id = (:workspace_id)\n    ),\n    classifier_classes AS (\n        SELECT\n            raw_prediction_classes AS classes\n        FROM contact_request_context_digest\n        WHERE jsonb_typeof(raw_prediction) = 'array'\n        ORDER BY created_at DESC\n        LIMIT 1\n    ),\n    classifier_classes_as_array AS (\n        SELECT ARRAY_AGG(cls) classes_array\n        FROM classifier_classes, JSONB_ARRAY_ELEMENTS_TEXT(classifier_classes.classes) cls\n    ),\n\n    -- # 1. define the eligible backlog for the UCE requesting an assignment\n    eligible_contact_request_backlog_with_context AS (\n        SELECT\n            ecrb.contact_request_id\n            , ecrb.intercom_conversation_id\n            , ecrb.priority_level\n            -- 1. SLA score = (1 min + waiting_time in min) / (1 min + SLA in min)\n            , CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (5 - 2)\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (30 - 2)\n                ELSE --  priority_level = 'SLA[12h]'\n                        (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now()))  / (12 * 60 - 2)\n                END AS sla_score\n            , CASE\n                WHEN ecrb.priority_level = 'SLA[5min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n                WHEN ecrb.priority_level = 'SLA[30min]'\n                    THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n                ELSE\n                    count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n                END AS sla_breached\n            -- 2. workforce level matching score\n            , distance_workforce_level_admin_conversation\n            -- the workforce_level_score_boost is here to give a higher assignment score when the level of the conversation matches the level of the admin\n            -- workforce_level_matching_score doesn't matter the escalation reviews of automated answers\n            , CASE\n                WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n                WHEN ecrb.reserved_for_review THEN 1.0\n                WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n                ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n                END AS workforce_level_matching_score\n            -- 3. parsing of classification result (to be used later for the expertise matching score computation)\n            , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n                THEN COALESCE(crcd.raw_prediction, crcd2.raw_prediction)\n                ELSE array_to_json(array_fill(1.0 / jsonb_array_length(cls.classes) , ARRAY[jsonb_array_length(cls.classes)]))::jsonb\n                END AS predicted_probabilities\n            , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)) = 'array'\n                THEN COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)\n                ELSE cls.classes\n                END AS predicted_classes\n            , ecrb.inbox_id\n            , ss.intercom_inbox_name as inbox_name\n            , ecrb.last_inbox_assignment_by\n            , ecrb.uce_member_matching_score\n            , ecrb.time_reference_for_priority_score\n            , ecrb.reserved_for_external_agent_platform\n            , ecrb.conversation_workforce_level\n            , ecrb.reserved_for_review\n            , ecrb.bypassed_classifier\n        FROM eligible_contact_request_backlog_with_priority_level ecrb\n        INNER JOIN support.support_specialization ss ON ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner AND ss.intercom_workspace_id = (:workspace_id) -- only consider inboxes recognized by the assigner\n        LEFT JOIN contact_request_context_digest crcd ON crcd.contact_request_id = ecrb.contact_request_id\n        LEFT JOIN contact_request_context_digest crcd2 ON crcd.original_contact_request_id = crcd2.contact_request_id  -- inherit context from parent convo when relevant\n        CROSS JOIN classifier_classes cls\n        -- we want to prevent assignments of tickets not yet classified, but don't want to wait more than 30 seconds for sync and 2 minutes for the rest\n        WHERE jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n            OR CASE\n                    WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n                    ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n                END\n    ),\n    -- # 2. compute expertise matching scores for all (eligible convo X UCE) pairs\n    convos_x_probabilities AS (\n        -- expand the eligible convos on all predicted classes\n        SELECT\n            s.*\n            , s._predicted_class AS predicted_class\n        FROM (\n            SELECT\n                contact_request_id\n                , intercom_conversation_id\n                , priority_level\n                , sla_score\n                , sla_breached\n                , conversation_workforce_level\n                , workforce_level_matching_score\n                , uce_member_matching_score\n                , jsonb_array_elements(predicted_probabilities)::numeric AS predicted_probability\n                , trim(jsonb_array_elements(predicted_classes)::text, '\"') AS _predicted_class\n                , last_inbox_assignment_by\n                , inbox_id\n                , inbox_name\n                , time_reference_for_priority_score\n                , reserved_for_external_agent_platform\n                , reserved_for_review\n                , bypassed_classifier\n            FROM eligible_contact_request_backlog_with_context\n        ) s\n        UNION ALL\n        -- expand for classes unknown by the classifier as well\n        SELECT\n            contact_request_id\n            , intercom_conversation_id\n            , priority_level\n            , sla_score\n            , sla_breached\n            , conversation_workforce_level\n            , workforce_level_matching_score\n            , uce_member_matching_score\n            , 0 AS predicted_probability\n            , t.inbox_unknown_by_classifier AS _predicted_class\n            , last_inbox_assignment_by\n            , inbox_id\n            , inbox_name\n            , time_reference_for_priority_score\n            , reserved_for_external_agent_platform\n            , reserved_for_review\n            , bypassed_classifier\n            , t.inbox_unknown_by_classifier AS predicted_class\n        FROM eligible_contact_request_backlog_with_context\n        CROSS JOIN (\n                SELECT intercom_inbox_name AS inbox_unknown_by_classifier FROM support.support_specialization\n                EXCEPT\n                SELECT JSONB_ARRAY_ELEMENTS_TEXT(classes) FROM classifier_classes\n        ) t\n    ),\n    -- Calibration pass 1: Compute min/max across ALL agents for calibration (lightweight aggregation)\n    calibration_min_max_per_agent AS (\n        SELECT\n            sa.support_agent_id\n            , cxp.contact_request_id\n            , sum((CASE\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n                    ELSE cxp.predicted_probability::numeric\n                END)\n                * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n        FROM convos_x_probabilities cxp\n        JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n        CROSS JOIN support_agents_for_computation sa\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n        CROSS JOIN classifier_classes_as_array cls\n        WHERE\n            CASE\n                WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n                ELSE TRUE\n            END\n            AND CASE\n                    WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n                    THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n                    ELSE TRUE\n            END\n        GROUP BY sa.support_agent_id, cxp.contact_request_id\n    ),\n    min_max_expertise_matching_score_per_convo AS (\n        SELECT\n            contact_request_id\n            , min(power(raw_expertise_matching_score, (:power_weight_a))) AS min_ems\n            , max(power(raw_expertise_matching_score, (:power_weight_a))) AS max_ems\n        FROM calibration_min_max_per_agent\n        GROUP BY contact_request_id\n    ),\n    -- calibration pass 2: Compute detailed scores ONLY for the requesting agent which is less costly\n    eligible_convos_with_expertise_matching_score_per_uce AS (\n        SELECT\n            sa.intercom_admin_id\n            , sa.support_agent_id\n            , cxp.intercom_conversation_id\n            , cxp.contact_request_id\n            , cxp.inbox_id\n            , cxp.inbox_name\n            , cxp.priority_level\n            , cxp.workforce_level_matching_score\n            , cxp.sla_score\n            , cxp.sla_breached\n            , cxp.uce_member_matching_score\n            , cxp.time_reference_for_priority_score\n            , cxp.reserved_for_external_agent_platform\n            , cxp.reserved_for_review\n            , sum((CASE\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n                    WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n                        AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n                    ELSE cxp.predicted_probability::numeric\n                END)\n                * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n            -- L1 & L2 agents have an extra boost on priority_level for sync conversations\n            , case when cxp.priority_level = 'SLA[5min]' and sa.level < 3 then 2 else 0 end as additional_power_weight_c\n        FROM convos_x_probabilities cxp\n        JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n        CROSS JOIN support_agents_for_computation sa\n        LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n        CROSS JOIN classifier_classes_as_array cls\n        -- additional exclusion conditions for both the care expert asking for the assignment and the calibration cohort\n        WHERE\n            sa.support_agent_id = (:support_agent_id)::uuid  -- ONLY requesting agent\n            AND CASE\n                WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n                ELSE TRUE\n            END\n            -- handle conversations reserved for specific external agent platforms\n            AND CASE\n                    WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n                    THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n                    ELSE TRUE\n            END\n        GROUP BY sa.intercom_admin_id, sa.support_agent_id, cxp.contact_request_id, cxp.inbox_id, cxp.inbox_name, cxp.priority_level, cxp.workforce_level_matching_score, cxp.sla_score, cxp.sla_breached, cxp.uce_member_matching_score,\n    cxp.time_reference_for_priority_score, cxp.reserved_for_external_agent_platform, cxp.reserved_for_review, cxp.intercom_conversation_id, sa.level\n    ),\n    eligible_convos_with_scores AS (\n        SELECT\n            ec.contact_request_id\n            , ec.intercom_conversation_id\n            , ec.inbox_id\n            , ec.inbox_name\n            , ec.time_reference_for_priority_score\n            -- normalization of the expertise matching score (distributed over [20%, 100%])\n            , CASE WHEN max_ems > min_ems\n                    THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n                ELSE 1\n                END AS expertise_matching_score\n            , POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score\n            , CASE\n                WHEN NOT ec.reserved_for_review THEN POWER(workforce_level_matching_score, (:power_weight_d)) ELSE 1\n                END AS workforce_level_matching_score\n            , uce_member_matching_score\n            , COALESCE(\n                    CASE\n                        WHEN max_ems > min_ems\n                        THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n                        ELSE 1\n                    END\n                    * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n                    * POWER(workforce_level_matching_score, (:power_weight_d))\n                    * uce_member_matching_score\n                , 0) AS assignment_score\n            , raw_expertise_matching_score\n            , sla_score\n            , sla_breached\n            , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score\n            , priority_level\n            , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level\n            , m.max_ems\n            , ec.reserved_for_external_agent_platform\n            , ec.reserved_for_review\n            , null as n_rejections\n            , ec.priority_level = 'SLA[5min]' as is_sync_conversation\n            , ec.additional_power_weight_c\n        FROM eligible_convos_with_expertise_matching_score_per_uce ec\n        JOIN min_max_expertise_matching_score_per_convo m USING(contact_request_id)\n        LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n        WHERE\n            crae.contact_request_id IS NULL\n            AND ec.raw_expertise_matching_score > 0  -- exclude conversations with negative expertise matching score for current UCE\n    ),\n    backlog_stats AS MATERIALIZED (\n        SELECT\n            count(*) as n_assignable_convos\n            , count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async\n            , count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync\n            , count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback\n            , count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached\n            , count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached\n            , count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m\n            , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n        FROM eligible_convos_with_scores\n    )\n    SELECT\n        row_number() OVER (ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS assignment_rank\n        , (:support_agent_id)\n        , *\n        , row_number() OVER (ORDER BY intercom_conversation_id = (:conversation_id) DESC, assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS row_nb\n    FROM eligible_convos_with_scores\n    CROSS JOIN backlog_stats stats\n    ORDER BY row_nb, assignment_score DESC NULLS LAST, time_reference_for_priority_score\n    LIMIT 10;\n"

components.support.public.dependencies

COMPONENT_NAME module-attribute

COMPONENT_NAME = 'support'

SupportDependency

Bases: ABC

This allows country-specific apps to define a class to inject dependencies to the support component. This is mainly used as a workaround until everything from resolution_platform has been migrated to the support component, and we can safely rely on a global user & alan employee.

cancel_legacy_callback_timeslot abstractmethod

cancel_legacy_callback_timeslot(
    callback_timeslot_id, commit=True
)

Cancels a legacy callback timeslot in case we cancel the global one

Source code in components/support/public/dependencies.py
@abstractmethod
def cancel_legacy_callback_timeslot(
    self, callback_timeslot_id: uuid.UUID, commit: bool = True
) -> None:
    """
    Cancels a legacy callback timeslot in case we cancel the global one
    """
    pass

create_legacy_conversation_context abstractmethod

create_legacy_conversation_context(
    user_id,
    context,
    callback_phone_number,
    recording_consent,
    commit=True,
)

For a given user id and context, creates a legacy conversation context in the current country. Should do exactly what the legacy does (no more no less)

Source code in components/support/public/dependencies.py
@abstractmethod
def create_legacy_conversation_context(
    self,
    user_id: str,
    context: str,
    callback_phone_number: str | None,
    recording_consent: bool | None,
    commit: bool = True,
) -> uuid.UUID | None:
    """
    For a given user id and context, creates a legacy conversation context in the current country.
    Should do exactly what the legacy does (no more no less)
    """
    pass

create_legacy_phone_support_csat abstractmethod

create_legacy_phone_support_csat(
    ino_interaction_id, conversation_context_id, user_id
)

Creates the phone support CSAT for the legacy phone support.

Source code in components/support/public/dependencies.py
@abstractmethod
def create_legacy_phone_support_csat(
    self,
    ino_interaction_id: str,
    conversation_context_id: uuid.UUID,
    user_id: str | None,
) -> None:
    """
    Creates the phone support CSAT for the legacy phone support.
    """
    pass

delete_legacy_callback_timeslots_suggestion abstractmethod

delete_legacy_callback_timeslots_suggestion(
    callback_timeslots_suggestion_id, commit=True
)

Delete a legacy callback timeslots suggestion to avoid reimporting it after we delete the existing global one

Source code in components/support/public/dependencies.py
@abstractmethod
def delete_legacy_callback_timeslots_suggestion(
    self, callback_timeslots_suggestion_id: uuid.UUID, commit: bool = True
) -> None:
    """
    Delete a legacy callback timeslots suggestion to avoid reimporting it after we delete the
    existing global one
    """
    pass

get_accounts_and_population_ids_for_role abstractmethod

get_accounts_and_population_ids_for_role(user_id, role)

This is used in the Support Status determination, more specific to know the member's eligibility to a given Support Channel.

Returns the list of account and potential population matches for a given user Returned array can be empty.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_accounts_and_population_ids_for_role(
    self, user_id: str | None, role: ContactRoleType | None
) -> ContactCountryDetails:
    """
    This is used in the Support Status determination, more specific to know the member's eligibility
    to a given Support Channel.

    Returns the list of account and potential population matches for a given user
    Returned array can be empty.
    """
    pass

get_active_employees abstractmethod

get_active_employees()

Returns a list of all employees in the current app. This is used to import support agents from employees.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_active_employees(self) -> list[BaseAlanEmployee]:
    """
    Returns a list of all employees in the current app.
    This is used to import support agents from employees.
    """
    pass

get_alan_employee abstractmethod

get_alan_employee(alan_email)

Returns the BaseAlanEmployee entry of the current app for the given alan_email

Source code in components/support/public/dependencies.py
@abstractmethod
def get_alan_employee(self, alan_email: str) -> BaseAlanEmployee:
    """
    Returns the BaseAlanEmployee entry of the current app for the given alan_email
    """
    pass

get_alan_employee_ids_by_alan_email abstractmethod

get_alan_employee_ids_by_alan_email(alan_emails)

Returns the alan employee ids for the given alan_emails, by alan_email

Source code in components/support/public/dependencies.py
@abstractmethod
def get_alan_employee_ids_by_alan_email(
    self, alan_emails: list[str]
) -> dict[str, str]:
    """
    Returns the alan employee ids for the given alan_emails, by alan_email
    """
    pass

get_assigner_sql_query abstractmethod

get_assigner_sql_query()

Returns the SQL Query used to get a list of conversations to assign. This query should return data that can be mapped to AssignerSQLResult fields. Expected parameters: - support_agent_id: str - workspace_id: str - power_weight_a: float (from AssignerConfigurationData) - power_weight_b: float (from AssignerConfigurationData) - power_weight_c: float (from AssignerConfigurationData) - power_weight_d: float (from AssignerConfigurationData) - sync_sla_score_boost: float (from AssignerConfigurationData) - member_matching_score_boost: float (from AssignerConfigurationData) - workforce_level_score_boost: float (from AssignerConfigurationData)

Source code in components/support/public/dependencies.py
@abstractmethod
def get_assigner_sql_query(self) -> str | None:
    """
    Returns the SQL Query used to get a list of conversations to assign.
    This query should return data that can be mapped to AssignerSQLResult fields.
    Expected parameters:
    - support_agent_id: str
    - workspace_id: str
    - power_weight_a: float (from AssignerConfigurationData)
    - power_weight_b: float (from AssignerConfigurationData)
    - power_weight_c: float (from AssignerConfigurationData)
    - power_weight_d: float (from AssignerConfigurationData)
    - sync_sla_score_boost: float (from AssignerConfigurationData)
    - member_matching_score_boost: float (from AssignerConfigurationData)
    - workforce_level_score_boost: float (from AssignerConfigurationData)
    """
    pass

get_callback_request_confirmation_email_template_name abstractmethod

get_callback_request_confirmation_email_template_name(
    user_id,
)

Gives the name of the template, including language, needed to send the confirmation email when a user requests a callback.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_callback_request_confirmation_email_template_name(
    self, user_id: str
) -> str | None:
    """
    Gives the name of the template, including language, needed to send the confirmation email
    when a user requests a callback.
    """
    pass

get_channel_management_population_configuration abstractmethod

get_channel_management_population_configuration()

Returns the population configuration for the current app name. It's used to configure the cutover periods in the channel management.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_channel_management_population_configuration(
    self,
) -> PopulationFrontendDescriptionType:
    """
    Returns the population configuration for the current app name.
    It's used to configure the cutover periods in the channel management.
    """
    pass

get_contact_request_source_type_from_context abstractmethod

get_contact_request_source_type_from_context(
    legacy_conversation_context_id,
)

For a given care conversation context id (in the current country), returns the relevant contact request source type (is it a callback request? an async conversation request? etc.)

Source code in components/support/public/dependencies.py
@abstractmethod
def get_contact_request_source_type_from_context(
    self, legacy_conversation_context_id: str
) -> ContactRequestSourceType:
    """
    For a given care conversation context id (in the current country), returns the relevant contact request
    source type (is it a callback request? an async conversation request? etc.)
    """
    pass

get_hotline_phone_number abstractmethod

get_hotline_phone_number()

Returns the hotline phone number for the current app.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_hotline_phone_number(self) -> str | None:
    """
    Returns the hotline phone number for the current app.
    """
    pass

get_identification_digits_from_profile_id abstractmethod

get_identification_digits_from_profile_id(profile_id)

Returns the identification digits the user will need to provide when calling the hotline.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_identification_digits_from_profile_id(
    self, profile_id: uuid.UUID | None
) -> str | None:
    """
    Returns the identification digits the user will need to provide when calling the hotline.
    """
    pass

get_last_inbox_assignment_by_from_legacy abstractmethod

get_last_inbox_assignment_by_from_legacy(
    intercom_conversation_id,
)

Returns the last inbox assignment by for a legacy conversation

Source code in components/support/public/dependencies.py
@abstractmethod
def get_last_inbox_assignment_by_from_legacy(
    self, intercom_conversation_id: str
) -> str | None:
    """
    Returns the last inbox assignment by for a legacy conversation
    """
    pass

get_legacy_callback_timeslots_for_conversation abstractmethod

get_legacy_callback_timeslots_for_conversation(
    intercom_conversation_id,
)

Returns legacy callback timeslots if they exist for that conversation and alan employee id

Source code in components/support/public/dependencies.py
@abstractmethod
def get_legacy_callback_timeslots_for_conversation(
    self, intercom_conversation_id: str
) -> list[LegacyCallbackTimeslot]:
    """
    Returns legacy callback timeslots if they exist for that conversation and alan employee id
    """
    pass

get_legacy_callback_timeslots_suggestion_for_conversation abstractmethod

get_legacy_callback_timeslots_suggestion_for_conversation(
    intercom_conversation_id, alan_email
)

Returns legacy callback timeslots suggestion if they exist for that conversation and alan employee id

Source code in components/support/public/dependencies.py
@abstractmethod
def get_legacy_callback_timeslots_suggestion_for_conversation(
    self, intercom_conversation_id: str, alan_email: str
) -> LegacyCallbackTimeslotsSuggestion | None:
    """
    Returns legacy callback timeslots suggestion if they exist for that conversation and alan employee id
    """
    pass

get_legacy_conversation_context_id_for_intercom_conversation abstractmethod

get_legacy_conversation_context_id_for_intercom_conversation(
    intercom_conversation_id, intercom_workspace_id
)

For a given intercom conversation id & workspace id, returns the relevant country-spefici care conversation context id & the relevant app name. Should throw if not found.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_legacy_conversation_context_id_for_intercom_conversation(
    self, intercom_conversation_id: str, intercom_workspace_id: str
) -> tuple[str | None, AppName]:
    """
    For a given intercom conversation id & workspace id, returns the relevant country-spefici
    care conversation context id & the relevant app name. Should throw if not found.
    """
    pass

get_legacy_conversation_context_phone_calls abstractmethod

get_legacy_conversation_context_phone_calls(
    intercom_workspace_id, legacy_conversation_context_id
)

For a given care conversation context id, returns the regular callback phone number & consent

Source code in components/support/public/dependencies.py
@abstractmethod
def get_legacy_conversation_context_phone_calls(
    self, intercom_workspace_id: str, legacy_conversation_context_id: str
) -> list[LegacyPhoneCall]:
    """
    For a given care conversation context id, returns the regular callback phone number & consent
    """
    pass

get_legacy_conversation_context_regular_callback_details abstractmethod

get_legacy_conversation_context_regular_callback_details(
    legacy_conversation_context_id,
)

For a given care conversation context id, returns the callback phone number & consent Returns a tuple: needs_callback_request, phone_number, recording_consent, updated_at

Source code in components/support/public/dependencies.py
@abstractmethod
def get_legacy_conversation_context_regular_callback_details(
    self, legacy_conversation_context_id: str
) -> tuple[bool, str | None, bool | None, datetime | None]:
    """
    For a given care conversation context id, returns the callback phone number & consent
    Returns a tuple:
    needs_callback_request, phone_number, recording_consent, updated_at
    """
    pass

get_marmot_conversation_phone_call_history_url abstractmethod

get_marmot_conversation_phone_call_history_url()

Returns the relevant country-specific Marmot URL to show the list of callbacks calls. (The component is global but the route is country-specific).

Source code in components/support/public/dependencies.py
@abstractmethod
def get_marmot_conversation_phone_call_history_url(self) -> str:
    """
    Returns the relevant country-specific Marmot URL to show the list of callbacks calls.
    (The component is global but the route is country-specific).
    """
    pass

get_non_offline_user_info_for_phone_support abstractmethod

get_non_offline_user_info_for_phone_support(user_id)

Tells if a user is an offline experience company. Returns a tuple: is_eligible, is_alan_member, is_alaner, is_offline_member, is_offline_experience_company, redirect_to_queue

Source code in components/support/public/dependencies.py
@abstractmethod
def get_non_offline_user_info_for_phone_support(
    self, user_id: str
) -> NonOfflineUserInfo:
    """
    Tells if a user is an offline experience company.
    Returns a tuple:
    is_eligible, is_alan_member, is_alaner, is_offline_member, is_offline_experience_company, redirect_to_queue
    """
    pass

get_platform_role_tags abstractmethod

get_platform_role_tags()

Get the list of role tags for country-specific external platforms

Source code in components/support/public/dependencies.py
@abstractmethod
def get_platform_role_tags(self) -> list[str]:
    """
    Get the list of role tags for country-specific external platforms
    """
    pass

get_primary_profiles_from_profile_ids abstractmethod

get_primary_profiles_from_profile_ids(profile_ids)

For a given list of profile ids, returns the list of primary profile ids

Source code in components/support/public/dependencies.py
@abstractmethod
def get_primary_profiles_from_profile_ids(
    self, profile_ids: list[uuid.UUID]
) -> list[uuid.UUID]:
    """
    For a given list of profile ids, returns the list of primary profile ids
    """
    pass

get_profile_id_from_identification_digits abstractmethod

get_profile_id_from_identification_digits(
    identification_digits,
)

For a given identification digits, returns the profile id. Used for the phone support identification.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_profile_id_from_identification_digits(
    self, identification_digits: str
) -> uuid.UUID | None:
    """
    For a given identification digits, returns the profile id. Used for the phone support identification.
    """
    pass

get_ranked_sql_query abstractmethod

get_ranked_sql_query()

Returns the SQL Query used to get a list of ranked conversations.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_ranked_sql_query(self) -> str:
    """
    Returns the SQL Query used to get a list of ranked conversations.
    """
    pass
get_recommended_intercom_admin_id_for_legacy_context(
    legacy_context_id,
)

Returns the recommended intercom admin id for a legacy context.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_recommended_intercom_admin_id_for_legacy_context(
    self, legacy_context_id: str
) -> str | None:
    """
    Returns the recommended intercom admin id for a legacy context.
    """
    pass

get_support_workspace_id abstractmethod

get_support_workspace_id()

Returns the Intercom workspace ID for support in the current app. This is used to create workspace affectations for support agents.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_support_workspace_id(self) -> str:
    """
    Returns the Intercom workspace ID for support in the current app.
    This is used to create workspace affectations for support agents.
    """
    pass

get_user_first_name abstractmethod

get_user_first_name(user_id)

For a given user id, returns the first name of a user of the current app.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_first_name(self, user_id: str) -> str | None:
    """
    For a given user id, returns the first name of a user of the current app.
    """
    pass

get_user_fullname abstractmethod

get_user_fullname(user_id)

For a given user id, returns the user's full name

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_fullname(self, user_id: str) -> str | None:
    """
    For a given user id, returns the user's full name
    """
    pass

get_user_id_for_legacy_conversation_context abstractmethod

get_user_id_for_legacy_conversation_context(
    conversation_context_id,
)

For a given care conversation context id (in the current country), returns the relevant user id.

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_id_for_legacy_conversation_context(
    self, conversation_context_id: str
) -> tuple[str | None, AppName]:
    """
    For a given care conversation context id (in the current country), returns the relevant user id.
    """
    pass

get_user_id_from_profile_id abstractmethod

get_user_id_from_profile_id(profile_id)

For a given profile id, returns the user id

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_id_from_profile_id(self, profile_id: uuid.UUID) -> str | None:
    """
    For a given profile id, returns the user id
    """
    pass

get_user_lang abstractmethod

get_user_lang(user_id)

Given a user id, returns the user's language

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_lang(self, user_id: str | None) -> Lang:
    """
    Given a user id, returns the user's language
    """
    pass

get_user_perso_email abstractmethod

get_user_perso_email(user_id)

For a given user id, returns the user's personal email

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_perso_email(self, user_id: str) -> str | None:
    """
    For a given user id, returns the user's personal email
    """
    pass

get_user_pro_email abstractmethod

get_user_pro_email(user_id)

For a given user id, returns the user's pro email

Source code in components/support/public/dependencies.py
@abstractmethod
def get_user_pro_email(self, user_id: str) -> str | None:
    """
    For a given user id, returns the user's pro email
    """
    pass

handle_legacy_new_conversation_event abstractmethod

handle_legacy_new_conversation_event(conversation)

Handles whatever we did in the legacy endpoint for new conversations in the global side (that helps ensure things are done synchronously).

Source code in components/support/public/dependencies.py
@abstractmethod
def handle_legacy_new_conversation_event(
    self,
    conversation: "IntercomConversation",
) -> None:
    """
    Handles whatever we did in the legacy endpoint for new conversations in the
    global side (that helps ensure things are done synchronously).
    """
    pass

handle_legacy_new_conversation_event_fallback abstractmethod

handle_legacy_new_conversation_event_fallback(
    conversation_dict, intercom_workspace_id, dry_run
)

Handles whatever we needed to do in the legacy for the new conversation fallback command, allowing to do it sequentially from the global new conversation fallback command.

Source code in components/support/public/dependencies.py
@abstractmethod
def handle_legacy_new_conversation_event_fallback(
    self,
    conversation_dict: dict[str, Any],
    intercom_workspace_id: str,
    dry_run: bool,
) -> None:
    """
    Handles whatever we needed to do in the legacy for the new conversation fallback command,
    allowing to do it sequentially from the global new conversation fallback command.
    """
    pass

is_eligible_to_callback abstractmethod

is_eligible_to_callback(user_id)

Tells if a user is eligible to the callback. Will be used to show the callback option in the frontend, and in the endpoint that creates a callback request (usually async message endpoint)

Source code in components/support/public/dependencies.py
@abstractmethod
def is_eligible_to_callback(self, user_id: str | None) -> bool:
    """
    Tells if a user is eligible to the callback. Will be used to show the callback option in the frontend,
    and in the endpoint that creates a callback request (usually async message endpoint)
    """
    pass

is_intercom_receipt_sent_for_context abstractmethod

is_intercom_receipt_sent_for_context(
    legacy_conversation_context_user_id,
)

For a given care conversation context id (in the current country), returns the fact that an initial async email answer was already sent to the user.

Source code in components/support/public/dependencies.py
@abstractmethod
def is_intercom_receipt_sent_for_context(
    self, legacy_conversation_context_user_id: str
) -> bool:
    """
    For a given care conversation context id (in the current country), returns the fact that an initial
    async email answer was already sent to the user.
    """
    pass

is_legacy_callback_timeslot_reminder_sent abstractmethod

is_legacy_callback_timeslot_reminder_sent(
    callback_timeslot_id,
)

Checks if a legacy callback reminder has already been sent before we send the global one to avoid duplicates

Source code in components/support/public/dependencies.py
@abstractmethod
def is_legacy_callback_timeslot_reminder_sent(
    self, callback_timeslot_id: uuid.UUID
) -> bool:
    """
    Checks if a legacy callback reminder has already been sent before we send the global one to avoid duplicates
    """
    pass

is_user_an_offline_experience_member abstractmethod

is_user_an_offline_experience_member(user_id)

Tells if a user is an offline experience member.

Source code in components/support/public/dependencies.py
@abstractmethod
def is_user_an_offline_experience_member(self, user_id: str) -> bool:
    """
    Tells if a user is an offline experience member.
    """
    pass

mark_intercom_receipt_sent_for_context abstractmethod

mark_intercom_receipt_sent_for_context(
    legacy_conversation_context_id, commit=True
)

For a given care conversation context id (in the current country), marks the fact that an initial async email answer was sent to the user.

Source code in components/support/public/dependencies.py
@abstractmethod
def mark_intercom_receipt_sent_for_context(
    self, legacy_conversation_context_id: str, commit: bool = True
) -> None:
    """
    For a given care conversation context id (in the current country), marks the fact that an initial
    async email answer was sent to the user.
    """
    pass

mark_legacy_callback_timeslot_reminder_sent abstractmethod

mark_legacy_callback_timeslot_reminder_sent(
    callback_timeslot_id, commit=True
)

Mark a legacy callback reminder has already been sent when we send the global one to avoid duplicates

Source code in components/support/public/dependencies.py
@abstractmethod
def mark_legacy_callback_timeslot_reminder_sent(
    self, callback_timeslot_id: uuid.UUID, commit: bool = True
) -> None:
    """
    Mark a legacy callback reminder has already been sent when we send the global one to avoid duplicates
    """
    pass

remove_legacy_phone_number_storage_for_conversation_context abstractmethod

remove_legacy_phone_number_storage_for_conversation_context(
    conversation_context_id, commit=True
)

Allows removing the phone number from a care conversation context, allowing to clean them even if/when we remove the existing country-specific scripts (only relevant for France).

Source code in components/support/public/dependencies.py
@abstractmethod
def remove_legacy_phone_number_storage_for_conversation_context(
    self, conversation_context_id: uuid.UUID, commit: bool = True
) -> None:
    """
    Allows removing the phone number from a care conversation context, allowing to clean them
    even if/when we remove the existing country-specific scripts (only relevant for France).
    """
    pass

sync_legacy_assigner_data abstractmethod

sync_legacy_assigner_data(conversation_ids)

Retrocompatibility logic to update legacy database with assignment changes

Source code in components/support/public/dependencies.py
@abstractmethod
def sync_legacy_assigner_data(self, conversation_ids: list[str]) -> None:
    """
    Retrocompatibility logic to update legacy database with assignment changes
    """
    pass

update_legacy_phone_support_csat abstractmethod

update_legacy_phone_support_csat(
    ino_interaction_id, rating, comment
)

Updates the phone support CSAT for the legacy phone support.

Source code in components/support/public/dependencies.py
@abstractmethod
def update_legacy_phone_support_csat(
    self,
    ino_interaction_id: str,
    rating: int | None,
    comment: str | None,
) -> None:
    """
    Updates the phone support CSAT for the legacy phone support.
    """
    pass

get_app_dependency

get_app_dependency()
Source code in components/support/public/dependencies.py
def get_app_dependency() -> SupportDependency:  # noqa: D103
    from flask import current_app

    return cast("CustomFlask", current_app).get_component_dependency(COMPONENT_NAME)  # type: ignore[no-any-return]

set_app_dependency

set_app_dependency(dependency)
Source code in components/support/public/dependencies.py
def set_app_dependency(dependency: SupportDependency) -> None:  # noqa: D103
    from flask import current_app

    cast("CustomFlask", current_app).add_component_dependency(
        COMPONENT_NAME, dependency
    )

components.support.public.entities

assignment_entities

ContactRequestAssignmentSource

Bases: AlanBaseEnum

backend class-attribute instance-attribute
backend = 'backend'
intercom class-attribute instance-attribute
intercom = 'intercom'
manual class-attribute instance-attribute
manual = 'manual'

channel_management_entities

CareChannel

Bases: AlanBaseEnum

All channels that are available for care

callback class-attribute instance-attribute
callback = 'callback'
chat class-attribute instance-attribute
chat = 'chat'
email class-attribute instance-attribute
email = 'email'
hotline class-attribute instance-attribute
hotline = 'hotline'
immediate_callback class-attribute instance-attribute
immediate_callback = 'immediate_callback'

ContactCountryDetails dataclass

ContactCountryDetails(
    account_ids,
    population_ids,
    is_eligible_for_hotline=False,
    is_eligible_for_one_hour_callback=False,
    is_eligible_for_two_days_callback=False,
    hotline_phone_number=None,
)

Country specific details for a user/role pair.

account_ids instance-attribute
account_ids
hotline_phone_number class-attribute instance-attribute
hotline_phone_number = None
is_eligible_for_hotline class-attribute instance-attribute
is_eligible_for_hotline = False
is_eligible_for_one_hour_callback class-attribute instance-attribute
is_eligible_for_one_hour_callback = False
is_eligible_for_two_days_callback class-attribute instance-attribute
is_eligible_for_two_days_callback = False
population_ids instance-attribute
population_ids

PhoneSupportEligibility dataclass

PhoneSupportEligibility(
    is_eligible_for_hotline=False,
    is_eligible_for_one_hour_callback=False,
    is_eligible_for_two_days_callback=False,
    hotline_phone_number=None,
    callback_phone_number=None,
)

Groups phone support eligibility information

callback_phone_number class-attribute instance-attribute
callback_phone_number = None
hotline_phone_number class-attribute instance-attribute
hotline_phone_number = None
is_eligible_for_hotline class-attribute instance-attribute
is_eligible_for_hotline = False
is_eligible_for_one_hour_callback class-attribute instance-attribute
is_eligible_for_one_hour_callback = False
is_eligible_for_two_days_callback class-attribute instance-attribute
is_eligible_for_two_days_callback = False

PopulationConfiguration dataclass

PopulationConfiguration(config)

A grouping of UserSubsetConfiguration instances, grouped by a top level setting.

config instance-attribute
config
from_config classmethod
from_config(config)

Initialize a PopulationConfiguration from a config dict.

Source code in components/support/subcomponents/channel_management/protected/entities/population_configuration.py
@classmethod
def from_config(
    cls, config: PopulationFrontendDescriptionType
) -> "PopulationConfiguration":
    """
    Initialize a PopulationConfiguration from a config dict.
    """
    return PopulationConfiguration(
        config={
            cast("str", c.get("id")): UserSubsetConfiguration.from_config(c)
            for c in config
        }
    )
is_valid
is_valid(settings)

Check if the settings are valid for the current PopulationConfiguration.

All subsets need to be valid, and at least one needs to be enabled.

Source code in components/support/subcomponents/channel_management/protected/entities/population_configuration.py
def is_valid(self, settings: list[str]) -> bool:
    """
    Check if the settings are valid for the current PopulationConfiguration.

    All subsets need to be valid, and at least one needs to be enabled.
    """
    depth_check = all(c.is_valid(settings) for c in self.config.values())
    first_level_check = any(k in settings for k in self.config.keys())

    return depth_check and first_level_check

PopulationFrontendDescriptionType module-attribute

PopulationFrontendDescriptionType = NewType(
    "PopulationFrontendDescriptionType",
    list[UserSubsetConfigurationType],
)

Frontend layout for the population configuration is driven by the following structure:

[ { "id": "disable_all_members", "description": "Disable for all members", "filtering": [ { "id": "disable_member_ani", "description": "disable ANI", }, ], }, ... ]

There may be no filtering, or multiple filtering options. If any filtering is present, the "top level" id cannot be present.

From this description, we expect the actual choice to be expressed as a simple list of string ids.

SupportAvailabilityDetails dataclass

SupportAvailabilityDetails(
    opening_status,
    phone_support_eligibility,
    active_cutoff_period=None,
)

Represents the availability of support for a given population and account.

active_cutoff_period class-attribute instance-attribute
active_cutoff_period = None
opening_status instance-attribute
opening_status
phone_support_eligibility instance-attribute
phone_support_eligibility

SupportOpeningStatus dataclass

SupportOpeningStatus(
    is_open,
    banner_message,
    closes_at,
    email_answer_by,
    chat_answer_by,
    hotline_answer_by,
    immediate_callback_answer_by,
    callback_answer_by,
)

Tells if the support is open or not. Optionally, a general (service wide) banner message can be provided.

banner_message instance-attribute
banner_message
callback_answer_by instance-attribute
callback_answer_by
chat_answer_by instance-attribute
chat_answer_by
closes_at instance-attribute
closes_at
email_answer_by instance-attribute
email_answer_by
hotline_answer_by instance-attribute
hotline_answer_by
immediate_callback_answer_by instance-attribute
immediate_callback_answer_by
is_open instance-attribute
is_open

classification_entities

ClassificationResult dataclass

ClassificationResult(
    conversation_id,
    prediction,
    prevent_assignment_reason=None,
    default_assignment_disabled=False,
    inbox_id=None,
    jtbd_id=None,
    is_assignment_to_current_inbox=False,
)
can_assign
can_assign()
Source code in components/support/internal/entities/classification_result.py
def can_assign(self) -> bool:
    return (
        self.prevent_assignment_reason is None
        and not self.default_assignment_disabled
        and self.inbox_id is not None
        and not self.is_assignment_to_current_inbox
    )
can_tag
can_tag()
Source code in components/support/internal/entities/classification_result.py
def can_tag(self) -> bool:
    return self.is_jtbd_prediction() and self.jtbd_id is not None
conversation_id instance-attribute
conversation_id
default_assignment_disabled class-attribute instance-attribute
default_assignment_disabled = False
get_classification_note
get_classification_note()
Source code in components/support/internal/entities/classification_result.py
def get_classification_note(self) -> str:
    if self.is_spe_prediction():
        return f"Classified by backend to {self.prediction.predicted_inbox} with probability {self.prediction.probability:.1%}."  # type: ignore[union-attr]
    else:
        jtbd_prediction = self.prediction.jtbd_prediction  # type: ignore[union-attr]
        return f"""
        🧙 **Tag <em>{jtbd_prediction}</em> automatiquement ajouté par notre I.A. Harry** 🧙
        Ce n'est pas le bon tag ?  Vous pouvez le changer directement depuis le Tag Helper dans l'Assistant et entraîner Harry pour la prochaine fois.
        """
inbox_id class-attribute instance-attribute
inbox_id = None
is_assignment_to_current_inbox class-attribute instance-attribute
is_assignment_to_current_inbox = False
is_jtbd_prediction
is_jtbd_prediction()
Source code in components/support/internal/entities/classification_result.py
def is_jtbd_prediction(self) -> bool:
    return isinstance(self.prediction, JTBDClassificationResult)
is_spe_prediction
is_spe_prediction()
Source code in components/support/internal/entities/classification_result.py
def is_spe_prediction(self) -> bool:
    return isinstance(self.prediction, SpeClassificationResult)
jtbd_id class-attribute instance-attribute
jtbd_id = None
prediction instance-attribute
prediction
prevent_assignment_reason class-attribute instance-attribute
prevent_assignment_reason = None
to_dict
to_dict()
Source code in components/support/internal/entities/classification_result.py
def to_dict(self) -> dict[str, Any]:
    return asdict(self)

HarrySpeClassificationResult dataclass

HarrySpeClassificationResult(
    top_4_spe_inboxes=None, chain_of_thought=None
)
chain_of_thought class-attribute instance-attribute
chain_of_thought = None
top_4_spe_inboxes class-attribute instance-attribute
top_4_spe_inboxes = None

JTBDClassificationResult dataclass

JTBDClassificationResult(
    chain_of_thought_1,
    top_3_jtbd_predictions,
    chain_of_thought_2,
    jtbd_prediction,
    predicted_inbox=None,
    raw_prediction=None,
    raw_prediction_classes=None,
)
chain_of_thought_1 instance-attribute
chain_of_thought_1
chain_of_thought_2 instance-attribute
chain_of_thought_2
jtbd_prediction instance-attribute
jtbd_prediction
predicted_inbox class-attribute instance-attribute
predicted_inbox = None
raw_prediction class-attribute instance-attribute
raw_prediction = None
raw_prediction_classes class-attribute instance-attribute
raw_prediction_classes = None
top_3_jtbd_predictions instance-attribute
top_3_jtbd_predictions

SpeClassificationResult dataclass

SpeClassificationResult(
    raw_prediction=None,
    raw_prediction_classes=None,
    predicted_inbox=None,
    probability=None,
)
predicted_inbox class-attribute instance-attribute
predicted_inbox = None
probability class-attribute instance-attribute
probability = None
raw_prediction class-attribute instance-attribute
raw_prediction = None
raw_prediction_classes class-attribute instance-attribute
raw_prediction_classes = None

contact_country_details

ContactCountryDetails dataclass

ContactCountryDetails(
    account_ids,
    population_ids,
    is_eligible_for_hotline=False,
    is_eligible_for_one_hour_callback=False,
    is_eligible_for_two_days_callback=False,
    hotline_phone_number=None,
)

Country specific details for a user/role pair.

account_ids instance-attribute
account_ids
hotline_phone_number class-attribute instance-attribute
hotline_phone_number = None
is_eligible_for_hotline class-attribute instance-attribute
is_eligible_for_hotline = False
is_eligible_for_one_hour_callback class-attribute instance-attribute
is_eligible_for_one_hour_callback = False
is_eligible_for_two_days_callback class-attribute instance-attribute
is_eligible_for_two_days_callback = False
population_ids instance-attribute
population_ids

contact_request_entities

ContactRequestWithTagsAndIntercomStateEntity dataclass

ContactRequestWithTagsAndIntercomStateEntity(
    id,
    intercom_conversation_id,
    intercom_workspace_id,
    source_type,
    has_been_processed_as_new_conversation,
    duplicated_from_id,
    classification_result,
    app_id,
    app_user_id,
    legacy_conversation_context_id,
    legacy_conversation_context_app_id,
    source_id,
    created_at,
    updated_at,
    intercom_state,
    tags,
)

Bases: ContactRequestEntity

from_model classmethod
from_model(model)

Beware, this method calls contact_request.intercom_state and contact_request.tags, which can trigger N+1 queries. Please make sure that's okay or that you've already joined the intercom_state and tags in the query that got the contact_request.

Source code in components/support/internal/entities/contact_request_entity.py
@classmethod
def from_model(
    cls, model: "ContactRequest"
) -> "ContactRequestWithTagsAndIntercomStateEntity":
    """
    Beware, this method calls contact_request.intercom_state and contact_request.tags,
    which can trigger N+1 queries. Please make sure that's okay or that you've already joined the intercom_state and tags
    in the query that got the contact_request.
    """
    return cls(
        id=model.id,
        intercom_conversation_id=model.intercom_conversation_id,
        intercom_workspace_id=model.intercom_workspace_id,
        source_type=model.source_type,
        has_been_processed_as_new_conversation=model.has_been_processed_as_new_conversation,
        duplicated_from_id=model.duplicated_from_id,
        classification_result=model.classification_result,
        intercom_state=ContactRequestIntercomStateEntity.from_model(
            model.intercom_state
        )
        if model.intercom_state
        else ContactRequestIntercomStateEntity(
            id=None,
            contact_request_id=model.id,
            waiting_since=None,
            state=None,
            last_inbox_assignment_by=None,
            started_at=None,
            last_user_reply_at=None,
            assigned_intercom_inbox_id=None,
            assigned_intercom_admin_id=None,
            last_admin_reply_at=None,
        ),
        tags=[ContactRequestTagEntity.from_model(tag) for tag in model.tags]
        if model.tags
        else [],
        app_id=model.app_id,
        app_user_id=model.app_user_id,
        legacy_conversation_context_id=model.legacy_conversation_context_id,
        legacy_conversation_context_app_id=model.legacy_conversation_context_app_id,
        source_id=model.source.id if model.source else None,
        created_at=model.created_at,
        updated_at=model.updated_at,
    )
intercom_state instance-attribute
intercom_state
tags instance-attribute
tags

legacy_conversation_backlog

LegacyConversationAssignment dataclass

LegacyConversationAssignment(
    id,
    admin_id,
    action,
    rejection_reason,
    comment,
    reassigned_to_inbox_id,
    assigner_result,
    assigner_results_for_audit,
    assignment_type,
    created_at,
    updated_at,
)

This class represents a legacy Contact Request Assignment

action instance-attribute
action
admin_id instance-attribute
admin_id
assigner_result instance-attribute
assigner_result
assigner_results_for_audit instance-attribute
assigner_results_for_audit
assignment_type instance-attribute
assignment_type
comment instance-attribute
comment
created_at instance-attribute
created_at
id instance-attribute
id
reassigned_to_inbox_id instance-attribute
reassigned_to_inbox_id
rejection_reason instance-attribute
rejection_reason
updated_at instance-attribute
updated_at

LegacyConversationBacklog dataclass

LegacyConversationBacklog(
    admin_id,
    inbox_id,
    state,
    last_inbox_assignment_by,
    started_at,
)

This class represents a legacy Contact Request Intercom State

admin_id instance-attribute
admin_id
inbox_id instance-attribute
inbox_id
last_inbox_assignment_by instance-attribute
last_inbox_assignment_by
started_at instance-attribute
started_at
state instance-attribute
state

LegacyConversationTag dataclass

LegacyConversationTag(tag_name, tag_id, notified_at)

This class represents a legacy Contact Request Tag

notified_at instance-attribute
notified_at
tag_id instance-attribute
tag_id
tag_name instance-attribute
tag_name

legacy_support_agent

LegacySupportAgent dataclass

LegacySupportAgent(
    alan_email,
    intercom_admin_id,
    spoken_languages,
    work_location,
    is_external_admin,
    platform_name,
)

This class represents a legacy support agent.

alan_email instance-attribute
alan_email
intercom_admin_id instance-attribute
intercom_admin_id
is_external_admin instance-attribute
is_external_admin
platform_name instance-attribute
platform_name
spoken_languages instance-attribute
spoken_languages
work_location instance-attribute
work_location

LegacySupportAgentAffectation dataclass

LegacySupportAgentAffectation(
    intercom_workspace_id,
    is_assigner_enabled,
    roles,
    level,
    spe_matching_scores,
)

This class represents a legacy support agent affectation.

intercom_workspace_id instance-attribute
intercom_workspace_id
is_assigner_enabled instance-attribute
is_assigner_enabled
level instance-attribute
level
roles instance-attribute
roles
spe_matching_scores instance-attribute
spe_matching_scores

LegacySupportAgentSpeMatchingScore dataclass

LegacySupportAgentSpeMatchingScore(
    score, intercom_inbox_id
)

This class represents a legacy support agent spe matching score.

intercom_inbox_id instance-attribute
intercom_inbox_id
score instance-attribute
score

phone_support_entities

CallbackSuggestedTimeslot dataclass

CallbackSuggestedTimeslot(start, end, event_link=None)

Bases: DataClassJsonMixin

end instance-attribute
end
event_link = None
from_controller_param_timeslot classmethod
from_controller_param_timeslot(controller_param_timeslot)
Source code in components/support/subcomponents/phone_support/internal/entities/callback_suggested_timeslot.py
@classmethod
def from_controller_param_timeslot(
    cls, controller_param_timeslot: dict[str, Any]
) -> "CallbackSuggestedTimeslot":
    try:
        return CallbackSuggestedTimeslot(
            start=isoparse(controller_param_timeslot["start"]),
            end=isoparse(controller_param_timeslot["end"]),
            event_link=None,
        )
    except KeyError:
        raise ValueError("Provided suggested timeslot data is not valid")
start instance-attribute
start

HotlineQueue

Bases: AlanBaseEnum

ECOLOGY class-attribute instance-attribute
ECOLOGY = 'ECOLOGY'
PRIORITY class-attribute instance-attribute
PRIORITY = 'PRIORITY'
REGULAR class-attribute instance-attribute
REGULAR = 'REGULAR'
RETIREE_MEF class-attribute instance-attribute
RETIREE_MEF = 'RETIREE_MEF'

INOPhoneCallStatus

Bases: AlanBaseEnum

attempt_limit class-attribute instance-attribute
attempt_limit = 'ATTEMPT_LIMIT'
canceled class-attribute instance-attribute
canceled = 'CANCELED'
card_not_found class-attribute instance-attribute
card_not_found = 'CARD_NOT_FOUND'
deprogrammed class-attribute instance-attribute
deprogrammed = 'DEPROGRAMMED'
dissuaded class-attribute instance-attribute
dissuaded = 'DISSUADED'
done class-attribute instance-attribute
done = 'DONE'
failed class-attribute instance-attribute
failed = 'FAILED'
machine_detected class-attribute instance-attribute
machine_detected = 'MACHINE_DETECTED'
max_try class-attribute instance-attribute
max_try = 'MAX_TRY'
no_phone_number class-attribute instance-attribute
no_phone_number = 'NO_PHONE_NUMBER'
no_response class-attribute instance-attribute
no_response = 'NO_RESPONSE'
no_user class-attribute instance-attribute
no_user = 'NO_USER'
not_posted class-attribute instance-attribute
not_posted = 'NOT_POSTED'
pending class-attribute instance-attribute
pending = 'PENDING'
treated class-attribute instance-attribute
treated = 'TREATED'

LegacyCallbackTimeslot dataclass

LegacyCallbackTimeslot(
    id,
    google_event_id,
    google_event_link,
    timeslot_start,
    timeslot_end,
    is_cancelled,
    is_reminder_sent,
    reminder_sent_at,
    alan_email,
    alan_employee_id,
    alan_employee_app_id,
)

Bases: DataClassJsonMixin

alan_email instance-attribute
alan_email
alan_employee_app_id instance-attribute
alan_employee_app_id
alan_employee_id instance-attribute
alan_employee_id
google_event_id instance-attribute
google_event_id
google_event_link
id instance-attribute
id
is_cancelled instance-attribute
is_cancelled
is_reminder_sent instance-attribute
is_reminder_sent
reminder_sent_at instance-attribute
reminder_sent_at
timeslot_end instance-attribute
timeslot_end
timeslot_start instance-attribute
timeslot_start

LegacyCallbackTimeslotsSuggestion dataclass

LegacyCallbackTimeslotsSuggestion(id, suggested_timeslots)

Bases: DataClassJsonMixin

id instance-attribute
id
suggested_timeslots instance-attribute
suggested_timeslots

LegacyPhoneCall dataclass

LegacyPhoneCall(
    id,
    ino_interaction_id,
    start,
    duration_in_seconds,
    alan_email,
    alan_employee_id,
    alan_employee_app_id,
    legacy_phone_call_recordings,
    created_at,
)

Bases: DataClassJsonMixin

Represents a phone call from legacy country-specific table

alan_email instance-attribute
alan_email
alan_employee_app_id instance-attribute
alan_employee_app_id
alan_employee_id instance-attribute
alan_employee_id
created_at instance-attribute
created_at
duration_in_seconds instance-attribute
duration_in_seconds
id instance-attribute
id
ino_interaction_id instance-attribute
ino_interaction_id
legacy_phone_call_recordings instance-attribute
legacy_phone_call_recordings
start instance-attribute
start

LegacyPhoneCallRecording dataclass

LegacyPhoneCallRecording(id, archived_at, uri, created_at)

Bases: DataClassJsonMixin

Represents a phone call recording from legacy country-specific table

archived_at instance-attribute
archived_at
created_at instance-attribute
created_at
id instance-attribute
id
uri instance-attribute
uri

NonOfflineUserInfo dataclass

NonOfflineUserInfo(
    is_eligible=False,
    is_alan_member=False,
    is_alaner=False,
    is_offline_member=False,
    is_offline_experience_company=False,
    redirect_to_queue=HotlineQueue.REGULAR,
)

Bases: DataClassJsonMixin

is_alan_member class-attribute instance-attribute
is_alan_member = False
is_alaner class-attribute instance-attribute
is_alaner = False
is_eligible class-attribute instance-attribute
is_eligible = False
is_offline_experience_company class-attribute instance-attribute
is_offline_experience_company = False
is_offline_member class-attribute instance-attribute
is_offline_member = False
redirect_to_queue class-attribute instance-attribute
redirect_to_queue = REGULAR

support_agent_for_response

CountryLocation

Bases: AlanBaseEnum

Work location of the support agents. Used to determine if the support agent is "offshore" or not. E.g. a support agent based in Morocco is considered offshore for France, and can't be assigned certain French company tickets (e.g. public sector etc).

be class-attribute instance-attribute
be = 'be'
ca class-attribute instance-attribute
ca = 'ca'
es class-attribute instance-attribute
es = 'es'
fr class-attribute instance-attribute
fr = 'fr'
ma class-attribute instance-attribute
ma = 'ma'
other class-attribute instance-attribute
other = 'other'

SpokenLanguage

Bases: AlanBaseEnum

Languages that can be spoken by a support agent. Uses ISO 639-1 two-letter language codes.

Current supported languages: - fr: French - en: English - es: Spanish - nl: Dutch

ca class-attribute instance-attribute
ca = 'ca'
en class-attribute instance-attribute
en = 'en'
es class-attribute instance-attribute
es = 'es'
fr class-attribute instance-attribute
fr = 'fr'
nl class-attribute instance-attribute
nl = 'nl'

SupportAgentRole

Bases: AlanBaseEnum

Roles that can be assigned to a support agent

automated_answer_reviewer class-attribute instance-attribute
automated_answer_reviewer = 'automated_answer_reviewer'
eligible_for_async class-attribute instance-attribute
eligible_for_async = 'eligible_for_async'
eligible_for_callback class-attribute instance-attribute
eligible_for_callback = 'eligible_for_callback'
eligible_for_sync class-attribute instance-attribute
eligible_for_sync = 'eligible_for_sync'

SupportAgentSpeMatchingScore

Bases: BaseModel

__table_args__ class-attribute instance-attribute
__table_args__ = (
    UniqueConstraint(
        "support_agent_workspace_affectation_id",
        "support_specialization_id",
        name="uq_support_agent_spe_matching_score_affectation_spe",
    ),
    {"schema": SUPPORT_SCHEMA_NAME},
)
__tablename__ class-attribute instance-attribute
__tablename__ = 'support_agent_spe_matching_score'
score class-attribute instance-attribute
score = mapped_column(Float, nullable=False, index=True)
support_agent_workspace_affectation class-attribute instance-attribute
support_agent_workspace_affectation = relationship(
    "SupportAgentWorkspaceAffectation",
    back_populates="spe_matching_scores",
)
support_agent_workspace_affectation_id class-attribute instance-attribute
support_agent_workspace_affectation_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    nullable=False,
    index=True,
)
support_specialization class-attribute instance-attribute
support_specialization = relationship(
    "SupportSpecialization"
)
support_specialization_id class-attribute instance-attribute
support_specialization_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    nullable=False,
    index=True,
)

SupportAgentWithAffectationForResponse dataclass

SupportAgentWithAffectationForResponse(
    id,
    work_location,
    spoken_languages,
    platform_name,
    affectations,
    alan_email,
    alan_employee_id,
    alan_employee_app_id,
    is_external_admin,
    is_deleted,
    intercom_admin_id,
)

Bases: DataClassJsonMixin

affectations instance-attribute
affectations
alan_email instance-attribute
alan_email
alan_employee_app_id instance-attribute
alan_employee_app_id
alan_employee_id instance-attribute
alan_employee_id
from_support_agent classmethod
from_support_agent(support_agent)
Source code in components/support/internal/entities/support_agent_for_response.py
@classmethod
def from_support_agent(
    cls,
    support_agent: SupportAgent,
) -> "SupportAgentWithAffectationForResponse":
    return SupportAgentWithAffectationForResponse(
        id=support_agent.id,
        work_location=support_agent.work_location,
        spoken_languages=support_agent.spoken_languages,
        platform_name=support_agent.platform_name,
        alan_email=support_agent.alan_email,
        alan_employee_id=support_agent.alan_employee_id,
        alan_employee_app_id=support_agent.alan_employee_app_id,
        is_external_admin=support_agent.is_external_admin,
        is_deleted=support_agent.is_deleted,
        intercom_admin_id=support_agent.intercom_admin_id,
        affectations=[
            SupportAgentWorkspaceAffectationForResponse.from_support_agent_workspace_affectation(
                affectation
            )
            for affectation in support_agent.affectations
        ],
    )
id instance-attribute
id
intercom_admin_id instance-attribute
intercom_admin_id
is_deleted instance-attribute
is_deleted
is_external_admin instance-attribute
is_external_admin
platform_name instance-attribute
platform_name
spoken_languages instance-attribute
spoken_languages
work_location instance-attribute
work_location

SupportAgentWorkspaceAffectation

Bases: BaseModel

__table_args__ class-attribute instance-attribute
__table_args__ = (
    UniqueConstraint(
        "support_agent_id",
        "intercom_workspace_id",
        name="uq_support_agent_workspace_affectation_agent_workspace",
    ),
    {"schema": SUPPORT_SCHEMA_NAME},
)
__tablename__ class-attribute instance-attribute
__tablename__ = 'support_agent_workspace_affectation'
intercom_workspace_id class-attribute instance-attribute
intercom_workspace_id = mapped_column(
    Text, nullable=False, index=True
)
is_active class-attribute instance-attribute
is_active = mapped_column(
    Boolean,
    nullable=False,
    server_default=true(),
    index=True,
)
is_assigner_enabled class-attribute instance-attribute
is_assigner_enabled = mapped_column(
    Boolean,
    nullable=False,
    server_default=false(),
    index=True,
)
level class-attribute instance-attribute
level = mapped_column(
    Integer,
    default=1,
    server_default="1",
    nullable=False,
    index=True,
)
roles class-attribute instance-attribute
roles = mapped_column(
    AlanBaseEnumArrayTypeDecorator(SupportAgentRole),
    nullable=False,
    default=[],
    index=True,
)
spe_matching_scores class-attribute instance-attribute
spe_matching_scores = relationship(
    "SupportAgentSpeMatchingScore",
    back_populates="support_agent_workspace_affectation",
    order_by="SupportAgentSpeMatchingScore.created_at.asc()",
)
support_agent class-attribute instance-attribute
support_agent = relationship(
    "SupportAgent", back_populates="affectations"
)
support_agent_id class-attribute instance-attribute
support_agent_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    nullable=False,
    index=True,
)

tag_entities

AutomatedAnswerTag

Bases: AlanBaseEnum

automated_answer_in_app class-attribute instance-attribute
automated_answer_in_app = (
    "Automated answer[answered in app]"
)
automated_answer_waiting_for_review class-attribute instance-attribute
automated_answer_waiting_for_review = (
    "Automated answer[waiting for review]"
)

RoleTag

Bases: AlanBaseEnum

callback class-attribute instance-attribute
callback = 'ROLE[CALLBACK]'
english class-attribute instance-attribute
english = 'ROLE[ENGLISH]'
georestricted class-attribute instance-attribute
georestricted = 'ROLE[GEORESTRICTED]'
level_1 class-attribute instance-attribute
level_1 = 'ROLE[LEVEL1]'
level_2 class-attribute instance-attribute
level_2 = 'ROLE[LEVEL2]'
level_3 class-attribute instance-attribute
level_3 = 'ROLE[LEVEL3]'
onepilot class-attribute instance-attribute
onepilot = 'ROLE[ONEPILOT]'
public_sector_ecology class-attribute instance-attribute
public_sector_ecology = 'Public Sector [MTEECPR]'
public_sector_spm class-attribute instance-attribute
public_sector_spm = 'Public Sector [SPM]'
role_level_tag_for_level staticmethod
role_level_tag_for_level(level)
Source code in components/support/internal/entities/tags.py
@staticmethod
def role_level_tag_for_level(level: int) -> "RoleTag":
    if level == 1:
        return RoleTag.level_1
    if level == 2:
        return RoleTag.level_2
    if level == 3:
        return RoleTag.level_3
    raise ValueError(f"Invalid role level: {level}")
webhelp class-attribute instance-attribute
webhelp = 'ROLE[WEBHELP]'

SlaTag

Bases: AlanBaseEnum

sla_async class-attribute instance-attribute
sla_async = 'SLA[12h]'
sla_callback class-attribute instance-attribute
sla_callback = 'SLA[30min]'
sla_sync class-attribute instance-attribute
sla_sync = 'SLA[5min]'

components.support.public.enums

contact_request_source_type

ContactRequestSourceType

Bases: AlanBaseEnum

A ContactRequest instance's "source_type" represents how the user contacted us. It can be an Intercom sync conversation, an async email, a callback request, etc.

async_conversation_request class-attribute instance-attribute
async_conversation_request = 'async_conversation_request'
callback_request class-attribute instance-attribute
callback_request = 'callback_request'
hotline_request class-attribute instance-attribute
hotline_request = 'hotline_request'
hotline_voice_message_request class-attribute instance-attribute
hotline_voice_message_request = (
    "hotline_voice_message_request"
)
immediate_callback_request class-attribute instance-attribute
immediate_callback_request = 'immediate_callback_request'
lead_callback_request class-attribute instance-attribute
lead_callback_request = 'lead_callback_request'
sync_conversation_request class-attribute instance-attribute
sync_conversation_request = 'sync_conversation_request'
unknown class-attribute instance-attribute
unknown = 'unknown'

contact_role_type

ContactRoleType

Bases: AlanBaseEnum

Tells the role of the user making the contact request, as seen from the apps.

admin class-attribute instance-attribute
admin = 'admin'
lead class-attribute instance-attribute
lead = 'lead'
member class-attribute instance-attribute
member = 'member'

population_id

PopulationId

Bases: AlanBaseEnum

Population identifiers used for support channel management and cutoff periods. These identify different user groups for support availability.

admins_large_companies class-attribute instance-attribute
admins_large_companies = 'admins_large_companies'
admins_small_companies class-attribute instance-attribute
admins_small_companies = 'admins_small_companies'
all_admins class-attribute instance-attribute
all_admins = 'all_admins'
all_leads class-attribute instance-attribute
all_leads = 'all_leads'
all_members class-attribute instance-attribute
all_members = 'all_members'
ani_members class-attribute instance-attribute
ani_members = 'ani_members'
covered_members class-attribute instance-attribute
covered_members = 'covered_members'
everyone class-attribute instance-attribute
everyone = 'everyone'
non_covered_members class-attribute instance-attribute
non_covered_members = 'non_covered_members'
tns_members class-attribute instance-attribute
tns_members = 'tns_members'

components.support.public.helpers

assignment_rejection_reasons

is_callback_rejection_reason

is_callback_rejection_reason(reason)

Returns a boolean based on whether the rejection reason is due to the ticket being a callback ticket

Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
def is_callback_rejection_reason(reason: str) -> bool:
    """
    Returns a boolean based on whether the rejection reason is due to the ticket being a callback ticket
    """
    return reason is not None and reason == AssignmentRejectionReason.call_back_handover

is_english_rejection_reason

is_english_rejection_reason(reason)

Returns a boolean based on whether the rejection reason is due to the ticket being in English

Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
def is_english_rejection_reason(reason: str) -> bool:
    """
    Returns a boolean based on whether the rejection reason is due to the ticket being in English
    """
    return reason is not None and reason in [
        AssignmentRejectionReason.english_rejection
    ]

is_handover_reason

is_handover_reason(reason)

Returns a boolean based on whether the rejection reason is due to a handover

Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
def is_handover_reason(reason: str) -> bool:
    """
    Returns a boolean based on whether the rejection reason is due to a handover
    """
    return reason is not None and reason in HANDOVER_REJECTION_REASONS

is_level_1_rejection_reason

is_level_1_rejection_reason(reason)

Returns a boolean based on whether the rejection reason is a "level 1" reason

Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
def is_level_1_rejection_reason(reason: str) -> bool:
    """
    Returns a boolean based on whether the rejection reason is a "level 1" reason
    """
    return reason is not None and reason in [
        AssignmentRejectionReason.level_1_workflow_rejection,
        AssignmentRejectionReason.level_1_workflow_handover,
    ]

is_level_2_rejection_reason

is_level_2_rejection_reason(reason)

Returns a boolean based on whether the rejection reason is a "level 2" reason

Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
def is_level_2_rejection_reason(reason: str) -> bool:
    """
    Returns a boolean based on whether the rejection reason is a "level 2" reason
    """
    return reason is not None and reason in [
        AssignmentRejectionReason.no_workflow_rejection,
        AssignmentRejectionReason.level_2_workflow_rejection,
        AssignmentRejectionReason.workflow_unclear_or_incomplete_handover,
        AssignmentRejectionReason.level_2_workflow_handover,
    ]

is_level_3_rejection_reason

is_level_3_rejection_reason(reason)

Returns a boolean based on whether the rejection reason is a "level 3" reason

Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
def is_level_3_rejection_reason(reason: str) -> bool:
    """
    Returns a boolean based on whether the rejection reason is a "level 3" reason
    """
    return reason is not None and reason in [
        AssignmentRejectionReason.level_3_workflow_rejection,
        AssignmentRejectionReason.level_3_workflow_handover,
    ]

components.support.public.queries

documents

get_document

get_document(document_id)

Get the document name and document content by its ID.

Source code in components/support/public/queries/documents.py
def get_document(document_id: UUID) -> tuple[str, io.BytesIO] | None:
    """
    Get the document name and document content by its ID.
    """
    from components.documents.public.business_logic.document.queries import (
        get_document_content,
        get_document_info,
    )
    from shared.errors.error_code import BaseErrorCode

    try:
        document = get_document_content(document_id=document_id)

        document_info = get_document_info(document_id=document_id)

        document_name: str = (
            str(document_info.upload_metadata["filename"])
            if document_info.upload_metadata
            and "filename" in document_info.upload_metadata
            else document_info.filename
        )
        return document_name, document.file
    except BaseErrorCode as e:
        current_logger.error(
            f"support: get_document not found for document_id: {document_id}: {str(e)}",
        )
        return None

components.support.public.rules

get_conversation_body_for_intercom_receipt

get_conversation_body_for_intercom_receipt(conversation)

For a given IntercomConversation we get from the Intercom API or webhook payload, returns the message body without the [alan-attr] that's added automatically in async conversations, and returns a string version (no html).

Source code in components/support/public/rules.py
def get_conversation_body_for_intercom_receipt(
    conversation: IntercomConversation,
) -> str:
    """
    For a given IntercomConversation we get from the Intercom API or webhook payload,
    returns the message body *without* the [alan-attr] that's added automatically in async
    conversations, and returns a string version (no html).
    """
    from bs4 import BeautifulSoup

    soup = BeautifulSoup(conversation.conversation_message.body, "html.parser")
    split_message = soup.stripped_strings
    message = ""

    for part_message in split_message:
        message += f"{part_message}\n"

    # remove the alan-attr part from the message
    message = message.split("[alan-attr:")[0]
    return message

components.support.public.services

base_intercom_handler_service

BaseIntercomHandlerService

Bases: ABC

Base class for all Intercom handler services.

handle_conversation_admin_assigned abstractmethod staticmethod
handle_conversation_admin_assigned(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_assigned topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_assigned(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_assigned topic.
    """
handle_conversation_admin_closed abstractmethod staticmethod
handle_conversation_admin_closed(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_closed topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_closed(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_closed topic.
    """
handle_conversation_admin_opened abstractmethod staticmethod
handle_conversation_admin_opened(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_opened topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_opened(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_opened topic.
    """
handle_conversation_admin_replied abstractmethod staticmethod
handle_conversation_admin_replied(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_replied topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_replied(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_replied topic.
    """
handle_conversation_admin_single_created abstractmethod staticmethod
handle_conversation_admin_single_created(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_single_created topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_single_created(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_single_created topic.
    """
handle_conversation_admin_snoozed abstractmethod staticmethod
handle_conversation_admin_snoozed(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_snoozed topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_snoozed(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_snoozed topic.
    """
handle_conversation_admin_unsnoozed abstractmethod staticmethod
handle_conversation_admin_unsnoozed(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_admin_unsnoozed topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_admin_unsnoozed(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_admin_unsnoozed topic.
    """
handle_conversation_deleted abstractmethod staticmethod
handle_conversation_deleted(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_deleted topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_deleted(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_deleted topic.
    """
handle_conversation_part_tag_created abstractmethod staticmethod
handle_conversation_part_tag_created(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_part_tag_created topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_part_tag_created(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_part_tag_created topic.
    """
handle_conversation_user_created abstractmethod staticmethod
handle_conversation_user_created(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_user_created topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_user_created(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_user_created topic.
    """
handle_conversation_user_replied abstractmethod staticmethod
handle_conversation_user_replied(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

Handle a conversation_user_replied topic.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_conversation_user_replied(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    Handle a conversation_user_replied topic.
    """
handle_new_conversation abstractmethod staticmethod
handle_new_conversation(
    intercom_workspace_id, intercom_conversation_id
)

Handle a new conversation.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_new_conversation(
    intercom_workspace_id: str,
    intercom_conversation_id: str,
) -> None:
    """
    Handle a new conversation.
    """
handle_topic abstractmethod staticmethod
handle_topic(
    intercom_workspace_id,
    intercom_conversation,
    topic,
    notified_at,
)

Handle any topic, might be used to call other handlers.

Source code in components/support/public/services/base_intercom_handler_service.py
@staticmethod
@abstractmethod
def handle_topic(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    topic: IntercomWebhookTopic,
    notified_at: datetime,
) -> None:
    """
    Handle any topic, might be used to call other handlers.
    """

phone_support_service

PhoneSupportService

Allows interacting with phone support features

check_if_member_has_pending_quick_callback_request staticmethod
check_if_member_has_pending_quick_callback_request(
    intercom_workspace_id, user_id
)

Check if a member has a pending quick callback request

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def check_if_member_has_pending_quick_callback_request(
    intercom_workspace_id: str,
    user_id: str,
) -> bool:
    """
    Check if a member has a pending quick callback request
    """
    from components.support.subcomponents.phone_support.internal.queries.quick_callback_request_queries import (
        has_pending_quick_callback_request,
    )

    return has_pending_quick_callback_request(
        intercom_workspace_id=intercom_workspace_id,
        user_id=user_id,
    )
create_callback_request staticmethod
create_callback_request(
    phone_number,
    recording_consent,
    contact_request_id,
    commit=True,
)

Create a callback request

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def create_callback_request(
    phone_number: str | None,
    recording_consent: bool | None,
    contact_request_id: UUID,
    commit: bool = True,
) -> CallbackRequest:
    """
    Create a callback request
    """
    return create_callback_request(
        phone_number=phone_number,
        recording_consent=recording_consent,
        contact_request_id=contact_request_id,
        commit=commit,
    )
create_hotline_request staticmethod
create_hotline_request(
    phone_number,
    contact_request_id,
    ino_interaction_id,
    recording_consent=None,
    commit=True,
)

Create a hotline request

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def create_hotline_request(
    phone_number: str | None,
    contact_request_id: UUID,
    ino_interaction_id: str,
    recording_consent: bool | None = None,
    commit: bool = True,
) -> HotlineCall | None:
    """
    Create a hotline request
    """
    from components.support.internal.business_logic.queries.contact_request_queries import (
        get_contact_request_by_id,
    )
    from components.support.internal.business_logic.queries.support_agent_queries import (
        get_support_agent_by_intercom_admin_id,
    )
    from components.support.subcomponents.phone_support.internal.actions.hotline_call_actions import (
        create_hotline_call,
    )
    from components.support.subcomponents.phone_support.internal.actions.phone_call_actions import (
        create_phone_call,
    )
    from shared.helpers.logging.logger import current_logger
    from shared.helpers.time.utc import utcnow

    contact_request = get_contact_request_by_id(
        contact_request_id=contact_request_id
    )

    if not contact_request:
        current_logger.warning(
            "create_hotline_request - No contact request found for contact_request_id",
            contact_request_id=contact_request_id,
        )
        return None

    support_agent_id = None
    if (
        contact_request.intercom_state
        and contact_request.intercom_state.assigned_intercom_admin_id
    ):
        support_agent = get_support_agent_by_intercom_admin_id(
            contact_request.intercom_state.assigned_intercom_admin_id
        )
        support_agent_id = support_agent.id if support_agent else None

    create_phone_call(
        support_agent_id=support_agent_id,
        contact_request_id=contact_request_id,
        ino_interaction_id=ino_interaction_id,
        start=utcnow(),
        commit=commit,
    )

    if not phone_number:
        current_logger.info(
            "create_hotline_request - No phone number provided",
            contact_request_id=contact_request_id,
        )
        return None

    return create_hotline_call(
        phone_number=phone_number,
        contact_request_id=contact_request_id,
        recording_consent=recording_consent,
        commit=commit,
    )
create_phone_support_csat staticmethod
create_phone_support_csat(
    ino_call_id, conversation_context_id, commit=True
)

Create a phone support CSAT

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def create_phone_support_csat(
    ino_call_id: str,
    conversation_context_id: UUID,
    commit: bool = True,
) -> None:
    """
    Create a phone support CSAT
    """
    from components.support.internal.business_logic.queries.contact_request_queries import (
        get_contact_request_by_legacy_context_id,
    )
    from components.support.internal.business_logic.queries.support_agent_queries import (
        get_support_agent_by_intercom_admin_id,
    )
    from components.support.subcomponents.phone_support.internal.actions.phone_call_actions import (
        create_phone_call,
    )
    from components.support.subcomponents.phone_support.internal.actions.phone_support_csat_actions import (
        create_phone_support_csat,
    )
    from components.support.subcomponents.phone_support.internal.queries.phone_call_queries import (
        get_phone_call_by_ino_interaction_id,
    )
    from shared.helpers.logging.logger import current_logger
    from shared.helpers.time.utc import utcnow

    contact_request = get_contact_request_by_legacy_context_id(
        legacy_context_id=conversation_context_id
    )

    if not contact_request:
        current_logger.warning(
            "create_phone_support_csat - No contact request found for conversation context id",
            conversation_context_id=conversation_context_id,
        )
        return None

    phone_call = get_phone_call_by_ino_interaction_id(ino_call_id)

    if not phone_call:
        support_agent_id = None
        if (
            contact_request.intercom_state
            and contact_request.intercom_state.assigned_intercom_admin_id
        ):
            support_agent = get_support_agent_by_intercom_admin_id(
                contact_request.intercom_state.assigned_intercom_admin_id
            )
            support_agent_id = support_agent.id if support_agent else None

        create_phone_call(
            support_agent_id=support_agent_id,
            contact_request_id=contact_request.id,
            ino_interaction_id=ino_call_id,
            start=utcnow(),
            commit=True,
        )

    try:
        create_phone_support_csat(
            ino_call_id=ino_call_id,
            commit=commit,
        )
    except Exception as e:
        current_logger.warning(
            f"create_phone_support_csat - Error creating global phone support CSAT: {e}"
        )
        return None

    return None
create_quick_callback_request staticmethod
create_quick_callback_request(
    phone_number,
    recording_consent,
    contact_request_id,
    status=INOPhoneCallStatus.not_posted,
    ino_voice_campaign_target_id=None,
    commit=True,
)

Create a quick callback request

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def create_quick_callback_request(
    phone_number: str,
    recording_consent: bool,
    contact_request_id: UUID,
    status: INOPhoneCallStatus = INOPhoneCallStatus.not_posted,
    ino_voice_campaign_target_id: str | None = None,
    commit: bool = True,
) -> QuickCallbackRequest:
    """
    Create a quick callback request
    """
    return create_quick_callback_request(
        phone_number=phone_number,
        recording_consent=recording_consent,
        contact_request_id=contact_request_id,
        status=status,
        ino_voice_campaign_target_id=ino_voice_campaign_target_id,
        commit=commit,
    )
get_callback_request_id_for_contact_request staticmethod
get_callback_request_id_for_contact_request(
    contact_request_id,
)

Get a callback request

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def get_callback_request_id_for_contact_request(
    contact_request_id: UUID,
) -> UUID | None:
    """
    Get a callback request
    """
    callback_request = get_callback_request_for_contact_request(
        contact_request_id=contact_request_id
    )

    return callback_request.id if callback_request else None
handle_new_conversation staticmethod
handle_new_conversation(
    legacy_conversation_context_id,
    legacy_conversation_context_app_id,
    intercom_conversation,
)

To be called as a side effect to the creation of any new Intercom conversation. This will determine if the conversation should be handled as a phone support conversation, and handle it as such. Example: for callbacks, we need to send a specific async email initial answer.

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def handle_new_conversation(
    legacy_conversation_context_id: str,
    legacy_conversation_context_app_id: AppName,
    intercom_conversation: IntercomConversation,
) -> bool:
    """
    To be called as a side effect to the creation of any new Intercom conversation.
    This will determine if the conversation should be handled as a phone support conversation, and
    handle it as such.
    Example: for callbacks, we need to send a specific async email initial answer.
    """
    from components.support.subcomponents.phone_support.internal.actions.callback_request_actions import (
        handle_new_conversation_for_callback_requests,
    )

    handled = handle_new_conversation_for_callback_requests(
        legacy_conversation_context_id=legacy_conversation_context_id,
        legacy_conversation_context_app_id=legacy_conversation_context_app_id,
        intercom_conversation=intercom_conversation,
    )

    return handled
is_user_eligible_to_callback staticmethod
is_user_eligible_to_callback(user_id)

Tells if a given user is eligible to create callback requests. Usually based off feature flags.

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def is_user_eligible_to_callback(
    user_id: str | None,
) -> bool:
    """
    Tells if a given user is eligible to create callback requests. Usually based off feature flags.
    """
    return get_app_dependency().is_eligible_to_callback(user_id=user_id)
update_callback_request staticmethod
update_callback_request(
    callback_request_id,
    phone_number,
    recording_consent,
    commit=True,
)

Update a callback request

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def update_callback_request(
    callback_request_id: UUID,
    phone_number: str | None,
    recording_consent: bool | None,
    commit: bool = True,
) -> None:
    """
    Update a callback request
    """
    callback_request = get_or_raise_missing_resource(
        CallbackRequest, callback_request_id
    )

    callback_request.phone_number = phone_number
    callback_request.recording_consent = recording_consent

    current_session.add(callback_request)

    if commit:
        current_session.commit()
update_phone_support_csat staticmethod
update_phone_support_csat(
    ino_interaction_id, rating, comment, commit=True
)

Update a phone support CSAT

Source code in components/support/public/services/phone_support_service.py
@staticmethod
def update_phone_support_csat(
    ino_interaction_id: str,
    rating: int | None,
    comment: str | None,
    commit: bool = True,
) -> None:
    """
    Update a phone support CSAT
    """
    from components.support.subcomponents.phone_support.internal.actions.phone_support_csat_actions import (
        update_phone_support_csat,
    )
    from components.support.subcomponents.phone_support.internal.queries.phone_support_csat_queries import (
        get_phone_support_csat_by_ino_interaction_id,
    )
    from shared.helpers.logging.logger import current_logger

    csat = get_phone_support_csat_by_ino_interaction_id(
        ino_interaction_id=ino_interaction_id
    )

    if csat is None:
        return None

    try:
        update_phone_support_csat(
            csat_id=csat.id,
            rating=rating,
            comment=comment,
            commit=commit,
        )
    except Exception as e:
        current_logger.warning(f"Error creating global phone support CSAT: {e}")
        return None

    return None

subcomponents_services

AssignerService

This service provides methods to interact with the Support Assigner feature from other components.

update_contact_request_automated_answers_review_reserved_status staticmethod
update_contact_request_automated_answers_review_reserved_status(
    intercom_conversation_id,
    intercom_workspace_id,
    reserved_for_review,
    commit=True,
)

Used to update a contact request's reserved_for_automated_answers_review in the assignment metadata table. This is to be used until we globalise the automated answers, which is the feature that sets this field (in the legacy stack, this field used to be set on care_conversation_backlog). That field is used by the assigner to determine which conversations can be assigned to automated answers reviewers.

Source code in components/support/subcomponents/assigner/protected/services/assigner_service.py
@staticmethod
def update_contact_request_automated_answers_review_reserved_status(
    intercom_conversation_id: str,
    intercom_workspace_id: str,
    reserved_for_review: bool,
    commit: bool = True,
) -> None:
    """
    Used to update a contact request's reserved_for_automated_answers_review in the
    assignment metadata table. This is to be used until we globalise the automated answers,
    which is the feature that sets this field (in the legacy stack, this field used to be
    set on care_conversation_backlog).
    That field is used by the assigner to determine which conversations can be assigned
    to automated answers reviewers.
    """
    from components.support.internal.business_logic.queries.contact_request_queries import (
        get_contact_request_by_intercom_conversation,
    )
    from components.support.public.dependencies import get_app_dependency

    contact_request = get_contact_request_by_intercom_conversation(
        intercom_conversation_id=intercom_conversation_id,
        intercom_workspace_id=intercom_workspace_id,
    )

    if not contact_request:
        # Fallback to legacy context id if we're in processing a new conversation and don't yet
        # have a conversation id on the contact_request (this method is likely to be called from the
        # legacy new conversation handler, that's called *before* the the contact_request gets reattached
        # to the conversation id)
        legacy_conversation_context_id, _ = (
            get_app_dependency().get_legacy_conversation_context_id_for_intercom_conversation(
                intercom_conversation_id=intercom_conversation_id,
                intercom_workspace_id=intercom_workspace_id,
            )
        )
        if legacy_conversation_context_id:
            contact_request = get_contact_request_by_id(
                contact_request_id=uuid.UUID(legacy_conversation_context_id),
            )

        if not contact_request:
            current_logger.error(
                "update_contact_request_automated_answers_review_reserved_status - contact request not found for intercom conversation",
                intercom_conversation_id=intercom_conversation_id,
                intercom_workspace_id=intercom_workspace_id,
            )
            return

    contact_request_metadata = get_contact_request_metadata_entity(
        contact_request.id
    )
    contact_request_metadata.reserved_for_automated_answers_review = (
        reserved_for_review
    )
    persist_contact_request_metadata(contact_request_metadata, commit=commit)
update_contact_request_last_inbox_assignment_by_from_legacy staticmethod
update_contact_request_last_inbox_assignment_by_from_legacy(
    intercom_conversation_id,
    intercom_workspace_id,
    commit=True,
)

Used to update a contact request's last_inbox_assignment_by in the intercom state table. This is to be used until we globalise the classifier, which is the feature that sets this field (in the legacy stack, this field used to be set on care_conversation_backlog). That field is used by the assigner to help calculate the raw_expertise_matching_score.

Source code in components/support/subcomponents/assigner/protected/services/assigner_service.py
@staticmethod
def update_contact_request_last_inbox_assignment_by_from_legacy(
    intercom_conversation_id: str,
    intercom_workspace_id: str,
    commit: bool = True,
) -> None:
    """
    Used to update a contact request's last_inbox_assignment_by in the
    intercom state table. This is to be used until we globalise the classifier,
    which is the feature that sets this field (in the legacy stack, this field used to be
    set on care_conversation_backlog).
    That field is used by the assigner to help calculate the raw_expertise_matching_score.
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        persist_contact_request_entity,
    )
    from components.support.internal.business_logic.queries.contact_request_queries import (
        get_contact_request_by_intercom_conversation,
    )
    from components.support.public.dependencies import get_app_dependency

    contact_request = get_contact_request_by_intercom_conversation(
        intercom_conversation_id=intercom_conversation_id,
        intercom_workspace_id=intercom_workspace_id,
    )

    if not contact_request:
        current_logger.error(
            "update_last_inbox_assignment_by - contact request not found for intercom conversation",
            intercom_conversation_id=intercom_conversation_id,
            intercom_workspace_id=intercom_workspace_id,
        )
        return

    legacy_last_inbox_assignment_by = (
        get_app_dependency().get_last_inbox_assignment_by_from_legacy(
            intercom_conversation_id
        )
    )
    last_inbox_assignment_by = None
    if legacy_last_inbox_assignment_by:
        try:
            last_inbox_assignment_by = ContactRequestAssignmentSource[
                legacy_last_inbox_assignment_by
            ]
        except KeyError:
            # if we can't convert to the proper enum we keep the None value
            pass
    contact_request.intercom_state.last_inbox_assignment_by = (
        last_inbox_assignment_by
    )
    persist_contact_request_entity(contact_request, commit=commit)

support_intercom_handler_service

SupportIntercomHandlerService

Bases: BaseIntercomHandlerService

Contains static methods to handle Intercom webhooks for Support.

handle_conversation_admin_assigned staticmethod
handle_conversation_admin_assigned(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when a conversation is assigned to an admin.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_admin_assigned(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    This method is called when a conversation is assigned to an admin.
    """
    from components.support.subcomponents.assigner.internal.business_logic.queries.assigner_queries import (
        retrieve_inbox_assignment_details,
    )
    from components.support.subcomponents.assigner.protected.services.assigner_intercom_handler_service import (
        AssignerIntercomHandlerService,
    )
    from shared.helpers.logging.logger import current_logger

    # Subcomponent handlers for conversation_admin_assigned topic:

    AssignerIntercomHandlerService.handle_conversation_admin_assigned_topic(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=notified_at,
    )

    # Update assigned inbox & admin id in the db

    contact_request = mandatory(
        get_contact_request_by_intercom_conversation(
            intercom_conversation_id=intercom_conversation.id,
            intercom_workspace_id=intercom_workspace_id,
        )
    )

    contact_request.intercom_state.assigned_intercom_admin_id = (
        intercom_conversation.admin_assignee_id
    )
    contact_request.intercom_state.assigned_intercom_inbox_id = (
        intercom_conversation.team_assignee_id
    )

    inbox_assignment_details = retrieve_inbox_assignment_details(
        conversation=intercom_conversation,
        intercom_workspace_id=intercom_workspace_id,
    )

    if inbox_assignment_details and inbox_assignment_details.inbox_has_changed():
        current_logger.info(
            "support - assigner_handle_conversation_admin_assigned_topic - tracking manual assignment",
            conversation_id=intercom_conversation.id,
            intercom_workspace_id=intercom_workspace_id,
        )

        contact_request.intercom_state.last_inbox_assignment_by = (
            inbox_assignment_details.assignment_source
        )
        current_logger.info(
            f"support - assigner_handle_conversation_admin_assigned_topic - tracking last_inbox_assignment_by ({inbox_assignment_details.assignment_source})",
            conversation_id=intercom_conversation.id,
            intercom_workspace_id=intercom_workspace_id,
            contact_request_id=contact_request.id,
            last_inbox_assignment_by=inbox_assignment_details.assignment_source,
        )

    persist_contact_request_entity(contact_request, commit=False)
handle_conversation_admin_closed staticmethod
handle_conversation_admin_closed(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when a conversation is closed by an admin.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_admin_closed(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,  # noqa: ARG004
) -> None:
    """
    This method is called when a conversation is closed by an admin.
    """
    from components.support.internal.business_logic.actions.intercom_conversation_actions import (
        reopen_conversation_if_missing_tag_async,
    )
    from components.support.internal.rules.intercom_inboxes_closing_rules import (
        is_conversation_tagging_correct,
    )

    if not is_conversation_tagging_correct(
        intercom_conversation=intercom_conversation,
        intercom_workspace_id=intercom_workspace_id,
    ):
        reopen_conversation_if_missing_tag_async(
            intercom_conversation=intercom_conversation,
        )

    # Subcomponent handlers for conversation_admin_closed topic:

    pass
handle_conversation_admin_replied staticmethod
handle_conversation_admin_replied(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when an admin replies to a conversation.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_admin_replied(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    This method is called when an admin replies to a conversation.
    """
    from components.support.subcomponents.assigner.protected.services.assigner_intercom_handler_service import (
        AssignerIntercomHandlerService,
    )

    # Subcomponent handlers for conversation_admin_replied topic:

    AssignerIntercomHandlerService.handle_conversation_admin_replied(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=notified_at,
    )
handle_conversation_admin_single_created staticmethod
handle_conversation_admin_single_created(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when a new conversation is created by an admin.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_admin_single_created(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    This method is called when a new conversation is created by an admin.
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        sync_or_create_contact_request_for_new_intercom_conversation,
    )

    # Sync the contact request with the conversation (intercom state, tags, etc.)

    sync_or_create_contact_request_for_new_intercom_conversation(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=notified_at,
    )

    # Subcomponent handlers for conversation_admin_single_created topic:

    pass
handle_conversation_part_tag_created staticmethod
handle_conversation_part_tag_created(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when a tag is created for a conversation part.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_part_tag_created(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    This method is called when a tag is created for a conversation part.
    """
    from components.support.internal.business_logic.actions.contact_request_tag_actions import (
        update_contact_request_tags_async,
    )

    update_contact_request_tags_async(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=notified_at,
        commit=True,
    )
handle_conversation_user_created staticmethod
handle_conversation_user_created(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when a new conversation is created by a user.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_user_created(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    This method is called when a new conversation is created by a user.
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        sync_or_create_contact_request_for_new_intercom_conversation,
    )

    # Sync the contact request with the conversation (intercom state, tags, etc.)

    sync_or_create_contact_request_for_new_intercom_conversation(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=notified_at,
    )

    # Subcomponent handlers for conversation_user_created topic:

    pass
handle_conversation_user_replied staticmethod
handle_conversation_user_replied(
    intercom_workspace_id,
    intercom_conversation,
    notified_at,
)

This method is called when a user replies to a conversation.

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_conversation_user_replied(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    notified_at: datetime,
) -> None:
    """
    This method is called when a user replies to a conversation.
    """
    from components.support.subcomponents.assigner.protected.services.assigner_intercom_handler_service import (
        AssignerIntercomHandlerService,
    )

    # Subcomponent handlers for conversation_admin_replied topic:

    AssignerIntercomHandlerService.handle_conversation_user_replied(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=notified_at,
    )
handle_new_conversation staticmethod
handle_new_conversation(
    intercom_workspace_id, intercom_conversation_id
)

Handles a new conversation event coming from Intercom (webhook or workflow API call)

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_new_conversation(
    intercom_workspace_id: str,
    intercom_conversation_id: str,
) -> None:
    """
    Handles a new conversation event coming from Intercom (webhook or workflow API call)
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        mark_contact_request_as_processed_as_new_conversation,
        sync_or_create_contact_request_for_new_intercom_conversation,
    )
    from components.support.internal.business_logic.queries.intercom_webhooks_queries import (
        is_global_intercom_webhook_active,
    )
    from shared.helpers.logging.logger import current_logger

    # Getting the Intercom conversation details from the API (here we only have the conversation id)
    intercom_client = get_care_intercom_client(get_current_app_name())
    conversation_dict = intercom_client.get_conversation_details(
        conversation_id=intercom_conversation_id
    )
    intercom_conversation = convert_to_intercom_conversation(conversation_dict)

    last_part = get_last_conversation_part(intercom_conversation)
    delivered_at = int(datetime.now(UTC).timestamp())

    current_logger.info(
        "SupportIntercomHandlerService.handle_new_conversation",
        conversation_id=intercom_conversation.id,
        workspace_id=intercom_workspace_id,
        admin_assignee_id=intercom_conversation.admin_assignee_id,
        team_assignee_id=intercom_conversation.team_assignee_id,
        last_part_author_id=get_last_conversation_part_author_id(
            intercom_conversation
        ),
        conversation_user=anonymize_user(intercom_conversation.user),
        links=intercom_conversation.links,
        state=intercom_conversation.state,
        parts_summary=parts_summary(intercom_conversation),
        delivered_at=delivered_at,
        conversation_created_at=intercom_conversation.created_at,
        conversation_updated_at=intercom_conversation.updated_at,
        last_part_created_at=last_part.created_at if last_part else None,
        tags=intercom_conversation.tags,
    )

    # If we need to handle the event on the legacy side (non globalised features)
    # and need to do them synchronously, we can do it through the injected dependency.
    # Used in BE.
    try:
        if is_global_intercom_webhook_active(get_current_app_name()):
            get_app_dependency().handle_legacy_new_conversation_event(
                conversation=intercom_conversation
            )
    except Exception as e:
        current_logger.error(
            "SuportIntercomHandlerService.handle_new_conversation - error while calling handle_legacy_new_conversation_event",
            error=e,
            conversation_id=intercom_conversation.id,
            wokspace_id=intercom_workspace_id,
        )

    # Find & sync the relevant contact_request with that conversation (contact request intercom state, contact request tags, etc.)
    contact_request = sync_or_create_contact_request_for_new_intercom_conversation(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        notified_at=timestamp_to_utc_datetime(intercom_conversation.created_at)
        or datetime.now(UTC),
    )

    if not contact_request:
        current_logger.error(
            "support - handle_new_conversation - no contact request found for conversation",
            conversation_id=intercom_conversation.id,
            workspace_id=intercom_workspace_id,
        )
        return

    if contact_request.has_been_processed_as_new_conversation:
        current_logger.info(
            "support - handle_new_conversation - Contact request has already been processed as new conversation, stopping there",
            conversation_id=intercom_conversation.id,
            contact_request_id=contact_request.id,
        )
        return

    contact_request = mark_contact_request_as_processed_as_new_conversation(
        contact_request.id
    )
    # Assignment metadata
    set_recommended_admin_for_contact_request(contact_request.id)

    # Subcomponent handlers for new conversation:

    QuickCallbackIntercomHandlerService.handle_new_conversation(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation_id=intercom_conversation.id,
    )
    AssignerService.update_contact_request_last_inbox_assignment_by_from_legacy(
        intercom_conversation_id=str(intercom_conversation_id),
        intercom_workspace_id=intercom_workspace_id,
    )
handle_topic staticmethod
handle_topic(
    intercom_workspace_id,
    intercom_conversation,
    topic,
    notified_at,
)

Handles a conversation webhook topic coming from Intercom

Source code in components/support/public/services/support_intercom_handler_service.py
@staticmethod
def handle_topic(
    intercom_workspace_id: str,
    intercom_conversation: IntercomConversation,
    topic: IntercomWebhookTopic,
    notified_at: datetime,
) -> None:
    """
    Handles a conversation webhook topic coming from Intercom
    """
    from components.support.internal.business_logic.actions.contact_request_intercom_state_actions import (
        update_contact_request_conversation_state,
    )
    from components.support.internal.rules.contact_request_rules import (
        can_conversation_webhook_topics_be_handled,
    )
    from components.support.subcomponents.assigner.protected.services.assigner_intercom_handler_service import (
        AssignerIntercomHandlerService,
    )
    from shared.helpers.logging.logger import current_logger

    current_logger.info(
        "SupportIntercomHandlerService.handle_topic",
        conversation_id=intercom_conversation.id,
        workspace_id=intercom_workspace_id,
        topic=topic,
    )

    if not can_conversation_webhook_topics_be_handled(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation_id=intercom_conversation.id,
    ):
        current_logger.info(
            "SupportService.handle_topic - skipping topic handling because conversation is not yet processed as new conversation",
            topic=topic,
            conversation_id=intercom_conversation.id,
            state=intercom_conversation.state,
        )
        return

    # Update contact_request state
    update_contact_request_conversation_state(
        intercom_conversation_id=intercom_conversation.id,
        intercom_workspace_id=intercom_workspace_id,
        state=intercom_conversation.state,
        started_at=datetime.fromtimestamp(intercom_conversation.created_at, UTC),
    )

    if topic == IntercomWebhookTopic.conversation_user_created:
        SupportIntercomHandlerService.handle_conversation_user_created(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_single_created:
        SupportIntercomHandlerService.handle_conversation_admin_single_created(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_assigned:
        SupportIntercomHandlerService.handle_conversation_admin_assigned(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_closed:
        SupportIntercomHandlerService.handle_conversation_admin_closed(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_opened:
        SupportIntercomHandlerService.handle_conversation_admin_opened(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_snoozed:
        SupportIntercomHandlerService.handle_conversation_admin_snoozed(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_unsnoozed:
        SupportIntercomHandlerService.handle_conversation_admin_unsnoozed(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_deleted:
        SupportIntercomHandlerService.handle_conversation_deleted(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_user_replied:
        SupportIntercomHandlerService.handle_conversation_user_replied(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_admin_replied:
        SupportIntercomHandlerService.handle_conversation_admin_replied(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    if topic == IntercomWebhookTopic.conversation_part_tag_created:
        SupportIntercomHandlerService.handle_conversation_part_tag_created(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )
    else:
        # There is no "delete tag" webhook topic in Intercom, so we
        # need to check and update tags no matter what
        # (except if we've already done it, as is the case when it's a tag topic)
        update_contact_request_tags_async(
            intercom_workspace_id=intercom_workspace_id,
            intercom_conversation=intercom_conversation,
            notified_at=notified_at,
        )

    # Subcomponent handlers for ALL topics

    AssignerIntercomHandlerService.handle_topic(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation=intercom_conversation,
        topic=topic,
        notified_at=notified_at,
    )

    current_session.commit()

support_service

SupportService

Contains static methods to interact with direct Support features

create_contact_request staticmethod
create_contact_request(
    app_id,
    app_user_id,
    legacy_conversation_context_id,
    legacy_conversation_context_app_id,
    intercom_workspace_id,
    source_type,
    intercom_conversation_id=None,
    classification_result=None,
    commit=True,
    id=None,
)

This allows creating a contact request. A Support contact request represents any request from a user to contact Care (whatever the medium, sync conversation, async conversation, callback, etc.)

Source code in components/support/public/services/support_service.py
@staticmethod
def create_contact_request(
    app_id: AppName,
    app_user_id: str | None,
    legacy_conversation_context_id: str,
    legacy_conversation_context_app_id: AppName,
    intercom_workspace_id: str,
    source_type: ContactRequestSourceType,
    intercom_conversation_id: str | None = None,
    classification_result: ClassificationResult | None = None,
    commit: bool = True,
    id: UUID | None = None,
) -> UUID:
    """
    This allows creating a contact request. A Support contact request represents any request from a user
    to contact Care (whatever the medium, sync conversation, async conversation, callback, etc.)
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        create_contact_request,
    )

    contact_request = create_contact_request(
        id=id,
        app_id=app_id,
        app_user_id=app_user_id,
        legacy_conversation_context_id=legacy_conversation_context_id,
        legacy_conversation_context_app_id=legacy_conversation_context_app_id,
        intercom_conversation_id=intercom_conversation_id,
        intercom_workspace_id=intercom_workspace_id,
        source_type=source_type,
        classification_result=classification_result,
        commit=commit,
    )
    contact_request_id: UUID = contact_request.id
    return contact_request_id
create_duplicate_contact_request staticmethod
create_duplicate_contact_request(
    app_id,
    app_user_id,
    legacy_conversation_context_id,
    legacy_conversation_context_app_id,
    intercom_workspace_id,
    duplicated_from_context_id,
    source_type,
    intercom_conversation_id=None,
    classification_result=None,
    commit=True,
    id=None,
    flush=True,
)

This allows creating a contact request that links to the original contact request. A Support contact request represents any request from a user to contact Care (whatever the medium, sync conversation, async conversation, callback, etc.) Since we don't usually know the source type, we'll default to unknown

Source code in components/support/public/services/support_service.py
@staticmethod
def create_duplicate_contact_request(
    app_id: AppName,
    app_user_id: str | None,
    legacy_conversation_context_id: str,
    legacy_conversation_context_app_id: AppName,
    intercom_workspace_id: str,
    duplicated_from_context_id: UUID,
    source_type: ContactRequestSourceType | None,
    intercom_conversation_id: str | None = None,
    classification_result: ClassificationResult | None = None,
    commit: bool = True,
    id: UUID | None = None,
    flush: bool = True,
) -> UUID:
    """
    This allows creating a contact request that links to the original contact request. A Support contact request represents any request from a user
    to contact Care (whatever the medium, sync conversation, async conversation, callback, etc.)
    Since we don't usually know the source type, we'll default to unknown
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        create_contact_request,
    )
    from components.support.internal.business_logic.queries.contact_request_queries import (
        get_contact_request_by_legacy_context_id,
    )

    duplicate_contact_request = get_contact_request_by_legacy_context_id(
        duplicated_from_context_id
    )
    contact_request = create_contact_request(
        id=id,
        app_id=app_id,
        app_user_id=app_user_id,
        legacy_conversation_context_id=legacy_conversation_context_id,
        legacy_conversation_context_app_id=legacy_conversation_context_app_id,
        intercom_conversation_id=intercom_conversation_id,
        intercom_workspace_id=intercom_workspace_id,
        source_type=source_type
        if source_type
        else ContactRequestSourceType.unknown,
        duplicated_from_id=duplicate_contact_request.id
        if duplicate_contact_request
        else None,
        classification_result=classification_result,
        commit=commit,
        flush=flush,
    )
    contact_request_id: UUID = contact_request.id
    return contact_request_id
get_contact_request_id_or_create_from_legacy staticmethod
get_contact_request_id_or_create_from_legacy(
    intercom_conversation_id, intercom_workspace_id
)

This allows creating a contact request. A Support contact request represents any request from a user to contact Care (whatever the medium, sync conversation, async conversation, callback, etc.)

Source code in components/support/public/services/support_service.py
@staticmethod
def get_contact_request_id_or_create_from_legacy(
    intercom_conversation_id: str,
    intercom_workspace_id: str,
) -> UUID | None:
    """
    This allows creating a contact request. A Support contact request represents any request from a user
    to contact Care (whatever the medium, sync conversation, async conversation, callback, etc.)
    """
    contact_request = get_contact_request_or_create_from_legacy_context(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation_id=intercom_conversation_id,
    )
    return contact_request.id if contact_request else None
get_contact_request_source_type_from_context staticmethod
get_contact_request_source_type_from_context(
    legacy_conversation_context_id,
)

This method allows us to get the contact request source type from an existing legacy context.

Source code in components/support/public/services/support_service.py
@staticmethod
def get_contact_request_source_type_from_context(
    legacy_conversation_context_id: str,
) -> ContactRequestSourceType:
    """
    This method allows us to get the contact request source type from an existing legacy context.
    """
    return get_app_dependency().get_contact_request_source_type_from_context(
        legacy_conversation_context_id
    )
get_contact_request_with_tags_and_intercom_state staticmethod
get_contact_request_with_tags_and_intercom_state(
    intercom_conversation_id, intercom_workspace_id
)

This method allows returning the contact request joined with tags and state in one query

Source code in components/support/public/services/support_service.py
@staticmethod
def get_contact_request_with_tags_and_intercom_state(
    intercom_conversation_id: str, intercom_workspace_id: str
) -> ContactRequestWithTagsAndIntercomStateEntity | None:
    """
    This method allows returning the contact request joined with tags and state in one query
    """
    return get_contact_request_by_intercom_conversation(
        intercom_conversation_id=intercom_conversation_id,
        intercom_workspace_id=intercom_workspace_id,
    )
get_support_opening_status staticmethod
get_support_opening_status(app_user_id, role)

Get the support status for a given profile

Source code in components/support/public/services/support_service.py
@staticmethod
def get_support_opening_status(
    app_user_id: str | None,
    role: ContactRoleType | None,
) -> SupportAvailabilityDetails:
    """
    Get the support status for a given profile
    """
    return get_support_opening_status(app_user_id=app_user_id, role=role)
handle_new_conversation staticmethod
handle_new_conversation(
    legacy_conversation_context_id,
    legacy_conversation_context_app_id,
    intercom_conversation_id,
    intercom_workspace_id,
)

This method handles a new conversation from Intercom (either via a webhook call or via a rule calling an endpoint to tell us a conversation was created - in the latter case we need to get the Intercom conversation from their API and pass it to this method).

Source code in components/support/public/services/support_service.py
@staticmethod
def handle_new_conversation(
    legacy_conversation_context_id: str,
    legacy_conversation_context_app_id: AppName,
    intercom_conversation_id: str,
    intercom_workspace_id: str,
) -> None:
    """
    This method handles a new conversation from Intercom (either via a webhook call or via a rule calling an endpoint
    to tell us a conversation was created - in the latter case we need to get the Intercom conversation from their API
    and pass it to this method).
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        handle_new_conversation_for_contact_request,
    )

    handle_new_conversation_for_contact_request(
        legacy_conversation_context_id=legacy_conversation_context_id,
        legacy_conversation_context_app_id=legacy_conversation_context_app_id,
        intercom_conversation_id=intercom_conversation_id,
        intercom_workspace_id=intercom_workspace_id,
    )
has_contact_request_been_processed_as_new_conversation staticmethod
has_contact_request_been_processed_as_new_conversation(
    intercom_conversation_id, intercom_workspace_id
)

This method checks if a contact request has been processed as a new conversation.

Source code in components/support/public/services/support_service.py
@staticmethod
def has_contact_request_been_processed_as_new_conversation(
    intercom_conversation_id: str,
    intercom_workspace_id: str,
) -> bool:
    """
    This method checks if a contact request has been processed as a new conversation.
    """
    contact_request = get_contact_request_by_intercom_conversation(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation_id=intercom_conversation_id,
    )
    return (
        contact_request.has_been_processed_as_new_conversation
        if contact_request
        else False
    )
is_global_intercom_webhook_active staticmethod
is_global_intercom_webhook_active(app_name)

Returns true if the global intercom webhook endpoints feature flag is active for the current app.

Source code in components/support/public/services/support_service.py
@staticmethod
def is_global_intercom_webhook_active(app_name: AppName) -> bool:
    """
    Returns true if the global intercom webhook endpoints feature flag is active for
    the current app.
    """
    from components.support.internal.business_logic.queries.intercom_webhooks_queries import (
        is_global_intercom_webhook_active as _is_global_intercom_webhook_active,
    )

    return _is_global_intercom_webhook_active(app_name=app_name)
is_global_intercom_webhook_fallback_active staticmethod
is_global_intercom_webhook_fallback_active(app_name)

Returns true if the global intercom webhook endpoints feature flag is active for the current app.

Source code in components/support/public/services/support_service.py
@staticmethod
def is_global_intercom_webhook_fallback_active(app_name: AppName) -> bool:
    """
    Returns true if the global intercom webhook endpoints feature flag is active for
    the current app.
    """
    from components.support.internal.business_logic.actions.intercom_fallback.process_new_conversation_fallback import (
        is_fallback_command_active as _is_fallback_command_active,
    )

    return _is_fallback_command_active(app_name=app_name)
is_global_intercom_webhook_handlers_enabled staticmethod
is_global_intercom_webhook_handlers_enabled(app_name)

This method checks if the global intercom webhook handlers are enabled. To be used in webhook endpoints before calling the handlers.

Source code in components/support/public/services/support_service.py
@staticmethod
def is_global_intercom_webhook_handlers_enabled(app_name: AppName) -> bool:
    """
    This method checks if the global intercom webhook handlers are enabled. To be used in webhook endpoints
    before calling the handlers.
    """
    from shared.feature_flags.client import (
        bool_feature_flag,
        get_anonymous_user_context_data,
    )

    feature_flag_key = None

    if app_name == AppName.ALAN_FR:
        feature_flag_key = "killswitch-fr-backend-global-intercom-webhook-handlers"
    elif app_name == AppName.ALAN_BE:
        feature_flag_key = "killswitch-be-backend-global-intercom-webhook-handlers"
    elif app_name == AppName.ALAN_ES:
        feature_flag_key = "killswitch-es-backend-global-intercom-webhook-handlers"
    elif app_name == AppName.ALAN_CA:
        feature_flag_key = "killswitch-ca-backend-global-intercom-webhook-handlers"

    if not feature_flag_key:
        return False

    return bool_feature_flag(
        feature_flag_key=feature_flag_key,
        context_data=get_anonymous_user_context_data(),
        default_value=False,
    )
is_support_open staticmethod
is_support_open()

Check if support is currently open

Source code in components/support/public/services/support_service.py
@staticmethod
def is_support_open() -> bool:
    """
    Check if support is currently open
    """
    return is_support_open_on(time_to_check=None).is_open
sync_assignment_from_legacy_data staticmethod
sync_assignment_from_legacy_data(
    intercom_workspace_id,
    intercom_conversation_id,
    legacy_conversation_backlog,
    legacy_conversation_tags,
    legacy_conversation_assignments,
)

Sync data related to conversation assignment / rejection / move with legacy

Source code in components/support/public/services/support_service.py
@staticmethod
def sync_assignment_from_legacy_data(
    intercom_workspace_id: str,
    intercom_conversation_id: str,
    legacy_conversation_backlog: LegacyConversationBacklog,
    legacy_conversation_tags: list[LegacyConversationTag],
    legacy_conversation_assignments: list[LegacyConversationAssignment],
) -> None:
    """
    Sync data related to conversation assignment / rejection / move with legacy
    """
    return sync_contact_request_assignment_from_legacy_data(
        intercom_workspace_id=intercom_workspace_id,
        intercom_conversation_id=intercom_conversation_id,
        legacy_conversation_backlog=legacy_conversation_backlog,
        legacy_conversation_tags=legacy_conversation_tags,
        legacy_conversation_assignments=legacy_conversation_assignments,
    )
sync_support_agent_from_legacy_data staticmethod
sync_support_agent_from_legacy_data(
    legacy_support_agent, legacy_support_agent_affectation
)

This is to be used during the transition from the French assigner to the global assigner. It updates global support agents from legacy data.

Source code in components/support/public/services/support_service.py
@staticmethod
def sync_support_agent_from_legacy_data(
    legacy_support_agent: "LegacySupportAgent",
    legacy_support_agent_affectation: "LegacySupportAgentAffectation",
) -> None:
    """
    This is to be used during the transition from the French assigner to the global assigner.
    It updates global support agents from legacy data.
    """
    from components.support.internal.business_logic.actions.legacy_support_agent_actions import (
        sync_support_agent_with_legacy_data,
    )

    sync_support_agent_with_legacy_data(
        legacy_support_agent=legacy_support_agent,
        legacy_support_agent_affectation=legacy_support_agent_affectation,
    )
update_contact_request_classification_data_from_context staticmethod
update_contact_request_classification_data_from_context(
    legacy_conversation_context_id, classification_data
)

This method gets the contact request from the context, then updates it with classification data.

Source code in components/support/public/services/support_service.py
@staticmethod
def update_contact_request_classification_data_from_context(
    legacy_conversation_context_id: UUID,
    classification_data: ClassificationResult | None,
) -> None:
    """
    This method gets the contact request from the context, then updates it with classification data.
    """
    from components.support.internal.business_logic.actions.contact_request_actions import (
        update_classification_result_for_contact_request,
    )

    update_classification_result_for_contact_request(
        legacy_conversation_context_id=legacy_conversation_context_id,
        classification_data=classification_data,
    )

components.support.public.support_app_group

support module-attribute

support = AppGroup('support')