mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-29 03:09:45 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656737009b | ||
|
|
ecdf70a2ab |
@@ -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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
# 🦜🕸️LangGraph
|
||||
|
||||
[](https://pypi.org/project/langgraph/)
|
||||

|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](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",
|
||||
|
||||
@@ -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 you’ll 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
|
||||
|
||||
Generated
+480
-629
File diff suppressed because it is too large
Load Diff
@@ -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,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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Generated
+44
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Test package for langgraph-cli-install."""
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
# 🦜🕸️LangGraph
|
||||
|
||||
[](https://pypi.org/project/langgraph/)
|
||||

|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+9
-9
@@ -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,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 -. 0 .-> gp_two___start__;
|
||||
gp_one -. 1 .-> __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 -. 0 .-> gp_two_p_two___start__;
|
||||
gp_two_p_one -. 1 .-> 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 -. 0 .-> 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 -. 0 .-> p_two___start__;
|
||||
p_one -. 1 .-> __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 -. 0 .-> p_two_c_two;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
}[]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
Reference in New Issue
Block a user