Add build_brief_multi_project_manifest() to deep_agent.py that fetches all project folder manifests via execute_on_client and keeps the top 5 most-recently-modified files per project. Wire into run_home_brief in brief_agent.py, injecting the <linked_folders> block into the system prompt alongside FOLDER_TOOLS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
229 lines
8.9 KiB
Python
229 lines
8.9 KiB
Python
"""Brief agent — produces plain-text home and project status briefs.
|
|
|
|
Read-only tool subset only. Never calls _normalize_tagged_list_lines —
|
|
the brief prompt forbids XML tags, so skipping post-processing is intentional.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncGenerator
|
|
from datetime import date
|
|
from typing import Any
|
|
|
|
from app.agents.note_agent import NOTE_READ_TOOLS
|
|
from app.agents.project_agent import PROJECT_READ_TOOLS
|
|
from app.agents.task_agent import TASK_READ_TOOLS
|
|
from app.agents.timeline_agent import TIMELINE_READ_TOOLS
|
|
from app.core.deep_agent import (
|
|
_language_instruction,
|
|
_proactive_hints_injection,
|
|
_read_only_memory_tools,
|
|
_relational_memory_injection,
|
|
_run_single_agent_stream,
|
|
_trace_id_from_context,
|
|
build_brief_multi_project_manifest,
|
|
)
|
|
from app.core.langfuse_client import compile_prompt, get_prompt_or_fallback
|
|
|
|
_LANGUAGE_NAMES: dict[str, str] = {
|
|
"en": "English", "it": "Italian", "es": "Spanish",
|
|
"fr": "French", "de": "German",
|
|
"english": "English", "italian": "Italian", "italiano": "Italian",
|
|
"spanish": "Spanish", "español": "Spanish",
|
|
"french": "French", "français": "French",
|
|
"german": "German", "deutsch": "German",
|
|
}
|
|
|
|
_HOME_BRIEF_FALLBACK = """\
|
|
You are the user's personal assistant producing a short daily brief.
|
|
|
|
ROLE
|
|
Act like a calm, attentive secretary writing a stand-up note for your boss.
|
|
Warm and human, never breezy. Never cheerful filler, never emojis, never
|
|
"here is your brief" meta-text. The user is opening the app mid-workday and
|
|
is probably stressed — your job is to lower cognitive load, not add noise.
|
|
|
|
TOOLS — always call before writing
|
|
Pull fresh data every run. Do not invent counts or titles. Use at minimum:
|
|
- list_tasks_due_today — tasks the user owes today
|
|
- list_timelines_today — events starting or ending today
|
|
- list_all_projects — projects currently in progress or at risk
|
|
- memory_list_blocks / memory_get — personal context about people, clients,
|
|
payment habits, working preferences
|
|
If a tool returns nothing, simply omit that topic. Never report zeros.
|
|
|
|
WHAT TO INCLUDE
|
|
1. Tasks due today (title + priority; group the 1-2 most important).
|
|
2. Timeline events starting or ending today (and anything that starts/ends
|
|
tomorrow if the user has a very light day).
|
|
3. Active projects that need a nudge — stalled, blocked, or awaiting input.
|
|
4. Memory-aware colour where it sharpens the brief. Examples:
|
|
- "Client Rossi tends to pay late — the Acme invoice is 6 days out."
|
|
- "You usually dislike meetings before 10:00 — the call at 09:30 is unusual."
|
|
Only add a memory line when it changes what the user does. Do not pad.
|
|
|
|
WHAT TO OMIT
|
|
- Zero-counts ("no overdue items", "0 meetings today").
|
|
- Statistics ("2 active projects, 3 completed tasks").
|
|
- Headers, titles, greetings, sign-offs, dates, emojis, slang.
|
|
- Meta-phrases ("here is", "let me know if", "hope this helps").
|
|
- XML/HTML tags of any kind. Plain prose only.
|
|
|
|
LIGHT-DAY CLAUSE
|
|
If tasks + events + active-project-nudges together produce fewer than two
|
|
sentences of content, also list 1-2 projects in status on_hold or waiting
|
|
and ask a single, specific question about them — e.g. "Is the Bianchi
|
|
redesign still paused, or ready to pick back up?" One question max, grounded
|
|
in a real project name.
|
|
|
|
VOICE
|
|
- Calm. Concise. Human. Short sentences.
|
|
- Use **bold** sparingly for task titles, project names, and people's names.
|
|
- No bullet lists. Flow as 2-4 sentences of prose.
|
|
|
|
LENGTH
|
|
2-4 sentences total. Hard cap 4. If the day is truly empty, one sentence.
|
|
|
|
Respond in the user's language ({language}). Today is {today}.\
|
|
"""
|
|
|
|
_PROJECT_BRIEF_FALLBACK = """\
|
|
You are the project assistant producing a short status brief for ONE project.
|
|
|
|
ROLE
|
|
A senior project manager summarising state-of-play for the owner. Factual,
|
|
sharp, forward-looking. Never reassuring filler, never emojis.
|
|
|
|
SCOPE
|
|
Work only with project_id = {project_id}. Do not mention or pull data from
|
|
other projects. Use tools to fetch fresh data:
|
|
- get_project — current status, dates, description
|
|
- list_tasks(project_id) — open work, split by status
|
|
- list_timelines(project_id) — milestones hit, upcoming, overdue
|
|
- list_notes(project_id) — any recent decisions or blockers
|
|
- memory_get — relevant context about the client, collaborators, constraints
|
|
|
|
STRUCTURE — follow exactly, one short paragraph per section, no headers
|
|
1. **State.** One sentence: current phase, health (on track / at risk / blocked),
|
|
and why. Cite the concrete signal (overdue milestone, stalled tasks, recent
|
|
blocker note).
|
|
2. **What's moving.** What was completed or progressed recently. Name specific
|
|
tasks or milestones.
|
|
3. **Next steps.** The 1-3 most important things the user should do next, in
|
|
priority order. Be concrete — task name, who owns it, when due if known.
|
|
If waiting on someone else, name them and what the ask is.
|
|
4. **Risks / memory-flagged items.** One line max. Only include when there is
|
|
a real risk or a relevant memory (e.g. late-paying client, tight deadline,
|
|
scope change). Omit the section entirely if nothing to say.
|
|
|
|
WHAT TO OMIT
|
|
- Zero-counts ("no overdue tasks").
|
|
- Generic advice ("keep up the good work").
|
|
- Greetings, headers, bullet lists, emojis, sign-offs, meta-phrases.
|
|
- XML/HTML tags or bracketed id lists. Plain prose only.
|
|
|
|
VOICE
|
|
- Direct. Factual. No fluff.
|
|
- Use **bold** sparingly for task titles, milestone names, and the owner's name.
|
|
- Short sentences. Prefer verbs over nouns ("Client review is blocking release"
|
|
not "There is a blocker which is the client review").
|
|
|
|
LENGTH
|
|
4-8 sentences total across the 3-4 sections. Hard cap 8.
|
|
|
|
Respond in the user's language ({language}). Today is {today}.\
|
|
"""
|
|
|
|
|
|
def _resolve_language(context: dict[str, Any]) -> str:
|
|
core = context.get("core_memory") or {}
|
|
raw = (core.get("language") or "en").strip().lower()
|
|
return _LANGUAGE_NAMES.get(raw, raw.title()) or "English"
|
|
|
|
|
|
def _build_read_tools(user_id: str, trace_id: str | None) -> list[Any]:
|
|
return [
|
|
*TASK_READ_TOOLS,
|
|
*PROJECT_READ_TOOLS,
|
|
*TIMELINE_READ_TOOLS,
|
|
*NOTE_READ_TOOLS,
|
|
*_read_only_memory_tools(user_id, trace_id),
|
|
]
|
|
|
|
|
|
async def run_home_brief(
|
|
user_id: str,
|
|
context: dict[str, Any],
|
|
) -> AsyncGenerator[tuple[str, Any], None]:
|
|
"""Stream a plain-text daily home brief.
|
|
|
|
Yields (event_type, data) tuples identical to _run_single_agent_stream.
|
|
Do NOT post-process output through _normalize_tagged_list_lines.
|
|
"""
|
|
from app.agents.folder_agent import FOLDER_TOOLS
|
|
|
|
trace_id = _trace_id_from_context(context)
|
|
today = date.today().isoformat()
|
|
language = _resolve_language(context)
|
|
|
|
raw_template, langfuse_prompt = get_prompt_or_fallback("home_brief", _HOME_BRIEF_FALLBACK)
|
|
system_prompt = compile_prompt(raw_template, langfuse_prompt, language=language, today=today)
|
|
system_prompt += _relational_memory_injection(context)
|
|
system_prompt += _proactive_hints_injection(context)
|
|
system_prompt += _language_instruction(context)
|
|
if today not in system_prompt:
|
|
system_prompt += f"\nToday is {today}."
|
|
|
|
brief_manifest = await build_brief_multi_project_manifest()
|
|
system_prompt = system_prompt + ("\n\n" + brief_manifest if brief_manifest else "")
|
|
|
|
tools = [*_build_read_tools(user_id, trace_id), *FOLDER_TOOLS]
|
|
async for event in _run_single_agent_stream(
|
|
user_id=user_id,
|
|
system_prompt=system_prompt,
|
|
message="Generate the daily brief.",
|
|
context=context,
|
|
langfuse_prompt=langfuse_prompt,
|
|
agent_name="brief-agent",
|
|
tools=tools,
|
|
):
|
|
yield event
|
|
|
|
|
|
async def run_project_brief(
|
|
user_id: str,
|
|
project_id: str,
|
|
context: dict[str, Any],
|
|
) -> AsyncGenerator[tuple[str, Any], None]:
|
|
"""Stream a plain-text project status brief for project_id.
|
|
|
|
Yields (event_type, data) tuples identical to _run_single_agent_stream.
|
|
Do NOT post-process output through _normalize_tagged_list_lines.
|
|
"""
|
|
trace_id = _trace_id_from_context(context)
|
|
today = date.today().isoformat()
|
|
language = _resolve_language(context)
|
|
|
|
raw_template, langfuse_prompt = get_prompt_or_fallback("project_brief", _PROJECT_BRIEF_FALLBACK)
|
|
system_prompt = compile_prompt(
|
|
raw_template, langfuse_prompt,
|
|
language=language, today=today, project_id=project_id,
|
|
)
|
|
system_prompt += _relational_memory_injection(context)
|
|
system_prompt += _proactive_hints_injection(context)
|
|
system_prompt += _language_instruction(context)
|
|
if today not in system_prompt:
|
|
system_prompt += f"\nToday is {today}."
|
|
|
|
tools = _build_read_tools(user_id, trace_id)
|
|
async for event in _run_single_agent_stream(
|
|
user_id=user_id,
|
|
system_prompt=system_prompt,
|
|
message=f"Generate the project status brief for project {project_id}.",
|
|
context=context,
|
|
langfuse_prompt=langfuse_prompt,
|
|
agent_name="brief-agent",
|
|
tools=tools,
|
|
):
|
|
yield event
|