Add task brief research agent: Stage 1 deep-research + canvas draft emission
- run_task_brief_research() runner with brief-specific tool set and max_steps=12 - New agents: client_agent (list_clients, get_client) and relations_agent (query_relations) - search_associative tool wrapping MemoryMiddleware semantic search - BRIEF_RESEARCH_TOOLS constant: read-only task/project/note/timeline + memory + client/relations - canvas block extraction in output_formatter (splits visible text from <canvas> draft) - device_ws.py: task_brief_research request type; emits canvas_draft mutation on stream_end - Stage 2 briefMode: briefing_context injected into floating system prompt when present - briefingContext kwarg wired through compile_prompt call chain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
52
app/agents/client_agent.py
Normal file
52
app/agents/client_agent.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Client agent — read-only tools for the clients table."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from app.core.ws_context import execute_on_client
|
||||
|
||||
|
||||
@tool
|
||||
async def list_clients(search: str = "", limit: int = 20) -> str:
|
||||
"""List clients, optionally filtered by a name/email substring search.
|
||||
|
||||
search: optional substring to match against client name or email.
|
||||
limit: max rows to return (default 20).
|
||||
"""
|
||||
filters: dict[str, Any] = {"limit": limit}
|
||||
if search:
|
||||
filters["search"] = search
|
||||
|
||||
result = await execute_on_client(action="select", table="clients", filters=filters)
|
||||
rows = result.get("rows", [])
|
||||
if not rows:
|
||||
return "No clients found."
|
||||
lines = [
|
||||
f"- {r.get('name', '?')} (id: {r.get('id')}, email: {r.get('email', '')}, "
|
||||
f"company: {r.get('company', '')})"
|
||||
for r in rows
|
||||
]
|
||||
return f"Found {len(rows)} client(s):\n" + "\n".join(lines)
|
||||
|
||||
|
||||
@tool
|
||||
async def get_client(id: str) -> str:
|
||||
"""Get full details for one client by UUID.
|
||||
|
||||
id: the client's UUID.
|
||||
"""
|
||||
if not id:
|
||||
return "Client id is required."
|
||||
|
||||
result = await execute_on_client(action="get", table="clients", data={"id": id})
|
||||
row = result.get("row") or result.get("rows", [None])[0] if result else None
|
||||
if not row:
|
||||
return f"Client '{id}' not found."
|
||||
return f"Client details:\n{json.dumps(row, ensure_ascii=False, indent=2)}"
|
||||
|
||||
|
||||
CLIENT_TOOLS: list[Any] = [list_clients, get_client]
|
||||
63
app/agents/relations_agent.py
Normal file
63
app/agents/relations_agent.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Relations agent — read-only tool wrapping MemoryMiddleware.query_relations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from app.core.memory_middleware import MemoryMiddleware
|
||||
from app.db import async_session
|
||||
|
||||
# Injected at tool-factory time by _brief_research_tools(); not a module-level global.
|
||||
# Each tool closure captures the user_id bound at factory time.
|
||||
|
||||
|
||||
def make_query_relations_tool(user_id: str, trace_id: str | None = None) -> Any:
|
||||
"""Return a query_relations tool bound to *user_id*."""
|
||||
|
||||
@tool
|
||||
async def query_relations(
|
||||
subject_label: str = "",
|
||||
predicate: str = "",
|
||||
object_label: str = "",
|
||||
limit: int = 10,
|
||||
) -> str:
|
||||
"""Query the relational memory graph for entity relationships.
|
||||
|
||||
Returns rows where subject ↔ predicate ↔ object match the given filters.
|
||||
All parameters are optional — omit to retrieve all relations up to limit.
|
||||
|
||||
subject_label: entity label on the left side (e.g. a client name, "Acme Corp").
|
||||
predicate: relationship type (e.g. "mentioned_in", "works_at", "related_to").
|
||||
object_label: entity label on the right side (e.g. a project name, "Website Redesign").
|
||||
limit: max rows to return (default 10).
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
"relations_agent: query_relations trace=%s user=%s subject=%r predicate=%r object=%r",
|
||||
trace_id or "-", user_id, subject_label, predicate, object_label,
|
||||
)
|
||||
|
||||
async with async_session() as db:
|
||||
memory = MemoryMiddleware(db)
|
||||
rows = await memory.query_relations(
|
||||
user_id=user_id,
|
||||
subject=subject_label or None,
|
||||
predicate=predicate or None,
|
||||
object_=object_label or None,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
if not rows:
|
||||
return "No relational memory entries found for the given filters."
|
||||
|
||||
lines = [
|
||||
f"- {r.subject_label} —[{r.predicate}]→ {r.object_label}"
|
||||
+ (f" (confidence: {r.confidence:.2f})" if r.confidence is not None else "")
|
||||
for r in rows
|
||||
]
|
||||
return f"Found {len(rows)} relation(s):\n" + "\n".join(lines)
|
||||
|
||||
return query_relations
|
||||
Reference in New Issue
Block a user