""" Org Chart Utilities for Partner Selection Algorithm This module provides functions for navigating the organizational hierarchy and selecting partners based on organizational relationships. """ import csv from pathlib import Path _ROOT_NODE_ID = 0 def _try_load_org_from_csv(): """If org_chart_directed.csv exists, replace ORG_NODES/ORG_EDGES from it. CSV format: headers include 'source' and 'target', where source is manager/parent and target is report/child. """ global ORG_NODES, ORG_EDGES, _ROOT_NODE_ID root = Path(__file__).resolve().parent.parent csv_path = root / 'org_chart_directed.csv' if not csv_path.exists(): return pairs = [] ordered_names = [] seen = set() try: with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f: reader = csv.DictReader(f) for r in reader: src = (r.get('source') or '').strip() tgt = (r.get('target') or '').strip() if not src or not tgt: continue pairs.append((src, tgt)) if src not in seen: seen.add(src) ordered_names.append(src) if tgt not in seen: seen.add(tgt) ordered_names.append(tgt) except Exception: return if not ordered_names: return id_by_name = {name: i for i, name in enumerate(ordered_names)} edges = [] seen_edges = set() in_deg = {id_by_name[name]: 0 for name in ordered_names} for src, tgt in pairs: if src == tgt: # Ignore self loops (often used just to mark the root). continue s_id = id_by_name[src] t_id = id_by_name[tgt] key = (s_id, t_id) if key in seen_edges: continue seen_edges.add(key) edges.append({'source': s_id, 'target': t_id}) in_deg[t_id] = in_deg.get(t_id, 0) + 1 roots = [node_id for node_id, deg in in_deg.items() if deg == 0] _ROOT_NODE_ID = roots[0] if roots else 0 ORG_NODES = [{'id': id_by_name[name], 'label': name, 'department': ''} for name in ordered_names] ORG_EDGES = edges # Org chart data - nodes and edges from org_chart.html ORG_NODES = [ {"id": 0, "label": "CEO", "department": "Executive"}, {"id": 1, "label": "President", "department": "Executive"}, {"id": 2, "label": "Chief Legal Officer", "department": "Legal"}, {"id": 3, "label": "Senior Director of Finance & Accounting ", "department": "Finance"}, {"id": 4, "label": "Chief of Staff", "department": "Executive"}, {"id": 5, "label": "Project Manager", "department": "Risk & Compliance"}, {"id": 6, "label": "Executive Assistant to the CEO, SF", "department": "Finance"}, {"id": 7, "label": "Senior Manager, Regional Finance & Accounting", "department": "Finance"}, {"id": 8, "label": "Regional Finance & Accounting Manager", "department": "Finance"}, {"id": 9, "label": "Finance & Accounting Manager --Ghana", "department": "Finance"}, {"id": 10, "label": "Senior Accountant", "department": "Finance"}, {"id": 11, "label": "Tax & Accounting Manager", "department": "Finance"}, {"id": 12, "label": "Accounting Associate and Admin", "department": "Finance"}, {"id": 13, "label": "Office Manager, Ghana", "department": "Finance"}, {"id": 14, "label": "Accountant, Rwanda", "department": "Finance"}, {"id": 15, "label": "Office Manager, Uganda", "department": "Finance"}, {"id": 16, "label": "People Operations Manager", "department": "People"}, {"id": 17, "label": "Senior People Operations Specialist", "department": "People"}, {"id": 18, "label": "People Operations Specialist", "department": "People"}, {"id": 19, "label": "Chief Product Officer", "department": "Product"}, {"id": 20, "label": "Head of Product, Nigeria", "department": "Product"}, {"id": 21, "label": "Licensing Assistant", "department": "Legal"}, {"id": 22, "label": "Licensing Project Manager", "department": "Legal"}, {"id": 23, "label": "Senior Director, Product & Country", "department": "Product"}, {"id": 24, "label": "Product Operations Associate II", "department": "Operations"}, {"id": 25, "label": "Senior Software Engineer", "department": "Engineering"}, {"id": 26, "label": "Senior Software Engineer, Data", "department": "Engineering"}, {"id": 27, "label": "Product Operations Associate II", "department": "Operations"}, {"id": 28, "label": "Senior Software Engineer", "department": "Engineering"}, {"id": 29, "label": "Product Designer II", "department": "Product"}, {"id": 30, "label": "Senior Customer Operations Team", "department": "Operations"}, {"id": 31, "label": "Senior Software Engineer", "department": "Engineering"}, {"id": 32, "label": "Head of Risk Compliance - Zambia", "department": "Product"}, {"id": 33, "label": "Agent Ops Associate I", "department": "Product"}, {"id": 34, "label": "Agent Ops Associate I", "department": "Product"}, {"id": 35, "label": "Senior Administrative Associate", "department": "Finance"}, {"id": 36, "label": "Senior Team Lead, Customer & Liquidity", "department": "Operations"}, {"id": 37, "label": "Liquidity Ops Associate I", "department": "Operations"}, {"id": 38, "label": "Liquidity Ops Associate I", "department": "Operations"}, {"id": 39, "label": "Customer Operations Assistant", "department": "Operations"}, {"id": 40, "label": "Customer Operations Associate I", "department": "Operations"}, {"id": 41, "label": "Customer Operations Associate I", "department": "Operations"}, {"id": 42, "label": "General Manager, East Africa", "department": "Expansion"}, {"id": 43, "label": "Lead Brokerage Manager - Uganda", "department": "Expansion"}, {"id": 44, "label": "Senior Director, Policy & Government", "department": "Expansion"}, {"id": 45, "label": "Country Director, Rwanda", "department": "Expansion"}, {"id": 46, "label": "Senior Manager, Data & Analytics", "department": "Engineering"}, {"id": 47, "label": "Lead Economist and Head of Growth", "department": "Revenue"}, {"id": 48, "label": "Growth Lead", "department": "Marketing"}, {"id": 49, "label": "Data & Revenue Strategy Intern", "department": "Engineering"}, {"id": 50, "label": "VP Operations, Technology", "department": "Operations"}, {"id": 51, "label": "Head of Product, Consumer", "department": "Product"}, {"id": 52, "label": "Senior Treasury Associate", "department": "Finance"}, {"id": 53, "label": "Senior Manager, Customer Operations", "department": "Operations"}, {"id": 54, "label": "Product Manager ", "department": "Product"}, {"id": 55, "label": "Senior Product Designer", "department": "Product"}, {"id": 56, "label": "Senior Visual Designer", "department": "Product"}, {"id": 57, "label": "Senior Customer Operations Associate", "department": "Operations"}, {"id": 58, "label": "Customer Operations Manager", "department": "Operations"}, {"id": 59, "label": "Senior Customer Operations Team", "department": "Operations"}, {"id": 60, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 61, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 62, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 63, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 64, "label": "Product Operations Associate II, C", "department": "Operations"}, {"id": 65, "label": "Product Operations Associate II, P", "department": "Operations"}, {"id": 66, "label": "Senior Customer Operations Team", "department": "Operations"}, {"id": 67, "label": "Customer Operations Team Lead", "department": "Operations"}, {"id": 68, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 69, "label": "Customer Operations Intern", "department": "Operations"}, {"id": 70, "label": "Customer Operations Intern", "department": "Operations"}, {"id": 71, "label": "Customer Operations Manager", "department": "Operations"}, {"id": 72, "label": "Onboarding Operations Associate", "department": "Operations"}, {"id": 73, "label": "Customer Operations Team Lead", "department": "Operations"}, {"id": 74, "label": "Senior Customer Operations Team", "department": "Operations"}, {"id": 75, "label": "Onboarding Operations Associate", "department": "Operations"}, {"id": 76, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 77, "label": "Senior Onboarding Operations Team", "department": "Operations"}, {"id": 78, "label": "Onboarding Operations Associate", "department": "Operations"}, {"id": 79, "label": "Senior Customer Operations Team", "department": "Operations"}, {"id": 80, "label": "Customer Operations Associate II", "department": "Operations"}, {"id": 81, "label": "Chief Compliance Officer", "department": "Risk & Compliance"}, {"id": 82, "label": "Project Manager - Marketing Operations", "department": "Marketing"}, {"id": 83, "label": "CTO", "department": "Engineering"}, {"id": 84, "label": "Global Head of Financial Crime", "department": "Risk & Compliance"}, {"id": 85, "label": "Head of Risk & Compliance - USA", "department": "Risk & Compliance"}, {"id": 86, "label": "Head of Risk & Compliance - Ghana", "department": "Risk & Compliance"}, {"id": 87, "label": "Compliance Analyst", "department": "Risk & Compliance"}, {"id": 88, "label": "Senior Data Scientist", "department": "Engineering"}, {"id": 89, "label": "Compliance Program Manager", "department": "Risk & Compliance"}, {"id": 90, "label": "Global Head of Compliance Technology", "department": "Engineering"}, {"id": 91, "label": "Senior Crypto Analyst", "department": "Risk & Compliance"}, {"id": 92, "label": "Fraud Analyst", "department": "Risk & Compliance"}, {"id": 93, "label": "Senior Financial Crime Analyst", "department": "Risk & Compliance"}, {"id": 94, "label": "Financial Crime Analyst", "department": "Risk & Compliance"}, {"id": 95, "label": "Financial Crime Analyst", "department": "Risk & Compliance"}, {"id": 96, "label": "Financial Crime/Screening Manager", "department": "Risk & Compliance"}, {"id": 97, "label": "Senior Screening Analyst", "department": "Risk & Compliance"}, {"id": 98, "label": "Screening Analyst", "department": "Risk & Compliance"}, {"id": 99, "label": "Compliance Program Analyst I", "department": "Risk & Compliance"}, {"id": 100, "label": "Compliance Program Analyst I", "department": "Risk & Compliance"}, {"id": 101, "label": "Head of Enterprise Risk Management", "department": "Risk & Compliance"}, {"id": 102, "label": "Compliance Program Analyst", "department": "Operations"}, {"id": 103, "label": "Regional Head of Risk & Compliance", "department": "Risk & Compliance"}, {"id": 104, "label": "Head of Risk & Compliance - Rwanda", "department": "Risk & Compliance"}, {"id": 105, "label": "Staff Software Engineer", "department": "Engineering"}, {"id": 106, "label": "Software Engineer II", "department": "Engineering"}, {"id": 107, "label": "Senior Fraud Data Analyst", "department": "Risk & Compliance"}, {"id": 108, "label": "Software Engineer", "department": "Engineering"}, {"id": 109, "label": "Senior Director, IT", "department": "Engineering"}, {"id": 110, "label": "TechOps Engineer", "department": "Engineering"}, {"id": 111, "label": "Senior IT Help Desk Analyst", "department": "Engineering"}, {"id": 112, "label": "Senior Product Analyst", "department": "Engineering"}, {"id": 113, "label": "Senior Director, Product Engineering", "department": "Engineering"}, {"id": 114, "label": "Engineering Team Lead", "department": "Engineering"}, {"id": 115, "label": "Engineering Team Lead", "department": "Engineering"}, {"id": 116, "label": "Software Engineer ", "department": "Engineering"}, {"id": 117, "label": "Software Engineer I", "department": "Engineering"}, {"id": 118, "label": "Software Engineer I", "department": "Engineering"}, {"id": 119, "label": "Senior Software Engineer", "department": "Engineering"}, {"id": 120, "label": "Senior React Native Engineer", "department": "Engineering"}, {"id": 121, "label": "Software Engineer II", "department": "Engineering"}, {"id": 122, "label": "Software Engineer II", "department": "Engineering"}, ] # Edges: source is parent, target is child ORG_EDGES = [ {"source": 0, "target": 1}, {"source": 0, "target": 2}, {"source": 0, "target": 4}, {"source": 0, "target": 3}, {"source": 4, "target": 5}, {"source": 16, "target": 17}, {"source": 17, "target": 18}, {"source": 20, "target": 21}, {"source": 20, "target": 22}, {"source": 19, "target": 20}, {"source": 19, "target": 23}, {"source": 23, "target": 27}, {"source": 23, "target": 28}, {"source": 23, "target": 26}, {"source": 23, "target": 25}, {"source": 23, "target": 30}, {"source": 23, "target": 29}, {"source": 23, "target": 31}, {"source": 23, "target": 32}, {"source": 32, "target": 33}, {"source": 32, "target": 34}, {"source": 32, "target": 35}, {"source": 23, "target": 36}, {"source": 36, "target": 37}, {"source": 36, "target": 38}, {"source": 36, "target": 39}, {"source": 36, "target": 40}, {"source": 36, "target": 41}, {"source": 19, "target": 42}, {"source": 42, "target": 43}, {"source": 19, "target": 44}, {"source": 19, "target": 45}, {"source": 19, "target": 46}, {"source": 19, "target": 47}, {"source": 47, "target": 48}, {"source": 47, "target": 49}, {"source": 19, "target": 50}, {"source": 50, "target": 53}, {"source": 50, "target": 52}, {"source": 53, "target": 58}, {"source": 58, "target": 59}, {"source": 59, "target": 60}, {"source": 59, "target": 61}, {"source": 59, "target": 62}, {"source": 59, "target": 63}, {"source": 59, "target": 64}, {"source": 59, "target": 65}, {"source": 53, "target": 66}, {"source": 66, "target": 67}, {"source": 66, "target": 68}, {"source": 67, "target": 70}, {"source": 67, "target": 69}, {"source": 53, "target": 71}, {"source": 71, "target": 77}, {"source": 77, "target": 78}, {"source": 71, "target": 72}, {"source": 71, "target": 73}, {"source": 71, "target": 74}, {"source": 74, "target": 75}, {"source": 74, "target": 76}, {"source": 53, "target": 79}, {"source": 53, "target": 57}, {"source": 79, "target": 80}, {"source": 19, "target": 51}, {"source": 51, "target": 54}, {"source": 51, "target": 55}, {"source": 55, "target": 56}, {"source": 1, "target": 81}, {"source": 81, "target": 84}, {"source": 84, "target": 91}, {"source": 84, "target": 92}, {"source": 84, "target": 93}, {"source": 84, "target": 94}, {"source": 84, "target": 95}, {"source": 84, "target": 96}, {"source": 96, "target": 97}, {"source": 96, "target": 98}, {"source": 81, "target": 85}, {"source": 81, "target": 86}, {"source": 81, "target": 87}, {"source": 81, "target": 88}, {"source": 81, "target": 89}, {"source": 89, "target": 99}, {"source": 89, "target": 101}, {"source": 89, "target": 100}, {"source": 89, "target": 102}, {"source": 89, "target": 103}, {"source": 103, "target": 104}, {"source": 81, "target": 90}, {"source": 90, "target": 105}, {"source": 90, "target": 106}, {"source": 90, "target": 107}, {"source": 90, "target": 108}, {"source": 83, "target": 112}, {"source": 83, "target": 109}, {"source": 109, "target": 110}, {"source": 109, "target": 111}, {"source": 83, "target": 113}, {"source": 113, "target": 114}, {"source": 114, "target": 117}, {"source": 114, "target": 119}, {"source": 114, "target": 116}, {"source": 114, "target": 120}, {"source": 113, "target": 115}, {"source": 115, "target": 121}, {"source": 115, "target": 118}, {"source": 115, "target": 122}, {"source": 3, "target": 6}, {"source": 3, "target": 7}, {"source": 7, "target": 8}, {"source": 8, "target": 12}, {"source": 7, "target": 9}, {"source": 7, "target": 10}, {"source": 7, "target": 11}, {"source": 11, "target": 14}, {"source": 11, "target": 15}, {"source": 9, "target": 13}, {"source": 1, "target": 82}, {"source": 1, "target": 16}, {"source": 1, "target": 83}, {"source": 1, "target": 19}, {"source": 23, "target": 24}, ] # Build lookup structures _parent_map = {} # child_id -> parent_id _children_map = {} # parent_id -> [child_ids] _node_labels = {} # id -> label _label_to_id = {} # label -> id def _build_maps(): """Build the lookup maps from edges and nodes.""" global _parent_map, _children_map, _node_labels, _label_to_id _parent_map = {} _children_map = {} _node_labels = {} _label_to_id = {} # Build node labels map for node in ORG_NODES: _node_labels[node['id']] = node['label'] _label_to_id[node['label']] = node['id'] # Build parent and children maps for edge in ORG_EDGES: parent = edge['source'] child = edge['target'] _parent_map[child] = parent if parent not in _children_map: _children_map[parent] = [] _children_map[parent].append(child) # Prefer the directed CSV chart if present, then build maps. _try_load_org_from_csv() _build_maps() def get_root_node_id(): """Get the root node ID for the active org chart.""" return _ROOT_NODE_ID def get_all_node_labels(): """All node labels in ID order (used for Intake suggestions).""" return [n['label'] for n in ORG_NODES] def get_node_id_by_label(label): """Map a node label to its node ID. Returns None if not found.""" return _label_to_id.get(label) def get_node_label(node_id): """Get the label/title for a node ID.""" return _node_labels.get(node_id, f"Unknown ({node_id})") def get_parent(node_id): """Get the immediate parent of a node. Returns None for root (CEO).""" return _parent_map.get(node_id) def get_children(node_id): """Get the immediate children of a node.""" return _children_map.get(node_id, []) def get_path_to_ceo(node_id): """ Get the path from a node to the root. Returns a list starting with the node itself and ending with the root. """ path = [node_id] current = node_id while current != _ROOT_NODE_ID and current in _parent_map: current = _parent_map[current] path.append(current) return path def get_ancestors(node_id): """ Get all ancestors of a node (nodes on path to CEO, excluding self). These are the "managers/superiors" - anyone above in the hierarchy. """ path = get_path_to_ceo(node_id) return path[1:] if len(path) > 1 else [] def get_all_descendants(node_id): """ Get all descendants of a node (all nodes below in hierarchy). These are the "subordinates" - anyone for whom this node is on their path to CEO. """ descendants = [] to_visit = get_children(node_id)[:] while to_visit: child = to_visit.pop(0) descendants.append(child) to_visit.extend(get_children(child)) return descendants def get_siblings(node_id): """ Get siblings of a node (nodes with the same parent, excluding self). These are "colleagues" - people at the same level with the same manager. """ parent = get_parent(node_id) if parent is None: return [] # CEO has no siblings return [c for c in get_children(parent) if c != node_id] def get_path_length_to_ceo(node_id): """Get the number of hops from a node to the CEO.""" return len(get_path_to_ceo(node_id)) - 1 def get_organizational_distance(node_a, node_b): """ Calculate organizational distance between two nodes. This is the sum of their path lengths to CEO. Used for the fallback "most distant" partner selection. """ return get_path_length_to_ceo(node_a) + get_path_length_to_ceo(node_b) def select_partners(current_node_id, available_participants, num_partners=3, include_extra_superior=False): """ Select partners for the current participant based on organizational hierarchy. Args: current_node_id: The org chart node ID of the current participant available_participants: List of dicts with 'node_id' and 'participant_id' keys representing previous participants who have completed num_partners: Number of partners to select (default 3) include_extra_superior: If True and multiple superiors available, add a random extra superior as 4th partner (for join_order >= 4) Returns: List of selected participant dicts, in priority order Priority: 1. Chain-of-command (Superiors): Available ancestors, nearest → root 2. Chain-of-command (Subordinates): Available descendants, closest-first 3. Colleague/Sibling: Same parent (same immediate manager) 4. Fallback: Maximize organizational distance """ import random if not available_participants: return [] selected = [] remaining = available_participants[:] # Create lookup: node_id -> participant info node_to_participant = {p['node_id']: p for p in remaining} available_nodes = set(p['node_id'] for p in remaining) def select_and_remove(participant): """Helper to add a participant to selected and remove from remaining.""" selected.append(participant) remaining.remove(participant) available_nodes.discard(participant['node_id']) # Track available superiors (useful for debugging / testing) ancestors = get_ancestors(current_node_id) available_superiors = [node_to_participant[a] for a in ancestors if a in available_nodes] # Priority 1: Chain-of-command (Superiors) # Select as many available ancestors as needed (nearest → root). for ancestor_id in ancestors: # ordered nearest → farthest (toward root) if len(selected) >= num_partners: break if ancestor_id in available_nodes: select_and_remove(node_to_participant[ancestor_id]) if len(selected) >= num_partners: return selected[:num_partners] # Priority 2: Chain-of-command (Subordinates) # Prefer closest subordinates (direct reports, then their reports, etc.). # If the caller requested an extra chain-of-command partner, allow selecting # a second subordinate (if available) before moving on. descendants = get_all_descendants(current_node_id) max_descendants = 2 if include_extra_superior else 1 picked_descendants = 0 for desc_id in descendants: # BFS order = closest first if len(selected) >= num_partners: break if picked_descendants >= max_descendants: break if desc_id in available_nodes: select_and_remove(node_to_participant[desc_id]) picked_descendants += 1 if len(selected) >= num_partners: return selected[:num_partners] # Priority 3: Colleague/Sibling - same parent siblings = get_siblings(current_node_id) for sib_id in siblings: if sib_id in available_nodes: select_and_remove(node_to_participant[sib_id]) break if len(selected) >= num_partners: return selected[:num_partners] # Priority 4 (Fallback): Maximize organizational distance # Sort remaining by organizational distance (descending) remaining_with_distance = [ (p, get_organizational_distance(current_node_id, p['node_id'])) for p in remaining ] remaining_with_distance.sort(key=lambda x: x[1], reverse=True) for participant, _ in remaining_with_distance: if len(selected) >= num_partners: break select_and_remove(participant) return selected[:num_partners] def get_relationship_type(current_node_id, partner_node_id): """ Determine the organizational relationship between two nodes. Returns one of: - 'superior': partner is an ancestor (manager/superior) - 'subordinate': partner is a descendant (reports to current) - 'sibling': partner has the same parent (colleague) - 'distant': no direct relationship """ if partner_node_id in get_ancestors(current_node_id): return 'superior' if partner_node_id in get_all_descendants(current_node_id): return 'subordinate' if partner_node_id in get_siblings(current_node_id): return 'sibling' return 'distant'