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:
2026-05-04 10:20:05 +00:00
parent f8d66aa01f
commit b3cf588f2f
14 changed files with 667 additions and 2 deletions

42
ml/agents/overdue_task.py Normal file
View File

@@ -0,0 +1,42 @@
from __future__ import annotations
from typing import ClassVar
from .base import BaseAgent, AgentInput, AgentOutput
class OverdueTaskAgent(BaseAgent):
"""Reports the user's overdue tasks by count and age."""
agent_id: ClassVar[str] = "overdue-task"
ttl_seconds: ClassVar[int] = 3600 # 1h — overdue status changes infrequently
version: ClassVar[str] = "1.0.0"
def compute(self, inp: AgentInput) -> AgentOutput:
overdue = [t for t in inp.tasks if t.get("is_overdue")]
top = sorted(overdue, key=lambda t: -t.get("task_age_days", 0))[:3]
if not overdue:
prompt = "The user has no overdue tasks at this time."
elif len(overdue) == 1:
t = top[0]
age = round(t.get("task_age_days", 0))
prompt = (
f'The user has 1 overdue task: "{t["content"]}" '
f"({age} day{'s' if age != 1 else ''} overdue)."
)
else:
items = ", ".join(
f'"{t["content"]}" ({round(t.get("task_age_days", 0))}d)'
for t in top
)
prompt = (
f"The user has {len(overdue)} overdue tasks. "
f"Top {len(top)}: {items}."
)
snapshot = {
"overdue_count": len(overdue),
"top_overdue": [
{"content": t["content"], "task_age_days": t.get("task_age_days", 0)}
for t in top
],
}
return self._make_output(inp, prompt, snapshot)