mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 22:25:44 +02:00
Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b5f9510b | ||
|
|
c6d7c80a99 | ||
|
|
909190cede | ||
|
|
43c8578eef | ||
|
|
56f5edb9ba | ||
|
|
41f0fd504e | ||
|
|
6357d496af | ||
|
|
52bd5b13a7 | ||
|
|
b633e0a4ed | ||
|
|
c14bcb6e9f | ||
|
|
8b29dc81e0 | ||
|
|
1546eddfbe | ||
|
|
9680e35beb | ||
|
|
837f215857 | ||
|
|
e13261ac0a | ||
|
|
8ab206043c | ||
|
|
3dbe37041a | ||
|
|
e00284b386 | ||
|
|
a36d2ac77d | ||
|
|
2a46534286 | ||
|
|
4b06791b8c | ||
|
|
661e20eec4 | ||
|
|
a486eb5e75 | ||
|
|
e9d62944d3 | ||
|
|
cbd09abe58 | ||
|
|
4798443e31 | ||
|
|
ce900864fa | ||
|
|
577f95bd50 | ||
|
|
59a11c63b0 | ||
|
|
08098688d4 | ||
|
|
687ee02509 | ||
|
|
451bc038b6 | ||
|
|
c865e8c070 | ||
|
|
d74ec2c2de | ||
|
|
f70bfc6d87 | ||
|
|
c86f0af107 | ||
|
|
c6ee807de5 | ||
|
|
7256752f48 | ||
|
|
dac84951aa | ||
|
|
3aaa3e38a0 | ||
|
|
400d83708a | ||
|
|
1d9c7ef461 | ||
|
|
01e5ecedfd | ||
|
|
76199701b0 | ||
|
|
2766fccb5b | ||
|
|
6aef3e0117 | ||
|
|
c5023ba147 | ||
|
|
9a9fe2fdec | ||
|
|
effddca494 | ||
|
|
2ab59840e7 | ||
|
|
0fb65f6e67 | ||
|
|
0ecd23eec6 | ||
|
|
e137dabf22 | ||
|
|
fdc1e47aa1 | ||
|
|
866780b477 | ||
|
|
18d3fa2e15 | ||
|
|
4b0c53fb5c | ||
|
|
5183484322 | ||
|
|
8213e4719b | ||
|
|
f993dfcfcb | ||
|
|
9e31b82d8d | ||
|
|
1e0aebc3ec | ||
|
|
056f581342 | ||
|
|
a0d7323bec |
@@ -8,7 +8,7 @@
|
||||
⚡ Building language agents as graphs ⚡
|
||||
|
||||
> [!NOTE]
|
||||
> Looking for the JS version? Click [here](https://github.com/langchain-ai/langgraphjs) ([JS docs](https://langchain-ai.github.io/langgraphjs/)).
|
||||
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import functools
|
||||
|
||||
from urllib3 import __version__ as urllib3version # type: ignore[import-untyped]
|
||||
from urllib3 import connection # type: ignore[import-untyped]
|
||||
|
||||
|
||||
def _ensure_str(s, encoding="utf-8", errors="strict") -> str:
|
||||
if isinstance(s, str):
|
||||
return s
|
||||
|
||||
if isinstance(s, bytes):
|
||||
return s.decode(encoding, errors)
|
||||
return str(s)
|
||||
|
||||
|
||||
# Copied from https://github.com/urllib3/urllib3/blob/1c994dfc8c5d5ecaee8ed3eb585d4785f5febf6e/src/urllib3/connection.py#L231
|
||||
def request(self, method, url, body=None, headers=None):
|
||||
"""Make the request.
|
||||
|
||||
This function is based on the urllib3 request method, with modifications
|
||||
to handle potential issues when using vcrpy in concurrent workloads.
|
||||
|
||||
Args:
|
||||
self: The HTTPConnection instance.
|
||||
method (str): The HTTP method (e.g., 'GET', 'POST').
|
||||
url (str): The URL for the request.
|
||||
body (Optional[Any]): The body of the request.
|
||||
headers (Optional[dict]): Headers to send with the request.
|
||||
|
||||
Returns:
|
||||
The result of calling the parent request method.
|
||||
"""
|
||||
# Update the inner socket's timeout value to send the request.
|
||||
# This only triggers if the connection is re-used.
|
||||
if getattr(self, "sock", None) is not None:
|
||||
self.sock.settimeout(self.timeout)
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
else:
|
||||
# Avoid modifying the headers passed into .request()
|
||||
headers = headers.copy()
|
||||
if "user-agent" not in (_ensure_str(k.lower()) for k in headers):
|
||||
headers["User-Agent"] = connection._get_default_user_agent()
|
||||
# The above is all the same ^^^
|
||||
# The following is different:
|
||||
return self._parent_request(method, url, body=body, headers=headers)
|
||||
|
||||
|
||||
_PATCHED = False
|
||||
|
||||
|
||||
def patch_urllib3():
|
||||
"""Patch the request method of urllib3 to avoid type errors when using vcrpy.
|
||||
|
||||
In concurrent workloads (such as the tracing background queue), the
|
||||
connection pool can get in a state where an HTTPConnection is created
|
||||
before vcrpy patches the HTTPConnection class. In urllib3 >= 2.0 this isn't
|
||||
a problem since they use the proper super().request(...) syntax, but in older
|
||||
versions, super(HTTPConnection, self).request is used, resulting in a TypeError
|
||||
since self is no longer a subclass of "HTTPConnection" (which at this point
|
||||
is vcr.stubs.VCRConnection).
|
||||
|
||||
This method patches the class to fix the super() syntax to avoid mixed inheritance.
|
||||
In the case of the LangSmith tracing logic, it doesn't really matter since we always
|
||||
exclude cache checks for calls to LangSmith.
|
||||
|
||||
The patch is only applied for urllib3 versions older than 2.0.
|
||||
"""
|
||||
global _PATCHED
|
||||
if _PATCHED:
|
||||
return
|
||||
from packaging import version
|
||||
|
||||
if version.parse(urllib3version) >= version.parse("2.0"):
|
||||
_PATCHED = True
|
||||
return
|
||||
|
||||
# Lookup the parent class and its request method
|
||||
parent_class = connection.HTTPConnection.__bases__[0]
|
||||
parent_request = parent_class.request
|
||||
|
||||
def new_request(self, *args, **kwargs):
|
||||
"""Handle parent request.
|
||||
|
||||
This method binds the parent's request method to self and then
|
||||
calls our modified request function.
|
||||
"""
|
||||
self._parent_request = functools.partial(parent_request, self)
|
||||
return request(self, *args, **kwargs)
|
||||
|
||||
connection.HTTPConnection.request = new_request
|
||||
_PATCHED = True
|
||||
@@ -6,6 +6,9 @@ import re
|
||||
from typing import List, Literal, Optional
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import nbformat
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
@@ -47,6 +50,8 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
|
||||
(["langgraph.constants"], "langgraph.types", "Send", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Command", "types"),
|
||||
([], "langgraph.types", "RetryPolicy", "types"),
|
||||
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
|
||||
@@ -83,8 +88,11 @@ _IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
|
||||
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
|
||||
|
||||
|
||||
def _get_full_module_name(module_path, class_name) -> Optional[str]:
|
||||
"""Get full module name using inspect"""
|
||||
|
||||
|
||||
@lru_cache(maxsize=10_000)
|
||||
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
"""Get full module name using inspect, with LRU cache to memoize results."""
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
class_ = getattr(module, class_name)
|
||||
@@ -95,13 +103,12 @@ def _get_full_module_name(module_path, class_name) -> Optional[str]:
|
||||
return module_path
|
||||
return module.__name__
|
||||
except AttributeError as e:
|
||||
logger.warning(f"Could not find module for {class_name}, {e}")
|
||||
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
|
||||
return None
|
||||
except ImportError as e:
|
||||
logger.warning(f"Failed to load for class {class_name}, {e}")
|
||||
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _get_doc_title(data: str, file_name: str) -> str:
|
||||
try:
|
||||
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
|
||||
@@ -115,10 +122,10 @@ def _get_doc_title(data: str, file_name: str) -> str:
|
||||
|
||||
|
||||
class ImportInformation(TypedDict):
|
||||
imported: str # imported class name
|
||||
source: str # module path
|
||||
docs: str # URL to the documentation
|
||||
title: str # Title of the document
|
||||
imported: str # The name of the class that was imported.
|
||||
source: str # The full module path from which the class was imported.
|
||||
docs: str # The URL pointing to the class's documentation.
|
||||
title: str # The title of the document where the import is used.
|
||||
|
||||
|
||||
def _get_imports(
|
||||
@@ -211,36 +218,73 @@ def _get_imports(
|
||||
return imports
|
||||
|
||||
|
||||
class ImportPreprocessor(Preprocessor):
|
||||
"""A preprocessor to replace imports in each Python code cell with links to their
|
||||
documentation and append the import info in a comment."""
|
||||
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
|
||||
"""Retrieve all import references from the given code for specified ecosystems.
|
||||
|
||||
def preprocess(self, nb, resources):
|
||||
self.all_imports = []
|
||||
file_name = os.path.basename(resources.get("metadata", {}).get("name", ""))
|
||||
_DOC_TITLE = _get_doc_title(nb.cells[0].source, file_name)
|
||||
Args:
|
||||
code: The source code from which to extract import references.
|
||||
doc_title: The documentation title associated with the code.
|
||||
|
||||
cells = []
|
||||
for cell in nb.cells:
|
||||
if cell.cell_type == "code":
|
||||
cells.append(cell)
|
||||
imports = _get_imports(
|
||||
cell.source, _DOC_TITLE, "langchain"
|
||||
) + _get_imports(cell.source, _DOC_TITLE, "langgraph")
|
||||
if not imports:
|
||||
continue
|
||||
Returns:
|
||||
A list of import information for each import found.
|
||||
"""
|
||||
ecosystems = ["langchain", "langgraph"]
|
||||
all_imports = []
|
||||
for package_ecosystem in ecosystems:
|
||||
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
|
||||
return all_imports
|
||||
|
||||
cells.append(
|
||||
nbformat.v4.new_markdown_cell(
|
||||
source=f"""
|
||||
<div>
|
||||
<b>API Reference:</b>
|
||||
{' | '.join(f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports)}
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
cells.append(cell)
|
||||
nb.cells = cells
|
||||
return nb, resources
|
||||
|
||||
def update_markdown_with_imports(markdown: str) -> str:
|
||||
"""Update markdown to include API reference links for imports in Python code blocks.
|
||||
|
||||
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
|
||||
|
||||
Args:
|
||||
markdown: The markdown content to process.
|
||||
|
||||
Returns:
|
||||
Updated markdown with API reference links appended to Python code blocks.
|
||||
|
||||
Example:
|
||||
Given a markdown with a Python code block:
|
||||
|
||||
```python
|
||||
from langchain.nlp import TextGenerator
|
||||
```
|
||||
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
|
||||
"""
|
||||
code_block_pattern = re.compile(
|
||||
r'(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```', re.DOTALL
|
||||
)
|
||||
|
||||
def replace_code_block(match: re.Match) -> str:
|
||||
"""Replace the matched code block with additional API reference links if imports are found.
|
||||
|
||||
Args:
|
||||
match (re.Match): The regex match object containing the code block.
|
||||
|
||||
Returns:
|
||||
str: The modified code block with API reference links appended if applicable.
|
||||
"""
|
||||
indent = match.group('indent')
|
||||
code_block = match.group('code')
|
||||
language = match.group('language') # Preserve the language from the regex match
|
||||
# Retrieve import information from the code block
|
||||
imports = get_imports(code_block, "__unused__")
|
||||
|
||||
original_code_block = match.group(0)
|
||||
# If no imports are found, return the original code block
|
||||
if not imports:
|
||||
return original_code_block
|
||||
|
||||
# Generate API reference links for each import
|
||||
api_links = ' | '.join(
|
||||
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
|
||||
)
|
||||
# Return the code block with appended API reference links
|
||||
return f'{original_code_block}\n\n{indent}API Reference: {api_links}'
|
||||
|
||||
# Apply the replace_code_block function to all matches in the markdown
|
||||
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
|
||||
return updated_markdown
|
||||
@@ -6,8 +6,6 @@ import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
from generate_api_reference_links import ImportPreprocessor
|
||||
|
||||
|
||||
class EscapePreprocessor(Preprocessor):
|
||||
def preprocess_cell(self, cell, resources, cell_index):
|
||||
@@ -107,7 +105,6 @@ exporter = MarkdownExporter(
|
||||
preprocessors=[
|
||||
EscapePreprocessor,
|
||||
ExtractAttachmentsPreprocessor,
|
||||
ImportPreprocessor,
|
||||
],
|
||||
template_name="mdoutput",
|
||||
extra_template_basedirs=[
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
from mkdocs.structure.pages import Page
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from notebook_convert import convert_notebook
|
||||
from generate_api_reference_links import update_markdown_with_imports
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -35,12 +38,83 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
|
||||
return new_files
|
||||
|
||||
|
||||
def _highlight_code_blocks(markdown: str) -> str:
|
||||
"""Find code blocks with highlight comments and add hl_lines attribute.
|
||||
|
||||
Args:
|
||||
markdown: The markdown content to process.
|
||||
|
||||
Returns:
|
||||
updated Markdown code with code blocks containing highlight comments
|
||||
updated to use the hl_lines attribute.
|
||||
"""
|
||||
# Pattern to find code blocks with highlight comments and without
|
||||
# existing hl_lines for Python and JavaScript
|
||||
# Pattern to find code blocks with highlight comments, handling optional indentation
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>py|python|js|javascript)(?!\s+hl_lines=)\n"
|
||||
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
|
||||
r"(?P=indent)```" # Match closing backticks with the same indentation
|
||||
)
|
||||
|
||||
def replace_highlight_comments(match: re.Match) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
code_block = match.group("code")
|
||||
lines = code_block.split("\n")
|
||||
highlighted_lines = []
|
||||
|
||||
# Skip initial empty lines
|
||||
while lines and not lines[0].strip():
|
||||
lines.pop(0)
|
||||
|
||||
lines_to_keep = []
|
||||
|
||||
comment_syntax = (
|
||||
"# highlight-next-line"
|
||||
if language in ["py", "python"]
|
||||
else "// highlight-next-line"
|
||||
)
|
||||
|
||||
for line in lines:
|
||||
if comment_syntax in line:
|
||||
count = len(lines_to_keep) + 1
|
||||
highlighted_lines.append(str(count))
|
||||
else:
|
||||
lines_to_keep.append(line)
|
||||
|
||||
# Reconstruct the new code block
|
||||
new_code_block = "\n".join(lines_to_keep)
|
||||
|
||||
if highlighted_lines:
|
||||
return (
|
||||
f'{indent}```{language} hl_lines="{" ".join(highlighted_lines)}"\n'
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f'{new_code_block}'
|
||||
f'{indent}```'
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"{indent}```{language}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
|
||||
# Replace all code blocks in the markdown
|
||||
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
if DISABLED:
|
||||
return markdown
|
||||
if page.file.src_path.endswith(".ipynb"):
|
||||
logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
body = convert_notebook(page.file.abs_src_path)
|
||||
return body
|
||||
markdown = convert_notebook(page.file.abs_src_path)
|
||||
|
||||
# Append API reference links to code blocks
|
||||
markdown = update_markdown_with_imports(markdown)
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
return markdown
|
||||
|
||||
@@ -43,7 +43,9 @@ NOTEBOOKS_NO_EXECUTION = [
|
||||
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # Cannot create a consistent method resolution error from VCR
|
||||
"docs/docs/how-tos/map-reduce.ipynb" # flakiness from structured output, only when running with VCR
|
||||
"docs/docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/docs/tutorials/tot/tot.ipynb",
|
||||
"docs/docs/how-tos/visualization.ipynb"
|
||||
]
|
||||
|
||||
|
||||
@@ -86,6 +88,7 @@ def add_vcr_to_notebook(
|
||||
) -> nbformat.NotebookNode:
|
||||
"""Inject `with vcr.cassette` into each code cell of the notebook."""
|
||||
|
||||
uses_langsmith = False
|
||||
# Inject VCR context manager into each code cell
|
||||
for idx, cell in enumerate(notebook.cells):
|
||||
if cell.cell_type != "code":
|
||||
@@ -120,6 +123,9 @@ def add_vcr_to_notebook(
|
||||
f" {line}" for line in lines
|
||||
)
|
||||
|
||||
if any("hub.pull" in line or "from langsmith import" in line for line in lines):
|
||||
uses_langsmith = True
|
||||
|
||||
# Add import statement
|
||||
vcr_import_lines = [
|
||||
"import nest_asyncio",
|
||||
@@ -152,6 +158,15 @@ def add_vcr_to_notebook(
|
||||
"custom_vcr.register_serializer('advanced_compressed', AdvancedCompressedSerializer())",
|
||||
"custom_vcr.serializer = 'advanced_compressed'",
|
||||
]
|
||||
if uses_langsmith:
|
||||
vcr_import_lines.extend(
|
||||
# patch urllib3 to handle vcr errors, see more here:
|
||||
# https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/_internal/_patch.py
|
||||
"import sys",
|
||||
f"sys.path.insert(0, '{os.path.join(DOCS_PATH, '_scripts')}')",
|
||||
"import _patch as patch_urllib3",
|
||||
"patch_urllib3.patch_urllib3()",
|
||||
)
|
||||
import_cell = nbformat.v4.new_code_cell(source="\n".join(vcr_import_lines))
|
||||
import_cell.pop("id", None)
|
||||
notebook.cells.insert(0, import_cell)
|
||||
|
||||
@@ -11,7 +11,7 @@ LangGraph Cloud is available within <a href="https://www.langchain.com/langsmith
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Platform`. The `LangGraph Platform` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the top-right corner, select `+ New Deployment` to create a new deployment.
|
||||
1. In the `Create New Deployment` panel, fill out the required fields.
|
||||
1. `Deployment details`
|
||||
@@ -38,7 +38,7 @@ When [creating a new deployment](#create-new-deployment), a new revision is crea
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Platform`. The `LangGraph Platform` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. Select an existing deployment to create a new revision for.
|
||||
1. In the `Deployment` view, in the top-right corner, select `+ New Revision`.
|
||||
1. In the `New Revision` modal, fill out the required fields.
|
||||
@@ -52,15 +52,15 @@ Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmi
|
||||
1. Update the value of existing secrets or environment variables.
|
||||
1. Select `Submit`. After a few seconds, the `New Revision` modal will close and the new revision will be queued for deployment.
|
||||
|
||||
## View Build and Deployment Logs
|
||||
## View Build and Server Logs
|
||||
|
||||
Build and deployment logs are available for each revision.
|
||||
Build and server logs are available for each revision.
|
||||
|
||||
Starting from the `LangGraph Cloud` view...
|
||||
Starting from the `LangGraph Platform` view...
|
||||
|
||||
1. Select the desired revision from the `Revisions` table. A panel slides open from the right-hand side and the `Build` tab is selected by default, which displays build logs for the revision.
|
||||
1. In the panel, select the `Deploy` tab to view deployment logs for the revision.
|
||||
1. Within the `Deploy` tab, adjust the date/time range picker as needed. By default, the date/time range picker is set to the `Last 15 minutes`.
|
||||
1. In the panel, select the `Server` tab to view server logs for the revision. Server logs are only available after a revision has been deployed.
|
||||
1. Within the `Server` tab, adjust the date/time range picker as needed. By default, the date/time range picker is set to the `Last 7 days`.
|
||||
|
||||
## Interrupt Revision
|
||||
|
||||
@@ -69,7 +69,7 @@ Interrupting a revision will stop deployment of the revision.
|
||||
!!! warning "Undefined Behavior"
|
||||
Interrupted revisions have undefined behavior. This is only useful if you need to deploy a new revision and you already have a revision "stuck" in progress. In the future, this feature may be removed.
|
||||
|
||||
Starting from the `LangGraph Cloud` view...
|
||||
Starting from the `LangGraph Platform` view...
|
||||
|
||||
1. Select the menu icon (three dots) on the right-hand side of the row for the desired revision from the `Revisions` table.
|
||||
1. Select `Interrupt` from the menu.
|
||||
@@ -79,13 +79,13 @@ Starting from the `LangGraph Cloud` view...
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Platform`. The `LangGraph Platform` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. Select the menu icon (three dots) on the right-hand side of the row for the desired deployment and select `Delete`.
|
||||
1. A `Confirmation` modal will appear. Select `Delete`.
|
||||
|
||||
## Deployment Settings
|
||||
|
||||
Starting from the `LangGraph Cloud` view...
|
||||
Starting from the `LangGraph Platform` view...
|
||||
|
||||
1. In the top-right corner, select the gear icon (`Deployment Settings`).
|
||||
1. Update the `Git Branch` to the desired branch.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>LangGraph Cloud API Reference</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<script id="api-reference" data-url="./openapi_control_plane.json"></script>
|
||||
<script>
|
||||
var configuration = {}
|
||||
document.getElementById('api-reference').dataset.configuration =
|
||||
JSON.stringify(configuration)
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,695 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "LangGraph Control Plane API (Beta)",
|
||||
"version": "0.0.1",
|
||||
"description": "The LangGraph Control Plane API is used to programmatically create and manage LangGraph Server deployments. For example, the APIs can be orchestrated to create custom CI/CD workflows.\n\n### Beta\nThis API is currently in beta and may change or break without notice. This API documentation may not be up-to-date with actual API functionality.\n### Host\nhttps://api.host.langchain.com/\n\n### Authentication\nTo authenticate with the LangGraph Control Plane API, set the `X-Api-Key` header to a valid LangSmith API key for each request.\n\n### Versioning\nEach endpoint path is prefixed with a version (e.g. `v1`).\n\n### Quick Start\n\n1. Call `GET /{version}/projects` to retrieve the `Project` `id`. The `Project` `id` is needed in subsequent API calls.\n2. Call `POST /{version}/projects/{project_id}/revisions` to create a new `Revision` for the `Project`.\n3. Call `GET /{version}/projects/{project_id}/revisions` to get the latest `Revision` (first element in returned list). Get the `Revision` `id`.\n4. Poll for `Revision` `status` until `status` is `DEPLOYED` by calling `GET /{version}/projects/{project_id}/revisions/{revision_id}`."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.host.langchain.com"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "Projects (v1)",
|
||||
"description": "A project corresponds to a LangGraph Server deployment and the associated LangSmith tracing project.\n\nCreating a project via API is not currently supported/documented."
|
||||
},
|
||||
{
|
||||
"name": "Revisions (v1)",
|
||||
"description": "A revision is a version of a LangGraph Server deployment. Different revisions may contain different code and/or environment variables. A project can have many revisions."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/projects": {
|
||||
"get": {
|
||||
"tags": ["Projects (v1)"],
|
||||
"summary": "List Projects",
|
||||
"description": "List all projects.",
|
||||
"operationId": "list_projects_projects_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Limit",
|
||||
"description": "Maximum number of results to return. Minimum: 1. Maximum: 100.",
|
||||
"default": 20
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Offset",
|
||||
"description": "Pagination offset value. Pass this value in subsequent requests to retrieve the next page of results. Minimum: 0.",
|
||||
"default": 0
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Name Contains",
|
||||
"description": "Filter string to filter projects by `name`."
|
||||
},
|
||||
"name": "name_contains",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/projects/{project_id}": {
|
||||
"get": {
|
||||
"tags": ["Projects (v1)"],
|
||||
"summary": "Get Project",
|
||||
"description": "Get project by ID.",
|
||||
"operationId": "get_project_projects__project_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Project ID"
|
||||
},
|
||||
"name": "project_id",
|
||||
"in": "path"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Projects (v1)"],
|
||||
"summary": "Delete Project",
|
||||
"description": "Delete project by ID.",
|
||||
"operationId": "delete_project_projects__project_id__delete",
|
||||
"parameters": [
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Project ID"
|
||||
},
|
||||
"name": "project_id",
|
||||
"in": "path"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/projects/{project_id}/revisions": {
|
||||
"get": {
|
||||
"tags": ["Revisions (v1)"],
|
||||
"summary": "List Revisions",
|
||||
"description": "List revisions of a project.",
|
||||
"operationId": "list_revisions_projects__project_id__revisions_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Project ID"
|
||||
},
|
||||
"name": "project_id",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Limit",
|
||||
"description": "Maximum number of results to return. Minimum: 1. Maximum: 100.",
|
||||
"default": 20
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Offset",
|
||||
"description": "Pagination offset value. Pass this value in subsequent requests to retrieve the next page of results. Minimum: 0.",
|
||||
"default": 0
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Revision"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["Revisions (v1)"],
|
||||
"summary": "Create Revision",
|
||||
"description": "Create a new revision for a project.",
|
||||
"operationId": "create_revision_projects__project_id__revisions_post",
|
||||
"parameters": [
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Project ID"
|
||||
},
|
||||
"name": "project_id",
|
||||
"in": "path"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateRevisionRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/projects/{project_id}/revisions/{revision_id}": {
|
||||
"get": {
|
||||
"tags": ["Revisions (v1)"],
|
||||
"summary": "Get Revision",
|
||||
"description": "Get revision by ID.",
|
||||
"operationId": "get_revision_projects__project_id__revisions__revision_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Project ID"
|
||||
},
|
||||
"name": "project_id",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Revision ID"
|
||||
},
|
||||
"name": "revision_id",
|
||||
"in": "path"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Revision"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/projects/{project_id}/revisions/{revision_id}/interrupt": {
|
||||
"post": {
|
||||
"tags": ["Revisions (v1)"],
|
||||
"summary": "Interrupt Revision",
|
||||
"description": "Interrupt revision by ID.\n\nIf the deployment of a revision appears \"stuck\", the revision may need to be interrupted. A new revision cannot be created if the latest revision is in a non-terminal `status`. In this scenario, the revision may need to be interrupted.",
|
||||
"operationId": "interrupt_revision_projects__project_id__revisions__revision_id__interrupt_post",
|
||||
"parameters": [
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Project ID"
|
||||
},
|
||||
"name": "project_id",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Revision ID"
|
||||
},
|
||||
"name": "revision_id",
|
||||
"in": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"apiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Api-Key"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"EnvVar": {
|
||||
"type": "object",
|
||||
"description": "An environment variable or secret.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Environment variable or secret name.",
|
||||
"required": true
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Environment variable or secret value.",
|
||||
"required": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"secret"
|
||||
],
|
||||
"description": "Field to designate type of the environment variable (default) or secret.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ContainerSpec": {
|
||||
"type": "object",
|
||||
"description": "Container specification for a revision's deployment.\n\nIf any field is omitted or set to `null`, the internal default value is used depending on the deployment type (`dev` or `prod`).",
|
||||
"properties": {
|
||||
"min_scale": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "Minimum number of replicas in deployment.",
|
||||
"default": "null"
|
||||
},
|
||||
"max_scale": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "Maximum number of replicas in deployment.",
|
||||
"default": "null"
|
||||
},
|
||||
"cpu": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "Number of vCPU cores per replica.",
|
||||
"default": "null"
|
||||
},
|
||||
"memory_mb": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "Amount of memory in MB per replica.",
|
||||
"default": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateRevisionRequest": {
|
||||
"type": "object",
|
||||
"description": "Object for creating a new revision.",
|
||||
"properties": {
|
||||
"image_path": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URI of the Docker image to deploy.\n\nIf this field is omitted or set to `null`, the previous revision's `image_path` value is used. Set this field for BYOC deployments. Omit this field if creating a new revision from a GitHub repository.",
|
||||
"default": "null"
|
||||
},
|
||||
"repo_path": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Path to `langgraph.json` configuration file. For example, `langgraph.json` or `src/langgraph.json`.\n\nIf this field is omitted or set to `null`, the previous revision's `repo_path` value is used. Set this field for deployments from a GitHub repository. Omit this field if creating a new revision from a Docker image.",
|
||||
"default": "null"
|
||||
},
|
||||
"env_vars": {
|
||||
"type": "array",
|
||||
"description": "List of environment variables or secrets.\n\nIf this field is omitted or set to `null`, the previous revision's `env_vars` value is used.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EnvVar"
|
||||
},
|
||||
"default": "null"
|
||||
},
|
||||
"shareable": {
|
||||
"type": ["boolean", "null"],
|
||||
"description": "Boolean flag to configure if a deployment is shareable through LangGraph Studio.\n\nIf this field is omitted or set to `null`, the previous revision's `shareable` value is used. This field does not apply to BYOC deployments.",
|
||||
"default": "null"
|
||||
},
|
||||
"container_spec": {
|
||||
"description": "If this field is omitted or set to `null`, the previous revision's `container_spec` value is used.",
|
||||
"$ref": "#/components/schemas/ContainerSpec",
|
||||
"default": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Project": {
|
||||
"type": "object",
|
||||
"description": "A project corresponds to a LangGraph Server deployment and the associated LangSmith tracing project.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "ID of the project.",
|
||||
"required": true
|
||||
},
|
||||
"tool_name": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"display_name": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"example_input": {
|
||||
"type": ["object", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "ID of the tenant/workspace of the project.",
|
||||
"required": true
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp of when the project was created.",
|
||||
"required": true
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp of when the project was updated.",
|
||||
"required": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the project.\n\nThis is also the name of the LangSmith tracing project for the LangGraph deployment.",
|
||||
"required": true
|
||||
},
|
||||
"lc_hosted": {
|
||||
"type": "boolean",
|
||||
"description": "Boolean flag to indicate if the deployment is hosted in LangChain's cloud or an external cloud (e.g. BYOC).",
|
||||
"required": true
|
||||
},
|
||||
"repo_url": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URL of the GitHub repository.\n\nThis field is not used for deployments from a Docker image."
|
||||
},
|
||||
"repo_branch": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Branch of the GitHub repository.\n\nThis field is not used for deployments from a Docker image."
|
||||
},
|
||||
"tracer_session_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "Do not use."
|
||||
},
|
||||
"api_key_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "Do not use."
|
||||
},
|
||||
"build_on_push": {
|
||||
"type": "boolean",
|
||||
"description": "Boolean flag to indicate if a new revision is automatically created on push to GitHub branch (`repo_branch`).\n\nThis field does not apply for BYOC deployments."
|
||||
},
|
||||
"input_json_schemas": {
|
||||
"type": ["object", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"output_json_schemas": {
|
||||
"type": ["object", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"host_integration_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "Do not use."
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/components/schemas/ProjectMetadata"
|
||||
},
|
||||
"resource": {
|
||||
"$ref": "#/components/schemas/ResourceService"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProjectMetadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata associated with a `Project`.",
|
||||
"properties": {
|
||||
"deployment_type": {
|
||||
"type": "string",
|
||||
"description": "Development (`dev`) or Production (`prod`) type deployment.",
|
||||
"enum": [
|
||||
"dev",
|
||||
"prod"
|
||||
]
|
||||
},
|
||||
"image_source": {
|
||||
"type": "string",
|
||||
"description": "Do not use.",
|
||||
"enum": [
|
||||
"github",
|
||||
"internal_docker",
|
||||
"external_docker"
|
||||
]
|
||||
},
|
||||
"shareable": {
|
||||
"type": "boolean",
|
||||
"description": "Boolean flag to configure if a deployment is shareable through LangGraph Studio.\n\nThis field does not apply to BYOC deployments."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "Region of deployment.\n\nRegion value is cloud provider specific."
|
||||
},
|
||||
"aws_account_id": {
|
||||
"type": "string",
|
||||
"description": "AWS account ID of BYOC deployment.\n\nThis field does not apply to non-BYOC deployments."
|
||||
},
|
||||
"aws_external_id": {
|
||||
"type": "string",
|
||||
"description": "Do not use."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResourceId": {
|
||||
"type": "object",
|
||||
"description": "Internal identifier for a `ResourceRevision` or `ResourceService`.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"revisions",
|
||||
"services"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResourceRevision": {
|
||||
"type": "object",
|
||||
"description": "Internal revision resource for a `ResourceService`.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/components/schemas/ResourceId"
|
||||
},
|
||||
"env_vars": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EnvVar"
|
||||
}
|
||||
},
|
||||
"hosted_langserve_revision_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "References `id` of a `Revision`."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResourceService": {
|
||||
"type": "object",
|
||||
"description": "Internal service resource for a `Project`.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/components/schemas/ResourceId"
|
||||
},
|
||||
"url": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URL of LangGraph Server deployment."
|
||||
},
|
||||
"latest_revision": {
|
||||
"description": "References latest `ResourceRevision`.\n\nThe latest `ResourceRevision` may not be active if it's currently being deployed.",
|
||||
"$ref": "#/components/schemas/ResourceRevision"
|
||||
},
|
||||
"latest_active_revision": {
|
||||
"description": "References latest active `ResourceRevision`.\n\nThe latest active `ResourceRevision` is not always the latest `ResourceRevision`.",
|
||||
"$ref": "#/components/schemas/ResourceRevision"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Revision": {
|
||||
"type": "object",
|
||||
"description": "A revision is a version of a LangGraph Server deployment.\n\nDifferent revisions may contain different code and/or environment variables. A project can have many revisions.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "ID of the revision.",
|
||||
"required": true
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "References `id` of `Project`.",
|
||||
"required": true
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp of when the revision was created.",
|
||||
"required": true
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp of when the revision was updated.",
|
||||
"required": true
|
||||
},
|
||||
"repo_path": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Path to `langgraph.json` configuration file. For example, `langgraph.json` or `src/langgraph.json`.\n\nThis field only applies to deployments from a GitHub repository.",
|
||||
"default": "null"
|
||||
},
|
||||
"repo_commit": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Git branch name of deployment.\n\nThis field only applies to deployments from a GitHub repository.",
|
||||
"default": "null"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CREATING",
|
||||
"AWAITING_BUILD",
|
||||
"BUILDING",
|
||||
"AWAITING_DEPLOY",
|
||||
"DEPLOYING",
|
||||
"CREATE_FAILED",
|
||||
"BUILD_FAILED",
|
||||
"DEPLOY_FAILED",
|
||||
"DEPLOYED",
|
||||
"INTERRUPTED",
|
||||
"UNKNOWN"
|
||||
],
|
||||
"description": "Deployment status of the revision.\n\nNon-terminal statuses: `CREATING`, `AWAITING_BUILD`, `BUILDING`, `AWAITING_DEPLOY`, `DEPLOYING`. All other statuses are terminal."
|
||||
},
|
||||
"status_message": {
|
||||
"type": "string",
|
||||
"description": "Message associated with the `status`."
|
||||
},
|
||||
"gcp_build_name": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Do not use."
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/components/schemas/RevisionMetadata"
|
||||
},
|
||||
"image_path": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URI of the Docker image to deploy.\n\nThis field does not apply to deployments from a GitHub repository.",
|
||||
"default": "null"
|
||||
},
|
||||
"container_spec": {
|
||||
"$ref": "#/components/schemas/ContainerSpec"
|
||||
},
|
||||
"resource": {
|
||||
"$ref": "#/components/schemas/ResourceRevision"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RevisionMetadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata associated with a `Revision`.",
|
||||
"properties": {
|
||||
"created_by": {
|
||||
"type": "object",
|
||||
"description": "Do not use."
|
||||
},
|
||||
"repo_commit_sha": {
|
||||
"type": "string",
|
||||
"description": "Git commit SHA of the deployment.\n\nThis field only applies to deployments from a GitHub repository."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
|
||||
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
|
||||
|
||||
The `fields` configuration determines which parts of your documents to embed:
|
||||
|
||||
- If omitted or set to `["$"]`, the entire document will be embedded
|
||||
- To embed specific fields, use JSON path notation: `["metadata.title", "content.text"]`
|
||||
- Documents missing specified fields will still be stored but won't have embeddings for those fields
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Environment Variables
|
||||
|
||||
The LangGraph Cloud API supports specific environment variables for configuring a deployment.
|
||||
The LangGraph Cloud Server supports specific environment variables for configuring a deployment.
|
||||
|
||||
## `LANGCHAIN_TRACING_SAMPLING_RATE`
|
||||
|
||||
@@ -10,10 +10,42 @@ See <a href="https://docs.smith.langchain.com/how_to_guides/tracing/sample_trace
|
||||
|
||||
## `LANGGRAPH_AUTH_TYPE`
|
||||
|
||||
Type of authentication for the LangGraph Cloud API deployment. Valid values: `langsmith`, `noop`.
|
||||
Type of authentication for the LangGraph Cloud Server deployment. Valid values: `langsmith`, `noop`.
|
||||
|
||||
For deployments to LangGraph Cloud, this environment variable is set automatically. For local development or deployments where authentication is handled externally (e.g. self-hosted), set this environment variable to `noop`.
|
||||
|
||||
## `LANGSMITH_RUNS_ENDPOINTS`
|
||||
|
||||
For [Bring Your Own Cloud (BYOC)](../../concepts/bring_your_own_cloud.md) deployments with [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) only.
|
||||
|
||||
Set this environment variable to have a BYOC deployment send traces to a self-hosted LangSmith instance. The value of `LANGSMITH_RUNS_ENDPOINTS` is a JSON string: `{"<SELF_HOSTED_LANGSMITH_HOSTNAME>":"<LANGSMITH_API_KEY>"}`.
|
||||
|
||||
`SELF_HOSTED_LANGSMITH_HOSTNAME` is the hostname of the self-hosted LangSmith instance. It must be accessible to the BYOC deployment. `LANGSMITH_API_KEY` is a LangSmith API generated from the self-hosted LangSmith instance.
|
||||
|
||||
## `N_JOBS_PER_WORKER`
|
||||
|
||||
Number of jobs per worker for the LangGraph Cloud task queue. Defaults to `10`.
|
||||
|
||||
## `POSTGRES_URI_CUSTOM`
|
||||
|
||||
For [Bring Your Own Cloud (BYOC)](../../concepts/bring_your_own_cloud.md) deployments only.
|
||||
|
||||
Specify `POSTGRES_URI_CUSTOM` to use an externally managed Postgres instance. The value of `POSTGRES_URI_CUSTOM` must be a valid [Postgres connection URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS).
|
||||
|
||||
Postgres:
|
||||
|
||||
- Version 15.8 or higher.
|
||||
- An initial database must be present and the connection URI must reference the database.
|
||||
|
||||
Control Plane Functionality:
|
||||
|
||||
- If `POSTGRES_URI_CUSTOM` is specified, the LangGraph Control Plane will not provision a database for the server.
|
||||
- If `POSTGRES_URI_CUSTOM` is removed, the LangGraph Control Plane will not provision a database for the server and will not delete the externally managed Postgres instance.
|
||||
- If `POSTGRES_URI_CUSTOM` is removed, deployment of the revision will not succeed. Once `POSTGRES_URI_CUSTOM` is specified, it must always be set for the lifecycle of the deployment.
|
||||
- If the deployment is deleted, the LangGraph Control Plane will not delete the externally managed Postgres instance.
|
||||
- The value of `POSTGRES_URI_CUSTOM` can be updated. For example, a password in the URI can be updated.
|
||||
|
||||
Database Connectivity:
|
||||
|
||||
- The externally managed Postgres instance must be accessible by the LangGraph Server service in the ECS cluster. The BYOC user is responsible for ensuring connectivity.
|
||||
- For example, if an AWS RDS Postgres instance is provisioned, it can be provisioned in the same VPC (`langgraph-cloud-vpc`) as the ECS cluster with the `langgraph-cloud-service-sg` security group to ensure connectivity.
|
||||
|
||||
@@ -39,6 +39,7 @@ LangChain has no direct access to the resources created in your cloud account, a
|
||||
- Read CloudWatch metrics/logs to monitor your instances/push deployment logs
|
||||
- https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonRDSFullAccess.html
|
||||
- Provision `RDS` instances for your LangGraph Cloud instances
|
||||
- Alternatively, an externally managed Postgres instance can be used instead of the default `RDS` instance. LangChain does not monitor or manage the externally managed Postgres instance. See details for [`POSTGRES_URI_CUSTOM` environment variable](../cloud/reference/env_var.md#postgres_uri_custom).
|
||||
2. Either
|
||||
- Tags an existing vpc / subnets as `langgraph-cloud-enabled`
|
||||
- Creates a new vpc and subnets and tags them as `langgraph-cloud-enabled`
|
||||
@@ -50,5 +51,5 @@ LangChain has no direct access to the resources created in your cloud account, a
|
||||
|
||||
Notes for customers using [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting):
|
||||
|
||||
- Creation of new LangGraph Cloud projects and revisions currently needs to be done on smith.langchain.com.
|
||||
- You can however set up the project to trace to your self-hosted LangSmith instance if desired
|
||||
- Creation of new LangGraph Cloud projects and revisions currently needs to be done on `smith.langchain.com`.
|
||||
- However, you can set up the project to trace to your self-hosted LangSmith instance if desired. See details for [`LANGSMITH_RUNS_ENDPOINTS` environment variable](../cloud/reference/env_var.md#langsmith_runs_endpoints).
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"\n",
|
||||
" # Check the signed-in user actually has this ticket\n",
|
||||
" cursor.execute(\n",
|
||||
" \"SELECT flight_id FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n",
|
||||
" \"SELECT ticket_no FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n",
|
||||
" (ticket_no, passenger_id),\n",
|
||||
" )\n",
|
||||
" current_ticket = cursor.fetchone()\n",
|
||||
@@ -4444,7 +4444,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -1659,7 +1659,7 @@
|
||||
"id": "584de971-6b10-4931-986e-cc35f7adbb3d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now the graph is complete, since we've provided the final response message! Since state updates simulate a graph step, they even generate corresponding traces. Inspec the [LangSmith trace](https://smith.langchain.com/public/6d72aeb5-3bca-4090-8684-a11d5a36b10c/r) of the `update_state` call above to see what's going on.\n",
|
||||
"Now the graph is complete, since we've provided the final response message! Since state updates simulate a graph step, they even generate corresponding traces. Inspect the [LangSmith trace](https://smith.langchain.com/public/6d72aeb5-3bca-4090-8684-a11d5a36b10c/r) of the `update_state` call above to see what's going on.\n",
|
||||
"\n",
|
||||
"**Notice** that our new messages are _appended_ to the messages already in the state. Remember how we defined the `State` type?\n",
|
||||
"\n",
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langgraph.graph import MessagesState\n",
|
||||
"from langgraph.graph import MessagesState, END\n",
|
||||
"from langgraph.types import Command\n",
|
||||
"\n",
|
||||
"\n",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"
|
||||
"%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters beautifulsoup4"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ site_name: ""
|
||||
site_description: Build language agents as graphs
|
||||
site_url: https://langchain-ai.github.io/langgraph/
|
||||
repo_url: https://github.com/langchain-ai/langgraph
|
||||
edit_uri: edit/main/docs/docs/
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: overrides
|
||||
@@ -16,6 +17,7 @@ theme:
|
||||
- content.code.copy
|
||||
- content.code.select
|
||||
- content.tabs.link
|
||||
- content.action.edit
|
||||
- content.tooltips
|
||||
- header.autohide
|
||||
- navigation.expand
|
||||
|
||||
@@ -58,8 +58,6 @@ MIGRATIONS = [
|
||||
);""",
|
||||
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
|
||||
"""
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
|
||||
""",
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.9"
|
||||
version = "2.0.10"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# type: ignore
|
||||
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
@@ -782,3 +783,10 @@ def test_scores(
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].score == pytest.approx(similarities[0], abs=1e-3)
|
||||
|
||||
|
||||
def test_nonnull_migrations() -> None:
|
||||
_leading_comment_remover = re.compile(r"^/\*.*?\*/")
|
||||
for migration in PostgresStore.MIGRATIONS:
|
||||
statement = _leading_comment_remover.sub("", migration).split()[0]
|
||||
assert statement.strip()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# type: ignore
|
||||
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@@ -238,3 +239,10 @@ def test_null_chars(saver_name: str, test_data) -> None:
|
||||
list(saver.list(None, filter={"my_key": "abc"}))[0].metadata["my_key"]
|
||||
== "abc"
|
||||
)
|
||||
|
||||
|
||||
def test_nonnull_migrations() -> None:
|
||||
_leading_comment_remover = re.compile(r"^/\*.*?\*/")
|
||||
for migration in PostgresSaver.MIGRATIONS:
|
||||
statement = _leading_comment_remover.sub("", migration).split()[0]
|
||||
assert statement.strip()
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
⚡ Building language agents as graphs ⚡
|
||||
|
||||
> [!NOTE]
|
||||
> Looking for the JS version? Click [here](https://github.com/langchain-ai/langgraphjs) ([JS docs](https://langchain-ai.github.io/langgraphjs/)).
|
||||
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import operator
|
||||
from typing import Annotated, TypedDict
|
||||
from typing import Annotated
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.constants import END, START, Send
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
import concurrent
|
||||
import concurrent.futures
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from functools import partial, update_wrapper
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
@@ -33,17 +33,17 @@ T = TypeVar("T")
|
||||
|
||||
|
||||
def call(
|
||||
func: Callable[[P1], T],
|
||||
input: P1,
|
||||
*,
|
||||
func: Callable[P, T],
|
||||
*args: Any,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
**kwargs: Any,
|
||||
) -> concurrent.futures.Future[T]:
|
||||
from langgraph.constants import CONFIG_KEY_CALL
|
||||
from langgraph.utils.config import get_configurable
|
||||
|
||||
conf = get_configurable()
|
||||
impl = conf[CONFIG_KEY_CALL]
|
||||
fut = impl(func, input, retry=retry)
|
||||
fut = impl(func, (args, kwargs), retry=retry)
|
||||
return fut
|
||||
|
||||
|
||||
@@ -59,16 +59,51 @@ def task( # type: ignore[overload-cannot-match]
|
||||
) -> Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def task(
|
||||
*, retry: Optional[RetryPolicy] = None
|
||||
__func_or_none__: Callable[P, T],
|
||||
) -> Callable[P, concurrent.futures.Future[T]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def task(
|
||||
__func_or_none__: Callable[P, Awaitable[T]],
|
||||
) -> Callable[P, asyncio.Future[T]]: ...
|
||||
|
||||
|
||||
def task(
|
||||
__func_or_none__: Optional[Union[Callable[P, T], Callable[P, Awaitable[T]]]] = None,
|
||||
*,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
) -> Union[
|
||||
Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]],
|
||||
Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]],
|
||||
Callable[P, asyncio.Future[T]],
|
||||
Callable[P, concurrent.futures.Future[T]],
|
||||
]:
|
||||
def _task(func: Callable[P, T]) -> Callable[P, concurrent.futures.Future[T]]:
|
||||
return update_wrapper(partial(call, func, retry=retry), func)
|
||||
def decorator(
|
||||
func: Union[Callable[P, Awaitable[T]], Callable[P, T]],
|
||||
) -> Callable[P, concurrent.futures.Future[T]]:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
|
||||
return _task
|
||||
@functools.wraps(func)
|
||||
async def _tick(__allargs__: tuple) -> T:
|
||||
return await func(*__allargs__[0], **__allargs__[1])
|
||||
|
||||
else:
|
||||
|
||||
@functools.wraps(func)
|
||||
def _tick(__allargs__: tuple) -> T:
|
||||
return func(*__allargs__[0], **__allargs__[1])
|
||||
|
||||
return functools.update_wrapper(
|
||||
functools.partial(call, _tick, retry=retry), func
|
||||
)
|
||||
|
||||
if __func_or_none__ is not None:
|
||||
return decorator(__func_or_none__)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def entrypoint(
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import (
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -22,6 +21,7 @@ from langchain_core.messages import (
|
||||
convert_to_messages,
|
||||
message_chunk_to_message,
|
||||
)
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
|
||||
@@ -398,9 +398,11 @@ class StateGraph(Graph):
|
||||
return self
|
||||
|
||||
def add_edge(self, start_key: Union[str, list[str]], end_key: str) -> Self:
|
||||
"""Adds a directed edge from the start node to the end node.
|
||||
"""Adds a directed edge from the start node (or list of start nodes) to the end node.
|
||||
|
||||
If the graph transitions to the start_key node, it will always transition to the end_key node next.
|
||||
When a single start node is provided, the graph will wait for that node to complete
|
||||
before executing the end node. When multiple start nodes are provided,
|
||||
the graph will wait for ALL of the start nodes to complete before executing the end node.
|
||||
|
||||
Args:
|
||||
start_key (Union[str, list[str]]): The key(s) of the start node(s) of the edge.
|
||||
|
||||
@@ -381,7 +381,7 @@ def create_react_agent(
|
||||
Add complex prompt with custom graph state:
|
||||
|
||||
```pycon
|
||||
>>> from typing import TypedDict
|
||||
>>> from typing_extensions import TypedDict
|
||||
>>>
|
||||
>>> from langgraph.managed import IsLastStep
|
||||
>>> prompt = ChatPromptTemplate.from_messages(
|
||||
@@ -554,6 +554,10 @@ def create_react_agent(
|
||||
)
|
||||
model_runnable = preprocessor | model
|
||||
|
||||
# If any of the tools are configured to return_directly after running,
|
||||
# our graph needs to check if these were called
|
||||
should_return_direct = {t.name for t in tool_classes if t.return_direct}
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state: AgentState, config: RunnableConfig) -> AgentState:
|
||||
_validate_chat_history(state["messages"])
|
||||
@@ -673,10 +677,6 @@ def create_react_agent(
|
||||
should_continue,
|
||||
)
|
||||
|
||||
# If any of the tools are configured to return_directly after running,
|
||||
# our graph needs to check if these were called
|
||||
should_return_direct = {t.name for t in tool_classes if t.return_direct}
|
||||
|
||||
def route_tool_responses(state: AgentState) -> Literal["agent", "__end__"]:
|
||||
for m in reversed(state["messages"]):
|
||||
if not isinstance(m, ToolMessage):
|
||||
|
||||
@@ -601,7 +601,8 @@ def tools_condition(
|
||||
>>> from langgraph.prebuilt import ToolNode, tools_condition
|
||||
>>> from langgraph.graph.message import add_messages
|
||||
...
|
||||
>>> from typing import TypedDict, Annotated
|
||||
>>> from typing import Annotated
|
||||
>>> from typing_extensions import TypedDict
|
||||
...
|
||||
>>> @tool
|
||||
>>> def divide(a: float, b: float) -> int:
|
||||
|
||||
@@ -74,7 +74,8 @@ class ValidationNode(RunnableCallable):
|
||||
|
||||
Examples:
|
||||
Example usage for re-prompting the model to generate a valid response:
|
||||
>>> from typing import Literal, Annotated, TypedDict
|
||||
>>> from typing import Literal, Annotated
|
||||
>>> from typing_extensions import TypedDict
|
||||
...
|
||||
>>> from langchain_anthropic import ChatAnthropic
|
||||
>>> from pydantic import BaseModel, validator
|
||||
|
||||
@@ -10,13 +10,13 @@ from typing import (
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.utils.input import get_bolded_text, get_colored_text
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata, PendingWrite
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections import Counter
|
||||
from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypeVar, Union
|
||||
from uuid import UUID
|
||||
|
||||
@@ -181,12 +182,27 @@ def map_output_updates(
|
||||
(task.name, value) for chan, value in writes if chan == output_channels
|
||||
)
|
||||
elif any(chan in output_channels for chan, _ in writes):
|
||||
updated.append(
|
||||
(
|
||||
task.name,
|
||||
{chan: value for chan, value in writes if chan in output_channels},
|
||||
counts = Counter(chan for chan, _ in writes)
|
||||
if any(counts[chan] > 1 for chan in output_channels):
|
||||
updated.extend(
|
||||
(
|
||||
task.name,
|
||||
{chan: value},
|
||||
)
|
||||
for chan, value in writes
|
||||
if chan in output_channels
|
||||
)
|
||||
else:
|
||||
updated.append(
|
||||
(
|
||||
task.name,
|
||||
{
|
||||
chan: value
|
||||
for chan, value in writes
|
||||
if chan in output_channels
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
grouped: dict[str, list[Any]] = {t.name: [] for t, _ in output_tasks}
|
||||
for node, value in updated:
|
||||
grouped[node].append(value)
|
||||
|
||||
@@ -1032,6 +1032,13 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
# unwind stack
|
||||
return await asyncio.shield(
|
||||
exit_task = asyncio.create_task(
|
||||
self.stack.__aexit__(exc_type, exc_value, traceback)
|
||||
)
|
||||
try:
|
||||
return await exit_task
|
||||
except asyncio.CancelledError as e:
|
||||
# Bubble up the exit task upon cancellation to permit the API
|
||||
# consumer to await it before e.g., re-using the DB connection.
|
||||
e.args = (*e.args, exit_task)
|
||||
raise
|
||||
|
||||
@@ -13,14 +13,13 @@ from typing import (
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from typing_extensions import Self
|
||||
from typing_extensions import Self, TypedDict
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
@@ -373,7 +372,8 @@ def interrupt(value: Any) -> Any:
|
||||
Example:
|
||||
```python
|
||||
import uuid
|
||||
from typing import TypedDict, Optional
|
||||
from typing import Optional
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.constants import START
|
||||
|
||||
Generated
+3
-3
@@ -965,13 +965,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
version = "3.1.5"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
|
||||
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.60"
|
||||
version = "0.2.61"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -38,7 +38,7 @@ py-spy = "^0.3.14"
|
||||
types-requests = "^2.32.0.20240914"
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [ "E", "F", "I" ]
|
||||
lint.select = [ "E", "F", "I", "TID251" ]
|
||||
lint.ignore = [ "E501" ]
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
@@ -52,6 +52,9 @@ line-ending = "auto"
|
||||
docstring-code-format = false
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
||||
"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
|
||||
|
||||
[tool.mypy]
|
||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||
disallow_untyped_defs = "True"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import TypedDict
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from tests.conftest import (
|
||||
|
||||
@@ -4,13 +4,14 @@ import re
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import replace
|
||||
from typing import Annotated, Any, Iterator, Literal, Optional, TypedDict, Union, cast
|
||||
from typing import Annotated, Any, Iterator, Literal, Optional, Union, cast
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig, RunnableMap, RunnablePick
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.context import Context
|
||||
from langgraph.channels.last_value import LastValue
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import (
|
||||
AsyncIterator,
|
||||
Literal,
|
||||
Optional,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -21,6 +20,7 @@ from langchain_core.runnables import RunnableConfig, RunnablePick
|
||||
from pydantic import BaseModel
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.context import Context
|
||||
from langgraph.channels.last_value import LastValue
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import dataclasses
|
||||
import inspect
|
||||
import json
|
||||
from functools import partial
|
||||
from typing import (
|
||||
@@ -2040,3 +2041,9 @@ def test__get_state_args() -> None:
|
||||
return 0.0
|
||||
|
||||
assert _get_state_args(foo) == {"a": None, "b": "bar"}
|
||||
|
||||
|
||||
def test_inspect_react() -> None:
|
||||
model = FakeToolCallingModel(tool_calls=[])
|
||||
agent = create_react_agent(model, [])
|
||||
inspect.getclosurevars(agent.nodes["agent"].bound.func)
|
||||
|
||||
@@ -21,7 +21,6 @@ from typing import (
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
get_type_hints,
|
||||
)
|
||||
@@ -36,6 +35,7 @@ from langchain_core.runnables import (
|
||||
from langsmith import traceable
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
@@ -1515,27 +1515,32 @@ def test_imp_stream_order(
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@task()
|
||||
def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo", "b": "bar"}
|
||||
def foo(state: dict) -> tuple:
|
||||
return state["a"] + "foo", "bar"
|
||||
|
||||
@task()
|
||||
def bar(state: dict) -> dict:
|
||||
return {"a": state["a"] + state["b"], "c": "bark"}
|
||||
@task
|
||||
def bar(a: str, b: str, c: Optional[str] = None) -> dict:
|
||||
return {"a": a + b, "c": (c or "") + "bark"}
|
||||
|
||||
@task()
|
||||
@task
|
||||
def baz(state: dict) -> dict:
|
||||
return {"a": state["a"] + "baz", "c": "something else"}
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(state: dict) -> dict:
|
||||
fut_foo = foo(state)
|
||||
fut_bar = bar(fut_foo.result())
|
||||
fut_bar = bar(*fut_foo.result())
|
||||
fut_baz = baz(fut_bar.result())
|
||||
return fut_baz.result()
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [c for c in graph.stream({"a": "0"}, thread1)] == [
|
||||
{"foo": {"a": "0foo", "b": "bar"}},
|
||||
{
|
||||
"foo": (
|
||||
"0foo",
|
||||
"bar",
|
||||
)
|
||||
},
|
||||
{"bar": {"a": "0foobar", "c": "bark"}},
|
||||
{"baz": {"a": "0foobarbaz", "c": "something else"}},
|
||||
{"graph": {"a": "0foobarbaz", "c": "something else"}},
|
||||
@@ -4168,10 +4173,12 @@ def test_store_injected(
|
||||
def __call__(self, inputs: State, config: RunnableConfig, store: BaseStore):
|
||||
assert isinstance(store, BaseStore)
|
||||
store.put(
|
||||
namespace
|
||||
if self.i is not None
|
||||
and config["configurable"]["thread_id"] in (thread_1, thread_2)
|
||||
else (f"foo_{self.i}", "bar"),
|
||||
(
|
||||
namespace
|
||||
if self.i is not None
|
||||
and config["configurable"]["thread_id"] in (thread_1, thread_2)
|
||||
else (f"foo_{self.i}", "bar")
|
||||
),
|
||||
doc_id,
|
||||
{
|
||||
**doc,
|
||||
@@ -5242,3 +5249,54 @@ def test_checkpoint_recovery(request: pytest.FixtureRequest, checkpointer_name:
|
||||
# Verify the error was recorded in checkpoint
|
||||
failed_checkpoint = next(c for c in history if c.tasks and c.tasks[0].error)
|
||||
assert "RuntimeError('Simulated failure')" in failed_checkpoint.tasks[0].error
|
||||
|
||||
|
||||
def test_multiple_updates_root() -> None:
|
||||
def node_a(state):
|
||||
return [Command(update="a1"), Command(update="a2")]
|
||||
|
||||
def node_b(state):
|
||||
return "b"
|
||||
|
||||
graph = (
|
||||
StateGraph(Annotated[str, operator.add])
|
||||
.add_sequence([node_a, node_b])
|
||||
.add_edge(START, "node_a")
|
||||
.compile()
|
||||
)
|
||||
|
||||
assert graph.invoke("") == "a1a2b"
|
||||
|
||||
# only streams the last update from node_a
|
||||
assert [c for c in graph.stream("", stream_mode="updates")] == [
|
||||
{"node_a": ["a1", "a2"]},
|
||||
{"node_b": "b"},
|
||||
]
|
||||
|
||||
|
||||
def test_multiple_updates() -> None:
|
||||
class State(TypedDict):
|
||||
foo: Annotated[str, operator.add]
|
||||
|
||||
def node_a(state):
|
||||
return [Command(update={"foo": "a1"}), Command(update={"foo": "a2"})]
|
||||
|
||||
def node_b(state):
|
||||
return {"foo": "b"}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_sequence([node_a, node_b])
|
||||
.add_edge(START, "node_a")
|
||||
.compile()
|
||||
)
|
||||
|
||||
assert graph.invoke({"foo": ""}) == {
|
||||
"foo": "a1a2b",
|
||||
}
|
||||
|
||||
# only streams the last update from node_a
|
||||
assert [c for c in graph.stream({"foo": ""}, stream_mode="updates")] == [
|
||||
{"node_a": [{"foo": "a1"}, {"foo": "a2"}]},
|
||||
{"node_b": {"foo": "b"}},
|
||||
]
|
||||
|
||||
@@ -19,7 +19,6 @@ from typing import (
|
||||
Literal,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
from uuid import UUID
|
||||
@@ -34,6 +33,7 @@ from langchain_core.runnables import (
|
||||
from langchain_core.utils.aiter import aclosing
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
@@ -180,6 +180,262 @@ async def test_checkpoint_errors() -> None:
|
||||
pass
|
||||
|
||||
|
||||
async def test_py_async_with_cancel_behavior() -> None:
|
||||
"""This test confirms that in all versions of Python we support, __aexit__
|
||||
is not cancelled when the coroutine containing the async with block is cancelled."""
|
||||
|
||||
logs: list[str] = []
|
||||
|
||||
class MyContextManager:
|
||||
async def __aenter__(self):
|
||||
logs.append("Entering")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
logs.append("Starting exit")
|
||||
try:
|
||||
# Simulate some cleanup work
|
||||
await asyncio.sleep(2)
|
||||
logs.append("Cleanup completed")
|
||||
except asyncio.CancelledError:
|
||||
logs.append("Cleanup was cancelled!")
|
||||
raise
|
||||
logs.append("Exit finished")
|
||||
|
||||
async def main():
|
||||
try:
|
||||
async with MyContextManager():
|
||||
logs.append("In context")
|
||||
await asyncio.sleep(1)
|
||||
logs.append("This won't print if cancelled")
|
||||
except asyncio.CancelledError:
|
||||
logs.append("Context was cancelled")
|
||||
raise
|
||||
|
||||
# create task
|
||||
t = asyncio.create_task(main())
|
||||
# cancel after 0.2 seconds
|
||||
await asyncio.sleep(0.2)
|
||||
t.cancel()
|
||||
# check logs before cancellation is handled
|
||||
assert logs == [
|
||||
"Entering",
|
||||
"In context",
|
||||
], "Cancelled before cleanup started"
|
||||
# wait for task to finish
|
||||
try:
|
||||
await t
|
||||
except asyncio.CancelledError:
|
||||
# check logs after cancellation is handled
|
||||
assert logs == [
|
||||
"Entering",
|
||||
"In context",
|
||||
"Starting exit",
|
||||
"Cleanup completed",
|
||||
"Exit finished",
|
||||
"Context was cancelled",
|
||||
], "Cleanup started and finished after cancellation"
|
||||
else:
|
||||
assert False, "Task should be cancelled"
|
||||
|
||||
|
||||
async def test_checkpoint_put_after_cancellation() -> None:
|
||||
logs: list[str] = []
|
||||
|
||||
class LongPutCheckpointer(MemorySaver):
|
||||
async def aput(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
logs.append("checkpoint.aput.start")
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
return await super().aput(config, checkpoint, metadata, new_versions)
|
||||
finally:
|
||||
logs.append("checkpoint.aput.end")
|
||||
|
||||
inner_task_cancelled = False
|
||||
|
||||
async def awhile(input: Any) -> None:
|
||||
logs.append("awhile.start")
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
nonlocal inner_task_cancelled
|
||||
inner_task_cancelled = True
|
||||
raise
|
||||
finally:
|
||||
logs.append("awhile.end")
|
||||
|
||||
builder = Graph()
|
||||
builder.add_node("agent", awhile)
|
||||
builder.set_entry_point("agent")
|
||||
builder.set_finish_point("agent")
|
||||
|
||||
graph = builder.compile(checkpointer=LongPutCheckpointer())
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# start the task
|
||||
t = asyncio.create_task(graph.ainvoke(1, thread1))
|
||||
# cancel after 0.2 seconds
|
||||
await asyncio.sleep(0.2)
|
||||
t.cancel()
|
||||
# check logs before cancellation is handled
|
||||
assert sorted(logs) == [
|
||||
"awhile.start",
|
||||
"checkpoint.aput.start",
|
||||
], "Cancelled before checkpoint put started"
|
||||
# wait for task to finish
|
||||
try:
|
||||
await t
|
||||
except asyncio.CancelledError:
|
||||
# check logs after cancellation is handled
|
||||
assert sorted(logs) == [
|
||||
"awhile.end",
|
||||
"awhile.start",
|
||||
"checkpoint.aput.end",
|
||||
"checkpoint.aput.start",
|
||||
], "Checkpoint put is not cancelled"
|
||||
else:
|
||||
assert False, "Task should be cancelled"
|
||||
|
||||
|
||||
async def test_checkpoint_put_after_cancellation_stream_anext() -> None:
|
||||
logs: list[str] = []
|
||||
|
||||
class LongPutCheckpointer(MemorySaver):
|
||||
async def aput(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
logs.append("checkpoint.aput.start")
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
return await super().aput(config, checkpoint, metadata, new_versions)
|
||||
finally:
|
||||
logs.append("checkpoint.aput.end")
|
||||
|
||||
inner_task_cancelled = False
|
||||
|
||||
async def awhile(input: Any) -> None:
|
||||
logs.append("awhile.start")
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
nonlocal inner_task_cancelled
|
||||
inner_task_cancelled = True
|
||||
raise
|
||||
finally:
|
||||
logs.append("awhile.end")
|
||||
|
||||
builder = Graph()
|
||||
builder.add_node("agent", awhile)
|
||||
builder.set_entry_point("agent")
|
||||
builder.set_finish_point("agent")
|
||||
|
||||
graph = builder.compile(checkpointer=LongPutCheckpointer())
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# start the task
|
||||
s = graph.astream(1, thread1)
|
||||
t = asyncio.create_task(s.__anext__())
|
||||
# cancel after 0.2 seconds
|
||||
await asyncio.sleep(0.2)
|
||||
t.cancel()
|
||||
# check logs before cancellation is handled
|
||||
assert sorted(logs) == [
|
||||
"awhile.start",
|
||||
"checkpoint.aput.start",
|
||||
], "Cancelled before checkpoint put started"
|
||||
# wait for task to finish
|
||||
try:
|
||||
await t
|
||||
except asyncio.CancelledError:
|
||||
# check logs after cancellation is handled
|
||||
assert sorted(logs) == [
|
||||
"awhile.end",
|
||||
"awhile.start",
|
||||
"checkpoint.aput.end",
|
||||
"checkpoint.aput.start",
|
||||
], "Checkpoint put is not cancelled"
|
||||
else:
|
||||
assert False, "Task should be cancelled"
|
||||
|
||||
|
||||
async def test_checkpoint_put_after_cancellation_stream_events_anext() -> None:
|
||||
logs: list[str] = []
|
||||
|
||||
class LongPutCheckpointer(MemorySaver):
|
||||
async def aput(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
logs.append("checkpoint.aput.start")
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
return await super().aput(config, checkpoint, metadata, new_versions)
|
||||
finally:
|
||||
logs.append("checkpoint.aput.end")
|
||||
|
||||
inner_task_cancelled = False
|
||||
|
||||
async def awhile(input: Any) -> None:
|
||||
logs.append("awhile.start")
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
nonlocal inner_task_cancelled
|
||||
inner_task_cancelled = True
|
||||
raise
|
||||
finally:
|
||||
logs.append("awhile.end")
|
||||
|
||||
builder = Graph()
|
||||
builder.add_node("agent", awhile)
|
||||
builder.set_entry_point("agent")
|
||||
builder.set_finish_point("agent")
|
||||
|
||||
graph = builder.compile(checkpointer=LongPutCheckpointer())
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# start the task
|
||||
s = graph.astream_events(1, thread1, version="v2", include_names=["LangGraph"])
|
||||
# skip first event (happens right away)
|
||||
await s.__anext__()
|
||||
# start the task for 2nd event
|
||||
t = asyncio.create_task(s.__anext__())
|
||||
# cancel after 0.2 seconds
|
||||
await asyncio.sleep(0.2)
|
||||
t.cancel()
|
||||
# check logs before cancellation is handled
|
||||
assert logs == [
|
||||
"checkpoint.aput.start",
|
||||
"awhile.start",
|
||||
], "Cancelled before checkpoint put started"
|
||||
# wait for task to finish
|
||||
try:
|
||||
await t
|
||||
except asyncio.CancelledError:
|
||||
# check logs after cancellation is handled
|
||||
assert logs == [
|
||||
"checkpoint.aput.start",
|
||||
"awhile.start",
|
||||
"awhile.end",
|
||||
"checkpoint.aput.end",
|
||||
], "Checkpoint put is not cancelled"
|
||||
else:
|
||||
assert False, "Task should be cancelled"
|
||||
|
||||
|
||||
async def test_node_cancellation_on_external_cancel() -> None:
|
||||
inner_task_cancelled = False
|
||||
|
||||
@@ -2315,9 +2571,9 @@ async def test_imp_sync_from_async(checkpointer_name: str) -> None:
|
||||
def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo", "b": "bar"}
|
||||
|
||||
@task()
|
||||
def bar(state: dict) -> dict:
|
||||
return {"a": state["a"] + state["b"], "c": "bark"}
|
||||
@task
|
||||
def bar(a: str, b: str, c: Optional[str] = None) -> dict:
|
||||
return {"a": a + b, "c": (c or "") + "bark"}
|
||||
|
||||
@task()
|
||||
def baz(state: dict) -> dict:
|
||||
@@ -2325,8 +2581,8 @@ async def test_imp_sync_from_async(checkpointer_name: str) -> None:
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(state: dict) -> dict:
|
||||
fut_foo = foo(state)
|
||||
fut_bar = bar(fut_foo.result())
|
||||
foo_result = foo(state).result()
|
||||
fut_bar = bar(foo_result["a"], foo_result["b"])
|
||||
fut_baz = baz(fut_bar.result())
|
||||
return fut_baz.result()
|
||||
|
||||
@@ -2351,9 +2607,9 @@ async def test_imp_stream_order(checkpointer_name: str) -> None:
|
||||
async def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo", "b": "bar"}
|
||||
|
||||
@task()
|
||||
async def bar(state: dict) -> dict:
|
||||
return {"a": state["a"] + state["b"], "c": "bark"}
|
||||
@task
|
||||
async def bar(a: str, b: str, c: Optional[str] = None) -> dict:
|
||||
return {"a": a + b, "c": (c or "") + "bark"}
|
||||
|
||||
@task()
|
||||
async def baz(state: dict) -> dict:
|
||||
@@ -2361,8 +2617,9 @@ async def test_imp_stream_order(checkpointer_name: str) -> None:
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def graph(state: dict) -> dict:
|
||||
fut_foo = foo(state)
|
||||
fut_bar = bar(await fut_foo)
|
||||
foo_res = await foo(state)
|
||||
|
||||
fut_bar = bar(foo_res["a"], foo_res["b"])
|
||||
fut_baz = baz(await fut_bar)
|
||||
return await fut_baz
|
||||
|
||||
@@ -6362,3 +6619,54 @@ async def test_checkpoint_recovery_async(checkpointer_name: str):
|
||||
# Verify the error was recorded in checkpoint
|
||||
failed_checkpoint = next(c for c in history if c.tasks and c.tasks[0].error)
|
||||
assert "RuntimeError('Simulated failure')" in failed_checkpoint.tasks[0].error
|
||||
|
||||
|
||||
async def test_multiple_updates_root() -> None:
|
||||
def node_a(state):
|
||||
return [Command(update="a1"), Command(update="a2")]
|
||||
|
||||
def node_b(state):
|
||||
return "b"
|
||||
|
||||
graph = (
|
||||
StateGraph(Annotated[str, operator.add])
|
||||
.add_sequence([node_a, node_b])
|
||||
.add_edge(START, "node_a")
|
||||
.compile()
|
||||
)
|
||||
|
||||
assert await graph.ainvoke("") == "a1a2b"
|
||||
|
||||
# only streams the last update from node_a
|
||||
assert [c async for c in graph.astream("", stream_mode="updates")] == [
|
||||
{"node_a": ["a1", "a2"]},
|
||||
{"node_b": "b"},
|
||||
]
|
||||
|
||||
|
||||
async def test_multiple_updates() -> None:
|
||||
class State(TypedDict):
|
||||
foo: Annotated[str, operator.add]
|
||||
|
||||
def node_a(state):
|
||||
return [Command(update={"foo": "a1"}), Command(update={"foo": "a2"})]
|
||||
|
||||
def node_b(state):
|
||||
return {"foo": "b"}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_sequence([node_a, node_b])
|
||||
.add_edge(START, "node_a")
|
||||
.compile()
|
||||
)
|
||||
|
||||
assert await graph.ainvoke({"foo": ""}) == {
|
||||
"foo": "a1a2b",
|
||||
}
|
||||
|
||||
# only streams the last update from node_a
|
||||
assert [c async for c in graph.astream({"foo": ""}, stream_mode="updates")] == [
|
||||
{"node_a": [{"foo": "a1"}, {"foo": "a2"}]},
|
||||
{"node_b": {"foo": "b"}},
|
||||
]
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Callable, Tuple, TypedDict, TypeVar
|
||||
from typing import Any, Callable, Tuple, TypeVar
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import langsmith as ls
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.tracers import LangChainTracer
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import (
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
@@ -17,7 +16,7 @@ from unittest.mock import patch
|
||||
|
||||
import langsmith
|
||||
import pytest
|
||||
from typing_extensions import Annotated, NotRequired, Required
|
||||
from typing_extensions import Annotated, NotRequired, Required, TypedDict
|
||||
|
||||
from langgraph.graph import END, StateGraph
|
||||
from langgraph.graph.graph import CompiledGraph
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.34",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
@@ -817,6 +817,8 @@ export class RunsClient extends BaseClient {
|
||||
command: payload?.command,
|
||||
config: payload?.config,
|
||||
metadata: payload?.metadata,
|
||||
stream_mode: payload?.streamMode,
|
||||
stream_subgraphs: payload?.streamSubgraphs,
|
||||
assistant_id: assistantId,
|
||||
interrupt_before: payload?.interruptBefore,
|
||||
interrupt_after: payload?.interruptAfter,
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { Checkpoint, Config, Metadata } from "./schema.js";
|
||||
|
||||
/**
|
||||
* Stream modes
|
||||
* - "values": Stream only the state values.
|
||||
* - "messages": Stream complete messages.
|
||||
* - "messages-tuple": Stream (message chunk, metadata) tuples.
|
||||
* - "updates": Stream updates to the state.
|
||||
* - "events": Stream events occurring during execution.
|
||||
* - "debug": Stream detailed debug information.
|
||||
* - "custom": Stream custom events.
|
||||
*/
|
||||
export type StreamMode =
|
||||
| "values"
|
||||
| "messages"
|
||||
@@ -32,7 +42,7 @@ export interface Command {
|
||||
/**
|
||||
* An object to update the thread state with.
|
||||
*/
|
||||
update?: Record<string, unknown>;
|
||||
update?: Record<string, unknown> | [string, unknown][];
|
||||
|
||||
/**
|
||||
* The value to return from an `interrupt` function call.
|
||||
@@ -140,13 +150,7 @@ interface RunsInvokePayload {
|
||||
|
||||
export interface RunsStreamPayload extends RunsInvokePayload {
|
||||
/**
|
||||
* One of `"values"`, `"messages"`, `"updates"` or `"events"`.
|
||||
* - `"values"`: Stream the thread state any time it changes.
|
||||
* - `"messages"`: Stream chat messages from thread state and calls to chat models,
|
||||
* token-by-token where possible.
|
||||
* - `"updates"`: Stream the state updates returned by each node.
|
||||
* - `"events"`: Stream all events produced by the run. You can also access these
|
||||
* afterwards using the `client.runs.listEvents()` method.
|
||||
* One of `"values"`, `"messages"`, `"messages-tuple"`, `"updates"`, `"events"`, `"debug"`, `"custom"`.
|
||||
*/
|
||||
streamMode?: StreamMode | Array<StreamMode>;
|
||||
|
||||
@@ -162,7 +166,17 @@ export interface RunsStreamPayload extends RunsInvokePayload {
|
||||
feedbackKeys?: string[];
|
||||
}
|
||||
|
||||
export interface RunsCreatePayload extends RunsInvokePayload {}
|
||||
export interface RunsCreatePayload extends RunsInvokePayload {
|
||||
/**
|
||||
* One of `"values"`, `"messages"`, `"messages-tuple"`, `"updates"`, `"events"`, `"debug"`, `"custom"`.
|
||||
*/
|
||||
streamMode?: StreamMode | Array<StreamMode>;
|
||||
|
||||
/**
|
||||
* Stream output from subgraphs. By default, streams only the top graph.
|
||||
*/
|
||||
streamSubgraphs?: boolean;
|
||||
}
|
||||
|
||||
export interface CronsCreatePayload extends RunsCreatePayload {
|
||||
/**
|
||||
|
||||
@@ -461,6 +461,73 @@ class _CronsOn(
|
||||
Search = types.CronsSearch
|
||||
|
||||
|
||||
class _StoreOn:
|
||||
def __init__(self, auth: Auth) -> None:
|
||||
self._auth = auth
|
||||
|
||||
@typing.overload
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
actions: typing.Optional[
|
||||
typing.Union[
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"],
|
||||
Sequence[
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||
],
|
||||
]
|
||||
] = None,
|
||||
) -> Callable[[AHO], AHO]: ...
|
||||
|
||||
@typing.overload
|
||||
def __call__(self, fn: AHO) -> AHO: ...
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
fn: typing.Optional[AHO] = None,
|
||||
*,
|
||||
actions: typing.Optional[
|
||||
typing.Union[
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"],
|
||||
Sequence[
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||
],
|
||||
]
|
||||
] = None,
|
||||
) -> typing.Union[AHO, Callable[[AHO], AHO]]:
|
||||
"""Register a handler for specific resources and actions.
|
||||
|
||||
Can be used as a decorator or with explicit resource/action parameters:
|
||||
|
||||
@auth.on.store
|
||||
async def handler(): ... # Handle all store ops
|
||||
|
||||
@auth.on.store(actions=("put", "get", "search", "delete"))
|
||||
async def handler(): ... # Handle specific store ops
|
||||
|
||||
@auth.on.store.put
|
||||
async def handler(): ... # Handle store.put ops
|
||||
"""
|
||||
if fn is not None:
|
||||
# Used as a plain decorator
|
||||
_register_handler(self._auth, "store", None, fn)
|
||||
return fn
|
||||
|
||||
# Used with parameters, return a decorator
|
||||
def decorator(
|
||||
handler: AHO,
|
||||
) -> AHO:
|
||||
if isinstance(actions, str):
|
||||
action_list = [actions]
|
||||
else:
|
||||
action_list = list(actions) if actions is not None else ["*"]
|
||||
for action in action_list:
|
||||
_register_handler(self._auth, "store", action, handler)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
AHO = typing.TypeVar("AHO", bound=_ActionHandler[dict[str, typing.Any]])
|
||||
|
||||
|
||||
@@ -524,6 +591,7 @@ class _On:
|
||||
"threads",
|
||||
"runs",
|
||||
"crons",
|
||||
"store",
|
||||
"value",
|
||||
)
|
||||
|
||||
@@ -532,6 +600,7 @@ class _On:
|
||||
self.assistants = _AssistantsOn(auth, "assistants")
|
||||
self.threads = _ThreadsOn(auth, "threads")
|
||||
self.crons = _CronsOn(auth, "crons")
|
||||
self.store = _StoreOn(auth)
|
||||
self.value = dict[str, typing.Any]
|
||||
|
||||
@typing.overload
|
||||
|
||||
@@ -5,7 +5,7 @@ request handling in LangGraph. It includes user protocols, authentication contex
|
||||
and typed dictionaries for various API operations.
|
||||
|
||||
Note:
|
||||
All typing.TypedDict classes use total=False to make all fields optional by default.
|
||||
All typing.TypedDict classes use total=False to make all fields typing.Optional by default.
|
||||
"""
|
||||
|
||||
import functools
|
||||
@@ -157,7 +157,7 @@ class MinimalUserDict(typing.TypedDict, total=False):
|
||||
identity: typing_extensions.Required[str]
|
||||
"""The required unique identifier for the user."""
|
||||
display_name: str
|
||||
"""The optional display name for the user."""
|
||||
"""The typing.Optional display name for the user."""
|
||||
is_authenticated: bool
|
||||
"""Whether the user is authenticated. Defaults to True."""
|
||||
permissions: Sequence[str]
|
||||
@@ -358,11 +358,34 @@ class AuthContext(BaseAuthContext):
|
||||
allowing for fine-grained access control decisions.
|
||||
"""
|
||||
|
||||
resource: typing.Literal["runs", "threads", "crons", "assistants"]
|
||||
resource: typing.Literal["runs", "threads", "crons", "assistants", "store"]
|
||||
"""The resource being accessed."""
|
||||
|
||||
action: typing.Literal["create", "read", "update", "delete", "search", "create_run"]
|
||||
"""The action being performed on the resource."""
|
||||
action: typing.Literal[
|
||||
"create",
|
||||
"read",
|
||||
"update",
|
||||
"delete",
|
||||
"search",
|
||||
"create_run",
|
||||
"put",
|
||||
"get",
|
||||
"list_namespaces",
|
||||
]
|
||||
"""The action being performed on the resource.
|
||||
|
||||
Most resources support the following actions:
|
||||
- create: Create a new resource
|
||||
- read: Read information about a resource
|
||||
- update: Update an existing resource
|
||||
- delete: Delete a resource
|
||||
- search: Search for resources
|
||||
|
||||
The store supports the following actions:
|
||||
- put: Add or update a document in the store
|
||||
- get: Get a document from the store
|
||||
- list_namespaces: List the namespaces in the store
|
||||
"""
|
||||
|
||||
|
||||
class ThreadsCreate(typing.TypedDict, total=False):
|
||||
@@ -759,6 +782,84 @@ class CronsSearch(typing.TypedDict, total=False):
|
||||
"""Offset for pagination."""
|
||||
|
||||
|
||||
class StoreGet(typing.TypedDict):
|
||||
"""Operation to retrieve a specific item by its namespace and key."""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Hierarchical path that uniquely identifies the item's location."""
|
||||
|
||||
key: str
|
||||
"""Unique identifier for the item within its specific namespace."""
|
||||
|
||||
|
||||
class StoreSearch(typing.TypedDict):
|
||||
"""Operation to search for items within a specified namespace hierarchy."""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Prefix filter for defining the search scope."""
|
||||
|
||||
filter: typing.Optional[dict[str, typing.Any]]
|
||||
"""Key-value pairs for filtering results based on exact matches or comparison operators."""
|
||||
|
||||
limit: int
|
||||
"""Maximum number of items to return in the search results."""
|
||||
|
||||
offset: int
|
||||
"""Number of matching items to skip for pagination."""
|
||||
|
||||
query: typing.Optional[str]
|
||||
"""Naturalj language search query for semantic search capabilities."""
|
||||
|
||||
|
||||
class StoreListNamespaces(typing.TypedDict):
|
||||
"""Operation to list and filter namespaces in the store."""
|
||||
|
||||
namespace: typing.Optional[tuple[str, ...]]
|
||||
"""Prefix filter namespaces."""
|
||||
|
||||
suffix: typing.Optional[tuple[str, ...]]
|
||||
"""Optional conditions for filtering namespaces."""
|
||||
|
||||
max_depth: typing.Optional[int]
|
||||
"""Maximum depth of namespace hierarchy to return.
|
||||
|
||||
Note:
|
||||
Namespaces deeper than this level will be truncated.
|
||||
"""
|
||||
|
||||
limit: int
|
||||
"""Maximum number of namespaces to return."""
|
||||
|
||||
offset: int
|
||||
"""Number of namespaces to skip for pagination."""
|
||||
|
||||
|
||||
class StorePut(typing.TypedDict):
|
||||
"""Operation to store, update, or delete an item in the store."""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Hierarchical path that identifies the location of the item."""
|
||||
|
||||
key: str
|
||||
"""Unique identifier for the item within its namespace."""
|
||||
|
||||
value: typing.Optional[dict[str, typing.Any]]
|
||||
"""The data to store, or None to mark the item for deletion."""
|
||||
|
||||
index: typing.Optional[typing.Union[typing.Literal[False], list[str]]]
|
||||
"""Optional index configuration for full-text search."""
|
||||
|
||||
|
||||
class StoreDelete(typing.TypedDict):
|
||||
"""Operation to delete an item from the store."""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Hierarchical path that uniquely identifies the item's location."""
|
||||
|
||||
key: str
|
||||
"""Unique identifier for the item within its specific namespace."""
|
||||
|
||||
|
||||
class on:
|
||||
"""Namespace for type definitions of different API operations.
|
||||
|
||||
@@ -894,6 +995,38 @@ class on:
|
||||
|
||||
value = CronsSearch
|
||||
|
||||
class store:
|
||||
"""Types for store-related operations."""
|
||||
|
||||
value = typing.Union[
|
||||
StoreGet, StoreSearch, StoreListNamespaces, StorePut, StoreDelete
|
||||
]
|
||||
|
||||
class put:
|
||||
"""Type for store put parameters."""
|
||||
|
||||
value = StorePut
|
||||
|
||||
class get:
|
||||
"""Type for store get parameters."""
|
||||
|
||||
value = StoreGet
|
||||
|
||||
class search:
|
||||
"""Type for store search parameters."""
|
||||
|
||||
value = StoreSearch
|
||||
|
||||
class delete:
|
||||
"""Type for store delete parameters."""
|
||||
|
||||
value = StoreDelete
|
||||
|
||||
class list_namespaces:
|
||||
"""Type for store list namespaces parameters."""
|
||||
|
||||
value = StoreListNamespaces
|
||||
|
||||
|
||||
__all__ = [
|
||||
"on",
|
||||
@@ -909,4 +1042,9 @@ __all__ = [
|
||||
"AssistantsUpdate",
|
||||
"AssistantsDelete",
|
||||
"AssistantsSearch",
|
||||
"StoreGet",
|
||||
"StoreSearch",
|
||||
"StoreListNamespaces",
|
||||
"StorePut",
|
||||
"StoreDelete",
|
||||
]
|
||||
|
||||
@@ -1779,7 +1779,7 @@ class RunsClient:
|
||||
|
||||
Args:
|
||||
thread_id: The thread ID to cancel.
|
||||
run_id: The run ID to cancek.
|
||||
run_id: The run ID to cancel.
|
||||
wait: Whether to wait until run has completed.
|
||||
action: Action to take when cancelling the run. Possible values
|
||||
are `interrupt` or `rollback`. Default is `interrupt`.
|
||||
@@ -3917,7 +3917,7 @@ class SyncRunsClient:
|
||||
|
||||
Args:
|
||||
thread_id: The thread ID to cancel.
|
||||
run_id: The run ID to cancek.
|
||||
run_id: The run ID to cancel.
|
||||
wait: Whether to wait until run has completed.
|
||||
action: Action to take when cancelling the run. Possible values
|
||||
are `interrupt` or `rollback`. Default is `interrupt`.
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
"""Data models for interacting with the LangGraph API."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Literal, NamedTuple, Optional, Sequence, TypedDict, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
Json = Optional[dict[str, Any]]
|
||||
"""Represents a JSON-like structure, which can be None or a dictionary with string keys and any values."""
|
||||
@@ -374,5 +384,5 @@ class Send(TypedDict):
|
||||
|
||||
class Command(TypedDict, total=False):
|
||||
goto: Union[Send, str, Sequence[Union[Send, str]]]
|
||||
update: dict[str, Any]
|
||||
update: Union[dict[str, Any], Sequence[Tuple[str, Any]]]
|
||||
resume: Any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.48"
|
||||
version = "0.1.51"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Generated
+56
-36
@@ -2251,13 +2251,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
version = "3.1.5"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
|
||||
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2862,21 +2862,21 @@ adal = ["adal (>=1.0.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "0.3.9"
|
||||
version = "0.3.14"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langchain-0.3.9-py3-none-any.whl", hash = "sha256:ade5a1fee2f94f2e976a6c387f97d62cc7f0b9f26cfe0132a41d2bda761e1045"},
|
||||
{file = "langchain-0.3.9.tar.gz", hash = "sha256:4950c4ad627d0aa95ce6bda7de453e22059b7e7836b562a8f781fb0b05d7294c"},
|
||||
{file = "langchain-0.3.14-py3-none-any.whl", hash = "sha256:5df9031702f7fe6c956e84256b4639a46d5d03a75be1ca4c1bc9479b358061a2"},
|
||||
{file = "langchain-0.3.14.tar.gz", hash = "sha256:4a5ae817b5832fa0e1fcadc5353fbf74bebd2f8e550294d4dc039f651ddcd3d1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.8.3,<4.0.0"
|
||||
async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
|
||||
langchain-core = ">=0.3.21,<0.4.0"
|
||||
langchain-text-splitters = ">=0.3.0,<0.4.0"
|
||||
langsmith = ">=0.1.17,<0.2.0"
|
||||
langchain-core = ">=0.3.29,<0.4.0"
|
||||
langchain-text-splitters = ">=0.3.3,<0.4.0"
|
||||
langsmith = ">=0.1.17,<0.3"
|
||||
numpy = [
|
||||
{version = ">=1.22.4,<2", markers = "python_version < \"3.12\""},
|
||||
{version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""},
|
||||
@@ -2906,45 +2906,46 @@ pydantic = ">=2.7.4,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-community"
|
||||
version = "0.3.1"
|
||||
version = "0.3.14"
|
||||
description = "Community contributed LangChain integrations."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langchain_community-0.3.1-py3-none-any.whl", hash = "sha256:627eb26c16417764762ac47dd0d3005109f750f40242a88bb8f2958b798bcf90"},
|
||||
{file = "langchain_community-0.3.1.tar.gz", hash = "sha256:c964a70628f266a61647e58f2f0434db633d4287a729f100a81dd8b0654aec93"},
|
||||
{file = "langchain_community-0.3.14-py3-none-any.whl", hash = "sha256:cc02a0abad0551edef3e565dff643386a5b2ee45b933b6d883d4a935b9649f3c"},
|
||||
{file = "langchain_community-0.3.14.tar.gz", hash = "sha256:d8ba0fe2dbb5795bff707684b712baa5ee379227194610af415ccdfdefda0479"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.8.3,<4.0.0"
|
||||
dataclasses-json = ">=0.5.7,<0.7"
|
||||
langchain = ">=0.3.1,<0.4.0"
|
||||
langchain-core = ">=0.3.6,<0.4.0"
|
||||
langsmith = ">=0.1.125,<0.2.0"
|
||||
httpx-sse = ">=0.4.0,<0.5.0"
|
||||
langchain = ">=0.3.14,<0.4.0"
|
||||
langchain-core = ">=0.3.29,<0.4.0"
|
||||
langsmith = ">=0.1.125,<0.3"
|
||||
numpy = [
|
||||
{version = ">=1,<2", markers = "python_version < \"3.12\""},
|
||||
{version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.22.4,<2", markers = "python_version < \"3.12\""},
|
||||
{version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
pydantic-settings = ">=2.4.0,<3.0.0"
|
||||
PyYAML = ">=5.3"
|
||||
requests = ">=2,<3"
|
||||
SQLAlchemy = ">=1.4,<3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.23"
|
||||
version = "0.3.29"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langchain_core-0.3.23-py3-none-any.whl", hash = "sha256:550c0b996990830fa6515a71a1192a8a0343367999afc36d4ede14222941e420"},
|
||||
{file = "langchain_core-0.3.23.tar.gz", hash = "sha256:f9e175e3b82063cc3b160c2ca2b155832e1c6f915312e1204828f97d4aabf6e1"},
|
||||
{file = "langchain_core-0.3.29-py3-none-any.whl", hash = "sha256:817db1474871611a81105594a3e4d11704949661008e455a10e38ca9ff601a1a"},
|
||||
{file = "langchain_core-0.3.29.tar.gz", hash = "sha256:773d6aeeb612e7ce3d996c0be403433d8c6a91e77bbb7a7461c13e15cfbe5b06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsonpatch = ">=1.33,<2.0"
|
||||
langsmith = ">=0.1.125,<0.2.0"
|
||||
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\""},
|
||||
@@ -3021,21 +3022,21 @@ tiktoken = ">=0.7,<1"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-text-splitters"
|
||||
version = "0.3.0"
|
||||
version = "0.3.5"
|
||||
description = "LangChain text splitting utilities"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langchain_text_splitters-0.3.0-py3-none-any.whl", hash = "sha256:e84243e45eaff16e5b776cd9c81b6d07c55c010ebcb1965deb3d1792b7358e83"},
|
||||
{file = "langchain_text_splitters-0.3.0.tar.gz", hash = "sha256:f9fe0b4d244db1d6de211e7343d4abc4aa90295aa22e1f0c89e51f33c55cd7ce"},
|
||||
{file = "langchain_text_splitters-0.3.5-py3-none-any.whl", hash = "sha256:8c9b059827438c5fa8f327b4df857e307828a5ec815163c9b5c9569a3e82c8ee"},
|
||||
{file = "langchain_text_splitters-0.3.5.tar.gz", hash = "sha256:11cb7ca3694e5bdd342bc16d3875b7f7381651d4a53cbb91d34f22412ae16443"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.3.0,<0.4.0"
|
||||
langchain-core = ">=0.3.29,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.2.59"
|
||||
version = "0.2.61"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
optional = false
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
@@ -3053,7 +3054,7 @@ url = "libs/langgraph"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.8"
|
||||
version = "2.0.9"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3087,7 +3088,7 @@ pymongo = ">=4.9.0,<4.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.8"
|
||||
version = "2.0.9"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3123,7 +3124,7 @@ url = "libs/checkpoint-sqlite"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.43"
|
||||
version = "0.1.49"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3140,23 +3141,28 @@ url = "libs/sdk-py"
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.1.129"
|
||||
version = "0.2.10"
|
||||
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langsmith-0.1.129-py3-none-any.whl", hash = "sha256:31393fbbb17d6be5b99b9b22d530450094fab23c6c37281a6a6efb2143d05347"},
|
||||
{file = "langsmith-0.1.129.tar.gz", hash = "sha256:6c3ba66471bef41b9f87da247cc0b493268b3f54656f73648a256a205261b6a0"},
|
||||
{file = "langsmith-0.2.10-py3-none-any.whl", hash = "sha256:b02f2f174189ff72e54c88b1aa63343defd6f0f676c396a690c63a4b6495dcc2"},
|
||||
{file = "langsmith-0.2.10.tar.gz", hash = "sha256:153c7b3ccbd823528ff5bec84801e7e50a164e388919fc583252df5b27dd7830"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
httpx = ">=0.23.0,<1"
|
||||
orjson = ">=3.9.14,<4.0.0"
|
||||
orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""}
|
||||
pydantic = [
|
||||
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
|
||||
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
|
||||
]
|
||||
requests = ">=2,<3"
|
||||
requests-toolbelt = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
compression = ["zstandard (>=0.23.0,<0.24.0)"]
|
||||
langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
@@ -5965,6 +5971,20 @@ requests = ">=2.0.0"
|
||||
[package.extras]
|
||||
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "1.0.0"
|
||||
description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
requests = ">=2.0.1,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "rfc3339-validator"
|
||||
version = "0.1.4"
|
||||
@@ -7485,4 +7505,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "367f5fb480a8fa5d8ab1c0964a1e9450dbb28e6998097e7536966e7a5fe30c90"
|
||||
content-hash = "981f40de9c31530b17537a089651f9e51901b945fbc01b43ac33a466c8a7d9eb"
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ langchain-fireworks = "^0.2.0"
|
||||
langchain-community = "^0.3.0"
|
||||
langchain-experimental = "^0.3.2"
|
||||
langgraph-checkpoint-mongodb = "^0.1.0"
|
||||
langsmith = "^0.1.129"
|
||||
langsmith = "^0.2.0"
|
||||
chromadb = "^0.5.5"
|
||||
gpt4all = "^2.8.2"
|
||||
scikit-learn = "^1.5.2"
|
||||
|
||||
Reference in New Issue
Block a user