Files
ALwrity/backend/services/today_workflow_service.py

275 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
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
PILLAR_IDS = ["plan", "generate", "publish", "analyze", "engage", "remarket"]
def _today_date_str() -> str:
return datetime.now(timezone.utc).date().isoformat()
def _coerce_priority(value: Any) -> str:
v = str(value or "medium").lower().strip()
return v if v in {"high", "medium", "low"} else "medium"
def _coerce_status(value: Any) -> str:
v = str(value or "pending").lower().strip()
if v in {"pending", "in_progress", "completed", "skipped", "dismissed"}:
return "skipped" if v == "dismissed" else v
return "pending"
def _fallback_tasks(date: str) -> List[Dict[str, Any]]:
return [
{
"pillarId": "plan",
"title": "Review todays plan",
"description": "Confirm priorities and adjust the content calendar for today.",
"priority": "high",
"estimatedTime": 15,
"actionType": "navigate",
"actionUrl": "/content-planning-dashboard",
"enabled": True,
},
{
"pillarId": "generate",
"title": "Generate one core content asset",
"description": "Create a draft aligned with your current strategy and voice.",
"priority": "high",
"estimatedTime": 45,
"actionType": "navigate",
"actionUrl": "/blog-writer",
"enabled": True,
},
{
"pillarId": "publish",
"title": "Publish or schedule todays content",
"description": "Publish or schedule content across the selected channel(s).",
"priority": "medium",
"estimatedTime": 20,
"actionType": "navigate",
"actionUrl": "/content-planning-dashboard",
"enabled": True,
},
{
"pillarId": "analyze",
"title": "Check semantic health and performance",
"description": "Review semantic health metrics and key performance indicators.",
"priority": "medium",
"estimatedTime": 15,
"actionType": "navigate",
"actionUrl": "/seo-dashboard",
"enabled": True,
},
{
"pillarId": "engage",
"title": "Engage on one channel",
"description": "Respond to comments and share one post to keep momentum.",
"priority": "medium",
"estimatedTime": 15,
"actionType": "navigate",
"actionUrl": "/linkedin-writer",
"enabled": True,
},
{
"pillarId": "remarket",
"title": "Repurpose and remarket content",
"description": "Create one repurposed snippet and distribute it to increase reach.",
"priority": "low",
"estimatedTime": 20,
"actionType": "navigate",
"actionUrl": "/facebook-writer",
"enabled": True,
},
]
def build_grounding_context(db: Session, user_id: str, date: str) -> Dict[str, Any]:
unread_agent_alerts = (
db.query(AgentAlert)
.filter(AgentAlert.user_id == user_id, AgentAlert.read_at.is_(None))
.order_by(AgentAlert.created_at.desc())
.limit(10)
.all()
)
return {
"date": date,
"user_id": user_id,
"pillars": PILLAR_IDS,
"recent_agent_alerts": [
{"type": a.alert_type, "severity": a.severity, "title": a.title, "message": a.message}
for a in unread_agent_alerts
],
}
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)
schema = {
"type": "object",
"properties": {
"date": {"type": "string"},
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"pillarId": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
"priority": {"type": "string"},
"estimatedTime": {"type": "number"},
"actionType": {"type": "string"},
"actionUrl": {"type": "string"},
"enabled": {"type": "boolean"},
"dependencies": {"type": "array", "items": {"type": "string"}},
"metadata": {"type": "object"},
},
},
},
},
}
prompt = (
"Generate a Today workflow plan for ALwrity with exactly 6 lifecycle pillars: "
"plan, generate, publish, analyze, engage, remarket.\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"
"- 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"
)
run = activity.start_run(agent_type="TodayWorkflowGenerator", prompt=prompt[:4000])
activity.log_event(
event_type="plan",
severity="info",
message="Building grounded daily workflow plan",
payload={"grounding": grounding},
run_id=run.id,
agent_type="TodayWorkflowGenerator",
)
try:
raw = llm_text_gen(prompt=prompt, json_struct=schema, user_id=user_id)
if isinstance(raw, dict):
result = raw
else:
try:
result = json.loads(raw)
except Exception:
result = {"date": date, "tasks": _fallback_tasks(date)}
except Exception as e:
activity.log_event(
event_type="warning",
severity="warning",
message=str(e)[:2000],
payload={"fallback": True},
run_id=run.id,
agent_type="TodayWorkflowGenerator",
)
result = {"date": date, "tasks": _fallback_tasks(date)}
tasks = result.get("tasks") if isinstance(result, dict) else None
if not isinstance(tasks, list) or not tasks:
result = {"date": date, "tasks": _fallback_tasks(date)}
activity.log_event(
event_type="final_summary",
severity="info",
message="Daily workflow plan generated",
payload={"date": date, "task_count": len(result.get("tasks", []))},
run_id=run.id,
agent_type="TodayWorkflowGenerator",
)
activity.finish_run(run.id, success=True, result_summary=json.dumps({"date": date, "tasks": result.get("tasks", [])})[:4000])
return result
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)
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
.first()
)
if existing:
return existing, False
plan_data = generate_agent_enhanced_plan(db, user_id, date_str)
tasks = plan_data.get("tasks", [])
plan = DailyWorkflowPlan(
user_id=user_id,
date=date_str,
source="agent",
plan_json=plan_data,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
db.add(plan)
db.commit()
db.refresh(plan)
for t in tasks:
pillar_id = str(t.get("pillarId") or "").lower().strip()
if pillar_id not in PILLAR_IDS:
continue
task = DailyWorkflowTask(
plan_id=plan.id,
user_id=user_id,
pillar_id=pillar_id,
title=str(t.get("title") or "Task").strip()[:255],
description=str(t.get("description") or "").strip(),
status=_coerce_status(t.get("status")),
priority=_coerce_priority(t.get("priority")),
estimated_time=int(t.get("estimatedTime") or 15),
action_type=str(t.get("actionType") or "navigate").strip()[:20],
action_url=str(t.get("actionUrl") or "").strip() or None,
enabled=bool(t.get("enabled", True)),
dependencies=t.get("dependencies") if isinstance(t.get("dependencies"), list) else None,
metadata_json=t.get("metadata") if isinstance(t.get("metadata"), dict) else None,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
db.add(task)
db.commit()
db.refresh(plan)
return plan, True
def update_task_status(
db: Session,
user_id: str,
task_id: int,
status: str,
completion_notes: Optional[str] = None,
) -> Optional[DailyWorkflowTask]:
task = db.query(DailyWorkflowTask).filter(DailyWorkflowTask.id == task_id, DailyWorkflowTask.user_id == user_id).first()
if not task:
return None
task.status = _coerce_status(status)
task.decided_at = datetime.utcnow()
if completion_notes is not None:
task.completion_notes = completion_notes[:4000]
db.add(task)
db.commit()
db.refresh(task)
return task