Skip to content

Commit

Permalink
adding tutorials to notebook checks (#1747)
Browse files Browse the repository at this point in the history
* wip

* actually catch errors

* fixing tool-calling-errors and persistence-redis

* poetry changes

* poetry update

* run tutorials

* only tutorials (testing)

* print errors

* rewoo fixes

* remove customer support because of user input

* skip notebooks programatically

* add back how-tos

* remove redundant if

* add cassetes for tutorials

* remove no execution since it is generated by CI,

* remove multi-agent/usaco

* try to run in parallel

* skip notebooks fix

* remove magic/non-magic cells

* msgpack instead of yaml

* prepare_notebooks change

* ignore msgpack for spelling

* use compression for cassettes

* update spell check

* reset poetry changes

* upgrade packages for latest

* no update

* poetry changes
  • Loading branch information
isahers1 authored Sep 20, 2024
1 parent 10a66ac commit 662eae4
Show file tree
Hide file tree
Showing 322 changed files with 345 additions and 32,249 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
skip: '*.ambr,*.lock,*.ipynb,*.yaml'
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib'
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
# We do this to avoid spellchecking cell outputs
- name: Codespell Notebooks
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/run_notebooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ jobs:
run: |
poetry install --with test
poetry run pip install jupyter
if [ "${{ matrix.lib-version }}" = "latest" ]; then
poetry add chromadb pandas langchain_nomic
poetry remove langchain-openai langchain-anthropic langchain-community
poetry add langchain-openai@latest langchain-anthropic@latest langchain-community@latest
fi
- name: Start services
run: make start-services
Expand All @@ -48,6 +53,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}
run: |
./docs/_scripts/execute_notebooks.sh
Expand Down
37 changes: 32 additions & 5 deletions docs/_scripts/execute_notebooks.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
for file in $(find docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints")
do
echo "Executing $file"
poetry run jupyter execute "$file"
done
#!/bin/bash

# Read the list of notebooks to skip from the JSON file
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('docs/notebooks_no_execution.json'))))")

# Function to execute a single notebook
execute_notebook() {
file="$1"
echo "Starting execution of $file"
start_time=$(date +%s)
if ! output=$(time poetry run jupyter execute --allow-errors "$file" 2>&1); then
end_time=$(date +%s)
execution_time=$((end_time - start_time))
echo "Error in $file. Execution time: $execution_time seconds"
echo "Error details: $output"
exit 1
fi
end_time=$(date +%s)
execution_time=$((end_time - start_time))
echo "Finished $file. Execution time: $execution_time seconds"
}

export -f execute_notebook

# Find all notebooks and filter out those in the skip list
notebooks=$(find docs/docs/tutorials docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))

# Run notebooks in parallel
if ! parallel execute_notebook ::: $notebooks; then
echo "Errors occurred during notebook execution"
exit 1
fi
83 changes: 78 additions & 5 deletions docs/_scripts/prepare_notebooks_for_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import logging
import os

import json
import click
import nbformat

logger = logging.getLogger(__name__)
NOTEBOOK_DIRS = ("docs/docs/how-tos",)
NOTEBOOK_DIRS = ("docs/docs/how-tos","docs/docs/tutorials")
DOCS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CASSETTES_PATH = os.path.join(DOCS_PATH, "cassettes")

Expand All @@ -16,6 +16,11 @@
"docs/docs/how-tos/many-tools.ipynb"
)

NOTEBOOKS_NO_EXECUTION = [
"docs/docs/tutorials/customer-support/customer-support.ipynb",
"docs/docs/tutorials/usaco/usaco.ipynb",
]


def comment_install_cells(notebook: nbformat.NotebookNode) -> nbformat.NotebookNode:
for cell in notebook.cells:
Expand Down Expand Up @@ -71,17 +76,71 @@ def add_vcr_to_notebook(
continue

cell_id = cell.get("id", idx)
cassette_name = f"{cassette_prefix}_{cell_id}.yaml"
cell.source = f"with vcr.use_cassette('{cassette_name}', filter_headers=['x-api-key', 'authorization'], record_mode='once'):\n" + "\n".join(
cassette_name = f"{cassette_prefix}_{cell_id}.msgpack.zlib"
cell.source = f"with custom_vcr.use_cassette('{cassette_name}', filter_headers=['x-api-key', 'authorization'], record_mode='once', serializer='advanced_compressed'):\n" + "\n".join(
f" {line}" for line in lines
)

# Add import statement
vcr_import_lines = [
"import vcr",
# this is needed for ChatAnthropic
"import msgpack",
"import nest_asyncio",
"import base64",
"import zlib",
"import re",
"",
"custom_vcr = vcr.VCR()",
"",
"def compress_data(data, compression_level=9):",
" packed = msgpack.packb(data, use_bin_type=True)",
" compressed = zlib.compress(packed, level=compression_level)",
" return base64.b64encode(compressed).decode('utf-8')",
"",
"def decompress_data(compressed_string):",
" try:",
" decoded = base64.b64decode(compressed_string)",
" decompressed = zlib.decompress(decoded)",
" return msgpack.unpackb(decompressed, raw=False)",
" except (ValueError, zlib.error, msgpack.exceptions.ExtraData, msgpack.exceptions.UnpackValueError):",
" return {\"requests\": [], \"responses\": []}",
"",
"def filter_cassette_data(cassette_dict):",
" for interaction in cassette_dict['interactions']:",
" if len(interaction['response']['body']['string']) > 1000:",
" interaction['response']['body']['string'] = interaction['response']['body']['string'][:1000] + '... (truncated)'",
" for req_or_res in [interaction['request'], interaction['response']]:",
" headers_to_remove = ['date', 'server', 'content-length']",
" for header in headers_to_remove:",
" req_or_res['headers'].pop(header, None)",
" return cassette_dict",
"",
"class AdvancedCompressedSerializer:",
" def serialize(self, cassette_dict):",
" filtered_dict = filter_cassette_data(cassette_dict)",
" return compress_data(filtered_dict)",
"",
" def deserialize(self, cassette_string):",
" return decompress_data(cassette_string)",
"",
"custom_vcr.register_serializer('advanced_compressed', AdvancedCompressedSerializer())",
"",
"def custom_matcher(r1, r2):",
" return (r1.method == r2.method and",
" r1.url == r2.url and",
" normalize_body(r1.body) == normalize_body(r2.body))",
"",
"def normalize_body(body):",
" return re.sub(r'\\s+', '', body.lower()) if body else ''",
"",
"nest_asyncio.apply()",
"",
"custom_vcr.serializer = 'advanced_compressed'",
"custom_vcr.record_mode = 'new_episodes'",
"custom_vcr.match_on = ['custom']",
"custom_vcr.register_matcher('custom', custom_matcher)",
"custom_vcr.filter_headers = ['authorization', 'user-agent', 'date', 'server']",
"custom_vcr.filter_post_data_parameters = ['password', 'token']",
]
import_cell = nbformat.v4.new_code_cell(source="\n".join(vcr_import_lines))
import_cell.pop("id", None)
Expand Down Expand Up @@ -110,10 +169,24 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
notebook, cassette_prefix=cassette_prefix
)

if notebook_path in NOTEBOOKS_NO_EXECUTION:
# Add a cell at the beginning to indicate that this notebook should not be executed
warning_cell = nbformat.v4.new_markdown_cell(
source="**Warning:** This notebook is not meant to be executed automatically."
)
notebook.cells.insert(0, warning_cell)

# Add a special tag to the first code cell
if notebook.cells and notebook.cells[1].cell_type == "code":
notebook.cells[1].metadata["tags"] = notebook.cells[1].metadata.get("tags", []) + ["no_execution"]

nbformat.write(notebook, notebook_path)
logger.info(f"Processed: {notebook_path}")
except Exception as e:
logger.error(f"Error processing {notebook_path}: {e}")

with open(os.path.join(DOCS_PATH, "notebooks_no_execution.json"), "w") as f:
json.dump(NOTEBOOKS_NO_EXECUTION, f)


@click.command()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eNrtVkFv3EQUVsUfGVkcErT2ene92c2iCkVRVKQ2KlILlwZZ4/Fbe5rxjDszjrOJcqD0xmn5BUCipIpa4IC4ABI3OPAHwh/hyht7N4EQVOgRcVjt7nvz3vveN+999tOzPdCGK3nrBZcWNGUW/5hPn55peFKBsc9OC7C5So/vbD08rjS/eDO3tjSTbpeWPDAFt3kgqMxYTrkMmCq6XE7VSaLS2Y9nOdAU0z87f9+A9jcykHb+jTvdxPnlrBsGvaDXH361wRiU1t+STKVcZvOX2QEvOySFqaAWTlv3/GtaloIz6jB2HxslzzeVlNBgnp/vApQ+FXwPnmswJbYBH58aS21lnp5gXvjl57MCjKEZfHH/7hLcJ9+55Mb4mMpqJfwNIVTt39c843L+2QuGZsTt21kJfwXw/Y3B2w1lZv75Wy8ZZTn4rHXPn0vlN5YvNxdp74HMbD4/jobD4w84nZ8jHyRTKhNwtiGs/2CPzS+CfHDbm0TRwHubFPR2f7jeD8Owkw/8/voNjm+vgdqm+478+fFaGN4M+N2WCwR8QW48sKkhRbicCjM/sbqCH64d29ovlYE/JGpG4CPkX+N9/nrrp0NvMWnexAuDUTAIvY6HXAJeUwz7JdcNq7HlBXgTWQnR8RJqWR5jPA5ijBxOeeZNDj3DqIC4KuMnhh9AjBWyDLQ36TlSrrzS5hrhmFhwHDd0ry2dqaplLKEo7ewqOkKvS7c83eS6NMTJzILxJv1wfdQb9sOjjsclDpdkEOOMZsYBwwHHFbIQUx7j9uhZDJImAlJv4jjreEpnMUNQTacpNwvnFGlFr8lVHVsr4oovAyxuJHbIQcdptWAopbOmmlAyc9OOCaIGbK60XRh6EQI0QDXydw1DrfSuKV1aw1QJscPE5R5v2lsiGcTGKo2b8ufoo6O/F4Z3XiUM+EGr6dbTvCtEgUtRlFyA7roVN/Z/zfiXmnHSH43D/7JovPHbodcOTZxTk6Nw9KO1/iAd96PRkPUYQJpEo/XBsM9YFPbSwXiYjEa9iPYH43SchMm4N16HNGKj8Tgarw2nCUpOQSWf4ri5DeI41o+8yylFb6lxKK3BX2ix+LWJX+81xoeoF26wvA9RtxiuGG4n3i+iwptCxBXDjcGI3ZrqVg4Wg4O/H/2jWg9mxkKx3Ua9btE266u6W5zqeK9bxi4jJt4d3B5JKKlwYUkjex3SiBygEQ9JYhUxSuwB4ZbUuMXE5kAqWyhjSUk1FQIEP6AJF9zOArKF094GoqRVIiVOKTQ3mA/rNJtLpoi7STNVbtZwXMihrIrYKiXMEXHozWRHHrr/cQqGaV42rzZHaLw6GJDHisuV1QnZxDyoCgZrNAUTLsEQFIVKoLEphxiUXgAwwY7ckcQnG5Lcu7dNqBMqwg1hrpuUVKglBGVV7TpobRHHA2D7SNOUS1SdA2iZuCSOYP6aIksVro1onI4HBKWBwD6wykIaNHVdyiU/VNT4TCBJm05QpHVBE55ZJuk0ndVcCHdwAdP5a7wdfAxT7M5RRghZoauET5tILF4jOobMY1QK+I5YIDMpSWYko64X1587qSpbVkuqLDW7xvW7fCS2d+W6JkuhDdpaCdYKgoCs4JhJlElIV38HB1H0IA==
Loading

0 comments on commit 662eae4

Please sign in to comment.