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

@@ -40,6 +40,7 @@ from .specialized_agents import (
)
from .trend_surfer_agent import TrendSurferAgent
from .content_gap_radar_agent import ContentGapRadarAgent
# Agent Orchestrator
from .agent_orchestrator import (
@@ -67,6 +68,7 @@ __all__ = [
'SEOOptimizationAgent',
'SocialAmplificationAgent',
'TrendSurferAgent',
'ContentGapRadarAgent',
'ALwrityAgentOrchestrator',
'orchestration_service'
]

View File

@@ -230,7 +230,7 @@ class ALwrityAgentOrchestrator:
# Content Guardian Agent
if enabled_by_key.get("content_guardian", True):
try:
from services.intelligence.sif_agents import ContentGuardianAgent
from services.intelligence.agents.specialized.content_guardian import ContentGuardianAgent
from services.intelligence.txtai_service import TxtaiIntelligenceService
# Initialize intelligence service if not already available
@@ -248,6 +248,19 @@ class ALwrityAgentOrchestrator:
except Exception as e:
logger.error(f"Failed to initialize ContentGuardianAgent: {e}")
# Content Gap Radar Agent
if enabled_by_key.get("content_gap_radar", True):
try:
from services.intelligence.agents import ContentGapRadarAgent
from services.intelligence.txtai_service import TxtaiIntelligenceService
intel_service = TxtaiIntelligenceService(self.user_id)
self.content_gap_radar_agent = ContentGapRadarAgent(intel_service, self.user_id)
self.agents['content_gap_radar'] = self.content_gap_radar_agent
initialized_agents.append("Content Gap Radar")
logger.info(f"Initialized ContentGapRadarAgent for user {self.user_id}")
except Exception as e:
logger.error(f"Failed to initialize ContentGapRadarAgent: {e}")
logger.info(f"Created {len(self.agents)} specialized agents for user {self.user_id}")
# Log initialization activity
@@ -449,7 +462,8 @@ class ALwrityAgentOrchestrator:
"competitor": ["Competitor monitoring", "Threat analysis", "Response generation", "Strategy execution"],
"seo": ["SEO auditing", "Issue prioritization", "Auto-fixing", "Strategy generation"],
"social": ["Social monitoring", "Content adaptation", "Engagement optimization", "Distribution management"],
"trend": ["Trend detection", "Opportunity analysis", "Content angle generation"]
"trend": ["Trend detection", "Opportunity analysis", "Content angle generation"],
"content_gap_radar": ["Content gap detection", "SERP opportunity scoring", "Competitor content deep-dive", "ROI-based topic prioritization", "Content brief generation"]
}
# Service class for agent orchestration

View File

@@ -0,0 +1,466 @@
"""
Content Gap Radar Agent
Scores and prioritizes content opportunities by combining SIF semantic gap analysis,
SERP ranking presence (Google CSE), competitor content deep-dive (Exa), and trend
momentum into a single ROI score per topic.
Phase 3 of the Content Gap Radar feature.
"""
import traceback
from typing import List, Dict, Any, Optional
from loguru import logger
from services.intelligence.agents.specialized import SIFBaseAgent
from services.intelligence.agents.specialized.strategy_architect import StrategyArchitectAgent
from services.intelligence.agents.trend_surfer_agent import TrendSurferAgent
from services.intelligence.agents.core_agent_framework import TaskProposal
from services.intelligence.txtai_service import TxtaiIntelligenceService
from services.seo_tools.serp_gap_service import SerpGapService
from services.seo_tools.competitor_content_service import CompetitorContentService
class ContentGapRadarAgent(SIFBaseAgent):
"""
Agent that scores and prioritizes content opportunities by combining
SIF semantic gap analysis, SERP ranking presence, Exa competitor content,
and trend momentum into a single ROI score.
"""
def __init__(self, intelligence_service: TxtaiIntelligenceService, user_id: str, **kwargs):
super().__init__(intelligence_service, user_id, agent_type="content_gap_radar", **kwargs)
self.user_id = user_id
self.serp_service = SerpGapService()
self.competitor_content_service = CompetitorContentService()
self.strategy_architect = StrategyArchitectAgent(intelligence_service, user_id)
async def analyze(
self,
competitor_domains: List[str],
competitor_indices: Optional[List[Any]] = None,
topics: Optional[List[str]] = None,
bypass_cache: bool = False,
) -> Dict[str, Any]:
"""
Full content gap radar pipeline.
1. Get topic-level gaps from SIF semantic analysis
2. Get SERP ranking data per topic
3. Get Exa competitor content for top topics
4. Get trend momentum data
5. Score each topic with ROI formula
6. Return prioritized results
Args:
competitor_domains: Known competitor domains
competitor_indices: SIF index positions for competitor docs
topics: Optional explicit topic list (derived from SIF if omitted)
bypass_cache: Force fresh API calls
Returns:
Dict with scored gaps list and summary.
"""
self._log_agent_operation(
"Running content gap radar",
competitor_count=len(competitor_domains),
topics_provided=bool(topics),
)
try:
sif_gaps = []
# Step 1: Derive topics from SIF semantic gaps if not provided
if not topics:
sif_gaps = await self.strategy_architect.find_semantic_gaps(
competitor_indices or []
)
topics = [g["topic"] for g in sif_gaps[:12]]
logger.info(
f"[{self.__class__.__name__}] Derived {len(topics)} topics from SIF gaps"
)
if not topics:
logger.info(f"[{self.__class__.__name__}] No topics to analyze")
return {"gaps": [], "summary": {}}
# If we got sif_gaps externally but topics were provided, fetch SIF data anyway
if not sif_gaps:
try:
sif_gaps = await self.strategy_architect.find_semantic_gaps(
competitor_indices or []
)
except Exception as e:
logger.warning(
f"[{self.__class__.__name__}] SIF gap fetch failed (non-fatal): {e}"
)
sif_gaps = []
# Build lookup maps for cross-referencing
sif_map = {g["topic"]: g for g in sif_gaps}
# Step 2: SERP gap analysis
serp_data = await self.serp_service.analyze_topic_gaps(
topics, competitor_domains, bypass_cache=bypass_cache
)
serp_map = {}
for g in serp_data.get("gaps", []):
serp_map[g["topic"]] = g
# Step 3: Exa deep-dive (top 6 topics — paid API)
exa_data = await self.competitor_content_service.deep_dive(
topics[:6], competitor_domains, bypass_cache=bypass_cache
)
exa_map = {}
for r in exa_data.get("results", []):
exa_map[r["topic"]] = r
# Step 4: Trend momentum data
trend_surfer = TrendSurferAgent(
self.intelligence, self.user_id
)
trend_signals = await trend_surfer.surf_trends()
# Step 5: Score each topic
scored = []
for topic in topics:
scored.append(
self._score_topic(
topic=topic,
sif_map=sif_map,
serp_map=serp_map,
exa_map=exa_map,
trend_signals=trend_signals,
)
)
scored.sort(key=lambda x: x["roi_score"], reverse=True)
# Step 6: Summary
high = [g for g in scored if g["priority"] == "high"]
medium = [g for g in scored if g["priority"] == "medium"]
low = [g for g in scored if g["priority"] == "low"]
logger.info(
f"[{self.__class__.__name__}] Scored {len(scored)} gaps: "
f"{len(high)} high, {len(medium)} medium, {len(low)} low"
)
return {
"gaps": scored,
"summary": {
"total_topics_analyzed": len(topics),
"high_priority": len(high),
"medium_priority": len(medium),
"low_priority": len(low),
},
}
except Exception as e:
logger.error(
f"[{self.__class__.__name__}] Content gap radar failed: {e}"
)
logger.error(
f"[{self.__class__.__name__}] Full traceback: {traceback.format_exc()}"
)
return {"gaps": [], "summary": {}, "error": str(e)}
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""
Propose high-ROI content tasks from gap radar analysis.
Integrates with Today's Workflow agent committee polling.
"""
proposals = []
onboarding = context.get("onboarding_data", {})
competitor_focus = onboarding.get("competitor_focus", {})
competitor_domains = competitor_focus.get("top_competitor_domains", [])
if not competitor_domains:
logger.info(f"[{self.__class__.__name__}] No competitor domains in context, skipping")
return proposals
try:
result = await self.analyze(
competitor_domains=competitor_domains,
competitor_indices=[],
)
except Exception as e:
logger.error(f"[{self.__class__.__name__}] propose_daily_tasks failed: {e}")
return proposals
gaps = result.get("gaps", [])
scored = [g for g in gaps if g["priority"] in ("high", "medium")]
scored.sort(key=lambda x: x["roi_score"], reverse=True)
for gap in scored[:3]:
pillar_id = self._action_to_pillar(gap["recommended_action"])
action_url = (
"/blog-writer"
if pillar_id == "generate"
else "/seo-dashboard#content-gap-radar"
)
proposals.append(TaskProposal(
title=f"Write about: {gap['topic']}",
description=gap["recommended_action"],
pillar_id=pillar_id,
priority=gap["priority"],
estimated_time=60 if pillar_id == "generate" else 30,
source_agent="ContentGapRadarAgent",
reasoning=(
f"Content gap with {gap['scoring']['gap_size']:.0%} gap size, "
f"{gap['scoring']['volume']:.0%} volume, "
f"{gap['scoring']['trend']:.0%} trend momentum, "
f"ROI {gap['roi_score']:.0%}"
),
action_type="navigate",
action_url=action_url,
context_data={"gap": gap},
))
return proposals
@staticmethod
def _action_to_pillar(recommended_action: str) -> str:
action_lower = recommended_action.lower()
if "optimize" in action_lower:
return "analyze"
return "generate"
def _score_topic(
self,
topic: str,
sif_map: Dict[str, Any],
serp_map: Dict[str, Any],
exa_map: Dict[str, Any],
trend_signals: List[Any],
) -> Dict[str, Any]:
"""Score a single topic with the ROI formula."""
# gap_size: from SIF coverage_delta
sif = sif_map.get(topic, {})
gap_size = sif.get("coverage_delta", 0.5)
# volume: from SERP gap — competitors ranking for this topic
serp = serp_map.get(topic, {})
comp_count = serp.get("competitor_count", 0)
total_domains = serp.get("total_domains_checked", 1)
volume = min(comp_count / max(total_domains, 1), 1.0)
# trend: match topic against TrendSurfer signals
trend_score = self._match_trend_score(topic, trend_signals)
# intent: classify topic commercial value
intent = self._classify_intent(topic)
# competition: Exa content depth as penalty
exa = exa_map.get(topic, {})
content_count = exa.get("total_results", 0)
competition = min(content_count / 10.0, 1.0)
# ROI = (gap_size × volume × trend × intent) × (1 - 0.3 × competition)
base_roi = gap_size * volume * trend_score * intent
roi = base_roi * (1 - 0.3 * competition)
# Priority thresholds
if roi >= 0.6:
priority = "high"
elif roi >= 0.3:
priority = "medium"
else:
priority = "low"
# Recommended action based on scoring profile
action = self._recommend_action(gap_size, competition, intent)
return {
"topic": topic,
"roi_score": round(roi, 3),
"priority": priority,
"recommended_action": action,
"scoring": {
"gap_size": round(gap_size, 3),
"volume": round(volume, 3),
"trend": round(trend_score, 3),
"intent": round(intent, 3),
"competition": round(competition, 3),
},
"sif_gap": sif if sif else None,
"serp_evidence": {
"competitors_found": serp.get("competitors_found", []),
"competitor_count": comp_count,
"domains_with_content": serp.get("domains_with_content", []),
} if serp else None,
"competitor_content": exa if exa else None,
}
def _match_trend_score(self, topic: str, signals: List[Dict[str, Any]]) -> float:
if not signals:
return 0.5
topic_lower = topic.lower()
topic_words = set(topic_lower.split())
best_score = 0.0
for signal in signals:
impact = signal.get("impact_score", 0.5)
text_fields = " ".join(filter(None, [
signal.get("topic", ""),
signal.get("headline", ""),
signal.get("suggested_angle", ""),
]))
text_lower = text_fields.lower()
if topic_lower in text_lower:
best_score = max(best_score, impact)
text_words = set(text_lower.split())
overlap = len(topic_words & text_words)
if overlap > 0:
word_score = (overlap / max(len(topic_words), 1)) * impact
best_score = max(best_score, word_score)
return max(best_score, 0.5)
def _classify_intent(self, topic: str) -> float:
"""
Classify topic intent using LLM with keyword fallback.
Returns intent score 0.0-1.0.
"""
topic_lower = topic.lower()
# Keyword-based heuristics
commercial_words = [
"best", "top", "review", "vs", "comparison", "alternative",
"vs.", "versus", "pricing", "cost", "price", "cheap",
"affordable", "discount", "coupon", "deal", "buy",
]
transactional_words = [
"buy", "purchase", "order", "subscribe", "sign up",
"download", "get started", "free trial", "demo",
]
has_commercial = any(w in topic_lower for w in commercial_words)
has_transactional = any(w in topic_lower for w in transactional_words)
if has_transactional:
return 0.9
if has_commercial:
return 0.7
return 0.4 # Informational default
def _recommend_action(
self, gap_size: float, competition: float, intent: float
) -> str:
"""Generate a recommended action based on scoring profile."""
if gap_size > 0.7 and competition < 0.3:
return "Create comprehensive pillar page — large gap, low competition"
elif gap_size > 0.5 and intent > 0.6:
return "Create high-conversion content — significant gap, strong intent"
elif competition > 0.7:
return "Create differentiated content — high competition requires unique angle"
elif gap_size < 0.3:
return "Optimize existing content — incremental gap, update current pages"
else:
return "Create targeted blog post — moderate opportunity"
async def generate_content_brief(
self,
topic: str,
recommended_action: str,
scoring: Optional[Dict[str, float]] = None,
serp_evidence: Optional[Dict[str, Any]] = None,
sif_gap: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Generate a structured content brief from a gap item.
Uses LLM to produce title options, outline sections, target keywords,
and a writing angle. Falls back to template-based generation on LLM failure.
"""
gap_size = (scoring or {}).get("gap_size", 0.5)
volume = (scoring or {}).get("volume", 0.5)
trend = (scoring or {}).get("trend", 0.5)
intent = (scoring or {}).get("intent", 0.5)
competition = (scoring or {}).get("competition", 0.5)
word_count = 800 if competition > 0.7 else 1200 if gap_size > 0.5 else 600
serp_context = ""
if serp_evidence and serp_evidence.get("competitors_found"):
snippets = [
f"- {c.get('title','')}: {c.get('snippet','')[:100]}"
for c in serp_evidence["competitors_found"][:3]
]
serp_context = "Competitor content already ranking:\n" + "\n".join(snippets)
sif_context = ""
if sif_gap:
sif_context = (
f"SIF coverage delta: {sif_gap.get('coverage_delta', 0):.2%}, "
f"confidence: {sif_gap.get('confidence', 0):.2%}"
)
prompt = f"""You are a senior content strategist. Create a detailed content brief for the topic below.
TOPIC: {topic}
RECOMMENDED ACTION: {recommended_action}
{serp_context}
{sif_context}
Scoring profile:
- Gap size: {gap_size:.0%}
- Search volume: {volume:.0%}
- Trend momentum: {trend:.0%}
- Intent score: {intent:.0%}
- Competition level: {competition:.0%}
- Target word count: {word_count}
Return a JSON object with these exact keys:
{{
"titles": ["Title option 1", "Title option 2", "Title option 3"],
"outline": [
{{"heading": "Section heading", "key_points": ["point 1", "point 2", "point 3"]}}
],
"keywords": ["keyword1", "keyword2", "keyword3", "keyword4", "keyword5"],
"angle": "A single paragraph describing the strategic writing angle",
"word_count": {word_count}
}}
Generate 4-6 outline sections. Only return valid JSON, no other text."""
try:
response = await self._generate_llm_response(prompt)
import json as _json
start = response.find("{")
end = response.rfind("}") + 1
if start >= 0 and end > start:
brief = _json.loads(response[start:end])
else:
raise ValueError("No JSON found in LLM response")
except Exception as e:
logger.warning(
f"[{self.__class__.__name__}] LLM brief generation failed, using template: {e}"
)
brief = {
"titles": [
f"The Ultimate Guide to {topic}",
f"{topic}: Strategies That Actually Work",
f"Why {topic} Matters More Than Ever",
],
"outline": [
{"heading": f"Introduction to {topic}", "key_points": ["Context and importance", "What this guide covers"]},
{"heading": "Why This Matters", "key_points": ["Current landscape", "Key challenges and opportunities"]},
{"heading": "Key Strategies", "key_points": ["Strategy 1 with examples", "Strategy 2 with implementation tips", "Strategy 3 for advanced practitioners"]},
{"heading": "Common Pitfalls to Avoid", "key_points": ["Mistake 1 and how to avoid it", "Mistake 2 and how to avoid it"]},
{"heading": "Measuring Success", "key_points": ["Key metrics to track", "Tools and methods for measurement"]},
{"heading": "Conclusion & Next Steps", "key_points": ["Summary of key takeaways", "Actionable next steps"]},
],
"keywords": [topic] + [topic.split()[-1]] if len(topic.split()) > 1 else [topic, "guide", "strategy"],
"angle": f"Create comprehensive, actionable content about {topic} that fills the gap identified in competitor analysis. Focus on providing unique insights and practical implementation guidance.",
"word_count": word_count,
}
return {
"topic": topic,
"recommended_action": recommended_action,
"brief": brief,
"scoring": scoring,
}

View File

@@ -144,25 +144,25 @@ class CompetitorResponseAgent(BaseALwrityAgent):
proposals.append(TaskProposal(
title="Review Competitor Content",
description=f"SIF found {competitor_count} competitor pages. Review for gap opportunities.",
pillar_id="create",
pillar_id="analyze",
priority="high",
estimated_time=45,
source_agent="CompetitorResponseAgent",
reasoning="SIF-detected competitor activity presents content gap opportunities.",
action_type="navigate",
action_url="/content-planning-dashboard"
action_url="/seo-dashboard"
))
else:
proposals.append(TaskProposal(
title="Research Competitor Topics",
description="Search for competitor content in your niche to identify coverage gaps.",
pillar_id="create",
pillar_id="analyze",
priority="medium",
estimated_time=30,
source_agent="CompetitorResponseAgent",
reasoning="Understanding competitor positioning improves content strategy.",
action_type="navigate",
action_url="/content-planning-dashboard"
action_url="/seo-dashboard"
))
return proposals

View File

@@ -1,6 +1,11 @@
"""
Content Guardian Agent implementation.
Content Guardian Agent — ALwrity's committee watchdog.
Audits committee proposals, evaluates agent behaviour, flags coverage gaps,
and alerts the user when agents need correction.
"""
import json
import traceback
import asyncio
from typing import List, Dict, Any, Optional
from datetime import datetime
from loguru import logger
@@ -8,59 +13,414 @@ from .base import SIFBaseAgent, TXTAI_AVAILABLE, Agent
from services.intelligence.agents.core_agent_framework import TaskProposal
from services.intelligence.txtai_service import TxtaiIntelligenceService
class ContentGuardianAgent(SIFBaseAgent):
"""Agent for monitoring brand consistency and quality."""
def __init__(self, intelligence_service: TxtaiIntelligenceService, user_id: str, **kwargs):
# Pass kwargs to superclass to handle 'task' and other framework arguments
super().__init__(intelligence_service, user_id, agent_type="content_guardian", **kwargs)
# ── known committee agents for critique ──────────────────────────
KNOWN_AGENTS = {
"ContentStrategyAgent": {"label": "Content Strategy", "short": "Strategy", "pillar_focus": "plan"},
"StrategyArchitectAgent": {"label": "Strategy Architect", "short": "Architect", "pillar_focus": "plan"},
"SEOOptimizationAgent": {"label": "SEO Optimization", "short": "SEO", "pillar_focus": "analyze"},
"SocialAmplificationAgent":{"label": "Social Amplification","short": "Social", "pillar_focus": "engage"},
"CompetitorResponseAgent": {"label": "Competitor Response", "short": "Competitor", "pillar_focus": "analyze"},
"ContentGapRadarAgent": {"label": "Content Gap Radar", "short": "Gap Radar", "pillar_focus": "generate"},
}
PILLAR_IDS = {"plan", "generate", "publish", "analyze", "engage", "remarket"}
COMMITTEE_CYCLE_WINDOW_DAYS = 30
class ContentGuardianAgent(SIFBaseAgent):
"""Committee watchdog — audits proposals, critiques agents, flags faults, alerts users."""
CANNIBALIZATION_THRESHOLD = 0.85
ORIGINALITY_THRESHOLD = 0.75
def __init__(self, intelligence_service: TxtaiIntelligenceService, user_id: str, sif_service: Any = None, **kwargs):
super().__init__(intelligence_service, user_id, agent_type="content_guardian", **kwargs)
self.sif_service = sif_service
# ── existing utilities ────────────────────────────────────────
async def _create_txtai_agent(self):
"""Create a specialized txtai Agent for content review."""
if not TXTAI_AVAILABLE or Agent is None:
return None
try:
_llm_for_agent = getattr(self.llm, "llm", self.llm)
return Agent(
tools=[
{
"name": "brand_voice_checker",
"description": "Checks content against brand voice guidelines",
"target": self._check_brand_voice
}
],
llm=_llm_for_agent,
max_iterations=3
)
tools=[{"name": "brand_voice_checker", "description": "Checks content against brand voice guidelines", "target": self._check_brand_voice}],
llm=_llm_for_agent, max_iterations=3)
except Exception as e:
logger.error(f"Failed to create txtai agent for ContentGuardian: {e}")
raise e
logger.error(f"Failed to create txtai agent for ContentGuardian: {e}"); raise e
def _check_brand_voice(self, content: str) -> Dict[str, Any]:
"""Tool to check brand voice consistency."""
# This would use semantic search to compare against brand guidelines
return {
"consistent": True,
"score": 0.95,
"notes": "Content aligns with professional/authoritative tone."
}
return {"consistent": True, "score": 0.95, "notes": "Content aligns with professional/authoritative tone."}
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""Propose quality assurance tasks."""
proposals = []
# 1. Content Freshness Audit
proposals.append(TaskProposal(
title="Audit Old Content",
description="Review top performing posts from >6 months ago for updates.",
pillar_id="create",
priority="low",
estimated_time=30,
source_agent="ContentGuardianAgent",
reasoning="Maintains content relevance and authority.",
action_type="navigate",
action_url="/content-planning-dashboard"
))
return proposals
return [TaskProposal(title="Audit Old Content", description="Review top performing posts from >6 months ago for updates.", pillar_id="create", priority="low", estimated_time=30, source_agent="ContentGuardianAgent", reasoning="Maintains content relevance and authority.", action_type="navigate", action_url="/content-planning-dashboard")]
async def perform_site_audit(self, website_url: str) -> Dict[str, Any]:
self._log_agent_operation("Performing site audit", website_url=website_url)
try:
results = await self.intelligence.search(f"website content analysis {website_url}", limit=10)
audit: Dict[str, Any] = {"website_url": website_url, "audit_timestamp": datetime.utcnow().isoformat(), "total_pages_crawled": len(results), "content_quality": None, "brand_voice_consistency": None, "safety_issues": None, "cannibalization_issues": None}
if not results: return audit
quality_scores, style_scores, safety_flags = [], [], []
for result in results:
text = result.get("text", "") or result.get("id", "")
if len(text) < 50: continue
quality = await self.assess_content_quality({"description": text, "title": website_url}); quality_scores.append(quality.get("score", 0.0))
style = await self.style_enforcer(text); style_scores.append(style.get("compliance_score", 0.0))
safety = await self.safety_filter(text)
if not safety.get("is_safe", True): safety_flags.append(safety.get("flags", []))
audit["content_quality"] = {"score": round(sum(quality_scores)/max(len(quality_scores),1),4), "pages_analyzed": len(quality_scores)}
audit["brand_voice_consistency"] = {"compliance_score": round(sum(style_scores)/max(len(style_scores),1),4), "pages_checked": len(style_scores)}
audit["safety_issues"] = {"has_issues": len(safety_flags)>0, "flagged_pages": len(safety_flags)}
audit["cannibalization_issues"] = await self.check_cannibalization(website_url)
return audit
except Exception as e: logger.error(f"[{self.__class__.__name__}] Site audit failed: {e}"); return {"website_url": website_url, "error": str(e), "audit_timestamp": datetime.utcnow().isoformat()}
async def assess_content_quality(self, website_data: Dict[str, Any]) -> Dict[str, Any]:
self._log_agent_operation("Assessing content quality")
try:
text = website_data.get('description','') or website_data.get('title','')
if not text: return {"score":0.5,"reason":"No content to analyze"}
style = await self.style_enforcer(text); safety = await self.safety_filter(text)
base = style.get('compliance_score',0.8)
if safety.get('action')=='flag_for_review': base*=0.5
return {"score":base,"style_analysis":style,"safety_analysis":safety,"analyzed_text_length":len(text)}
except Exception as e: return {"score":0.0,"error":str(e)}
async def check_cannibalization(self, new_draft: str) -> Dict[str, Any]:
self._log_agent_operation("Checking for semantic cannibalization", draft_length=len(new_draft))
try:
if not await self._ensure_intelligence_ready(): return {"warning":False,"error":"Service not initialized"}
if not new_draft or len(new_draft.strip())<50: return {"warning":False,"reason":"Draft too short"}
results = await self.intelligence.search(new_draft, limit=1)
if not results: return {"warning":False,"uniqueness_score":1.0}
score = results[0].get('score',0.0)
if score > self.CANNIBALIZATION_THRESHOLD: return {"warning":True,"similar_to":results[0].get('id','unknown'),"score":score,"threshold":self.CANNIBALIZATION_THRESHOLD,"recommendation":"Consider revising the draft to target a different angle or merge with existing content"}
return {"warning":False,"uniqueness_score":1.0-score}
except Exception as e: return {"warning":False,"error":str(e)}
async def verify_originality(self, text: str, competitor_index: Any) -> Dict[str, Any]:
"""(unchanged — kept for backward compat)"""
self._log_agent_operation("Verifying originality against competitors", text_length=len(text))
try:
if not text or len(text.strip())<50: return {"originality_score":0.0,"reason":"Text too short"}
query = text.strip(); competitor_results = []; method="user_index_competitor_filter"
if competitor_index is not None and hasattr(competitor_index,"search"):
method="competitor_index_search"; raw=competitor_index.search(query,limit=5)
if asyncio.iscoroutine(raw): raw=await raw
competitor_results=raw or []
else:
raw=await self.intelligence.search(query,limit=10)
for r in raw or []:
m_raw=r.get("object"); m=m_raw if isinstance(m_raw,dict) else {}
if not m and isinstance(m_raw,str):
try: m=json.loads(m_raw)
except Exception: m={}
if "competitor" in str(m.get("type","")).lower() or "competitor" in str(m.get("source","")).lower():
competitor_results.append(r)
if not competitor_results: return {"originality_score":1.0,"confidence":0.6,"method":method,"notes":"No competitor overlap detected"}
top=max(competitor_results,key=lambda i:float(i.get("score",0.0))); s=max(0.0,min(1.0,float(top.get("score",0.0))))
os_=max(0.0,round(1.0-s,4)); c=round(min(1.0,0.55+(min(len(competitor_results),5)*0.07)),3)
return {"originality_score":os_,"confidence":c,"method":method,"warning":os_<self.ORIGINALITY_THRESHOLD,"threshold":self.ORIGINALITY_THRESHOLD,"top_competitor_match":{"id":top.get("id"),"score":round(s,4)},"matches_evaluated":len(competitor_results)}
except Exception as e: return {"originality_score":0.0,"error":str(e)}
async def style_enforcer(self, text: str, style_guidelines: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
self._log_agent_operation("Enforcing style guidelines", text_length=len(text))
try:
if not text: return {"compliance_score":0.0,"issues":["No text provided"]}
if not style_guidelines and self.sif_service:
try:
r=await self.intelligence.search("website analysis brand voice style",limit=1)
if r:
m_raw=r[0].get('object'); m=json.loads(m_raw) if isinstance(m_raw,str) else (m_raw or r[0])
if m.get('type')=='website_analysis':
rep=m.get('full_report',{}); style_guidelines={"tone":rep.get('brand_analysis',{}).get('brand_voice','neutral'),"style_patterns":rep.get('style_patterns',{}),"writing_style":rep.get('writing_style',{})}
except Exception: pass
issues=[]; score=1.0
tone=(style_guidelines or {}).get('tone','').lower()
if 'formal' in tone or 'professional' in tone:
found=[c for c in ["can't","won't","don't","it's"] if c in text.lower()]
if found: issues.append(f"Found contractions in formal text: {', '.join(found[:3])}..."); score-=0.1
sentences=text.split('.'); avg=sum(len(s.split()) for s in sentences if s)/max(1,len(sentences))
if avg>25: issues.append("Average sentence length is too high (>25 words). Consider shortening."); score-=0.1
return {"compliance_score":max(0.0,score),"issues":issues,"is_compliant":score>0.8,"guidelines_source":"sif_index" if not style_guidelines and self.sif_service else "provided"}
except Exception as e: return {"error":str(e)}
async def safety_filter(self, text: str) -> Dict[str, Any]:
self._log_agent_operation("Running safety filter", text_length=len(text))
try:
kw=["hate","kill","murder","attack","destroy","scam","fraud","steal","explicit","adult"]
found=[k for k in kw if f" {k} " in text.lower()]
ok=len(found)==0
return {"is_safe":ok,"flags":found,"safety_score":1.0 if ok else 0.0,"action":"approve" if ok else "flag_for_review"}
except Exception as e: return {"error":str(e)}
# ═══════════════════════════════════════════════════════════════
# COMMITTEE WATCHDOG — the core audit entry point
# ═══════════════════════════════════════════════════════════════
async def audit_committee(self, proposals: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Audits a batch of committee proposals and returns a structured report.
proposals: list of dicts with at minimum:
agent, title, pillar_id, priority, reasoning, accepted, valid
"""
if not proposals:
return {
"health_score": 0, "verdict": "No proposals received from any agent",
"agent_critiques": [], "coverage_gaps": [], "overlaps": [],
"alerts": []
}
by_agent: Dict[str, List[Dict]] = {}
for p in proposals:
by_agent.setdefault(p.get("agent", "unknown"), []).append(p)
# 1. Critique each agent
agent_critiques = []
for agent_name, agent_props in sorted(by_agent.items()):
critique = self._critique_agent(agent_name, agent_props)
agent_critiques.append(critique)
# 2. Coverage check
coverage_gaps = self._find_coverage_gaps(proposals)
overstuffed = self._find_overstuffed_pillars(proposals)
# 3. Overlap detection
overlaps = self._find_overlaps(proposals)
# 4. Overall health score
health_score = self._compute_health_score(agent_critiques, coverage_gaps, overlaps)
# 5. Generate actionable alerts
alerts = self._generate_alerts(agent_critiques, coverage_gaps, overlaps)
verdict = self._verdict_text(health_score, agent_critiques, coverage_gaps)
return {
"health_score": health_score,
"verdict": verdict,
"agent_critiques": agent_critiques,
"coverage_gaps": coverage_gaps,
"overstuffed_pillars": overstuffed,
"overlaps": overlaps,
"alerts": alerts,
"audit_timestamp": datetime.utcnow().isoformat(),
}
# ── agent critique ────────────────────────────────────────────
def _critique_agent(self, agent_name: str, proposals: List[Dict]) -> Dict[str, Any]:
info = KNOWN_AGENTS.get(agent_name, {"label": agent_name, "short": agent_name[:6], "pillar_focus": None})
total = len(proposals)
accepted = sum(1 for p in proposals if p.get("accepted"))
rejected = total - accepted
acceptance_rate = accepted / total if total > 0 else 0
weak_reasoning = []
poor_priority = []
off_pillar = []
for p in proposals:
# Reasoning quality
reason = (p.get("reasoning") or "").strip()
r_score = self._reasoning_score(reason)
if r_score < 0.5:
weak_reasoning.append({"title": p.get("title",""), "reasoning": reason, "score": r_score})
# Priority appropriateness
pr = (p.get("priority") or "").lower()
if info["pillar_focus"] and pr == "low" and p.get("pillar_id") == info["pillar_focus"]:
poor_priority.append({"title": p.get("title",""), "pillar": p.get("pillar_id",""), "priority": pr,
"note": f"Pillar '{info['pillar_focus']}' is {info['label']}'s core — low priority seems wrong"})
# Pillar relevance
if info["pillar_focus"] and p.get("pillar_id") and p["pillar_id"] != info["pillar_focus"]:
off_pillar.append({"title": p.get("title",""), "proposed_pillar": p.get("pillar_id",""),
"expected_pillar": info["pillar_focus"],
"note": f"'{info['label']}' proposed for '{p['pillar_id']}' pillar but typically operates in '{info['pillar_focus']}'"})
issues = []
if weak_reasoning:
issues.append({"type": "weak_reasoning", "severity": "warning", "count": len(weak_reasoning),
"summary": f"{len(weak_reasoning)} proposal(s) with vague or empty reasoning",
"details": weak_reasoning,
"action_label": "Improve reasoning", "action_url": None})
if poor_priority:
issues.append({"type": "poor_priority", "severity": "warning", "count": len(poor_priority),
"summary": f"{len(poor_priority)} proposal(s) under-prioritised for core pillar",
"details": poor_priority,
"action_label": "Review priorities", "action_url": None})
if off_pillar:
issues.append({"type": "off_pillar", "severity": "info", "count": len(off_pillar),
"summary": f"{len(off_pillar)} proposal(s) outside usual pillar",
"details": off_pillar,
"action_label": "Review pillar assignment", "action_url": None})
if rejected > 0:
issues.append({"type": "rejected_proposals", "severity": "error" if acceptance_rate < 0.3 else "warning",
"count": rejected,
"summary": f"{rejected} proposal(s) rejected by committee" if rejected > 0 else "",
"details": [{"title": p.get("title",""), "reason": p.get("rejected_reason","no reason")} for p in proposals if not p.get("accepted")],
"action_label": "Review rejections", "action_url": None})
# Agent score (0-100)
score = 100
if weak_reasoning: score -= len(weak_reasoning) * 15
if poor_priority: score -= len(poor_priority) * 10
if acceptance_rate < 0.3: score -= 20
if acceptance_rate == 0: score = max(0, score - 30)
score = max(0, min(100, score))
health = "good" if score >= 80 else "warning" if score >= 50 else "failing"
return {
"agent": agent_name,
"label": info["label"],
"short": info["short"],
"score": score,
"health": health,
"total_proposals": total,
"accepted": accepted,
"rejected": rejected,
"acceptance_rate": round(acceptance_rate, 2),
"issues": issues,
"summary": self._agent_summary(health, score, accepted, total, weak_reasoning, poor_priority),
}
# ── reasoning quality ─────────────────────────────────────────
def _reasoning_score(self, reasoning: str) -> float:
if not reasoning or len(reasoning) < 10:
return 0.0
# Short = weak
if len(reasoning) < 25:
return 0.2
if len(reasoning) < 50:
return 0.4
# Has specifics
specifics = ["because", "since", "based on", "data", "metric", "trend", "observed",
"target", "audience", "competitor", "gap", "opportunity", "improve",
"increase", "reduce", "goal", "kpi", "score", "result"]
found = sum(1 for s in specifics if s in reasoning.lower())
base = min(1.0, 0.4 + found * 0.1)
# Length bonus
if len(reasoning) > 100:
base = min(1.0, base + 0.15)
return min(1.0, base)
# ── coverage ──────────────────────────────────────────────────
def _find_coverage_gaps(self, proposals: List[Dict]) -> List[Dict]:
covered = set()
for p in proposals:
pid = p.get("pillar_id")
if pid and pid in PILLAR_IDS:
covered.add(pid)
gaps = []
for pid in sorted(PILLAR_IDS):
if pid not in covered:
gaps.append({"pillar_id": pid, "severity": "warning",
"summary": f"Pillar '{pid}' has no proposals from any agent",
"action_label": "Add task", "action_url": None})
return gaps
def _find_overstuffed_pillars(self, proposals: List[Dict]) -> List[Dict]:
counts: Dict[str, int] = {}
for p in proposals:
pid = p.get("pillar_id")
if pid and pid in PILLAR_IDS:
counts[pid] = counts.get(pid, 0) + 1
total = len(proposals)
overstuffed = []
for pid, count in sorted(counts.items()):
if total > 0 and count / total > 0.5:
overstuffed.append({"pillar_id": pid, "count": count, "total": total,
"severity": "info",
"summary": f"Pillar '{pid}' has {count}/{total} proposals ({count/total*100:.0f}%) — may be over-represented",
"action_label": None, "action_url": None})
return overstuffed
# ── overlap detection ─────────────────────────────────────────
def _find_overlaps(self, proposals: List[Dict]) -> List[Dict]:
overlaps = []
by_title: Dict[str, List[Dict]] = {}
for p in proposals:
t = (p.get("title") or "").strip().lower()
by_title.setdefault(t, []).append(p)
for title, dups in by_title.items():
if len(dups) > 1 and title:
agents = [d.get("agent","?") for d in dups]
overlaps.append({"title": dups[0].get("title",""), "pillar": dups[0].get("pillar_id",""),
"agents": agents, "count": len(dups),
"severity": "warning",
"summary": f"{len(dups)} agents proposed '{dups[0].get('title','')}': {', '.join(agents)}",
"action_label": "Resolve conflict", "action_url": None})
return overlaps
# ── health ────────────────────────────────────────────────────
def _compute_health_score(self, critiques: List[Dict], gaps: List[Dict], overlaps: List[Dict]) -> int:
score = 100
for c in critiques:
if c["health"] == "failing": score -= 15
elif c["health"] == "warning": score -= 8
score -= len(gaps) * 10
score -= len(overlaps) * 5
return max(0, min(100, score))
def _verdict_text(self, health: int, critiques: List[Dict], gaps: List[Dict]) -> str:
if health >= 90:
return "Committee is performing well — all agents submitting quality proposals with good coverage."
failing = [c for c in critiques if c["health"] == "failing"]
warning = [c for c in critiques if c["health"] == "warning"]
parts = []
if failing:
parts.append(f"{len(failing)} agent(s) need attention: {', '.join(c['label'] for c in failing)}")
if warning:
parts.append(f"{len(warning)} agent(s) showing issues: {', '.join(c['label'] for c in warning)}")
if gaps:
parts.append(f"Missing coverage: {', '.join(g['pillar_id'] for g in gaps)}")
if not parts:
parts.append("Minor issues detected — monitoring.")
return "".join(parts)
def _agent_summary(self, health: str, score: int, accepted: int, total: int, weak: List, poor: List) -> str:
if health == "failing":
return f"Score {score}/100 — {accepted}/{total} accepted, {len(weak)} weak reasoning, {len(poor)} under-prioritised"
if health == "warning":
return f"Score {score}/100 — {accepted}/{total} accepted, {len(weak)} weak reasoning"
return f"Score {score}/100 — {accepted}/{total} accepted"
# ── alerts ────────────────────────────────────────────────────
def _generate_alerts(self, critiques: List[Dict], gaps: List[Dict], overlaps: List[Dict]) -> List[Dict]:
alerts = []
for c in critiques:
if c["health"] == "failing":
alerts.append({
"type": "agent_failing", "severity": "error",
"agent": c["agent"], "label": c["label"],
"title": f"{c['label']} needs attention",
"message": c["summary"],
"cta_path": None,
})
for issue in c.get("issues", []):
if issue["type"] == "weak_reasoning" and issue["count"] >= 3:
alerts.append({
"type": "weak_reasoning", "severity": "warning",
"agent": c["agent"], "label": c["label"],
"title": f"{c['label']}: {issue['count']} proposals with weak reasoning",
"message": issue["summary"],
"cta_path": None,
})
for g in gaps:
alerts.append({
"type": "coverage_gap", "severity": "warning",
"agent": None, "label": None,
"title": f"Coverage gap: pillar '{g['pillar_id']}'",
"message": g["summary"],
"cta_path": None,
})
for o in overlaps:
alerts.append({
"type": "proposal_overlap", "severity": "warning",
"agent": None, "label": None,
"title": f"Duplicate proposal: '{o['title']}'",
"message": o["summary"],
"cta_path": None,
})
return alerts

View File

@@ -294,21 +294,95 @@ class ContentStrategyAgent(BaseALwrityAgent):
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""
Propose strategic tasks based on content analysis.
Propose strategic tasks based on user onboarding context.
Derives content pillars, industry, and competitor info to
generate personalized daily content suggestions.
"""
proposals = []
# 1. Content Refresh
onboarding = context.get("onboarding_data", {})
if not isinstance(onboarding, dict):
return proposals
# Extract user profile hints from onboarding data
industry = ""
content_pillars = []
competitor_domains = []
try:
cp = onboarding.get("core_persona") or {}
if isinstance(cp, dict):
industry = str(cp.get("industry") or cp.get("company_type") or "")
step2 = onboarding.get("step2_summary") or onboarding.get("industry_context") or {}
if isinstance(step2, dict):
content_pillars = (
step2.get("content_pillars")
or step2.get("topics")
or onboarding.get("content_pillars")
or []
)
cf = onboarding.get("competitor_focus") or {}
if isinstance(cf, dict):
competitor_domains = cf.get("top_competitor_domains") or []
except Exception:
pass
# Task 1: Create content for a key pillar (generate)
if content_pillars:
pillar_topic = content_pillars[0] if isinstance(content_pillars[0], str) else (
content_pillars[0].get("topic") or content_pillars[0].get("name") or "your audience"
)
proposals.append(TaskProposal(
title=f"Create content for '{pillar_topic}'",
description=f"Write a blog post or social content around your {pillar_topic} content pillar.",
pillar_id="generate",
priority="high",
estimated_time=45,
source_agent="ContentStrategyAgent",
reasoning=f"'{pillar_topic}' is a core content pillar in your strategy. Regular publishing keeps your topical authority growing.",
action_type="navigate",
action_url="/blog-writer",
context_data={"pillar_topic": pillar_topic, "industry": industry},
))
else:
proposals.append(TaskProposal(
title="Define your content pillars",
description="Set up your core content topics to get personalized daily suggestions.",
pillar_id="plan",
priority="high",
estimated_time=20,
source_agent="ContentStrategyAgent",
reasoning="Content pillars drive every other task in your workflow. Defining them unlocks the full agent committee.",
action_type="navigate",
action_url="/content-planning-dashboard",
))
# Task 2: Competitor content review (analyze)
if competitor_domains:
domain = competitor_domains[0]
proposals.append(TaskProposal(
title=f"Review competitor: {domain}",
description=f"Analyze recently published content from {domain} to find gaps and opportunities.",
pillar_id="analyze",
priority="medium",
estimated_time=25,
source_agent="ContentStrategyAgent",
reasoning=f"{domain} is your top tracked competitor. Regular reviews help you stay ahead of their content strategy moves.",
action_type="navigate",
action_url="/seo-dashboard",
context_data={"competitor_domain": domain},
))
# Task 3: Content audit (analyze) — always suggested
proposals.append(TaskProposal(
title="Refresh 'SEO Basics'",
description="Update your SEO basics guide with 2024 trends.",
pillar_id="create",
priority="high",
estimated_time=45,
title="Quick content performance audit",
description="Review your top 3 pieces from last month. Identify what worked and what to update.",
pillar_id="analyze",
priority="medium",
estimated_time=20,
source_agent="ContentStrategyAgent",
reasoning="Declining traffic and outdated references.",
reasoning="Regular audits surface declining pages that need refreshing and winning formats to double down on.",
action_type="navigate",
action_url="/content-planning-dashboard"
action_url="/content-planning-dashboard",
))
return proposals

View File

@@ -168,25 +168,25 @@ class SEOOptimizationAgent(BaseALwrityAgent):
proposals.append(TaskProposal(
title="Review SEO Issues",
description=f"SIF indexed content suggests {issues_found} areas that may need SEO attention.",
pillar_id="distribute",
pillar_id="analyze",
priority="high",
estimated_time=30,
source_agent="SEOOptimizationAgent",
reasoning="Addressing SEO gaps improves organic visibility.",
action_type="navigate",
action_url="/content-planning-dashboard"
action_url="/seo-dashboard"
))
else:
proposals.append(TaskProposal(
title="Run SEO Audit",
description="Perform a comprehensive SEO audit to identify optimization opportunities.",
pillar_id="distribute",
pillar_id="analyze",
priority="medium",
estimated_time=15,
source_agent="SEOOptimizationAgent",
reasoning="Regular audits prevent SEO degradation.",
action_type="navigate",
action_url="/content-planning-dashboard"
action_url="/seo-dashboard"
))
return proposals

View File

@@ -126,21 +126,85 @@ class SocialAmplificationAgent(BaseALwrityAgent):
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""
Propose social media tasks.
Propose social media tasks based on user's onboarding context.
Derives platforms and content types from user data.
"""
proposals = []
# 1. Social Post Creation
onboarding = context.get("onboarding_data", {})
if not isinstance(onboarding, dict):
return proposals
# Extract selected platforms from onboarding step 5
selected_platforms = []
try:
step5 = onboarding.get("step5_summary") or onboarding.get("distribution_channels") or {}
if isinstance(step5, dict):
sp = step5.get("selected_platforms") or step5.get("platforms") or []
selected_platforms = [p for p in sp if isinstance(p, str)]
if not selected_platforms:
# Fallback: check top-level keys
for key in ("selected_platforms", "platforms", "social_platforms"):
val = onboarding.get(key)
if isinstance(val, list):
selected_platforms = [p for p in val if isinstance(p, str)]
break
except Exception:
pass
platform_urls = {
"linkedin": "/linkedin-writer",
"facebook": "/facebook-writer",
"twitter": "/linkedin-writer", # no dedicated twitter writer, use linkedin as fallback
"instagram": "/linkedin-writer",
"tiktok": "/linkedin-writer",
"youtube": "/linkedin-writer",
}
target_platforms = [p for p in selected_platforms if p.lower() in platform_urls]
if not target_platforms:
# No known platforms configured — generic engage task
proposals.append(TaskProposal(
title="Share content on social media",
description="Promote your latest published piece across your social channels.",
pillar_id="engage",
priority="medium",
estimated_time=20,
source_agent="SocialAmplificationAgent",
reasoning="Social distribution drives referral traffic and builds audience engagement.",
action_type="navigate",
action_url="/linkedin-writer",
))
return proposals
platform = target_platforms[0]
platform_label = platform.capitalize()
proposals.append(TaskProposal(
title="Create LinkedIn Thread",
description="Summarize your latest blog post into a 5-tweet thread.",
pillar_id="distribute",
title=f"Share content on {platform_label}",
description=f"Adapt and publish your latest content as a {platform_label} post to drive engagement.",
pillar_id="engage",
priority="medium",
estimated_time=20,
source_agent="SocialAmplificationAgent",
reasoning="Repurpose existing content.",
reasoning=f"Consistent {platform_label} posting maintains audience engagement and extends content reach.",
action_type="navigate",
action_url="/content-planning-dashboard"
action_url=platform_urls[platform.lower()],
context_data={"platform": platform.lower()},
))
if len(target_platforms) > 1:
platform2 = target_platforms[1]
proposals.append(TaskProposal(
title=f"Cross-post to {platform2.capitalize()}",
description=f"Repurpose your latest content for your {platform2.capitalize()} audience.",
pillar_id="engage",
priority="low",
estimated_time=15,
source_agent="SocialAmplificationAgent",
reasoning=f"Cross-posting to {platform2.capitalize()} increases reach without additional content creation cost.",
action_type="navigate",
action_url=platform_urls[platform2.lower()],
context_data={"platform": platform2.lower()},
))
return proposals