Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn 656737009b Update 2025-03-07 10:49:59 -08:00
William Fu-Hinthorn ecdf70a2ab langgraph-cli-install 2025-03-07 10:21:20 -08:00
57 changed files with 1290 additions and 1923 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry check --lock
run: poetry lock --check
- name: Install dependencies
if: steps.changed-files.outputs.all
-6
View File
@@ -39,12 +39,6 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
- name: Check Lock
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
poetry check --lock
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦜🕸️LangGraph
[![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/)
![Version](https://img.shields.io/pypi/v/langgraph)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
@@ -10,7 +10,6 @@
"One of the most common use cases for persistence is to use it to keep track of conversation history. This is great - it makes it easy to continue conversations. As conversations get longer and longer, however, this conversation history can build up and take up more and more of the context window. This can often be undesirable as it leads to more expensive and longer calls to the LLM, and potentially ones that error. One way to work around that is to create a summary of the conversation to date, and use that with the past N messages. This guide will go through an example of how to do that.\n",
"\n",
"This will involve a few steps:\n",
"\n",
"- Check if the conversation is too long (can be done by checking number of messages or length of messages)\n",
"- If yes, the create summary (will need a prompt for this)\n",
"- Then remove all except the last N messages\n",
-191
View File
@@ -1,191 +0,0 @@
# LangGraph
## Quickstart
These guides are designed to help you get started with LangGraph.
- [LangGraph Quickstart](https://langchain-ai.github.io/langgraph/tutorials/introduction/): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [Common Workflows](https://langchain-ai.github.io/langgraph/tutorials/workflows/): Overview of the most common workflows using LLMs implemented with LangGraph.
- [LangGraph Server Quickstart](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/): Launch a LangGraph server locally and interact with it using REST API and LangGraph Studio Web UI.
- [Deploy with LangGraph Cloud Quickstart](https://langchain-ai.github.io/langgraph/cloud/quick_start/): Deploy a LangGraph app using LangGraph Cloud.
## Concepts
These guides provide explanations of the key concepts behind the LangGraph framework.
- [Why LangGraph?](https://langchain-ai.github.io/langgraph/concepts/high_level/): Motivation for LangGraph, a library for building agentic applications with LLMs.
- [LangGraph Glossary](https://langchain-ai.github.io/langgraph/concepts/low_level/): LangGraph workflows are designed as graphs, with nodes representing different components and edges representing the flow of information between them. This guide provides an overview of the key concepts associated with LangGraph graph primitives.
- [Common Agentic Patterns](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/): An agent uses an LLM to pick its own control flow to solve more complex problems! Agents are a key building block in many LLM applications. This guide explains the different types of agent architectures and how they can be used to control the flow of an application.
- [Multi-Agent Systems](https://langchain-ai.github.io/langgraph/concepts/multi_agent/): Complex LLM applications can often be broken down into multiple agents, each responsible for a different part of the application. This guide explains common patterns for building multi-agent systems.
- [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/breakpoints/): Breakpoints allow pausing the execution of a graph at specific points. Breakpoints allow stepping through graph execution for debugging purposes.
- [Human-in-the-Loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Explains different ways of integrating human feedback into a LangGraph application.
- [Time Travel](https://langchain-ai.github.io/langgraph/concepts/time-travel/): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
- [Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](https://langchain-ai.github.io/langgraph/concepts/streaming/): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API](https://langchain-ai.github.io/langgraph/concepts/functional_api/): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
- [Durable Execution](https://langchain-ai.github.io/langgraph/concepts/durable_execution/): LangGraph's built-in [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
- [Pregel](https://langchain-ai.github.io/langgraph/concepts/pregel/): Pregel is LangGraph's runtime, which is responsible for managing the execution of LangGraph applications.
- [FAQ](https://langchain-ai.github.io/langgraph/concepts/faq/): Frequently asked questions about LangGraph.
## How-tos
Here youll find answers to “How do I...?” types of questions.
These guides are **goal-oriented** and concrete.
They're meant to help you complete a specific task.
### Graph API Basics
- [How to update graph state from nodes](https://langchain-ai.github.io/langgraph/how-tos/state-reducers/)
- [How to create a sequence of steps](https://langchain-ai.github.io/langgraph/how-tos/sequence/)
- [How to create branches for parallel execution](https://langchain-ai.github.io/langgraph/how-tos/branching/)
- [How to create and control loops with recursion limits](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/)
- [How to visualize your graph](https://langchain-ai.github.io/langgraph/how-tos/visualization/)
### Fine-grained Control
These guides demonstrate LangGraph features that grant fine-grained control over the execution of your graph.
- [How to create map-reduce branches for parallel execution](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/)
- [How to update state and jump to nodes in graphs and subgraphs](https://langchain-ai.github.io/langgraph/how-tos/command/)
- [How to add runtime configuration to your graph](https://langchain-ai.github.io/langgraph/how-tos/configuration/)
- [How to add node retries](https://langchain-ai.github.io/langgraph/how-tos/node-retries/)
- [How to return state before hitting recursion limit](https://langchain-ai.github.io/langgraph/how-tos/return-when-recursion-limit-hits/)
### Persistence
Persistence makes it easy to persist state across graph runs (per-thread persistence) and across threads (cross-thread persistence).
These how-to guides show how to add persistence to your graph.
- [How to add thread-level persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/persistence/)
- [How to add thread-level persistence to a subgraph](https://langchain-ai.github.io/langgraph/how-tos/subgraph-persistence/)
- [How to add cross-thread persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/cross-thread-persistence/)
- [How to use Postgres checkpointer for persistence](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/)
- [How to use MongoDB checkpointer for persistence](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/)
- [How to create a custom checkpointer using Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/)
See the below guides for how-to add persistence to your workflow using the [Functional API](https://langchain-ai.github.io/langgraph/concepts/functional_api/):
- [How to add thread-level persistence (functional API)](https://langchain-ai.github.io/langgraph/how-tos/persistence-functional/)
- [How to add cross-thread persistence (functional API)](https://langchain-ai.github.io/langgraph/how-tos/cross-thread-persistence-functional/)
### Memory
LangGraph makes it easy to manage conversation memory in your graph. These how-to guides show how to implement different strategies for that.
- [How to manage conversation history](https://langchain-ai.github.io/langgraph/how-tos/memory/manage-conversation-history/)
- [How to delete messages](https://langchain-ai.github.io/langgraph/how-tos/memory/delete-messages/)
- [How to add summary conversation memory](https://langchain-ai.github.io/langgraph/how-tos/memory/add-summary-conversation-history/)
- [How to add long-term memory (cross-thread)](https://langchain-ai.github.io/langgraph/how-tos/memory/cross-thread-persistence/)
- [How to use semantic search for long-term memory](https://langchain-ai.github.io/langgraph/how-tos/memory/semantic-search/)
### Human-in-the-loop
Human-in-the-loop functionality allows you to involve humans in the decision-making process of your graph.
These how-to guides show how to implement human-in-the-loop workflows in your graph.
- [How to wait for user input](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/): A basic example that shows how to implement a human-in-the-loop workflow in your graph using the `interrupt` function.
- [How to review tool calls](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/review-tool-calls/): Incorporate human-in-the-loop for reviewing/editing/accepting tool call requests before they executed using the `interrupt` function.
- [How to add static breakpoints](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): Use for debugging purposes. For human-in-the-loop workflows, we recommend the [`interrupt` function](https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt) instead.
- [How to edit graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/edit-graph-state/): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
See the below guides for how-to implement human-in-the-loop workflows with the Functional API.
- [How to wait for user input (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/wait-user-input-functional/)
- [How to review tool calls (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/review-tool-calls-functional/)
### Time Travel
[Time travel](https://langchain-ai.github.io/langgraph/concepts/time-travel/) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
- [How to view and update past graph state](https://langchain-ai.github.io/langgraph/how-tos/time-travel/)
### Streaming
[Streaming](https://langchain-ai.github.io/langgraph/concepts/streaming/) is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [How to stream](https://langchain-ai.github.io/langgraph/how-tos/streaming/)
- [How to stream LLM tokens](https://langchain-ai.github.io/langgraph/how-tos/streaming-tokens/)
- [How to stream LLM tokens from specific nodes](https://langchain-ai.github.io/langgraph/how-tos/streaming-specific-nodes/)
- [How to stream data from within a tool](https://langchain-ai.github.io/langgraph/how-tos/streaming-events-from-within-tools/)
- [How to stream from subgraphs](https://langchain-ai.github.io/langgraph/how-tos/streaming-subgraphs/)
- [How to disable streaming for models that don't support it](https://langchain-ai.github.io/langgraph/how-tos/disable-streaming/)
### Tool calling
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of [chat model](https://python.langchain.com/docs/concepts/chat_models/) API.
It accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
These how-to guides show common patterns for tool calling with LangGraph:
- [How to call tools using ToolNode](https://langchain-ai.github.io/langgraph/how-tos/tool-calling/)
- [How to handle tool calling errors](https://langchain-ai.github.io/langgraph/how-tos/tool-calling-errors/)
- [How to pass runtime values to tools](https://langchain-ai.github.io/langgraph/how-tos/pass-run-time-values-to-tools/)
- [How to pass config to tools](https://langchain-ai.github.io/langgraph/how-tos/pass-config-to-tools/)
- [How to update graph state from tools](https://langchain-ai.github.io/langgraph/how-tos/update-state-from-tools/)
- [How to handle large numbers of tools](https://langchain-ai.github.io/langgraph/how-tos/many-tools/)
### Subgraphs
Subgraphs allow you to reuse an existing graph from another graph.
These how-to guides show how to use subgraphs:
- [How to use subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraph/)
- [How to view and update state in subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/)
- [How to transform inputs and outputs of a subgraph](https://langchain-ai.github.io/langgraph/how-tos/subgraph-transform-state/)
### Multi-agent
Multi-agent systems are useful to break down complex LLM applications into multiple agents, each responsible for a different part of the application.
These how-to guides show how to implement multi-agent systems in LangGraph:
- [How to implement handoffs between agents](https://langchain-ai.github.io/langgraph/how-tos/agent-handoffs/)
- [How to build a multi-agent network](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-network/)
- [How to add multi-turn conversation in a multi-agent application](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-multi-turn-convo/)
### State Management
- [How to use Pydantic model as graph state](https://langchain-ai.github.io/langgraph/how-tos/state-model/)
- [How to define input/output schema for your graph](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/)
- [How to pass private state between nodes inside the graph](https://langchain-ai.github.io/langgraph/how-tos/pass_private_state/)
### Other
- [How to run graph asynchronously](https://langchain-ai.github.io/langgraph/how-tos/async/)
- [How to force tool-calling agent to structure output](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/)
- [How to pass custom LangSmith run ID for graph runs](https://langchain-ai.github.io/langgraph/how-tos/run-id-langsmith/)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](https://langchain-ai.github.io/langgraph/how-tos/autogen-integration/)
## Use cases
Explore practical implementations tailored for specific scenarios:
### Chatbots
- [Customer Support](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/): Build a multi-functional support bot for flights, hotels, and car rentals.
- [Prompt Generation from User Requirements](https://langchain-ai.github.io/langgraph/tutorials/chatbots/information-gather-prompting/): Build an information gathering chatbot.
- [Code Assistant](https://langchain-ai.github.io/langgraph/tutorials/code_assistant/langgraph_code_assistant/): Build a code analysis and generation assistant.
### RAG
- [Agentic RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_agentic_rag/): Use an agent to figure out how to retrieve the most relevant information before using the retrieved information to answer the user's question.
- [Adaptive RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag/): Adaptive RAG is a strategy for RAG that unites (1) query analysis with (2) active / self-corrective RAG. Implementation of: https://arxiv.org/abs/2403.14403
- For a version that uses a local LLM: [Adaptive RAG using local LLMs](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/)
- [Corrective RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_crag/): Uses an LLM to grade the quality of the retrieved information from the given source, and if the quality is low, it will try to retrieve the information from another source. Implementation of: https://arxiv.org/pdf/2401.15884.pdf
- For a version that uses a local LLM: [Corrective RAG using local LLMs](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_crag_local/)
- [Self-RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_self_rag/): Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. Implementation of https://arxiv.org/abs/2310.11511.
- For a version that uses a local LLM: [Self-RAG using local LLMs](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_self_rag_local/)
- [SQL Agent](https://langchain-ai.github.io/langgraph/tutorials/sql-agent/): Build a SQL agent that can answer questions about a SQL database.
### Multi-Agent Systems
- [Network](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/): Enable two or more agents to collaborate on a task
- [Supervisor](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/): Use an LLM to orchestrate and delegate to individual agents
- [Hierarchical Teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/): Orchestrate nested teams of agents to solve problems
@@ -39,7 +39,6 @@ from langgraph.store.base import (
Result,
SearchItem,
SearchOp,
TTLConfig,
ensure_embeddings,
get_text_at_path,
tokenize_path,
@@ -623,7 +622,6 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
index: Optional[PostgresIndexConfig] = None,
ttl: Optional[TTLConfig] = None,
) -> None:
super().__init__()
self._deserializer = deserializer
@@ -636,7 +634,6 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
self.embeddings, self.index_config = _ensure_index_config(self.index_config)
else:
self.embeddings = None
self.ttl_config = ttl
@classmethod
@contextmanager
+480 -629
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.16"
version = "2.0.15"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -530,7 +530,6 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
for idx, (channel, value) in enumerate(writes)
],
)
await self.conn.commit()
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
"""Generate the next version ID for a channel.
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "2.0.6"
version = "2.0.5"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
+21 -145
View File
@@ -11,19 +11,9 @@ Core types:
from abc import ABC, abstractmethod
from datetime import datetime
from typing import (
Any,
Iterable,
Literal,
NamedTuple,
Optional,
TypedDict,
Union,
cast,
)
from typing import Any, Iterable, Literal, NamedTuple, Optional, TypedDict, Union, cast
from langchain_core.embeddings import Embeddings
from typing_extensions import override
from langgraph.store.base.embed import (
AEmbeddingsFunc,
@@ -34,20 +24,6 @@ from langgraph.store.base.embed import (
)
class NotProvided:
"""Sentinel singleton."""
def __bool__(self) -> Literal[False]:
return False
@override
def __repr__(self) -> str:
return "NOT_GIVEN"
NOT_PROVIDED = NotProvided()
class Item:
"""Represents a stored item with metadata.
@@ -83,7 +59,7 @@ class Item:
else created_at
)
self.updated_at = (
datetime.fromisoformat(cast(str, updated_at))
datetime.fromisoformat(cast(str, created_at))
if isinstance(updated_at, str)
else updated_at
)
@@ -520,25 +496,6 @@ class InvalidNamespaceError(ValueError):
"""Provided namespace is invalid."""
class TTLConfig(TypedDict, total=False):
"""Configuration for TTL (time-to-live) behavior in the store."""
refresh_on_read: bool
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
If True, TTLs will be refreshed on read operations (get/search) by default.
This can be overridden per-operation by explicitly setting refresh_ttl.
Defaults to True if not configured.
"""
default_ttl: Optional[float]
"""Default TTL (time-to-live) in minutes for new items.
If provided, new items will expire after this many minutes after their last access.
The expiration timer refreshes on both read and write operations.
Defaults to None (no expiration).
"""
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
@@ -683,8 +640,7 @@ class BaseStore(ABC):
Subclasses must explicitly set `supports_ttl = True` to enable this feature.
"""
supports_ttl: bool = False
ttl_config: Optional[TTLConfig] = None
supports_ttl = False
__slots__ = ("__weakref__",)
@@ -713,11 +669,7 @@ class BaseStore(ABC):
"""
def get(
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
) -> Optional[Item]:
"""Retrieve a single item.
@@ -725,15 +677,12 @@ class BaseStore(ABC):
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
refresh_ttl: Whether to refresh TTLs for the returned item.
If None (default), uses the store's default refresh_ttl setting.
If no TTL is specified, this argument is ignored.
Returns:
The retrieved item or None if not found.
"""
return self.batch(
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
)[0]
return self.batch([GetOp(namespace, key, refresh_ttl)])[0]
def search(
self,
@@ -744,7 +693,7 @@ class BaseStore(ABC):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: Optional[bool] = None,
refresh_ttl: bool = True,
) -> list[SearchItem]:
"""Search for items within a namespace prefix.
@@ -794,16 +743,7 @@ class BaseStore(ABC):
and requires proper embedding configuration.
"""
return self.batch(
[
SearchOp(
namespace_prefix,
filter,
limit,
offset,
query,
_ensure_refresh(self.ttl_config, refresh_ttl),
)
]
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
)[0]
def put(
@@ -813,7 +753,7 @@ class BaseStore(ABC):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
ttl: Optional[float] = None,
) -> None:
"""Store or update an item in the store.
@@ -866,22 +806,12 @@ class BaseStore(ABC):
```
"""
_validate_namespace(namespace)
if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
if ttl is not None and not self.supports_ttl:
raise NotImplementedError(
f"TTL is not supported by {self.__class__.__name__}. "
f"Use a store implementation that supports TTL or set ttl=None."
)
self.batch(
[
PutOp(
namespace,
str(key),
value,
index=index,
ttl=_ensure_ttl(self.ttl_config, ttl),
)
]
)
self.batch([PutOp(namespace, key, value, index=index, ttl=ttl)])
def delete(self, namespace: tuple[str, ...], key: str) -> None:
"""Delete an item.
@@ -890,7 +820,7 @@ class BaseStore(ABC):
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
"""
self.batch([PutOp(namespace, str(key), None, ttl=None)])
self.batch([PutOp(namespace, key, None, ttl=None)])
def list_namespaces(
self,
@@ -946,11 +876,7 @@ class BaseStore(ABC):
return self.batch([op])[0]
async def aget(
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
) -> Optional[Item]:
"""Asynchronously retrieve a single item.
@@ -961,17 +887,7 @@ class BaseStore(ABC):
Returns:
The retrieved item or None if not found.
"""
return (
await self.abatch(
[
GetOp(
namespace,
str(key),
_ensure_refresh(self.ttl_config, refresh_ttl),
)
]
)
)[0]
return (await self.abatch([GetOp(namespace, key, refresh_ttl)]))[0]
async def asearch(
self,
@@ -982,7 +898,7 @@ class BaseStore(ABC):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: Optional[bool] = None,
refresh_ttl: bool = True,
) -> list[SearchItem]:
"""Asynchronously search for items within a namespace prefix.
@@ -993,8 +909,8 @@ class BaseStore(ABC):
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
refresh_ttl: Whether to refresh TTLs for the returned items.
If None (default), uses the store's TTLConfig.refresh_default setting.
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
Defaults to True. If no TTL is specified, this argument
is ignored.
Returns:
List of items matching the search criteria.
@@ -1034,16 +950,7 @@ class BaseStore(ABC):
"""
return (
await self.abatch(
[
SearchOp(
namespace_prefix,
filter,
limit,
offset,
query,
_ensure_refresh(self.ttl_config, refresh_ttl),
)
]
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
)
)[0]
@@ -1054,7 +961,7 @@ class BaseStore(ABC):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
ttl: Optional[float] = None,
) -> None:
"""Asynchronously store or update an item in the store.
@@ -1115,22 +1022,12 @@ class BaseStore(ABC):
```
"""
_validate_namespace(namespace)
if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
if ttl is not None and not self.supports_ttl:
raise NotImplementedError(
f"TTL is not supported by {self.__class__.__name__}. "
f"Use a store implementation that supports TTL or set ttl=None."
)
await self.abatch(
[
PutOp(
namespace,
str(key),
value,
index=index,
ttl=_ensure_ttl(self.ttl_config, ttl),
)
]
)
await self.abatch([PutOp(namespace, key, value, index=index, ttl=ttl)])
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
"""Asynchronously delete an item.
@@ -1139,7 +1036,7 @@ class BaseStore(ABC):
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
"""
await self.abatch([PutOp(namespace, str(key), None)])
await self.abatch([PutOp(namespace, key, None)])
async def alist_namespaces(
self,
@@ -1219,27 +1116,6 @@ def _validate_namespace(namespace: tuple[str, ...]) -> None:
)
def _ensure_refresh(
ttl_config: Optional[TTLConfig], refresh_ttl: Optional[bool] = None
) -> bool:
if refresh_ttl is not None:
return refresh_ttl
if ttl_config is not None:
return ttl_config.get("refresh_on_read", True)
return True
def _ensure_ttl(
ttl_config: Optional[TTLConfig],
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
) -> Optional[float]:
if ttl is NOT_PROVIDED:
if ttl_config:
return ttl_config.get("default_ttl")
return None
return ttl
__all__ = [
"BaseStore",
"Item",
+10 -45
View File
@@ -5,21 +5,17 @@ from collections.abc import Iterable
from typing import Any, Callable, Literal, Optional, TypeVar, Union
from langgraph.store.base import (
NOT_PROVIDED,
BaseStore,
GetOp,
Item,
ListNamespacesOp,
MatchCondition,
NamespacePath,
NotProvided,
Op,
PutOp,
Result,
SearchItem,
SearchOp,
_ensure_refresh,
_ensure_ttl,
_validate_namespace,
)
@@ -69,24 +65,11 @@ class AsyncBatchedBaseStore(BaseStore):
pass
async def aget(
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
) -> Optional[Item]:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue.put_nowait(
(
fut,
GetOp(
namespace,
key,
refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
),
)
)
self._aqueue.put_nowait((fut, GetOp(namespace, key, refresh_ttl=refresh_ttl)))
return await fut
async def asearch(
@@ -98,7 +81,7 @@ class AsyncBatchedBaseStore(BaseStore):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: Optional[bool] = None,
refresh_ttl: bool = True,
) -> list[SearchItem]:
assert not self._task.done()
fut = self._loop.create_future()
@@ -111,7 +94,7 @@ class AsyncBatchedBaseStore(BaseStore):
limit,
offset,
query,
refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
refresh_ttl=refresh_ttl,
),
)
)
@@ -124,19 +107,12 @@ class AsyncBatchedBaseStore(BaseStore):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
ttl: Optional[float] = None,
) -> None:
assert not self._task.done()
_validate_namespace(namespace)
fut = self._loop.create_future()
self._aqueue.put_nowait(
(
fut,
PutOp(
namespace, key, value, index, ttl=_ensure_ttl(self.ttl_config, ttl)
),
)
)
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index, ttl=ttl)))
return await fut
async def adelete(
@@ -181,11 +157,7 @@ class AsyncBatchedBaseStore(BaseStore):
@_check_loop
def get(
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
) -> Optional[Item]:
return asyncio.run_coroutine_threadsafe(
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
@@ -201,7 +173,7 @@ class AsyncBatchedBaseStore(BaseStore):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: Optional[bool] = None,
refresh_ttl: bool = True,
) -> list[SearchItem]:
return asyncio.run_coroutine_threadsafe(
self.asearch(
@@ -223,18 +195,11 @@ class AsyncBatchedBaseStore(BaseStore):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
ttl: Optional[float] = None,
) -> None:
_validate_namespace(namespace)
asyncio.run_coroutine_threadsafe(
self.aput(
namespace,
key=key,
value=value,
index=index,
ttl=_ensure_ttl(self.ttl_config, ttl),
),
self._loop,
self.aput(namespace, key=key, value=value, index=index, ttl=ttl), self._loop
).result()
@_check_loop
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.19"
version = "2.0.17"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+1 -1
View File
@@ -130,7 +130,7 @@ def test_serde_jsonplus() -> None:
key="my-key",
namespace=("a", "name", " "),
created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
),
}
+41
View File
@@ -0,0 +1,41 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
env/
ENV/
# Editor
.idea/
.vscode/
*.swp
*.swo
# OS specific
.DS_Store
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+24
View File
@@ -0,0 +1,24 @@
.PHONY: install format lint clean build publish
install:
poetry install
format:
poetry run ruff format langgraph_cli_install tests
lint:
poetry run ruff check langgraph_cli_install tests
test:
poetry run pytest
clean:
rm -rf dist/
rm -rf build/
rm -rf *.egg-info/
build: clean
poetry build
publish: build
poetry publish
+50
View File
@@ -0,0 +1,50 @@
# LangGraph CLI Installer
A simple installer for the LangGraph CLI that uses `uv` to create an isolated environment.
## Why?
This lightweight installer creates an isolated installation of LangGraph CLI without worrying about Python environment conflicts or dependencies. It uses [uv](https://github.com/astral-sh/uv) to create a standalone environment with LangGraph CLI.
Key benefits:
- Prevents conflicts with other Python packages
- No knowledge of virtual environments needed
- Adds to your PATH automatically
- Installs the latest version of LangGraph CLI
## Quick Install
Simply run:
```bash
pip install langgraph-cli-install && langgraph-cli-install
```
This will:
1. Install the uv package if not already installed
2. Create an isolated environment with the latest LangGraph CLI
3. Add the CLI to your PATH automatically
After installation, you can run `langgraph --help` to get started.
## How It Works
This installer is similar to [aider-install](https://github.com/paul-gauthier/aider/blob/main/aider_install/main.py). It:
1. Uses the `uv` Python installer to create an isolated environment
2. Installs the latest `langgraph-cli` in that environment
3. Adds the installed binary to your PATH
This approach dramatically reduces installation issues caused by Python environment conflicts.
## Manual Installation
If you prefer not to use this installer, you can install LangGraph CLI directly:
```bash
pip install langgraph-cli
```
## License
MIT
@@ -0,0 +1,7 @@
"""LangGraph CLI Installer package."""
__version__ = "0.1.0"
from .main import main
__all__ = ["main"]
@@ -0,0 +1,109 @@
"""LangGraph CLI Installation Script.
Main entry point for installing langgraph-cli in an isolated environment.
This script uses uv to create an isolated installation of langgraph-cli.
"""
import platform
import subprocess
import sys
import uv
def main():
"""Install langgraph-cli using uv in an isolated environment."""
print("Installing LangGraph CLI...")
try:
uv_bin = uv.find_uv_bin()
# Get best Python version for installation (prefer 3.12 if available)
python_version = get_latest_python_version()
# Create an isolated environment with langgraph-cli
print(f"Creating isolated environment using {python_version}...")
subprocess.check_call(
[
uv_bin,
"tool",
"install",
"--force",
"--python",
python_version,
"langgraph-cli@latest",
]
)
# Update PATH so the tool is available
subprocess.check_call([uv_bin, "tool", "update-shell"])
# Show install location and help
show_success_message(uv_bin)
except subprocess.CalledProcessError as e:
print(f"\nFailed to install langgraph-cli: {e}")
sys.exit(1)
except Exception as e:
print(f"\nAn error occurred: {e}")
sys.exit(1)
def get_latest_python_version() -> str:
"""Get the latest compatible Python version for installation."""
# Try to use Python 3.13 if possible, otherwise fall back to the current version
target_version = "3.13"
try:
# Check if this version is available through uv
uv_bin = uv.find_uv_bin()
result = subprocess.run(
[uv_bin, "python", "list"],
capture_output=True,
text=True,
check=False,
)
if target_version in result.stdout:
return f"python{target_version}"
except Exception:
pass
# Fall back to current version
major, minor = sys.version_info.major, sys.version_info.minor
return f"python{major}.{minor}"
def show_success_message(uv_bin):
"""Show success message and installation details."""
# Get installation path
result = subprocess.run(
[uv_bin, "tool", "list"],
capture_output=True,
text=True,
check=True,
)
install_path = None
for line in result.stdout.splitlines():
if "langgraph-cli" in line:
parts = line.strip().split()
if len(parts) >= 2:
install_path = parts[1]
break
# Success message
print("\n🎉 LangGraph CLI has been successfully installed!\n")
print("You can now use it by running:")
print(" langgraph --help")
if install_path:
print(f"\nInstalled at: {install_path}")
# Provide hint about shell restart if needed
if platform.system() != "Windows":
print("\nNote: You may need to restart your terminal or run:")
print(" source ~/.bashrc # or ~/.zshrc depending on your shell")
print("to ensure the langgraph command is available in your PATH.")
if __name__ == "__main__":
main()
+44
View File
@@ -0,0 +1,44 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "uv"
version = "0.1.45"
description = "An extremely fast Python package installer and resolver, written in Rust."
optional = false
python-versions = ">=3.8"
files = [
{file = "uv-0.1.45-py3-none-linux_armv6l.whl", hash = "sha256:088af576fb0e0462cd5f718d03fb1a9f16ce5ae61fdb2a9d3ea938fc826cecc1"},
{file = "uv-0.1.45-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b94180009264f3f7ee74250f8e4f99c8cb0cb3633e3a9c9c66cdef3eb69be575"},
{file = "uv-0.1.45-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e5d55f0f8b6ae416c72d78106e224c8e8338356da21ddebecc7b1723de80924"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7fdb235aaf420fa8ac9009999b1654a23540f03e25c35094543c2f48d7c41aef"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de81501c0b03160d0944906d1a713f108258360e20c58385974acb7253b56166"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346aa2d0a4ad3c0c3f7852c1edf5e5a8e5d2ef34c7474e9089877291c2da979c"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a601eed14d484d36d421e4208911a56aaf758ea6c385ef8edf8ad9f8ead57ce1"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ca2d5a5e06c5f71c7b213e14fa59129e63b77de3ffbcf84ecc98d647d73a821"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90b68c80dddebeca69b26a2af1e2e683804bcf2b5f22d107af03d9156d6218c6"},
{file = "uv-0.1.45-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7f2f64fdded940342dc37234c11ae3508222c3c9b6b0eac5879dcd586010fa"},
{file = "uv-0.1.45-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a39141e179fea043151a165c9155031e7976b0e4b076c0c33a45b58a420134e0"},
{file = "uv-0.1.45-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:68718add6ee2cef2816f9bf8a1dbf2d8cf63d98ddf45840f340029f65a49fd89"},
{file = "uv-0.1.45-py3-none-musllinux_1_1_i686.whl", hash = "sha256:110e0f45ddb2fe832ce50b0308be90e5439e0c02d3ffe042feeb3f759811f31f"},
{file = "uv-0.1.45-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:0f6cfe885f109bacc055edd5df2c837616ae2238b9324a9d37835a96b204ab2f"},
{file = "uv-0.1.45-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:87e77d25e8f358c0d5de1983497ee4cf4cea8fc73373d1ef1063533352db2f89"},
{file = "uv-0.1.45-py3-none-win32.whl", hash = "sha256:ddb93620c9e01fa83573c2648df4bee3fa548ca940de51c8a2c3566a23a0c776"},
{file = "uv-0.1.45-py3-none-win_amd64.whl", hash = "sha256:8e2eeea4eec0e09f7d67378152428b5308dba8b33990d045d7a31d19bf18ca1f"},
{file = "uv-0.1.45.tar.gz", hash = "sha256:40fab956bc7af50dfa4bda14e5871528f57603eb9bf8595eb3144aace0ed8c47"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.8.0,<4.0"
content-hash = "ac29a6587488fe83583561554cb37b0812f4609cf34fedc4afae8df804db0d73"
+36
View File
@@ -0,0 +1,36 @@
[tool.poetry]
name = "langgraph-cli-install"
version = "0.0.1-rc1"
description = "Simple installer for langgraph-cli"
authors = []
license = "MIT"
readme = "README.md"
repository = "https://www.github.com/langchain-ai/langgraph"
packages = [{ include = "langgraph_cli_install" }]
[tool.poetry.scripts]
langgraph-cli-install = "langgraph_cli_install.main:main"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
packaging = ">=23.0"
uv = ">=0.6.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
lint.select = [
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# isort
"I",
]
lint.ignore = ["E501", "B008"]
+36
View File
@@ -0,0 +1,36 @@
"""Setup script for the langgraph-cli-install package."""
from setuptools import find_packages, setup
if __name__ == "__main__":
setup(
name="langgraph-cli-install",
version="0.1.0",
description="Simple installer for langgraph-cli",
author="",
author_email="",
license="MIT",
packages=find_packages(),
include_package_data=True,
entry_points={
"console_scripts": [
"langgraph-cli-install=langgraph_cli_install.main:main",
],
},
python_requires=">=3.8",
install_requires=[
"uv>=0.1.24",
"packaging>=23.0",
],
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
)
+1
View File
@@ -0,0 +1 @@
"""Test package for langgraph-cli-install."""
+50
View File
@@ -0,0 +1,50 @@
"""Tests for the main module."""
import sys
from unittest.mock import MagicMock, patch
from langgraph_cli_install.main import get_latest_python_version, main
def test_get_latest_python_version():
"""Test that the get_latest_python_version function returns a string."""
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.stdout = "python3.12"
mock_run.return_value = mock_result
with patch("uv.find_uv_bin", return_value="/path/to/uv"):
version = get_latest_python_version()
assert isinstance(version, str)
assert "python" in version
def test_get_latest_python_version_fallback():
"""Test fallback to current version when 3.12 is not available."""
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.stdout = "python3.8" # No 3.12 here
mock_run.return_value = mock_result
with patch("uv.find_uv_bin", return_value="/path/to/uv"):
# Mock sys.version_info
old_version_info = sys.version_info
sys.version_info = MagicMock()
sys.version_info.major = 3
sys.version_info.minor = 9
try:
version = get_latest_python_version()
assert isinstance(version, str)
assert "python3.9" in version
finally:
# Restore original version_info
sys.version_info = old_version_info
def test_main_exception():
"""Test main function handles exceptions."""
with patch("uv.find_uv_bin", side_effect=Exception("Test error")):
with patch("sys.exit") as mock_exit:
main()
mock_exit.assert_called_once_with(1)
+8
View File
@@ -0,0 +1,8 @@
"""Test that the version is defined."""
import langgraph_cli_install
def test_version():
"""Test that the version is defined."""
assert langgraph_cli_install.__version__ is not None
+1 -26
View File
@@ -11,24 +11,6 @@ MIN_NODE_VERSION = "20"
MIN_PYTHON_VERSION = "3.11"
class TTLConfig(TypedDict, total=False):
"""Configuration for TTL (time-to-live) behavior in the store."""
refresh_on_read: bool
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
If True, TTLs will be refreshed on read operations (get/search) by default.
This can be overridden per-operation by explicitly setting refresh_ttl.
Defaults to True if not configured.
"""
default_ttl: Optional[float]
"""Optional. Default TTL (time-to-live) in minutes for new items.
If provided, all new items will have this TTL unless explicitly overridden.
If omitted, items will have no TTL by default.
"""
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
@@ -97,13 +79,6 @@ class StoreConfig(TypedDict, total=False):
If omitted, no vector index is initialized.
"""
ttl: Optional[TTLConfig]
"""Optional. Defines the TTL (time-to-live) behavior configuration.
If provided, the store will apply TTL settings according to the configuration.
If omitted, no TTL behavior is configured.
"""
class SecurityConfig(TypedDict, total=False):
"""Configuration for OpenAPI security definitions and requirements.
@@ -223,7 +198,7 @@ class CorsConfig(TypedDict, total=False):
allow_origin_regex: str
"""Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
Example: "^https://.*\.mycompany\.com$"
Example: "^https://\\.*\\.mycompany\\.com$"
"""
expose_headers: list[str]
"""Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.76"
version = "0.1.75"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
-32
View File
@@ -397,17 +397,6 @@
}
],
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
},
"ttl": {
"anyOf": [
{
"$ref": "#/$defs/TTLConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
}
},
"required": []
@@ -441,27 +430,6 @@
}
},
"required": []
},
"TTLConfig": {
"title": "TTLConfig",
"description": "Configuration for TTL (time-to-live) behavior in the store.",
"type": "object",
"properties": {
"default_ttl": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"refresh_on_read": {
"type": "boolean"
}
},
"required": []
}
},
"title": "LangGraph CLI Configuration",
-32
View File
@@ -397,17 +397,6 @@
}
],
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
},
"ttl": {
"anyOf": [
{
"$ref": "#/$defs/TTLConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
}
},
"required": []
@@ -441,27 +430,6 @@
}
},
"required": []
},
"TTLConfig": {
"title": "TTLConfig",
"description": "Configuration for TTL (time-to-live) behavior in the store.",
"type": "object",
"properties": {
"default_ttl": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"refresh_on_read": {
"type": "boolean"
}
},
"required": []
}
},
"title": "LangGraph CLI Configuration",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦜🕸️LangGraph
[![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/)
![Version](https://img.shields.io/pypi/v/langgraph)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
-215
View File
@@ -1,215 +0,0 @@
import asyncio
from inspect import (
isfunction,
ismethod,
signature,
)
from types import FunctionType
from typing import (
Any,
Awaitable,
Callable,
Hashable,
Literal,
NamedTuple,
Optional,
Sequence,
Type,
Union,
cast,
get_args,
get_origin,
get_type_hints,
)
from langchain_core.runnables import (
Runnable,
RunnableConfig,
RunnableLambda,
)
from langgraph.constants import END, START
from langgraph.errors import InvalidUpdateError
from langgraph.pregel.write import ChannelWrite
from langgraph.types import Send
from langgraph.utils.runnable import (
RunnableCallable,
)
def _get_branch_path_input_schema(
path: Union[
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
Runnable[Any, Union[Hashable, list[Hashable]]],
],
) -> Optional[Type[Any]]:
input = None
# detect input schema annotation in the branch callable
try:
callable_: Optional[
Union[
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
]
] = None
if isinstance(path, (RunnableCallable, RunnableLambda)):
if isfunction(path.func) or ismethod(path.func):
callable_ = path.func
elif (callable_method := getattr(path.func, "__call__", None)) and ismethod(
callable_method
):
callable_ = callable_method
elif isfunction(path.afunc) or ismethod(path.afunc):
callable_ = path.afunc
elif (
callable_method := getattr(path.afunc, "__call__", None)
) and ismethod(callable_method):
callable_ = callable_method
elif callable(path):
callable_ = path
if callable_ is not None and (hints := get_type_hints(callable_)):
first_parameter_name = next(
iter(signature(cast(FunctionType, callable_)).parameters.keys())
)
if input_hint := hints.get(first_parameter_name):
if isinstance(input_hint, type) and get_type_hints(input_hint):
input = input_hint
except (TypeError, StopIteration):
pass
return input
class Branch(NamedTuple):
path: Runnable[Any, Union[Hashable, list[Hashable]]]
ends: Optional[dict[Hashable, str]]
then: Optional[str] = None
input_schema: Optional[Type[Any]] = None
@classmethod
def from_path(
cls,
path: Runnable[Any, Union[Hashable, list[Hashable]]],
path_map: Optional[Union[dict[Hashable, str], list[str]]],
then: Optional[str] = None,
infer_schema: bool = False,
) -> "Branch":
# coerce path_map to a dictionary
path_map_: Optional[dict[Hashable, str]] = None
try:
if isinstance(path_map, dict):
path_map_ = path_map.copy()
elif isinstance(path_map, list):
path_map_ = {name: name for name in path_map}
else:
# find func
func: Optional[Callable] = None
if isinstance(path, (RunnableCallable, RunnableLambda)):
func = path.func or path.afunc
if func is not None:
# find callable method
if (cal := getattr(path, "__call__", None)) and ismethod(cal):
func = cal
# get the return type
if rtn_type := get_type_hints(func).get("return"):
if get_origin(rtn_type) is Literal:
path_map_ = {name: name for name in get_args(rtn_type)}
except Exception:
pass
# infer input schema
input_schema = _get_branch_path_input_schema(path) if infer_schema else None
# create branch
return cls(path=path, ends=path_map_, then=then, input_schema=input_schema)
def run(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
reader: Optional[Callable[[RunnableConfig], Any]] = None,
) -> RunnableCallable:
return ChannelWrite.register_writer(
RunnableCallable(
func=self._route,
afunc=self._aroute,
writer=writer,
reader=reader,
name=None,
trace=False,
)
)
def _route(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = reader(config)
# passthrough additional keys from node to branch
# only doable when using dict states
if (
isinstance(value, dict)
and isinstance(input, dict)
and self.input_schema is None
):
value = {**input, **value}
else:
value = input
result = self.path.invoke(value, config)
return self._finish(writer, input, result, config)
async def _aroute(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = await asyncio.to_thread(reader, config)
# passthrough additional keys from node to branch
# only doable when using dict states
if (
isinstance(value, dict)
and isinstance(input, dict)
and self.input_schema is None
):
value = {**input, **value}
else:
value = input
result = await self.path.ainvoke(value, config)
return self._finish(writer, input, result, config)
def _finish(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
input: Any,
result: Any,
config: RunnableConfig,
) -> Union[Runnable, Any]:
if not isinstance(result, (list, tuple)):
result = [result]
if self.ends:
destinations: Sequence[Union[Send, str]] = [
r if isinstance(r, Send) else self.ends[r] for r in result
]
else:
destinations = cast(Sequence[Union[Send, str]], result)
if any(dest is None or dest == START for dest in destinations):
raise ValueError("Branch did not return a valid destination")
if any(p.node == END for p in destinations if isinstance(p, Send)):
raise InvalidUpdateError("Cannot send a packet to the END node")
return writer(destinations, config) or input
+117 -5
View File
@@ -1,3 +1,4 @@
import asyncio
import logging
from collections import defaultdict
from typing import (
@@ -5,11 +6,15 @@ from typing import (
Awaitable,
Callable,
Hashable,
Literal,
NamedTuple,
Optional,
Sequence,
Union,
cast,
get_args,
get_origin,
get_type_hints,
overload,
)
@@ -29,12 +34,12 @@ from langgraph.constants import (
TAG_HIDDEN,
Send,
)
from langgraph.graph.branch import Branch
from langgraph.errors import InvalidUpdateError
from langgraph.pregel import Channel, Pregel
from langgraph.pregel.read import PregelNode
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.types import All, Checkpointer
from langgraph.utils.runnable import RunnableLike, coerce_to_runnable
from langgraph.utils.runnable import RunnableCallable, RunnableLike, coerce_to_runnable
logger = logging.getLogger(__name__)
@@ -45,6 +50,95 @@ class NodeSpec(NamedTuple):
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
class Branch(NamedTuple):
path: Runnable[Any, Union[Hashable, list[Hashable]]]
ends: Optional[dict[Hashable, str]]
then: Optional[str] = None
def run(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
reader: Optional[Callable[[RunnableConfig], Any]] = None,
) -> RunnableCallable:
return ChannelWrite.register_writer(
RunnableCallable(
func=self._route,
afunc=self._aroute,
writer=writer,
reader=reader,
name=None,
trace=False,
)
)
def _route(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = reader(config)
# passthrough additional keys from node to branch
# only doable when using dict states
if isinstance(value, dict) and isinstance(input, dict):
value = {**input, **value}
else:
value = input
result = self.path.invoke(value, config)
return self._finish(writer, input, result, config)
async def _aroute(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = await asyncio.to_thread(reader, config)
# passthrough additional keys from node to branch
# only doable when using dict states
if isinstance(value, dict) and isinstance(input, dict):
value = {**input, **value}
else:
value = input
result = await self.path.ainvoke(value, config)
return self._finish(writer, input, result, config)
def _finish(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
input: Any,
result: Any,
config: RunnableConfig,
) -> Union[Runnable, Any]:
if not isinstance(result, (list, tuple)):
result = [result]
if self.ends:
destinations: Sequence[Union[Send, str]] = [
r if isinstance(r, Send) else self.ends[r] for r in result
]
else:
destinations = cast(Sequence[Union[Send, str]], result)
if any(dest is None or dest == START for dest in destinations):
raise ValueError("Branch did not return a valid destination")
if any(p.node == END for p in destinations if isinstance(p, Send)):
raise InvalidUpdateError("Cannot send a packet to the END node")
return writer(destinations, config) or input
class Graph:
def __init__(self) -> None:
self.nodes: dict[str, NodeSpec] = {}
@@ -173,7 +267,25 @@ class Graph:
"Adding an edge to a graph that has already been compiled. This will "
"not be reflected in the compiled graph."
)
# coerce path_map to a dictionary
try:
if isinstance(path_map, dict):
path_map_ = path_map.copy()
elif isinstance(path_map, list):
path_map_ = {name: name for name in path_map}
elif isinstance(path, Runnable):
path_map_ = None
elif rtn_type := get_type_hints(path.__call__).get( # type: ignore[operator]
"return"
) or get_type_hints(path).get("return"):
if get_origin(rtn_type) is Literal:
path_map_ = {name: name for name in get_args(rtn_type)}
else:
path_map_ = None
else:
path_map_ = None
except Exception:
path_map_ = None
# find a name for the condition
path = coerce_to_runnable(path, name=None, trace=True)
name = path.name or "condition"
@@ -183,7 +295,7 @@ class Graph:
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
)
# save it
self.branches[source][name] = Branch.from_path(path, path_map, then, False)
self.branches[source][name] = Branch(path, path_map_, then)
return self
def set_entry_point(self, key: str) -> Self:
@@ -472,7 +584,7 @@ class CompiledGraph(Pregel):
)
subgraph.trim_first_node()
subgraph.trim_last_node()
if len(subgraph.nodes) >= 1:
if len(subgraph.nodes) > 1:
e, s = graph.extend(subgraph, prefix=key)
if e is None:
raise ValueError(
+13 -94
View File
@@ -7,9 +7,7 @@ from inspect import isclass, isfunction, ismethod, signature
from types import FunctionType
from typing import (
Any,
Awaitable,
Callable,
Hashable,
Literal,
NamedTuple,
Optional,
@@ -42,14 +40,7 @@ from langgraph.errors import (
ParentCommand,
create_error_message,
)
from langgraph.graph.branch import Branch
from langgraph.graph.graph import (
END,
START,
CompiledGraph,
Graph,
Send,
)
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
from langgraph.managed.base import (
ChannelKeyPlaceholder,
ChannelTypePlaceholder,
@@ -470,57 +461,6 @@ class StateGraph(Graph):
self.waiting_edges.add((tuple(start_key), end_key))
return self
def add_conditional_edges(
self,
source: str,
path: Union[
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
Runnable[Any, Union[Hashable, list[Hashable]]],
],
path_map: Optional[Union[dict[Hashable, str], list[str]]] = None,
then: Optional[str] = None,
) -> Self:
"""Add a conditional edge from the starting node to any number of destination nodes.
Args:
source (str): The starting node. This conditional edge will run when
exiting this node.
path (Union[Callable, Runnable]): The callable that determines the next
node or nodes. If not specifying `path_map` it should return one or
more nodes. If it returns END, the graph will stop execution.
path_map (Optional[dict[Hashable, str]]): Optional mapping of paths to node
names. If omitted the paths returned by `path` should be node names.
then (Optional[str]): The name of a node to execute after the nodes
selected by `path`.
Returns:
Self: The instance of the graph, allowing for method chaining.
Note: Without typehints on the `path` function's return value (e.g., `-> Literal["foo", "__end__"]:`)
or a path_map, the graph visualization assumes the edge could transition to any node in the graph.
""" # noqa: E501
if self.compiled:
logger.warning(
"Adding an edge to a graph that has already been compiled. This will "
"not be reflected in the compiled graph."
)
# find a name for the condition
path = coerce_to_runnable(path, name=None, trace=True)
name = path.name or "condition"
# validate the condition
if name in self.branches[source]:
raise ValueError(
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
)
# save it
self.branches[source][name] = Branch.from_path(path, path_map, then, True)
if schema := self.branches[source][name].input_schema:
self._add_schema(schema)
return self
def add_sequence(
self,
nodes: Sequence[Union[RunnableLike, tuple[str, RunnableLike]]],
@@ -626,11 +566,6 @@ class StateGraph(Graph):
compiled = CompiledStateGraph(
builder=self,
config_type=self.config_schema,
input_model=self.input
if len(self.channels) > 1
and isclass(self.input)
and issubclass(self.input, (BaseModel, BaseModelV1))
else None,
nodes={},
channels={
**self.channels,
@@ -817,7 +752,11 @@ class CompiledStateGraph(CompiledGraph):
# read state keys and managed values
channels=(list(input_values) if is_single_input else input_values),
# coerce state dict to schema class (eg. pydantic model)
mapper=_pick_mapper(list(input_values), input_schema),
mapper=(
None
if is_single_input or issubclass(input_schema, dict)
else partial(_coerce_state, input_schema)
),
writers=[
# publish to this channel and state keys
ChannelWrite(
@@ -887,12 +826,12 @@ class CompiledStateGraph(CompiledGraph):
config, cast(Sequence[Union[Send, ChannelWriteEntry]], writes)
)
schema = branch.input_schema or (
# attach branch publisher
schema = (
self.builder.nodes[start].input
if start in self.builder.nodes
else self.builder.schema
)
# attach branch publisher
self.nodes[start] |= branch.run(
branch_writer,
_get_state_reader(self.builder, schema) if with_reader else None,
@@ -932,34 +871,14 @@ def _get_state_reader(
select=select[0] if select == ["__root__"] else select,
fresh=True,
# coerce state dict to schema class (eg. pydantic model)
mapper=_pick_mapper(state_keys, schema),
mapper=(
None
if state_keys == ["__root__"] or issubclass(schema, dict)
else partial(_coerce_state, schema)
),
)
def _pick_mapper(
state_keys: Sequence[str], schema: Type[Any]
) -> Optional[Callable[[Any], Any]]:
if state_keys == ["__root__"]:
return None
if issubclass(schema, dict):
return None
if issubclass(schema, BaseModel):
return partial(_coerce_state_pydantic, schema)
if issubclass(schema, BaseModelV1):
return partial(_coerce_state_pydantic_v1, schema)
return partial(_coerce_state, schema)
def _coerce_state_pydantic(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
return schema.model_construct(**input)
def _coerce_state_pydantic_v1(
schema: Type[Any], input: dict[str, Any]
) -> dict[str, Any]:
return schema.construct(**input)
def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
return schema(**input)
@@ -496,8 +496,6 @@ class Pregel(PregelProtocol):
config_type: Optional[Type[Any]] = None
input_model: Optional[Type[BaseModel]] = None
config: Optional[RunnableConfig] = None
name: str = "LangGraph"
@@ -521,7 +519,6 @@ class Pregel(PregelProtocol):
store: Optional[BaseStore] = None,
retry_policy: Optional[RetryPolicy] = None,
config_type: Optional[Type[Any]] = None,
input_model: Optional[Type[BaseModel]] = None,
config: Optional[RunnableConfig] = None,
name: str = "LangGraph",
) -> None:
@@ -540,7 +537,6 @@ class Pregel(PregelProtocol):
self.store = store
self.retry_policy = retry_policy
self.config_type = config_type
self.input_model = input_model
self.config = config
self.name = name
if auto_validate:
@@ -654,8 +650,6 @@ class Pregel(PregelProtocol):
def get_input_schema(
self, config: Optional[RunnableConfig] = None
) -> Type[BaseModel]:
if self.input_model is not None:
return self.input_model
config = merge_configs(self.config, config)
if isinstance(self.input_channels, str):
return super().get_input_schema(config)
@@ -1973,7 +1967,6 @@ class Pregel(PregelProtocol):
)
with SyncPregelLoop(
input,
input_model=self.input_model,
stream=StreamProtocol(stream.put, stream_modes),
config=config,
store=store,
@@ -2264,7 +2257,6 @@ class Pregel(PregelProtocol):
)
async with AsyncPregelLoop(
input,
input_model=self.input_model,
stream=StreamProtocol(stream.put_nowait, stream_modes),
config=config,
store=store,
+3 -30
View File
@@ -23,7 +23,6 @@ from typing import (
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel
from typing_extensions import ParamSpec, Self
from langgraph.channels.base import BaseChannel
@@ -126,7 +125,6 @@ P = ParamSpec("P")
INPUT_DONE = object()
INPUT_RESUMING = object()
INPUT_SHOULD_VALIDATE = object()
SPECIAL_CHANNELS = (ERROR, INTERRUPT, SCHEDULED)
@@ -141,7 +139,6 @@ def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
class PregelLoop(LoopProtocol):
input: Optional[Any]
input_model: Optional[Type[BaseModel]]
checkpointer: Optional[BaseCheckpointSaver]
nodes: Mapping[str, PregelNode]
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
@@ -205,7 +202,6 @@ class PregelLoop(LoopProtocol):
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
input_model: Optional[Type[BaseModel]] = None,
debug: bool = False,
) -> None:
super().__init__(
@@ -216,7 +212,6 @@ class PregelLoop(LoopProtocol):
store=store,
)
self.input = input
self.input_model = input_model
self.checkpointer = checkpointer
self.nodes = nodes
self.specs = specs
@@ -400,7 +395,7 @@ class PregelLoop(LoopProtocol):
if self.status != "pending":
raise RuntimeError("Cannot tick when status is no longer 'pending'")
if self.input not in (INPUT_DONE, INPUT_RESUMING, INPUT_SHOULD_VALIDATE):
if self.input not in (INPUT_DONE, INPUT_RESUMING):
self._first(input_keys=input_keys)
elif self.to_interrupt:
# if we need to interrupt, do so
@@ -430,13 +425,6 @@ class PregelLoop(LoopProtocol):
# apply writes to managed values
for key, values in mv_writes.items():
self._update_mv(key, values)
# validate input if requested
if self.input is INPUT_SHOULD_VALIDATE:
self.input = INPUT_DONE
# validate
cast(Type[BaseModel], self.input_model)(
**read_channels(self.channels, self.stream_keys)
)
# produce values output
self._emit(
"values", map_output_values, self.output_keys, writes, self.channels
@@ -634,8 +622,6 @@ class PregelLoop(LoopProtocol):
self._emit(
"values", map_output_values, self.output_keys, True, self.channels
)
# set flag
self.input = INPUT_RESUMING
# map inputs to channel updates
elif input_writes := deque(map_input(input_keys, self.input)):
# TODO shouldn't these writes be passed to put_writes too?
@@ -676,19 +662,10 @@ class PregelLoop(LoopProtocol):
assert not mv_writes, "Can't write to SharedValues in graph input"
# save input checkpoint
self._put_checkpoint({"source": "input", "writes": dict(input_writes)})
# set flag
if (
self.input_model is not None
and not isinstance(self.input, self.input_model)
and not isinstance(self.stream_keys, str)
):
self.input = INPUT_SHOULD_VALIDATE
else:
self.input = INPUT_DONE
elif CONFIG_KEY_RESUMING not in configurable:
raise EmptyInputError(f"Received no input for {input_keys}")
else:
self.input = INPUT_DONE
# done with input
self.input = INPUT_RESUMING if is_resuming else INPUT_DONE
# update config
if not self.is_nested:
self.config = patch_configurable(
@@ -863,12 +840,10 @@ class SyncPregelLoop(PregelLoop, ContextManager):
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
input_model: Optional[Type[BaseModel]] = None,
debug: bool = False,
) -> None:
super().__init__(
input,
input_model=input_model,
stream=stream,
config=config,
checkpointer=checkpointer,
@@ -1004,12 +979,10 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
input_model: Optional[Type[BaseModel]] = None,
debug: bool = False,
) -> None:
super().__init__(
input,
input_model=input_model,
stream=stream,
config=config,
checkpointer=checkpointer,
+9 -9
View File
@@ -1324,19 +1324,19 @@ files = [
[[package]]
name = "langchain-core"
version = "0.3.44"
version = "0.3.30"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main", "dev"]
files = [
{file = "langchain_core-0.3.44-py3-none-any.whl", hash = "sha256:d989ce8bd62f1d07765acd575e6ec1254aec0cf7775aaea39fe4af8102377459"},
{file = "langchain_core-0.3.44.tar.gz", hash = "sha256:7c0a01e78360f007cbca448178fe7e032404068e6431dbe8ce905f84febbdfa5"},
{file = "langchain_core-0.3.30-py3-none-any.whl", hash = "sha256:0a4c4e02fac5968b67fbb0142c00c2b976c97e45fce62c7ac9eb1636a6926493"},
{file = "langchain_core-0.3.30.tar.gz", hash = "sha256:0f1281b4416977df43baf366633ad18e96c5dcaaeae6fcb8a799f9889c853243"},
]
[package.dependencies]
jsonpatch = ">=1.33,<2.0"
langsmith = ">=0.1.125,<0.4"
langsmith = ">=0.1.125,<0.3"
packaging = ">=23.2,<25"
pydantic = [
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
@@ -1348,7 +1348,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.18"
version = "2.0.16"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1366,7 +1366,7 @@ url = "../checkpoint"
[[package]]
name = "langgraph-checkpoint-postgres"
version = "2.0.16"
version = "2.0.15"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1386,7 +1386,7 @@ url = "../checkpoint-postgres"
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "2.0.6"
version = "2.0.5"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0"
@@ -1404,7 +1404,7 @@ url = "../checkpoint-sqlite"
[[package]]
name = "langgraph-prebuilt"
version = "0.1.2"
version = "0.1.1"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1422,7 +1422,7 @@ url = "../prebuilt"
[[package]]
name = "langgraph-sdk"
version = "0.1.55"
version = "0.1.53"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.3.7"
version = "0.3.5"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -3116,6 +3116,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3124,8 +3126,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3141,6 +3141,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3149,8 +3151,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3166,6 +3166,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3174,8 +3176,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3191,6 +3191,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3199,8 +3201,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3216,6 +3216,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3224,8 +3226,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3241,6 +3241,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3249,8 +3251,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -6,6 +6,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -14,8 +16,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -31,6 +31,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -39,8 +41,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -56,6 +56,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -64,8 +66,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -81,6 +81,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -89,8 +91,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -106,6 +106,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -114,8 +116,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -131,6 +131,8 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -139,8 +141,6 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -1722,13 +1722,13 @@
__start__([<p>__start__</p>]):::first
uno(uno)
dos(dos)
subgraph_one(one)
subgraph_two(two)
subgraph_three(three)
__start__ --> uno;
uno -.-> dos;
uno -.-> subgraph_one;
subgraph subgraph
subgraph_one(one)
subgraph_two(two)
subgraph_three(three)
subgraph_one -.-> subgraph_two;
subgraph_one -.-> subgraph_three;
end
@@ -1752,14 +1752,12 @@
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
inner(inner)
side(side)
__end__([<p>__end__</p>]):::last
__start__ --> inner_up;
inner_up --> side;
__start__ --> inner;
inner --> side;
side --> __end__;
subgraph inner
inner_up(up)
end
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
@@ -1897,6 +1895,10 @@
graph TD;
__start__([<p>__start__</p>]):::first
tool_one(tool_one)
tool_two___start__(<p>__start__</p>)
tool_two_tool_two_slow(tool_two_slow)
tool_two_tool_two_fast(tool_two_fast)
tool_two___end__(<p>__end__</p>)
tool_three(tool_three)
__end__([<p>__end__</p>]):::last
__start__ -.-> tool_one;
@@ -1906,10 +1908,6 @@
__start__ -.-> tool_three;
tool_three --> __end__;
subgraph tool_two
tool_two___start__(<p>__start__</p>)
tool_two_tool_two_slow(tool_two_slow)
tool_two_tool_two_fast(tool_two_fast)
tool_two___end__(<p>__end__</p>)
tool_two___start__ -.-> tool_two_tool_two_slow;
tool_two_tool_two_slow --> tool_two___end__;
tool_two___start__ -.-> tool_two_tool_two_fast;
@@ -1964,24 +1962,24 @@
graph TD;
__start__([<p>__start__</p>]):::first
gp_one(gp_one)
gp_two___start__(<p>__start__</p>)
gp_two_p_one(p_one)
gp_two_p_two___start__(<p>__start__</p>)
gp_two_p_two_c_one(c_one)
gp_two_p_two_c_two(c_two)
gp_two_p_two___end__(<p>__end__</p>)
gp_two___end__(<p>__end__</p>)
__end__([<p>__end__</p>]):::last
__start__ --> gp_one;
gp_two___end__ --> gp_one;
gp_one -. &nbsp;0&nbsp; .-> gp_two___start__;
gp_one -. &nbsp;1&nbsp; .-> __end__;
subgraph gp_two
gp_two___start__(<p>__start__</p>)
gp_two_p_one(p_one)
gp_two___end__(<p>__end__</p>)
gp_two___start__ --> gp_two_p_one;
gp_two_p_two___end__ --> gp_two_p_one;
gp_two_p_one -. &nbsp;0&nbsp; .-> gp_two_p_two___start__;
gp_two_p_one -. &nbsp;1&nbsp; .-> gp_two___end__;
subgraph p_two
gp_two_p_two___start__(<p>__start__</p>)
gp_two_p_two_c_one(c_one)
gp_two_p_two_c_two(c_two)
gp_two_p_two___end__(<p>__end__</p>)
gp_two_p_two___start__ --> gp_two_p_two_c_one;
gp_two_p_two_c_two --> gp_two_p_two_c_one;
gp_two_p_two_c_one -. &nbsp;0&nbsp; .-> gp_two_p_two_c_two;
@@ -2000,16 +1998,16 @@
graph TD;
__start__([<p>__start__</p>]):::first
p_one(p_one)
p_two___start__(<p>__start__</p>)
p_two_c_one(c_one)
p_two_c_two(c_two)
p_two___end__(<p>__end__</p>)
__end__([<p>__end__</p>]):::last
__start__ --> p_one;
p_two___end__ --> p_one;
p_one -. &nbsp;0&nbsp; .-> p_two___start__;
p_one -. &nbsp;1&nbsp; .-> __end__;
subgraph p_two
p_two___start__(<p>__start__</p>)
p_two_c_one(c_one)
p_two_c_two(c_two)
p_two___end__(<p>__end__</p>)
p_two___start__ --> p_two_c_one;
p_two_c_two --> p_two_c_one;
p_two_c_one -. &nbsp;0&nbsp; .-> p_two_c_two;
+1 -1
View File
@@ -2827,7 +2827,7 @@ def test_state_graph_packets(
}
# Define decision-making logic
def should_continue(data: dict) -> str:
def should_continue(data: AgentState) -> str:
assert isinstance(data["session"], httpx.Client)
assert (
data["something_extra"] == "hi there"
-53
View File
@@ -7697,56 +7697,3 @@ async def test_interrupt_subgraph_reenter_checkpointer_true(
}
# confirm that we preserve the state values from the previous invocation
assert bar_values == [None, "barbaz", "quxbaz"]
@NEEDS_CONTEXTVARS
async def test_handles_multiple_interrupts_from_tasks() -> None:
@task
async def add_participant(name: str) -> str:
feedback = interrupt(f"Hey do you want to add {name}?")
if feedback is False:
return f"The user changed their mind and doesn't want to add {name}!"
if feedback is True:
return f"Added {name}!"
raise ValueError("Invalid feedback")
@entrypoint(checkpointer=MemorySaver())
async def program(_state: Any) -> list[str]:
first = await add_participant("James")
second = await add_participant("Will")
return [first, second]
config = {"configurable": {"thread_id": "1"}}
result = await program.ainvoke("this is ignored", config=config)
assert result is None
state = await program.aget_state(config=config)
assert len(state.tasks[0].interrupts) == 1
task_interrupt = state.tasks[0].interrupts[0]
assert task_interrupt.resumable is True
assert len(task_interrupt.ns) == 2
assert task_interrupt.ns[0].startswith("program:")
assert task_interrupt.ns[1].startswith("add_participant:")
assert task_interrupt.value == "Hey do you want to add James?"
result = await program.ainvoke(Command(resume=True), config=config)
assert result is None
state = await program.aget_state(config=config)
assert len(state.tasks[0].interrupts) == 1
task_interrupt = state.tasks[0].interrupts[0]
assert task_interrupt.resumable is True
assert len(task_interrupt.ns) == 2
assert task_interrupt.ns[0].startswith("program:")
assert task_interrupt.ns[1].startswith("add_participant:")
assert task_interrupt.value == "Hey do you want to add Will?"
result = await program.ainvoke(Command(resume=True), config=config)
assert result is not None
assert len(result) == 2
assert result[0] == "Added James!"
assert result[1] == "Added Will!"
-72
View File
@@ -1,5 +1,4 @@
import inspect
import operator
import warnings
from dataclasses import dataclass, field
from typing import Annotated as Annotated2
@@ -329,74 +328,3 @@ def test__get_node_name() -> None:
# class method
assert _get_node_name(MyClass().class_method) == "class_method"
def test_input_schema_conditional_edge():
class OverallState(TypedDict):
foo: Annotated[int, operator.add]
bar: str
class PrivateState(TypedDict):
baz: str
builder = StateGraph(OverallState)
def node_1(state: OverallState):
return {"foo": 1, "baz": "bar"}
def node_2(state: PrivateState):
return {"foo": 1, "bar": state["baz"], "something_else": "meow"}
def node_3(state: OverallState):
return {"foo": 1}
def router(state: OverallState):
assert state == {"foo": 2, "bar": "bar"}
if state["foo"] == 2:
return "node_3"
else:
return "__end__"
builder.add_node(node_1)
builder.add_node(node_2)
builder.add_node(node_3)
builder.add_conditional_edges("node_2", router)
builder.add_edge("__start__", "node_1")
builder.add_edge("node_1", "node_2")
graph = builder.compile()
assert graph.invoke({"foo": 0}) == {"foo": 3, "bar": "bar"}
def test_private_input_schema_conditional_edge():
class OverallState(TypedDict):
foo: Annotated[int, operator.add]
bar: str
class RouterState(TypedDict):
baz: str
class Node2State(TypedDict):
foo: Annotated[int, operator.add]
baz: str
builder = StateGraph(OverallState)
def node_1(state: OverallState):
return {"foo": 1, "baz": "meow"}
def node_2(state: Node2State):
return {"foo": 1, "bar": state["baz"]}
def router(state: RouterState):
assert state == {"baz": "meow"}
if state["baz"] == "meow":
return "node_2"
else:
return "__end__"
builder.add_node(node_1)
builder.add_node(node_2)
builder.add_conditional_edges("node_1", router)
builder.add_edge("__start__", "node_1")
graph = builder.compile()
assert graph.invoke({"foo": 0}) == {"foo": 2, "bar": "meow"}
@@ -10,7 +10,6 @@ from typing import (
TypeVar,
Union,
cast,
get_type_hints,
)
from langchain_core.language_models import (
@@ -58,27 +57,13 @@ class AgentState(TypedDict):
remaining_steps: RemainingSteps
class AgentStatePydantic(BaseModel):
"""The state of the agent."""
messages: Annotated[Sequence[BaseMessage], add_messages]
remaining_steps: RemainingSteps = 25
class AgentStateWithStructuredResponse(AgentState):
"""The state of the agent with a structured response."""
structured_response: StructuredResponse
class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):
"""The state of the agent with a structured response."""
structured_response: StructuredResponse
StateSchema = TypeVar("StateSchema", bound=Union[AgentState, AgentStatePydantic])
StateSchema = TypeVar("StateSchema", bound=AgentState)
StateSchemaType = Type[StateSchema]
PROMPT_RUNNABLE_NAME = "Prompt"
@@ -91,29 +76,21 @@ Prompt = Union[
]
def _get_state_value(state: StateSchema, key: str, default: Any = None) -> Any:
return (
state.get(key, default)
if isinstance(state, dict)
else getattr(state, key, default)
)
def _get_prompt_runnable(prompt: Optional[Prompt]) -> Runnable:
prompt_runnable: Runnable
if prompt is None:
prompt_runnable = RunnableCallable(
lambda state: _get_state_value(state, "messages"), name=PROMPT_RUNNABLE_NAME
lambda state: state["messages"], name=PROMPT_RUNNABLE_NAME
)
elif isinstance(prompt, str):
_system_message: BaseMessage = SystemMessage(content=prompt)
prompt_runnable = RunnableCallable(
lambda state: [_system_message] + _get_state_value(state, "messages"),
lambda state: [_system_message] + state["messages"],
name=PROMPT_RUNNABLE_NAME,
)
elif isinstance(prompt, SystemMessage):
prompt_runnable = RunnableCallable(
lambda state: [prompt] + _get_state_value(state, "messages"),
lambda state: [prompt] + state["messages"],
name=PROMPT_RUNNABLE_NAME,
)
elif inspect.iscoroutinefunction(prompt):
@@ -306,7 +283,7 @@ def create_react_agent(
The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.
This is not the only strategy to get structured responses, see more options in [this guide](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/).
state_schema: An optional state schema that defines graph state.
Must have `messages` and `remaining_steps` keys.
Must have `messages` and `is_last_step` keys.
Defaults to `AgentState` that defines those two keys.
config_schema: An optional schema for configuration.
Use this to expose configurable parameters via agent.config_specs.
@@ -618,8 +595,7 @@ def create_react_agent(
if response_format is not None:
required_keys.add("structured_response")
schema_keys = set(get_type_hints(state_schema))
if missing_keys := required_keys - set(schema_keys):
if missing_keys := required_keys - set(state_schema.__annotations__):
raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
if state_schema is None:
@@ -660,34 +636,35 @@ def create_react_agent(
# our graph needs to check if these were called
should_return_direct = {t.name for t in tool_classes if t.return_direct}
def _are_more_steps_needed(state: StateSchema, response: BaseMessage) -> bool:
# Define the function that calls the model
def call_model(state: AgentState, config: RunnableConfig) -> AgentState:
_validate_chat_history(state["messages"])
response = cast(AIMessage, model_runnable.invoke(state, config))
# add agent name to the AIMessage
response.name = name
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
all_tools_return_direct = (
all(call["name"] in should_return_direct for call in response.tool_calls)
if isinstance(response, AIMessage)
else False
)
remaining_steps = _get_state_value(state, "remaining_steps", None)
is_last_step = _get_state_value(state, "is_last_step", False)
return (
(remaining_steps is None and is_last_step and has_tool_calls)
if (
(
"remaining_steps" not in state
and state.get("is_last_step", False)
and has_tool_calls
)
or (
remaining_steps is not None
and remaining_steps < 1
"remaining_steps" in state
and state["remaining_steps"] < 1
and all_tools_return_direct
)
or (remaining_steps is not None and remaining_steps < 2 and has_tool_calls)
)
# Define the function that calls the model
def call_model(state: StateSchema, config: RunnableConfig) -> StateSchema:
messages = _get_state_value(state, "messages")
_validate_chat_history(messages)
response = cast(AIMessage, model_runnable.invoke(state, config))
# add agent name to the AIMessage
response.name = name
if _are_more_steps_needed(state, response):
or (
"remaining_steps" in state
and state["remaining_steps"] < 2
and has_tool_calls
)
):
return {
"messages": [
AIMessage(
@@ -699,13 +676,34 @@ def create_react_agent(
# We return a list, because this will get added to the existing list
return {"messages": [response]}
async def acall_model(state: StateSchema, config: RunnableConfig) -> StateSchema:
messages = _get_state_value(state, "messages")
_validate_chat_history(messages)
async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
_validate_chat_history(state["messages"])
response = cast(AIMessage, await model_runnable.ainvoke(state, config))
# add agent name to the AIMessage
response.name = name
if _are_more_steps_needed(state, response):
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
all_tools_return_direct = (
all(call["name"] in should_return_direct for call in response.tool_calls)
if isinstance(response, AIMessage)
else False
)
if (
(
"remaining_steps" not in state
and state.get("is_last_step", False)
and has_tool_calls
)
or (
"remaining_steps" in state
and state["remaining_steps"] < 1
and all_tools_return_direct
)
or (
"remaining_steps" in state
and state["remaining_steps"] < 2
and has_tool_calls
)
):
return {
"messages": [
AIMessage(
@@ -718,11 +716,11 @@ def create_react_agent(
return {"messages": [response]}
def generate_structured_response(
state: StateSchema, config: RunnableConfig
) -> StateSchema:
state: AgentState, config: RunnableConfig
) -> AgentState:
# NOTE: we exclude the last message because there is enough information
# for the LLM to generate the structured response
messages = _get_state_value(state, "messages")[:-1]
messages = state["messages"][:-1]
structured_response_schema = response_format
if isinstance(response_format, tuple):
system_prompt, structured_response_schema = response_format
@@ -735,11 +733,11 @@ def create_react_agent(
return {"structured_response": response}
async def agenerate_structured_response(
state: StateSchema, config: RunnableConfig
) -> StateSchema:
state: AgentState, config: RunnableConfig
) -> AgentState:
# NOTE: we exclude the last message because there is enough information
# for the LLM to generate the structured response
messages = _get_state_value(state, "messages")[:-1]
messages = state["messages"][:-1]
structured_response_schema = response_format
if isinstance(response_format, tuple):
system_prompt, structured_response_schema = response_format
@@ -775,8 +773,8 @@ def create_react_agent(
)
# Define the function that determines whether to continue or not
def should_continue(state: StateSchema) -> Union[str, list]:
messages = _get_state_value(state, "messages")
def should_continue(state: AgentState) -> Union[str, list]:
messages = state["messages"]
last_message = messages[-1]
# If there is no function call, then we finish
if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
@@ -826,8 +824,8 @@ def create_react_agent(
path_map=should_continue_destinations,
)
def route_tool_responses(state: StateSchema) -> Literal["agent", "__end__"]:
for m in reversed(_get_state_value(state, "messages")):
def route_tool_responses(state: AgentState) -> Literal["agent", "__end__"]:
for m in reversed(state["messages"]):
if not isinstance(m, ToolMessage):
break
if m.name in should_return_direct:
+24 -69
View File
@@ -5,7 +5,6 @@ from functools import partial
from typing import (
Annotated,
List,
Optional,
Type,
TypeVar,
Union,
@@ -36,8 +35,6 @@ from langgraph.prebuilt import (
)
from langgraph.prebuilt.chat_agent_executor import (
AgentState,
AgentStatePydantic,
StateSchemaType,
_get_model,
_should_bind_tools,
_validate_chat_history,
@@ -531,31 +528,22 @@ def test_react_agent_with_structured_response(version: str) -> None:
assert response["messages"][-2].content == "The weather is sunny and 75°F."
class CustomState(AgentState):
user_name: str
class CustomStatePydantic(AgentStatePydantic):
user_name: Optional[str] = None
@pytest.mark.skipif(
not IS_LANGCHAIN_CORE_030_OR_GREATER,
reason="Langchain core 0.3.0 or greater is required",
)
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
@pytest.mark.parametrize("state_schema", [CustomState, CustomStatePydantic])
def test_react_agent_update_state(
request: pytest.FixtureRequest,
checkpointer_name: str,
version: str,
state_schema: StateSchemaType,
request: pytest.FixtureRequest, checkpointer_name: str, version: str
) -> None:
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
"checkpointer_" + checkpointer_name
)
class State(AgentState):
user_name: str
@dec_tool
def get_user_name(tool_call_id: Annotated[str, InjectedToolCallId]):
"""Retrieve user name"""
@@ -571,31 +559,20 @@ def test_react_agent_update_state(
}
)
if issubclass(state_schema, AgentStatePydantic):
def prompt(state: State):
user_name = state.get("user_name")
if user_name is None:
return state["messages"]
def prompt(state: CustomStatePydantic):
user_name = state.user_name
if user_name is None:
return state.messages
system_msg = f"User name is {user_name}"
return [{"role": "system", "content": system_msg}] + state.messages
else:
def prompt(state: CustomState):
user_name = state.get("user_name")
if user_name is None:
return state["messages"]
system_msg = f"User name is {user_name}"
return [{"role": "system", "content": system_msg}] + state["messages"]
system_msg = f"User name is {user_name}"
return [{"role": "system", "content": system_msg}] + state["messages"]
tool_calls = [[{"args": {}, "id": "1", "name": "get_user_name"}]]
model = FakeToolCallingModel(tool_calls=tool_calls)
agent = create_react_agent(
model,
[get_user_name],
state_schema=state_schema,
state_schema=State,
prompt=prompt,
checkpointer=checkpointer,
version=version,
@@ -825,45 +802,23 @@ def test_tool_node_inject_state(schema_: Type[T]) -> None:
assert tool_message.content == "hi?"
class AgentStateExtraKey(AgentState):
foo: int
class AgentStateExtraKeyPydantic(AgentStatePydantic):
foo: int
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
@pytest.mark.parametrize(
"state_schema", [AgentStateExtraKey, AgentStateExtraKeyPydantic]
)
def test_create_react_agent_inject_vars(
version: str, state_schema: StateSchemaType
) -> None:
def test_create_react_agent_inject_vars(version: str) -> None:
class AgentStateExtraKey(AgentState):
foo: int
store = InMemoryStore()
namespace = ("test",)
store.put(namespace, "test_key", {"bar": 3})
if issubclass(state_schema, AgentStatePydantic):
def tool1(
some_val: int,
state: Annotated[AgentStateExtraKeyPydantic, InjectedState],
store: Annotated[BaseStore, InjectedStore()],
) -> str:
"""Tool 1 docstring."""
store_val = store.get(namespace, "test_key").value["bar"]
return some_val + state.foo + store_val
else:
def tool1(
some_val: int,
state: Annotated[dict, InjectedState],
store: Annotated[BaseStore, InjectedStore()],
) -> str:
"""Tool 1 docstring."""
store_val = store.get(namespace, "test_key").value["bar"]
return some_val + state["foo"] + store_val
def tool1(
some_val: int,
state: Annotated[dict, InjectedState],
store: Annotated[BaseStore, InjectedStore()],
) -> str:
"""Tool 1 docstring."""
store_val = store.get(namespace, "test_key").value["bar"]
return some_val + state["foo"] + store_val
tool_call = {
"name": "tool1",
@@ -875,7 +830,7 @@ def test_create_react_agent_inject_vars(
agent = create_react_agent(
model,
[tool1],
state_schema=state_schema,
state_schema=AgentStateExtraKey,
store=store,
version=version,
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.53",
"version": "0.0.49",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
-41
View File
@@ -1320,40 +1320,6 @@ export class StoreClient extends BaseClient {
}
}
class UiClient extends BaseClient {
private static promiseCache: Record<string, Promise<unknown> | undefined> =
{};
private static getOrCached<T>(key: string, fn: () => Promise<T>): Promise<T> {
if (UiClient.promiseCache[key] != null) {
return UiClient.promiseCache[key] as Promise<T>;
}
const promise = fn();
UiClient.promiseCache[key] = promise;
return promise;
}
async getComponent(assistantId: string, agentName: string): Promise<string> {
return UiClient["getOrCached"](
`${this.apiUrl}-${assistantId}-${agentName}`,
async () => {
const response = await this.asyncCaller.fetch(
...this.prepareFetchOptions(`/ui/${assistantId}`, {
headers: {
Accept: "text/html",
"Content-Type": "application/json",
},
method: "POST",
json: { name: agentName },
}),
);
return response.text();
},
);
}
}
export class Client<
TStateType = DefaultValues,
TUpdateType = TStateType,
@@ -1384,18 +1350,11 @@ export class Client<
*/
public store: StoreClient;
/**
* The client for interacting with the UI.
* @internal Used by LoadExternalComponent and the API might change in the future.
*/
public "~ui": UiClient;
constructor(config?: ClientConfig) {
this.assistants = new AssistantsClient(config);
this.threads = new ThreadsClient(config);
this.runs = new RunsClient(config);
this.crons = new CronsClient(config);
this.store = new StoreClient(config);
this["~ui"] = new UiClient(config);
}
}
+34 -13
View File
@@ -1,5 +1,3 @@
"use client";
import { useStream } from "../react/index.js";
import type { UIMessage } from "./types.js";
@@ -107,11 +105,19 @@ class ComponentStore {
}
const COMPONENT_STORE = new ComponentStore();
const COMPONENT_PROMISE_CACHE: Record<string, Promise<string> | undefined> = {};
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
interface LoadExternalComponentProps
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
/** API URL of the LangGraph Platform */
apiUrl?: string;
/** ID of the assistant */
assistantId: string;
/** Stream of the assistant */
stream: ReturnType<typeof useStream>;
@@ -131,7 +137,29 @@ interface LoadExternalComponentProps
components?: Record<string, React.FunctionComponent | React.ComponentClass>;
}
function fetchComponent(
apiUrl: string,
assistantId: string,
agentName: string,
): Promise<string> {
const cacheKey = `${apiUrl}-${assistantId}-${agentName}`;
if (COMPONENT_PROMISE_CACHE[cacheKey] != null) {
return COMPONENT_PROMISE_CACHE[cacheKey] as Promise<string>;
}
const request: Promise<string> = fetch(`${apiUrl}/ui/${assistantId}`, {
headers: { Accept: "text/html", "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ name: agentName }),
}).then((a) => a.text());
COMPONENT_PROMISE_CACHE[cacheKey] = request;
return request;
}
export function LoadExternalComponent({
apiUrl = "http://localhost:2024",
assistantId,
stream,
message,
meta,
@@ -152,10 +180,9 @@ export function LoadExternalComponent({
const clientComponent = components?.[message.name];
const hasClientComponent = clientComponent != null;
const uiClient = stream.client["~ui"];
React.useEffect(() => {
if (hasClientComponent) return;
uiClient.getComponent(stream.assistantId, message.name).then((html) => {
fetchComponent(apiUrl, assistantId, message.name).then((html) => {
const dom = ref.current;
if (!dom) return;
const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" });
@@ -166,16 +193,10 @@ export function LoadExternalComponent({
);
root.appendChild(fragment);
});
}, [
uiClient,
stream.assistantId,
message.name,
shadowRootId,
hasClientComponent,
]);
}, [apiUrl, assistantId, message.name, shadowRootId, hasClientComponent]);
if (hasClientComponent) {
return React.createElement(clientComponent, message.props);
return React.createElement(clientComponent, message.content);
}
return (
@@ -185,7 +206,7 @@ export function LoadExternalComponent({
<UseStreamContext.Provider value={{ stream, meta }}>
{state?.target != null
? ReactDOM.createPortal(
React.createElement(state.comp, message.props),
React.createElement(state.comp, message.content),
state.target,
)
: fallback}
+1 -5
View File
@@ -2,8 +2,4 @@ import { bootstrapUiContext } from "./client.js";
bootstrapUiContext();
export { useStreamContext, LoadExternalComponent } from "./client.js";
export {
uiMessageReducer,
type UIMessage,
type RemoveUIMessage,
} from "./types.js";
export type { UIMessage, RemoveUIMessage } from "./types.js";
+23 -36
View File
@@ -2,10 +2,6 @@ import { v4 as uuidv4 } from "uuid";
import type { ComponentPropsWithoutRef, ElementType } from "react";
import type { RemoveUIMessage, UIMessage } from "../types.js";
interface MessageLike {
id?: string;
}
export const typedUi = <Decl extends Record<string, ElementType>>(config: {
writer?: (chunk: unknown) => void;
runId?: string;
@@ -14,7 +10,7 @@ export const typedUi = <Decl extends Record<string, ElementType>>(config: {
runName?: string;
}) => {
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
let items: (UIMessage | RemoveUIMessage)[] = [];
let collect: (UIMessage | RemoveUIMessage)[] = [];
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
if (!runId) throw new Error("run_id is required");
@@ -26,37 +22,28 @@ export const typedUi = <Decl extends Record<string, ElementType>>(config: {
run_id: runId,
};
const handlePush = <K extends keyof PropMap & string>(
message: {
id?: string;
name: K;
props: PropMap[K];
metadata?: Record<string, unknown>;
const create = <K extends keyof PropMap & string>(
name: K,
props: PropMap[K],
): UIMessage => ({
type: "ui" as const,
id: uuidv4(),
name,
content: props,
additional_kwargs: metadata,
});
const remove = (id: string): RemoveUIMessage => ({ type: "remove-ui", id });
return {
create,
remove,
collect,
write: <K extends keyof PropMap & string>(name: K, props: PropMap[K]) => {
const evt: UIMessage = create(name, props);
collect.push(evt);
config.writer?.(evt);
},
options?: { message?: MessageLike },
): UIMessage => {
const evt: UIMessage = {
type: "ui" as const,
id: message?.id ?? uuidv4(),
name: message?.name,
props: message?.props,
metadata: {
...metadata,
...message?.metadata,
...(options?.message ? { message_id: options.message.id } : null),
},
};
items.push(evt);
config.writer?.(evt);
return evt;
};
const handleDelete = (id: string): RemoveUIMessage => {
const evt: RemoveUIMessage = { type: "remove-ui", id };
items.push(evt);
config.writer?.(evt);
return evt;
};
return { push: handlePush, delete: handleDelete, items };
};
+2 -3
View File
@@ -3,10 +3,9 @@ export interface UIMessage {
id: string;
name: string;
props: Record<string, unknown>;
metadata: {
content: Record<string, unknown>;
additional_kwargs: {
run_id: string;
message_id?: string;
[key: string]: unknown;
};
}
+1 -31
View File
@@ -464,11 +464,6 @@ interface UseStreamOptions<
*/
onCustomEvent?: (
data: CustomStreamEvent<GetCustomEventType<Bag>>["data"],
options: {
mutate: (
update: Partial<StateType> | ((prev: StateType) => Partial<StateType>),
) => void;
},
) => void;
/**
@@ -563,16 +558,6 @@ export interface UseStream<
message: Message,
index?: number,
) => MessageMetadata<StateType> | undefined;
/**
* LangGraph SDK client used to send request and receive responses.
*/
client: Client;
/**
* The ID of the assistant to use.
*/
assistantId: string;
}
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
@@ -642,7 +627,6 @@ export function useStream<
options.defaultHeaders,
],
);
const [threadId, onThreadId] = useControllableThreadId(options);
const [branch, setBranch] = useState<string>("");
@@ -850,18 +834,7 @@ export function useStream<
}
if (event === "updates") options.onUpdateEvent?.(data);
if (event === "custom")
options.onCustomEvent?.(data, {
mutate: (update) =>
setStreamValues((prev) => {
// should not happen
if (prev == null) return prev;
return {
...prev,
...(typeof update === "function" ? update(prev) : update),
};
}),
});
if (event === "custom") options.onCustomEvent?.(data);
if (event === "metadata") options.onMetadataEvent?.(data);
if (event === "values") setStreamValues(data);
@@ -930,9 +903,6 @@ export function useStream<
return values;
},
client,
assistantId,
error,
isLoading,
+1 -1
View File
@@ -26,7 +26,7 @@ export type AIMessage = {
tool_calls?:
| {
name: string;
args: { [x: string]: any };
args: { [x: string]: { [x: string]: any } };
id?: string | undefined;
type?: "tool_call" | undefined;
}[]
+3 -3
View File
@@ -1177,9 +1177,9 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.6.7:
version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
+2 -2
View File
@@ -2163,7 +2163,7 @@ class StoreClient:
"index": index,
"ttl": ttl,
}
await self.http.put("/store/items", json=_provided_vals(payload))
await self.http.put("/store/items", json=payload)
async def get_item(
self,
@@ -4307,7 +4307,7 @@ class SyncStoreClient:
"index": index,
"ttl": ttl,
}
self.http.put("/store/items", json=_provided_vals(payload))
self.http.put("/store/items", json=payload)
def get_item(
self,
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.56"
version = "0.1.55"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"