Compare commits

..
Author SHA1 Message Date
open-swe-dev[bot] 290cd4b773 Apply patch 2025-07-17 20:05:48 +00:00
open-swe-dev[bot] 53035d41ea Apply patch 2025-07-17 20:04:12 +00:00
21 changed files with 107 additions and 302 deletions
-4
View File
@@ -23,7 +23,3 @@ body:
attributes:
label: Issue Content
description: Add the content of the issue here.
- type: markdown
attributes:
value: |
Community members should **NOT** work on Privileged issues unless these issues have been explicitly marked with a "help-wanted" tag.
+1 -1
View File
@@ -34,7 +34,7 @@
id: extract_ignore_words
- name: Codespell
uses: codespell-project/actions-codespell@v2.1
uses: codespell-project/actions-codespell@v2.0
with:
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map'
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
-119
View File
@@ -1,119 +0,0 @@
# Egress for Subscription Metrics and Operational Metadata
> **Important: Self Hosted Only**
> This section only applies to customers who are not running in offline mode and assumes you are using a self-hosted LangGraph Platform instance.
> This does not apply to SaaS or Hybrid deployments.
Self-Hosted LangGraph Platform instances store all information locally and will never send sensitive information outside of your network. We currently only track platform usage for billing purposes according to the entitlements in your order. In order to better remotely support our customers, we do require egress to `https://beacon.langchain.com`.
In the future, we will be introducing support diagnostics to help us ensure that the LangGraph Platform is running at an optimal level within your environment.
> **Warning**
> **This will require egress to `https://beacon.langchain.com` from your network.**
> **If using an API key, you will also need to allow egress to `https://api.smith.langchain.com` or `https://eu.api.smith.langchain.com` for API key verification.**
Generally, data that we send to Beacon can be categorized as follows:
- **Subscription Metrics**
- Subscription metrics are used to determine level of access and utilization of LangSmith. This includes, but are not limited to:
- Nodes Executed
- Runs Executed
- License Key Verification
- **Operational Metadata**
- This metadata will contain and collect the above subscription metrics to assist with remote support, allowing the LangChain team to diagnose and troubleshoot performance issues more effectively and proactively.
## Example Payloads
In an effort to maximize transparency, we provide sample payloads here:
### License Verification (If using an Enterprise License)
**Endpoint:**
`POST beacon.langchain.com/v1/beacon/verify`
**Request:**
```json
{
"license": "<YOUR_LICENSE_KEY>"
}
```
**Response:**
```json
{
"token": "Valid JWT" // Short-lived JWT token to avoid repeated license checks
}
```
### Api Key Verification (If using a LangSmith API Key)
**Endpoint:**
`POST api.smith.langchain.com/auth`
**Request:**
```json
"Headers": {
X-Api-Key: <YOUR_API_KEY>
}
```
**Response:**
```json
{
"org_config": {
"org_id": "3a1c2b6f-4430-4b92-8a5b-79b8b567bbc1",
... // Additional organization details
}
}
```
### Usage Reporting
**Endpoint:**
`POST beacon.langchain.com/v1/metadata/submit`
**Request:**
```json
{
"license": "<YOUR_LICENSE_KEY>",
"from_timestamp": "2025-01-06T09:00:00Z",
"to_timestamp": "2025-01-06T10:00:00Z",
"tags": {
"langgraph.python.version": "0.1.0",
"langgraph_api.version": "0.2.0",
"langgraph.platform.revision": "abc123",
"langgraph.platform.variant": "standard",
"langgraph.platform.host": "host-1",
"langgraph.platform.tenant_id": "3a1c2b6f-4430-4b92-8a5b-79b8b567bbc1",
"langgraph.platform.project_id": "c5b5f53a-4716-4326-8967-d4f7f7799735",
"langgraph.platform.plan": "enterprise",
"user_app.uses_indexing": "true",
"user_app.uses_custom_app": "false",
"user_app.uses_custom_auth": "true",
"user_app.uses_thread_ttl": "true",
"user_app.uses_store_ttl": "false"
},
"measures": {
"langgraph.platform.runs": 150,
"langgraph.platform.nodes": 450
},
"logs": []
}
```
**Response:**
```json
"204 No Content"
```
## Our Commitment
LangChain will not store any sensitive information in the Subscription Metrics or Operational Metadata. Any data collected will not be shared with a third party. If you have any concerns about the data being sent, please reach out to your account team.
@@ -23,8 +23,6 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
kubectl get storageclass
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
## Setup
1. As part of configuring your Self-Hosted LangSmith instance, you enable the `langgraphPlatform` option. This will provision a few key resources.
@@ -24,7 +24,6 @@ Before deploying, review the [conceptual guide for the Standalone Container](../
1. `LANGSMITH_API_KEY`: (if using [Lite](../../concepts/langgraph_server.md#server-versions)) LangSmith API key. This will be used to authenticate ONCE at server start up.
1. `LANGGRAPH_CLOUD_LICENSE_KEY`: (if using [Enterprise](../../concepts/langgraph_data_plane.md#licensing)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
1. `LANGSMITH_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGSMITH_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
## Kubernetes (Helm)
+11 -27
View File
@@ -30,33 +30,17 @@ export default {
Next, define your UI components in your `langgraph.json` configuration:
=== "Python agent"
```json title="langgraph.json"
{
"node_version": "20",
"graphs": {
"agent": "./src/agent.py:graph"
},
"ui": {
"agent": "./src/agent/ui.tsx"
}
}
```
=== "JS agent"
```json title="langgraph.json"
{
"node_version": "20",
"graphs": {
"agent": "./src/agent/index.ts:graph"
},
"ui": {
"agent": "./src/agent/ui.tsx"
}
}
```
```json
{
"node_version": "20",
"graphs": {
"agent": "./src/agent/index.ts:graph"
},
"ui": {
"agent": "./src/agent/ui.tsx"
}
}
```
The `ui` section points to the UI components that will be used by graphs. By default, we recommend using the same key as the graph name, but you can split out the components however you like, see [Customise the namespace of UI components](#customise-the-namespace-of-ui-components) for more details.
@@ -4,9 +4,6 @@
---
## v0.2.96 (2025-07-17)
- Added a fallback mechanism for configurable header patterns to handle exclude/include settings more effectively.
## v0.2.95 (2025-07-17)
- Avoided setting the future if it is already done to prevent redundant operations.
- Resolved compatibility errors in CI by switching from `typing.TypedDict` to `typing_extensions.TypedDict` for Python versions below 3.12.
+1 -1
View File
@@ -10,7 +10,7 @@ search:
There are two free options for deploying LangGraph applications via the LangGraph Server:
1. [Local](../tutorials/langgraph-platform/local-server.md): Deploy for local testing and development.
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more than 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more that 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
## Production deployment
+1 -1
View File
@@ -290,7 +290,7 @@ my-autogen-agent/
```
langgraph>=0.1.0
ag2>=0.2.0
pyautogen>=0.2.0
langchain-core>=0.1.0
langchain-openai>=0.0.5
```
+4 -5
View File
@@ -1152,13 +1152,12 @@ LangGraph supports map-reduce and other advanced branching patterns using the Se
```python
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from typing_extensions import TypedDict, Annotated
import operator
from typing_extensions import TypedDict
class OverallState(TypedDict):
topic: str
subjects: list[str]
jokes: Annotated[list[str], operator.add]
jokes: list[str]
best_selected_joke: str
def generate_topics(state: OverallState):
@@ -1567,9 +1566,9 @@ class State(TypedDict):
def node_a(state: State) -> Command[Literal["node_b", "node_c"]]:
print("Called A")
value = random.choice(["b", "c"])
value = random.choice(["a", "b"])
# this is a replacement for a conditional edge function
if value == "b":
if value == "a":
goto = "node_b"
else:
goto = "node_c"
+50 -13
View File
@@ -974,13 +974,51 @@ class CompiledStateGraph(
)
raise InvalidUpdateError(msg)
# Create a validated version of _control_branch that has access to available nodes
def _control_branch_with_validation(value: Any) -> Sequence[tuple[str, Any]]:
if isinstance(value, Send):
return ((TASKS, value),)
commands: list[Command] = []
if isinstance(value, Command):
commands.append(value)
elif isinstance(value, (list, tuple)):
for cmd in value:
if isinstance(cmd, Command):
commands.append(cmd)
rtn: list[tuple[str, Any]] = []
for command in commands:
if command.graph == Command.PARENT:
raise ParentCommand(command)
if isinstance(command.goto, Send):
rtn.append((TASKS, command.goto))
elif isinstance(command.goto, str):
# Validate that the goto target exists in the graph
if command.goto != END and command.goto not in self.builder.nodes:
raise InvalidUpdateError(
f"Node '{command.goto}' does not exist in the graph"
)
rtn.append((CHANNEL_BRANCH_TO.format(command.goto), None))
else:
# Handle sequence of goto targets
for go in command.goto:
if isinstance(go, Send):
rtn.append((TASKS, go))
else:
# Validate that the goto target exists in the graph
if go != END and go not in self.builder.nodes:
raise InvalidUpdateError(
f"Node '{go}' does not exist in the graph"
)
rtn.append((CHANNEL_BRANCH_TO.format(go), None))
return rtn
# state updaters
write_entries: tuple[ChannelWriteEntry | ChannelWriteTupleEntry, ...] = (
ChannelWriteTupleEntry(
mapper=_get_root if output_keys == ["__root__"] else _get_updates
),
ChannelWriteTupleEntry(
mapper=_control_branch,
mapper=_control_branch_with_validation,
static=_control_static(node.ends)
if node is not None and node.ends is not None
else None,
@@ -1236,18 +1274,17 @@ def _control_branch(value: Any) -> Sequence[tuple[str, Any]]:
for command in commands:
if command.graph == Command.PARENT:
raise ParentCommand(command)
goto_targets = (
[command.goto] if isinstance(command.goto, (Send, str)) else command.goto
)
for go in goto_targets:
if isinstance(go, Send):
rtn.append((TASKS, go))
elif isinstance(go, str) and go != END:
# END is a special case, it's not actually a node in a practical sense
# but rather a special terminal node that we don't need to branch to
rtn.append((CHANNEL_BRANCH_TO.format(go), None))
if isinstance(command.goto, Send):
rtn.append((TASKS, command.goto))
elif isinstance(command.goto, str):
rtn.append((CHANNEL_BRANCH_TO.format(command.goto), None))
else:
rtn.extend(
(TASKS, go)
if isinstance(go, Send)
else (CHANNEL_BRANCH_TO.format(go), None)
for go in command.goto
)
return rtn
-27
View File
@@ -6955,30 +6955,3 @@ def test_weather_subgraph(
},
),
]
def test_subgraph_to_end_does_not_warn() -> None:
"""Regression test for https://github.com/langchain-ai/langgraph/issues/5572."""
class State(TypedDict):
x: str
def update_x(state: State):
return Command(goto=END, update={"x": state["x"] + "!"})
# Subgraph
subgraph_builder = StateGraph(State)
subgraph_builder.add_node("update_x", update_x)
subgraph_builder.add_edge(START, "update_x")
subgraph_builder.add_edge("update_x", END)
subgraph = subgraph_builder.compile()
# Parent graph
builder = StateGraph(State)
builder.add_node("subgraph_node", subgraph)
builder.add_edge(START, "subgraph_node")
builder.add_edge("subgraph_node", END)
graph = builder.compile()
response = graph.invoke({"x": "hello"})
print(response)
+1 -1
View File
@@ -1463,7 +1463,7 @@ wheels = [
[[package]]
name = "langgraph-sdk"
version = "0.1.74"
version = "0.1.73"
source = { editable = "../sdk-py" }
dependencies = [
{ name = "httpx" },
@@ -1,26 +0,0 @@
from typing import Any, Literal, TypedDict
from langchain_core.messages import ToolCall
class ToolCallWithContext(TypedDict):
"""ToolCall with additional context for graph state.
This is an internal data-structure meant to help the ToolNode accept
tools calls with additional context (e.g. state) when dispatched using the
`Send` API.
The Send API is used in create_react_agent to be able to distribute the tool
calls in parallel and support human-in-the-loop workflows where graph execution
may be paused for an indefinite time.
"""
tool_call: ToolCall
__type: Literal["tool_call_with_context"]
"""Type to parameterize the payload.
Using "__" as a prefix to be defensive against potential name collisions with
regular user state.
"""
state: Any
"""The state is provided as additional context."""
@@ -39,7 +39,6 @@ from langgraph.graph import END, StateGraph
from langgraph.graph.message import add_messages
from langgraph.graph.state import CompiledStateGraph
from langgraph.managed import IsLastStep, RemainingSteps
from langgraph.prebuilt._internal import ToolCallWithContext
from langgraph.prebuilt.tool_node import ToolNode
from langgraph.store.base import BaseStore
from langgraph.types import Checkpointer, Send
@@ -651,17 +650,11 @@ def create_react_agent(
elif version == "v2":
if post_model_hook is not None:
return "post_model_hook"
return [
Send(
"tools",
ToolCallWithContext(
__type="tool_call_with_context",
tool_call=tool_call,
state=state,
),
)
for tool_call in last_message.tool_calls
tool_calls = [
tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
for call in last_message.tool_calls
]
return [Send("tools", [tool_call]) for tool_call in tool_calls]
# Define a new graph
workflow = StateGraph(state_schema or AgentState, config_schema=config_schema)
@@ -740,17 +733,11 @@ def create_react_agent(
]
if pending_tool_calls:
return [
Send(
"tools",
ToolCallWithContext(
__type="tool_call_with_context",
tool_call=tool_call,
state=state,
),
)
for tool_call in pending_tool_calls
pending_tool_calls = [
tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
for call in pending_tool_calls
]
return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]
elif isinstance(messages[-1], ToolMessage):
return entrypoint
elif response_format is not None:
+16 -32
View File
@@ -71,7 +71,6 @@ from pydantic import BaseModel
from typing_extensions import Annotated, get_args, get_origin
from langgraph.errors import GraphBubbleUp
from langgraph.prebuilt._internal import ToolCallWithContext
from langgraph.store.base import BaseStore
from langgraph.types import Command, Send
from langgraph.utils.runnable import RunnableCallable
@@ -359,8 +358,7 @@ class ToolNode(RunnableCallable):
*,
store: Optional[BaseStore],
) -> Any:
tool_calls, input_type = self._parse_input(input)
tool_calls = [self.inject_tool_args(call, input, store) for call in tool_calls]
tool_calls, input_type = self._parse_input(input, store)
config_list = get_config_list(config, len(tool_calls))
input_types = [input_type] * len(tool_calls)
with get_executor_for_config(config) as executor:
@@ -381,8 +379,7 @@ class ToolNode(RunnableCallable):
*,
store: Optional[BaseStore],
) -> Any:
tool_calls, input_type = self._parse_input(input)
tool_calls = [self.inject_tool_args(call, input, store) for call in tool_calls]
tool_calls, input_type = self._parse_input(input, store)
outputs = await asyncio.gather(
*(self._arun_one(call, input_type, config) for call in tool_calls)
)
@@ -439,19 +436,18 @@ class ToolNode(RunnableCallable):
input_type: Literal["list", "dict", "tool_calls"],
config: RunnableConfig,
) -> ToolMessage:
"""Run a single tool call synchronously."""
if invalid_tool_message := self._validate_tool_call(call):
return invalid_tool_message
try:
call_args = {**call, **{"type": "tool_call"}}
response = self.tools_by_name[call["name"]].invoke(call_args, config)
input = {**call, **{"type": "tool_call"}}
response = self.tools_by_name[call["name"]].invoke(input, config)
# GraphInterrupt is a special exception that will always be raised.
# It can be triggered in the following scenarios:
# (1) a NodeInterrupt is raised inside a tool
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph
# called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphBubbleUp as e:
raise e
@@ -495,7 +491,6 @@ class ToolNode(RunnableCallable):
input_type: Literal["list", "dict", "tool_calls"],
config: RunnableConfig,
) -> ToolMessage:
"""Run a single tool call asynchronously."""
if invalid_tool_message := self._validate_tool_call(call):
return invalid_tool_message
@@ -553,25 +548,16 @@ class ToolNode(RunnableCallable):
dict[str, Any],
BaseModel,
],
store: Optional[BaseStore],
) -> Tuple[list[ToolCall], Literal["list", "dict", "tool_calls"]]:
input_type: Literal["list", "dict", "tool_calls"]
if isinstance(input, list):
if isinstance(input[-1], dict) and input[-1].get("type") == "tool_call":
input_type = "tool_calls"
tool_calls = cast(list[ToolCall], input)
tool_calls = input
return tool_calls, input_type
else:
input_type = "list"
messages = input
elif (
isinstance(input, dict) and input.get("__type") == "tool_call_with_context"
):
# mypy will not be able to type narrow correctly since the signature
# for input contains dict[str, Any]. We'd need to type dict[str, Any]
# before we can apply correct typing.
input = cast(ToolCallWithContext, input) # type: ignore[assignment]
input_type = "tool_calls"
return [input["tool_call"]], input_type
elif isinstance(input, dict) and (messages := input.get(self.messages_key, [])):
input_type = "dict"
elif messages := getattr(input, self.messages_key, []):
@@ -587,7 +573,10 @@ class ToolNode(RunnableCallable):
except StopIteration:
raise ValueError("No AIMessage found in input")
tool_calls = [call for call in latest_ai_message.tool_calls]
tool_calls = [
self.inject_tool_args(call, input, store)
for call in latest_ai_message.tool_calls
]
return tool_calls, input_type
def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]:
@@ -629,20 +618,15 @@ class ToolNode(RunnableCallable):
required_fields_str = ", ".join(f for f in required_fields if f)
err_msg += f" State should contain fields {required_fields_str}."
raise ValueError(err_msg)
if isinstance(input, dict) and input.get("__type") == "tool_call_with_context":
state = input["state"]
else:
state = input
if isinstance(state, dict):
if isinstance(input, dict):
tool_state_args = {
tool_arg: state[state_field] if state_field else state
tool_arg: input[state_field] if state_field else input
for tool_arg, state_field in state_args.items()
}
else:
tool_state_args = {
tool_arg: getattr(state, state_field) if state_field else state
tool_arg: getattr(input, state_field) if state_field else input
for tool_arg, state_field in state_args.items()
}
+10 -12
View File
@@ -5,7 +5,6 @@ from functools import partial
from typing import (
Annotated,
List,
Literal,
Optional,
Type,
TypeVar,
@@ -510,7 +509,7 @@ class CustomStatePydantic(AgentStatePydantic):
@pytest.mark.parametrize("state_schema", [CustomState, CustomStatePydantic])
def test_react_agent_update_state(
sync_checkpointer: BaseCheckpointSaver,
version: Literal["v1", "v2"],
version: str,
state_schema: StateSchemaType,
) -> None:
@dec_tool
@@ -558,7 +557,7 @@ def test_react_agent_update_state(
version=version,
)
config = {"configurable": {"thread_id": "1"}}
# Run until interrupted
# run until interrpupted
agent.invoke({"messages": [("user", "what's my name")]}, config)
# supply the value for the interrupt
response = agent.invoke(Command(resume="Archibald"), config)
@@ -782,9 +781,8 @@ class AgentStateExtraKeyPydantic(AgentStatePydantic):
"state_schema", [AgentStateExtraKey, AgentStateExtraKeyPydantic]
)
def test_create_react_agent_inject_vars(
version: Literal["v1", "v2"], state_schema: StateSchemaType
version: str, state_schema: StateSchemaType
) -> None:
"""Test that the agent can inject state and store into tool functions."""
store = InMemoryStore()
namespace = ("test",)
store.put(namespace, "test_key", {"bar": 3})
@@ -819,14 +817,15 @@ def test_create_react_agent_inject_vars(
model = FakeToolCallingModel(tool_calls=[[tool_call], []])
agent = create_react_agent(
model,
ToolNode([tool1], handle_tool_errors=False),
[tool1],
state_schema=state_schema,
store=store,
version=version,
)
result = agent.invoke({"messages": [{"role": "user", "content": "hi"}], "foo": 2})
input_message = HumanMessage("hi")
result = agent.invoke({"messages": [input_message], "foo": 2})
assert result["messages"] == [
_AnyIdHumanMessage(content="hi"),
input_message,
AIMessage(content="hi", tool_calls=[tool_call], id="0"),
_AnyIdToolMessage(content="6", name="tool1", tool_call_id="some 0"),
AIMessage("hi-hi-6", id="1"),
@@ -1581,14 +1580,13 @@ def test_create_react_agent_inject_vars_with_post_model_hook(
"type": "tool_call",
}
def post_model_hook(state: dict) -> dict:
"""Post model hook is injecting a new foo key."""
return {"foo": 2}
def post_model_hook(state: dict) -> None:
return
model = FakeToolCallingModel(tool_calls=[[tool_call], []])
agent = create_react_agent(
model,
ToolNode([tool1], handle_tool_errors=False),
[tool1],
state_schema=state_schema,
store=store,
post_model_hook=post_model_hook,
+1 -1
View File
@@ -507,7 +507,7 @@ dev = [
[[package]]
name = "langgraph-sdk"
version = "0.1.74"
version = "0.1.73"
source = { editable = "../sdk-py" }
dependencies = [
{ name = "httpx" },
+1 -3
View File
@@ -251,7 +251,7 @@ class Thread(TypedDict):
values: Json
"""The current state of the thread."""
interrupts: dict[str, list[Interrupt]]
"""Mapping of task ids to interrupts that were raised in that task."""
"""Interrupts which were thrown in this thread"""
class ThreadTask(TypedDict):
@@ -284,8 +284,6 @@ class ThreadState(TypedDict):
"""The ID of the parent checkpoint. If missing, this is the root checkpoint."""
tasks: Sequence[ThreadTask]
"""Tasks to execute in this step. If already attempted, may contain an error."""
interrupts: list[Interrupt]
"""Interrupts which were thrown in this thread."""
class ThreadUpdateStateResponse(TypedDict):
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-sdk"
version = "0.1.74"
version = "0.1.73"
description = "SDK for interacting with LangGraph API"
authors = []
requires-python = ">=3.9"
+1 -1
View File
@@ -119,7 +119,7 @@ wheels = [
[[package]]
name = "langgraph-sdk"
version = "0.1.74"
version = "0.1.73"
source = { editable = "." }
dependencies = [
{ name = "httpx" },