LangGraph seems to ignore tool outputs with Gemini models

I’m experiencing an issue where the LLM correctly invokes a tool (tool call is successful), but it appears unable to interpret or process the tool’s result.

Here is the code to reproduce the issue:

import asyncio
import base64
import os
from pathlib import Path
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langgraph.prebuilt import ToolNode

# ============== CONFIGURATION ==============
LLM_MODEL = "gemini-2.5-flash"
LLM_MODEL_PROVIDER = "google_genai"
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")

# Path to test file
SCRIPT_DIR = Path(__file__).parent
TEST_FILE = SCRIPT_DIR / "test.pdf"


# ============== FAKE DATA ==============
FAKE_CLIENTS = [
    {"id": "f4764430-5add-446f-b35b-a9c4e272c27c", "name": "Apple"},
]


# ============== TOOLS ==============
@tool
def get_client(company_name: str) -> str:
    """
    Search for a client by name.
    Returns matching clients or empty if not found.
    """
    return FAKE_CLIENTS


TOOLS = [get_client]


# ============== STATE ==============
class AgentState(MessagesState):
    """Simple state with just messages."""
    pass


# ============== SYSTEM PROMPT ==============
SYSTEM_PROMPT = """You are an order entry assistant. Extract client information from documents.

TASK:
1. Look at the provided document/text and find the client/customer name
2. Call get_client with the client name you found
3. Report the result (client name and its ID)

IMPORTANT: 
- Read tool responses carefully
- The tool returns a list of similar companies. Evaluate the results:
    - SUCCESS: If any result has a company name that is clearly the same entity (ignore minor formatting like spaces, periods, capitalization), that IS a match. Use that result's `id` and report success.
    - FAILURE: Only if the list is empty OR the names are completely different companies.
"""


# ============== LLM ==============
llm = init_chat_model(
    model=LLM_MODEL,
    model_provider=LLM_MODEL_PROVIDER,
    google_api_key=GEMINI_API_KEY,
)
llm_with_tools = llm.bind_tools(TOOLS)

# ============== NODES ==============
async def process_node(state: AgentState) -> AgentState:
    """Main processing node - calls LLM with tools."""
    print("\n>>> PROCESS NODE")
    print(f"    Messages count: {len(state['messages'])}")
    
    # Log last message
    last_msg = state["messages"][-1]
    print(f"    Last message type: {type(last_msg).__name__}")
    if hasattr(last_msg, 'content'):
        if isinstance(last_msg.content, str):
            print(f"    Last message content: {last_msg.content[:100]}...")
        else:
            print(f"    Last message content: [multipart content]")
    
    result = await llm_with_tools.ainvoke(state["messages"])
    
    print(f"    LLM response content: {result.content[:100] if result.content else '(empty)'}...")
    print(f"    Tool calls: {len(result.tool_calls) if result.tool_calls else 0}")
    if result.tool_calls:
        for tc in result.tool_calls:
            print(f"      - {tc['name']}: {tc['args']}")
    
    return {"messages": [result]}

# ============== ROUTING ==============
def route_tools(state: AgentState) -> str:
    """Route to tools if there are tool calls, otherwise to finalize."""
    messages = state.get("messages", [])
    if not messages:
        return END
    
    last_message = messages[-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        print("    -> Routing to: tools")
        return "tools"
    
    print("    -> Routing to: finalize")
    return "finalize"

# ============== BUILD GRAPH ==============
def build_graph():
    """Build the agent graph."""
    builder = StateGraph(AgentState)
    
    tool_node = ToolNode(TOOLS)
    builder.add_node("process", process_node)
    builder.add_node("tools", tool_node)
    builder.add_edge(START, "process")
    builder.add_conditional_edges("process", route_tools, {
        "tools": "tools",
        "finalize": END,
    })
    builder.add_edge("tools", "process")
    
    return builder.compile()

# ============== FILE READING ==============
def read_file_as_base64(filepath: Path) -> tuple[str, str]:
    """Read file and return (base64_content, mime_type)."""
    with open(filepath, "rb") as f:
        content = f.read()
    
    base64_content = base64.b64encode(content).decode('utf-8')
    
    # Detect mime type
    if content.startswith(b'%PDF'):
        mime_type = 'application/pdf'
    elif content.startswith(b'\xff\xd8\xff'):
        mime_type = 'image/jpeg'
    elif content.startswith(b'\x89PNG'):
        mime_type = 'image/png'
    else:
        mime_type = 'application/pdf'  # default
    
    return base64_content, mime_type

def create_file_message(filepath: Path) -> HumanMessage:
    """Create a HumanMessage with file content."""
    base64_content, mime_type = read_file_as_base64(filepath)
    
    # Use proper LangChain format for multimodal content
    # For images: image_url type with data URL
    # For PDFs: Gemini supports PDFs via image_url format
    content = [
        {"type": "text", "text": f"Process this document and find the client: {filepath.name}"},
        {"type": "file", "source_type": "base64", "mime_type": mime_type, "data": base64_content}
    ]
    
    return HumanMessage(content=content)

# ============== MAIN ==============
async def run_agent(filepath: Path = None, text_input: str = None):
    """Run the agent with file and/or text input."""
    graph = build_graph()
    
    # Build messages
    messages = [SystemMessage(content=SYSTEM_PROMPT)]
    
    if filepath and filepath.exists():
        print(f"Reading file: {filepath}")
        messages.append(create_file_message(filepath))
    elif text_input:
        messages.append(HumanMessage(content=text_input))
    else:
        print("No input provided!")
        return
    
    initial_state = {"messages": messages}
    
    # Run graph
    config = {"configurable": {"thread_id": "debug-1"}, "recursion_limit": 20}
    result = await graph.ainvoke(initial_state, config=config)   
    return result


async def main():
    # Try file first, fall back to text
    if TEST_FILE.exists():
        await run_agent(filepath=TEST_FILE)
    else:
        print(f"File not found: {TEST_FILE}")
        print("Using text input instead...\n")
        await run_agent(text_input="Find the id of the client Apple")


if __name__ == "__main__":
    asyncio.run(main())

The issue seems to be linked with the presence of a file, since when called in textual mode everything works. I cannot upload pdf files, but the test.pdf files is nothing but this:

The agent properly read the document and identifies Apple as the target company. Nevertheless it’s not able to perform the match with the tool ouput. My environment:

langchain==1.2.0
langchain-core==1.2.4
langchain-google-genai==4.1.2
langgraph==1.0.5
google-genai==1.56.0

hi @mcec82

IMHO, LangGraph is not ignoring your tool outputs -this is a quirk of how Gemini handles multimodal file blocks together with tools. The same graph works in text-only mode because then the model is in a “plain text + tools” configuration, which Gemini handles more reliably. With the inline PDF, the model sees a file_data part plus tools, and its behavior around reading the tool result becomes much less consistent.

I think the cleanest workaround (and the one that mirrors LangChain’s own tests) is to pass the PDF as an image_url data URI instead of a "file" + source_type: base64 block. Once you do that, the model will again reliably use the get_client output.

Could you try it and give a feedback?

Hi pawel-twardziak,

Unfortunately it doesn’t work. This is how I changed the file function:

def create_file_message(filepath: Path) → HumanMessage:

"""Create a HumanMessage with file content."""

base64_content, mime_type = read_file_as_base64(filepath)

data_uri = f"data:{mime_type};base64,{base64_content}"

content = [
    {"type": "text", "text": f"Process this document and find the client: {filepath.name}"},
    {"type": "image_url", "image_url": {"url": data_uri}}
]



return HumanMessage(content=content)

I’m not 100% sure this is what you were suggesting, but I assume the recommendation was not to upload the file remotely and then pass the uri, which would be very limiting since it would need an ad hoc remote storage.

I meant upload the file to any storage and take the public url (not the file content itself).
Are you able to try it?

As mentioned, unfortunately, that is not a viable option for me. I would assume that the public uri would work, but we are trying to migrate our existing custom google-genai code to langgraph and introducing a public storage system would be too disruptive. Hope that there is a workaround for this.