Spaces:
Sleeping
Sleeping
| """ | |
| Deterministic heuristics for bug-triage inference and action suggestion. | |
| This module is the single source of truth for all rule-based logic that | |
| maps ticket attributes to severity, priority, component, and team. Both | |
| the standalone ``inference.py`` runner and the FastAPI ``server/app.py`` | |
| import from here so that fixes only need to be applied in one place. | |
| """ | |
| from __future__ import annotations | |
| # --------------------------------------------------------------------------- | |
| # Lookup tables | |
| # --------------------------------------------------------------------------- | |
| #: Maps a component slug to the engineering team responsible for it. | |
| COMPONENT_TEAM_MAP: dict[str, str] = { | |
| "api-gateway": "backend-api", | |
| "auth-service": "backend-api", | |
| "user-service": "backend-api", | |
| "payment-service": "backend-api", | |
| "web-app": "frontend-web", | |
| "ios-app": "mobile-ios", | |
| "android-app": "mobile-android", | |
| "database": "data-platform", | |
| "cache": "infrastructure", | |
| "cdn": "infrastructure", | |
| } | |
| #: Free-text keywords that strongly suggest a component when found in a ticket. | |
| COMPONENT_KEYWORDS: dict[str, tuple[str, ...]] = { | |
| "api-gateway": ("api gateway", "gateway", "edge", "proxy", "routing", "/api/"), | |
| "auth-service": ("auth", "authentication", "login", "signin", "token", "session"), | |
| "user-service": ("user service", "users endpoint", "profile", "identity", "account"), | |
| "payment-service": ("payment", "checkout", "charge", "billing", "tax", "order"), | |
| "web-app": ("web app", "web-app", "browser", "dashboard", "frontend", "page"), | |
| "ios-app": ("ios", "iphone", "ipad", "apple"), | |
| "android-app": ("android",), | |
| "database": ("database", "query", "sql", "db", "index"), | |
| "cache": ("cache", "redis", "memcache"), | |
| "cdn": ("cdn", "image", "asset", "static content"), | |
| } | |
| #: A service name may hint that certain components are likely candidates. | |
| SERVICE_COMPONENT_HINTS: dict[str, tuple[str, ...]] = { | |
| "api": ("api-gateway", "user-service"), | |
| "auth": ("auth-service",), | |
| "identity": ("user-service",), | |
| "payments": ("payment-service",), | |
| "web-app": ("web-app", "cdn", "database", "cache"), | |
| "mobile-app": ("ios-app", "android-app", "auth-service"), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Helper β build a single lowercase string from a ticket for keyword search | |
| # --------------------------------------------------------------------------- | |
| def ticket_text(ticket) -> str: | |
| """Return a single lowercase string combining ticket fields for keyword search.""" | |
| return " ".join( | |
| str(part) | |
| for part in ( | |
| ticket.title, | |
| ticket.description, | |
| ticket.service, | |
| " ".join(ticket.component_candidates), | |
| ) | |
| ).lower() | |
| # --------------------------------------------------------------------------- | |
| # Component inference | |
| # --------------------------------------------------------------------------- | |
| def infer_component(ticket, available_components: list[str]) -> str: | |
| """ | |
| Pick the most likely component for *ticket* from *available_components*. | |
| Scoring heuristic (higher wins): | |
| - Position bonus: earlier in ``component_candidates`` β up to +3 | |
| - Service hint match β +3 | |
| - Component name appears in ticket text β +4 | |
| - Keyword match β +3 per keyword | |
| - Ad-hoc tie-breakers for common patterns β +1 | |
| """ | |
| candidates = [c for c in ticket.component_candidates if c in available_components] | |
| if not candidates: | |
| return available_components[0] if available_components else "api-gateway" | |
| text = ticket_text(ticket) | |
| service_hints = SERVICE_COMPONENT_HINTS.get(ticket.service, ()) | |
| best_candidate = candidates[0] | |
| best_score = -1 | |
| for index, candidate in enumerate(candidates): | |
| score = max(0, 3 - index) # position bonus | |
| if candidate in service_hints: | |
| score += 3 | |
| normalized = candidate.replace("-", " ") | |
| if normalized in text: | |
| score += 4 | |
| for keyword in COMPONENT_KEYWORDS.get(candidate, ()): | |
| if keyword in text: | |
| score += 3 | |
| # Ad-hoc tie-breakers | |
| if candidate == "ios-app" and "login" in text: | |
| score += 1 | |
| if candidate == "payment-service" and "gateway" in text: | |
| score += 1 | |
| if candidate == "database" and "slow" in text: | |
| score += 1 | |
| if score > best_score: | |
| best_score = score | |
| best_candidate = candidate | |
| return best_candidate | |
| # --------------------------------------------------------------------------- | |
| # Severity inference | |
| # --------------------------------------------------------------------------- | |
| def infer_severity(ticket, component: str) -> str: | |
| """ | |
| Return the inferred severity level (``sev0``β``sev3``) for *ticket*. | |
| Rules are applied in priority order; the first match wins. | |
| """ | |
| text = ticket_text(ticket) | |
| synthetic_high_signal = "signal quality is high" in text | |
| synthetic_low_signal = "signal quality is low" in text | |
| # sev0 β data / security / money corruption | |
| if any(k in text for k in ["security", "unauthorized", "double charge", "data loss", "corrupt"]): | |
| return "sev0" | |
| # sev0 β critical monitoring alert on enterprise | |
| if any(k in text for k in ["500 internal server error", "null pointer exception", "multiple monitoring alerts"]): | |
| if ticket.reporter_type == "monitoring" and ticket.customer_tier == "enterprise": | |
| return "sev0" | |
| # sev1 β high-signal synthetic flag on enterprise monitoring | |
| if synthetic_high_signal and ticket.reporter_type == "monitoring" and ticket.customer_tier == "enterprise": | |
| return "sev1" | |
| # sev1 β service down / timeout / outage | |
| if any(k in text for k in ["timeout", "timing out", "503", "outage", "down"]): | |
| return "sev1" | |
| # sev1 β high-signal on paid tiers | |
| if synthetic_high_signal and ticket.customer_tier in {"pro", "enterprise"}: | |
| return "sev1" | |
| # sev1/sev2 β incorrect tax (tier-dependent) | |
| if "incorrect tax" in text or ("tax" in text and "wrong" in text): | |
| return "sev1" if ticket.customer_tier in {"pro", "enterprise"} else "sev2" | |
| # sev2/sev3 β low-signal synthetic flag | |
| if synthetic_low_signal: | |
| return "sev3" if ticket.customer_tier == "free" else "sev2" | |
| # sev2 β clearly broken functionality | |
| if any(k in text for k in ["crash", "not responding", "not working", "broken image", "wrong values"]): | |
| return "sev2" | |
| # sev2 (or sev3 for free DB slowness) β degraded performance | |
| if any(k in text for k in ["latency", "slow", "degraded", "error", "failed"]): | |
| if component == "database" and ticket.customer_tier == "free": | |
| return "sev3" | |
| return "sev2" | |
| return "sev3" | |
| # --------------------------------------------------------------------------- | |
| # Priority inference | |
| # --------------------------------------------------------------------------- | |
| _SEV_TO_PRI: dict[str, str] = { | |
| "sev0": "p0", | |
| "sev1": "p1", | |
| "sev2": "p2", | |
| "sev3": "p3", | |
| } | |
| def severity_to_priority(severity: str) -> str: | |
| """Return the default priority matching *severity*.""" | |
| return _SEV_TO_PRI.get(severity, "p2") | |
| def infer_priority(ticket, severity: str) -> str: | |
| """ | |
| Return the inferred priority for *ticket* given its *severity*. | |
| Upgrades sev2 β p1 when the ticket is enterprise, monitoring-sourced, | |
| or touches high-value areas (payments, CDN, images, shopping). | |
| """ | |
| text = ticket_text(ticket) | |
| if severity == "sev2" and ( | |
| ticket.customer_tier == "enterprise" | |
| or ticket.reporter_type == "monitoring" | |
| or any(k in text for k in ["payment", "checkout", "tax", "cdn", "image", "shopping"]) | |
| ): | |
| return "p1" | |
| return severity_to_priority(severity) | |
| # --------------------------------------------------------------------------- | |
| # Info-request heuristics | |
| # --------------------------------------------------------------------------- | |
| def needs_more_info(ticket) -> bool: | |
| """Return ``True`` if the ticket is missing evidence needed for triage.""" | |
| text = ticket_text(ticket) | |
| # Duplicates are handled separately; skip info request | |
| if ticket.suspected_duplicate_ids: | |
| return False | |
| if "signal quality is low" in text: | |
| return True | |
| if not ticket.repro_steps_present and not ticket.logs_present: | |
| return True | |
| # Non-monitoring reporters need repro steps | |
| if not ticket.repro_steps_present and ticket.reporter_type != "monitoring": | |
| return True | |
| # User/QA reporters should supply logs | |
| if not ticket.logs_present and ticket.reporter_type in {"user", "qa"}: | |
| return True | |
| return False | |
| def suggested_info_type(ticket) -> str: | |
| """Return which type of information to request for *ticket*.""" | |
| if not ticket.repro_steps_present and not ticket.logs_present: | |
| return "both" | |
| if not ticket.repro_steps_present: | |
| return "repro_steps" | |
| if not ticket.logs_present: | |
| return "logs" | |
| return "both" | |
| # --------------------------------------------------------------------------- | |
| # Public API | |
| # --------------------------------------------------------------------------- | |
| __all__ = [ | |
| "COMPONENT_TEAM_MAP", | |
| "COMPONENT_KEYWORDS", | |
| "SERVICE_COMPONENT_HINTS", | |
| "ticket_text", | |
| "infer_component", | |
| "infer_severity", | |
| "infer_priority", | |
| "severity_to_priority", | |
| "needs_more_info", | |
| "suggested_info_type", | |
| ] | |