trustcall

repository·main·Indexed 22 days ago

https://github.com/hinthornw/trustcall

A library built on LangGraph that enables LLMs to interact with large JSON structures by generating JSON patch operations instead of full blobs. trustcall improves the efficiency, cost, and resilience of structured output generation by using JSON Patch (RFC 6902) to correct validation errors and update existing schemas. It supports Pydantic models, Python functions, and JSON schemas, and integrates with any tool-calling LLM from the LangChain ecosystem.

Tokens
4.4K
Snippets
14
Records
21
Agent score
74%

What's inside trustcall

  1. Overview of trustcall

    main

    trustcall is a library designed to help LLMs handle large JSON blobs more effectively. Instead of asking an LLM to generate or modify an entire JSON object at once, trustcall instructs the LLM to generate JSON patch operations.

    This approach provides several benefits:

    • Efficiency: Faster and cheaper generation of structured output.
    • Resilience: Robust retrying of validation errors, even for complex, nested schemas (supporting Pydantic models, schema dictionaries, or regular Python functions).
    • Accuracy: Precise updates to existing schemas that avoid accidental deletions.

    It is suitable for workflows involving extraction, LLM routing, and multi-step agent tool use.

  2. Supported schema types for tools

    main

    trustcall supports several ways to define the schemas used for tool validation:

    1. Regular Python functions: Uses the function's type hints to apply validation.
    2. Pydantic objects: Uses Pydantic's validation logic (including custom @validator methods).
    3. JSON schemas: Validates calls against the provided JSON schema constraints.
    4. langchain-core tools: Supports standard LangChain tool definitions.
  3. Update existing schemas with trustcall

    main
    The extractor can be used to update existing structured representations of objects. You can provide a dictionary where the keys are schema names and the values are the current schema/object state. trustcall will then prompt the LLM to generate JSON Patches to extend or update those schemas based on new information.
  4. Use trustcall with LangChain tool-calling LLMs

    main

    You can use trustcall with any tool-calling LLM from the LangChain ecosystem. The create_extractor function takes an LLM and a list of tools (which can be Pydantic models, regular Python functions, or JSON schemas) to create an extractor that validates and cleans tool calls.

    When a tool call fails validation (e.g., a Pydantic @validator raises an error), trustcall automatically re-prompts the LLM to fix the output, often by generating a JSON Patch to correct the invalid arguments.

    from langchain_fireworks import ChatFireworks
    from pydantic.v1 import BaseModel, Field, validator
    from trustcall import create_extractor
    
    class Preferences(BaseModel):
        foods: List[str] = Field(description="Favorite foods")
    
        @validator("foods")
        def at_least_three_foods(cls, v):
            if len(v) < 3:
                raise ValueError("Must have at least three favorite foods")
            return v
    
    llm = ChatFireworks(model="accounts/fireworks/models/firefunction-v2")
    
    # Create the extractor with the LLM and the tool schema
    extractor = create_extractor(llm, tools=[Preferences], tool_choice="Preferences")
    
    # Invoke the extractor with a message
    res = extractor.invoke({"messages": [("user", "I like apple pie and ice cream.")]})
    
    # Access the validated tool calls and the parsed Pydantic objects
    print(res["messages"][-1].tool_calls)
    print(res["responses"])
  5. Run trustcall evaluation benchmarks

    main

    To run the built-in evaluation benchmarks, you must first clone the required dataset using the LangSmith client and then run the make evals command. This requires API keys for the models being evaluated and specific dependencies.

    from langsmith import Client
    
    # Clone the dataset
    Client().clone_public_dataset("https://smith.langchain.com/public/0544c02f-9617-4095-bc15-3a9af1189819/d")
    # Run the evals
    make evals
  6. How existing schemas are structured in ExtractionInputs

    main

    The existing field in ExtractionInputs can take several forms to support different workflows:

    1. Dictionary: A mapping of schema names to schema instances (e.g., {"UserInfo": {...}}).
    2. List of SchemaInstances: A list of SchemaInstance named tuples.
    3. List of Tuples: A list of (record_id, schema_name, record_dict) tuples.

    This flexibility allows you to pass either a single record or multiple records that the LLM can then choose to update, insert, or delete.

  7. Understand the `ExtractionState` and `ExtendedExtractState` data models

    main

    The extraction process is driven by state objects that track the conversation history and attempt counts.

    • ExtractionState: The base state containing:

      • messages: A list of AnyMessage objects. It uses a custom reducer _reduce_messages to handle MessageOp updates (like delete, update_tool_call, or update_tool_name).
      • attempts: An integer tracking how many times extraction has been attempted.
      • msg_id: A unique identifier for the message being patched.
      • existing: An optional dictionary representing existing data if you are performing an update rather than a fresh extraction.
    • ExtendedExtractState: Inherits from ExtractionState and adds:

      • tool_call_id: The specific ID of the tool call that needs to be patched.
      • bump_attempt: A boolean flag used to signal if the current attempt should be incremented.
    from dataclasses import dataclass
    from typing import Annotated, List, Optional, Dict, Any
    from langchain_core.messages import AnyMessage
    
    @dataclass(kw_only=True)
    class ExtractionState:
        messages: Annotated[List[AnyMessage], _reduce_messages]
        attempts: Annotated[int, operator.add]
        msg_id: Annotated[str, _keep_first]
        existing: Optional[Dict[str, Any]] = None
    
    @dataclass(kw_only=True)
    class ExtendedExtractState(ExtractionState):
        tool_call_id: str = ""
        bump_attempt: bool = False
  8. How JSONPatch is used to heal invalid tool calls

    main

    Instead of re-generating an entire tool call from scratch when a validation error occurs, trustcall uses JsonPatch operations to modify the existing invalid call. This is more token-efficient and reliable.

    The JsonPatch Schema

    A patch operation requires:

    • op: One of "add", "remove", or "replace".
    • path: A JSON Pointer string referencing the location in the document.
    • value: The data to be used (required for add and replace).

    Patching Tools

    • PatchFunctionErrors: Used when a tool call fails validation. It requires a json_doc_id, planned_edits (reasoning), and a list of patches.
    • PatchFunctionName: Used when the LLM calls an unrecognized tool name. It requires a json_doc_id, reasoning, and the fixed_name.
    • PatchDoc: Used to update existing structured data. It follows a specific planning order: replace first, then remove (from highest index to lowest to avoid shifting), then add (using /- for array appends).
    # Example of a JsonPatch structure
    {
        "op": "replace",
        "path": "/path/to/my_array/1",
        "value": "the newer value to be patched"
    }
  9. Integrate trustcall into a LangGraph agent

    main

    Because the extractor returned by create_extractor follows the LangChain Runnable interface and returns chat messages containing validated tool calls, it can be used directly as a node in a LangGraph state machine. This allows for conversational agents that can automatically recover from invalid tool-calling attempts before the tools are actually executed.

    from langgraph.graph import START, StateGraph
    from langgraph.prebuilt import ToolNode, tools_condition
    from trustcall import create_extractor
    
    # ... define tools and LLM ...
    
    # The extractor acts as the agent node
    agent = create_extractor(llm, tools=[save_user_information, lookup_time])
    
    class State(TypedDict):
        messages: Annotated[list, operator.add]
    
    builder = StateGraph(State)
    builder.add_node("agent", agent)
    builder.add_node("tools", ToolNode([save_user_information, lookup_time]))
    builder.add_edge("tools", "agent")
    builder.add_edge(START, "agent")
    builder.add_conditional_edges("agent", tools_condition)
    
    graph = builder.compile(checkpointer=MemorySaver())
  10. How trustcall improves structured extraction reliability

    main

    Trustcall is designed to solve two primary limitations of standard LLM tool calling:

    1. Complex, Nested Schemas: Standard tool calling often fails (e.g., ValidationError) when attempting to populate deeply nested Pydantic models. Trustcall handles this by using a "patch-don't-post" approach: instead of re-generating the entire output upon error, it prompts the LLM to generate a concise patch to fix the specific error, making it more reliable and cheaper.

    2. Schema Updates without Information Loss: When updating existing JSON documents (like user memories) based on new conversation data, standard LLMs often

  11. Use the `create_extractor` factory to build an extractor

    main

    The create_extractor function is the primary entrypoint for creating an extraction engine. It likely returns an object capable of performing structured data extraction from LLM tool calls, supporting features like retries and schema validation. (Note: Detailed signature and parameters are not present in this segment, but it is the main exported factory).

    from trustcall import create_extractor
    
    # Example usage (based on export list)
    extractor = create_extractor(...)