Api reference
components.support.public.blueprint ¶
support_blueprint
module-attribute
¶
support_blueprint = create_blueprint(
name="support",
import_name=__name__,
template_folder=join(
dirname(__file__), "..", "templates"
),
cli_group="support",
)
components.support.public.commands ¶
fix_fr_contact_request_data_2025_09_15 ¶
fix_fr_contact_request_data_2025_09_15 ¶
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_legacy_quick_callback_data ¶
run_import_fr_legacy_quick_callback_data ¶
Import legacy quick callback data from CareCallback and CareConversationContext to create ContactRequest, QuickCallbackRequest and PhoneCall records.
Only imports CareCallback records where is_immediate_callback=True. This command should be run to migrate existing quick callback data to the new schema.
Source code in components/support/public/commands/import_fr_legacy_quick_callback_data.py
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_contact_request_data ¶
run_import_be_legacy_contact_request_data ¶
To globalize the Assigner, we need to import all existing contact requests from Belgium. We already are creating new contact requests for new convos. This command should be run regularly automatically until we stop needing it.
Source code in components/support/public/commands/import_legacy_contact_request_data.py
run_import_fr_legacy_contact_request_data ¶
To globalize the Assigner, we need to import all existing contact requests from France. We already are creating new contact requests for new convos. This command should be run regularly automatically until we stop needing it.
Source code in components/support/public/commands/import_legacy_contact_request_data.py
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_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 cris.assigned_intercom_inbox_id as inbox_id,\n cris.last_inbox_assignment_by,\n cris.waiting_since,\n greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n cris.last_admin_reply_at,\n cr.source_type as type,\n cram.recommended_admin_id,\n cris.started_at,\n cris.state,\n cris.assigned_intercom_admin_id as admin_id,\n cr.created_at,\n -- using tags to compute properties of the conversations\n CASE\n WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n WHEN cr.source_type in ('async_conversation_request', 'unknown') THEN 'SLA[12h]'\n ELSE 'SLA[5min]'\n END AS priority_level,\n CASE\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n ELSE 1\n END AS conversation_workforce_level,\n tags.*\n FROM support.contact_request cr\n JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n CROSS JOIN LATERAL (\n SELECT\n MAX(CASE WHEN crt.name in ('ROLE[CALLBACK]', '#OTHER - CARE TEAM - Eligible call-back request') THEN crt.name END) AS callback_tag,\n MAX(CASE WHEN crt.name = 'App language Dutch' THEN crt.name END) AS dutch_tag,\n MAX(CASE WHEN crt.name = 'App language English' THEN crt.name END) AS english_tag,\n MAX(CASE WHEN crt.name = 'App language French' THEN crt.name END) AS french_tag,\n MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag,\n MAX(CASE WHEN crt.name = 'SENSITIVE' THEN crt.name END) AS uce_reserved_tag\n FROM support.contact_request_tag crt\n WHERE crt.contact_request_id = cr.id\n ) tags\n WHERE\n cr.intercom_workspace_id = (:workspace_id)\n AND cris.state = 'open'\n AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n ),\n\n eligible_contact_request_backlog_with_priority_level AS (\n SELECT\n pcrb.contact_request_id,\n pcrb.intercom_conversation_id,\n pcrb.inbox_id,\n pcrb.last_inbox_assignment_by,\n pcrb.time_reference_for_priority_score,\n pcrb.priority_level,\n pcrb.conversation_workforce_level,\n sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n pcrb.sla_tag,\n pcrb.started_at\n FROM prefiltered_contact_request_backlog pcrb\n CROSS JOIN support_agents_for_computation sa\n WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n -- reduce scope for agents depending on their sync/async eligibility configuration\n AND (\n (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n )\n -- reduce scope for agents not speaking English when not ASYNC\n AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n -- reduce scope for agents not speaking Dutch when not ASYNC\n AND (COALESCE(sa.speaks_dutch, True) OR pcrb.dutch_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n -- reduce scope for agents not speaking French when not ASYNC\n AND (COALESCE(sa.speaks_french, True) OR pcrb.french_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n ),\n eligible_contact_request_backlog_with_context AS (\n SELECT\n ecrb.contact_request_id,\n ecrb.intercom_conversation_id,\n ecrb.priority_level,\n sa.intercom_admin_id,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (5 - 2)\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (30 - 2)\n ELSE -- priority_level = 'SLA[12h]'\n (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (12 * 60 - 2)\n END AS sla_score,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n ELSE\n count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n END AS sla_breached,\n distance_workforce_level_admin_conversation,\n CASE\n WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n END AS workforce_level_matching_score,\n ecrb.inbox_id,\n ss.intercom_inbox_name as inbox_name,\n ecrb.last_inbox_assignment_by,\n ecrb.uce_member_matching_score,\n ecrb.time_reference_for_priority_score,\n ecrb.sla_tag,\n ecrb.conversation_workforce_level,\n CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n FROM eligible_contact_request_backlog_with_priority_level ecrb\n INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n CROSS JOIN support_agents_for_computation sa\n WHERE\n sa.level - ecrb.conversation_workforce_level >= 0\n AND CASE\n WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n END\n ),\n eligible_convos_with_scores AS (\n SELECT\n ec.contact_request_id,\n ec.intercom_conversation_id,\n ec.inbox_id,\n ec.inbox_name,\n NULL as reserved_for_external_agent_platform,\n ec.time_reference_for_priority_score,\n m.score AS expertise_matching_score,\n m.score AS raw_expertise_matching_score,\n POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n uce_member_matching_score,\n COALESCE(\n power(m.score, (:power_weight_a))\n * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n * POWER(workforce_level_matching_score, (:power_weight_d))\n * uce_member_matching_score\n , 0) AS assignment_score,\n sla_score,\n sla_breached,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n priority_level,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n null as n_rejections,\n ec.sla_tag,\n ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n ec.additional_power_weight_c\n FROM eligible_contact_request_backlog_with_context ec\n LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n WHERE crae.contact_request_id IS NULL\n AND COALESCE(m.score, 0.2) >= 0\n ),\n backlog_stats AS MATERIALIZED (\n SELECT\n count(*) as n_assignable_convos,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n FROM eligible_convos_with_scores\n )\n SELECT *, 1.0 AS shift_matching_score\n FROM eligible_convos_with_scores\n CROSS JOIN backlog_stats stats\n ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score\n LIMIT 10;\n "
BE_RANKED_SQL_QUERY
module-attribute
¶
BE_RANKED_SQL_QUERY = "\n WITH support_agents_for_computation AS (\n SELECT\n sa.id as support_agent_id,\n sa.intercom_admin_id,\n sawa.level,\n sawa.intercom_workspace_id,\n --Roles are now stored in an array, so we check for the presence of a role in the array.\n 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '17:00:00'\n THEN 'eligible_for_async' = ANY(sawa.roles)\n ELSE True\n END as is_eligible_for_async,\n NOT sa.is_external_admin AS has_role_uce,\n CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '17:00:00'\n THEN 'eligible_for_callback' = ANY(sawa.roles)\n ELSE False\n END as has_role_callback,\n 'en' = ANY(sa.spoken_languages) as speaks_english,\n 'fr' = ANY(sa.spoken_languages) as speaks_french,\n 'nl' = ANY(sa.spoken_languages) as speaks_dutch\n FROM support.support_agent as sa\n JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n WHERE sa.id = (:support_agent_id) -- the admin requesting an assignment\n ),\n uce_specialisations_mapping AS (\n SELECT sa.intercom_admin_id\n , ss.intercom_inbox_id\n , sasms.score\n FROM support_agents_for_computation sa\n LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n WHERE COALESCE(sasms.score, 0.2) != 0\n ),\n prefiltered_contact_request_backlog AS (\n SELECT\n DISTINCT ON (cr.intercom_conversation_id)\n cr.id as contact_request_id,\n cr.intercom_conversation_id,\n cris.assigned_intercom_inbox_id as inbox_id,\n cris.last_inbox_assignment_by,\n cris.waiting_since,\n greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n cris.last_admin_reply_at,\n cr.source_type as type,\n cram.recommended_admin_id,\n cris.started_at,\n cris.state,\n cris.assigned_intercom_admin_id as admin_id,\n cr.created_at,\n -- using tags to compute properties of the conversations\n CASE\n WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n WHEN cr.source_type in ('async_conversation_request', 'unknown') THEN 'SLA[12h]'\n ELSE 'SLA[5min]'\n END AS priority_level,\n CASE\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n ELSE 1\n END AS conversation_workforce_level,\n tags.*\n FROM support.contact_request cr\n JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n CROSS JOIN LATERAL (\n SELECT\n MAX(CASE WHEN crt.name in ('ROLE[CALLBACK]', '#OTHER - CARE TEAM - Eligible call-back request') THEN crt.name END) AS callback_tag,\n MAX(CASE WHEN crt.name = 'App language Dutch' THEN crt.name END) AS dutch_tag,\n MAX(CASE WHEN crt.name = 'App language English' THEN crt.name END) AS english_tag,\n MAX(CASE WHEN crt.name = 'App language French' THEN crt.name END) AS french_tag,\n MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag,\n MAX(CASE WHEN crt.name = 'SENSITIVE' THEN crt.name END) AS uce_reserved_tag\n FROM support.contact_request_tag crt\n WHERE crt.contact_request_id = cr.id\n ) tags\n WHERE\n cr.intercom_workspace_id = (:workspace_id)\n AND cris.state = 'open'\n AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n ),\n\n eligible_contact_request_backlog_with_priority_level AS (\n SELECT\n pcrb.contact_request_id,\n pcrb.intercom_conversation_id,\n pcrb.inbox_id,\n pcrb.last_inbox_assignment_by,\n pcrb.time_reference_for_priority_score,\n pcrb.priority_level,\n pcrb.conversation_workforce_level,\n sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n pcrb.sla_tag,\n pcrb.started_at\n FROM prefiltered_contact_request_backlog pcrb\n CROSS JOIN support_agents_for_computation sa\n WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n -- reduce scope for agents depending on their sync/async eligibility configuration\n AND (\n (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n )\n -- reduce scope for agents not speaking English when not ASYNC\n AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n -- reduce scope for agents not speaking Dutch when not ASYNC\n AND (COALESCE(sa.speaks_dutch, True) OR pcrb.dutch_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n -- reduce scope for agents not speaking French when not ASYNC\n AND (COALESCE(sa.speaks_french, True) OR pcrb.french_tag IS NULL OR (pcrb.priority_level = 'SLA[12h]' AND sa.has_role_uce))\n ),\n eligible_contact_request_backlog_with_context AS (\n SELECT\n ecrb.contact_request_id,\n ecrb.intercom_conversation_id,\n ecrb.priority_level,\n sa.intercom_admin_id,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (5 - 2)\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (30 - 2)\n ELSE -- priority_level = 'SLA[12h]'\n (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (12 * 60 - 2)\n END AS sla_score,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n ELSE\n count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n END AS sla_breached,\n distance_workforce_level_admin_conversation,\n CASE\n WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n END AS workforce_level_matching_score,\n ecrb.inbox_id,\n ss.intercom_inbox_name as inbox_name,\n ecrb.last_inbox_assignment_by,\n ecrb.uce_member_matching_score,\n ecrb.time_reference_for_priority_score,\n ecrb.sla_tag,\n ecrb.conversation_workforce_level,\n CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n FROM eligible_contact_request_backlog_with_priority_level ecrb\n INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n CROSS JOIN support_agents_for_computation sa\n WHERE\n sa.level - ecrb.conversation_workforce_level >= 0\n AND CASE\n WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n END\n ),\n eligible_convos_with_scores AS (\n SELECT\n ec.contact_request_id,\n ec.intercom_conversation_id,\n ec.inbox_id,\n ec.inbox_name,\n NULL as reserved_for_external_agent_platform,\n ec.time_reference_for_priority_score,\n m.score AS expertise_matching_score,\n m.score AS raw_expertise_matching_score,\n POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n uce_member_matching_score,\n COALESCE(\n power(m.score, (:power_weight_a))\n * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n * POWER(workforce_level_matching_score, (:power_weight_d))\n * uce_member_matching_score\n , 0) AS assignment_score,\n sla_score,\n sla_breached,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n priority_level,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n null as n_rejections,\n ec.sla_tag,\n ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n ec.additional_power_weight_c\n FROM eligible_contact_request_backlog_with_context ec\n LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n WHERE crae.contact_request_id IS NULL\n AND COALESCE(m.score, 0.2) >= 0\n ),\n backlog_stats AS MATERIALIZED (\n SELECT\n count(*) as n_assignable_convos,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n FROM eligible_convos_with_scores\n )\n SELECT\n row_number() OVER (ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS assignment_rank\n , (:support_agent_id)\n , *\n , row_number() OVER (ORDER BY intercom_conversation_id = (:conversation_id) DESC, assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS row_nb\n FROM eligible_convos_with_scores\n CROSS JOIN backlog_stats stats\n ORDER BY row_nb, assignment_score DESC NULLS LAST, time_reference_for_priority_score\n LIMIT 10;\n "
ES_ASSIGNER_SQL_QUERY
module-attribute
¶
ES_ASSIGNER_SQL_QUERY = "\n WITH support_agents_for_computation AS (\n SELECT\n sa.id as support_agent_id,\n sa.intercom_admin_id,\n sawa.level,\n sawa.intercom_workspace_id,\n --Roles are now stored in an array, so we check for the presence of a role in the array.\n 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n THEN 'eligible_for_async' = ANY(sawa.roles)\n ELSE True\n END as is_eligible_for_async,\n CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n THEN 'eligible_for_callback' = ANY(sawa.roles)\n ELSE False\n END as has_role_callback, -- not used yet in SP yet but keep it for consistency\n NOT sa.is_external_admin AS has_role_uce,\n 'en' = ANY(sa.spoken_languages) as speaks_english,\n 'es' = ANY(sa.spoken_languages) as speaks_spanish,\n 'ca' = ANY(sa.spoken_languages) as speaks_catalan\n FROM support.support_agent as sa\n JOIN support.support_agent_workspace_affectation sawa on sa.id = sawa.support_agent_id\n WHERE sa.id = (:support_agent_id) -- the admin requesting an assignment\n AND sawa.intercom_workspace_id = (:workspace_id)\n ),\n uce_specialisations_mapping AS (\n SELECT sa.intercom_admin_id\n , ss.intercom_inbox_id\n , sasms.score\n FROM support_agents_for_computation sa\n LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n WHERE COALESCE(sasms.score, 0.2) != 0\n ),\n prefiltered_contact_request_backlog AS (\n SELECT\n DISTINCT ON (cr.intercom_conversation_id)\n cr.id as contact_request_id,\n cr.intercom_conversation_id,\n cris.assigned_intercom_inbox_id as inbox_id,\n cris.last_inbox_assignment_by,\n cris.waiting_since,\n greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n cris.last_admin_reply_at,\n cr.source_type as type,\n cram.recommended_admin_id,\n cris.started_at,\n cris.state,\n cris.assigned_intercom_admin_id as admin_id,\n cr.created_at,\n -- using tags to compute properties of the conversations\n CASE\n WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n WHEN tags.sla_tag = 'SLA[30min]' THEN 'SLA[30min]'\n WHEN cr.source_type = 'async_conversation_request' THEN 'SLA[12h]'\n ELSE 'SLA[5min]'\n END AS priority_level,\n CASE\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' THEN 3\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n ELSE 1\n END AS conversation_workforce_level,\n tags.*\n FROM support.contact_request cr\n JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n CROSS JOIN LATERAL (\n SELECT\n MAX(CASE WHEN crt.name = 'ROLE[SPANISH]' THEN crt.name END) AS spanish_tag,\n MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag,\n MAX(CASE WHEN crt.name = 'ROLE[CATALAN]' THEN crt.name END) AS catalan_tag,\n MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n FROM support.contact_request_tag crt\n WHERE crt.contact_request_id = cr.id\n ) tags\n WHERE\n cr.intercom_workspace_id = (:workspace_id)\n AND cris.state = 'open'\n AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n ),\n eligible_contact_request_backlog_with_priority_level AS (\n SELECT\n pcrb.contact_request_id,\n pcrb.intercom_conversation_id,\n pcrb.inbox_id,\n pcrb.last_inbox_assignment_by,\n pcrb.time_reference_for_priority_score,\n pcrb.priority_level,\n pcrb.conversation_workforce_level,\n sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n pcrb.sla_tag,\n pcrb.started_at\n FROM prefiltered_contact_request_backlog pcrb\n CROSS JOIN support_agents_for_computation sa\n WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n -- reduce scope for agents depending on their sync/async eligibility configuration\n AND (\n (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]')\n OR\n (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]')\n )\n -- reduce scope for agents not speaking English\n AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n -- reduce scope for agents not speaking Spanish\n AND (COALESCE(sa.speaks_spanish, True) OR pcrb.spanish_tag IS NULL)\n -- reduce scope for agents not speaking Catalan\n AND (COALESCE(sa.speaks_catalan, True) OR pcrb.catalan_tag IS NULL)\n ),\n eligible_contact_request_backlog_with_context AS (\n SELECT\n ecrb.contact_request_id,\n ecrb.intercom_conversation_id,\n ecrb.priority_level,\n sa.intercom_admin_id,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (5 - 2)\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (30 - 2)\n ELSE -- priority_level = 'SLA[12h]'\n (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (12 * 60 - 2)\n END AS sla_score,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n ELSE\n count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n END AS sla_breached,\n distance_workforce_level_admin_conversation,\n CASE\n WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n END AS workforce_level_matching_score,\n ecrb.inbox_id,\n ss.intercom_inbox_name as inbox_name,\n ecrb.last_inbox_assignment_by,\n ecrb.uce_member_matching_score,\n ecrb.time_reference_for_priority_score,\n ecrb.sla_tag,\n ecrb.conversation_workforce_level,\n CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n FROM eligible_contact_request_backlog_with_priority_level ecrb\n INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n CROSS JOIN support_agents_for_computation sa\n WHERE\n sa.level - ecrb.conversation_workforce_level >= 0\n AND CASE\n WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n END\n ),\n eligible_convos_with_scores AS (\n SELECT\n ec.contact_request_id,\n ec.intercom_conversation_id,\n ec.inbox_id,\n ec.inbox_name,\n NULL AS reserved_for_external_agent_platform,\n ec.time_reference_for_priority_score,\n m.score AS expertise_matching_score,\n m.score AS raw_expertise_matching_score,\n POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n uce_member_matching_score,\n COALESCE(\n power(m.score, ((:power_weight_a)))\n * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n * POWER(workforce_level_matching_score, (:power_weight_d))\n * uce_member_matching_score\n , 0) AS assignment_score,\n sla_score,\n sla_breached,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n priority_level,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n null as n_rejections,\n ec.sla_tag,\n ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n ec.additional_power_weight_c\n FROM eligible_contact_request_backlog_with_context ec\n LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n WHERE crae.contact_request_id IS NULL\n AND COALESCE(m.score, 0.2) >= 0\n ),\n backlog_stats AS MATERIALIZED (\n SELECT\n count(*) as n_assignable_convos,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n FROM eligible_convos_with_scores\n )\n SELECT *, 1.0 AS shift_matching_score\n FROM eligible_convos_with_scores\n CROSS JOIN backlog_stats stats\n ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score\n LIMIT 10;\n "
ES_RANKED_SQL_QUERY
module-attribute
¶
ES_RANKED_SQL_QUERY = "\n WITH support_agents_for_computation AS (\n SELECT\n sa.id as support_agent_id,\n sa.intercom_admin_id,\n sawa.level,\n sawa.intercom_workspace_id,\n --Roles are now stored in an array, so we check for the presence of a role in the array.\n 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync,\n CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n THEN 'eligible_for_async' = ANY(sawa.roles)\n ELSE True\n END as is_eligible_for_async,\n CASE WHEN (NOW() AT TIME ZONE 'Europe/Madrid')::TIME BETWEEN '09:00:00' AND '18:00:00'\n THEN 'eligible_for_callback' = ANY(sawa.roles)\n ELSE False\n END as has_role_callback, -- not used yet in SP yet but keep it for consistency\n NOT sa.is_external_admin AS has_role_uce,\n 'en' = ANY(sa.spoken_languages) as speaks_english,\n 'es' = ANY(sa.spoken_languages) as speaks_spanish,\n 'ca' = ANY(sa.spoken_languages) as speaks_catalan\n FROM support.support_agent as sa\n JOIN support.support_agent_workspace_affectation sawa on sa.id = sawa.support_agent_id\n WHERE sa.id = (:support_agent_id) -- the admin requesting an assignment\n AND sawa.intercom_workspace_id = (:workspace_id)\n ),\n uce_specialisations_mapping AS (\n SELECT sa.intercom_admin_id\n , ss.intercom_inbox_id\n , sasms.score\n FROM support_agents_for_computation sa\n LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n WHERE COALESCE(sasms.score, 0.2) != 0\n ),\n prefiltered_contact_request_backlog AS (\n SELECT\n DISTINCT ON (cr.intercom_conversation_id)\n cr.id as contact_request_id,\n cr.intercom_conversation_id,\n cris.assigned_intercom_inbox_id as inbox_id,\n cris.last_inbox_assignment_by,\n cris.waiting_since,\n greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score,\n cris.last_admin_reply_at,\n cr.source_type as type,\n cram.recommended_admin_id,\n cris.started_at,\n cris.state,\n cris.assigned_intercom_admin_id as admin_id,\n cr.created_at,\n -- using tags to compute properties of the conversations\n CASE\n WHEN tags.sla_tag = 'SLA[12h]' or tags.sla_tag = 'ASYNC' THEN 'SLA[12h]'\n WHEN tags.sla_tag = 'SLA[5min]' or tags.sla_tag = 'SYNC' THEN 'SLA[5min]'\n WHEN tags.sla_tag = 'SLA[30min]' THEN 'SLA[30min]'\n WHEN cr.source_type = 'async_conversation_request' THEN 'SLA[12h]'\n ELSE 'SLA[5min]'\n END AS priority_level,\n CASE\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' THEN 3\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n ELSE 1\n END AS conversation_workforce_level,\n tags.*\n FROM support.contact_request cr\n JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id\n CROSS JOIN LATERAL (\n SELECT\n MAX(CASE WHEN crt.name = 'ROLE[SPANISH]' THEN crt.name END) AS spanish_tag,\n MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag,\n MAX(CASE WHEN crt.name = 'ROLE[CATALAN]' THEN crt.name END) AS catalan_tag,\n MAX(CASE WHEN crt.name IN ('SLA[5min]', 'SLA[30min]', 'SLA[12h]', 'SYNC', 'ASYNC') THEN crt.name END) as sla_tag,\n MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n FROM support.contact_request_tag crt\n WHERE crt.contact_request_id = cr.id\n ) tags\n WHERE\n cr.intercom_workspace_id = (:workspace_id)\n AND cris.state = 'open'\n AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n ),\n eligible_contact_request_backlog_with_priority_level AS (\n SELECT\n pcrb.contact_request_id,\n pcrb.intercom_conversation_id,\n pcrb.inbox_id,\n pcrb.last_inbox_assignment_by,\n pcrb.time_reference_for_priority_score,\n pcrb.priority_level,\n pcrb.conversation_workforce_level,\n sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation,\n CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id THEN (:member_matching_score_boost) ELSE 1 END AS uce_member_matching_score,\n pcrb.sla_tag,\n pcrb.started_at\n FROM prefiltered_contact_request_backlog pcrb\n CROSS JOIN support_agents_for_computation sa\n WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n -- reduce scope for agents depending on their sync/async eligibility configuration\n AND (\n (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]')\n OR\n (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]')\n )\n -- reduce scope for agents not speaking English\n AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n -- reduce scope for agents not speaking Spanish\n AND (COALESCE(sa.speaks_spanish, True) OR pcrb.spanish_tag IS NULL)\n -- reduce scope for agents not speaking Catalan\n AND (COALESCE(sa.speaks_catalan, True) OR pcrb.catalan_tag IS NULL)\n ),\n eligible_contact_request_backlog_with_context AS (\n SELECT\n ecrb.contact_request_id,\n ecrb.intercom_conversation_id,\n ecrb.priority_level,\n sa.intercom_admin_id,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (5 - 2)\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (30 - 2)\n ELSE -- priority_level = 'SLA[12h]'\n (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (12 * 60 - 2)\n END AS sla_score,\n CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n ELSE\n count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n END AS sla_breached,\n distance_workforce_level_admin_conversation,\n CASE\n WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n END AS workforce_level_matching_score,\n ecrb.inbox_id,\n ss.intercom_inbox_name as inbox_name,\n ecrb.last_inbox_assignment_by,\n ecrb.uce_member_matching_score,\n ecrb.time_reference_for_priority_score,\n ecrb.sla_tag,\n ecrb.conversation_workforce_level,\n CASE WHEN ecrb.priority_level = 'SLA[5min]' AND sa.level < 3 THEN 2 ELSE 0 END as additional_power_weight_c\n FROM eligible_contact_request_backlog_with_priority_level ecrb\n INNER JOIN support.support_specialization ss on ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner\n CROSS JOIN support_agents_for_computation sa\n WHERE\n sa.level - ecrb.conversation_workforce_level >= 0\n AND CASE\n WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n END\n ),\n eligible_convos_with_scores AS (\n SELECT\n ec.contact_request_id,\n ec.intercom_conversation_id,\n ec.inbox_id,\n ec.inbox_name,\n NULL AS reserved_for_external_agent_platform,\n ec.time_reference_for_priority_score,\n m.score AS expertise_matching_score,\n m.score AS raw_expertise_matching_score,\n POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score,\n POWER(workforce_level_matching_score, (:power_weight_d)) AS workforce_level_matching_score,\n uce_member_matching_score,\n COALESCE(\n power(m.score, ((:power_weight_a)))\n * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n * POWER(workforce_level_matching_score, (:power_weight_d))\n * uce_member_matching_score\n , 0) AS assignment_score,\n sla_score,\n sla_breached,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score,\n priority_level,\n MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level,\n null as n_rejections,\n ec.sla_tag,\n ec.priority_level = 'SLA[5min]' as is_sync_conversation,\n ec.additional_power_weight_c\n FROM eligible_contact_request_backlog_with_context ec\n LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ec.inbox_id\n WHERE crae.contact_request_id IS NULL\n AND COALESCE(m.score, 0.2) >= 0\n ),\n backlog_stats AS MATERIALIZED (\n SELECT\n count(*) as n_assignable_convos,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback,\n count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached,\n count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m,\n percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n FROM eligible_convos_with_scores\n )\n SELECT\n row_number() OVER (ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS assignment_rank\n , (:support_agent_id)\n , *\n , row_number() OVER (ORDER BY intercom_conversation_id = (:conversation_id) DESC, assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS row_nb\n FROM eligible_convos_with_scores\n CROSS JOIN backlog_stats stats\n ORDER BY row_nb, assignment_score DESC NULLS LAST, time_reference_for_priority_score\n LIMIT 10;\n "
FR_ASSIGNER_SQL_QUERY
module-attribute
¶
FR_ASSIGNER_SQL_QUERY = "\n -- This query is a translation of the original GET_CONVERSATION_BY_ASSIGNMENT_SCORE_EU_SQL\n -- from the FR resolution_platform to the global support component.\n -- Major changes include:\n -- - Replacing FR tables with global support tables (e.g., care_conversation_backlog -> contact_request_intercom_state).\n -- - Adapting to new schemas (e.g., support_agent roles are now in an array).\n -- - Removing the old_spe_new_spe_mapping CTE as it is no longer needed.\n WITH recalibrated_support_agents AS (\n SELECT\n sa.id\n FROM support.assigner_recalibration ar\n JOIN support.support_agent sa ON ar.alan_email = sa.alan_email\n JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND ar.intercom_workspace_id = sawa.intercom_workspace_id\n WHERE ar.intercom_workspace_id = (:workspace_id)\n ),\n support_agents_for_computation AS (\n SELECT\n sa.id as support_agent_id\n , sa.intercom_admin_id\n , sawa.level\n , sawa.intercom_workspace_id\n --Roles are now stored in an array, so we check for the presence of a role in the array.\n , 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync\n , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n THEN 'eligible_for_async' = ANY(sawa.roles)\n ELSE True\n END as is_eligible_for_async\n , sa.platform_name as platform\n , NOT sa.is_external_admin AS has_role_uce\n , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n THEN 'eligible_for_callback' = ANY(sawa.roles)\n ELSE False\n END as has_role_callback\n , 'automated_answer_reviewer' = ANY(sawa.roles) as has_reviewer_role\n , 'en' = ANY(sa.spoken_languages) as speaks_english\n , sa.work_location != 'fr' as is_offshore\n FROM support.support_agent as sa\n JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n WHERE (sa.id = ANY((select array(SELECT id FROM recalibrated_support_agents))::uuid[])\n OR sa.id = (:support_agent_id)) -- the admin requesting an assignment\n ),\n uce_specialisations_mapping AS (\n SELECT sa.intercom_admin_id\n , ss.intercom_inbox_id\n , sasms.score\n FROM support_agents_for_computation sa\n LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id\n LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n WHERE sasms.score != 0\n ),\n prefiltered_contact_request_backlog AS (\n SELECT\n DISTINCT ON (cr.intercom_conversation_id)\n cr.id as contact_request_id\n , cr.intercom_conversation_id\n , cris.assigned_intercom_inbox_id as inbox_id\n , cris.last_inbox_assignment_by\n , cris.waiting_since\n , greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score\n , cris.last_admin_reply_at\n , cram.recommended_admin_id as recommended_admin_id\n , cris.started_at\n , COALESCE(cram.reserved_for_automated_answers_review, FALSE) as reserved_for_review\n , cris.state\n , cris.assigned_intercom_admin_id as admin_id\n , cr.created_at\n -- using tags to compute properties of the conversations\n , SUBSTRING(tags.platform_tag, 'ROLE\\[(.*)\\]') AS reserved_for_external_agent_platform\n -- priority level is defined by the sync/async nature of the convo but can be overwritten during a handover\n , CASE\n WHEN tags.sla_tag = 'SLA[12h]' THEN 'SLA[12h]'\n WHEN tags.sla_tag = 'SLA[5min]' THEN 'SLA[5min]'\n WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n -- if not sync tagged or if one of the hotline source type → mark async.\n WHEN tags.type_tag != 'SYNC' or cr.source_type in ('immediate_callback_request', 'hotline_request')THEN 'SLA[12h]'\n ELSE 'SLA[5min]'\n END AS priority_level\n -- workforce level of the conversation\n , CASE\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n ELSE 1\n END AS conversation_workforce_level\n , tags.*\n FROM support.contact_request cr\n JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id -- to define priority level\n CROSS JOIN LATERAL (\n SELECT\n MAX(CASE WHEN crt.name IN ('ROLE[UCE]', 'COMPLEX CLAIM', 'SENSITIVE') THEN crt.name END) AS uce_reserved_tag\n , MAX(CASE WHEN crt.name = 'ROLE[CALLBACK]' THEN crt.name END) AS callback_tag\n , MAX(CASE WHEN crt.name IN ('ROLE[WEBHELP]', 'ROLE[ONEPILOT]') THEN crt.name END) AS platform_tag\n , MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag\n , MAX(CASE WHEN crt.name = 'ROLE[GEORESTRICTED]' THEN crt.name END) AS geo_tag\n , MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n , MAX(CASE WHEN crt.name IN ('bypass-assigner', 'Abusive Conversation') THEN crt.name END) AS bypass_tag\n , MAX(CASE WHEN crt.name IN ('SLA[12h]', 'SLA[5min]', 'SLA[30min]') THEN crt.name END) as sla_tag\n , MAX(CASE WHEN crt.name IN ('SYNC', 'ASYNC') THEN crt.name END) as type_tag\n , MAX(CASE WHEN crt.name IN ('bypass-classification') THEN crt.name END) AS classification_bypass_tag\n FROM support.contact_request_tag crt\n WHERE crt.contact_request_id = cr.id\n ) tags\n WHERE\n cr.intercom_workspace_id = (:workspace_id)\n AND cris.state = 'open'\n AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n AND cr.created_at > '2021-11-26' -- until data is cleared in prod\n AND tags.bypass_tag IS NULL -- exclude convos explicitly flagged as bypassing the Assigner\n ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n ),\n eligible_contact_request_backlog_with_priority_level AS (\n SELECT\n pcrb.contact_request_id\n , pcrb.intercom_conversation_id\n , pcrb.inbox_id\n , pcrb.last_inbox_assignment_by\n , pcrb.time_reference_for_priority_score\n , pcrb.priority_level\n , pcrb.conversation_workforce_level\n , sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation\n , CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id\n THEN (:member_matching_score_boost)\n ELSE 1\n END AS uce_member_matching_score\n , pcrb.started_at\n , pcrb.reserved_for_external_agent_platform\n , pcrb.reserved_for_review\n , pcrb.classification_bypass_tag IS NOT NULL as bypassed_classifier\n FROM prefiltered_contact_request_backlog pcrb\n CROSS JOIN support_agents_for_computation sa\n WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n -- reduce scope for agents depending on their sync/async eligibility configuration\n AND (\n -- Admin is ELIGIBLE_FOR_SYNC and conversation is NOT ASYNC and NOT CALLBACK\n (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n -- Admin is ELIGIBLE_FOR_ASYNC and conversation is NOT SLA[5min] and NOT CALLBACK\n (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n -- Admin has CALLBACK role and conversation is CALLBACK\n (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n )\n -- assign automated answer reviews for reviewers\n AND (COALESCE(sa.has_reviewer_role, False) OR pcrb.reserved_for_review IS NOT TRUE)\n -- reduce scope for agents not speaking english\n AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n -- reduce scope of georestricted convos for offshore agents\n AND (COALESCE(NOT sa.is_offshore, False) OR pcrb.geo_tag IS NULL)\n AND CASE\n WHEN NOT pcrb.reserved_for_review THEN sa.level - pcrb.conversation_workforce_level >= 0\n ELSE TRUE\n END\n -- handle conversations reserved for specific external agent platforms\n AND CASE\n WHEN pcrb.reserved_for_external_agent_platform IS NOT NULL\n THEN UPPER(sa.platform) = UPPER(pcrb.reserved_for_external_agent_platform)\n ELSE TRUE\n END\n ),\n\n contact_request_context_digest AS (\n SELECT\n cr.id as contact_request_id\n , cr.duplicated_from_id as original_contact_request_id\n , COALESCE(cr.classification_result->'raw_prediction_classes', cr.classification_result->'prediction'->'raw_prediction_classes' ) as raw_prediction_classes\n , COALESCE(cr.classification_result->'raw_prediction', cr.classification_result->'prediction'->'raw_prediction') as raw_prediction\n , created_at\n FROM support.contact_request cr\n INNER JOIN eligible_contact_request_backlog_with_priority_level e ON e.contact_request_id = cr.id\n WHERE cr.intercom_workspace_id = (:workspace_id)\n ),\n classifier_classes AS (\n SELECT\n raw_prediction_classes AS classes\n FROM contact_request_context_digest\n WHERE jsonb_typeof(raw_prediction) = 'array'\n ORDER BY created_at DESC\n LIMIT 1\n ),\n classifier_classes_as_array AS (\n SELECT ARRAY_AGG(cls) classes_array\n FROM classifier_classes, JSONB_ARRAY_ELEMENTS_TEXT(classifier_classes.classes) cls\n ),\n\n -- # 1. define the eligible backlog for the UCE requesting an assignment\n eligible_contact_request_backlog_with_context AS (\n SELECT\n ecrb.contact_request_id\n , ecrb.intercom_conversation_id\n , ecrb.priority_level\n -- 1. SLA score = (1 min + waiting_time in min) / (1 min + SLA in min)\n , CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (5 - 2)\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (30 - 2)\n ELSE -- priority_level = 'SLA[12h]'\n (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (12 * 60 - 2)\n END AS sla_score\n , CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n ELSE\n count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n END AS sla_breached\n -- 2. workforce level matching score\n , distance_workforce_level_admin_conversation\n -- the workforce_level_score_boost is here to give a higher assignment score when the level of the conversation matches the level of the admin\n -- workforce_level_matching_score doesn't matter the escalation reviews of automated answers\n , CASE\n WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n WHEN ecrb.reserved_for_review THEN 1.0\n WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n END AS workforce_level_matching_score\n -- 3. parsing of classification result (to be used later for the expertise matching score computation)\n , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n THEN COALESCE(crcd.raw_prediction, crcd2.raw_prediction)\n ELSE array_to_json(array_fill(1.0 / jsonb_array_length(cls.classes) , ARRAY[jsonb_array_length(cls.classes)]))::jsonb\n END AS predicted_probabilities\n , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)) = 'array'\n THEN COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)\n ELSE cls.classes\n END AS predicted_classes\n , ecrb.inbox_id\n , ss.intercom_inbox_name as inbox_name\n , ecrb.last_inbox_assignment_by\n , ecrb.uce_member_matching_score\n , ecrb.time_reference_for_priority_score\n , ecrb.reserved_for_external_agent_platform\n , ecrb.conversation_workforce_level\n , ecrb.reserved_for_review\n , ecrb.bypassed_classifier\n FROM eligible_contact_request_backlog_with_priority_level ecrb\n INNER JOIN support.support_specialization ss ON ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner AND ss.intercom_workspace_id = (:workspace_id) -- only consider inboxes recognized by the assigner\n LEFT JOIN contact_request_context_digest crcd ON crcd.contact_request_id = ecrb.contact_request_id\n LEFT JOIN contact_request_context_digest crcd2 ON crcd.original_contact_request_id = crcd2.contact_request_id -- inherit context from parent convo when relevant\n CROSS JOIN classifier_classes cls\n -- we want to prevent assignments of tickets not yet classified, but don't want to wait more than 30 seconds for sync and 2 minutes for the rest\n WHERE jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n OR CASE\n WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n END\n ),\n -- # 2. compute expertise matching scores for all (eligible convo X UCE) pairs\n convos_x_probabilities AS (\n -- expand the eligible convos on all predicted classes\n SELECT\n s.*\n , s._predicted_class AS predicted_class\n FROM (\n SELECT\n contact_request_id\n , intercom_conversation_id\n , priority_level\n , sla_score\n , sla_breached\n , conversation_workforce_level\n , workforce_level_matching_score\n , uce_member_matching_score\n , jsonb_array_elements(predicted_probabilities)::numeric AS predicted_probability\n , trim(jsonb_array_elements(predicted_classes)::text, '\"') AS _predicted_class\n , last_inbox_assignment_by\n , inbox_id\n , inbox_name\n , time_reference_for_priority_score\n , reserved_for_external_agent_platform\n , reserved_for_review\n , bypassed_classifier\n FROM eligible_contact_request_backlog_with_context\n ) s\n UNION ALL\n -- expand for classes unknown by the classifier as well\n SELECT\n contact_request_id\n , intercom_conversation_id\n , priority_level\n , sla_score\n , sla_breached\n , conversation_workforce_level\n , workforce_level_matching_score\n , uce_member_matching_score\n , 0 AS predicted_probability\n , t.inbox_unknown_by_classifier AS _predicted_class\n , last_inbox_assignment_by\n , inbox_id\n , inbox_name\n , time_reference_for_priority_score\n , reserved_for_external_agent_platform\n , reserved_for_review\n , bypassed_classifier\n , t.inbox_unknown_by_classifier AS predicted_class\n FROM eligible_contact_request_backlog_with_context\n CROSS JOIN (\n SELECT intercom_inbox_name AS inbox_unknown_by_classifier FROM support.support_specialization\n EXCEPT\n SELECT JSONB_ARRAY_ELEMENTS_TEXT(classes) FROM classifier_classes\n ) t\n ),\n -- Calibration pass 1: Compute min/max across ALL agents for calibration (lightweight aggregation)\n calibration_min_max_per_agent AS (\n SELECT\n sa.support_agent_id\n , cxp.contact_request_id\n , sum((CASE\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n ELSE cxp.predicted_probability::numeric\n END)\n * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n FROM convos_x_probabilities cxp\n JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n CROSS JOIN support_agents_for_computation sa\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n CROSS JOIN classifier_classes_as_array cls\n WHERE\n CASE\n WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n ELSE TRUE\n END\n AND CASE\n WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n ELSE TRUE\n END\n GROUP BY sa.support_agent_id, cxp.contact_request_id\n ),\n min_max_expertise_matching_score_per_convo AS (\n SELECT\n contact_request_id\n , min(power(raw_expertise_matching_score, (:power_weight_a))) AS min_ems\n , max(power(raw_expertise_matching_score, (:power_weight_a))) AS max_ems\n FROM calibration_min_max_per_agent\n GROUP BY contact_request_id\n ),\n -- calibration pass 2: Compute detailed scores ONLY for the requesting agent which is less costly\n eligible_convos_with_expertise_matching_score_per_uce AS (\n SELECT\n sa.intercom_admin_id\n , sa.support_agent_id\n , cxp.intercom_conversation_id\n , cxp.contact_request_id\n , cxp.inbox_id\n , cxp.inbox_name\n , cxp.priority_level\n , cxp.workforce_level_matching_score\n , cxp.sla_score\n , cxp.sla_breached\n , cxp.uce_member_matching_score\n , cxp.time_reference_for_priority_score\n , cxp.reserved_for_external_agent_platform\n , cxp.reserved_for_review\n , sum((CASE\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n ELSE cxp.predicted_probability::numeric\n END)\n * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n -- L1 & L2 agents have an extra boost on priority_level for sync conversations\n , case when cxp.priority_level = 'SLA[5min]' and sa.level < 3 then 2 else 0 end as additional_power_weight_c\n FROM convos_x_probabilities cxp\n JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n CROSS JOIN support_agents_for_computation sa\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n CROSS JOIN classifier_classes_as_array cls\n -- additional exclusion conditions for both the care expert asking for the assignment and the calibration cohort\n WHERE\n sa.support_agent_id = (:support_agent_id)::uuid -- ONLY requesting agent\n AND CASE\n WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n ELSE TRUE\n END\n -- handle conversations reserved for specific external agent platforms\n AND CASE\n WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n ELSE TRUE\n END\n GROUP BY sa.intercom_admin_id, sa.support_agent_id, cxp.contact_request_id, cxp.inbox_id, cxp.inbox_name, cxp.priority_level, cxp.workforce_level_matching_score, cxp.sla_score, cxp.sla_breached, cxp.uce_member_matching_score,\n cxp.time_reference_for_priority_score, cxp.reserved_for_external_agent_platform, cxp.reserved_for_review, cxp.intercom_conversation_id, sa.level\n ),\n eligible_convos_with_scores AS (\n SELECT\n ec.contact_request_id\n , ec.intercom_conversation_id\n , ec.inbox_id\n , ec.inbox_name\n , ec.time_reference_for_priority_score\n -- normalization of the expertise matching score (distributed over [20%, 100%])\n , CASE WHEN max_ems > min_ems\n THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n ELSE 1\n END AS expertise_matching_score\n , POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score\n , CASE\n WHEN NOT ec.reserved_for_review THEN POWER(workforce_level_matching_score, (:power_weight_d)) ELSE 1\n END AS workforce_level_matching_score\n , uce_member_matching_score\n , COALESCE(\n CASE\n WHEN max_ems > min_ems\n THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n ELSE 1\n END\n * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n * POWER(workforce_level_matching_score, (:power_weight_d))\n * uce_member_matching_score\n , 0) AS assignment_score\n , raw_expertise_matching_score\n , sla_score\n , sla_breached\n , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score\n , priority_level\n , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level\n , m.max_ems\n , ec.reserved_for_external_agent_platform\n , ec.reserved_for_review\n , null as n_rejections\n , ec.priority_level = 'SLA[5min]' as is_sync_conversation\n , ec.additional_power_weight_c\n FROM eligible_convos_with_expertise_matching_score_per_uce ec\n JOIN min_max_expertise_matching_score_per_convo m USING(contact_request_id)\n LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n WHERE\n crae.contact_request_id IS NULL\n AND ec.raw_expertise_matching_score > 0 -- exclude conversations with negative expertise matching score for current UCE\n ),\n backlog_stats AS MATERIALIZED (\n SELECT\n count(*) as n_assignable_convos\n , count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async\n , count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync\n , count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback\n , count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached\n , count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached\n , count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n FROM eligible_convos_with_scores\n )\n SELECT\n *\n , 1.0 AS shift_matching_score\n FROM eligible_convos_with_scores\n CROSS JOIN backlog_stats stats\n ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score\n LIMIT 10;\n "
FR_RANKED_SQL_QUERY
module-attribute
¶
FR_RANKED_SQL_QUERY = "\n -- This query is a translation of the original GET_CONVERSATION_BY_ASSIGNMENT_SCORE_EU_SQL\n -- from the FR resolution_platform to the global support component.\n -- Major changes include:\n -- - Replacing FR tables with global support tables (e.g., care_conversation_backlog -> contact_request_intercom_state).\n -- - Adapting to new schemas (e.g., support_agent roles are now in an array).\n -- - Removing the old_spe_new_spe_mapping CTE as it is no longer needed.\n WITH recalibrated_support_agents AS (\n SELECT\n sa.id\n FROM support.assigner_recalibration ar\n JOIN support.support_agent sa ON ar.alan_email = sa.alan_email\n JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND ar.intercom_workspace_id = sawa.intercom_workspace_id\n WHERE ar.intercom_workspace_id = (:workspace_id)\n ),\n support_agents_for_computation AS (\n SELECT\n sa.id as support_agent_id\n , sa.intercom_admin_id\n , sawa.level\n , sawa.intercom_workspace_id\n --Roles are now stored in an array, so we check for the presence of a role in the array.\n , 'eligible_for_sync' = ANY(sawa.roles) as is_eligible_for_sync\n , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n THEN 'eligible_for_async' = ANY(sawa.roles)\n ELSE True\n END as is_eligible_for_async\n , sa.platform_name as platform\n , NOT sa.is_external_admin AS has_role_uce\n , CASE WHEN (NOW() AT TIME ZONE 'Europe/Paris')::TIME BETWEEN '09:00:00' AND '19:00:00'\n THEN 'eligible_for_callback' = ANY(sawa.roles)\n ELSE False\n END as has_role_callback\n , 'automated_answer_reviewer' = ANY(sawa.roles) as has_reviewer_role\n , 'en' = ANY(sa.spoken_languages) as speaks_english\n , sa.work_location != 'fr' as is_offshore\n FROM support.support_agent as sa\n JOIN support.support_agent_workspace_affectation sawa ON sa.id = sawa.support_agent_id AND sawa.intercom_workspace_id = (:workspace_id)\n WHERE (sa.id = ANY((select array(SELECT id FROM recalibrated_support_agents))::uuid[])\n OR sa.id = (:support_agent_id)) -- the admin requesting an assignment\n ),\n uce_specialisations_mapping AS (\n SELECT sa.intercom_admin_id\n , ss.intercom_inbox_id\n , sasms.score\n FROM support_agents_for_computation sa\n LEFT JOIN support.support_agent_workspace_affectation sawa ON sawa.support_agent_id = sa.support_agent_id\n LEFT JOIN support.support_agent_spe_matching_score sasms ON sasms.support_agent_workspace_affectation_id = sawa.id\n LEFT JOIN support.support_specialization ss ON ss.id = sasms.support_specialization_id\n WHERE sasms.score != 0\n ),\n prefiltered_contact_request_backlog AS (\n SELECT\n DISTINCT ON (cr.intercom_conversation_id)\n cr.id as contact_request_id\n , cr.intercom_conversation_id\n , cris.assigned_intercom_inbox_id as inbox_id\n , cris.last_inbox_assignment_by\n , cris.waiting_since\n , greatest(cris.waiting_since, cris.last_admin_reply_at) AS time_reference_for_priority_score\n , cris.last_admin_reply_at\n , cram.recommended_admin_id as recommended_admin_id\n , cris.started_at\n , COALESCE(cram.reserved_for_automated_answers_review, FALSE) as reserved_for_review\n , cris.state\n , cris.assigned_intercom_admin_id as admin_id\n , cr.created_at\n -- using tags to compute properties of the conversations\n , SUBSTRING(tags.platform_tag, 'ROLE\\[(.*)\\]') AS reserved_for_external_agent_platform\n -- priority level is defined by the sync/async nature of the convo but can be overwritten during a handover\n , CASE\n WHEN tags.sla_tag = 'SLA[12h]' THEN 'SLA[12h]'\n WHEN tags.sla_tag = 'SLA[5min]' THEN 'SLA[5min]'\n WHEN tags.sla_tag = 'SLA[30min]' OR tags.callback_tag = 'ROLE[CALLBACK]' THEN 'SLA[30min]'\n -- if not sync tagged or if one of the hotline source type → mark async.\n WHEN tags.type_tag != 'SYNC' or cr.source_type in ('immediate_callback_request', 'hotline_request')THEN 'SLA[12h]'\n ELSE 'SLA[5min]'\n END AS priority_level\n -- workforce level of the conversation\n , CASE\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL3]' OR tags.uce_reserved_tag IS NOT NULL THEN 3\n WHEN tags.workforce_level_tag = 'ROLE[LEVEL2]' THEN 2\n ELSE 1\n END AS conversation_workforce_level\n , tags.*\n FROM support.contact_request cr\n JOIN support.contact_request_intercom_state cris ON cr.id = cris.contact_request_id\n LEFT JOIN support.contact_request_assignment_metadata cram ON cr.id = cram.contact_request_id\n LEFT JOIN support.contact_request_tag crt ON cr.id = crt.contact_request_id -- to define priority level\n CROSS JOIN LATERAL (\n SELECT\n MAX(CASE WHEN crt.name IN ('ROLE[UCE]', 'COMPLEX CLAIM', 'SENSITIVE') THEN crt.name END) AS uce_reserved_tag\n , MAX(CASE WHEN crt.name = 'ROLE[CALLBACK]' THEN crt.name END) AS callback_tag\n , MAX(CASE WHEN crt.name IN ('ROLE[WEBHELP]', 'ROLE[ONEPILOT]') THEN crt.name END) AS platform_tag\n , MAX(CASE WHEN crt.name = 'ROLE[ENGLISH]' THEN crt.name END) AS english_tag\n , MAX(CASE WHEN crt.name = 'ROLE[GEORESTRICTED]' THEN crt.name END) AS geo_tag\n , MAX(CASE WHEN crt.name IN ('ROLE[LEVEL1]','ROLE[LEVEL2]','ROLE[LEVEL3]') THEN crt.name END) AS workforce_level_tag\n , MAX(CASE WHEN crt.name IN ('bypass-assigner', 'Abusive Conversation') THEN crt.name END) AS bypass_tag\n , MAX(CASE WHEN crt.name IN ('SLA[12h]', 'SLA[5min]', 'SLA[30min]') THEN crt.name END) as sla_tag\n , MAX(CASE WHEN crt.name IN ('SYNC', 'ASYNC') THEN crt.name END) as type_tag\n , MAX(CASE WHEN crt.name IN ('bypass-classification') THEN crt.name END) AS classification_bypass_tag\n FROM support.contact_request_tag crt\n WHERE crt.contact_request_id = cr.id\n ) tags\n WHERE\n cr.intercom_workspace_id = (:workspace_id)\n AND cris.state = 'open'\n AND (cris.assigned_intercom_admin_id IS NULL OR cris.assigned_intercom_admin_id = '') -- testing for empty string for records updated via flask admin\n AND cr.created_at > '2021-11-26' -- until data is cleared in prod\n AND tags.bypass_tag IS NULL -- exclude convos explicitly flagged as bypassing the Assigner\n ORDER BY cr.intercom_conversation_id, cr.id, crt.notified_at DESC\n ),\n eligible_contact_request_backlog_with_priority_level AS (\n SELECT\n pcrb.contact_request_id\n , pcrb.intercom_conversation_id\n , pcrb.inbox_id\n , pcrb.last_inbox_assignment_by\n , pcrb.time_reference_for_priority_score\n , pcrb.priority_level\n , pcrb.conversation_workforce_level\n , sa.level - pcrb.conversation_workforce_level AS distance_workforce_level_admin_conversation\n , CASE WHEN pcrb.recommended_admin_id = sa.intercom_admin_id\n THEN (:member_matching_score_boost)\n ELSE 1\n END AS uce_member_matching_score\n , pcrb.started_at\n , pcrb.reserved_for_external_agent_platform\n , pcrb.reserved_for_review\n , pcrb.classification_bypass_tag IS NOT NULL as bypassed_classifier\n FROM prefiltered_contact_request_backlog pcrb\n CROSS JOIN support_agents_for_computation sa\n WHERE sa.support_agent_id = (:support_agent_id) -- join only on the admin requesting an assignment\n -- reduce scope for agents depending on their sync/async eligibility configuration\n AND (\n -- Admin is ELIGIBLE_FOR_SYNC and conversation is NOT ASYNC and NOT CALLBACK\n (COALESCE(sa.is_eligible_for_sync, True) AND pcrb.priority_level = 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n -- Admin is ELIGIBLE_FOR_ASYNC and conversation is NOT SLA[5min] and NOT CALLBACK\n (COALESCE(sa.is_eligible_for_async, True) AND pcrb.priority_level != 'SLA[5min]' AND pcrb.callback_tag IS NULL)\n OR\n -- Admin has CALLBACK role and conversation is CALLBACK\n (COALESCE(sa.has_role_callback, True) AND pcrb.callback_tag IS NOT NULL)\n )\n -- assign automated answer reviews for reviewers\n AND (COALESCE(sa.has_reviewer_role, False) OR pcrb.reserved_for_review IS NOT TRUE)\n -- reduce scope for agents not speaking english\n AND (COALESCE(sa.speaks_english, True) OR pcrb.english_tag IS NULL)\n -- reduce scope of georestricted convos for offshore agents\n AND (COALESCE(NOT sa.is_offshore, False) OR pcrb.geo_tag IS NULL)\n AND CASE\n WHEN NOT pcrb.reserved_for_review THEN sa.level - pcrb.conversation_workforce_level >= 0\n ELSE TRUE\n END\n -- handle conversations reserved for specific external agent platforms\n AND CASE\n WHEN pcrb.reserved_for_external_agent_platform IS NOT NULL\n THEN UPPER(sa.platform) = UPPER(pcrb.reserved_for_external_agent_platform)\n ELSE TRUE\n END\n ),\n\n contact_request_context_digest AS (\n SELECT\n cr.id as contact_request_id\n , cr.duplicated_from_id as original_contact_request_id\n , COALESCE(cr.classification_result->'raw_prediction_classes', cr.classification_result->'prediction'->'raw_prediction_classes' ) as raw_prediction_classes\n , COALESCE(cr.classification_result->'raw_prediction', cr.classification_result->'prediction'->'raw_prediction') as raw_prediction\n , created_at\n FROM support.contact_request cr\n INNER JOIN eligible_contact_request_backlog_with_priority_level e ON e.contact_request_id = cr.id\n WHERE cr.intercom_workspace_id = (:workspace_id)\n ),\n classifier_classes AS (\n SELECT\n raw_prediction_classes AS classes\n FROM contact_request_context_digest\n WHERE jsonb_typeof(raw_prediction) = 'array'\n ORDER BY created_at DESC\n LIMIT 1\n ),\n classifier_classes_as_array AS (\n SELECT ARRAY_AGG(cls) classes_array\n FROM classifier_classes, JSONB_ARRAY_ELEMENTS_TEXT(classifier_classes.classes) cls\n ),\n\n -- # 1. define the eligible backlog for the UCE requesting an assignment\n eligible_contact_request_backlog_with_context AS (\n SELECT\n ecrb.contact_request_id\n , ecrb.intercom_conversation_id\n , ecrb.priority_level\n -- 1. SLA score = (1 min + waiting_time in min) / (1 min + SLA in min)\n , CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN (:sync_sla_score_boost) * (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (5 - 2)\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (30 - 2)\n ELSE -- priority_level = 'SLA[12h]'\n (3 + count_business_minutes_between(ecrb.time_reference_for_priority_score, now())) / (12 * 60 - 2)\n END AS sla_score\n , CASE\n WHEN ecrb.priority_level = 'SLA[5min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 5\n WHEN ecrb.priority_level = 'SLA[30min]'\n THEN count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 30\n ELSE\n count_business_minutes_between(ecrb.time_reference_for_priority_score, now()) >= 12 * 60\n END AS sla_breached\n -- 2. workforce level matching score\n , distance_workforce_level_admin_conversation\n -- the workforce_level_score_boost is here to give a higher assignment score when the level of the conversation matches the level of the admin\n -- workforce_level_matching_score doesn't matter the escalation reviews of automated answers\n , CASE\n WHEN distance_workforce_level_admin_conversation = 0 THEN (:workforce_level_score_boost)\n WHEN ecrb.reserved_for_review THEN 1.0\n WHEN distance_workforce_level_admin_conversation < 0 THEN NULL\n ELSE 1.0 / (distance_workforce_level_admin_conversation + 1)\n END AS workforce_level_matching_score\n -- 3. parsing of classification result (to be used later for the expertise matching score computation)\n , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n THEN COALESCE(crcd.raw_prediction, crcd2.raw_prediction)\n ELSE array_to_json(array_fill(1.0 / jsonb_array_length(cls.classes) , ARRAY[jsonb_array_length(cls.classes)]))::jsonb\n END AS predicted_probabilities\n , CASE WHEN jsonb_typeof(COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)) = 'array'\n THEN COALESCE(crcd.raw_prediction_classes, crcd2.raw_prediction_classes)\n ELSE cls.classes\n END AS predicted_classes\n , ecrb.inbox_id\n , ss.intercom_inbox_name as inbox_name\n , ecrb.last_inbox_assignment_by\n , ecrb.uce_member_matching_score\n , ecrb.time_reference_for_priority_score\n , ecrb.reserved_for_external_agent_platform\n , ecrb.conversation_workforce_level\n , ecrb.reserved_for_review\n , ecrb.bypassed_classifier\n FROM eligible_contact_request_backlog_with_priority_level ecrb\n INNER JOIN support.support_specialization ss ON ss.intercom_inbox_id = ecrb.inbox_id AND ss.is_used_by_assigner AND ss.intercom_workspace_id = (:workspace_id) -- only consider inboxes recognized by the assigner\n LEFT JOIN contact_request_context_digest crcd ON crcd.contact_request_id = ecrb.contact_request_id\n LEFT JOIN contact_request_context_digest crcd2 ON crcd.original_contact_request_id = crcd2.contact_request_id -- inherit context from parent convo when relevant\n CROSS JOIN classifier_classes cls\n -- we want to prevent assignments of tickets not yet classified, but don't want to wait more than 30 seconds for sync and 2 minutes for the rest\n WHERE jsonb_typeof(COALESCE(crcd.raw_prediction, crcd2.raw_prediction)) = 'array'\n OR CASE\n WHEN ecrb.priority_level = 'SLA[5min]' THEN COALESCE(ecrb.started_at < now() at time zone 'utc' - '30 seconds'::interval, TRUE)\n ELSE COALESCE(ecrb.started_at < now() at time zone 'utc' - '2 minutes'::interval, TRUE)\n END\n ),\n -- # 2. compute expertise matching scores for all (eligible convo X UCE) pairs\n convos_x_probabilities AS (\n -- expand the eligible convos on all predicted classes\n SELECT\n s.*\n , s._predicted_class AS predicted_class\n FROM (\n SELECT\n contact_request_id\n , intercom_conversation_id\n , priority_level\n , sla_score\n , sla_breached\n , conversation_workforce_level\n , workforce_level_matching_score\n , uce_member_matching_score\n , jsonb_array_elements(predicted_probabilities)::numeric AS predicted_probability\n , trim(jsonb_array_elements(predicted_classes)::text, '\"') AS _predicted_class\n , last_inbox_assignment_by\n , inbox_id\n , inbox_name\n , time_reference_for_priority_score\n , reserved_for_external_agent_platform\n , reserved_for_review\n , bypassed_classifier\n FROM eligible_contact_request_backlog_with_context\n ) s\n UNION ALL\n -- expand for classes unknown by the classifier as well\n SELECT\n contact_request_id\n , intercom_conversation_id\n , priority_level\n , sla_score\n , sla_breached\n , conversation_workforce_level\n , workforce_level_matching_score\n , uce_member_matching_score\n , 0 AS predicted_probability\n , t.inbox_unknown_by_classifier AS _predicted_class\n , last_inbox_assignment_by\n , inbox_id\n , inbox_name\n , time_reference_for_priority_score\n , reserved_for_external_agent_platform\n , reserved_for_review\n , bypassed_classifier\n , t.inbox_unknown_by_classifier AS predicted_class\n FROM eligible_contact_request_backlog_with_context\n CROSS JOIN (\n SELECT intercom_inbox_name AS inbox_unknown_by_classifier FROM support.support_specialization\n EXCEPT\n SELECT JSONB_ARRAY_ELEMENTS_TEXT(classes) FROM classifier_classes\n ) t\n ),\n -- Calibration pass 1: Compute min/max across ALL agents for calibration (lightweight aggregation)\n calibration_min_max_per_agent AS (\n SELECT\n sa.support_agent_id\n , cxp.contact_request_id\n , sum((CASE\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n ELSE cxp.predicted_probability::numeric\n END)\n * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n FROM convos_x_probabilities cxp\n JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n CROSS JOIN support_agents_for_computation sa\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n CROSS JOIN classifier_classes_as_array cls\n WHERE\n CASE\n WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n ELSE TRUE\n END\n AND CASE\n WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n ELSE TRUE\n END\n GROUP BY sa.support_agent_id, cxp.contact_request_id\n ),\n min_max_expertise_matching_score_per_convo AS (\n SELECT\n contact_request_id\n , min(power(raw_expertise_matching_score, (:power_weight_a))) AS min_ems\n , max(power(raw_expertise_matching_score, (:power_weight_a))) AS max_ems\n FROM calibration_min_max_per_agent\n GROUP BY contact_request_id\n ),\n -- calibration pass 2: Compute detailed scores ONLY for the requesting agent which is less costly\n eligible_convos_with_expertise_matching_score_per_uce AS (\n SELECT\n sa.intercom_admin_id\n , sa.support_agent_id\n , cxp.intercom_conversation_id\n , cxp.contact_request_id\n , cxp.inbox_id\n , cxp.inbox_name\n , cxp.priority_level\n , cxp.workforce_level_matching_score\n , cxp.sla_score\n , cxp.sla_breached\n , cxp.uce_member_matching_score\n , cxp.time_reference_for_priority_score\n , cxp.reserved_for_external_agent_platform\n , cxp.reserved_for_review\n , sum((CASE\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id = ss.intercom_inbox_id THEN 1.0\n WHEN (cxp.last_inbox_assignment_by IN ('manual', 'intercom') OR cxp.bypassed_classifier OR NOT cxp.inbox_name = ANY(cls.classes_array))\n AND cxp.inbox_id != ss.intercom_inbox_id THEN 0.0\n ELSE cxp.predicted_probability::numeric\n END)\n * COALESCE(m.score, 0)) AS raw_expertise_matching_score\n -- L1 & L2 agents have an extra boost on priority_level for sync conversations\n , case when cxp.priority_level = 'SLA[5min]' and sa.level < 3 then 2 else 0 end as additional_power_weight_c\n FROM convos_x_probabilities cxp\n JOIN support.support_specialization ss ON cxp.predicted_class = ss.intercom_inbox_name\n CROSS JOIN support_agents_for_computation sa\n LEFT JOIN uce_specialisations_mapping m ON m.intercom_inbox_id = ss.intercom_inbox_id AND m.intercom_admin_id = sa.intercom_admin_id\n CROSS JOIN classifier_classes_as_array cls\n -- additional exclusion conditions for both the care expert asking for the assignment and the calibration cohort\n WHERE\n sa.support_agent_id = (:support_agent_id)::uuid -- ONLY requesting agent\n AND CASE\n WHEN NOT cxp.reserved_for_review THEN sa.level - cxp.conversation_workforce_level >= 0\n ELSE TRUE\n END\n -- handle conversations reserved for specific external agent platforms\n AND CASE\n WHEN cxp.reserved_for_external_agent_platform IS NOT NULL\n THEN UPPER(sa.platform) = UPPER(cxp.reserved_for_external_agent_platform)\n ELSE TRUE\n END\n GROUP BY sa.intercom_admin_id, sa.support_agent_id, cxp.contact_request_id, cxp.inbox_id, cxp.inbox_name, cxp.priority_level, cxp.workforce_level_matching_score, cxp.sla_score, cxp.sla_breached, cxp.uce_member_matching_score,\n cxp.time_reference_for_priority_score, cxp.reserved_for_external_agent_platform, cxp.reserved_for_review, cxp.intercom_conversation_id, sa.level\n ),\n eligible_convos_with_scores AS (\n SELECT\n ec.contact_request_id\n , ec.intercom_conversation_id\n , ec.inbox_id\n , ec.inbox_name\n , ec.time_reference_for_priority_score\n -- normalization of the expertise matching score (distributed over [20%, 100%])\n , CASE WHEN max_ems > min_ems\n THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n ELSE 1\n END AS expertise_matching_score\n , POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c) AS priority_score\n , CASE\n WHEN NOT ec.reserved_for_review THEN POWER(workforce_level_matching_score, (:power_weight_d)) ELSE 1\n END AS workforce_level_matching_score\n , uce_member_matching_score\n , COALESCE(\n CASE\n WHEN max_ems > min_ems\n THEN 0.2 + 0.8 * (power(raw_expertise_matching_score, (:power_weight_a)) - min_ems) / (max_ems - min_ems)\n ELSE 1\n END\n * POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)\n * POWER(workforce_level_matching_score, (:power_weight_d))\n * uce_member_matching_score\n , 0) AS assignment_score\n , raw_expertise_matching_score\n , sla_score\n , sla_breached\n , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER () AS max_priority_score\n , priority_level\n , MAX(POWER(sla_score, (:power_weight_c) + ec.additional_power_weight_c)) OVER (PARTITION BY priority_level) AS max_priority_score_per_priority_level\n , m.max_ems\n , ec.reserved_for_external_agent_platform\n , ec.reserved_for_review\n , null as n_rejections\n , ec.priority_level = 'SLA[5min]' as is_sync_conversation\n , ec.additional_power_weight_c\n FROM eligible_convos_with_expertise_matching_score_per_uce ec\n JOIN min_max_expertise_matching_score_per_convo m USING(contact_request_id)\n LEFT JOIN support.contact_request_assignment_event crae ON crae.contact_request_id = ec.contact_request_id AND crae.action = 'rejected' AND crae.intercom_admin_id = ec.intercom_admin_id\n WHERE\n crae.contact_request_id IS NULL\n AND ec.raw_expertise_matching_score > 0 -- exclude conversations with negative expertise matching score for current UCE\n ),\n backlog_stats AS MATERIALIZED (\n SELECT\n count(*) as n_assignable_convos\n , count(*) FILTER (WHERE priority_level = 'SLA[12h]') as n_assignable_async\n , count(*) FILTER (WHERE priority_level = 'SLA[5min]') as n_assignable_sync\n , count(*) FILTER (WHERE priority_level = 'SLA[30min]') as n_assignable_callback\n , count(*) FILTER (WHERE priority_level = 'SLA[12h]' AND sla_breached) as n_assignable_async_sla_breached\n , count(*) FILTER (WHERE priority_level = 'SLA[5min]' AND sla_breached) as n_assignable_sync_sla_breached\n , count(*) FILTER (WHERE priority_level = 'SLA[30min]' AND sla_breached) as n_assignable_callback_sla_breached\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_12h\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_5m\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN power(sla_score, (:power_weight_c) + additional_power_weight_c) END) AS q90_priority_score_sla_30m\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[12h]' THEN expertise_matching_score END) AS q90_expertise_score_sla_12h\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[5min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_5m\n , percentile_cont(0.9) WITHIN GROUP (ORDER BY CASE WHEN priority_level = 'SLA[30min]' THEN expertise_matching_score END) AS q90_expertise_score_sla_30m\n FROM eligible_convos_with_scores\n )\n SELECT\n row_number() OVER (ORDER BY assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS assignment_rank\n , (:support_agent_id)\n , *\n , row_number() OVER (ORDER BY intercom_conversation_id = (:conversation_id) DESC, assignment_score DESC NULLS LAST, time_reference_for_priority_score) AS row_nb\n FROM eligible_convos_with_scores\n CROSS JOIN backlog_stats stats\n ORDER BY row_nb, assignment_score DESC NULLS LAST, time_reference_for_priority_score\n LIMIT 10;\n"
components.support.public.dependencies ¶
SupportDependency ¶
Bases: ABC
This allows country-specific apps to define a class to inject dependencies to the support component. This is mainly used as a workaround until everything from resolution_platform has been migrated to the support component, and we can safely rely on a global user & alan employee.
cancel_legacy_callback_timeslot
abstractmethod
¶
Cancels a legacy callback timeslot in case we cancel the global one
create_legacy_conversation_context
abstractmethod
¶
create_legacy_conversation_context(
user_id,
context,
callback_phone_number,
recording_consent,
commit=True,
)
For a given user id and context, creates a legacy conversation context in the current country. Should do exactly what the legacy does (no more no less)
Source code in components/support/public/dependencies.py
create_legacy_phone_support_csat
abstractmethod
¶
Creates the phone support CSAT for the legacy phone support.
Source code in components/support/public/dependencies.py
delete_legacy_callback_timeslots_suggestion
abstractmethod
¶
Delete a legacy callback timeslots suggestion to avoid reimporting it after we delete the existing global one
Source code in components/support/public/dependencies.py
get_accounts_and_population_ids_for_role
abstractmethod
¶
This is used in the Support Status determination, more specific to know the member's eligibility to a given Support Channel.
Returns the list of account and potential population matches for a given user Returned array can be empty.
Source code in components/support/public/dependencies.py
get_active_employees
abstractmethod
¶
Returns a list of all employees in the current app. This is used to import support agents from employees.
get_alan_employee
abstractmethod
¶
Returns the BaseAlanEmployee entry of the current app for the given alan_email
get_alan_employee_ids_by_alan_email
abstractmethod
¶
Returns the alan employee ids for the given alan_emails, by alan_email
get_assigner_sql_query
abstractmethod
¶
Returns the SQL Query used to get a list of conversations to assign. This query should return data that can be mapped to AssignerSQLResult fields. Expected parameters: - support_agent_id: str - workspace_id: str - power_weight_a: float (from AssignerConfigurationData) - power_weight_b: float (from AssignerConfigurationData) - power_weight_c: float (from AssignerConfigurationData) - power_weight_d: float (from AssignerConfigurationData) - sync_sla_score_boost: float (from AssignerConfigurationData) - member_matching_score_boost: float (from AssignerConfigurationData) - workforce_level_score_boost: float (from AssignerConfigurationData)
Source code in components/support/public/dependencies.py
get_callback_request_confirmation_email_template_name
abstractmethod
¶
Gives the name of the template, including language, needed to send the confirmation email when a user requests a callback.
Source code in components/support/public/dependencies.py
get_channel_management_population_configuration
abstractmethod
¶
Returns the population configuration for the current app name. It's used to configure the cutover periods in the channel management.
Source code in components/support/public/dependencies.py
get_contact_request_source_type_from_context
abstractmethod
¶
For a given care conversation context id (in the current country), returns the relevant contact request source type (is it a callback request? an async conversation request? etc.)
Source code in components/support/public/dependencies.py
get_hotline_phone_number
abstractmethod
¶
get_identification_digits_from_profile_id
abstractmethod
¶
Returns the identification digits the user will need to provide when calling the hotline.
Source code in components/support/public/dependencies.py
get_last_inbox_assignment_by_from_legacy
abstractmethod
¶
Returns the last inbox assignment by for a legacy conversation
get_legacy_callback_timeslots_for_conversation
abstractmethod
¶
Returns legacy callback timeslots if they exist for that conversation and alan employee id
Source code in components/support/public/dependencies.py
get_legacy_callback_timeslots_suggestion_for_conversation
abstractmethod
¶
Returns legacy callback timeslots suggestion if they exist for that conversation and alan employee id
Source code in components/support/public/dependencies.py
get_legacy_conversation_context_id_for_intercom_conversation
abstractmethod
¶
get_legacy_conversation_context_id_for_intercom_conversation(
intercom_conversation_id, intercom_workspace_id
)
For a given intercom conversation id & workspace id, returns the relevant country-spefici care conversation context id & the relevant app name. Should throw if not found.
Source code in components/support/public/dependencies.py
get_legacy_conversation_context_phone_calls
abstractmethod
¶
get_legacy_conversation_context_phone_calls(
intercom_workspace_id, legacy_conversation_context_id
)
For a given care conversation context id, returns the regular callback phone number & consent
Source code in components/support/public/dependencies.py
get_legacy_conversation_context_regular_callback_details
abstractmethod
¶
For a given care conversation context id, returns the callback phone number & consent Returns a tuple: needs_callback_request, phone_number, recording_consent, updated_at
Source code in components/support/public/dependencies.py
get_marmot_conversation_phone_call_history_url
abstractmethod
¶
Returns the relevant country-specific Marmot URL to show the list of callbacks calls. (The component is global but the route is country-specific).
Source code in components/support/public/dependencies.py
get_non_offline_user_info_for_phone_support
abstractmethod
¶
Tells if a user is an offline experience company. Returns a tuple: is_eligible, is_alan_member, is_alaner, is_offline_member, is_offline_experience_company, redirect_to_queue
Source code in components/support/public/dependencies.py
get_platform_role_tags
abstractmethod
¶
get_primary_profiles_from_profile_ids
abstractmethod
¶
For a given list of profile ids, returns the list of primary profile ids
get_profile_id_from_identification_digits
abstractmethod
¶
For a given identification digits, returns the profile id. Used for the phone support identification.
Source code in components/support/public/dependencies.py
get_ranked_sql_query
abstractmethod
¶
get_recommended_intercom_admin_id_for_legacy_context
abstractmethod
¶
Returns the recommended intercom admin id for a legacy context.
get_support_workspace_id
abstractmethod
¶
Returns the Intercom workspace ID for support in the current app. This is used to create workspace affectations for support agents.
get_user_first_name
abstractmethod
¶
For a given user id, returns the first name of a user of the current app.
get_user_fullname
abstractmethod
¶
get_user_id_for_legacy_conversation_context
abstractmethod
¶
For a given care conversation context id (in the current country), returns the relevant user id.
Source code in components/support/public/dependencies.py
get_user_id_from_profile_id
abstractmethod
¶
get_user_lang
abstractmethod
¶
get_user_perso_email
abstractmethod
¶
get_user_pro_email
abstractmethod
¶
handle_legacy_new_conversation_event
abstractmethod
¶
Handles whatever we did in the legacy endpoint for new conversations in the global side (that helps ensure things are done synchronously).
Source code in components/support/public/dependencies.py
handle_legacy_new_conversation_event_fallback
abstractmethod
¶
Handles whatever we needed to do in the legacy for the new conversation fallback command, allowing to do it sequentially from the global new conversation fallback command.
Source code in components/support/public/dependencies.py
is_eligible_to_callback
abstractmethod
¶
Tells if a user is eligible to the callback. Will be used to show the callback option in the frontend, and in the endpoint that creates a callback request (usually async message endpoint)
Source code in components/support/public/dependencies.py
is_intercom_receipt_sent_for_context
abstractmethod
¶
For a given care conversation context id (in the current country), returns the fact that an initial async email answer was already sent to the user.
Source code in components/support/public/dependencies.py
is_legacy_callback_timeslot_reminder_sent
abstractmethod
¶
Checks if a legacy callback reminder has already been sent before we send the global one to avoid duplicates
Source code in components/support/public/dependencies.py
is_user_an_offline_experience_member
abstractmethod
¶
mark_intercom_receipt_sent_for_context
abstractmethod
¶
For a given care conversation context id (in the current country), marks the fact that an initial async email answer was sent to the user.
Source code in components/support/public/dependencies.py
mark_legacy_callback_timeslot_reminder_sent
abstractmethod
¶
Mark a legacy callback reminder has already been sent when we send the global one to avoid duplicates
Source code in components/support/public/dependencies.py
remove_legacy_phone_number_storage_for_conversation_context
abstractmethod
¶
Allows removing the phone number from a care conversation context, allowing to clean them even if/when we remove the existing country-specific scripts (only relevant for France).
Source code in components/support/public/dependencies.py
sync_legacy_assigner_data
abstractmethod
¶
Retrocompatibility logic to update legacy database with assignment changes
update_legacy_phone_support_csat
abstractmethod
¶
Updates the phone support CSAT for the legacy phone support.
Source code in components/support/public/dependencies.py
get_app_dependency ¶
set_app_dependency ¶
components.support.public.entities ¶
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 ¶
HarrySpeClassificationResult
dataclass
¶
JTBDClassificationResult
dataclass
¶
JTBDClassificationResult(
chain_of_thought_1,
top_3_jtbd_predictions,
chain_of_thought_2,
jtbd_prediction,
predicted_inbox=None,
raw_prediction=None,
raw_prediction_classes=None,
)
SpeClassificationResult
dataclass
¶
SpeClassificationResult(
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,
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
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.
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
¶
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
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
¶
components.support.public.enums ¶
contact_request_source_type ¶
ContactRequestSourceType ¶
Bases: AlanBaseEnum
A ContactRequest instance's "source_type" represents how the user contacted us. It can be an Intercom sync conversation, an async email, a callback request, etc.
async_conversation_request
class-attribute
instance-attribute
¶
hotline_voice_message_request
class-attribute
instance-attribute
¶
immediate_callback_request
class-attribute
instance-attribute
¶
lead_callback_request
class-attribute
instance-attribute
¶
sync_conversation_request
class-attribute
instance-attribute
¶
contact_role_type ¶
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 ¶
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_english_rejection_reason ¶
Returns a boolean based on whether the rejection reason is due to the ticket being in English
Source code in components/support/subcomponents/assigner/protected/helpers/assignment_rejection_reasons.py
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_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
components.support.public.queries ¶
documents ¶
get_document ¶
Get the document name and document content by its ID.
Source code in components/support/public/queries/documents.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 any topic, might be used to call other handlers.
Source code in components/support/public/services/base_intercom_handler_service.py
phone_support_service ¶
PhoneSupportService ¶
Allows interacting with phone support features
check_if_member_has_pending_quick_callback_request
staticmethod
¶
Check if a member has a pending quick callback request
Source code in components/support/public/services/phone_support_service.py
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_phone_support_csat
staticmethod
¶
Create a phone support CSAT
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
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
update_phone_support_csat
staticmethod
¶
Update a phone support CSAT
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_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_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_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
50 51 52 53 54 55 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 | |
handle_topic
staticmethod
¶
Handles a conversation webhook topic coming from Intercom
Source code in components/support/public/services/support_intercom_handler_service.py
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 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 | |
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_id_or_create_from_legacy
staticmethod
¶
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
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_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
sync_support_agent_from_legacy_data
staticmethod
¶
This is to be used during the transition from the French assigner to the global assigner. It updates global support agents from legacy data.
Source code in components/support/public/services/support_service.py
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.