Files
ALwrity/backend/services/integrations/wix/retry.py
ajaysi 923fa671fe feat: ContentGuardianAgent, onboarding UX, Team Activity action wiring, docs, agent help modal
ContentGuardianAgent consolidation:
- Merge 3 duplicate classes into single source in specialized/content_guardian.py
- Watchdog audit_committee() with heuristic scoring, coverage gaps, overlaps, alerts
- Remove misleading rejection_rate() helper; use acceptance_rate directly
- Integrate audit + alerts + trend signals into today_workflow_service.py

Team Activity page:
- QualityAuditPanel: health ring, per-agent critiques, coverage gaps, overlaps
- TrendSignalsPanel: opportunity cards with urgency/impact/coverage bars
- AlertBanner: persistent dismiss via POST /alerts/{id}/mark-read
- AgentHelpModal: dialog showing all 8 agents with descriptions, tools, schedule
- QualityAuditPanel action buttons: Fill gap -> /content-planning, Resolve overlap, View CTA on alerts/issues
- TrendSignalsPanel action buttons: Create content from this trend -> /blog-writer with trend context state

Onboarding system:
- Step 4 validation: no auto-pass via basic_ready; requires persona data or explicit progression
- Step 5 validation: logs warning on auto-pass without integration data
- OnboardingCompletionService: single DB session, transactional task creation, upsert pattern
- Business-without-website: nullable website_url on SIFIndexingTask and MarketTrendsTask
- DeepCompetitorAnalysisExecutor: 5-min timeout, 10-competitor cap, asyncio.wait_for
- Persona generation: async with 30s timeout, falls back to scheduler
- OnboardingProgressService.reset_onboarding(): resets session + pauses all DB tasks
- OnboardingControlService.reset_onboarding(): also cancels APScheduler jobs
- FinalStep TaskSchedulingPanel: shows scheduled/failed tasks after completion, 8s auto-redirect
- onboarding_completed agent activity event logged to feed

Documentation:
- docs-site/features/onboarding/: overview, steps, scheduler-tasks, technical-reference (4 pages)
- docs-site/mkdocs.yml: added Onboarding System nav section
- docs-site/features/sif-agents/: overview, agent-directory, committee-system, content-guardian (4 pages)
- docs-site/features/team-activity/: overview, quality-audit, trend-signals, alert-system (4 pages)
- docs-site/features/todays-workflow/: updated overview, technical-architecture, workflow-guide, api-reference
2026-06-01 12:24:31 +05:30

169 lines
5.8 KiB
Python

"""
Retry utilities for Wix API calls with exponential backoff.
Production-grade retry logic that respects Wix rate limits and handles
transient failures gracefully.
"""
import time
import random
from typing import Callable, TypeVar, Optional
from loguru import logger
T = TypeVar('T')
class WixAPIError(Exception):
"""Custom exception for Wix API errors with status code context."""
def __init__(self, message: str, status_code: Optional[int] = None, response_body: Optional[str] = None):
super().__init__(message)
self.status_code = status_code
self.response_body = response_body
def is_retryable(self) -> bool:
"""Determine if this error is retryable based on status code."""
if self.status_code is None:
return True # Network errors are retryable
# 429 = rate limit, 502/503/504 = gateway errors, 500 = internal server error (sometimes transient)
return self.status_code in (429, 500, 502, 503, 504)
def is_rate_limit(self) -> bool:
"""Check if this is a rate limit error."""
return self.status_code == 429
def with_retry(
fn: Callable[[], T],
max_attempts: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
retryable_exceptions: tuple = (Exception,),
operation_name: str = "Wix API call"
) -> T:
"""
Execute a function with exponential backoff retry logic.
Args:
fn: Function to execute (should make the API call)
max_attempts: Maximum number of attempts (default: 3)
base_delay: Initial delay in seconds (default: 1.0)
max_delay: Maximum delay in seconds (default: 30.0)
retryable_exceptions: Tuple of exception types to retry on
operation_name: Name for logging
Returns:
Result of fn()
Raises:
WixAPIError: If all retries are exhausted
Exception: If a non-retryable exception occurs
"""
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return fn()
except WixAPIError as e:
last_exception = e
if attempt >= max_attempts:
break
if not e.is_retryable():
logger.warning(f"{operation_name}: non-retryable error (HTTP {e.status_code}), failing fast")
raise
# Calculate delay with exponential backoff and jitter
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
# Add jitter (±25%) to prevent thundering herd
jitter = delay * 0.25
actual_delay = delay + random.uniform(-jitter, jitter)
actual_delay = max(0.1, actual_delay) # Minimum 100ms delay
if e.is_rate_limit():
# For rate limits, use a longer base delay
actual_delay = max(actual_delay, 2.0)
logger.warning(f"{operation_name}: rate limited (429), waiting {actual_delay:.1f}s before retry {attempt + 1}/{max_attempts}")
else:
logger.warning(f"{operation_name}: attempt {attempt}/{max_attempts} failed (HTTP {e.status_code}), waiting {actual_delay:.1f}s before retry")
time.sleep(actual_delay)
except retryable_exceptions as e:
last_exception = e
if attempt >= max_attempts:
break
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
jitter = delay * 0.25
actual_delay = delay + random.uniform(-jitter, jitter)
actual_delay = max(0.1, actual_delay)
logger.warning(f"{operation_name}: attempt {attempt}/{max_attempts} failed ({type(e).__name__}), waiting {actual_delay:.1f}s before retry")
time.sleep(actual_delay)
# All retries exhausted
if last_exception:
if isinstance(last_exception, WixAPIError):
raise last_exception
raise WixAPIError(f"{operation_name}: failed after {max_attempts} attempts: {last_exception}")
raise WixAPIError(f"{operation_name}: failed after {max_attempts} attempts")
def wix_api_call_with_retry(
method: str,
url: str,
headers: dict,
json_payload: Optional[dict] = None,
max_attempts: int = 3
) -> dict:
"""
Convenience wrapper for making Wix API calls with retry logic.
Args:
method: HTTP method ('GET', 'POST', etc.)
url: Full API URL
headers: Request headers
json_payload: Optional JSON payload for POST/PUT
max_attempts: Maximum retry attempts
Returns:
Parsed JSON response
Raises:
WixAPIError: On failure after retries
"""
import requests
def _call():
if method.upper() == 'GET':
resp = requests.get(url, headers=headers, timeout=30)
elif method.upper() == 'POST':
resp = requests.post(url, headers=headers, json=json_payload, timeout=30)
elif method.upper() == 'PUT':
resp = requests.put(url, headers=headers, json=json_payload, timeout=30)
elif method.upper() == 'DELETE':
resp = requests.delete(url, headers=headers, timeout=30)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
if resp.status_code >= 400:
body = None
try:
body = resp.text[:500]
except:
body = str(resp.content)[:500]
raise WixAPIError(
f"Wix API {method} {url} failed: HTTP {resp.status_code}",
status_code=resp.status_code,
response_body=body
)
return resp.json()
return with_retry(
_call,
max_attempts=max_attempts,
operation_name=f"Wix {method} {url.split('/')[-1]}"
)