mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-25 09:02:25 +02:00
fix cassette hashing
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
hook_state = {
|
||||
"document_filename": "__UNKNOWN__",
|
||||
"document_content": "__UNKNOWN__",
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ 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()
|
||||
@@ -157,7 +157,12 @@ def handle_vcr_setup(
|
||||
try:
|
||||
if hook_state['document_filename'] == '__UNKNOWN__':
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: document filename is unknown"
|
||||
f"error while processing {language} block: document filename hasn't been set yet"
|
||||
)
|
||||
|
||||
if hook_state['document_content'] == '__UNKNOWN__':
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: document content hasn't been set yet"
|
||||
)
|
||||
|
||||
if session is None or session == "" and id is None or id == "":
|
||||
@@ -181,8 +186,9 @@ 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,
|
||||
]
|
||||
|
||||
@@ -220,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
|
||||
|
||||
path = last_inputs.get("extra", {}).get("path", None)
|
||||
|
||||
if path is None:
|
||||
logger.warning(f"no document filename found while tearing down {session}!")
|
||||
else:
|
||||
logger.info(f"tearing down {language} {session} on {path}")
|
||||
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={},
|
||||
@@ -344,4 +343,5 @@ def on_post_build(config):
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user