mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f26d28ea | ||
|
|
06d4ba7fa0 | ||
|
|
2dbdb36743 | ||
|
|
2b72fbd5de | ||
|
|
c681545c97 | ||
|
|
5ebdefba63 | ||
|
|
441923282c |
@@ -56,6 +56,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: langchain-ai/langchainjs
|
||||
token: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
||||
path: docs/langchainjs
|
||||
|
||||
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
@@ -72,6 +78,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd langchainjs && yarn && yarn build && cd ..
|
||||
yarn
|
||||
poetry install --with test --with docs --no-root
|
||||
poetry run pip install -U \
|
||||
|
||||
+873
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.5.1.cjs
|
||||
+17
-3
@@ -13,7 +13,22 @@ build-prebuilt:
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml
|
||||
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
|
||||
|
||||
build-docs: build-typedoc build-prebuilt
|
||||
grab-langgraphjs:
|
||||
if [ -d "langgraphjs" ]; then \
|
||||
if [ ! -d "langgraphjs/.git" ]; then \
|
||||
rm -rf langgraphjs; \
|
||||
fi \
|
||||
fi
|
||||
if [ ! -d "langgraphjs" ]; then \
|
||||
git clone https://github.com/langchain-ai/langgraphjs.git; \
|
||||
else \
|
||||
cd langgraphjs && git checkout main && git pull; \
|
||||
fi
|
||||
cd langgraphjs && yarn
|
||||
cd langgraphjs && yarn build
|
||||
yarn
|
||||
|
||||
build-docs: build-typedoc build-prebuilt grab-langgraphjs
|
||||
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
@@ -32,7 +47,6 @@ install-vercel-deps:
|
||||
poetry run tslab install --python=python3
|
||||
poetry run jupyter kernelspec list
|
||||
|
||||
|
||||
tests:
|
||||
# RUn unit tests
|
||||
poetry run pytest tests/unit_tests
|
||||
@@ -45,7 +59,7 @@ vercel-build-docs: install-vercel-deps
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
serve-docs: build-typedoc grab-langgraphjs
|
||||
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
|
||||
@@ -24,14 +24,16 @@ function decompressData(compressedString: string): NockCassetteData {
|
||||
return msgpack.decode(decompressed) as NockCassetteData;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-unused-vars
|
||||
class HashedCassette {
|
||||
hash: string;
|
||||
private recording = true;
|
||||
|
||||
constructor(
|
||||
private readonly cassettePath: string,
|
||||
private readonly hash: string
|
||||
) {}
|
||||
hash: string
|
||||
) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
async enter() {
|
||||
try {
|
||||
|
||||
@@ -63,7 +63,7 @@ class HashedCassette:
|
||||
content = f.read()
|
||||
try:
|
||||
cassette_data = serializer.deserialize(content)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
os.remove(self.cassette_path)
|
||||
else:
|
||||
existing_hash = cassette_data.get("cassette_hash")
|
||||
@@ -96,7 +96,7 @@ class HashedCassette:
|
||||
content = f.read()
|
||||
try:
|
||||
cassette_data = serializer.deserialize(content)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return result
|
||||
# Update the cassette data with the expected hash.
|
||||
if cassette_data.get("cassette_hash") != self.hash_value:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
hook_state = {
|
||||
"document_filename": "__UNKNOWN__",
|
||||
"document_content": "__UNKNOWN__",
|
||||
}
|
||||
@@ -6,14 +6,17 @@ import traceback
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from markdown import Markdown
|
||||
from markdown_exec.hooks import SessionHistoryEntry
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
from pymdownx.superfences import SuperFencesException
|
||||
|
||||
from _scripts.hook_state import hook_state
|
||||
from markdown_exec.hooks import SessionHistoryEntry
|
||||
|
||||
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
from _scripts.setup_vcr import load_postamble, load_preamble, _hash_string
|
||||
from _scripts.setup_vcr import get_hash_for_session, load_postamble, load_preamble, _hash_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -62,29 +65,6 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
|
||||
return new_files
|
||||
|
||||
|
||||
def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
|
||||
"""Add the path to the code blocks."""
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\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_code_block_header(match: re.Match) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
if 'exec="on"' not in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
code = match.group("code")
|
||||
return f'{indent}```{language} {attributes} path="{page.file.src_path}"\n{code}{indent}```'
|
||||
|
||||
return code_block_pattern.sub(replace_code_block_header, markdown)
|
||||
|
||||
|
||||
def _highlight_code_blocks(markdown: str) -> str:
|
||||
"""Find code blocks with highlight comments and add hl_lines attribute.
|
||||
|
||||
@@ -162,7 +142,6 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
|
||||
return markdown
|
||||
|
||||
|
||||
def handle_vcr_setup(
|
||||
*,
|
||||
formatter: Callable,
|
||||
@@ -174,26 +153,25 @@ def handle_vcr_setup(
|
||||
**kwargs: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle VCR setup in markdown content if necessary."""
|
||||
logger.info(f"handle_vcr_setup: {hook_state['document_filename']}")
|
||||
try:
|
||||
if kwargs.get("extra", None) is None:
|
||||
if hook_state['document_filename'] == '__UNKNOWN__':
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: extra dict is required"
|
||||
f"error while processing {language} block: document filename hasn't been set yet"
|
||||
)
|
||||
|
||||
if kwargs["extra"].get("path", None) is None:
|
||||
|
||||
if hook_state['document_content'] == '__UNKNOWN__':
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: path is required"
|
||||
f"error while processing {language} block: document content hasn't been set yet"
|
||||
)
|
||||
|
||||
document_filename = kwargs["extra"]["path"]
|
||||
|
||||
if session is None or session == "" and id is None or id == "":
|
||||
id = _hash_string(code)
|
||||
|
||||
if session is not None and session != "":
|
||||
logger.info(f"new {language} session {session} on page {document_filename}")
|
||||
logger.info(f"new {language} session {session} on page {hook_state['document_filename']}")
|
||||
|
||||
cassette_prefix = document_filename.replace(".md", "").replace(os.path.sep, "_")
|
||||
cassette_prefix = hook_state['document_filename'].replace(".md", "").replace(os.path.sep, "_")
|
||||
|
||||
cassette_dir = os.path.abspath(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "cassettes")
|
||||
@@ -208,14 +186,15 @@ def handle_vcr_setup(
|
||||
|
||||
# Add context manager at start with explicit __enter__ and __exit__ calls
|
||||
|
||||
hash_ = get_hash_for_session(language, session, hook_state['document_content'])
|
||||
wrapped_lines = [
|
||||
load_preamble(language, code, cassette_name),
|
||||
load_preamble(language, hash_, cassette_name),
|
||||
code,
|
||||
]
|
||||
|
||||
if session is None or session == "":
|
||||
logger.info(
|
||||
f"no session, adding postamble for {language} in {document_filename}"
|
||||
f"no session, adding postamble for {language} in {hook_state['document_filename']}"
|
||||
)
|
||||
wrapped_lines.append(load_postamble(language))
|
||||
|
||||
@@ -234,7 +213,7 @@ def handle_vcr_setup(
|
||||
return dict(
|
||||
transform_source=lambda code: (transformed_source, code),
|
||||
id=id,
|
||||
extra=keep_extras,
|
||||
extra={ **keep_extras, "path": hook_state['document_filename'] },
|
||||
)
|
||||
except Exception as e:
|
||||
raise SuperFencesException(traceback.format_exc()) from e
|
||||
@@ -247,24 +226,17 @@ def handle_vcr_teardown(
|
||||
session: str,
|
||||
history: list[SessionHistoryEntry],
|
||||
):
|
||||
last_inputs = dict(history[-1].inputs)
|
||||
code = load_postamble(language)
|
||||
md = last_inputs["md"]
|
||||
html = False
|
||||
update_toc = False
|
||||
|
||||
document_filename = last_inputs.get("extra", {}).get("path", None)
|
||||
|
||||
if document_filename is None:
|
||||
logger.warning(f"no document filename found while tearing down {session}!")
|
||||
else:
|
||||
logger.info(f"tearing down {language} {session} on {document_filename}")
|
||||
logger.info(f"tearing down {language} {session} on {hook_state['document_filename']}")
|
||||
|
||||
kwargs = dict(
|
||||
code=code,
|
||||
session=session,
|
||||
id=f"{id}_vcr_end",
|
||||
md=md,
|
||||
md=None, # md is unused by the formatter, but it's a required argument
|
||||
html=html,
|
||||
update_toc=update_toc,
|
||||
extra={},
|
||||
@@ -296,11 +268,6 @@ def _on_page_markdown_with_config(
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
# Add file path as an attribute to code blocks that are executable.
|
||||
# This file path is used to associate fixtures with the executable code
|
||||
# which can be used in CI to test the docs without making network requests.
|
||||
markdown = _add_path_to_code_blocks(markdown, page)
|
||||
|
||||
if remove_base64_images:
|
||||
# Remove base64 encoded images from markdown
|
||||
markdown = re.sub(r"!\[.*?\]\(data:image/+;base64,[^\)]+\)", "", markdown)
|
||||
@@ -309,6 +276,8 @@ def _on_page_markdown_with_config(
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
logger.info(f"on_page_markdown: {page.file.src_path}")
|
||||
hook_state['document_filename'] = page.file.src_path
|
||||
return _on_page_markdown_with_config(
|
||||
markdown,
|
||||
page,
|
||||
@@ -370,3 +339,9 @@ def on_post_build(config):
|
||||
+ suffix
|
||||
)
|
||||
write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
def on_pre_page(page: Page, **kwargs: Dict[str, Any]):
|
||||
logger.info(f"on_pre_page: {page.file.src_path}")
|
||||
hook_state['document_filename'] = page.file.src_path
|
||||
hook_state['document_content'] = page.file.content_string
|
||||
return page
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from textwrap import dedent
|
||||
import re
|
||||
from textwrap import dedent, indent
|
||||
|
||||
from mistune import BlockParser, BlockState, Markdown, create_markdown
|
||||
from mistune.renderers.markdown import MarkdownRenderer
|
||||
|
||||
preambles = {
|
||||
"python": "vcr_setup_preamble.py",
|
||||
@@ -50,21 +55,19 @@ preamble_cleanups = {
|
||||
}
|
||||
|
||||
|
||||
def load_preamble(language: str, code: str, cassette_name: str) -> str:
|
||||
def load_preamble(language: str, hash_: str, cassette_name: str) -> str:
|
||||
"""Load the source code for the preamble for a given language."""
|
||||
_assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
||||
|
||||
preamble_path = os.path.join(_assets_dir, preambles[language])
|
||||
with open(preamble_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
hash_ = _hash_string(code)
|
||||
lines.append(preamble_inits[language](cassette_name, hash_))
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def load_postamble(language: str) -> str:
|
||||
"""Load the source code for the postamble for a given language."""
|
||||
|
||||
return preamble_cleanups[language]()
|
||||
|
||||
|
||||
@@ -75,3 +78,63 @@ def _hash_string(input_string: str) -> str:
|
||||
sha256_hash = hashlib.sha256(encoded_string)
|
||||
# Get the hexadecimal digest of the hash
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
def extract_code_blocks_for_session(language: str, session: str, content: str) -> str:
|
||||
code_blocks_for_session = []
|
||||
|
||||
TAB_REGEX = r"^===!? \"(?P<title>[^\"]+)\"\n(?P<content>(?:(?P<indent> )+[^\n]*\n)+)"
|
||||
def parse_tabs(block: BlockParser, m: re.Match, state: BlockState) -> str:
|
||||
state.append_token(
|
||||
{
|
||||
"raw": m.group(0),
|
||||
"type": "block_tab",
|
||||
"attrs": {
|
||||
"title": m.group("title"),
|
||||
"level": len(m.group("indent")) // 4,
|
||||
"content": dedent(m.group("content")).strip(),
|
||||
},
|
||||
}
|
||||
)
|
||||
return m.end()
|
||||
|
||||
def render_tabs(self, token: dict, state: BlockState):
|
||||
recursive_transformer = create_markdown(renderer=DocumentRenderer())
|
||||
recursive_transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
|
||||
recursive_transformer.renderer.register("block_tab", render_tabs)
|
||||
return (
|
||||
f'=== "{token["attrs"]["title"]}"\n'
|
||||
f'{indent(recursive_transformer(token["attrs"]["content"]), " " * token["attrs"]["level"])}\n'
|
||||
)
|
||||
|
||||
class DocumentRenderer(MarkdownRenderer):
|
||||
def block_code(self, token: dict, state: BlockState):
|
||||
if token["style"] == "fenced":
|
||||
if token["attrs"]["info"]:
|
||||
attributes = {}
|
||||
block_language = token["attrs"]["info"].split()[0]
|
||||
for match in re.finditer(r'(?P<key>\w+)=(?:(?P<value>(?:[\w]+))|"(?P<value_quoted>(?:[^"\s]+))")', token["attrs"]["info"]):
|
||||
attributes[match.group("key")] = match.group("value") or match.group("value_quoted")
|
||||
if block_language == language and "session" in attributes and attributes["session"] == session:
|
||||
code_blocks_for_session.append(token["raw"].rstrip())
|
||||
return super().block_code(token, state)
|
||||
|
||||
transformer: Markdown = create_markdown(renderer=DocumentRenderer())
|
||||
transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
|
||||
transformer.renderer.register("block_tab", render_tabs)
|
||||
|
||||
# Parses the page content, which causes the code blocks to be added to the code_blocks_for_session list.
|
||||
# There's probably some way to do this by using the renderer as a filter, but I would've had to NO-OP
|
||||
# all of the default behavior, and this was easier.
|
||||
transformer(content)
|
||||
|
||||
return code_blocks_for_session
|
||||
|
||||
|
||||
def get_hash_for_session(language: str, session: str, content: str) -> str:
|
||||
# include the preamble in the hash so we invalidate if it changes
|
||||
preamble_hash = _hash_string(load_preamble(language, session, "test"))
|
||||
|
||||
code_blocks_for_session = [preamble_hash, *extract_code_blocks_for_session(language, session, content)]
|
||||
|
||||
return _hash_string("\n".join(code_blocks_for_session))
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12,9 +12,9 @@ below. These libraries can extend LangGraph's functionality in various ways.
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
| Name | GitHub URL | Description | Weekly Downloads |
|
||||
| --- | --- | --- | --- |
|
||||
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 8803 |
|
||||
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 636 |
|
||||
| **breeze-agent** | [andrestorres123/breeze-agent](https://github.com/andrestorres123/breeze-agent) | A streamlined research system built inspired on STORM and built on LangGraph | 184 |
|
||||
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 11189 |
|
||||
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 1291 |
|
||||
| **breeze-agent** | [andrestorres123/breeze-agent](https://github.com/andrestorres123/breeze-agent) | A streamlined research system built inspired on STORM and built on LangGraph | 226 |
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
|
||||
|
||||
+8
-1
@@ -2,11 +2,18 @@
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.5.1",
|
||||
"scripts": {
|
||||
"build": "echo 'export OPENAI_API_KEY=\"sk-proj-1234567890\"' >> ~/.bashrc && echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-1234567890\"' >> ~/.bashrc && echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' >> ~/.bashrc && source ~/.bashrc && make vercel-build-docs"
|
||||
"build": "make build-docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.38",
|
||||
"@langchain/langgraph": "portal:./langgraphjs/libs/langgraph",
|
||||
"@langchain/langgraph-checkpoint": "portal:./langgraphjs/libs/checkpoint",
|
||||
"@langchain/langgraph-checkpoint-mongodb": "portal:./langgraphjs/libs/checkpoint-mongodb",
|
||||
"@langchain/langgraph-checkpoint-postgres": "portal:./langgraphjs/libs/checkpoint-postgres",
|
||||
"@langchain/langgraph-checkpoint-sqlite": "portal:./langgraphjs/libs/checkpoint-sqlite",
|
||||
"@langchain/langgraph-checkpoint-validation": "portal:./langgraphjs/libs/checkpoint-validation",
|
||||
"@langchain/openai": "^0.4.2",
|
||||
"msgpack-lite": "^0.1.26",
|
||||
"nock": "^14.0.1"
|
||||
|
||||
Generated
+60
-6
@@ -1016,12 +1016,12 @@ version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
markers = {docs = "python_version <= \"3.11\" or python_version >= \"3.12\"", test = "(platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\") and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
|
||||
markers = {main = "sys_platform == \"win32\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", docs = "python_version <= \"3.11\" or python_version >= \"3.12\"", test = "(platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\") and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
|
||||
|
||||
[[package]]
|
||||
name = "coloredlogs"
|
||||
@@ -1416,7 +1416,7 @@ version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
@@ -2462,6 +2462,19 @@ enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "6.29.5"
|
||||
@@ -5234,7 +5247,7 @@ version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
@@ -5522,6 +5535,23 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
|
||||
type = ["mypy (>=1.11.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "posthog"
|
||||
version = "3.12.1"
|
||||
@@ -6382,6 +6412,30 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["build", "flake8", "mypy", "pytest", "twine"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -7755,7 +7809,7 @@ version = "2.2.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
|
||||
@@ -8634,4 +8688,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "06debb82135affdb2baf1fdcc028c062c236121508d787588cd0de1db2da11e4"
|
||||
content-hash = "cbcc30bb9bdead3545070eedf47fb83fe971d4914613e2188323fb3e91035e76"
|
||||
|
||||
@@ -10,6 +10,7 @@ readme = "README.md"
|
||||
python = "^3.10"
|
||||
aiohappyeyeballs = "2.4.3"
|
||||
hub = "^3.0.1"
|
||||
pytest = "^8.3.4"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
langgraph = { path = "../libs/langgraph/", develop = true }
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from _scripts.hook_state import hook_state
|
||||
from _scripts.notebook_hooks import handle_vcr_setup
|
||||
from _scripts.setup_vcr import extract_code_blocks_for_session, get_hash_for_session
|
||||
|
||||
INITIAL_DOCUMENT_CONTENT = dedent(
|
||||
"""
|
||||
Blah blah blah
|
||||
|
||||
```python exec="on" source="above" session="1" result="ansi"
|
||||
print("FIRST_CODE_BLOCK")
|
||||
```
|
||||
|
||||
Blah blah blah!
|
||||
|
||||
```python exec="on" source="above" session="1" result="ansi"
|
||||
print("SECOND_CODE_BLOCK")
|
||||
```
|
||||
|
||||
more blah blah blah
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"replace_string",
|
||||
[
|
||||
"FIRST_CODE_BLOCK",
|
||||
"SECOND_CODE_BLOCK",
|
||||
],
|
||||
)
|
||||
def test_changing_block_in_session_invalidates_hash(replace_string: str):
|
||||
hook_state['document_filename'] = 'test.md'
|
||||
hook_state['document_content'] = INITIAL_DOCUMENT_CONTENT
|
||||
|
||||
code = "print('Hello, world!')"
|
||||
|
||||
result1 = handle_vcr_setup(
|
||||
formatter=lambda **kwargs: None,
|
||||
language="python",
|
||||
session="1",
|
||||
id="test",
|
||||
code=code,
|
||||
md=None,
|
||||
extra={},
|
||||
)
|
||||
|
||||
cassette_init_expr = re.compile(r"^_cassette = HashedCassette\('[^']+', '(?P<hash>[^']+)'\)$")
|
||||
assert result1['transform_source']
|
||||
execute_source, display_source = result1['transform_source'](code)
|
||||
assert display_source == code
|
||||
cassette_init_line = [line for line in execute_source.splitlines() if line.startswith("_cassette = HashedCassette(")][0]
|
||||
assert cassette_init_line
|
||||
match = cassette_init_expr.match(cassette_init_line)
|
||||
assert match
|
||||
hash_ = str(match.group('hash'))
|
||||
|
||||
# change the content of the second block of code
|
||||
hook_state['document_content'] = INITIAL_DOCUMENT_CONTENT.replace(replace_string, "world")
|
||||
assert hook_state['document_content'] != INITIAL_DOCUMENT_CONTENT
|
||||
|
||||
result2 = handle_vcr_setup(
|
||||
formatter=lambda **kwargs: None,
|
||||
language="python",
|
||||
session="1",
|
||||
id="test",
|
||||
code=code,
|
||||
md=None,
|
||||
extra={},
|
||||
)
|
||||
|
||||
assert result2['transform_source']
|
||||
execute_source, display_source = result2['transform_source'](code)
|
||||
assert display_source == code
|
||||
cassette_init_line = [line for line in execute_source.splitlines() if line.startswith("_cassette = HashedCassette(")][0]
|
||||
assert cassette_init_line
|
||||
match = cassette_init_expr.match(cassette_init_line)
|
||||
assert match
|
||||
|
||||
# this is the important part
|
||||
assert str(match.group('hash')) != hash_
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"replace_string",
|
||||
[
|
||||
"FIRST_CODE_BLOCK",
|
||||
"SECOND_CODE_BLOCK",
|
||||
],
|
||||
)
|
||||
def test_get_hash_for_session(replace_string: str):
|
||||
hash_ = get_hash_for_session(
|
||||
language="python",
|
||||
session="1",
|
||||
content=INITIAL_DOCUMENT_CONTENT,
|
||||
)
|
||||
|
||||
content = INITIAL_DOCUMENT_CONTENT.replace(replace_string, "world")
|
||||
assert content != INITIAL_DOCUMENT_CONTENT
|
||||
assert get_hash_for_session(
|
||||
language="python",
|
||||
session="1",
|
||||
content=INITIAL_DOCUMENT_CONTENT.replace(replace_string, "world"),
|
||||
) != hash_
|
||||
|
||||
def test_get_code_blocks_for_session():
|
||||
code_blocks = extract_code_blocks_for_session(
|
||||
language="python",
|
||||
session="1",
|
||||
content=INITIAL_DOCUMENT_CONTENT,
|
||||
)
|
||||
assert code_blocks
|
||||
assert len(code_blocks) == 2
|
||||
assert code_blocks[0] == 'print("FIRST_CODE_BLOCK")'
|
||||
assert code_blocks[1] == 'print("SECOND_CODE_BLOCK")'
|
||||
@@ -111,7 +111,7 @@ print("Hello, World!")
|
||||
|
||||
|
||||
END_TO_END_INPUT_HIGHLIGHT_1_EXPECT = """\
|
||||
```python exec="on" source="below" hl_lines="2" path="dummy.md"
|
||||
```python exec="on" source="below" hl_lines="2"
|
||||
print("Hello, World!")
|
||||
print("Hello, World!")
|
||||
```
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"buildCommand": "yarn build",
|
||||
"outputDirectory": "site"
|
||||
"buildCommand": "echo 'export OPENAI_API_KEY=\"sk-proj-1234567890\"' >> ~/.bashrc && echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-1234567890\"' >> ~/.bashrc && echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' >> ~/.bashrc && source ~/.bashrc && make vercel-build-docs",
|
||||
"outputDirectory": "site",
|
||||
"installCommand": "echo done"
|
||||
}
|
||||
|
||||
+4823
-408
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user