Commit Graph

94 Commits

Author SHA1 Message Date
Sydney Runkle
e18a21a8b0 tests' 2025-10-16 14:24:54 -04:00
Sydney Runkle
d8d94e6c95 Apply suggestions from code review 2025-10-16 13:38:45 -04:00
Sydney Runkle
e4ab6896a7 another pass 2025-10-16 13:33:46 -04:00
Sydney Runkle
6efea75d1c linting 2025-10-16 13:24:02 -04:00
Sydney Runkle
5155fbe072 better structure 2025-10-16 13:20:47 -04:00
Sydney Runkle
7157c2f69c openai and xai 2025-10-16 13:12:14 -04:00
Sydney Runkle
602284dc3e provider specific kwargs 2025-10-16 12:14:48 -04:00
Sydney Runkle
e10d99b728 fix(langchain): conditional edge from tools to end (#33520) 2025-10-16 11:56:45 -04:00
Eugene Yurtsev
31718492c7 fix(langchain_v1): relax tool node validation to allow claude text editing tools (#33512)
Relax tool node validation to allow claude text editing tools
2025-10-16 14:56:41 +00:00
Sydney Runkle
dd77dbe3ab chore(langchain_v1): adding back state_schema to create_agent (#33519)
To make migration easier, things are more backwards compat

Very minimal footprint here

Will need to upgrade migration guide and other docs w/ this change
2025-10-16 10:12:34 -04:00
Sydney Runkle
551e86a517 chore(langchain): use runtime not tool_runtime for injected tool arg (#33522)
fast follow to https://github.com/langchain-ai/langchain/pull/33500
2025-10-16 13:53:54 +00:00
Eugene Yurtsev
8734c05f64 feat(langchain_v1): tool retry middleware (#33503)
Adds `ToolRetryMiddleware` to automatically retry failed tool calls with
configurable exponential backoff, exception filtering, and error
handling.

## Example

```python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolRetryMiddleware
from langchain_openai import ChatOpenAI

# Retry up to 3 times with exponential backoff
retry = ToolRetryMiddleware(
    max_retries=3,
    initial_delay=1.0,
    backoff_factor=2.0,
)

agent = create_agent(
    model=ChatOpenAI(model="gpt-4"),
    tools=[search_tool, database_tool],
    middleware=[retry],
)

# Tool failures are automatically retried
result = agent.invoke({"messages": [{"role": "user", "content": "Search for AI news"}]})
```

For advanced usage with specific exception handling:

```python
from requests.exceptions import Timeout, HTTPError

def should_retry(exc: Exception) -> bool:
    # Only retry on 5xx errors or timeouts
    if isinstance(exc, HTTPError):
        return 500 <= exc.response.status_code < 600
    return isinstance(exc, Timeout)

retry = ToolRetryMiddleware(
    max_retries=4,
    retry_on=should_retry,
    tools=["search_database"],  # Only apply to specific tools
)
```
2025-10-16 09:47:43 -04:00
Sydney Runkle
89c3428d85 feat(langchain_v1): injected runtime (#33500)
Goal here is 2 fold

1. Improved devx for injecting args into tools
2. Support runtime injection for Python 3.10 async

One consequence of this PR is that `ToolNode` now expects `config`
available with `runtime`, which only happens in LangGraph execution
contexts. Hence the config patch for tests.

Are we ok reserving `tool_runtime`?

before, eek:
```py
from langchain.agents import create_agent
from langchain.tools import tool, InjectedState, InjectedStore
from langgraph.runtime import get_runtime
from typing_extensions import Annotated
from langgraph.store.base import BaseStore

@tool
def do_something(
    arg: int,
    state: Annotated[dict, InjectedState],
    store: Annotated[BaseStore, InjectedStore],
) -> None:
    """does something."""
    print(state)
    print(store)
    print(get_runtime().context)
    ...
```

after, woo!
```py
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime

@tool
def do_something_better(
    arg: int,
    tool_runtime: ToolRuntime,
) -> None:
    """does something better."""
    print(tool_runtime.state)
    print(tool_runtime.store)
    print(tool_runtime.context)
    ...
```

```python
@dataclass
class ToolRuntime(InjectedToolArg, Generic[StateT, ContextT]):
    state: StateT
    context: ContextT
    config: RunnableConfig
    tool_call_id: str
    stream_writer: StreamWriter
    context: ContextT
    store: BaseStore | None
2025-10-16 07:41:09 -04:00
Mason Daugherty
26e0a00c4c style: more work for refs (#33508)
Largely:
- Remove explicit `"Default is x"` since new refs show default inferred
from sig
- Inline code (useful for eventual parsing)
- Fix code block rendering (indentations)
2025-10-15 18:46:55 -04:00
Sydney Runkle
8f6851c349 fix(langchain_v1): keep state to relevant middlewares for tool/model call limits (#33493)
The one risk point that I can see here is that model + tool call
counting now occurs in the `after_model` hook which introduces order
dependency (what if you have HITL execute before this hook and we jump
early to `model`, for example).

This is something users can work around at the moment and we can
document. We could also introduce a priority concept to middleware.
2025-10-15 14:24:59 -04:00
Eugene Yurtsev
99e0a60aab chore(langchain_v1): remove invocation request (#33482)
Remove ToolNode primitives from langchain
2025-10-14 15:07:30 -04:00
Eugene Yurtsev
d38729fbac feat(langchain_v1): add async implementations to wrap_model_call (#33467)
Add async implementations to wrap_model_call for prebuilt middleware
2025-10-14 17:39:38 +00:00
Eugene Yurtsev
9140a7cb86 feat(langchain_v1): add override to model request and tool call request (#33465)
Add override to model request and tool call request
2025-10-14 10:31:46 -04:00
Sydney Runkle
fff87e95d1 fix(langchain): rename PlanningMiddleware to TodoListMiddleware (#33476) 2025-10-14 09:06:06 -04:00
Eugene Yurtsev
1cf851e054 chore(langchain_v1,anthropic): migrate anthropic middleware to langchain_anthropic (#33463)
Migrate prompt caching implementation into langchain_anthropic.middleware
2025-10-13 15:12:54 -04:00
Sydney Runkle
760fc3bc12 chore(langchain_v1): use args for HITL (#33442) 2025-10-11 07:12:46 -04:00
Eugene Yurtsev
6dc34beb71 chore(langchain_v1): stricter handling of sync vs. async for wrap_model_call and wrap_tool_call (#33429)
Wrap model call and wrap tool call
2025-10-10 16:54:42 -04:00
Eugene Yurtsev
c2205f88e6 chore(langchain_v1): further namespace clean up (#33428)
Reduce exposed namespace for now
2025-10-10 20:48:24 +00:00
Eugene Yurtsev
0559558715 feat(langchain_v1): add async implementation for wrap_tool_call (#33420)
Add async implementation. No automatic delegation to sync at the moment.
2025-10-10 15:07:19 -04:00
Eugene Yurtsev
75965474fc chore(langchain_v1): tool error exceptions (#33424)
Tool error exceptions
2025-10-10 15:06:40 -04:00
Eugene Yurtsev
74983f8a96 chore(langchain_v1): update on_tool_call to wrap_tool (#33410)
Improve naming on ToolNode for on_tool_call interceptor
2025-10-10 03:19:45 +00:00
Eugene Yurtsev
11c5b86981 chore(langchain_v1): update wrap_on_model return (#33408)
Update wrap on model return to capture the full return type of the model
so we can accommodate dynamic structured outputs.
2025-10-09 23:01:21 -04:00
Eugene Yurtsev
045e7ad4a1 feat(langchain_v1): tool emulator (#33357)
This is tool emulation middleware. The idea is to help test out an agent
that may have some tools that either take a long time to run or are
expensive to set up. This could allow simulating the behavior a bit.
2025-10-10 01:39:40 +00:00
Sydney Runkle
c99773b652 feat(langchain_v1): refactoring HITL API (#33397)
Easiest to review side by side (not inline)

* Adding `dict` type requests + responses so that we can ship config w/
interrupts. Also more extensible.
* Keeping things generic in terms of `interrupt_on` rather than
`tool_config`
* Renaming allowed decisions -- approve, edit, reject
* Draws differentiation between actions (requested + performed by the
agent), in this case tool calls, though we generalize beyond that and
decisions - human feedback for said actions

New request structure

```py
class Action(TypedDict):
    """Represents an action with a name and arguments."""

    name: str
    """The type or name of action being requested (e.g., "add_numbers")."""

    arguments: dict[str, Any]
    """Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""


DecisionType = Literal["approve", "edit", "reject"]


class ReviewConfig(TypedDict):
    """Policy for reviewing a HITL request."""

    action_name: str
    """Name of the action associated with this review configuration."""

    allowed_decisions: list[DecisionType]
    """The decisions that are allowed for this request."""

    description: NotRequired[str]
    """The description of the action to be reviewed."""

    arguments_schema: NotRequired[dict[str, Any]]
    """JSON schema for the arguments associated with the action, if edits are allowed."""

class HITLRequest(TypedDict):
    """Request for human feedback on a sequence of actions requested by a model."""

    action_requests: list[Action]
    """A list of agent actions for human review."""

    review_configs: list[ReviewConfig]
    """Review configuration for all possible actions."""
```

New response structure

```py
class ApproveDecision(TypedDict):
    """Response when a human approves the action."""

    type: Literal["approve"]
    """The type of response when a human approves the action."""


class EditDecision(TypedDict):
    """Response when a human edits the action."""

    type: Literal["edit"]
    """The type of response when a human edits the action."""

    edited_action: Action
    """Edited action for the agent to perform.

    Ex: for a tool call, a human reviewer can edit the tool name and args.
    """


class RejectDecision(TypedDict):
    """Response when a human rejects the action."""

    type: Literal["reject"]
    """The type of response when a human rejects the action."""

    message: NotRequired[str]
    """The message sent to the model explaining why the action was rejected."""


Decision = ApproveDecision | EditDecision | RejectDecision


class HITLResponse(TypedDict):
    """Response payload for a HITLRequest."""

    decisions: list[Decision]
    """The decisions made by the human."""
```

User facing API:

NEW

```py
HumanInTheLoopMiddleware(interrupt_on={
    'send_email': True,
    # can also use a callable for description that takes tool call, state, and runtime
    'execute_sql': {
        'allowed_decisions': ['approve', 'edit', 'reject'], 
        'description': 'please review sensitive tool execution'},
    }
})

Command(resume={"decisions": [{"type": "approve"}, {"type": "reject": "message": "db down"}]})
```

OLD

```py
HumanInTheLoopMiddleware(interrupt_on={
    'send_email': True,
    'execute_sql': {
        'allow_accept': True, 
        'allow_edit': True, 
        'allow_respond': True, 
        description='please review sensitive tool execution'
    },
})

Command(resume=[{"type": "approve"}, {"type": "reject": "message": "db down"}])
```
2025-10-09 17:51:28 -04:00
Mason Daugherty
6fc21afbc9 style: .. code-block:: admonition translations (#33400)
biiiiiiiiiiiiiiiigggggggg pass
2025-10-09 16:52:58 -04:00
Mason Daugherty
d8a680ee57 style: address Sphinx double-backtick snippet syntax (#33389) 2025-10-09 13:35:51 -04:00
Mason Daugherty
b6132fc23e style: remove more Optional syntax (#33371) 2025-10-08 23:28:43 -04:00
Eugene Yurtsev
f33b1b3d77 chore(langchain_v1): rename on_model_call to wrap_model_call (#33370)
rename on_model_call to wrap_model_call
2025-10-08 23:28:14 -04:00
Eugene Yurtsev
e193a1f273 chore(langchain_v1): replace modify model request with on model call (#33368)
* Replace modify model request with on model call
* Remove modify model request
2025-10-09 02:46:48 +00:00
Eugene Yurtsev
eb70672f4a chore(langchain): add unit tests for wrap_tool_call decorator (#33367)
Add unit tests for wrap_tool_call decorator
2025-10-09 02:30:07 +00:00
Eugene Yurtsev
87df179ca9 chore(langchain_v1): rename on_tool_call to wrap_tool_call (#33366)
Replace on tool call with wrap tool call
2025-10-08 22:10:36 -04:00
Eugene Yurtsev
982a950ccf chore(langchain_v1): add runtime and context to model request (#33365)
Add runtime and context to ModelRequest to make the API more convenient
2025-10-08 21:59:56 -04:00
Eugene Yurtsev
c2435eeca5 chore(langchain_v1): update on_tool_call to regular callbacks (#33364)
Refactor tool call middleware from generator-based to handler-based
pattern

Simplifies on_tool_call middleware by replacing the complex generator
protocol with a straightforward handler pattern. Instead of yielding
requests and receiving results via .send(),
handlers now receive an execute callable that can be invoked multiple
times for retry logic.


Before vs. After

Before (Generator):
```python
class RetryMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        for attempt in range(3):
            response = yield request  # Yield request, receive result via .send()
            if is_valid(response) or attempt == 2:
                return  # Final result is last value sent to generator
```

After (Handler):

```python
class RetryMiddleware(AgentMiddleware):
    def on_tool_call(self, request, handler):
        for attempt in range(3):
            result = handler(request)  # Direct function call
            if is_valid(result):
                return result
        return result
```


Follow up after this PR:

* Rename the interceptor to wrap_tool_call
* Fix the async path for the ToolNode
2025-10-08 21:46:03 -04:00
Mason Daugherty
d13823043d style: monorepo pass for refs (#33359)
* Delete some double backticks previously used by Sphinx (not done
everywhere yet)
* Fix some code blocks / dropdowns

Ignoring CLI CI for now
2025-10-08 18:41:39 -04:00
Eugene Yurtsev
b665b81a0e chore(langchain_v1): simplify on model call logic (#33358)
Moving from the generator pattern to the slightly less verbose (but explicit) handler pattern.

This will be more familiar to users

**Before (Generator Pattern):**
```python
def on_model_call(self, request, state, runtime):
    try:
        result = yield request
    except Exception:
        result = yield request  # Retry
```

**After (Handler Pattern):**
```python
def on_model_call(self, request, state, runtime, handler):
    try:
        return handler(request)
    except Exception:
        return handler(request)  # Retry
```
2025-10-08 17:23:11 -04:00
Eugene Yurtsev
1bf29da0d6 feat(langchain_v1): add on_tool_call middleware hook (#33329)
Adds generator-based middleware for intercepting tool execution in
agents. Middleware can retry on errors, cache results, modify requests,
or short-circuit execution.

### Implementation

**Middleware Protocol**
```python
class AgentMiddleware:
    def on_tool_call(
        self,
        request: ToolCallRequest,
        state: StateT,
        runtime: Runtime[ContextT],
    ) -> Generator[ToolCallRequest | ToolMessage | Command, ToolMessage | Command, None]:
        """
        Yields: ToolCallRequest (execute), ToolMessage (cached result), or Command (control flow)
        Receives: ToolMessage or Command via .send()
        Returns: None (final result is last value sent to handler)
        """
        yield request  # passthrough
```

**Composition**
Multiple middleware compose automatically (first = outermost), with
`_chain_tool_call_handlers()` stacking them like nested function calls.

### Examples

**Retry on error:**
```python
class RetryMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        for attempt in range(3):
            response = yield request
            if not isinstance(response, ToolMessage) or response.status != "error":
                return
            if attempt == 2:
                return  # Give up
```

**Cache results:**
```python
class CacheMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        cache_key = (request.tool_call["name"], tuple(request.tool_call["args"].items()))
        if cached := self.cache.get(cache_key):
            yield ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
        else:
            response = yield request
            self.cache[cache_key] = response.content
```

**Emulate tools with LLM**
```python
class ToolEmulator(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        prompt = f"""Emulate: {request.tool_call["name"]}
Description: {request.tool.description}
Args: {request.tool_call["args"]}
Return ONLY the tool's output."""

        response = emulator_model.invoke([HumanMessage(prompt)])
        yield ToolMessage(
            content=response.content,
            tool_call_id=request.tool_call["id"],
            name=request.tool_call["name"],
        )
```

**Modify requests:**
```python
class ScalingMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        if "value" in request.tool_call["args"]:
            request.tool_call["args"]["value"] *= 2
        yield request
```
2025-10-08 16:43:32 +00:00
Eugene Yurtsev
2c3fec014f feat(langchain_v1): on_model_call middleware (#33328)
Introduces a generator-based `on_model_call` hook that allows middleware
to intercept model calls with support for retry logic, error handling,
response transformation, and request modification.

## Overview

Middleware can now implement `on_model_call()` using a generator
protocol that:
- **Yields** `ModelRequest` to execute the model
- **Receives** `AIMessage` via `.send()` on success, or exception via
`.throw()` on error
- **Yields again** to retry or transform responses
- Uses **implicit last-yield semantics** (no return values from
generators)

## Usage Examples

### Basic Retry on Error

```python
from langchain.agents.middleware.types import AgentMiddleware

class RetryMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        for attempt in range(3):
            try:
                yield request  # Execute model
                break  # Success
            except Exception:
                if attempt == 2:
                    raise  # Max retries exceeded
```

### Response Transformation

```python
class UppercaseMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        result = yield request
        modified = AIMessage(content=result.content.upper())
        yield modified  # Return transformed response
```

### Error Recovery

```python
class FallbackMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        try:
            yield request
        except Exception:
            fallback = AIMessage(content="Service unavailable")
            yield fallback  # Convert error to fallback response
```

### Caching / Short-Circuit

```python
class CacheMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        if cached := get_cache(request):
            yield cached  # Skip model execution
        else:
            result = yield request
            save_cache(request, result)
```

### Request Modification

```python
class SystemPromptMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        modified_request = ModelRequest(
            model=request.model,
            system_prompt="You are a helpful assistant.",
            messages=request.messages,
            tools=request.tools,
        )
        yield modified_request
```

### Function Decorator

```python
from langchain.agents.middleware.types import on_model_call

@on_model_call
def retry_three_times(request, state, runtime):
    for attempt in range(3):
        try:
            yield request
            break
        except Exception:
            if attempt == 2:
                raise

agent = create_agent(model="openai:gpt-4o", middleware=[retry_three_times])
```

## Middleware Composition

Middleware compose with first in list as outermost layer:

```python
agent = create_agent(
    model="openai:gpt-4o",
    middleware=[
        RetryMiddleware(),      # Outer - wraps others
        LoggingMiddleware(),    # Middle
        UppercaseMiddleware(),  # Inner - closest to model
    ]
)
```
2025-10-08 12:34:04 -04:00
Sydney Runkle
c8205ff511 fix(langchain_v1): fix edges when there's no middleware (#33321)
1. Main fix: when we don't have a response format or middleware, don't
draw a conditional edge back to the loop entrypoint (self loop on model)
2. Supplementary fix: when we jump to `end` and there is an
`after_agent` hook, jump there instead of `__end__`

Other improvements -- I can remove these if they're more harmful than
helpful
1. Use keyword only arguments for edge generator functions for clarity
2. Rename args to `model_destination` and `end_destination` for clarity
2025-10-06 18:08:08 -04:00
Sydney Runkle
2fa9741f99 chore(langchain_v1): rename model_request node -> model (#33310) 2025-10-06 16:18:18 -04:00
Sydney Runkle
00f4db54c4 chore(langchain_v1): remove support for ToolNode in create_agent (#33306)
Let's add a note to help w/ migration once we add the tool call retry
middleware.
2025-10-06 15:06:20 -04:00
Sydney Runkle
62ccf7e8a4 feat(langchain_v1): simplify to use ONE agent (#33302)
This reduces confusion w/ types like `AgentState`, different arg names,
etc.

Second attempt, following
https://github.com/langchain-ai/langchain/pull/33249

* Ability to pass through `cache` and name in `create_agent` as
compilation args for the agent
* Right now, removing `test_react_agent.py` but we should add these
tests back as implemented w/ the new agent
* Add conditional edge when structured output tools are present to allow
for retries
* Rename `tracking` to `model_call_limit` to be consistent w/ tool call
limits

We need in the future (I'm happy to own):
* Significant test refactor
* Significant test overhaul where we emphasize and enforce coverage
2025-10-06 14:46:29 -04:00
Eugene Yurtsev
0ff2bc890b chore(langchain_v1): remove text splitters from langchain v1 namespace (#33297)
Removing text splitters for now for a lighter dependency. We may re-introduce
2025-10-06 14:42:23 -04:00
Sydney Runkle
0ccc0cbdae feat(langchain_v1): before_agent and after_agent hooks (#33279)
We're adding enough new nodes that I think a refactor in terms of graph
building is warranted here, but not necessarily required for merging.
2025-10-06 11:31:52 -04:00
Nuno Campos
f308139283 feat(langchain_v1): Implement Context Editing Middleware (#33267)
Brings functionality similar to Anthropic's context editing to all chat
models
https://docs.claude.com/en/docs/build-with-claude/context-editing

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-10-06 10:34:04 -04:00
ccurme
4e50ec4b98 feat(openai): enable stream_usage when using default base URL and client (#33205) 2025-10-06 08:56:38 -04:00