Customizing Genesys Cloud Routing Algorithms with Python SDK
What You Will Build
- A Python service that retrieves real-time agent availability and skill matrices from Genesys Cloud, calculates weighted routing scores based on proficiency, language match, and historical satisfaction, and exposes a FastAPI endpoint for QA testing.
- This implementation uses the
purecloudplatformclientv2Python SDK alongside FastAPI to orchestrate data retrieval, scoring logic, and simulation endpoints. - The tutorial covers Python 3.9+ with type hints, production-grade error handling, and configurable algorithm parameters.
Prerequisites
- OAuth 2.0 Client Credentials flow configured in Genesys Cloud
- Required scopes:
routing:agent:view,routing:skill:view,routing:queue:view,analytics:conversations:view - SDK:
genesys-cloud-purecloud-v2(purecloudplatformclientv2) version 150.0.0+ - Runtime: Python 3.9+
- External dependencies:
fastapi,uvicorn,pydantic,pandas,scipy - Install dependencies:
pip install genesys-cloud-purecloud-v2 fastapi uvicorn pydantic pandas scipy
Authentication Setup
Genesys Cloud uses OAuth 2.0 for all API access. The Python SDK includes built-in token caching and automatic refresh logic when initialized with client credentials.
from purecloudplatformclientv2 import PlatformClient
import os
def initialize_genesys_client() -> PlatformClient:
client = PlatformClient()
client.login_client_credentials(
client_id=os.getenv("GENESYS_CLIENT_ID"),
client_secret=os.getenv("GENESYS_CLIENT_SECRET"),
base_url=os.getenv("GENESYS_BASE_URL", "https://api.mypurecloud.com"),
scopes=[
"routing:agent:view",
"routing:skill:view",
"routing:queue:view",
"analytics:conversations:view"
]
)
# SDK automatically caches tokens and handles 401 refresh cycles
return client
The PlatformClient maintains a token cache in memory. When a 401 Unauthorized response is returned, the SDK silently requests a new token using the stored refresh token or client credentials and retries the original request. No manual token management is required.
Implementation
Step 1: Query Real-Time Agent Availability and Skill Matrices
The Routing API provides agent presence, skill levels, and language assignments. We use GET /api/v2/routing/users via the SDK to retrieve agents filtered by a specific queue or skill.
OAuth Scope: routing:agent:view, routing:skill:view
from purecloudplatformclientv2 import RoutingApi, GetRoutingUsersRequest
from typing import List, Dict, Any
def fetch_agents_by_queue(client: PlatformClient, queue_id: str) -> List[Dict[str, Any]]:
routing_api = RoutingApi(client)
# Filter for agents with at least "Available" status
query_body = GetRoutingUsersRequest(
filter=f"status eq 'Available'",
expand=["skills", "languages"],
page_size=100
)
agents = []
try:
response = routing_api.get_routing_users(body=query_body)
for agent in response.entities:
agent_data = {
"id": agent.id,
"name": agent.name,
"status": agent.status,
"skills": {s.skill.id: s.level for s in (agent.skills or [])},
"languages": [l.language.id for l in (agent.languages or [])]
}
agents.append(agent_data)
except Exception as e:
raise RuntimeError(f"Failed to fetch agents: {e}")
return agents
Expected response structure from /api/v2/routing/users:
{
"entities": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Agent Smith",
"status": "Available",
"skills": [{"skill": {"id": "skill_123"}, "level": 5}],
"languages": [{"language": {"id": "en-US"}}]
}
],
"page_size": 100,
"total": 1
}
Step 2: Define Custom Scoring Functions with Configurable Parameters
The scoring engine evaluates three dimensions: skill proficiency, language match, and historical customer satisfaction (CSAT). Weights are configurable at runtime.
from dataclasses import dataclass
from typing import Optional
@dataclass
class RoutingWeights:
proficiency: float = 0.4
language_match: float = 0.3
historical_csat: float = 0.3
min_score_threshold: float = 0.5
def calculate_agent_score(
agent: Dict[str, Any],
required_skill: str,
required_language: str,
historical_csat_map: Dict[str, float],
weights: RoutingWeights
) -> float:
# Proficiency: normalize skill level to 0-1 range (assuming max level 5)
skill_level = agent["skills"].get(required_skill, 0)
proficiency_score = min(skill_level / 5.0, 1.0)
# Language match: binary 1.0 or 0.0
language_score = 1.0 if required_language in agent["languages"] else 0.0
# Historical CSAT: fallback to 0.5 if no data
csat_score = historical_csat_map.get(agent["id"], 0.5)
# Weighted sum
total_score = (
weights.proficiency * proficiency_score +
weights.language_match * language_score +
weights.historical_csat * csat_score
)
return total_score if total_score >= weights.min_score_threshold else 0.0
Step 3: Handle Tie-Breaking with Least-Queue-Time Heuristics
When multiple agents share the same composite score, the algorithm breaks ties by selecting the agent with the lowest current queue wait time or longest idle duration. Genesys Cloud exposes wrapup_code and status_updated_time in routing payloads, but for simulation we approximate idle time via a mock queue-time map. In production, you would fetch GET /api/v2/routing/queues/{queueId}/members to retrieve actual position metrics.
from typing import List, Tuple
def apply_tie_breaking(
scored_agents: List[Tuple[Dict[str, Any], float]],
queue_time_map: Dict[str, int]
) -> List[Tuple[Dict[str, Any], float]]:
# Sort by score descending, then by queue time ascending
scored_agents.sort(
key=lambda x: (-x[1], queue_time_map.get(x[0]["id"], 9999))
)
return scored_agents
Step 4: Apply Dynamic Weight Adjustments Based on Campaign SLAs
Service level agreements dictate priority shifts. If a campaign targets 80/20 SLA (80% answered in 20 seconds), the algorithm increases the language match weight to reduce transfer rates. We implement a policy engine that mutates weights before scoring.
def apply_sla_weight_adjustments(
base_weights: RoutingWeights,
sla_target: str,
current_performance: float
) -> RoutingWeights:
adjusted = RoutingWeights(
proficiency=base_weights.proficiency,
language_match=base_weights.language_match,
historical_csat=base_weights.historical_csat
)
if sla_target == "80/20" and current_performance < 0.75:
# Prioritize immediate language match to reduce transfers
adjusted.language_match += 0.15
adjusted.proficiency -= 0.075
adjusted.historical_csat -= 0.075
elif sla_target == "95/30" and current_performance >= 0.90:
# Relax language priority, emphasize historical quality
adjusted.historical_csat += 0.1
adjusted.language_match -= 0.05
adjusted.proficiency -= 0.05
# Normalize weights to sum to 1.0
total = adjusted.proficiency + adjusted.language_match + adjusted.historical_csat
adjusted.proficiency /= total
adjusted.language_match /= total
adjusted.historical_csat /= total
return adjusted
Step 5: Simulate Routing Outcomes Against Historical Interaction Datasets
We retrieve historical conversation data via POST /api/v2/analytics/conversations/details/query. The response includes customer_satisfaction_survey objects. We aggregate CSAT per agent and run a batch simulation.
OAuth Scope: analytics:conversations:view
from purecloudplatformclientv2 import AnalyticsApi, QueryConversationRequest, DateFilter
import pandas as pd
def fetch_historical_csat(client: PlatformClient, start_date: str, end_date: str) -> Dict[str, float]:
analytics_api = AnalyticsApi(client)
body = QueryConversationRequest(
date_filter=DateFilter(start_date=start_date, end_date=end_date),
group_by=["agent"],
select=["customer_satisfaction_survey"],
count=5000
)
try:
response = analytics_api.post_analytics_conversations_details_query(body=body)
csat_map = {}
for entity in response.entities:
agent_id = entity.agent.id if entity.agent else None
if not agent_id:
continue
csat_values = [
s.score for s in (entity.customer_satisfaction_survey or [])
if s.score is not None
]
if csat_values:
csat_map[agent_id] = sum(csat_values) / len(csat_values)
return csat_map
except Exception as e:
raise RuntimeError(f"Analytics query failed: {e}")
def simulate_routing_batch(
agents: List[Dict[str, Any]],
interaction_log: pd.DataFrame,
weights: RoutingWeights,
queue_time_map: Dict[str, int]
) -> pd.DataFrame:
results = []
for _, row in interaction_log.iterrows():
required_skill = row["required_skill"]
required_language = row["required_language"]
scored = [
(agent, calculate_agent_score(agent, required_skill, required_language, row.get("csat_map", {}), weights))
for agent in agents
]
scored = [s for s in scored if s[1] > 0]
if not scored:
continue
ranked = apply_tie_breaking(scored, queue_time_map)
selected_agent = ranked[0][0]
results.append({
"interaction_id": row["id"],
"required_skill": required_skill,
"required_language": required_language,
"selected_agent_id": selected_agent["id"],
"selected_agent_name": selected_agent["name"],
"composite_score": ranked[0][1]
})
return pd.DataFrame(results)
Step 6: Generate Routing Fairness Reports to Detect Bias
Fairness evaluation compares agent selection frequency against expected distribution. We calculate the Gini coefficient and chi-square statistic to flag disproportionate routing.
from scipy.stats import chi2
import numpy as np
def generate_fairness_report(simulation_df: pd.DataFrame) -> Dict[str, Any]:
agent_counts = simulation_df["selected_agent_id"].value_counts()
total_interactions = len(simulation_df)
expected_count = total_interactions / len(agent_counts) if len(agent_counts) > 0 else 0
# Chi-square test for uniform distribution
observed = agent_counts.values
expected = np.full(len(observed), expected_count)
chi2_stat, p_value = chi2(observed, f_exp=expected)
# Gini coefficient for inequality measurement
sorted_counts = np.sort(observed)
n = len(sorted_counts)
index = np.arange(1, n + 1)
gini = (2 * np.sum(index * sorted_counts) - (n + 1) * np.sum(sorted_counts)) / (n * np.sum(sorted_counts))
return {
"total_interactions": total_interactions,
"agent_distribution": agent_counts.to_dict(),
"chi2_statistic": float(chi2_stat),
"p_value": float(p_value),
"gini_coefficient": float(gini),
"is_fair": p_value > 0.05 and gini < 0.3
}
Step 7: Expose a Routing Simulation Endpoint for QA Testing
We wrap the logic in a FastAPI application. The endpoint accepts a JSON payload with campaign parameters, executes the scoring pipeline, and returns ranked agents alongside fairness metrics.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
import uvicorn
app = FastAPI(title="Genesys Cloud Routing Simulator")
class SimulationRequest(BaseModel):
queue_id: str
required_skill: str
required_language: str
sla_target: str = "80/20"
current_performance: float = 0.80
start_date: str
end_date: str
@app.post("/simulate/routing")
async def run_routing_simulation(req: SimulationRequest):
client = initialize_genesys_client()
# Step 1: Fetch agents
agents = fetch_agents_by_queue(client, req.queue_id)
if not agents:
raise HTTPException(status_code=404, detail="No available agents found")
# Step 2: Fetch historical CSAT
csat_map = fetch_historical_csat(client, req.start_date, req.end_date)
# Step 3: Adjust weights based on SLA
base_weights = RoutingWeights()
adjusted_weights = apply_sla_weight_adjustments(base_weights, req.sla_target, req.current_performance)
# Step 4: Score and rank
scored = [
(agent, calculate_agent_score(agent, req.required_skill, req.required_language, csat_map, adjusted_weights))
for agent in agents
]
scored = [(a, s) for a, s in scored if s > 0]
if not scored:
raise HTTPException(status_code=422, detail="No agents met the minimum score threshold")
# Step 5: Tie-breaking
queue_time_map = {a["id"]: 0 for a in agents} # Placeholder for real queue time data
ranked = apply_tie_breaking(scored, queue_time_map)
# Step 6: Fairness check (requires batch data, simplified here)
fairness = generate_fairness_report(pd.DataFrame([{"selected_agent_id": ranked[0][0]["id"]}]))
return {
"top_agent": {
"id": ranked[0][0]["id"],
"name": ranked[0][0]["name"],
"score": ranked[0][1]
},
"ranked_agents": [
{"id": a["id"], "name": a["name"], "score": s} for a, s in ranked[:5]
],
"weights_used": adjusted_weights.__dict__,
"fairness_metrics": fairness
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Complete Working Example
The following script combines authentication, data retrieval, scoring, tie-breaking, SLA adjustment, simulation, and the FastAPI endpoint into a single production-ready module. Save as routing_simulator.py.
import os
import pandas as pd
import numpy as np
from typing import List, Dict, Any, Tuple
from dataclasses import dataclass
from scipy.stats import chi2
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from purecloudplatformclientv2 import PlatformClient, RoutingApi, AnalyticsApi, GetRoutingUsersRequest, QueryConversationRequest, DateFilter
import uvicorn
@dataclass
class RoutingWeights:
proficiency: float = 0.4
language_match: float = 0.3
historical_csat: float = 0.3
min_score_threshold: float = 0.5
def initialize_genesys_client() -> PlatformClient:
client = PlatformClient()
client.login_client_credentials(
client_id=os.getenv("GENESYS_CLIENT_ID"),
client_secret=os.getenv("GENESYS_CLIENT_SECRET"),
base_url=os.getenv("GENESYS_BASE_URL", "https://api.mypurecloud.com"),
scopes=["routing:agent:view", "routing:skill:view", "routing:queue:view", "analytics:conversations:view"]
)
return client
def fetch_agents_by_queue(client: PlatformClient, queue_id: str) -> List[Dict[str, Any]]:
routing_api = RoutingApi(client)
query_body = GetRoutingUsersRequest(filter="status eq 'Available'", expand=["skills", "languages"], page_size=100)
agents = []
try:
response = routing_api.get_routing_users(body=query_body)
for agent in response.entities:
agents.append({
"id": agent.id, "name": agent.name, "status": agent.status,
"skills": {s.skill.id: s.level for s in (agent.skills or [])},
"languages": [l.language.id for l in (agent.languages or [])]
})
except Exception as e:
raise RuntimeError(f"Failed to fetch agents: {e}")
return agents
def fetch_historical_csat(client: PlatformClient, start_date: str, end_date: str) -> Dict[str, float]:
analytics_api = AnalyticsApi(client)
body = QueryConversationRequest(
date_filter=DateFilter(start_date=start_date, end_date=end_date),
group_by=["agent"], select=["customer_satisfaction_survey"], count=5000
)
try:
response = analytics_api.post_analytics_conversations_details_query(body=body)
csat_map = {}
for entity in response.entities:
agent_id = entity.agent.id if entity.agent else None
if not agent_id: continue
csat_values = [s.score for s in (entity.customer_satisfaction_survey or []) if s.score is not None]
if csat_values: csat_map[agent_id] = sum(csat_values) / len(csat_values)
return csat_map
except Exception as e:
raise RuntimeError(f"Analytics query failed: {e}")
def calculate_agent_score(agent: Dict[str, Any], required_skill: str, required_language: str, historical_csat_map: Dict[str, float], weights: RoutingWeights) -> float:
proficiency_score = min(agent["skills"].get(required_skill, 0) / 5.0, 1.0)
language_score = 1.0 if required_language in agent["languages"] else 0.0
csat_score = historical_csat_map.get(agent["id"], 0.5)
total = weights.proficiency * proficiency_score + weights.language_match * language_score + weights.historical_csat * csat_score
return total if total >= weights.min_score_threshold else 0.0
def apply_sla_weight_adjustments(base_weights: RoutingWeights, sla_target: str, current_performance: float) -> RoutingWeights:
adjusted = RoutingWeights(proficiency=base_weights.proficiency, language_match=base_weights.language_match, historical_csat=base_weights.historical_csat)
if sla_target == "80/20" and current_performance < 0.75:
adjusted.language_match += 0.15; adjusted.proficiency -= 0.075; adjusted.historical_csat -= 0.075
elif sla_target == "95/30" and current_performance >= 0.90:
adjusted.historical_csat += 0.1; adjusted.language_match -= 0.05; adjusted.proficiency -= 0.05
total = adjusted.proficiency + adjusted.language_match + adjusted.historical_csat
adjusted.proficiency /= total; adjusted.language_match /= total; adjusted.historical_csat /= total
return adjusted
def apply_tie_breaking(scored_agents: List[Tuple[Dict[str, Any], float]], queue_time_map: Dict[str, int]) -> List[Tuple[Dict[str, Any], float]]:
scored_agents.sort(key=lambda x: (-x[1], queue_time_map.get(x[0]["id"], 9999)))
return scored_agents
def generate_fairness_report(simulation_df: pd.DataFrame) -> Dict[str, Any]:
agent_counts = simulation_df["selected_agent_id"].value_counts()
total = len(simulation_df)
expected = total / len(agent_counts) if len(agent_counts) > 0 else 0
observed = agent_counts.values
chi2_stat, p_value = chi2(observed, f_exp=np.full(len(observed), expected))
sorted_counts = np.sort(observed)
n = len(sorted_counts)
index = np.arange(1, n + 1)
gini = (2 * np.sum(index * sorted_counts) - (n + 1) * np.sum(sorted_counts)) / (n * np.sum(sorted_counts))
return {"total_interactions": total, "agent_distribution": agent_counts.to_dict(), "chi2_statistic": float(chi2_stat), "p_value": float(p_value), "gini_coefficient": float(gini), "is_fair": p_value > 0.05 and gini < 0.3}
app = FastAPI(title="Genesys Cloud Routing Simulator")
class SimulationRequest(BaseModel):
queue_id: str
required_skill: str
required_language: str
sla_target: str = "80/20"
current_performance: float = 0.80
start_date: str
end_date: str
@app.post("/simulate/routing")
async def run_routing_simulation(req: SimulationRequest):
client = initialize_genesys_client()
agents = fetch_agents_by_queue(client, req.queue_id)
if not agents: raise HTTPException(status_code=404, detail="No available agents found")
csat_map = fetch_historical_csat(client, req.start_date, req.end_date)
adjusted_weights = apply_sla_weight_adjustments(RoutingWeights(), req.sla_target, req.current_performance)
scored = [(agent, calculate_agent_score(agent, req.required_skill, req.required_language, csat_map, adjusted_weights)) for agent in agents]
scored = [(a, s) for a, s in scored if s > 0]
if not scored: raise HTTPException(status_code=422, detail="No agents met the minimum score threshold")
ranked = apply_tie_breaking(scored, {a["id"]: 0 for a in agents})
fairness = generate_fairness_report(pd.DataFrame([{"selected_agent_id": ranked[0][0]["id"]}]))
return {"top_agent": {"id": ranked[0][0]["id"], "name": ranked[0][0]["name"], "score": ranked[0][1]}, "ranked_agents": [{"id": a["id"], "name": a["name"], "score": s} for a, s in ranked[:5]], "weights_used": adjusted_weights.__dict__, "fairness_metrics": fairness}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired OAuth token or invalid client credentials.
- Fix: Verify
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETenvironment variables. The SDK handles automatic refresh, but initial credentials must be valid. Restart the service if the token cache becomes corrupted. - Code fix: The
PlatformClientrefresh logic is automatic. If it fails, log the raw response and rotate credentials in the Genesys Cloud admin console.
Error: 403 Forbidden
- Cause: Missing OAuth scopes or insufficient organizational permissions.
- Fix: Ensure the client credentials include
routing:agent:view,routing:skill:view,routing:queue:view, andanalytics:conversations:view. Verify the user associated with the client credentials has Analyst or Administrator role access to routing and analytics data.
Error: 429 Too Many Requests
- Cause: Exceeding Genesys Cloud rate limits (typically 30-100 requests per second depending on endpoint).
- Fix: Implement exponential backoff with jitter. The SDK does not include built-in retry logic, so wrap API calls in a retry decorator.
import time
import random
def retry_on_429(func, *args, max_retries=3, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if "429" in str(e) or "Too Many Requests" in str(e):
wait_time = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait_time)
else:
raise
raise RuntimeError("Max retries exceeded for 429 rate limit")
Error: 5xx Server Error
- Cause: Genesys Cloud platform outage or temporary backend failure.
- Fix: Implement circuit breaker logic. Return a cached ranking or fallback to round-robin routing until the platform recovers. Log the 5xx response and alert via monitoring tools.
Error: Empty Agent List or Missing Skills
- Cause: Queue configuration mismatch or agents set to
Not Available. - Fix: Verify the
queue_idmatches a live queue. Adjust the filter tostatus in ['Available', 'Talking', 'Wrapup']if you want to include agents who can accept callbacks. Validate skill IDs againstGET /api/v2/routing/skills.