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

View File

@@ -108,6 +108,52 @@ PROMPTS: dict[str, Prompt] = {
}
# ── v4-orchestrator ────────────────────────────────────────────────────────
# Not a Prompt entry — takes pre-computed agent snippets, not a _Ctx.
_SYS_V4_ORCHESTRATOR = (
"You are a personal advisor generating a single, perfectly-timed tip. "
"Multiple specialized agents have analyzed the user's current context and provided "
"their insights below. Synthesize their combined perspective to generate exactly ONE "
"tip that is specific, actionable, and relevant right now. "
"Respond ONLY with a JSON object with keys: "
'"id" (short slug), "content" (the tip, ≤2 sentences), '
'"rationale" (why now, ≤1 sentence). '
"No markdown, no prose outside the JSON object."
)
def build_orchestrator_messages(
agent_outputs: list[dict],
tasks: list[dict],
hour_of_day: int,
day_of_week: int,
) -> list[dict]:
"""Build the [system, user] message list for the orchestrator LLM call.
agent_outputs: list of {agent_id, prompt_text} dicts.
Falls back to raw task summary when agent_outputs is empty.
"""
lines = [f"Current time: {hour_of_day:02d}:00, day_of_week={day_of_week}", ""]
if agent_outputs:
lines.append("Context from analysis agents:")
for s in agent_outputs:
lines.append(f"[{s['agent_id']}] {s['prompt_text']}")
else:
overdue = [t for t in tasks if t.get("is_overdue")]
lines.append(
f"No pre-computed agent context available. "
f"Tasks: {len(tasks)} total, {len(overdue)} overdue."
)
for t in tasks[:3]:
lines.append(f" - {t.get('content', '?')}")
lines.append("\nGenerate one tip as a JSON object.")
return [
{"role": "system", "content": _SYS_V4_ORCHESTRATOR},
{"role": "user", "content": "\n".join(lines)},
]
def default_version() -> str:
return os.getenv("DEFAULT_PROMPT_VERSION", "v1")