feat(ml): multi-agent context framework + v4 orchestrator prompt
Adds ml/agents/ — five specialised sub-agents (overdue_task, momentum, time_of_day, recent_patterns, focus_area) each producing a prompt snippet from user signals. A registry wires them up; the orchestrator prompt in ml/serving/prompts.py synthesises their outputs into one tip via LiteLLM. Also wires /api/agents route in the API and updates the Dockerfile to copy the full ml/ tree with PYTHONPATH=/app so agent imports resolve correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
46
ml/agents/focus_area.py
Normal file
46
ml/agents/focus_area.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from typing import ClassVar
|
||||
from .base import BaseAgent, AgentInput, AgentOutput
|
||||
|
||||
|
||||
class FocusAreaAgent(BaseAgent):
|
||||
"""Identifies the most congested project/area in the user's task list."""
|
||||
agent_id: ClassVar[str] = "focus-area"
|
||||
ttl_seconds: ClassVar[int] = 43_200 # 12h
|
||||
version: ClassVar[str] = "1.0.0"
|
||||
|
||||
def compute(self, inp: AgentInput) -> AgentOutput:
|
||||
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})
|
||||
|
||||
# Score each project: overdue tasks count double
|
||||
def score(tasks: list[dict]) -> float:
|
||||
return sum(2.0 if t.get("is_overdue") else 1.0 for t in tasks)
|
||||
|
||||
top_project, top_tasks = max(by_project.items(), key=lambda kv: score(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)
|
||||
|
||||
parts = [
|
||||
f"The user's most congested area is {label} "
|
||||
f"({n} task{'s' if n != 1 else ''}, {overdue_in_top} overdue)."
|
||||
]
|
||||
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),
|
||||
}
|
||||
return self._make_output(inp, prompt, snapshot)
|
||||
Reference in New Issue
Block a user