feat(api): multi-project folder manifest for daily brief
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>
This commit is contained in:
@@ -21,6 +21,7 @@ from app.core.deep_agent import (
|
|||||||
_relational_memory_injection,
|
_relational_memory_injection,
|
||||||
_run_single_agent_stream,
|
_run_single_agent_stream,
|
||||||
_trace_id_from_context,
|
_trace_id_from_context,
|
||||||
|
build_brief_multi_project_manifest,
|
||||||
)
|
)
|
||||||
from app.core.langfuse_client import compile_prompt, get_prompt_or_fallback
|
from app.core.langfuse_client import compile_prompt, get_prompt_or_fallback
|
||||||
|
|
||||||
@@ -159,6 +160,8 @@ async def run_home_brief(
|
|||||||
Yields (event_type, data) tuples identical to _run_single_agent_stream.
|
Yields (event_type, data) tuples identical to _run_single_agent_stream.
|
||||||
Do NOT post-process output through _normalize_tagged_list_lines.
|
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)
|
trace_id = _trace_id_from_context(context)
|
||||||
today = date.today().isoformat()
|
today = date.today().isoformat()
|
||||||
language = _resolve_language(context)
|
language = _resolve_language(context)
|
||||||
@@ -171,7 +174,10 @@ async def run_home_brief(
|
|||||||
if today not in system_prompt:
|
if today not in system_prompt:
|
||||||
system_prompt += f"\nToday is {today}."
|
system_prompt += f"\nToday is {today}."
|
||||||
|
|
||||||
tools = _build_read_tools(user_id, trace_id)
|
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(
|
async for event in _run_single_agent_stream(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
|
|||||||
@@ -110,6 +110,35 @@ async def _fetch_project_manifest(project_id: str) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def build_brief_multi_project_manifest() -> str:
|
||||||
|
"""Build a compact multi-project manifest for the daily brief agent.
|
||||||
|
|
||||||
|
Calls execute_on_client('list_projects_with_folder_manifests') and keeps
|
||||||
|
the top 5 most-recently-modified files per project.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = await execute_on_client(
|
||||||
|
action="list_projects_with_folder_manifests",
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
projects = (result or {}).get("projects") or []
|
||||||
|
if not projects:
|
||||||
|
return ""
|
||||||
|
blocks: list[str] = ["<linked_folders>"]
|
||||||
|
for p in projects:
|
||||||
|
files = sorted(p.get("files", []), key=lambda f: f.get("mtimeMs", 0), reverse=True)[:5]
|
||||||
|
if not files:
|
||||||
|
continue
|
||||||
|
blocks.append(f"project: {p.get('projectName','?')} [{p.get('projectId','?')}]")
|
||||||
|
blocks.append(f" path: {p.get('folderPath','?')} (scanned {p.get('lastScannedAt','?')})")
|
||||||
|
for f in files:
|
||||||
|
blocks.append(f" - /{f['relPath']} [{f.get('kind','text')}] {f.get('summary','')}")
|
||||||
|
blocks.append("</linked_folders>")
|
||||||
|
return "\n".join(blocks)
|
||||||
|
|
||||||
|
|
||||||
def _datetime_context_injection(context: dict[str, Any]) -> str:
|
def _datetime_context_injection(context: dict[str, Any]) -> str:
|
||||||
"""Build a comprehensive DATE CONTEXT block with pre-computed ms-epoch boundaries for common ranges."""
|
"""Build a comprehensive DATE CONTEXT block with pre-computed ms-epoch boundaries for common ranges."""
|
||||||
fp = context.get("format_prefs")
|
fp = context.get("format_prefs")
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.core.deep_agent import format_folder_manifest, MANIFEST_TOKEN_BUDGET
|
from app.core.deep_agent import format_folder_manifest, MANIFEST_TOKEN_BUDGET
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
def test_format_folder_manifest_basic():
|
def test_format_folder_manifest_basic():
|
||||||
manifest = {
|
manifest = {
|
||||||
@@ -33,3 +39,31 @@ def test_format_folder_manifest_truncates_past_budget():
|
|||||||
def test_format_folder_manifest_null_returns_empty():
|
def test_format_folder_manifest_null_returns_empty():
|
||||||
assert format_folder_manifest(None) == ""
|
assert format_folder_manifest(None) == ""
|
||||||
assert format_folder_manifest({"files": []}) == ""
|
assert format_folder_manifest({"files": []}) == ""
|
||||||
|
|
||||||
|
|
||||||
|
async def test_brief_multi_project_manifest_top_5_per_project():
|
||||||
|
fake_response = [
|
||||||
|
{
|
||||||
|
"projectId": "p1", "projectName": "Acme", "folderPath": "/a",
|
||||||
|
"lastScannedAt": "now",
|
||||||
|
"files": [
|
||||||
|
{"relPath": f"f{i}.md", "kind": "text", "summary": "s", "mtimeMs": i}
|
||||||
|
for i in range(10)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"projectId": "p2", "projectName": "Beta", "folderPath": "/b",
|
||||||
|
"lastScannedAt": "now",
|
||||||
|
"files": [{"relPath": "x.md", "kind": "text", "summary": "s", "mtimeMs": 1}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
with patch(
|
||||||
|
"app.core.deep_agent.execute_on_client",
|
||||||
|
new=AsyncMock(return_value={"projects": fake_response}),
|
||||||
|
):
|
||||||
|
from app.core.deep_agent import build_brief_multi_project_manifest
|
||||||
|
out = await build_brief_multi_project_manifest()
|
||||||
|
# Project 1 has 10 files, only top 5 by mtimeMs should appear
|
||||||
|
assert out.count("[p1]") <= 5
|
||||||
|
# Project 2 has 1 file, must appear
|
||||||
|
assert "[p2]" in out or "Beta" in out
|
||||||
|
|||||||
Reference in New Issue
Block a user