From 24a9c1b752df6d27469c7a6842df2627c2902b11 Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 12 Mar 2026 01:21:14 +0100 Subject: [PATCH] refactor: migrate from create_react_agent to create_deep_agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/core/deep_agent.py | 146 +++++++++++++++++++---------------------- app/core/ws_context.py | 4 +- requirements.txt | 1 + 3 files changed, 70 insertions(+), 81 deletions(-) diff --git a/app/core/deep_agent.py b/app/core/deep_agent.py index 8eff8b0..c7c4086 100644 --- a/app/core/deep_agent.py +++ b/app/core/deep_agent.py @@ -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)) diff --git a/app/core/ws_context.py b/app/core/ws_context.py index 1dd6eec..af56d2b 100644 --- a/app/core/ws_context.py +++ b/app/core/ws_context.py @@ -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 diff --git a/requirements.txt b/requirements.txt index a3b88ce..5d993c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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