refactor: migrate from create_react_agent to create_deep_agent

- Replace langgraph create_react_agent with deepagents create_deep_agent
- Sub-agents now configured as SubAgent dicts dispatched via built-in task tool
- Stream filter updated: langgraph_node 'agent' → 'model'
- Accept both AIMessage and AIMessageChunk in stream filter
- Collector only captures write mutations (insert/update/delete)
- Add deepagents>=0.4.10 to requirements.txt
This commit is contained in:
2026-03-12 01:21:14 +01:00
parent 706bf88883
commit 24a9c1b752
3 changed files with 70 additions and 81 deletions

View File

@@ -1,13 +1,16 @@
"""Deep Agent — LangGraph hierarchical supervisors for home and floating modes.
"""Deep Agent — ``create_deep_agent`` supervisors for home and floating modes.
Two supervisor graphs (both ``create_react_agent``):
Two supervisor graphs (via ``deepagents.create_deep_agent``):
* **HomeSupervisor** — gathers data from multiple domains, presents
structured overview with tool-result blocks.
structured overview with entity/chart tags.
* **FloatingSupervisor** — focused, scoped assistant for a single entity/domain.
Each supervisor delegates to four sub-agent tools, each a compiled
``create_react_agent`` wrapping the domain CRUD tools (task, project, note,
timeline). The sub-agents talk to Electron via ``execute_on_client``.
Each supervisor delegates to four sub-agents (task, project, note, timeline)
via the built-in ``task`` tool provided by ``SubAgentMiddleware``.
The sub-agents talk to Electron via ``execute_on_client``.
Built-in middleware provides: todo-list tracking, virtual filesystem,
automatic context summarisation, prompt-caching, and tool-call patching.
Streaming uses ``astream(stream_mode=["messages", "updates"])`` so that
callers can sniff:
@@ -24,9 +27,9 @@ import json
import logging
from typing import Any, AsyncGenerator
from langchain_core.messages import AIMessageChunk, HumanMessage
from deepagents import create_deep_agent
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from app.core.llm import get_llm
from app.core.ws_context import (
@@ -97,45 +100,23 @@ _PROJECT_TOOLS = [
_TIMELINE_TOOLS = [list_timelines, create_timeline, update_timeline, delete_timeline]
def _build_subagent_tool(
name: str,
description: str,
system_prompt: str,
tools: list,
):
"""Build a compiled sub-agent graph and wrap it as a LangChain tool."""
subgraph = create_react_agent(
model=get_llm(),
tools=tools,
prompt=system_prompt,
name=name,
)
def _make_subagent_specs() -> list[dict[str, Any]]:
"""Return SubAgent dicts for the four workspace domains.
@tool(name, description=description)
async def _run(query: str) -> str:
result = await subgraph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
messages = result["messages"]
# Return the last AI message content
for msg in reversed(messages):
if hasattr(msg, "content") and msg.content and not getattr(msg, "tool_calls", None):
return str(msg.content)
return "No response from sub-agent."
Each dict follows the ``deepagents`` ``SubAgent`` TypedDict:
name, description, system_prompt, tools, model
The model and middleware are filled in by ``create_deep_agent`` automatically.
"""
llm = get_llm()
return _run
def _make_subagent_tools() -> list:
"""Create the four sub-agent tools for the supervisor."""
return [
_build_subagent_tool(
name="task_agent",
description=(
{
"name": "task_agent",
"description": (
"Manages tasks and comments: list, create, update, delete, "
"due-today, comments. Delegate task-related queries here."
),
system_prompt=(
"system_prompt": (
"You are a task management assistant. You create, update, list, "
"and track tasks and their comments.\n\n"
"Rules:\n"
@@ -147,15 +128,16 @@ def _make_subagent_tools() -> list:
" - For update_task, use -1 for integer fields you do not want to change\n"
" - Always confirm the action in plain, user-friendly language."
),
tools=_TASK_TOOLS,
),
_build_subagent_tool(
name="note_agent",
description=(
"tools": _TASK_TOOLS,
"model": llm,
},
{
"name": "note_agent",
"description": (
"Manages notes: list, get, create, update, delete. "
"Delegate note-related queries here."
),
system_prompt=(
"system_prompt": (
"You are a note-taking assistant. You help users create, retrieve, "
"update, and delete Markdown notes in their workspace.\n\n"
"Rules:\n"
@@ -164,15 +146,16 @@ def _make_subagent_tools() -> list:
"content before appending or replacing sections\n"
" - Do not fabricate note content."
),
tools=_NOTE_TOOLS,
),
_build_subagent_tool(
name="project_agent",
description=(
"tools": _NOTE_TOOLS,
"model": llm,
},
{
"name": "project_agent",
"description": (
"Manages projects: list, get, create, update, archive, delete. "
"Delegate project-related queries here."
),
system_prompt=(
"system_prompt": (
"You are a project management assistant. You help users create, "
"find, update, and archive projects.\n\n"
"Rules:\n"
@@ -180,15 +163,16 @@ def _make_subagent_tools() -> list:
" - Prefer archiving over deletion\n"
" - ai_summary is populated only when the user asks for a summary."
),
tools=_PROJECT_TOOLS,
),
_build_subagent_tool(
name="timeline_agent",
description=(
"tools": _PROJECT_TOOLS,
"model": llm,
},
{
"name": "timeline_agent",
"description": (
"Manages project timelines (milestones): list, create, update, "
"delete. Delegate timeline/milestone queries here."
),
system_prompt=(
"system_prompt": (
"You are a project timeline assistant. Timelines are milestone "
"dates that track progress on a project.\n\n"
"Rules:\n"
@@ -197,8 +181,9 @@ def _make_subagent_tools() -> list:
" - For update_timeline, use -1 for integer fields you do not "
"want to change."
),
tools=_TIMELINE_TOOLS,
),
"tools": _TIMELINE_TOOLS,
"model": llm,
},
]
@@ -230,10 +215,10 @@ _HOME_SYSTEM = (
"You are Adiuva, a smart workspace assistant on the Home dashboard.\n"
"Your job is to help the user by gathering data from their workspace and "
"presenting a comprehensive overview.\n\n"
"You have sub-agent tools (task_agent, note_agent, project_agent, "
"timeline_agent) that can query and mutate workspace data. Delegate to "
"You have sub-agents (task_agent, note_agent, project_agent, "
"timeline_agent) accessible via the `task` tool. Delegate to "
"the appropriate sub-agent(s) based on the user's request. You can call "
"multiple sub-agents if needed.\n\n"
"multiple sub-agents in parallel if needed.\n\n"
"You also have an update_core_memory tool — use it when the user states "
"a preference or important fact worth remembering long-term.\n\n"
"## Entity References\n"
@@ -272,8 +257,8 @@ _FLOATING_SYSTEM = (
"You are Adiuva, a focused workspace assistant in the floating panel.\n"
"The user is currently working in the '{scope_type}' section"
"{scope_detail}.\n\n"
"You have sub-agent tools (task_agent, note_agent, project_agent, "
"timeline_agent) that can query and mutate workspace data. Focus your "
"You have sub-agents (task_agent, note_agent, project_agent, "
"timeline_agent) accessible via the `task` tool. Focus your "
"help on the user's current scope, but you can use other sub-agents "
"if the request requires it.\n\n"
"You also have an update_core_memory tool — use it when the user states "
@@ -307,18 +292,18 @@ def build_home_graph(
db_session_factory,
):
"""Build the Home supervisor graph."""
subagent_tools = _make_subagent_tools()
subagent_specs = _make_subagent_specs()
memory_tool = _make_update_core_memory_tool(user_id, db_session_factory)
all_tools = subagent_tools + [memory_tool]
prompt = _HOME_SYSTEM.format(
memory_context=_format_memory_context(memory_context),
)
return create_react_agent(
return create_deep_agent(
model=get_llm(),
tools=all_tools,
prompt=prompt,
tools=[memory_tool],
system_prompt=prompt,
subagents=subagent_specs,
name="home_supervisor",
)
@@ -330,9 +315,8 @@ def build_floating_graph(
db_session_factory,
):
"""Build the Floating supervisor graph."""
subagent_tools = _make_subagent_tools()
subagent_specs = _make_subagent_specs()
memory_tool = _make_update_core_memory_tool(user_id, db_session_factory)
all_tools = subagent_tools + [memory_tool]
scope_type = scope.get("type", "general")
scope_id = scope.get("id")
@@ -344,10 +328,11 @@ def build_floating_graph(
memory_context=_format_memory_context(memory_context),
)
return create_react_agent(
return create_deep_agent(
model=get_llm(),
tools=all_tools,
prompt=prompt,
tools=[memory_tool],
system_prompt=prompt,
subagents=subagent_specs,
name="floating_supervisor",
)
@@ -383,13 +368,16 @@ async def _run_graph_stream(
if stream_mode == "messages":
msg, metadata = chunk
# Only yield tokens from the supervisor's final response
# (not from sub-agent internal LLM calls)
# (not from sub-agent internal LLM calls).
# Accept both AIMessageChunk (streamed tokens) and AIMessage
# (full response from non-streaming providers).
# create_deep_agent names the LLM node "model".
if (
isinstance(msg, AIMessageChunk)
isinstance(msg, (AIMessage, AIMessageChunk))
and msg.content
and not msg.tool_calls
and isinstance(metadata, dict)
and metadata.get("langgraph_node") == "agent"
and metadata.get("langgraph_node") == "model"
):
yield ("token", str(msg.content))

View File

@@ -91,10 +91,10 @@ async def execute_on_client(
else:
logger.info("execute_on_client: got result type=%s keys=%s", type(result).__name__, list(result.keys()) if isinstance(result, dict) else "N/A")
collector = _tool_result_collector.get(None)
if collector is not None:
if collector is not None and action in ("insert", "update", "delete"):
collector.append({
"action": action,
"table": table,
"data": result,
"data": data or {},
})
return result

View File

@@ -5,6 +5,7 @@ langchain>=0.3.0
langchain-openai>=0.3.0
langchain-litellm>=0.1.0
langgraph>=0.3.0
deepagents>=0.4.10
litellm>=1.50.0
pydantic>=2.10.0
pydantic-settings>=2.7.0