feat(briefing): actionable intelligence — days overdue, project context, suggest next steps
This commit is contained in:
@@ -23,14 +23,25 @@ SLOT_NAMES = ("compilation", "morning", "midday", "afternoon")
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def format_task(task: dict) -> str:
|
def format_task(task: dict, today: str | None = None) -> str:
|
||||||
parts = [task.get("title", "Untitled")]
|
parts = [task.get("title", "Untitled")]
|
||||||
if task.get("status"):
|
if task.get("status"):
|
||||||
parts.append(f"[{task['status'].replace('_', ' ').title()}]")
|
parts.append(f"[{task['status'].replace('_', ' ').title()}]")
|
||||||
if task.get("due_date"):
|
if task.get("due_date"):
|
||||||
parts.append(f"due {task['due_date']}")
|
parts.append(f"due {task['due_date']}")
|
||||||
|
if today and task["due_date"] < today and task.get("status") not in ("done", "cancelled"):
|
||||||
|
from datetime import date
|
||||||
|
try:
|
||||||
|
due = date.fromisoformat(task["due_date"])
|
||||||
|
td = date.fromisoformat(today)
|
||||||
|
days = (td - due).days
|
||||||
|
parts.append(f"({days} day{'s' if days != 1 else ''} overdue)")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
if task.get("priority") and task["priority"] not in ("none", ""):
|
if task.get("priority") and task["priority"] not in ("none", ""):
|
||||||
parts.append(f"priority: {task['priority']}")
|
parts.append(f"priority: {task['priority']}")
|
||||||
|
if task.get("project_title"):
|
||||||
|
parts.append(f"project: {task['project_title']}")
|
||||||
return " — ".join(parts)
|
return " — ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -134,6 +145,13 @@ async def _gather_internal(user_id: int) -> dict:
|
|||||||
all_tasks: list[dict] = []
|
all_tasks: list[dict] = []
|
||||||
try:
|
try:
|
||||||
all_task_objs, _total = await list_notes(user_id, is_task=True, limit=100)
|
all_task_objs, _total = await list_notes(user_id, is_task=True, limit=100)
|
||||||
|
# Build project title lookup for enrichment
|
||||||
|
project_titles: dict[int, str] = {}
|
||||||
|
try:
|
||||||
|
for p in await list_projects(user_id):
|
||||||
|
project_titles[p.id] = p.title or "Untitled"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
all_tasks = [
|
all_tasks = [
|
||||||
{
|
{
|
||||||
"task_id": t.id,
|
"task_id": t.id,
|
||||||
@@ -141,21 +159,22 @@ async def _gather_internal(user_id: int) -> dict:
|
|||||||
"status": t.status,
|
"status": t.status,
|
||||||
"due_date": t.due_date.isoformat() if t.due_date else None,
|
"due_date": t.due_date.isoformat() if t.due_date else None,
|
||||||
"priority": t.priority,
|
"priority": t.priority,
|
||||||
|
"project_title": project_titles.get(t.project_id) if t.project_id else None,
|
||||||
}
|
}
|
||||||
for t in all_task_objs
|
for t in all_task_objs
|
||||||
]
|
]
|
||||||
overdue = [
|
overdue = [
|
||||||
format_task(t) for t in all_tasks
|
format_task(t, today) for t in all_tasks
|
||||||
if t.get("due_date") and t["due_date"] < today and t.get("status") != "done"
|
if t.get("due_date") and t["due_date"] < today and t.get("status") not in ("done", "cancelled")
|
||||||
]
|
]
|
||||||
due_today = [
|
due_today = [
|
||||||
format_task(t) for t in all_tasks
|
format_task(t, today) for t in all_tasks
|
||||||
if t.get("due_date") == today and t.get("status") != "done"
|
if t.get("due_date") == today and t.get("status") not in ("done", "cancelled")
|
||||||
]
|
]
|
||||||
high_priority = [
|
high_priority = [
|
||||||
format_task(t) for t in all_tasks
|
format_task(t, today) for t in all_tasks
|
||||||
if t.get("priority") == "high" and t.get("status") not in ("done",) and
|
if t.get("priority") == "high" and t.get("status") not in ("done", "cancelled") and
|
||||||
format_task(t) not in overdue and format_task(t) not in due_today
|
format_task(t, today) not in overdue and format_task(t, today) not in due_today
|
||||||
][:5]
|
][:5]
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to gather tasks for briefing", exc_info=True)
|
logger.warning("Failed to gather tasks for briefing", exc_info=True)
|
||||||
@@ -267,8 +286,11 @@ def _unified_system_prompt(profile_body: str) -> str:
|
|||||||
"Weave together what matters today: mention the weather in a sentence, note any calendar "
|
"Weave together what matters today: mention the weather in a sentence, note any calendar "
|
||||||
"events or tasks due today, and briefly reference one or two noteworthy news stories. "
|
"events or tasks due today, and briefly reference one or two noteworthy news stories. "
|
||||||
"Only mention projects if a task from one is specifically due today. "
|
"Only mention projects if a task from one is specifically due today. "
|
||||||
"Be warm, concise, and human — aim for 3 to 5 sentences. "
|
"Be warm, concise, and human — aim for 4 to 8 sentences.\n\n"
|
||||||
"Future context like emails and messages will be added over time — keep the tone open and helpful.\n\n"
|
"IMPORTANT: Be actionable, not just informational. For overdue or due-today tasks, "
|
||||||
|
"suggest a concrete next step — offer to reschedule, mark in progress, or break it down. "
|
||||||
|
"For upcoming events, mention if preparation might help. "
|
||||||
|
"The user can reply to act on your suggestions via tool calls.\n\n"
|
||||||
+ (f"User profile:\n{profile_body}\n" if profile_body else "")
|
+ (f"User profile:\n{profile_body}\n" if profile_body else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -300,17 +322,23 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te
|
|||||||
|
|
||||||
# Tasks due today
|
# Tasks due today
|
||||||
if internal_data.get("due_today"):
|
if internal_data.get("due_today"):
|
||||||
lines.append("DUE TODAY:")
|
lines.append("DUE TODAY (suggest next steps — offer to mark in progress or help prioritize):")
|
||||||
lines.extend(f" - {t}" for t in internal_data["due_today"])
|
lines.extend(f" - {t}" for t in internal_data["due_today"])
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Overdue tasks (brief mention only)
|
# Overdue tasks
|
||||||
if internal_data.get("overdue_tasks"):
|
if internal_data.get("overdue_tasks"):
|
||||||
overdue = internal_data["overdue_tasks"]
|
overdue = internal_data["overdue_tasks"]
|
||||||
lines.append(f"OVERDUE ({len(overdue)} task{'s' if len(overdue) != 1 else ''}):")
|
lines.append(f"OVERDUE ({len(overdue)} task{'s' if len(overdue) != 1 else ''}) — suggest rescheduling or breaking down:")
|
||||||
lines.extend(f" - {t}" for t in overdue[:3])
|
lines.extend(f" - {t}" for t in overdue[:5])
|
||||||
if len(overdue) > 3:
|
if len(overdue) > 5:
|
||||||
lines.append(f" (and {len(overdue) - 3} more)")
|
lines.append(f" (and {len(overdue) - 5} more)")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# High priority in-progress
|
||||||
|
if internal_data.get("high_priority"):
|
||||||
|
lines.append("HIGH PRIORITY:")
|
||||||
|
lines.extend(f" - {t}" for t in internal_data["high_priority"])
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# News highlights (top 3 with excerpts — right panel shows full list)
|
# News highlights (top 3 with excerpts — right panel shows full list)
|
||||||
|
|||||||
Reference in New Issue
Block a user