Api reference
components.support.public.actions ¶
csat_actions ¶
Public actions for escalated conversation CSAT mutations.
Used by country handlers (e.g. FR) to write SupportCSAT records without directly accessing internal models (ContactRequest, SupportCSAT).
answer_escalated_csat ¶
Record a member's CSAT response for an escalated Intercom conversation.
Source code in components/support/public/actions/csat_actions.py
dismiss_escalated_csat ¶
Dismiss a CSAT survey for an escalated Intercom conversation.
Source code in components/support/public/actions/csat_actions.py
ensure_escalated_csat_on_close ¶
Create or re-trigger SupportCSAT for an escalated conversation on close.
On first close: creates a new CSAT if eligible.
On re-close (after reopen): resets existing CSAT to sent status,
updates survey_sent_at and support_agent_id so the member gets
a fresh survey while previous rating/comment are preserved until overwritten.
Source code in components/support/public/actions/csat_actions.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | |
merge_users ¶
Public merge helper for the support component.
Reassigns ContactRequest ownership during user deduplication so that SupportCSAT, PhoneSupportCSAT, and all entities linked via ContactRequest follow the merged user.
merge_users_in_support_component ¶
Merge all support data from source user to target user.
Updates ContactRequest.app_user_id for every contact request that
belongs to source_user_id within the given app_name so it points
to target_user_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_user_id
|
str
|
User ID to merge from. |
required |
target_user_id
|
str
|
User ID to merge into. |
required |
app_name
|
AppName
|
App identifier to scope the update. |
required |
commit
|
bool
|
Whether to commit the transaction (default: False). |
False
|
Returns:
| Type | Description |
|---|---|
list[str]
|
List of log messages describing actions taken. |
Source code in components/support/public/actions/merge_users.py
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 ¶
backfill_fr_contact_requests_from_turing ¶
backfill_fr_contact_requests_from_turing ¶
Create missing FR ContactRequest records from Turing for rated conversations.
Source code in components/support/public/commands/backfill_fr_contact_requests_from_turing.py
fix_fr_contact_request_data_2025_09_15 ¶
fix_fr_contact_request_data_2025_09_15 ¶
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
import_fr_intercom_admins ¶
import_fr_intercom_admins ¶
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
import_fr_intercom_spe_inboxes ¶
import_fr_intercom_spe_inboxes ¶
Import all existing intercom_spe_inbox entries into support.support_specialization.
Source code in components/support/public/commands/import_fr_intercom_spe_inboxes.py
import_fr_support_agents_from_turing ¶
import_fr_support_agents_from_turing ¶
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
import_intercom_inboxes_command ¶
import_intercom_inboxes ¶
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
import_legacy_intercom_csat_data ¶
import_legacy_intercom_csat_data ¶
Import legacy Intercom CSAT ratings from Turing into SupportCSAT table.
Source code in components/support/public/commands/import_legacy_intercom_csat_data.py
import_support_agents_from_employees_command ¶
import_support_agents_from_employees ¶
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
intercom_fallback ¶
intercom_deferred_push_notifications ¶
send_deferred_intercom_push_notifications ¶
Send push notifications for admin messages missed by the Intercom webhook.
Runs at support opening time to deliver deferred push notifications. Skips silently if support is currently closed.
Only processes conversations where the member has not yet been notified about the latest admin message (last_admin_reply_at > last_member_notified_at).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lookback_hours
|
int
|
Look back this many hours for unnotified messages. Defaults to 14h. |
14
|
Source code in components/support/public/commands/intercom_fallback/intercom_deferred_push_notifications.py
intercom_new_conversation_fallback ¶
process_new_conversations_not_sent_by_intercom ¶
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
intercom_update_fallback ¶
sync_conversation_updates_not_sent_by_intercom ¶
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
synchronize_support_specializations ¶
synchronize_support_specializations ¶
Synchronize support specializations from Intercom to our backend.
Source code in components/support/public/commands/synchronize_support_specializations.py
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 CASE\n\t\t\t\tWHEN cris.assigned_intercom_inbox_id IS NULL AND cris.assigned_intercom_admin_id IS NULL AND (:default_inbox_id) IS NOT NULL\n\t\t\t\tTHEN (:default_inbox_id)\n\t\t\t\tELSE cris.assigned_intercom_inbox_id\n END 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 in ('ROLE[DUTCH]') THEN crt.name END) AS dutch_tag,\n MAX(CASE WHEN crt.name in ('ROLE[ENGLISH]') THEN crt.name END) AS english_tag,\n MAX(CASE WHEN crt.name in ('ROLE[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 MAX(CASE WHEN crt.name = 'bypass-assigner' THEN crt.name END) AS 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 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\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, NULL as marrakech_scope_protection_boost\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 CASE\n\t\t\t\tWHEN cris.assigned_intercom_inbox_id IS NULL AND cris.assigned_intercom_admin_id IS NULL AND (:default_inbox_id) IS NOT NULL\n\t\t\t\tTHEN (:default_inbox_id)\n\t\t\t\tELSE cris.assigned_intercom_inbox_id\n END 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 in ('ROLE[DUTCH]') THEN crt.name END) AS dutch_tag,\n MAX(CASE WHEN crt.name in ('ROLE[ENGLISH]') THEN crt.name END) AS english_tag,\n MAX(CASE WHEN crt.name in ('ROLE[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 MAX(CASE WHEN crt.name = 'bypass-assigner' THEN crt.name END) AS 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 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\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 CASE\n\t\t\t\tWHEN cris.assigned_intercom_inbox_id IS NULL AND cris.assigned_intercom_admin_id IS NULL AND (:default_inbox_id) IS NOT NULL\n\t\t\t\tTHEN (:default_inbox_id)\n\t\t\t\tELSE cris.assigned_intercom_inbox_id\n END 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 MAX(CASE WHEN crt.name = 'bypass-assigner' THEN crt.name END) AS 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 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 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, NULL as marrakech_scope_protection_boost\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 CASE\n\t\t\t\tWHEN cris.assigned_intercom_inbox_id IS NULL AND cris.assigned_intercom_admin_id IS NULL AND (:default_inbox_id) IS NOT NULL\n\t\t\t\tTHEN (:default_inbox_id)\n\t\t\t\tELSE cris.assigned_intercom_inbox_id\n END 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 MAX(CASE WHEN crt.name = 'bypass-assigner' THEN crt.name END) AS 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 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 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 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 , CASE WHEN sa.work_location != 'fr' AND sa.platform_name = 'Webhelp'\n THEN TRUE\n ELSE FALSE\n END AS is_marrakech_agent\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 , CASE\n WHEN cris.assigned_intercom_inbox_id IS NULL AND cris.assigned_intercom_admin_id IS NULL AND (:default_inbox_id) IS NOT NULL\n THEN (:default_inbox_id) -- defaults to 'Care team' inbox id\n ELSE cris.assigned_intercom_inbox_id\n END 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 -- marrakech scope fit is a boolean flag to indicate if the conversation is a good fit for a marrakech agent\n -- basically an L1 convo, not georestricted, not english, not uce reserved, not reserved for review\n , CASE WHEN pcrb.callback_tag IS NULL\n AND pcrb.conversation_workforce_level = 1\n AND pcrb.geo_tag IS NULL\n AND pcrb.english_tag IS NULL\n AND pcrb.uce_reserved_tag IS NULL\n AND NOT pcrb.reserved_for_review\n THEN TRUE\n ELSE FALSE\n END AS marrakech_scope_fit\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 (: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 -- 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 , ecrb.marrakech_scope_fit\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.contact_request_id\n , s.intercom_conversation_id\n , s.priority_level\n , s.sla_score\n , s.sla_breached\n , s.conversation_workforce_level\n , s.workforce_level_matching_score\n , s.uce_member_matching_score\n , s.predicted_probability\n , s._predicted_class\n , s.last_inbox_assignment_by\n , s.inbox_id\n , s.inbox_name\n , s.time_reference_for_priority_score\n , s.reserved_for_external_agent_platform\n , s.reserved_for_review\n , s.bypassed_classifier\n , s._predicted_class AS predicted_class\n , s.marrakech_scope_fit\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 , marrakech_scope_fit\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 , marrakech_scope_fit\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 -- when it's a ticket which could NOT be handled by Marrakech, boost it if the requesting support agent is external (and not Marrakech based)\n , CASE WHEN cxp.marrakech_scope_fit = FALSE AND NOT sa.has_role_uce AND NOT sa.is_marrakech_agent\n THEN (:marrakech_scope_protection_boost)\n ELSE 1\n END AS marrakech_scope_protection_boost\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, cxp.marrakech_scope_fit, marrakech_scope_protection_boost\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 , POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score\n , ec.marrakech_scope_protection_boost\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 * marrakech_scope_protection_boost\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 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 , CASE WHEN sa.work_location != 'fr' AND sa.platform_name = 'Webhelp'\n THEN TRUE\n ELSE FALSE\n END AS is_marrakech_agent\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 , CASE\n WHEN cris.assigned_intercom_inbox_id IS NULL AND cris.assigned_intercom_admin_id IS NULL AND (:default_inbox_id) IS NOT NULL\n THEN (:default_inbox_id) -- defaults to 'Care team' inbox id\n ELSE cris.assigned_intercom_inbox_id\n END 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 -- marrakech scope fit is a boolean flag to indicate if the conversation is a good fit for a marrakech agent\n -- basically an L1 convo, not georestricted, not english, not uce reserved, not reserved for review\n , CASE WHEN pcrb.callback_tag IS NULL\n AND pcrb.conversation_workforce_level = 1\n AND pcrb.geo_tag IS NULL\n AND pcrb.english_tag IS NULL\n AND pcrb.uce_reserved_tag IS NULL\n AND NOT pcrb.reserved_for_review\n THEN TRUE\n ELSE FALSE\n END AS marrakech_scope_fit\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 (: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 -- 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 , ecrb.marrakech_scope_fit\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.contact_request_id\n , s.intercom_conversation_id\n , s.priority_level\n , s.sla_score\n , s.sla_breached\n , s.conversation_workforce_level\n , s.workforce_level_matching_score\n , s.uce_member_matching_score\n , s.predicted_probability\n , s._predicted_class\n , s.last_inbox_assignment_by\n , s.inbox_id\n , s.inbox_name\n , s.time_reference_for_priority_score\n , s.reserved_for_external_agent_platform\n , s.reserved_for_review\n , s.bypassed_classifier\n , s._predicted_class AS predicted_class\n , s.marrakech_scope_fit\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 , marrakech_scope_fit\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 , marrakech_scope_fit\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 -- when it's a ticket which could NOT be handled by Marrakech, boost it if the requesting support agent is external (and not Marrakech based)\n , CASE WHEN cxp.marrakech_scope_fit = FALSE AND NOT sa.has_role_uce AND NOT sa.is_marrakech_agent\n THEN (:marrakech_scope_protection_boost)\n ELSE 1\n END AS marrakech_scope_protection_boost\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, cxp.marrakech_scope_fit, marrakech_scope_protection_boost\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 , POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score\n , ec.marrakech_scope_protection_boost\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 * marrakech_scope_protection_boost\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 ¶
SupportSnoozeDependency ¶
Bases: ABC
This allows country-specific apps to define a class to inject dependencies to the support component for the Snooze feature.
get_legacy_cancellable_on_event_snooze_sequences
abstractmethod
¶
Pull ALL legacy sequences cancellable on event as sync data.
force_include_legacy_ids: legacy sequence IDs to include even if already cancelled (used to sync cancellation state back to global sequences that are still active).
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_legacy_snooze_sequences_sync_data
abstractmethod
¶
Pull ALL legacy sequences for a conversation as sync data (pre-sync legacy→global).
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_legacy_snooze_templates_for_admin
abstractmethod
¶
Pull legacy snooze sequences marked as templates for a given admin (pre-sync for templates).
Returns empty list for countries without legacy snooze tables.
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_possible_cancel_on_event_metadata_for_care_event
abstractmethod
¶
Returns care event metadata + display data from conversation context.
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_possible_cancel_on_event_metadata_for_teletransmission
abstractmethod
¶
Returns one entry per beneficiary on user's active policy, with TT display data.
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_snooze_auto_cancel_data_for_care_event
abstractmethod
¶
Returns current data for a care event (for snooze auto-cancel logic). Used for: end-of-sequence notes, automated status cancellation, metadata completion. Returns None if care event not found or feature not supported.
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_snooze_auto_cancel_data_for_teletransmission
abstractmethod
¶
Returns current teletransmission data (for snooze auto-cancel logic). Used for: end-of-sequence notes, automated status cancellation, metadata completion. Returns None if insurance profile not found or feature not supported.
Source code in components/support/subcomponents/snooze/protected/dependencies.py
get_user_greeting_context
abstractmethod
¶
Returns greeting-related user data for snooze reply message interpolation.
sync_snooze_sequences_to_legacy
abstractmethod
¶
Push ALL global sequences for a conversation to legacy tables (post-sync global→legacy).
Source code in components/support/subcomponents/snooze/protected/dependencies.py
components.support.public.entities ¶
ai_tooling_entities ¶
HarryComposerContextSection
dataclass
¶
HarryComposerContextSectionType ¶
Bases: BaseContextSectionType
Defines type of context section - Harry Composer
MemberAttributesContextSection
dataclass
¶
Bases: BaseContextSection
Context section in the debug tool to view member attributes used in the conversation. member_attributes: JSON dump of member attributes
assignment_entities ¶
channel_management_entities ¶
CareChannel ¶
Bases: AlanBaseEnum
All channels that are available for care
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,
)
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,
)
PopulationConfiguration
dataclass
¶
A grouping of UserSubsetConfiguration instances, grouped by a top level setting.
from_config
classmethod
¶
Initialize a PopulationConfiguration from a config dict.
Source code in components/support/subcomponents/channel_management/protected/entities/population_configuration.py
is_valid ¶
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
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
¶
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.
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_tag ¶
default_assignment_disabled
class-attribute
instance-attribute
¶
get_classification_note ¶
Source code in components/support/internal/entities/classification_result.py
is_assignment_to_current_inbox
class-attribute
instance-attribute
¶
is_jtbd_prediction ¶
is_spe_prediction ¶
HarryClassificationResult
dataclass
¶
JTBDClassificationResult
dataclass
¶
JTBDClassificationResult(
chain_of_thought,
top_3_jtbd_predictions,
jtbd_prediction=None,
predicted_inbox=None,
raw_prediction=None,
raw_prediction_classes=None,
)
Bases: DataClassJsonMixin
LegacySpeClassificationResult
dataclass
¶
LegacySpeClassificationResult(
raw_prediction=None,
raw_prediction_classes=None,
predicted_inbox=None,
probability=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,
)
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,
language,
source_id,
created_at,
updated_at,
intercom_state,
tags,
)
Bases: ContactRequestEntity
from_model
classmethod
¶
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
conversation_overview ¶
ConversationOverview
dataclass
¶
Bases: DataClassJsonMixin
Overview of a member support conversation.
conversation_response ¶
ConversationDocument
dataclass
¶
ConversationMessageResponse
dataclass
¶
ConversationMessageResponse(
id,
type,
created_at,
text,
action_type=None,
documents=None,
voice_message=None,
ctas=None,
)
Bases: DataClassJsonMixin
ConversationResponse
dataclass
¶
ConversationVoiceMessage
dataclass
¶
CtaType ¶
EscalationInfo
dataclass
¶
MessageCta
dataclass
¶
NavigationInfo
dataclass
¶
NavigationInfo(
navigate_to,
link_text,
navigation_params=None,
text_color=None,
use_secondary_button=False,
icon_visual=None,
icon_variant=None,
emoji_visual=None,
url_path=None,
is_external_url=False,
disabled=False,
)
Bases: DataClassJsonMixin
csat_data ¶
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
LegacyConversationBacklog
dataclass
¶
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.
LegacySupportAgentAffectation
dataclass
¶
LegacySupportAgentAffectation(
intercom_workspace_id,
is_assigner_enabled,
roles,
level,
spe_matching_scores,
)
This class represents a legacy support agent affectation.
marmot_search_entities ¶
MarmortSearchUserResult
dataclass
¶
MarmortSearchUserResult(
*,
user_id,
first_name,
last_name,
email=None,
address=None,
birth_date=None,
is_sensitive_user=False
)
Bases: DataClassJsonMixin
Results for users entity
phone_support_entities ¶
CallbackSuggestedTimeslot
dataclass
¶
Bases: DataClassJsonMixin
from_controller_param_timeslot
classmethod
¶
Source code in components/support/subcomponents/phone_support/internal/entities/callback_suggested_timeslot.py
HotlineQueue ¶
INOPhoneCallStatus ¶
Bases: AlanBaseEnum
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
LegacyCallbackTimeslotsSuggestion
dataclass
¶
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
LegacyPhoneCallRecording
dataclass
¶
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_offline_experience_company
class-attribute
instance-attribute
¶
snooze_entities ¶
PossibleCancelOnEventMetadata
dataclass
¶
SnoozeAutoCancelDataForCareEvent
dataclass
¶
SnoozeAutoCancelDataForCareEvent(
user_id,
user_first_name,
care_event_id,
care_event_status,
care_event_emoji,
care_event_label,
care_event_date,
care_event_url,
user_marmot_url,
insurance_profile_id,
)
Bases: DataClassJsonMixin
Data needed for care event auto-cancel logic (end notes, status checks, metadata completion).
SnoozeAutoCancelDataForTeletransmission
dataclass
¶
SnoozeAutoCancelDataForTeletransmission(
insurance_profile_id,
user_id,
user_first_name,
current_status,
user_marmot_url,
)
Bases: DataClassJsonMixin
Data needed for teletransmission auto-cancel logic.
SnoozeSequenceSyncData
dataclass
¶
SnoozeSequenceSyncData(
global_sequence_id,
is_cancelled,
cancelled_at,
cancel_reason,
cancelled_on_event,
cancel_on_event,
cancel_on_event_metadata,
template_name,
used_template_id,
used_template_type,
created_at,
updated_at,
intercom_admin_id,
legacy_snooze_sequence_id=None,
intercom_conversation_id=None,
intercom_workspace_id=None,
snoozes=list(),
)
Data for a full snooze sequence, used for bidirectional sync between legacy and global.
from_global_snooze_sequence
staticmethod
¶
Build a SnoozeSequenceSyncData from a global SnoozeSequence model.
Source code in components/support/subcomponents/snooze/internal/compatibility/data/snooze_sequence_sync_data.py
SnoozeSyncData
dataclass
¶
SnoozeSyncData(
snoozed_until,
snoozed_at,
unsnoozed_at,
target_time_string,
on_unsnooze_reply_message,
replied_at,
on_unsnooze_close,
closed_at,
on_unsnooze_leave_note,
note_left_at,
)
Data for a single snooze step, used for bidirectional sync between legacy and global.
UserGreetingContext
dataclass
¶
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).
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: Catalan
SupportAgentRole ¶
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
¶
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_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
from_support_agent
classmethod
¶
Source code in components/support/internal/entities/support_agent_for_response.py
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
¶
intercom_workspace_id
class-attribute
instance-attribute
¶
is_active
class-attribute
instance-attribute
¶
is_assigner_enabled
class-attribute
instance-attribute
¶
level
class-attribute
instance-attribute
¶
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_id
class-attribute
instance-attribute
¶
tag_entities ¶
AutomatedAnswerTag ¶
Bases: AlanBaseEnum
RoleTag ¶
Bases: AlanBaseEnum
language_tag_for_language
staticmethod
¶
Returns the language tag for a given language
Source code in components/support/internal/entities/tags.py
public_sector_ecology
class-attribute
instance-attribute
¶
role_level_tag_for_level
staticmethod
¶
Source code in components/support/internal/entities/tags.py
role_public_sector_tag_by_entity
staticmethod
¶
Source code in components/support/internal/entities/tags.py
components.support.public.enums ¶
action_types ¶
ActionType ¶
Bases: AlanBaseEnum
Represents the type of action that can be taken in a conversation.
dismiss_csat_survey
class-attribute
instance-attribute
¶
resume_conversation
class-attribute
instance-attribute
¶
select_end_conversation
class-attribute
instance-attribute
¶
select_followup_channel
class-attribute
instance-attribute
¶
select_satisfaction
class-attribute
instance-attribute
¶
select_start_new_conversation
class-attribute
instance-attribute
¶
submit_phone_conversation
class-attribute
instance-attribute
¶
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
¶
immediate_callback_request
class-attribute
instance-attribute
¶
lead_callback_request
class-attribute
instance-attribute
¶
legacy_snooze_backfill
class-attribute
instance-attribute
¶
sync_conversation_request
class-attribute
instance-attribute
¶
contact_role_type ¶
conversation_state ¶
ConversationState ¶
Bases: AlanBaseEnum
Enum for member support conversation state
await_csat_response
class-attribute
instance-attribute
¶
await_intercom_conversation_creation
class-attribute
instance-attribute
¶
await_user_close_conversation
class-attribute
instance-attribute
¶
awaiting_channel_selection
class-attribute
instance-attribute
¶
awaiting_email_input
class-attribute
instance-attribute
¶
awaiting_satisfaction
class-attribute
instance-attribute
¶
awaiting_unsatisfied_reason
class-attribute
instance-attribute
¶
awaiting_user_message
class-attribute
instance-attribute
¶
entry_point_origin ¶
EntryPointOrigin ¶
Bases: AlanBaseEnum
Represents the origin of an entry point.
message_type ¶
migration_status ¶
MigrationStatus ¶
Bases: AlanBaseEnum
Migration status of conversation from old model to new model on ai_assistant_conversation_part table
conversation_is_using_back_end_architecture_conversation
class-attribute
instance-attribute
¶
conversation_is_using_back_end_architecture_conversation = "conversation_is_using_back_end_architecture_conversation"
conversation_not_migrated_as_there_is_no_conversation_snapshot
class-attribute
instance-attribute
¶
conversation_to_migrate_to_back_end_architecture_conversation
class-attribute
instance-attribute
¶
conversation_to_migrate_to_back_end_architecture_conversation = "conversation_to_migrate_to_back_end_architecture_conversation"
population_id ¶
PopulationId ¶
Bases: AlanBaseEnum
Population identifiers used for support channel management and cutoff periods. These identify different user groups for support availability.
components.support.public.helpers ¶
assignment_rejection_reasons ¶
get_language_tag_from_rejection_reason ¶
Returns the language tag from the rejection reason
Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
is_callback_rejection_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
is_handover_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
is_language_rejection_reason ¶
Returns a boolean based on whether the rejection reason is due to the ticket being in a language mismatch
Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
is_level_1_rejection_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
is_level_2_rejection_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
is_level_3_rejection_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
get_hash_from_string ¶
get_hash_from_string ¶
Generate a deterministic UUID from str using MD5 hashing.
Source code in components/support/public/helpers/get_hash_from_string.py
i18n ¶
translate ¶
Source code in components/support/internal/business_logic/i18n.py
components.support.public.member_attributes ¶
user_role
module-attribute
¶
user_role = MemberAttributeDefinition[ContactRoleType](
name="user_role",
display_name="User role",
description="Role of the user making the contact (member, admin, or lead)",
getter=_get_user_role,
raw_type=ContactRoleType,
)
components.support.public.queries ¶
csat_queries ¶
Public queries for escalated conversation CSAT data.
Read-only lookups used by country handlers (e.g. FR) to inspect SupportCSAT records without directly accessing internal models.
get_escalated_csat ¶
Return CSAT data if the escalated CSAT has been answered, else None.
Source code in components/support/public/queries/csat_queries.py
get_escalated_pending_csat_conversation_ids ¶
Return the subset of conversation IDs that have a pending (sent) SupportCSAT.
Source code in components/support/public/queries/csat_queries.py
is_escalated_csat_pending ¶
Check if an unanswered SupportCSAT exists for this Intercom conversation.
Source code in components/support/public/queries/csat_queries.py
documents ¶
get_document ¶
Get the document name and document content by its ID.
Source code in components/support/public/queries/documents.py
get_document_name_uri_type ¶
Get the document name, uri and type content by its ID. :param document_id: id of the document :return: name, uri and type content
Source code in components/support/public/queries/documents.py
intercom ¶
get_intercom_conversation_as_messages ¶
Re-export from internal queries for cross-component access.
Source code in components/support/public/queries/intercom.py
get_intercom_user_id ¶
Get an Intercom user ID for the given user.
Source code in components/support/public/queries/intercom.py
get_or_create_intercom_contact_id_from_user_id_and_profile_id ¶
get_or_create_intercom_contact_id_from_user_id_and_profile_id(
user_id, profile_id, intercom_client, is_pro=False
)
Get or create an Intercom contact ID for a given user and profile.
This function attempts to find an existing Intercom contact using the user's personal or professional email. If no contact is found, it creates a new one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_id
|
str
|
The Alan user ID |
required |
profile_id
|
UUID | None
|
The user's profile ID (optional) |
required |
intercom_client
|
IntercomClient
|
The Intercom client to use for API calls |
required |
is_pro
|
bool
|
Whether the user is using a professional user account (admin or pro) |
False
|
Returns:
| Type | Description |
|---|---|
str
|
The Intercom contact ID (existing or newly created) |
Source code in components/support/public/queries/intercom.py
components.support.public.rules ¶
get_conversation_body_for_intercom_receipt ¶
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
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 a conversation_admin_assigned topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_admin_closed
abstractmethod
staticmethod
¶
Handle a conversation_admin_closed topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_admin_opened
abstractmethod
staticmethod
¶
Handle a conversation_admin_opened topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_admin_replied
abstractmethod
staticmethod
¶
Handle a conversation_admin_replied topic.
Source code in components/support/public/services/base_intercom_handler_service.py
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
handle_conversation_admin_snoozed
abstractmethod
staticmethod
¶
Handle a conversation_admin_snoozed topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_admin_unsnoozed
abstractmethod
staticmethod
¶
Handle a conversation_admin_unsnoozed topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_deleted
abstractmethod
staticmethod
¶
Handle a conversation_deleted topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_part_tag_created
abstractmethod
staticmethod
¶
Handle a conversation_part_tag_created topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_user_created
abstractmethod
staticmethod
¶
Handle a conversation_user_created topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_conversation_user_replied
abstractmethod
staticmethod
¶
Handle a conversation_user_replied topic.
Source code in components/support/public/services/base_intercom_handler_service.py
handle_new_conversation
abstractmethod
staticmethod
¶
handle_topic
abstractmethod
staticmethod
¶
handle_topic(
intercom_workspace_id,
intercom_conversation,
topic,
notified_at,
delivery_attempts,
)
Handle any topic, might be used to call other handlers.
Source code in components/support/public/services/base_intercom_handler_service.py
conversation_handler_service ¶
ConversationHandlerService ¶
Bases: ABC
Abstract interface for country-specific conversation handling.
Each country implements this to plug into the global ConversationStateMachine. The handler owns persistence and country-specific side effects (DB writes, DoctorAI triggers, Intercom integration, etc.).
apply_transition
abstractmethod
¶
Apply a state transition to the conversation.
claim_conversation ¶
Claim a guest conversation after smart login. Returns conversation_id.
Source code in components/support/public/services/conversation_handler_service.py
create_conversation ¶
create_conversation(
user_id,
entry_point,
origin,
conversation_context=None,
documents=None,
message=None,
voice_message_id=None,
platform=PlatformType.unknown,
app_version=None,
conversation_language=None,
channel=None,
is_pro=None,
)
Create a new conversation and return its ID.
Source code in components/support/public/services/conversation_handler_service.py
create_phone_conversation
staticmethod
¶
create_phone_conversation(
user_id,
channel,
phone_number,
phone_message,
recording_consent,
conversation_context=None,
)
Handle phone conversation submission — create callback request and link intercom conversation.
Source code in components/support/public/services/conversation_handler_service.py
get_conversation_channel
abstractmethod
¶
get_conversation_messages
abstractmethod
¶
Load all messages for a conversation.
get_conversation_owner_user_id
abstractmethod
¶
Return the user_id that owns this conversation, or None if not found.
get_conversation_state ¶
Load conversation state from ContactRequestIntercomState.
Source code in components/support/public/services/conversation_handler_service.py
get_conversations_overview ¶
List conversation overviews for a user.
Source code in components/support/public/services/conversation_handler_service.py
get_csat ¶
Return CSAT data if CSAT has been answered, else None.
Source code in components/support/public/services/conversation_handler_service.py
get_initial_state ¶
Return the initial state for a newly created conversation.
is_csat_pending ¶
Check if a CSAT survey is pending for this conversation.
Source code in components/support/public/services/conversation_handler_service.py
on_channel_selected
abstractmethod
¶
Handle follow-up channel selection.
on_csat_dismissed ¶
Handle CSAT survey dismissal.
Source code in components/support/public/services/conversation_handler_service.py
on_csat_response ¶
Handle CSAT survey answer. Default uses SupportCSAT via Intercom conversation.
Source code in components/support/public/services/conversation_handler_service.py
on_email_input ¶
Handle email input from NLI user during escalation.
on_end_conversation
abstractmethod
¶
on_escalation
abstractmethod
¶
on_generate_answer
abstractmethod
¶
Stream DoctorAI answer chunks. Handler owns DoctorAI call + persistence.
Source code in components/support/public/services/conversation_handler_service.py
on_reopen_conversation ¶
Handle conversation reopen by admin (closed → escalated).
Source code in components/support/public/services/conversation_handler_service.py
on_resume_conversation ¶
Resume a conversation after smart login claim, triggering DoctorAI reclassification.
Source code in components/support/public/services/conversation_handler_service.py
on_satisfaction_response
abstractmethod
¶
Handle user satisfaction response.
on_send_message ¶
Handle a user sending a message. Returns transition result.
Source code in components/support/public/services/conversation_handler_service.py
on_submit_phone_conversation ¶
on_submit_phone_conversation(
channel,
phone_number,
phone_message,
recording_consent,
conversation_context=None,
conversation_id=None,
user_id=None,
)
Handle phone conversation submission.
Source code in components/support/public/services/conversation_handler_service.py
intercom_conversation_service ¶
IntercomConversationService ¶
Concrete Intercom operations extracted from ConversationHandlerService.
This is a standalone service (no inheritance relationship with handlers). Country-specific handlers delegate to this when they need Intercom interactions.
create_conversation ¶
create_conversation(
user_id,
message,
channel,
conversation_context=None,
documents=None,
is_pro=False,
)
Create a new Intercom conversation and return its ID.
Source code in components/support/public/services/intercom_conversation_service.py
get_conversation_state ¶
Load conversation state from ContactRequestIntercomState.
Source code in components/support/public/services/intercom_conversation_service.py
get_intercom_conversations_overview ¶
Fetch conversation overviews from Intercom.
Source code in components/support/public/services/intercom_conversation_service.py
send_message ¶
Send a member message on an existing Intercom conversation.
Returns the new conversation state.
Source code in components/support/public/services/intercom_conversation_service.py
phone_support_service ¶
PhoneSupportService ¶
Allows interacting with phone support features
create_callback_request
staticmethod
¶
Create a callback request
Source code in components/support/public/services/phone_support_service.py
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
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
get_callback_request_id_for_contact_request
staticmethod
¶
Get a callback request
Source code in components/support/public/services/phone_support_service.py
get_conversation_id_for_callback_request
staticmethod
¶
Retrieve intercom conversation ID for a callback request
Source code in components/support/public/services/phone_support_service.py
get_conversation_id_for_quick_callback_request
staticmethod
¶
Retrieve intercom conversation ID for a callback request
Source code in components/support/public/services/phone_support_service.py
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
is_user_eligible_to_callback
staticmethod
¶
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
update_callback_request
staticmethod
¶
Update a callback request
Source code in components/support/public/services/phone_support_service.py
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
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
support_intercom_handler_service ¶
SupportIntercomHandlerService ¶
Bases: BaseIntercomHandlerService
Contains static methods to handle Intercom webhooks for Support.
handle_conversation_admin_assigned
staticmethod
¶
This method is called when a conversation is assigned to an admin.
Source code in components/support/public/services/support_intercom_handler_service.py
handle_conversation_admin_closed
staticmethod
¶
This method is called when a conversation is closed by an admin.
Source code in components/support/public/services/support_intercom_handler_service.py
handle_conversation_admin_opened
staticmethod
¶
This method is called when a conversation is opened by an admin.
Source code in components/support/public/services/support_intercom_handler_service.py
handle_conversation_admin_replied
staticmethod
¶
This method is called when an admin replies to a conversation.
Source code in components/support/public/services/support_intercom_handler_service.py
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
handle_conversation_admin_snoozed
staticmethod
¶
This method is called when a conversation is snoozed by an admin.
Note: Snooze sequence creation is handled via the API endpoint, not through webhooks. This handler exists for completeness but doesn't trigger any snooze-specific logic.
Source code in components/support/public/services/support_intercom_handler_service.py
handle_conversation_admin_unsnoozed
staticmethod
¶
This method is called when a conversation is unsnoozed (either manually or automatically).
Source code in components/support/public/services/support_intercom_handler_service.py
handle_conversation_deleted
staticmethod
¶
This method is called when a conversation is deleted.
Source code in components/support/public/services/support_intercom_handler_service.py
handle_conversation_part_tag_created
staticmethod
¶
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
handle_conversation_rating_added
staticmethod
¶
Handle conversation rating added event.
Fetches full conversation details from Intercom API (webhook payload doesn't include rating data) and creates/updates SupportCSAT record.
Source code in components/support/public/services/support_intercom_handler_service.py
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 | |
handle_conversation_user_created
staticmethod
¶
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
handle_conversation_user_replied
staticmethod
¶
This method is called when a user replies to a conversation.
Source code in components/support/public/services/support_intercom_handler_service.py
handle_new_conversation
staticmethod
¶
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
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
handle_topic
staticmethod
¶
handle_topic(
intercom_workspace_id,
intercom_conversation,
topic,
notified_at,
delivery_attempts,
)
Handles a conversation webhook topic coming from Intercom
Source code in components/support/public/services/support_intercom_handler_service.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | |
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
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
get_contact_request_source_type_from_context
staticmethod
¶
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
get_contact_request_with_tags_and_intercom_state
staticmethod
¶
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
get_support_opening_status
staticmethod
¶
Get the support status for a given profile
Source code in components/support/public/services/support_service.py
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
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
is_global_intercom_webhook_active
staticmethod
¶
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
is_global_intercom_webhook_fallback_active
staticmethod
¶
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
is_global_intercom_webhook_handlers_enabled
staticmethod
¶
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
is_global_snooze_cron_active
staticmethod
¶
Controls which side (legacy vs global) runs cancel-on-event cron jobs.
When active: global action runs (pre-sync legacy→global, cancel, post-sync global→legacy). When inactive: legacy cron handles everything, global action is a no-op.
Source code in components/support/public/services/support_service.py
is_global_snooze_webhooks_active
staticmethod
¶
Controls which side (legacy vs global) performs Intercom API calls for snooze webhooks.
When active: global handler makes Intercom calls, legacy does DB writes only. When inactive: legacy handler makes Intercom calls, global does DB writes only.
Source code in components/support/public/services/support_service.py
is_support_open
staticmethod
¶
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
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
update_global_snooze_sequence_legacy_id
staticmethod
¶
Sets the legacy_snooze_sequence_id on a support.snooze_sequence