from __future__ import annotations from collections import defaultdict from typing import ClassVar from .base import BaseAgent, AgentInput, AgentOutput from .manifest import AgentManifest MANIFEST = AgentManifest( id="focus-area", version="1.1.0", # bumped: preferred_areas pref is now honoured in compute (#113) description="Identifies the most congested project/area in the user's task list.", pref_schema={ "type": "object", "additionalProperties": False, "properties": { "preferred_areas": { "type": "array", "items": {"type": "string"}, "default": [], "description": "Project / label names to prioritise when multiple areas tie.", }, }, }, context_schema=["todoist.tasks"], required_consents=["data:core", "data:todoist", "agent:focus-area"], output_contract={"type": "snippet", "format": "free_text"}, ttl_sec=43_200, # No inferred_params: preferred_areas requires project-level feedback linkage # that isn't available in feedback_history alone. Revisit with #78 (signal # abstraction) once per-task reactions can be traced back to a project. ) class FocusAreaAgent(BaseAgent): """Identifies the most congested project/area in the user's task list.""" agent_id: ClassVar[str] = MANIFEST.id ttl_seconds: ClassVar[int] = MANIFEST.ttl_sec version: ClassVar[str] = MANIFEST.version def compute(self, inp: AgentInput) -> AgentOutput: preferred: list[str] = inp.agent_prefs.get("preferred_areas", []) by_project: dict[str, list[dict]] = defaultdict(list) for task in inp.tasks: project = task.get("project_id") or task.get("project") or "default" by_project[project].append(task) if not by_project: prompt = "No tasks available to identify a focus area." return self._make_output(inp, prompt, {"project_count": 0}) def score(project: str, tasks: list[dict]) -> tuple[float, bool]: base = sum(2.0 if t.get("is_overdue") else 1.0 for t in tasks) # Boost preferred areas to break ties in their favour boosted = project in preferred or any(p in project for p in preferred) return (base + (0.5 if boosted else 0.0), boosted) top_project, top_tasks = max( by_project.items(), key=lambda kv: score(kv[0], kv[1]), ) overdue_in_top = sum(1 for t in top_tasks if t.get("is_overdue")) label = "the default project" if top_project == "default" else f'"{top_project}"' n = len(top_tasks) boosted = top_project in preferred or any(p in top_project for p in preferred) parts = [ f"The user's most congested area is {label} " f"({n} task{'s' if n != 1 else ''}, {overdue_in_top} overdue)." ] if boosted: parts.append("This area matches the user's stated focus preferences.") if overdue_in_top >= 3: parts.append("Consider surfacing an action from this area.") prompt = " ".join(parts) snapshot = { "top_project": top_project, "top_task_count": n, "top_overdue_count": overdue_in_top, "project_count": len(by_project), "preferred_areas": preferred, } return self._make_output(inp, prompt, snapshot)