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
This commit is contained in:
ajaysi
2026-06-01 12:24:31 +05:30
parent 9b472f1c18
commit 923fa671fe
90 changed files with 8914 additions and 2731 deletions

View File

@@ -2,20 +2,22 @@ from typing import Any, Dict, List, Optional
import requests
from loguru import logger
from .retry import wix_api_call_with_retry, WixAPIError
class WixBlogService:
"""Service for Wix Blog API operations with retry logic and error handling."""
def __init__(self, base_url: str, client_id: Optional[str]):
self.base_url = base_url
self.client_id = client_id
def headers(self, access_token: str, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
"""Build headers with automatic token type detection."""
h: Dict[str, str] = {
'Content-Type': 'application/json',
}
# Support both OAuth tokens and API keys
# API keys don't use 'Bearer' prefix
# Ensure access_token is a string (defensive check)
if access_token:
# Normalize token to string if needed
if not isinstance(access_token, str):
@@ -28,20 +30,18 @@ class WixBlogService:
token = access_token.strip()
if token:
# CRITICAL: Wix OAuth tokens can have format "OauthNG.JWS.xxx.yyy.zzz"
# These should use "Bearer" prefix even though they have more than 2 dots
if token.startswith('OauthNG.JWS.'):
# Wix OAuth token - use Bearer prefix
h['Authorization'] = f'Bearer {token}'
logger.debug("Using Wix OAuth token with Bearer prefix (OauthNG.JWS. format detected)")
elif '.' not in token or len(token) > 500:
# Likely an API key - use directly without Bearer prefix
elif token.startswith('IST.'):
h['Authorization'] = token
logger.debug("Using API key for authorization")
else:
# Standard JWT OAuth token (xxx.yyy.zzz format) - use Bearer prefix
logger.debug("Using Wix API key for authorization (IST. format detected)")
elif token.count('.') == 2:
h['Authorization'] = f'Bearer {token}'
logger.debug("Using OAuth Bearer token for authorization")
logger.debug("Using OAuth Bearer token for authorization (JWT: 2 dots)")
else:
h['Authorization'] = token
logger.debug("Using token as-is for authorization")
if self.client_id:
h['wix-client-id'] = self.client_id
@@ -50,12 +50,12 @@ class WixBlogService:
return h
def create_draft_post(self, access_token: str, payload: Dict[str, Any], extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""Create draft post with consolidated logging"""
"""Create draft post with retry logic and consolidated logging."""
from .logger import wix_logger
import json
import traceback as tb
# Build payload summary for logging
# Build payload summary for logging (safe, no sensitive data)
payload_summary = {}
if 'draftPost' in payload:
dp = payload['draftPost']
@@ -66,64 +66,114 @@ class WixBlogService:
}
request_headers = self.headers(access_token, extra_headers)
logger.debug(f"Wix API request headers: {list(request_headers.keys())}")
if 'wix-site-id' in request_headers:
logger.info(f"Wix API call includes wix-site-id: {request_headers['wix-site-id'][:8]}...")
else:
logger.warning("Wix API call MISSING wix-site-id header — this may fail for multi-site tokens")
url = f"{self.base_url}/blog/v3/draft-posts"
try:
response = requests.post(f"{self.base_url}/blog/v3/draft-posts", headers=request_headers, json=payload)
except TypeError as e:
logger.error(f"TypeError during requests.post in create_draft_post: {e}")
logger.error(f"Traceback: {tb.format_exc()}")
logger.error(f"access_token type: {type(access_token)}")
logger.error(f"payload type: {type(payload)}, keys: {list(payload.keys()) if isinstance(payload, dict) else 'N/A'}")
result = wix_api_call_with_retry('POST', url, request_headers, json_payload=payload, max_attempts=3)
wix_logger.log_api_call("POST", "/blog/v3/draft-posts", 200, payload_summary, None)
return result
except WixAPIError as e:
wix_logger.log_api_call("POST", "/blog/v3/draft-posts", e.status_code or 500, payload_summary, e.response_body)
logger.error(f"Wix create_draft_post failed after retries: HTTP {e.status_code} - {e.response_body}")
raise
except Exception as e:
wix_logger.log_api_call("POST", "/blog/v3/draft-posts", 500, payload_summary, str(e)[:200])
logger.error(f"Unexpected error in create_draft_post: {e}")
raise
# Consolidated error logging
error_body = None
if response.status_code >= 400:
try:
error_body = response.json()
except:
error_body = {'message': response.text[:200]}
wix_logger.log_api_call("POST", "/blog/v3/draft-posts", response.status_code, payload_summary, error_body)
if response.status_code >= 400:
# Only show detailed error info for debugging
if response.status_code == 500:
logger.debug(f" Full error: {json.dumps(error_body, indent=2) if isinstance(error_body, dict) else error_body}")
response.raise_for_status()
return response.json()
def publish_draft(self, access_token: str, draft_post_id: str, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
response = requests.post(f"{self.base_url}/blog/v3/draft-posts/{draft_post_id}/publish", headers=self.headers(access_token, extra_headers))
response.raise_for_status()
return response.json()
"""Publish a draft post with retry logic."""
url = f"{self.base_url}/blog/v3/draft-posts/{draft_post_id}/publish"
headers = self.headers(access_token, extra_headers)
try:
return wix_api_call_with_retry('POST', url, headers, max_attempts=3)
except WixAPIError as e:
logger.error(f"Wix publish_draft failed: HTTP {e.status_code} - {e.response_body}")
raise
def list_categories(self, access_token: str, extra_headers: Optional[Dict[str, str]] = None) -> List[Dict[str, Any]]:
response = requests.get(f"{self.base_url}/blog/v3/categories", headers=self.headers(access_token, extra_headers))
response.raise_for_status()
return response.json().get('categories', [])
"""List blog categories with retry logic."""
url = f"{self.base_url}/blog/v3/categories"
headers = self.headers(access_token, extra_headers)
try:
result = wix_api_call_with_retry('GET', url, headers, max_attempts=3)
return result.get('categories', [])
except WixAPIError as e:
logger.error(f"Wix list_categories failed: HTTP {e.status_code}")
raise
def create_category(self, access_token: str, label: str, description: Optional[str] = None, language: Optional[str] = None, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
def create_category(self, access_token: str, label: str, description: Optional[str] = None,
language: Optional[str] = None, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""Create a blog category with retry logic."""
url = f"{self.base_url}/blog/v3/categories"
headers = self.headers(access_token, extra_headers)
payload: Dict[str, Any] = {'category': {'label': label}, 'fieldsets': ['URL']}
if description:
payload['category']['description'] = description
if language:
payload['category']['language'] = language
response = requests.post(f"{self.base_url}/blog/v3/categories", headers=self.headers(access_token, extra_headers), json=payload)
response.raise_for_status()
return response.json()
try:
return wix_api_call_with_retry('POST', url, headers, json_payload=payload, max_attempts=3)
except WixAPIError as e:
logger.error(f"Wix create_category failed: HTTP {e.status_code}")
raise
def list_tags(self, access_token: str, extra_headers: Optional[Dict[str, str]] = None) -> List[Dict[str, Any]]:
response = requests.get(f"{self.base_url}/blog/v3/tags", headers=self.headers(access_token, extra_headers))
response.raise_for_status()
return response.json().get('tags', [])
"""List blog tags with retry logic."""
url = f"{self.base_url}/blog/v3/tags"
headers = self.headers(access_token, extra_headers)
try:
result = wix_api_call_with_retry('GET', url, headers, max_attempts=3)
return result.get('tags', [])
except WixAPIError as e:
logger.error(f"Wix list_tags failed: HTTP {e.status_code}")
raise
def create_tag(self, access_token: str, label: str, language: Optional[str] = None, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
def create_tag(self, access_token: str, label: str, language: Optional[str] = None,
extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""Create a blog tag with retry logic."""
url = f"{self.base_url}/blog/v3/tags"
headers = self.headers(access_token, extra_headers)
payload: Dict[str, Any] = {'label': label, 'fieldsets': ['URL']}
if language:
payload['language'] = language
response = requests.post(f"{self.base_url}/blog/v3/tags", headers=self.headers(access_token, extra_headers), json=payload)
response.raise_for_status()
return response.json()
try:
return wix_api_call_with_retry('POST', url, headers, json_payload=payload, max_attempts=3)
except WixAPIError as e:
logger.error(f"Wix create_tag failed: HTTP {e.status_code}")
raise
def get_draft_post(self, access_token: str, draft_post_id: str,
extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""Get a draft post by ID with retry logic."""
url = f"{self.base_url}/blog/v3/draft-posts/{draft_post_id}"
headers = self.headers(access_token, extra_headers)
try:
return wix_api_call_with_retry('GET', url, headers, max_attempts=3)
except WixAPIError as e:
logger.error(f"Wix get_draft_post failed: HTTP {e.status_code}")
raise
def update_draft_post(self, access_token: str, draft_post_id: str, payload: Dict[str, Any],
extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""Update a draft post with retry logic."""
url = f"{self.base_url}/blog/v3/draft-posts/{draft_post_id}"
headers = self.headers(access_token, extra_headers)
try:
return wix_api_call_with_retry('PUT', url, headers, json_payload=payload, max_attempts=3)
except WixAPIError as e:
logger.error(f"Wix update_draft_post failed: HTTP {e.status_code}")
raise