Source code for langgraph_agent_toolkit.agents.components.middlewares.immediate_generation

"""Middleware that forces a direct, tool-free answer when the model-call budget is nearly used up."""

from collections.abc import Awaitable, Callable

from langchain.agents.middleware import AgentMiddleware
from langchain.agents.middleware.types import ModelRequest, ModelResponse
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage

from langgraph_agent_toolkit.core.settings import settings


# Worded to make the model *answer now from what it already has* — not as a prohibition. Phrasing it
# as "DO NOT make tool calls" makes some models reply "I can't search", refusing instead of
# synthesizing even when the tool results are right there in the context.
DEFAULT_IMMEDIATE_INSTRUCTION = (
    "You have gathered enough information and reached the tool-use budget for this task. "
    "Provide your best final answer now, using the information already collected in this "
    "conversation, including any tool results above. Do not request additional tools; if some "
    "details are missing, answer with what you have and clearly note any gaps."
)


[docs] class ImmediateGenerationMiddleware(AgentMiddleware): """Force a final, tool-free answer on the last allowed model call of a run. Mirrors the custom ``create_react_agent``'s ``immediate_generation`` router: rather than running out of steps mid-tool-loop (returning "need more steps" or hitting the recursion limit), the model is asked to synthesize a direct answer from what it already has once it reaches the budget. The tool *results* are always kept — only ``tools`` and the system message are changed — so the model answers from the data it already collected. The budget is the number of model calls already made in the current run (AI messages since the latest human message), so the middleware needs no extra state. It should fire only as a graceful fallback *near the recursion limit*, not cut off legitimate multi-step work, so ``model_call_limit`` defaults to about half the recursion limit (a model→tools cycle is ~2 steps): with the default recursion limit of 64 that is 32 model calls. """
[docs] def __init__(self, model_call_limit: int | None = None, instruction: str | None = None) -> None: super().__init__() if model_call_limit is None: model_call_limit = max(1, settings.DEFAULT_RECURSION_LIMIT // 2) if model_call_limit < 1: raise ValueError("model_call_limit must be >= 1") self.model_call_limit = model_call_limit self.instruction = instruction or DEFAULT_IMMEDIATE_INSTRUCTION
def _calls_made_this_run(self, messages: list[BaseMessage]) -> int: """Count AI messages since the latest human message (= model calls already made this run).""" count = 0 for message in reversed(messages): if isinstance(message, HumanMessage): break if isinstance(message, AIMessage): count += 1 return count def _force_immediate(self, request: ModelRequest) -> ModelRequest: """Strip tools and append the synthesize-now instruction to the system message.""" existing = request.system_message.content if request.system_message else "" merged = f"{existing}\n\n{self.instruction}".strip() if existing else self.instruction return request.override(tools=[], system_message=SystemMessage(content=merged))
[docs] def wrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse], ) -> ModelResponse: if self._calls_made_this_run(request.messages) >= self.model_call_limit - 1: request = self._force_immediate(request) return handler(request)
[docs] async def awrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], Awaitable[ModelResponse]], ) -> ModelResponse: if self._calls_made_this_run(request.messages) >= self.model_call_limit - 1: request = self._force_immediate(request) return await handler(request)