feat: Implement Today's Workflow and Agent Huddle enhancements

This commit is contained in:
ajaysi
2026-03-01 20:15:31 +05:30
parent 62d9c2e836
commit f8f7ddeb2a
25 changed files with 1852 additions and 272 deletions

View File

@@ -21,6 +21,7 @@ router = APIRouter()
@router.get("/dashboard/{user_id}")
async def get_dashboard_data(
user_id: str,
billing_period: str = None,
db: Session = Depends(get_db)
) -> Dict[str, Any]:
"""Get comprehensive dashboard data for usage monitoring."""
@@ -29,16 +30,17 @@ async def get_dashboard_data(
ensure_subscription_plan_columns(db)
ensure_usage_summaries_columns(db)
# Check cache first
cached_data = get_cached_dashboard(user_id)
if cached_data:
return cached_data
# Check cache first (skip if billing_period is specified)
if not billing_period:
cached_data = get_cached_dashboard(user_id)
if cached_data:
return cached_data
usage_service = UsageTrackingService(db)
pricing_service = PricingService(db)
# Get current usage stats
current_usage = usage_service.get_user_usage_stats(user_id)
# Get current usage stats (for the requested period)
current_usage = usage_service.get_user_usage_stats(user_id, billing_period)
# Get usage trends (last 6 months)
trends = usage_service.get_usage_trends(user_id, 6)
@@ -47,10 +49,14 @@ async def get_dashboard_data(
limits = pricing_service.get_user_limits(user_id)
# Get unread alerts
alerts = db.query(UsageAlert).filter(
alerts_query = db.query(UsageAlert).filter(
UsageAlert.user_id == user_id,
UsageAlert.is_read == False
).order_by(UsageAlert.created_at.desc()).limit(5).all()
)
if billing_period:
alerts_query = alerts_query.filter(UsageAlert.billing_period == billing_period)
alerts = alerts_query.order_by(UsageAlert.created_at.desc()).limit(5).all()
alerts_data = [
{
@@ -64,11 +70,17 @@ async def get_dashboard_data(
for alert in alerts
]
# Calculate cost projections
# Calculate cost projections (only relevant for current month)
current_cost = current_usage.get('total_cost', 0)
days_in_period = 30
current_day = datetime.now().day
projected_cost = (current_cost / current_day) * days_in_period if current_day > 0 else 0
# Only project costs if viewing current month
is_current_month = not billing_period or billing_period == datetime.now().strftime("%Y-%m")
if is_current_month:
projected_cost = (current_cost / current_day) * days_in_period if current_day > 0 else 0
else:
projected_cost = current_cost # For past months, projected is actual
response_payload = {
"success": True,
@@ -91,8 +103,10 @@ async def get_dashboard_data(
}
}
# Cache the response
set_cached_dashboard(user_id, response_payload)
# Cache the response only for default view
if not billing_period:
set_cached_dashboard(user_id, response_payload)
return response_payload
except (sqlite3.OperationalError, Exception) as e:

View File

@@ -48,7 +48,7 @@ async def get_today_workflow(
db: Session = Depends(get_db),
) -> Dict[str, Any]:
user_id = str(current_user.get("id"))
plan, created = get_or_create_daily_workflow_plan(db, user_id, date=date)
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date)
tasks = (
db.query(DailyWorkflowTask)
@@ -154,6 +154,8 @@ async def get_today_workflow(
}
from services.task_memory_service import TaskMemoryService
@router.post("/tasks/{task_id}/status")
async def set_task_status(
task_id: int,
@@ -171,6 +173,17 @@ async def set_task_status(
if not task:
raise HTTPException(status_code=404, detail="Task not found")
# Record outcome in memory for self-learning
try:
memory = TaskMemoryService(user_id, db)
await memory.record_task_outcome(
task,
feedback_score=1 if status == "completed" else -1 if status == "dismissed" else 0,
feedback_text=completion_notes
)
except Exception as e:
pass # Don't block response on memory update failure
plan_for_date = db.query(DailyWorkflowPlan).filter(DailyWorkflowPlan.id == task.plan_id).first()
plan_date = plan_for_date.date if plan_for_date and plan_for_date.date else ""
task_payload = {

View File

@@ -46,4 +46,27 @@ class DailyWorkflowTask(Base):
plan = relationship("DailyWorkflowPlan", back_populates="tasks")
class TaskHistory(Base):
"""
Tracks historical tasks for self-learning.
Used by TaskMemoryService to prevent redundant suggestions and learn from rejections.
"""
__tablename__ = "task_history"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String(255), nullable=False, index=True)
task_hash = Column(String(64), nullable=False, index=True) # Hash of title + description
title = Column(String(255), nullable=False)
description = Column(Text, nullable=False)
pillar_id = Column(String(30), nullable=False)
status = Column(String(30), nullable=False) # completed, dismissed, rejected
source_agent = Column(String(50), nullable=True)
feedback_score = Column(Integer, nullable=True) # -1 (bad), 0 (neutral), 1 (good)
feedback_text = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
# Metadata for vector index linking
vector_id = Column(String(36), nullable=True)
Index("ix_daily_workflow_plans_user_date", DailyWorkflowPlan.user_id, DailyWorkflowPlan.date, unique=True)
Index("ix_task_history_user_hash", TaskHistory.user_id, TaskHistory.task_hash)

View File

@@ -0,0 +1,52 @@
import sys
import os
from sqlalchemy import text
# Add backend to sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from services.database import get_session_for_user
from models.subscription_models import APIUsageLog, UsageSummary
USER_ID = "user_33Gz1FPI86VDXhRY8QN4ragRFGN"
def debug_user():
print(f"Checking usage for user: {USER_ID}")
try:
db = get_session_for_user(USER_ID)
if not db:
print("Could not get DB session.")
return
# 1. Check UsageSummary
print("\n--- UsageSummary ---")
summaries = db.query(UsageSummary).all()
for s in summaries:
print(f"Period: {s.billing_period}, Calls: {s.total_calls}, Cost: {s.total_cost}, Status: {s.usage_status}")
# 2. Check APIUsageLog
print("\n--- APIUsageLog Stats ---")
# Count logs
count = db.query(APIUsageLog).count()
print(f"Total Logs: {count}")
# Group by billing period
try:
logs_by_period = db.execute(text("SELECT billing_period, COUNT(*), SUM(cost_total) FROM api_usage_logs GROUP BY billing_period")).fetchall()
for row in logs_by_period:
print(f"Period: {row[0]}, Count: {row[1]}, Sum Cost: {row[2]}")
except Exception as e:
print(f"Error querying logs group by: {e}")
# 3. Check specific provider logs (to see if they are 'gemini' or 'GEMINI')
print("\n--- Provider Check (First 5 logs) ---")
logs = db.query(APIUsageLog).limit(5).all()
for l in logs:
print(f"ID: {l.id}, Provider: {l.provider}, Actual: {l.actual_provider_name}, Cost: {l.cost_total}, Period: {l.billing_period}")
db.close()
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
debug_user()

View File

@@ -53,11 +53,20 @@ class GSCAnalyticsHandler(BaseAnalyticsHandler):
logger.info("Fetching fresh GSC analytics for user {user_id}", user_id=user_id)
try:
# Get user's sites
sites = self.gsc_service.get_site_list(user_id)
logger.info(f"GSC Sites found for user {user_id}: {sites}")
try:
sites = self.gsc_service.get_site_list(user_id)
except Exception as e:
logger.warning(f"GSC site list fetch failed for user {user_id}: {e}")
sites = []
# logger.info(f"GSC Sites found for user {user_id}: {sites}")
if not sites:
logger.warning(f"No GSC sites found for user {user_id}")
return self.create_error_response('No GSC sites found')
# logger.warning(f"No GSC sites found for user {user_id}")
# Return standard empty response instead of error to avoid logs noise
return self.create_success_response(
metrics={"clicks": 0, "impressions": 0, "ctr": 0, "position": 0},
date_range={'start': start_date, 'end': end_date}
)
# Select site: Prefer target_url match, otherwise first site
selected_site = sites[0]
@@ -125,7 +134,7 @@ class GSCAnalyticsHandler(BaseAnalyticsHandler):
'error': None
}
except Exception as e:
self.log_analytics_error(user_id, "get_connection_status", e)
# self.log_analytics_error(user_id, "get_connection_status", e)
return {
'connected': False,
'sites_count': 0,

View File

@@ -366,17 +366,24 @@ class GSCService:
service = self.get_authenticated_service(user_id)
except ValueError:
# User not connected or credentials invalid
logger.warning(f"User {user_id} not connected to GSC. Returning empty site list.")
# logger.warning(f"User {user_id} not connected to GSC. Returning empty site list.")
return []
except Exception as e:
logger.warning(f"Failed to get authenticated service for {user_id}: {e}")
return []
if not service:
return []
sites = service.sites().list().execute()
site_list = []
for site in sites.get('siteEntry', []):
site_list.append({
'siteUrl': site.get('siteUrl'),
'permissionLevel': site.get('permissionLevel')
})
if 'siteEntry' in sites:
for site in sites.get('siteEntry', []):
site_list.append({
'siteUrl': site.get('siteUrl'),
'permissionLevel': site.get('permissionLevel')
})
logger.info(f"Retrieved {len(site_list)} sites for user: {user_id}")
return site_list

View File

@@ -155,7 +155,7 @@ def track_agent_usage_sync(user_id: str, model_name: str, prompt: str, response_
db.execute(log_query, {
'user_id': user_id,
'provider': provider_enum.name, # Use name (GEMINI) not value (gemini) for SQLAlchemy Enum
'provider': provider_enum.value, # Use value (gemini) not name (GEMINI) for consistency
'endpoint': 'agent_action',
'method': 'GENERATE',
'model_used': model_name,

View File

@@ -107,6 +107,20 @@ class AgentAction:
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
@dataclass
class TaskProposal:
"""Represents a daily task proposed by an agent"""
title: str
description: str
pillar_id: str # plan, generate, publish, analyze, engage, remarket
priority: str # high, medium, low
estimated_time: int # minutes
source_agent: str
reasoning: str
context_data: Optional[Dict[str, Any]] = None
action_type: str = "navigate"
action_url: Optional[str] = None
@dataclass
class MarketSignal:
"""Represents a market change or opportunity"""
@@ -833,6 +847,13 @@ class BaseALwrityAgent(ABC):
self.performance.success_rate = (
self.performance.successful_actions / self.performance.total_actions
)
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""
Propose daily tasks based on the agent's domain and context.
Must be implemented by specialized agents.
"""
return []
# Calculate efficiency score (0.0 to 1.0)
# Based on success rate and response time

View File

@@ -11,7 +11,7 @@ from typing import List, Dict, Any, Optional
from datetime import datetime
from loguru import logger
from ..txtai_service import TxtaiIntelligenceService
from services.intelligence.agents.core_agent_framework import BaseALwrityAgent, AgentAction
from services.intelligence.agents.core_agent_framework import BaseALwrityAgent, AgentAction, TaskProposal
from services.seo_tools.content_strategy_service import ContentStrategyService
from services.analytics import PlatformAnalyticsService
from services.intelligence.sif_agents import SharedLLMWrapper, LocalLLMWrapper
@@ -122,6 +122,56 @@ class StrategyArchitectAgent(SIFBaseAgent):
# Simple confidence based on cluster size - larger clusters are more reliable
return min(1.0, len(cluster_indices) / 10.0)
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""Propose PLAN pillar tasks based on semantic analysis."""
proposals = []
# 1. Pillar Health Check
try:
# We use a shorter timeout or cached check if possible, but discover_pillars is fairly fast
pillars = await self.discover_pillars()
if not pillars:
proposals.append(TaskProposal(
title="Establish Content Pillars",
description="Your content strategy lacks defined pillars. Let's analyze your niche to find core topics.",
pillar_id="plan",
priority="high",
estimated_time=15,
source_agent="StrategyArchitectAgent",
reasoning="No content pillars detected via SIF clustering.",
action_type="navigate",
action_url="/content-planning-dashboard"
))
elif len(pillars) < 3:
proposals.append(TaskProposal(
title="Expand Content Pillars",
description=f"You only have {len(pillars)} active pillars. Consider diversifying your strategy.",
pillar_id="plan",
priority="medium",
estimated_time=20,
source_agent="StrategyArchitectAgent",
reasoning=f"Low pillar diversity ({len(pillars)} detected).",
action_type="navigate",
action_url="/content-planning-dashboard"
))
except Exception as e:
logger.warning(f"[{self.__class__.__name__}] Error checking pillars for proposals: {e}")
# 2. Strategy Review (Generic fallback)
proposals.append(TaskProposal(
title="Review Strategic Goals",
description="Ensure your content output aligns with your quarterly business goals.",
pillar_id="plan",
priority="low",
estimated_time=10,
source_agent="StrategyArchitectAgent",
reasoning="Routine strategy maintenance.",
action_type="navigate",
action_url="/content-planning-dashboard"
))
return proposals
async def find_semantic_gaps(self, competitor_indices: List[int]) -> List[Dict[str, Any]]:
"""Compare user content vs competitor content to find missing topics."""
self._log_agent_operation("Finding semantic content gaps", competitor_count=len(competitor_indices))
@@ -856,6 +906,38 @@ class ContentStrategyAgent(BaseALwrityAgent):
self.sif_service = SIFIntegrationService(user_id)
except Exception as e:
logger.warning(f"Failed to initialize SIF service for ContentStrategyAgent: {e}")
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""Propose GENERATE pillar tasks."""
proposals = []
# 1. Content Gap Analysis
proposals.append(TaskProposal(
title="Analyze Content Gaps",
description="Identify missing topics in your strategy compared to competitors.",
pillar_id="generate",
priority="high",
estimated_time=30,
source_agent="ContentStrategyAgent",
reasoning="Regular gap analysis ensures competitive relevance.",
action_type="navigate",
action_url="/content-planning-dashboard"
))
# 2. Draft New Content
proposals.append(TaskProposal(
title="Draft New Blog Post",
description="Create a new article targeting your primary keywords.",
pillar_id="generate",
priority="medium",
estimated_time=45,
source_agent="ContentStrategyAgent",
reasoning="Maintain publishing consistency.",
action_type="navigate",
action_url="/blog-writer"
))
return proposals
def _create_txtai_agent(self) -> Agent:
"""Create Content Strategy Agent using txtai native framework"""
@@ -1274,7 +1356,26 @@ class CompetitorResponseAgent(BaseALwrityAgent):
self.sif_service = SIFIntegrationService(user_id)
except Exception as e:
logger.warning(f"Failed to initialize SIF service for CompetitorResponseAgent: {e}")
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""Propose REMARKET pillar tasks."""
proposals = []
# 1. Competitor Monitoring
proposals.append(TaskProposal(
title="Monitor Competitor Activity",
description="Check for new moves from your key competitors.",
pillar_id="remarket",
priority="medium",
estimated_time=15,
source_agent="CompetitorResponseAgent",
reasoning="Stay ahead of market changes.",
action_type="navigate",
action_url="/seo-dashboard"
))
return proposals
def _create_txtai_agent(self) -> Agent:
"""Create Competitor Response Agent using txtai native framework"""
if not TXTAI_AVAILABLE:
@@ -1463,7 +1564,39 @@ class SEOOptimizationAgent(BaseALwrityAgent):
self.sif_service = SIFIntegrationService(user_id)
except Exception as e:
logger.warning(f"Failed to initialize SIF service for SEOOptimizationAgent: {e}")
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""Propose ANALYZE pillar tasks."""
proposals = []
# 1. Technical Audit
proposals.append(TaskProposal(
title="Review SEO Health",
description="Check for critical technical issues affecting your search visibility.",
pillar_id="analyze",
priority="high",
estimated_time=20,
source_agent="SEOOptimizationAgent",
reasoning="Regular health checks prevent traffic drops.",
action_type="navigate",
action_url="/seo-dashboard"
))
# 2. Keyword Opportunities
proposals.append(TaskProposal(
title="Optimize Underperforming Keywords",
description="Identify keywords where you rank on page 2 and optimize content to boost them.",
pillar_id="analyze",
priority="medium",
estimated_time=40,
source_agent="SEOOptimizationAgent",
reasoning="Low-hanging fruit for traffic growth.",
action_type="navigate",
action_url="/seo-dashboard"
))
return proposals
def _create_txtai_agent(self) -> Agent:
"""Create SEO Optimization Agent using txtai native framework"""
if not TXTAI_AVAILABLE:
@@ -2101,7 +2234,39 @@ class SocialAmplificationAgent(BaseALwrityAgent):
self.sif_service = SIFIntegrationService(user_id)
except Exception as e:
logger.warning(f"Failed to initialize SIF service for SocialAmplificationAgent: {e}")
async def propose_daily_tasks(self, context: Dict[str, Any]) -> List[TaskProposal]:
"""Propose PUBLISH and ENGAGE pillar tasks."""
proposals = []
# 1. Publish Task
proposals.append(TaskProposal(
title="Schedule Social Content",
description="Plan and schedule your posts for the week to maintain consistent presence.",
pillar_id="publish",
priority="high",
estimated_time=20,
source_agent="SocialAmplificationAgent",
reasoning="Consistency is key for algorithm growth.",
action_type="navigate",
action_url="/scheduler-dashboard"
))
# 2. Engage Task
proposals.append(TaskProposal(
title="Engage with Community",
description="Respond to comments and interact with industry leaders' posts.",
pillar_id="engage",
priority="medium",
estimated_time=15,
source_agent="SocialAmplificationAgent",
reasoning="Community building increases reach.",
action_type="navigate",
action_url="/social-dashboard"
))
return proposals
def _create_txtai_agent(self) -> Agent:
"""Create Social Amplification Agent using txtai native framework"""
if not TXTAI_AVAILABLE:

View File

@@ -46,17 +46,18 @@ class CompetitorSemanticSnapshot:
@dataclass
class ContentSemanticInsight:
"""Real-time semantic insight for content monitoring."""
"""Represents an actionable content insight."""
insight_id: str
insight_type: str # "gap", "opportunity", "trend", "threat"
insight_type: str # 'gap', 'trend', 'optimization', 'threat'
title: str
description: str
confidence_score: float
impact_score: float
confidence_score: float # 0.0 to 1.0
impact_score: float # 0.0 to 10.0
related_topics: List[str]
suggested_actions: List[str]
created_at: str
expires_at: str
source_agent: str = "SIF Intelligence" # New field for agent attribution
class RealTimeSemanticMonitor:
@@ -274,78 +275,172 @@ class RealTimeSemanticMonitor:
async def _monitor_competitors(self) -> List[CompetitorSemanticSnapshot]:
"""Monitor competitor semantic positioning."""
snapshots = []
for competitor in self.monitored_competitors:
try:
# This would perform actual competitor analysis
# For now, return sample data
snapshot = CompetitorSemanticSnapshot(
competitor_id=f"comp_{competitor}",
competitor_name=competitor,
semantic_overlap=0.65,
unique_topics=["AI automation", "Voice search", "Video marketing"],
content_volume=random.randint(50, 200),
authority_score=random.uniform(0.4, 0.9),
last_updated=datetime.now().isoformat(),
trending_topics=["AI content", "Voice optimization"]
)
snapshots.append(snapshot)
except Exception as e:
logger.error(f"Failed to monitor competitor {competitor}: {e}")
try:
# 1. Get competitors from SIF integration
# We assume SIFIntegrationService has methods to get competitor data or we query index
# Let's try to search for "competitor_analysis" type in txtai index
results = await self.intelligence_service.search("competitor analysis", limit=10)
competitors_found = []
if results:
for res in results:
try:
metadata_str = res.get('object')
metadata = json.loads(metadata_str) if isinstance(metadata_str, str) else (metadata_str or res)
if metadata.get('type') == 'competitor_analysis':
competitors_found.append(metadata)
except: continue
# If no semantic data found, try fallback to DB/Integration service logic if needed
# For now, if we found semantic docs:
for comp_meta in competitors_found:
try:
full_report = comp_meta.get('full_report', {})
domain = comp_meta.get('url', 'Unknown')
# Calculate real metrics from the full report
# Use semantic overlap from SIF if available, or estimate
overlap = full_report.get('semantic_overlap', 0.5)
# Extract topics from the analysis content
topics = full_report.get('content_topics', [])
if not topics and 'analysis' in full_report:
# Try to extract from unstructured text if structured topics missing
topics = ["General Strategy"] # Fallback
snapshot = CompetitorSemanticSnapshot(
competitor_id=f"comp_{domain}",
competitor_name=domain,
semantic_overlap=overlap,
unique_topics=topics[:5],
content_volume=full_report.get('page_count', 0),
authority_score=full_report.get('authority_score', 0.5),
last_updated=comp_meta.get('timestamp', datetime.now().isoformat()),
trending_topics=full_report.get('trending_topics', [])
)
snapshots.append(snapshot)
except Exception as e:
logger.error(f"Error processing competitor snapshot: {e}")
if not snapshots and self.monitored_competitors:
# Fallback for manually added competitors that might not be fully indexed yet
for competitor in self.monitored_competitors:
snapshots.append(CompetitorSemanticSnapshot(
competitor_id=f"comp_{competitor}",
competitor_name=competitor,
semantic_overlap=0.0,
unique_topics=["Pending Analysis"],
content_volume=0,
authority_score=0.0,
last_updated=datetime.now().isoformat(),
trending_topics=[]
))
except Exception as e:
logger.error(f"Failed to monitor competitors: {e}")
return snapshots
async def _analyze_content_performance(self) -> List[ContentSemanticInsight]:
"""Analyze content performance and identify insights."""
"""Analyze content performance and identify insights using SIF Agents."""
insights = []
try:
# Generate various types of insights
current_time = datetime.now()
# Content gap insight
insights.append(ContentSemanticInsight(
insight_id="gap_001",
insight_type="gap",
title="Voice Search Optimization Gap",
description="Competitors are covering voice search topics 40% more than your content",
confidence_score=0.85,
impact_score=8.5,
related_topics=["voice search", "featured snippets", "conversational AI"],
suggested_actions=["Create voice search content", "Optimize for featured snippets"],
created_at=current_time.isoformat(),
expires_at=(current_time + timedelta(days=7)).isoformat()
))
# Trending opportunity insight
insights.append(ContentSemanticInsight(
insight_id="trend_001",
insight_type="trend",
title="AI Content Tools Trending",
description="AI content creation tools showing 300% increase in search volume",
confidence_score=0.92,
impact_score=9.2,
related_topics=["AI content", "content automation", "AI writing tools"],
suggested_actions=["Create AI tool reviews", "Develop AI content strategy"],
created_at=current_time.isoformat(),
expires_at=(current_time + timedelta(days=14)).isoformat()
))
# Threat insight
insights.append(ContentSemanticInsight(
insight_id="threat_001",
insight_type="threat",
title="Competitor Content Surge",
description="Top competitor increased content production by 150% in your key topics",
confidence_score=0.78,
impact_score=7.8,
related_topics=["content strategy", "competitor analysis"],
suggested_actions=["Increase content frequency", "Focus on unique angles"],
created_at=current_time.isoformat(),
expires_at=(current_time + timedelta(days=5)).isoformat()
))
# 1. Initialize Agents if needed (lazy load to avoid circular imports)
if not self.strategy_agent:
from ..agents.specialized_agents import StrategyArchitectAgent, ContentStrategyAgent, CompetitorResponseAgent
self.strategy_agent = StrategyArchitectAgent(self.user_id)
self.content_agent = ContentStrategyAgent(self.user_id)
self.competitor_agent = CompetitorResponseAgent(self.user_id)
# 2. Get Real Insights from Agents
# Content Gaps
try:
# We can reuse the propose_daily_tasks logic or call specific methods
# Let's manually construct a "gap analysis" context for the agent
gap_context = {"analysis_type": "gaps", "website_url": "user_site"}
# Ideally we call a specific method like find_semantic_gaps if available publicly
# But propose_daily_tasks returns TaskProposal objects.
# Let's check if we can get raw insights.
# The agents have methods like find_semantic_gaps (StrategyArchitect)
# Using StrategyArchitect for pillar/gap analysis
if hasattr(self.strategy_agent, 'find_semantic_gaps'):
# This method requires competitor indices, which is complex to get here without full context.
# Let's use the SIF service directly for lighter weight insights or call the agent's high level method.
pass
# Alternative: Query SIF directly for "content gaps" if they are indexed as such
# Or generate them now via LLM + SIF Context
# Let's generate ONE high quality insight via ContentStrategyAgent
# We'll simulate a task proposal request but specifically for "insights"
# Actually, let's look at SIFIntegrationService.get_content_strategy_context
# For now, to fix the "mock data" issue quickly:
# We will check if we have ANY data in SIF.
# If yes, we generate dynamic insights based on that data.
dashboard_context = await self.sif_service.get_seo_dashboard_context()
if "error" not in dashboard_context:
data = dashboard_context.get("dashboard_data", {})
summary = data.get("summary", {})
# Insight 1: Performance Trend
ctr = summary.get("ctr", 0)
if ctr < 0.02:
insights.append(ContentSemanticInsight(
insight_id="perf_low_ctr",
insight_type="opportunity",
title="Low CTR Opportunity",
description=f"Your average CTR is {ctr:.1%}. Optimizing meta descriptions could boost traffic.",
confidence_score=0.9,
impact_score=8.0,
related_topics=["meta tags", "titles", "ctr optimization"],
suggested_actions=["Rewrite titles for high-impression low-click pages"],
created_at=current_time.isoformat(),
expires_at=(current_time + timedelta(days=7)).isoformat(),
source_agent="SEO Specialist Agent"
))
# Insight 2: Keyword Opportunities (from AI insights in dashboard data)
ai_insights = data.get("ai_insights", [])
for i, ai_ins in enumerate(ai_insights[:2]): # Take top 2
insights.append(ContentSemanticInsight(
insight_id=f"ai_insight_{i}",
insight_type="trend", # Map category
title=f"AI Recommendation: {ai_ins.get('category', 'General')}",
description=ai_ins.get('insight', 'No description'),
confidence_score=0.85,
impact_score=7.5,
related_topics=[ai_ins.get('category', 'seo')],
suggested_actions=[ai_ins.get('insight')], # Simplification
created_at=current_time.isoformat(),
expires_at=(current_time + timedelta(days=7)).isoformat(),
source_agent="Strategy Architect Agent"
))
except Exception as agent_err:
logger.warning(f"Agent insight generation failed: {agent_err}")
# If still no insights (e.g. no dashboard data), AND we have no fallback,
# THEN we might return an empty list or a "Setup" insight.
if not insights:
insights.append(ContentSemanticInsight(
insight_id="setup_001",
insight_type="gap",
title="Awaiting Data Analysis",
description="Connect Search Console or complete competitor analysis to see real-time insights.",
confidence_score=1.0,
impact_score=5.0,
related_topics=["onboarding"],
suggested_actions=["Complete Step 5 Onboarding"],
created_at=current_time.isoformat(),
expires_at=(current_time + timedelta(days=1)).isoformat(),
source_agent="Onboarding Assistant"
))
except Exception as e:
logger.error(f"Failed to analyze content performance: {e}")

View File

@@ -27,13 +27,8 @@ async def generate_facebook_persona_task(user_id: str):
try:
logger.info(f"Scheduled Facebook persona generation started for user {user_id}")
# Ensure we have a valid session factory before trying to get session
from services.database import SessionLocal
if not SessionLocal:
logger.error("Database session factory not initialized")
return
db = get_db_session()
# Use user-specific session
db = get_db_session(user_id)
if not db:
logger.error(f"Failed to get database session for Facebook persona generation (user: {user_id})")
return

View File

@@ -10,6 +10,7 @@ import asyncio
from typing import Dict, Any, List, Tuple
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import func, desc
from loguru import logger
import json
@@ -338,36 +339,59 @@ class UsageTrackingService:
).order_by(UsageAlert.created_at.desc()).limit(10).all()
if not summary:
# No usage this period - return complete structure with zeros
provider_breakdown = {}
usage_percentages = {}
# If no summary exists for current period, we should initialize it
# This handles the "start of month" case where a user logs in but hasn't made calls yet
if billing_period == datetime.now().strftime("%Y-%m"):
logger.info(f"Initializing empty UsageSummary for user {user_id} in period {billing_period}")
summary = UsageSummary(
user_id=user_id,
billing_period=billing_period,
usage_status=UsageStatus.ACTIVE,
total_calls=0,
total_tokens=0,
total_cost=0.0
)
try:
self.db.add(summary)
self.db.commit()
self.db.refresh(summary)
except Exception as e:
logger.error(f"Failed to initialize summary: {e}")
self.db.rollback()
# Fallback to zero-struct return if DB write fails
pass
# Initialize provider breakdown with zeros
for provider in APIProvider:
provider_name = provider.value
provider_breakdown[provider_name] = {
'calls': 0,
'tokens': 0,
'cost': 0.0
if not summary: # Still no summary after attempt
# No usage this period - return complete structure with zeros
provider_breakdown = {}
usage_percentages = {}
# Initialize provider breakdown with zeros
for provider in APIProvider:
provider_name = provider.value
provider_breakdown[provider_name] = {
'calls': 0,
'tokens': 0,
'cost': 0.0
}
usage_percentages[f"{provider_name}_calls"] = 0
usage_percentages['cost'] = 0
return {
'billing_period': billing_period,
'usage_status': 'active',
'total_calls': 0,
'total_tokens': 0,
'total_cost': 0.0,
'avg_response_time': 0.0,
'error_rate': 0.0,
'last_updated': datetime.now().isoformat(),
'limits': limits,
'provider_breakdown': provider_breakdown,
'alerts': [],
'usage_percentages': usage_percentages
}
usage_percentages[f"{provider_name}_calls"] = 0
usage_percentages['cost'] = 0
return {
'billing_period': billing_period,
'usage_status': 'active',
'total_calls': 0,
'total_tokens': 0,
'total_cost': 0.0,
'avg_response_time': 0.0,
'error_rate': 0.0,
'last_updated': datetime.now().isoformat(),
'limits': limits,
'provider_breakdown': provider_breakdown,
'alerts': [],
'usage_percentages': {}
}
# Provider breakdown - calculate costs first, then use for percentages
# Only include Gemini and HuggingFace (HuggingFace is stored under MISTRAL enum)
@@ -547,12 +571,18 @@ class UsageTrackingService:
stability_cost + image_edit_cost + tavily_cost + serper_cost + exa_cost
)
summary_total_cost = summary.total_cost or 0.0
# Use calculated cost if summary cost is 0, otherwise use summary cost (it's more accurate)
final_total_cost = summary_total_cost if summary_total_cost > 0 else calculated_total_cost
# If we calculated costs from logs, update the summary for future requests
if calculated_total_cost > 0 and summary_total_cost == 0.0:
logger.info(f"[UsageStats] Updating summary costs: total_cost={final_total_cost:.6f}, gemini_cost={gemini_cost:.6f}, mistral_cost={mistral_cost:.6f}, video_cost={video_cost:.6f}, audio_cost={audio_cost:.6f}, image_cost={stability_cost:.6f}")
# Determine the best cost value to use
# If summary cost is 0 but we have calculated cost, use calculated cost
# If summary cost exists but is less than calculated cost (out of sync), use calculated cost
if calculated_total_cost > summary_total_cost:
final_total_cost = calculated_total_cost
else:
final_total_cost = summary_total_cost
# If we found a discrepancy (summary cost is 0 or less than calculated), update the DB
if calculated_total_cost > 0 and (summary_total_cost == 0.0 or calculated_total_cost > summary_total_cost):
logger.info(f"[UsageStats] Updating summary costs (was {summary_total_cost}): total_cost={final_total_cost:.6f}, gemini_cost={gemini_cost:.6f}, mistral_cost={mistral_cost:.6f}, video_cost={video_cost:.6f}, audio_cost={audio_cost:.6f}, image_cost={stability_cost:.6f}")
summary.total_cost = final_total_cost
summary.gemini_cost = gemini_cost
summary.mistral_cost = mistral_cost
@@ -622,7 +652,7 @@ class UsageTrackingService:
}
def get_usage_trends(self, user_id: str, months: int = 6) -> Dict[str, Any]:
"""Get usage trends over time."""
"""Get usage trends over time with self-healing from logs."""
# Calculate billing periods
end_date = datetime.now()
@@ -633,13 +663,111 @@ class UsageTrackingService:
periods.reverse() # Oldest first
# Get usage summaries for these periods
# 1. Fetch existing summaries
summaries = self.db.query(UsageSummary).filter(
UsageSummary.user_id == user_id,
UsageSummary.billing_period.in_(periods)
).order_by(UsageSummary.billing_period).all()
).all()
summary_dict = {s.billing_period: s for s in summaries}
# Create trends data
# 2. Fetch aggregated logs for self-healing
# Group by (billing_period, provider) to fix provider breakdowns too
try:
log_stats = self.db.query(
APIUsageLog.billing_period,
APIUsageLog.provider,
func.count(APIUsageLog.id).label('calls'),
func.sum(APIUsageLog.cost_total).label('cost'),
func.sum(APIUsageLog.tokens_total).label('tokens')
).filter(
APIUsageLog.user_id == user_id,
APIUsageLog.billing_period.in_(periods)
).group_by(APIUsageLog.billing_period, APIUsageLog.provider).all()
# Organize log stats by period -> provider
log_data_by_period = {}
for period, provider_enum, calls, cost, tokens in log_stats:
if period not in log_data_by_period:
log_data_by_period[period] = {}
# Handle provider enum or string
provider_name = provider_enum.value if hasattr(provider_enum, 'value') else str(provider_enum).lower()
# Normalize provider names (e.g. 'GEMINI' -> 'gemini')
if '.' in provider_name:
provider_name = provider_name.split('.')[-1].lower()
if provider_name not in log_data_by_period[period]:
log_data_by_period[period][provider_name] = {'calls': 0, 'cost': 0.0, 'tokens': 0}
log_data_by_period[period][provider_name]['calls'] += (calls or 0)
log_data_by_period[period][provider_name]['cost'] += float(cost or 0.0)
log_data_by_period[period][provider_name]['tokens'] += (tokens or 0)
# 3. Update/Create Summaries based on logs
for period in periods:
period_logs = log_data_by_period.get(period, {})
summary = summary_dict.get(period)
# If no summary exists but logs do, create one
if not summary and period_logs:
logger.info(f"[UsageStats] Self-healing: Creating missing summary for {period}")
summary = UsageSummary(
user_id=user_id,
billing_period=period,
usage_status=UsageStatus.ACTIVE,
total_calls=0,
total_cost=0.0,
total_tokens=0
)
self.db.add(summary)
summary_dict[period] = summary
if summary and period_logs:
total_calls_calc = 0
total_cost_calc = 0.0
total_tokens_calc = 0
for prov, data in period_logs.items():
total_calls_calc += data['calls']
total_cost_calc += data['cost']
total_tokens_calc += data['tokens']
# Update provider specific fields if logs > summary
calls_attr = f"{prov}_calls"
cost_attr = f"{prov}_cost"
tokens_attr = f"{prov}_tokens"
if hasattr(summary, calls_attr):
current_val = getattr(summary, calls_attr, 0)
if current_val < data['calls']:
setattr(summary, calls_attr, data['calls'])
if hasattr(summary, cost_attr):
current_val = getattr(summary, cost_attr, 0.0)
# Use significant difference to avoid float noise
if (data['cost'] - current_val) > 0.000001:
setattr(summary, cost_attr, data['cost'])
if hasattr(summary, tokens_attr):
current_val = getattr(summary, tokens_attr, 0)
if current_val < data['tokens']:
setattr(summary, tokens_attr, data['tokens'])
# Update totals if under-reported
if (summary.total_cost or 0.0) < total_cost_calc:
logger.info(f"[UsageStats] Self-healing cost for {period}: {summary.total_cost} -> {total_cost_calc}")
summary.total_cost = total_cost_calc
if (summary.total_calls or 0) < total_calls_calc:
summary.total_calls = total_calls_calc
if (summary.total_tokens or 0) < total_tokens_calc:
summary.total_tokens = total_tokens_calc
self.db.commit()
except Exception as e:
logger.error(f"Failed to self-heal usage trends: {e}")
self.db.rollback()
# 4. Construct Return Data
trends = {
'periods': periods,
'total_calls': [],
@@ -648,7 +776,14 @@ class UsageTrackingService:
'provider_trends': {}
}
summary_dict = {s.billing_period: s for s in summaries}
# Initialize provider trends structure
for provider in APIProvider:
provider_name = provider.value
trends['provider_trends'][provider_name] = {
'calls': [],
'cost': [],
'tokens': []
}
for period in periods:
summary = summary_dict.get(period)
@@ -661,13 +796,6 @@ class UsageTrackingService:
# Provider-specific trends
for provider in APIProvider:
provider_name = provider.value
if provider_name not in trends['provider_trends']:
trends['provider_trends'][provider_name] = {
'calls': [],
'cost': [],
'tokens': []
}
trends['provider_trends'][provider_name]['calls'].append(
getattr(summary, f"{provider_name}_calls", 0) or 0
)
@@ -685,13 +813,6 @@ class UsageTrackingService:
for provider in APIProvider:
provider_name = provider.value
if provider_name not in trends['provider_trends']:
trends['provider_trends'][provider_name] = {
'calls': [],
'cost': [],
'tokens': []
}
trends['provider_trends'][provider_name]['calls'].append(0)
trends['provider_trends'][provider_name]['cost'].append(0.0)
trends['provider_trends'][provider_name]['tokens'].append(0)

View File

@@ -0,0 +1,143 @@
"""
Self-Learning Task Memory Service (Phase 3)
Uses txtai and TaskHistory DB model to filter and improve daily task suggestions.
"""
import hashlib
import uuid
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from loguru import logger
from sqlalchemy.orm import Session
from models.daily_workflow_models import TaskHistory, DailyWorkflowTask
from services.intelligence.txtai_service import TxtaiIntelligenceService
class TaskMemoryService:
"""
Manages the long-term memory of user tasks.
Responsibilities:
1. Record completed/rejected tasks to DB and txtai index.
2. Check if a proposed task is redundant or previously rejected.
3. Retrieve relevant past tasks for context.
"""
def __init__(self, user_id: str, db: Session):
self.user_id = user_id
self.db = db
self.intelligence = TxtaiIntelligenceService(user_id)
def _compute_hash(self, title: str, description: str) -> str:
"""Compute a consistent hash for task deduplication."""
text = f"{title.strip().lower()}|{description.strip().lower()}"
return hashlib.sha256(text.encode()).hexdigest()
async def record_task_outcome(self, task: DailyWorkflowTask, feedback_score: int = 0, feedback_text: str = None):
"""
Record a task's final status (completed, dismissed, rejected) into memory.
"""
try:
task_hash = self._compute_hash(task.title, task.description)
# 1. Update/Create DB Record
history = TaskHistory(
user_id=self.user_id,
task_hash=task_hash,
title=task.title,
description=task.description,
pillar_id=task.pillar_id,
status=task.status,
source_agent=task.metadata_json.get("source_agent") if task.metadata_json else None,
feedback_score=feedback_score,
feedback_text=feedback_text,
created_at=datetime.utcnow(),
vector_id=str(uuid.uuid4())
)
self.db.add(history)
self.db.commit()
# 2. Index into txtai (if status is meaningful)
if task.status in ["completed", "dismissed", "rejected"]:
# We index the task text with metadata about its outcome
# This allows us to search: "Has the user rejected similar tasks?"
doc = {
"id": history.vector_id,
"text": f"{task.title}. {task.description}",
"tags": f"task_memory {task.status} {task.pillar_id}",
"status": task.status,
"timestamp": datetime.utcnow().isoformat()
}
# Use Txtai service to upsert
# Note: TxtaiService usually handles batching, but for single updates we can use add
if hasattr(self.intelligence.embeddings, "upsert"):
self.intelligence.embeddings.upsert([doc])
# save() requires a path argument in some txtai versions, but TxtaiService manages paths
# If we are using the service wrapper, we should rely on its internal management
# However, self.intelligence.embeddings is the raw txtai object.
# We should check if we need to call save with the index path.
index_path = getattr(self.intelligence, "index_path", None)
if index_path:
self.intelligence.embeddings.save(index_path)
logger.info(f"Indexed task outcome: {task.title} -> {task.status}")
else:
logger.warning("Could not save embeddings: index_path not found on service")
except Exception as e:
logger.error(f"Failed to record task outcome for user {self.user_id}: {e}")
async def filter_redundant_proposals(self, proposals: List[Any]) -> List[Any]:
"""
Filter out proposals that are:
1. Exact duplicates of recently completed/rejected tasks (Hash check).
2. Semantically too similar to recently rejected tasks (Vector check).
"""
filtered = []
# Get recent history hashes (last 7 days)
cutoff = datetime.utcnow() - timedelta(days=7)
recent_hashes = {
row.task_hash for row in
self.db.query(TaskHistory.task_hash)
.filter(TaskHistory.user_id == self.user_id, TaskHistory.created_at >= cutoff)
.all()
}
for p in proposals:
p_hash = self._compute_hash(p.title, p.description)
# 1. Exact Match Check
if p_hash in recent_hashes:
logger.info(f"Filtering redundant task (exact match): {p.title}")
continue
# 2. Semantic Similarity Check (only for potential rejections)
# If we have the vector index ready
is_semantic_duplicate = False
try:
# Check if similar tasks were REJECTED recently
results = self.intelligence.search(
f"{p.title} {p.description}",
limit=1
)
if results:
top = results[0]
# If very similar (>0.85) and was REJECTED/DISMISSED
# We might need to fetch the metadata from the result if txtai returns it
# For now, this is a heuristic stub. Txtai search returns dict with 'id', 'score', 'text', etc.
# If we stored 'status' in metadata, we check it.
if top['score'] > 0.85:
# Retrieve status from DB using vector_id if needed, or if metadata is returned
# Assuming we want to avoid repeating REJECTED ideas
# This requires storing 'status' in the index metadata
pass
except Exception:
pass
if not is_semantic_duplicate:
filtered.append(p)
return filtered

View File

@@ -8,7 +8,8 @@ from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
from models.agent_activity_models import AgentAlert
from services.agent_activity_service import AgentActivityService
from services.llm_providers.main_text_generation import llm_text_gen
from api.content_planning.services.content_strategy.onboarding.data_integration import OnboardingDataIntegrationService
from loguru import logger
PILLAR_IDS = ["plan", "generate", "publish", "analyze", "engage", "remarket"]
@@ -95,6 +96,7 @@ def _fallback_tasks(date: str) -> List[Dict[str, Any]]:
def build_grounding_context(db: Session, user_id: str, date: str) -> Dict[str, Any]:
# 1. Fetch unread alerts
unread_agent_alerts = (
db.query(AgentAlert)
.filter(AgentAlert.user_id == user_id, AgentAlert.read_at.is_(None))
@@ -102,10 +104,32 @@ def build_grounding_context(db: Session, user_id: str, date: str) -> Dict[str, A
.limit(10)
.all()
)
# 2. Fetch comprehensive onboarding data (SIF)
onboarding_context = {}
try:
svc = OnboardingDataIntegrationService()
integrated = svc.get_integrated_data_sync(user_id, db) or {}
canonical = integrated.get("canonical_profile", {})
website_analysis = integrated.get("website_analysis", {})
onboarding_context = {
"website_url": website_analysis.get("website_url"),
"business_type": website_analysis.get("business_type"),
"industry": canonical.get("industry") or website_analysis.get("industry"),
"target_audience": canonical.get("target_audience") or website_analysis.get("target_audience"),
"content_pillars": canonical.get("content_pillars", []),
"competitors": [c.get("domain") for c in website_analysis.get("competitors", [])[:3]] if website_analysis.get("competitors") else []
}
except Exception as e:
logger.warning(f"Failed to fetch onboarding data for workflow generation: {e}")
return {
"date": date,
"user_id": user_id,
"pillars": PILLAR_IDS,
"onboarding_data": onboarding_context,
"recent_agent_alerts": [
{"type": a.alert_type, "severity": a.severity, "title": a.title, "message": a.message}
for a in unread_agent_alerts
@@ -113,9 +137,113 @@ def build_grounding_context(db: Session, user_id: str, date: str) -> Dict[str, A
}
def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) -> Dict[str, Any]:
import asyncio
from services.intelligence.agents.agent_orchestrator import AgentOrchestrationService
from services.task_memory_service import TaskMemoryService
# Initialize orchestration service (singleton)
orchestration_service = AgentOrchestrationService()
async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) -> Dict[str, Any]:
activity = AgentActivityService(db, user_id)
grounding = build_grounding_context(db, user_id, date)
memory_service = TaskMemoryService(user_id, db)
# 1. Get Orchestrator
try:
orchestrator = await orchestration_service.get_or_create_orchestrator(user_id)
except Exception as e:
logger.error(f"Failed to get orchestrator: {e}")
return {"date": date, "tasks": _fallback_tasks(date)}
# 2. Parallel "Committee" Proposal Gathering
logger.info(f"Gathering daily task proposals from agent committee for user {user_id}")
agent_tasks = []
try:
# Define agents to poll
agents_to_poll = [
orchestrator.agents.get('content'), # ContentStrategyAgent
orchestrator.agents.get('seo'), # SEOOptimizationAgent
orchestrator.agents.get('social'), # SocialAmplificationAgent
orchestrator.agents.get('competitor'), # CompetitorResponseAgent
# Add StrategyArchitect if available in orchestrator.agents
]
# Filter out None agents (disabled/failed init)
active_agents = [a for a in agents_to_poll if a]
# Execute propose_daily_tasks in parallel
results = await asyncio.gather(
*[a.propose_daily_tasks(grounding) for a in active_agents],
return_exceptions=True
)
# Collect successful proposals
raw_proposals = []
for res in results:
if isinstance(res, list):
raw_proposals.extend(res)
elif isinstance(res, Exception):
logger.warning(f"Agent proposal failed: {res}")
# 3. Filter Redundant Proposals (Self-Learning)
# Note: We need to ensure we don't filter out essential recurring tasks if they were completed long ago
# But for now, we filter exact duplicates from recent history (last 7 days)
# We can implement semantic filtering later
# Simple deduplication based on title+pillar
unique_map = {}
for p in raw_proposals:
key = f"{p.pillar_id}:{p.title}"
if key not in unique_map:
unique_map[key] = p
elif p.priority == "high": # Overwrite with higher priority
unique_map[key] = p
agent_tasks = list(unique_map.values())
# Phase 3: Check memory for rejections (Semantic Filter)
# For now, we rely on exact match logic in memory service if implemented fully
# agent_tasks = await memory_service.filter_redundant_proposals(agent_tasks)
except Exception as e:
logger.error(f"Committee proposal phase failed: {e}")
# Continue to fallback or LLM generation if committee fails
# 4. Final Selection
# If we have agent tasks, use them. Otherwise fall back to LLM generation.
if agent_tasks:
logger.info(f"Generated {len(agent_tasks)} tasks via Agent Committee")
# Convert TaskProposal objects to dicts for frontend
final_tasks = []
for prop in agent_tasks:
final_tasks.append({
"pillarId": prop.pillar_id,
"title": prop.title,
"description": prop.description,
"priority": prop.priority,
"estimatedTime": prop.estimated_time,
"actionType": prop.action_type,
"actionUrl": prop.action_url,
"enabled": True,
"metadata": {
"source_agent": prop.source_agent,
"reasoning": prop.reasoning,
"context_data": prop.context_data
}
})
# Ensure we have coverage for all pillars (fill gaps with fallback/LLM if needed)
# For now, let's just return what the agents proposed
return {
"date": date,
"tasks": final_tasks
}
# Fallback to original LLM generation if agents returned nothing
logger.info("Agent committee returned no tasks, falling back to LLM generation")
schema = {
"type": "object",
@@ -143,17 +271,21 @@ def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) -> Dict[s
}
prompt = (
"Generate a Today workflow plan for ALwrity with exactly 6 lifecycle pillars: "
"Generate a personalized Today workflow plan for ALwrity with exactly 6 lifecycle pillars: "
"plan, generate, publish, analyze, engage, remarket.\n\n"
"User Context (Onboarding & Strategy):\n"
f"{json.dumps(grounding.get('onboarding_data', {}), indent=2)}\n\n"
"Rules:\n"
"- Produce JSON only that matches the schema.\n"
"- Include 1-3 tasks per pillar.\n"
"- Each task must have pillarId in {plan, generate, publish, analyze, engage, remarket}.\n"
"- Customize tasks based on the user's industry, business type, and content pillars found in User Context.\n"
"- If competitors are listed, include a task to analyze one of them.\n"
"- Prefer actionable tasks that can be completed today.\n"
"- Use these common actionUrl routes when relevant: "
"/content-planning-dashboard, /blog-writer, /linkedin-writer, /facebook-writer, /seo-dashboard, /scheduler-dashboard.\n"
"- Keep descriptions concise.\n\n"
f"Grounding context:\n{json.dumps(grounding, indent=2)}\n"
f"Grounding context (Alerts):\n{json.dumps(grounding.get('recent_agent_alerts', []), indent=2)}\n"
)
run = activity.start_run(agent_type="TodayWorkflowGenerator", prompt=prompt[:4000])
@@ -202,7 +334,7 @@ def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) -> Dict[s
return result
def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Optional[str] = None) -> tuple[DailyWorkflowPlan, bool]:
async def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Optional[str] = None) -> tuple[DailyWorkflowPlan, bool]:
date_str = date or _today_date_str()
existing = (
db.query(DailyWorkflowPlan)
@@ -212,7 +344,7 @@ def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Optional[
if existing:
return existing, False
plan_data = generate_agent_enhanced_plan(db, user_id, date_str)
plan_data = await generate_agent_enhanced_plan(db, user_id, date_str)
tasks = plan_data.get("tasks", [])
plan = DailyWorkflowPlan(