diff --git a/cookbook/langgraph-vendor-agent/README.md b/cookbook/langgraph-vendor-agent/README.md index 313c8b7..8245d1e 100644 --- a/cookbook/langgraph-vendor-agent/README.md +++ b/cookbook/langgraph-vendor-agent/README.md @@ -9,7 +9,7 @@ START → planner → executor (react) → reviewer → (loop or compiler) → E ``` - **Planner**: Reads the task and produces a JSON plan of 4-5 steps -- **Executor**: A `create_react_agent` subgraph that handles multi-round tool calling per step +- **Executor**: A LangGraph react subgraph that handles multi-round tool calling per step - **Reviewer**: Marks step done, routes back to executor or forward to compiler - **Compiler**: Combines all step results into the final formatted response diff --git a/cookbook/langgraph-vendor-agent/src/langgraph_vendor_agent/plan_and_execute.py b/cookbook/langgraph-vendor-agent/src/langgraph_vendor_agent/plan_and_execute.py index bf8304b..ee7fe53 100644 --- a/cookbook/langgraph-vendor-agent/src/langgraph_vendor_agent/plan_and_execute.py +++ b/cookbook/langgraph-vendor-agent/src/langgraph_vendor_agent/plan_and_execute.py @@ -8,13 +8,22 @@ import json import operator +from importlib import import_module from typing import Annotated, Any, TypedDict from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import AIMessage, HumanMessage, SystemMessage from langchain_core.tools import BaseTool from langgraph.graph import END, START, StateGraph -from langgraph.prebuilt import create_react_agent + +# Prefer the new langchain.agents.create_agent; fall back to the legacy +# langgraph.prebuilt.create_react_agent for older installs. +_USE_NEW_CREATE_AGENT = False +try: + _create_agent_fn = import_module("langchain.agents").create_agent + _USE_NEW_CREATE_AGENT = True +except (ModuleNotFoundError, AttributeError): + _create_agent_fn = import_module("langgraph.prebuilt").create_react_agent class PlanStep(TypedDict): @@ -84,17 +93,17 @@ def build_plan_and_execute_graph( else: exec_tools = list(tools) - react_executor = create_react_agent( - model, - exec_tools, - prompt=SystemMessage( - content=( - "You are an execution agent. Use the available tools to " - "complete the step you are given. Call as many tools as needed. " - "After gathering data, summarize your findings." - ) - ), + executor_prompt = SystemMessage( + content=( + "You are an execution agent. Use the available tools to " + "complete the step you are given. Call as many tools as needed. " + "After gathering data, summarize your findings." + ) ) + if _USE_NEW_CREATE_AGENT: + react_executor = _create_agent_fn(model, tools=exec_tools, system_prompt=executor_prompt) + else: + react_executor = _create_agent_fn(model, exec_tools, prompt=executor_prompt) # ---- planner ---- def planner(state: PlanExecState) -> dict[str, Any]: diff --git a/cookbook/langgraph-vendor-agent/tests/test_create_agent_compat.py b/cookbook/langgraph-vendor-agent/tests/test_create_agent_compat.py new file mode 100644 index 0000000..58d83cf --- /dev/null +++ b/cookbook/langgraph-vendor-agent/tests/test_create_agent_compat.py @@ -0,0 +1,82 @@ +"""Tests for create_agent / create_react_agent compatibility in plan_and_execute.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from langchain_core.messages import SystemMessage + + +def test_build_graph_calls_create_fn_with_correct_kwarg() -> None: + """The executor node should pass system_prompt= or prompt= depending on the resolved API.""" + from langgraph_vendor_agent import plan_and_execute as module + + mock_fn = MagicMock(return_value=MagicMock()) + original_fn = module._create_agent_fn + original_flag = module._USE_NEW_CREATE_AGENT + + # --- new API path (system_prompt=) --- + module._create_agent_fn = mock_fn + module._USE_NEW_CREATE_AGENT = True + try: + model = MagicMock() + tool = MagicMock() + tool.name = "server__tool1" + tools = [tool] + module.build_plan_and_execute_graph(model, tools, safe_tool_filter=False) + _, kwargs = mock_fn.call_args + assert "system_prompt" in kwargs + assert isinstance(kwargs["system_prompt"], SystemMessage) + finally: + module._create_agent_fn = original_fn + module._USE_NEW_CREATE_AGENT = original_flag + + +def test_build_graph_calls_legacy_fn_with_prompt_kwarg() -> None: + """When falling back to create_react_agent, prompt= should be used.""" + from langgraph_vendor_agent import plan_and_execute as module + + mock_fn = MagicMock(return_value=MagicMock()) + original_fn = module._create_agent_fn + original_flag = module._USE_NEW_CREATE_AGENT + + module._create_agent_fn = mock_fn + module._USE_NEW_CREATE_AGENT = False + try: + model = MagicMock() + tool = MagicMock() + tool.name = "server__tool1" + tools = [tool] + module.build_plan_and_execute_graph(model, tools, safe_tool_filter=False) + _, kwargs = mock_fn.call_args + assert "prompt" in kwargs + assert isinstance(kwargs["prompt"], SystemMessage) + finally: + module._create_agent_fn = original_fn + module._USE_NEW_CREATE_AGENT = original_flag + + +def test_import_fallback_to_langgraph_prebuilt() -> None: + """When langchain.agents has no create_agent, we fall back to langgraph.prebuilt.""" + from importlib import import_module as real_import_module + + def fake_import_module(name: str) -> object: + if name == "langchain.agents": + mod = MagicMock(spec=[]) # spec=[] means no attributes + del mod.create_agent # ensure AttributeError + return mod + return real_import_module(name) + + with patch("langgraph_vendor_agent.plan_and_execute.import_module", fake_import_module): + # Re-run the import logic + try: + _create_agent_fn = getattr(fake_import_module("langchain.agents"), "create_agent") + use_new = True + except (ModuleNotFoundError, AttributeError): + _create_agent_fn = getattr(real_import_module("langgraph.prebuilt"), "create_react_agent") + use_new = False + + assert not use_new + # The fallback should resolve to the real create_react_agent + from langgraph.prebuilt import create_react_agent + assert _create_agent_fn is create_react_agent