diff --git a/README.md b/README.md index 4dc09e6..e37ba58 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,9 @@ from langchain_sandbox import PyodideSandbox # Create a sandbox instance sandbox = PyodideSandbox( - "./sessions", # Directory to store session files - # Allow Pyodide to install python packages that - # might be required. - allow_net=True, + # Allow Pyodide to install python packages that + # might be required. + allow_net=True, ) code = """\ import numpy as np @@ -51,18 +50,47 @@ print(x) """ # Execute Python code -print(await sandbox.execute(code, session_id="123")) +print(await sandbox.execute(code)) # CodeExecutionResult( # result=None, # stdout='[1 2 3]', # stderr=None, # status='success', -# execution_time=2.8578367233276367 +# execution_time=2.8578367233276367, +# session_metadata={'created': '2025-05-15T21:26:37.204Z', 'lastModified': '2025-05-15T21:26:37.831Z', 'packages': ['numpy']}, +# session_bytes=None # ) +``` -# Can still access a previous result! -print(await sandbox.execute("float(x[0])", session_id="123")) +### Stateful Sandbox + +If you want to persist state between code executions (to persist variables, imports, +and definitions, etc.), you can set `stateful=True` in the sandbox. This will return +`session_bytes` and `session_metadata` that you can pass to `.execute()`. + +> [!warning] +> `session_bytes` contains pickled session state. It should not be unpickled +> and is only meant to be used by the sandbox itself + +```python +sandbox = PyodideSandbox( + # Create stateful sandbox + stateful=True, + # Allow Pyodide to install python packages that + # might be required. + allow_net=True, +) +code = """\ +import numpy as np +x = np.array([1, 2, 3]) +print(x) +""" + +result = await sandbox.execute(code) + +# Pass previous result +print(await sandbox.execute("float(x[0])", session_bytes=result.session_bytes, session_metadata=result.session_metadata)) # CodeExecutionResult( # result=1, @@ -70,6 +98,8 @@ print(await sandbox.execute("float(x[0])", session_id="123")) # stderr=None, # status='success', # execution_time=2.7027177810668945 +# session_metadata={'created': '2025-05-15T21:27:57.120Z', 'lastModified': '2025-05-15T21:28:00.061Z', 'packages': ['numpy', 'dill']}, +# session_bytes=b'\x80\x04\x95d\x01\x00..." # ) ``` @@ -84,51 +114,81 @@ tool = PyodideSandboxTool() result = await tool.ainvoke("print('Hello, world!')") ``` -If you want to persist state between code executions (to persist variables, imports, -and definitions, etc.), you need to invoke the tool with `thread_id` in the config: - -```python -code = """\ -import numpy as np -x = np.array([1, 2, 3]) -print(x) -""" -result = await tool.ainvoke( - code, - config={"configurable": {"thread_id": "123"}}, -) - -second_result = await tool.ainvoke( - "print(float(x[0]))", # tool is aware of the previous result - config={"configurable": {"thread_id": "123"}}, -) -``` - ### Using with an agent -You can use `PyodideSandboxTool` inside a LangGraph agent. If you are using this tool inside an agent, you can invoke the agent with a config, and it will automatically be passed to the tool: +You can use sandbox tools inside a LangGraph agent: ```python from langgraph.prebuilt import create_react_agent -from langgraph.checkpoint.memory import InMemorySaver from langchain_sandbox import PyodideSandboxTool -tool = PyodideSandboxTool() +tool = PyodideSandboxTool( + # Allow Pyodide to install python packages that + # might be required. + allow_net=True +) agent = create_react_agent( "anthropic:claude-3-7-sonnet-latest", tools=[tool], - checkpointer=InMemorySaver() ) result = await agent.ainvoke( {"messages": [{"role": "user", "content": "what's 5 + 7?"}]}, +) +``` + +#### Stateful Tool + +> [!important] +> **Stateful** `PyodideSandboxTool` works only in LangGraph agents that use the prebuilt [`create_react_agent`](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.chat_agent_executor.create_react_agent) or [`ToolNode`](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode). + +If you want to persist state between code executions (to persist variables, imports, +and definitions, etc.), you need to set `stateful=True`: + +```python +from langgraph.prebuilt import create_react_agent +from langgraph.prebuilt.chat_agent_executor import AgentState +from langgraph.checkpoint.memory import InMemorySaver +from langchain_sandbox import PyodideSandboxTool, PyodideSandbox + +class State(AgentState): + # important: add session_bytes & session_metadata keys to your graph state schema - + # these keys are required to store the session data between tool invocations. + # `session_bytes` contains pickled session state. It should not be unpickled + # and is only meant to be used by the sandbox itself + session_bytes: bytes + session_metadata: dict + +tool = PyodideSandboxTool( + # Create stateful sandbox + stateful=True, + # Allow Pyodide to install python packages that + # might be required. + allow_net=True +) +agent = create_react_agent( + "anthropic:claude-3-7-sonnet-latest", + tools=[tool], + checkpointer=InMemorySaver(), + state_schema=State +) +result = await agent.ainvoke( + { + "messages": [ + {"role": "user", "content": "what's 5 + 7? save result as 'a'"} + ], + "session_bytes": None, + "session_metadata": None + }, config={"configurable": {"thread_id": "123"}}, ) second_result = await agent.ainvoke( - {"messages": [{"role": "user", "content": "what's the sine of that?"}]}, + {"messages": [{"role": "user", "content": "what's the sine of 'a'?"}]}, config={"configurable": {"thread_id": "123"}}, ) ``` + + See full examples here: * [ReAct agent](examples/react_agent.py) diff --git a/examples/codeact_agent.py b/examples/codeact_agent.py index aea5d7e..09359f6 100644 --- a/examples/codeact_agent.py +++ b/examples/codeact_agent.py @@ -6,24 +6,13 @@ from typing import Any from langchain.chat_models import init_chat_model from langchain_sandbox import PyodideSandbox -from langgraph.checkpoint.memory import MemorySaver from langgraph_codeact import EvalCoroutine, create_codeact -def create_pyodide_eval_fn( - sandbox_dir: str = "./sessions", session_id: str | None = None -) -> EvalCoroutine: +def create_pyodide_eval_fn(sandbox: PyodideSandbox) -> EvalCoroutine: """Create an eval_fn that uses PyodideSandbox. - - Args: - sandbox_dir: Directory to store session files - session_id: ID of the session to use - - Returns: - A function that evaluates code using PyodideSandbox """ - sandbox = PyodideSandbox(sandbox_dir, allow_net=True) async def async_eval_fn( code: str, _locals: dict[str, Any] @@ -54,7 +43,6 @@ execute() # Execute the code and get the result response = await sandbox.execute( code=context_setup + "\n\n" + wrapper_code, - session_id=session_id, ) # Check if execution was successful @@ -162,20 +150,19 @@ tools = [ model = init_chat_model("claude-3-7-sonnet-latest", model_provider="anthropic") -eval_fn = create_pyodide_eval_fn() +sandbox = PyodideSandbox(allow_net=True) +eval_fn = create_pyodide_eval_fn(sandbox) code_act = create_codeact(model, tools, eval_fn) -agent = code_act.compile(checkpointer=MemorySaver()) +agent = code_act.compile() query = """A batter hits a baseball at 45.847 m/s at an angle of 23.474° above the horizontal. The outfielder, who starts facing the batter, picks up the baseball as it lands, then throws it back towards the batter at 24.12 m/s at an angle of 39.12 degrees. How far is the baseball from where the batter originally hit it? Assume zero air resistance.""" -async def run_agent(query: str, thread_id: str): - config = {"configurable": {"thread_id": thread_id}} +async def run_agent(query: str): # Stream agent outputs async for typ, chunk in agent.astream( {"messages": query}, stream_mode=["values", "messages"], - config=config, ): if typ == "messages": print(chunk[0].content, end="") @@ -185,4 +172,4 @@ async def run_agent(query: str, thread_id: str): if __name__ == "__main__": # Run the agent - asyncio.run(run_agent(query, str(uuid.uuid4()))) + asyncio.run(run_agent(query)) diff --git a/examples/react_agent.py b/examples/react_agent.py index ad763e8..89e2590 100644 --- a/examples/react_agent.py +++ b/examples/react_agent.py @@ -1,39 +1,31 @@ # pip install langgraph "langchain[anthropic]" import asyncio -import uuid -from langchain_sandbox import PyodideSandbox, PyodideSandboxTool +from langchain_sandbox import PyodideSandboxTool from langgraph.prebuilt import create_react_agent -from langgraph.checkpoint.memory import InMemorySaver -# Create a sandbox instance -sandbox = PyodideSandbox( - "./sessions", # Directory to store session files +# Define the sandbox tool +sandbox_tool = PyodideSandboxTool( # Allow Pyodide to install python packages that # might be required. allow_net=True, ) -# Define the sandbox tool -sandbox_tool = PyodideSandboxTool(sandbox=sandbox) - -checkpointer = InMemorySaver() # Create an agent with the sandbox tool agent = create_react_agent( - "anthropic:claude-3-7-sonnet-latest", [sandbox_tool], checkpointer=checkpointer + "anthropic:claude-3-7-sonnet-latest", [sandbox_tool] ) query = """A batter hits a baseball at 45.847 m/s at an angle of 23.474° above the horizontal. The outfielder, who starts facing the batter, picks up the baseball as it lands, then throws it back towards the batter at 24.12 m/s at an angle of 39.12 degrees. How far is the baseball from where the batter originally hit it? Assume zero air resistance.""" -async def run_agent(query: str, thread_id: str): - config = {"configurable": {"thread_id": thread_id}} +async def run_agent(query: str): # Stream agent outputs - async for chunk in agent.astream({"messages": query}, config): + async for chunk in agent.astream({"messages": query}): print(chunk) print("\n") if __name__ == "__main__": # Run the agent - asyncio.run(run_agent(query, str(uuid.uuid4()))) + asyncio.run(run_agent(query)) diff --git a/libs/pyodide-sandbox-js/main.ts b/libs/pyodide-sandbox-js/main.ts index e468c88..1d0c80f 100644 --- a/libs/pyodide-sandbox-js/main.ts +++ b/libs/pyodide-sandbox-js/main.ts @@ -88,18 +88,23 @@ async def install_imports( return to_install -def load_session(path: str) -> List[str]: +def load_session_bytes(session_bytes: bytes) -> list[str]: """Load the session module.""" import dill + import io - dill.session.load_session(filename=path) + buffer = io.BytesIO(session_bytes.to_py()) + dill.session.load_session(filename=buffer) -def dump_session(path: str) -> None: +def dump_session_bytes() -> bytes: """Dump the session module.""" import dill + import io - dill.session.dump_session(filename=path) + buffer = io.BytesIO() + dill.session.dump_session(filename=buffer) + return buffer.getvalue() def robust_serialize(obj): @@ -159,6 +164,8 @@ interface PyodideResult { stderr?: string[]; error?: string; jsonResult?: string; + sessionBytes?: Uint8Array; + sessionMetadata?: SessionMetadata; } async function initPyodide(pyodide: any): Promise { @@ -174,8 +181,9 @@ async function initPyodide(pyodide: any): Promise { async function runPython( pythonCode: string, options: { - session?: string; - sessionsDir?: string; + stateful?: boolean; + sessionBytes?: string; + sessionMetadata?: string; } ): Promise { const output: string[] = []; @@ -197,95 +205,40 @@ async function runPython( await initPyodide(pyodide); // Determine session directory - const sessionsDir = options.sessionsDir || Deno.cwd(); - let sessionMetadata: SessionMetadata = { - created: new Date().toISOString(), - lastModified: new Date().toISOString(), - packages: [], + let sessionMetadata: SessionMetadata; + if (options.sessionMetadata) { + sessionMetadata = JSON.parse(options.sessionMetadata); + } else { + sessionMetadata = { + created: new Date().toISOString(), + lastModified: new Date().toISOString(), + packages: [], + }; }; - let sessionJsonPath: string; - let isExistingSession = false; + let sessionData: Uint8Array | null = null; - // Handle session if provided - if (options.session) { - // Create session directory path - const sessionDirPath = join(sessionsDir, options.session); - const sessionPklPath = join(sessionDirPath, `session.pkl`); - sessionJsonPath = join(sessionDirPath, `session.json`); - - // Ensure session directory exists - try { - const dirInfo = await Deno.stat(sessionDirPath); - if (!dirInfo.isDirectory) { - console.error(`Path exists but is not a directory: ${sessionDirPath}`); - return { success: false, error: `Path exists but is not a directory: ${sessionDirPath}` }; - } - } catch (error) { - // Directory doesn't exist, create it - if (error instanceof Deno.errors.NotFound) { - try { - await Deno.mkdir(sessionDirPath, { recursive: true }); - } catch (mkdirError: any) { - console.error(`Error creating session directory: ${mkdirError.message}`); - return { success: false, error: mkdirError.message }; - } - } else { - console.error(`Error accessing session directory: ${(error as Error).message}`); - return { success: false, error: (error as Error).message }; - } - } - - try { - // Check if both session pickle and metadata files exist - const [pklStat, jsonStat] = await Promise.all([ - Deno.stat(sessionPklPath), - Deno.stat(sessionJsonPath) - ]).catch(() => [null, null]); - - isExistingSession = (pklStat?.isFile && jsonStat?.isFile) || false; - } catch { - // Error checking files, assume they don't exist - isExistingSession = false; - } - - // Create or load session metadata - if (!isExistingSession) { - // Create new session metadata file - await Deno.writeTextFile( - sessionJsonPath, - JSON.stringify(sessionMetadata, null, 2) - ); - } else { - // Load existing session metadata - const jsonContent = await Deno.readTextFile(sessionJsonPath); - sessionMetadata = JSON.parse(jsonContent); - } - - // Load PKL file into pyodide if it exists - try { - const sessionData = await Deno.readFile(sessionPklPath); - pyodide.FS.writeFile(`/${options.session}.pkl`, sessionData); - } catch (error) { - // File doesn't exist or can't be read, skip loading - } + if (options.sessionBytes && !options.sessionMetadata) { + console.error("sessionMetadata is required when providing sessionBytes"); + return { success: false, error: "sessionMetadata is required when providing sessionBytes" }; } - + // Import our prepared environment module const prepare_env = pyodide.pyimport("prepare_env"); - - // Prepare additional packages to install (include dill if using sessions) - const additionalPackagesToInstall = options.session - ? [...new Set([...sessionMetadata.packages, "dill"])] - : []; + // Prepare additional packages to install (include dill) + const defaultPackages = options.stateful ? ["dill"] : []; + const additionalPackagesToInstall = options.sessionBytes + ? [...new Set([...defaultPackages, ...sessionMetadata.packages])] + : defaultPackages; const installedPackages = await prepare_env.install_imports( pythonCode, additionalPackagesToInstall, ); - if (options.session && isExistingSession) { + if (options.sessionBytes) { + sessionData = Uint8Array.from(JSON.parse(options.sessionBytes)); // Run session preamble - await prepare_env.load_session(`/${options.session}.pkl`); + await prepare_env.load_session_bytes(sessionData); } const packages = installedPackages.map((pkg: any) => pkg.get("package")); @@ -296,35 +249,30 @@ async function runPython( const rawValue = await pyodide.runPythonAsync(pythonCode); // Dump result to string const jsonValue = await prepare_env.dumps(rawValue); - - if (options.session) { - // Save session state - await prepare_env.dump_session(`/${options.session}.pkl`); - // Update session metadata with installed packages - sessionMetadata.packages = [ - ...new Set([...sessionMetadata.packages, ...packages]), - ]; - sessionMetadata.lastModified = new Date().toISOString(); - await Deno.writeTextFile( - sessionJsonPath as string, - JSON.stringify(sessionMetadata, null, 2) - ); + // Update session metadata with installed packages + sessionMetadata.packages = [ + ...new Set([...sessionMetadata.packages, ...packages]), + ]; + sessionMetadata.lastModified = new Date().toISOString(); - // Save session file back to host machine - const sessionData = pyodide.FS.readFile(`/${options.session}.pkl`); - const sessionDirPath = join(sessionsDir, options.session); - const sessionPklPath = join(sessionDirPath, `session.pkl`); - await Deno.writeFile(sessionPklPath, sessionData); - } + if (options.stateful) { + // Save session state to sessionBytes + sessionData = await prepare_env.dump_session_bytes() as Uint8Array; + }; // Return the result with stdout and stderr output - return { + const result: PyodideResult = { success: true, result: rawValue, jsonResult: jsonValue, stdout: output, - stderr: err_output + stderr: err_output, + sessionMetadata: sessionMetadata, }; + if (options.stateful && sessionData) { + result["sessionBytes"] = sessionData; + } + return result; } catch (error: any) { return { success: false, @@ -337,17 +285,18 @@ async function runPython( async function main(): Promise { const flags = parseArgs(Deno.args, { - string: ["code", "file", "session", "sessions-dir"], + string: ["code", "file", "session-bytes", "session-metadata"], alias: { c: "code", f: "file", - s: "session", - d: "sessions-dir", h: "help", V: "version", + s: "stateful", + b: "session-bytes", + m: "session-metadata", }, - boolean: ["help", "version"], - default: { help: false, version: false }, + boolean: ["help", "version", "stateful"], + default: { help: false, version: false, stateful: false }, }); if (flags.help) { @@ -358,8 +307,9 @@ Run Python code in a sandboxed environment using Pyodide OPTIONS: -c, --code Python code to execute -f, --file Path to Python file to execute - -s, --session Session name - -d, --sessions-dir Directory to store session files + -s, --stateful Use a stateful session + -b, --session-bytes Session bytes + -m, --session-metadata Session metadata -h, --help Display help -V, --version Display version `); @@ -368,14 +318,15 @@ OPTIONS: if (flags.version) { console.log(pkgVersion) - return; + return } const options = { code: flags.code, file: flags.file, - session: flags.session, - sessionsDir: flags["sessions-dir"], + stateful: flags.stateful, + sessionBytes: flags["session-bytes"], + sessionMetadata: flags["session-metadata"], }; if (!options.code && !options.file) { @@ -385,41 +336,6 @@ OPTIONS: Deno.exit(1); } - // Validate session ID if provided - if (options.session) { - const validSessionIdRegex = /^[a-zA-Z0-9_-]+$/; - if (!validSessionIdRegex.test(options.session)) { - console.error( - "Error: Session ID must only contain letters, numbers, underscores, and hyphens." - ); - Deno.exit(1); - } - } - - // Ensure sessions directory exists if specified - if (options.sessionsDir) { - try { - try { - const dirInfo = await Deno.stat(options.sessionsDir); - if (!dirInfo.isDirectory) { - throw new Error( - `Path exists but is not a directory: ${options.sessionsDir}` - ); - } - } catch (error) { - // Directory doesn't exist, create it - if (error instanceof Deno.errors.NotFound) { - await Deno.mkdir(options.sessionsDir, { recursive: true }); - } else { - throw error; - } - } - } catch (error: any) { - console.error(`Error creating sessions directory: ${error.message}`); - Deno.exit(1); - } - } - // Get Python code from file or command line argument let pythonCode = ""; @@ -440,8 +356,9 @@ OPTIONS: } const result = await runPython(pythonCode, { - session: options.session, - sessionsDir: options.sessionsDir, + stateful: options.stateful, + sessionBytes: options.sessionBytes, + sessionMetadata: options.sessionMetadata, }); // Exit with error code if Python execution failed @@ -451,6 +368,8 @@ OPTIONS: stderr: result.success ? (result.stderr.join('') || null) : result.error || null, result: result.success ? JSON.parse(result.jsonResult || 'null') : null, success: result.success, + sessionBytes: result.sessionBytes, + sessionMetadata: result.sessionMetadata, }; // Output as JSON to stdout diff --git a/libs/sandbox-py/langchain_sandbox/__init__.py b/libs/sandbox-py/langchain_sandbox/__init__.py index 69f0f06..b45a681 100644 --- a/libs/sandbox-py/langchain_sandbox/__init__.py +++ b/libs/sandbox-py/langchain_sandbox/__init__.py @@ -1,7 +1,9 @@ """LangChain code sandbox.""" -from langchain_sandbox.pyodide import PyodideSandbox -from langchain_sandbox.tool import PyodideSandboxTool +from langchain_sandbox.pyodide import ( + PyodideSandbox, + PyodideSandboxTool, +) __all__ = [ "PyodideSandbox", diff --git a/libs/sandbox-py/langchain_sandbox/pyodide.py b/libs/sandbox-py/langchain_sandbox/pyodide.py index e7069e4..642de94 100644 --- a/libs/sandbox-py/langchain_sandbox/pyodide.py +++ b/libs/sandbox-py/langchain_sandbox/pyodide.py @@ -4,10 +4,18 @@ import asyncio import dataclasses import json import logging -import re import subprocess import time -from typing import Any, Literal +from typing import Annotated, Any, Literal + +from langchain_core.callbacks import ( + AsyncCallbackManagerForToolRun, + CallbackManagerForToolRun, +) +from langchain_core.messages import ToolMessage +from langchain_core.runnables import RunnableConfig +from langchain_core.tools import BaseTool, InjectedToolCallId +from pydantic import BaseModel, Field logger = logging.getLogger(__name__) @@ -24,8 +32,11 @@ class CodeExecutionResult: stderr: str | None = None status: Status execution_time: float + session_metadata: dict | None = None + session_bytes: bytes | None = None +# Published package name PKG_NAME = "jsr:@eyurtsev/test-sandbox@0.0.7" @@ -79,10 +90,10 @@ class PyodideSandbox: - Streaming stdout/stderr capture """ - def __init__( # noqa: PLR0913 + def __init__( self, - sessions_dir: str, *, + stateful: bool = False, allow_env: list[str] | bool = False, allow_read: list[str] | bool = False, allow_write: list[str] | bool = False, @@ -99,9 +110,11 @@ class PyodideSandbox: based on the needs of the code being executed. Args: - sessions_dir: Directory for storing session data. This directory must - be writable by the Deno subprocess. It is used to persist session - state between executions. + stateful: Whether to use a stateful session. If True, `sandbox.execute` + will include session metadata and the session bytes containing the + session state (variables, imports, etc.) in the execution result. + This allows saving and reusing the session state between executions. + allow_env: Environment variable access configuration: - False: No environment access (default, most secure) - True: Unrestricted access to all environment variables @@ -114,7 +127,7 @@ class PyodideSandbox: - List[str]: Read access restricted to specific paths, e.g. ["/tmp/sandbox", "./data"] - By default allows read to node_modules and to sessions dir + By default allows read from node_modules allow_write: File system write access configuration: - False: No file system write access (default, most secure) @@ -122,7 +135,7 @@ class PyodideSandbox: - List[str]: Write access restricted to specific paths, e.g. ["/tmp/sandbox/output"] - By default allows read to node_modules and to sessions dir + By default allows write to node_modules allow_net: Network access configuration: - False: No network access (default, most secure) @@ -146,15 +159,7 @@ class PyodideSandbox: the default directory for Deno modules. """ - if "," in sessions_dir: - # Very simple check to protect a user against typos. - # The goal isn't to be exhaustive on validation here. - msg = "Please provide a valid session directory." - raise ValueError(msg) - - # Store configuration - self.sessions_dir = sessions_dir - + self.stateful = stateful # Configure permissions self.permissions = [] @@ -173,9 +178,9 @@ class PyodideSandbox: perm_defs = [ ("--allow-env", allow_env, None), # For file system permissions, if no permission is specified, - # force session_dir and node_modules - ("--allow-read", allow_read, [sessions_dir, "node_modules"]), - ("--allow-write", allow_write, [sessions_dir, "node_modules"]), + # force node_modules + ("--allow-read", allow_read, ["node_modules"]), + ("--allow-write", allow_write, ["node_modules"]), ("--allow-net", allow_net, None), ("--allow-run", allow_run, None), ("--allow-ffi", allow_ffi, None), @@ -192,42 +197,12 @@ class PyodideSandbox: self.permissions.append(f"--node-modules-dir={node_modules_dir}") - # Regular expression for validating session IDs - self.session_id_pattern = re.compile(r"^[a-zA-Z0-9\-_]+$") - - def _validate_session_id(self, session_id: str | None) -> str | None: - """Validate the session ID against the allowed pattern. - - Args: - session_id: The session ID to validate - - Returns: - The session ID if valid, None otherwise - - Raises: - ValueError: If the session ID contains invalid characters - - """ - if session_id is None: - return None - - if not self.session_id_pattern.match(session_id): - msg = ( - f"Invalid session ID: {session_id}. " - "Session IDs must contain only alphanumeric characters, " - "hyphens, and underscores." - ) - raise ValueError( - msg, - ) - - return session_id - async def execute( self, code: str, *, - session_id: str | None = None, + session_bytes: bytes | None = None, + session_metadata: dict | None = None, timeout_seconds: float | None = None, memory_limit_mb: int | None = None, ) -> CodeExecutionResult: @@ -253,10 +228,13 @@ class PyodideSandbox: Args: code: The Python code to execute in the sandbox - session_id: Optional session identifier for maintaining state between - executions. Can be used to persist variables, imports, - and definitions across multiple execute() calls. If None, - a new session is created. + session_bytes: Optional bytes to be used as the initial session state. + This is used to resume code execution from the same session. + You can use this instead of `session_id`. + Note: when using this, you need to provide `session_metadata`. + session_metadata: Optional metadata to be used as the initial session state. + This is used to resume code execution from the same session. + Note: when using this, you need to provide `session_bytes`. timeout_seconds: Maximum execution time in seconds before the process is terminated. If None, execution may run indefinitely (not recommended for untrusted code). @@ -298,18 +276,21 @@ class PyodideSandbox: cmd.append(f"--v8-flags=--max-old-space-size={memory_limit_mb}") # Add the path to the JavaScript wrapper script - # Developer version cmd.append(PKG_NAME) # Add script path and code cmd.extend(["-c", code]) - # Add session ID if provided - if session_id: - cmd.extend(["-s", session_id]) + if self.stateful: + cmd.extend(["-s"]) - # Ensure the sessions directory exists - cmd.extend(["-d", self.sessions_dir]) + if session_bytes: + # Convert bytes to list of integers and then to JSON string + bytes_array = list(session_bytes) + cmd.extend(["-b", json.dumps(bytes_array)]) + + if session_metadata: + cmd.extend(["-m", json.dumps(session_metadata)]) # Create and run the subprocess process = await asyncio.create_subprocess_exec( @@ -334,6 +315,12 @@ class PyodideSandbox: stderr = full_result.get("stderr", None) result = full_result.get("result", None) status = "success" if full_result.get("success", False) else "error" + session_metadata = full_result.get("sessionMetadata", None) + # Convert the Uint8Array to Python bytes + session_bytes_array = full_result.get("sessionBytes", None) + session_bytes = ( + bytes(session_bytes_array) if session_bytes_array else None + ) else: stderr = stderr_bytes.decode("utf-8", errors="replace") status = "error" @@ -353,4 +340,206 @@ class PyodideSandbox: stdout=stdout or None, stderr=stderr or None, result=result, + session_metadata=session_metadata, + session_bytes=session_bytes, ) + + +class PyodideSandboxTool(BaseTool): + """Tool for running python code in a PyodideSandbox. + + If you use a stateful sandbox (PyodideSandboxTool(stateful=True)), + the state between code executions (to variables, imports, + and definitions, etc.), will be persisted using LangGraph checkpointer. + + !!! important + When you use a stateful sandbox, this tool can only be used + inside a LangGraph graph with a checkpointer, and + has to be used with the prebuilt `create_react_agent` or `ToolNode`. + + Example: stateless sandbox usage + + ```python + from langgraph.prebuilt import create_react_agent + from langchain_sandbox import PyodideSandboxTool + + tool = PyodideSandboxTool(allow_net=True) + agent = create_react_agent( + "anthropic:claude-3-7-sonnet-latest", + tools=[tool], + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "what's 5 + 7?"}]}, + ) + ``` + + Example: stateful sandbox usage + + ```python + from langgraph.prebuilt import create_react_agent + from langgraph.prebuilt.chat_agent_executor import AgentState + from langgraph.checkpoint.memory import InMemorySaver + from langchain_sandbox import PyodideSandboxTool, PyodideSandbox + + class State(AgentState): + session_bytes: bytes + session_metadata: dict + + tool = PyodideSandboxTool(stateful=True, allow_net=True) + agent = create_react_agent( + "anthropic:claude-3-7-sonnet-latest", + tools=[tool], + checkpointer=InMemorySaver(), + state_schema=State + ) + result = await agent.ainvoke( + { + "messages": [ + {"role": "user", "content": "what's 5 + 7? save result as 'a'"} + ], + "session_bytes": None, + "session_metadata": None + }, + config={"configurable": {"thread_id": "123"}}, + ) + second_result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "what's the sine of 'a'?"}]}, + config={"configurable": {"thread_id": "123"}}, + ) + ``` + """ + + name: str = "python_code_sandbox" + description: str = ( + "A secure Python code sandbox. Use this to execute python commands.\n" + "- Input should be a valid python command.\n" + "- To return output, you should print it out with `print(...)`.\n" + "- Don't use f-strings when printing outputs.\n" + "- If you need to make web requests, use `httpx.AsyncClient`." + ) + + # Mirror the PyodideSandbox constructor arguments + stateful: bool = False + allow_env: list[str] | bool = False + allow_read: list[str] | bool = False + allow_write: list[str] | bool = False + allow_net: list[str] | bool = False + allow_run: list[str] | bool = False + allow_ffi: list[str] | bool = False + node_modules_dir: str = "auto" + + _sandbox: PyodideSandbox + + def __init__(self, **kwargs: dict[str, Any]) -> None: + """Initialize the tool.""" + super().__init__(**kwargs) + + if self.stateful: + try: + from langgraph.prebuilt import InjectedState + except ImportError as e: + error_msg = ( + "The 'langgraph' package is required when using a stateful sandbox." + " Please install it with 'pip install langgraph'." + ) + raise ImportError(error_msg) from e + + class PyodideSandboxToolInput(BaseModel): + """Python code to execute in the sandbox.""" + + code: str = Field(description="Code to execute.") + # these fields will be ignored by the LLM + # and automatically injected by LangGraph's ToolNode + state: Annotated[dict[str, Any] | BaseModel, InjectedState] + tool_call_id: Annotated[str, InjectedToolCallId] + + else: + + class PyodideSandboxToolInput(BaseModel): + """Python code to execute in the sandbox.""" + + code: str = Field(description="Code to execute.") + + self.args_schema: type[BaseModel] = PyodideSandboxToolInput + self._sandbox = PyodideSandbox( + stateful=self.stateful, + allow_env=self.allow_env, + allow_read=self.allow_read, + allow_write=self.allow_write, + allow_net=self.allow_net, + allow_run=self.allow_run, + allow_ffi=self.allow_ffi, + node_modules_dir=self.node_modules_dir, + ) + + def _run( + self, + code: str, + state: dict[str, Any] | BaseModel | None = None, + tool_call_id: str | None = None, + config: RunnableConfig | None = None, + run_manager: CallbackManagerForToolRun | None = None, + ) -> Any: # noqa: ANN401 + """Use the tool.""" + error_msg = ( + "Sync invocation of PyodideSandboxTool is not supported - " + "please invoke the tool asynchronously using `await tool.ainvoke()`" + ) + raise NotImplementedError(error_msg) + + async def _arun( + self, + code: str, + state: dict[str, Any] | BaseModel | None = None, + tool_call_id: str | None = None, + config: RunnableConfig | None = None, + run_manager: AsyncCallbackManagerForToolRun | None = None, + ) -> Any: # noqa: ANN401 + """Use the tool asynchronously.""" + if self.stateful: + required_keys = {"session_bytes", "session_metadata", "messages"} + actual_keys = set(state) if isinstance(state, dict) else set(state.__dict__) + if missing_keys := required_keys - actual_keys: + error_msg = ( + "Input state is missing " + f"the following required keys: {missing_keys}" + ) + raise ValueError(error_msg) + + if isinstance(state, dict): + session_bytes = state["session_bytes"] + session_metadata = state["session_metadata"] + else: + session_bytes = state.session_bytes + session_metadata = state.session_metadata + + result = await self._sandbox.execute( + code, session_bytes=session_bytes, session_metadata=session_metadata + ) + else: + result = await self._sandbox.execute(code) + + if result.stderr: + tool_result = f"Error during execution: {result.stderr}" + else: + tool_result = result.stdout + + if self.stateful: + from langgraph.types import Command + + # if the tool is used with a stateful sandbox, + # we need to update the graph state with the new session bytes and metadata + return Command( + update={ + "session_bytes": result.session_bytes, + "session_metadata": result.session_metadata, + "messages": [ + ToolMessage( + content=tool_result, + tool_call_id=tool_call_id, + ) + ], + } + ) + + return tool_result diff --git a/libs/sandbox-py/langchain_sandbox/tool.py b/libs/sandbox-py/langchain_sandbox/tool.py deleted file mode 100644 index 891d033..0000000 --- a/libs/sandbox-py/langchain_sandbox/tool.py +++ /dev/null @@ -1,106 +0,0 @@ -"""LangChain tool for running python code in a PyodideSandbox.""" - -from langchain_core.callbacks import ( - AsyncCallbackManagerForToolRun, - CallbackManagerForToolRun, -) -from langchain_core.runnables import RunnableConfig -from langchain_core.tools import BaseTool -from pydantic import BaseModel, Field - -from langchain_sandbox import PyodideSandbox - - -def _get_default_pyodide_sandbox() -> PyodideSandbox: - """Get default sandbox for the tool.""" - return PyodideSandbox( - "./sessions", # Directory to store session files - # Allow Pyodide to install python packages that - # might be required. - allow_net=True, - ) - - -class PythonInputs(BaseModel): - """Python code to execute in the sandbox.""" - - code: str = Field(description="Code to execute.") - - -class PyodideSandboxTool(BaseTool): - """Tool for running python code in a PyodideSandbox. - - If you want to persist state between code executions (to persist variables, imports, - and definitions, etc.), you need to invoke the tool with `thread_id` in the config: - - ```python - from langchain_sandbox import PyodideSandboxTool - - tool = PyodideSandboxTool() - result = await tool.ainvoke( - "print('Hello, world!')", - config={"configurable": {"thread_id": "123"}}, - ) - ``` - - If you are using this tool inside an agent, like LangGraph `create_react_agent`, you - can invoke the agent with a config, and it will automatically be passed to the tool: - - ```python - from langgraph.prebuilt import create_react_agent - from langgraph.checkpoint.memory import InMemorySaver - from langchain_sandbox import PyodideSandboxTool - - tool = PyodideSandboxTool() - agent = create_react_agent( - "anthropic:claude-3-7-sonnet-latest", - tools=[tool], - checkpointer=InMemorySaver() - ) - result = await agent.ainvoke( - {"messages": [{"role": "user", "content": "what's 5 + 7?"}]}, - config={"configurable": {"thread_id": "123"}}, - ) - second_result = await agent.ainvoke( - {"messages": [{"role": "user", "content": "what's the sine of that?"}]}, - config={"configurable": {"thread_id": "123"}}, - ) - ``` - """ - - name: str = "python_code_sandbox" - description: str = ( - "A secure Python code sandbox. Use this to execute python commands.\n" - "- Input should be a valid python command.\n" - "- To return output, you should print it out with `print(...)`.\n" - "- Don't use f-strings when printing outputs.\n" - "- If you need to make web requests, use `httpx.AsyncClient`." - ) - sandbox: PyodideSandbox = Field(default_factory=_get_default_pyodide_sandbox) - args_schema: type[BaseModel] = PythonInputs - - def _run( - self, - code: str, - config: RunnableConfig, - run_manager: CallbackManagerForToolRun | None = None, - ) -> str: - """Use the tool.""" - error_msg = ( - "Sync invocation of PyodideSandboxTool is not supported - " - "please invoke the tool asynchronously using `await tool.ainvoke()`" - ) - raise NotImplementedError(error_msg) - - async def _arun( - self, - code: str, - config: RunnableConfig, - run_manager: AsyncCallbackManagerForToolRun | None = None, - ) -> str: - """Use the tool asynchronously.""" - session_id = config.get("configurable", {}).get("thread_id") - result = await self.sandbox.execute(code, session_id=session_id) - if result.stderr: - return f"Error during execution: {result.stderr}" - return result.stdout diff --git a/libs/sandbox-py/pyproject.toml b/libs/sandbox-py/pyproject.toml index 3eff12b..54f6cdd 100644 --- a/libs/sandbox-py/pyproject.toml +++ b/libs/sandbox-py/pyproject.toml @@ -5,7 +5,8 @@ description = "LangChain Sandbox" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "langchain-core>=0.3.56,<0.4.0" + "langchain-core>=0.3.56,<0.4.0", + "langgraph>=0.4.3" ] [dependency-groups] @@ -46,6 +47,7 @@ select = [ ignore = [ "ARG", "COM812", + "PLR0913", ] diff --git a/libs/sandbox-py/tests/unit_tests/test_pyodide_sandbox.py b/libs/sandbox-py/tests/unit_tests/test_pyodide_sandbox.py index 0120fdc..0b716c1 100644 --- a/libs/sandbox-py/tests/unit_tests/test_pyodide_sandbox.py +++ b/libs/sandbox-py/tests/unit_tests/test_pyodide_sandbox.py @@ -1,166 +1,90 @@ """Test pyodide sandbox functionality.""" -import shutil -import tempfile -from collections.abc import Iterator -from contextlib import contextmanager +from pathlib import Path + +import pytest from langchain_sandbox import PyodideSandbox - -@contextmanager -def sandbox_context( # noqa: PLR0913 - *, - allow_read: list[str] | str | bool = "node_modules", - allow_write: list[str] | str | bool = "node_modules", - allow_net: list[str] | bool = True, - allow_env: list[str] | bool = False, - allow_run: list[str] | bool = False, - allow_ffi: list[str] | bool = False, -) -> Iterator[tuple[PyodideSandbox, str]]: - """Create a PyodideSandbox instance with a temporary directory for sessions. - - This context manager creates a sandbox with a temporary directory and ensures - cleanup when the context exits. - - Args: - allow_read: File system read permissions - allow_write: File system write permissions - allow_net: Network access permissions - allow_env: Environment variable access permissions - allow_run: Subprocess execution permissions - allow_ffi: Foreign Function Interface permissions - - Yields: - A tuple containing (sandbox_instance, temp_directory_path) - - """ - # Create temporary directory - temp_sessions_dir = tempfile.mkdtemp(prefix="pyodide_test_sessions_") - - try: - # Set default permissions for temp dir - actual_read = [] if allow_read is None else allow_read - actual_write = [] if allow_write is None else allow_write - - # convert str to list - if isinstance(actual_read, str): - actual_read = [actual_read] - if isinstance(actual_write, str): - actual_write = [actual_write] - - # Ensure the temp directory is always readable and writable - if isinstance(actual_read, list) and temp_sessions_dir not in actual_read: - actual_read.append(temp_sessions_dir) - - if isinstance(actual_write, list) and temp_sessions_dir not in actual_write: - actual_write.append(temp_sessions_dir) - - # Create the sandbox - sandbox = PyodideSandbox( - sessions_dir=temp_sessions_dir, - allow_read=actual_read, - allow_write=actual_write, - allow_net=allow_net, - allow_env=allow_env, - allow_run=allow_run, - allow_ffi=allow_ffi, - ) - - # Yield the sandbox and temp dir - yield sandbox, temp_sessions_dir - - finally: - # Clean up the temporary directory - shutil.rmtree(temp_sessions_dir, ignore_errors=True) +current_dir = Path(__file__).parent -async def test_stdout_sessionless() -> None: +@pytest.fixture +def pyodide_package(monkeypatch: pytest.MonkeyPatch) -> None: + """Patch PKG_NAME to point to a local deno typescript file.""" + local_script = str(current_dir / "../../../pyodide-sandbox-js/main.ts") + monkeypatch.setattr("langchain_sandbox.pyodide.PKG_NAME", local_script) + + +def get_default_sandbox(stateful: bool = False) -> PyodideSandbox: + """Get default PyodideSandbox instance for testing.""" + return PyodideSandbox( + stateful=stateful, + allow_read=True, + allow_write=True, + allow_net=True, + allow_env=False, + allow_run=False, + allow_ffi=False, + ) + + +async def test_stdout_sessionless(pyodide_package: None) -> None: """Test without a session ID.""" - with sandbox_context() as (sandbox, _): - # Execute a simple piece of code synchronously - result = await sandbox.execute("x = 5; print(x); x") - assert result.status == "success" - assert result.stdout == "5" - assert result.result == 5 - assert result.stderr is None + sandbox = get_default_sandbox() + # Execute a simple piece of code synchronously + result = await sandbox.execute("x = 5; print(x); x") + assert result.status == "success" + assert result.stdout == "5" + assert result.result == 5 + assert result.stderr is None + assert result.session_bytes is None -async def test_session_state_persistence_basic() -> None: +async def test_session_state_persistence_basic(pyodide_package: None) -> None: """Simple test to verify that a session ID is used to persist state. We'll assign a variable in one execution and check if it's available in the next. """ - with sandbox_context() as (sandbox, _): - # Test with a session ID to ensure state persistence - session_id = "test_session_1" - result1 = await sandbox.execute("y = 10; print(y)", session_id=session_id) - result2 = await sandbox.execute("print(y)", session_id=session_id) + sandbox = get_default_sandbox(stateful=True) - # Check session state persistence - assert result1.status == "success", f"Encountered error: {result1.stderr}" - assert result1.stdout == "10" - assert result1.result is None - assert result2.status == "success", f"Encountered error: {result2.stderr}" - assert result2.stdout == "10" - assert result1.result is None + result1 = await sandbox.execute("y = 10; print(y)") + result2 = await sandbox.execute( + "print(y)", + session_bytes=result1.session_bytes, + session_metadata=result1.session_metadata, + ) + + # Check session state persistence + assert result1.status == "success", f"Encountered error: {result1.stderr}" + assert result1.stdout == "10" + assert result1.result is None + assert result2.status == "success", f"Encountered error: {result2.stderr}" + assert result2.stdout == "10" + assert result1.result is None -async def test_pyodide_sandbox_error_handling() -> None: +async def test_pyodide_sandbox_error_handling(pyodide_package: None) -> None: """Test PyodideSandbox error handling.""" - with sandbox_context() as (sandbox, _): - # Test syntax error - result = await sandbox.execute("x = 5; y = x +") - assert result.status == "error" - assert "SyntaxError" in result.stderr + sandbox = get_default_sandbox() - # Test undefined variable error - result = await sandbox.execute("undefined_variable") - assert result.status == "error" - assert "NameError" in result.stderr + # Test syntax error + result = await sandbox.execute("x = 5; y = x +") + assert result.status == "error" + assert "SyntaxError" in result.stderr + + # Test undefined variable error + result = await sandbox.execute("undefined_variable") + assert result.status == "error" + assert "NameError" in result.stderr -async def test_pyodide_sandbox_timeout() -> None: +async def test_pyodide_sandbox_timeout(pyodide_package: None) -> None: """Test PyodideSandbox timeout handling.""" - with sandbox_context() as (sandbox, _): - # Test timeout with infinite loop - # Using a short timeout to avoid long test runs - result = await sandbox.execute("while True: pass", timeout_seconds=0.5) - assert result.status == "error" - assert "timed out" in result.stderr.lower() + sandbox = get_default_sandbox() - -# Currently, we do not support file operations persisting across sessions -async def test_pyodide_sandbox_file_operations() -> None: - """Test file operations are not persisted across sessions.""" - with sandbox_context() as (sandbox, _): - # Test file I/O within the allowed session directory - # Note: In Pyodide, the sessions directory might be mapped differently - # We are testing the ability to write files via the sandbox, - # which is what matters - session_id = "file_test_session" - - # Create a file in the session - code_write = """ -import json -data = {'test': 'value', 'number': 42} -with open('test_data.json', 'w') as f: - json.dump(data, f) -""" - result = await sandbox.execute(code_write, session_id=session_id) - assert result.status == "success" - assert result.stdout is None - assert result.stderr is None - assert result.result is None - - # Confirm that the file does not exist in the session directory - code_read = """\ -import os -if os.path.exists('test_data.json'): - raise Exception("File should not exist in the session directory.") -print("OK") -""" - result = await sandbox.execute(code_read, session_id=session_id) - assert result.status == "error" - assert result.stdout is None - assert "File should not exist" in result.stderr + # Test timeout with infinite loop + # Using a short timeout to avoid long test runs + result = await sandbox.execute("while True: pass", timeout_seconds=0.5) + assert result.status == "error" + assert "timed out" in result.stderr.lower() diff --git a/libs/sandbox-py/uv.lock b/libs/sandbox-py/uv.lock index 4783937..f121ddb 100644 --- a/libs/sandbox-py/uv.lock +++ b/libs/sandbox-py/uv.lock @@ -340,6 +340,7 @@ version = "0.0.4" source = { editable = "." } dependencies = [ { name = "langchain-core" }, + { name = "langgraph" }, ] [package.dev-dependencies] @@ -354,7 +355,10 @@ test = [ ] [package.metadata] -requires-dist = [{ name = "langchain-core", specifier = ">=0.3.56,<0.4.0" }] +requires-dist = [ + { name = "langchain-core", specifier = ">=0.3.56,<0.4.0" }, + { name = "langgraph", specifier = ">=0.4.3" }, +] [package.metadata.requires-dev] test = [ @@ -367,6 +371,62 @@ test = [ { name = "ruff", specifier = ">=0.9.7" }, ] +[[package]] +name = "langgraph" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core", marker = "python_full_version < '4.0'" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt", marker = "python_full_version < '4.0'" }, + { name = "langgraph-sdk", marker = "python_full_version < '4.0'" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/9e/5a64602eff18a99d0216a80eff823051ffbdb7c11b5a16171cee8b1ccce5/langgraph-0.4.3.tar.gz", hash = "sha256:272d5d5903f2c2882dbeeba849846a0f2500bd83fb3734a3801ebe64c1a60bdd", size = 125407 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/53/0a20edd9f41eb3707722444ec1b43752b792bbe904d1c8cc3ba27f8eb2c8/langgraph-0.4.3-py3-none-any.whl", hash = "sha256:dec926e034f4d440b92a3c52139cb6e9763bc1791e79a6ea53a233309cec864f", size = 151191 }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "2.0.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/72/d49828e6929cb3ded1472aa3e5e4a369d292c4f21021ac683d28fbc8f4f8/langgraph_checkpoint-2.0.25.tar.gz", hash = "sha256:77a63cab7b5f84dec1d49db561326ec28bdd48bcefb7fe4ac372069d2609287b", size = 36952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/52/bceb5b5348c7a60ef0625ab0a0a0a9ff5d78f0e12aed8cc55c49d5e8a8c9/langgraph_checkpoint-2.0.25-py3-none-any.whl", hash = "sha256:23416a0f5bc9dd712ac10918fc13e8c9c4530c419d2985a441df71a38fc81602", size = 42312 }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/30/f31f0e076c37d097b53e4cff5d479a3686e1991f6c86a1a4727d5d1f5489/langgraph_prebuilt-0.1.8.tar.gz", hash = "sha256:4de7659151829b2b955b6798df6800e580e617782c15c2c5b29b139697491831", size = 24543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/72/9e092665502f8f52f2708065ed14fbbba3f95d1a1b65d62049b0c5fcdf00/langgraph_prebuilt-0.1.8-py3-none-any.whl", hash = "sha256:ae97b828ae00be2cefec503423aa782e1bff165e9b94592e224da132f2526968", size = 25903 }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.1.69" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/78/4ca0603240332be5fc8ebbb9bc418896310643bef32e3319a311fab37e4c/langgraph_sdk-0.1.69.tar.gz", hash = "sha256:2e85d73b78a03f9606d0fafd62048b3060371149f6f9e61f07f087fd56c766fa", size = 45343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/e6/8e82a0373e233392d83ae37f473c9799c536b307322f0caf49a59bce9522/langgraph_sdk-0.1.69-py3-none-any.whl", hash = "sha256:0ed117bcdf67285a17c57f6265f1d94f2dbd71346cf48a8e1a5fa25e523eb6b8", size = 48905 }, +] + [[package]] name = "langsmith" version = "0.3.33" @@ -447,6 +507,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 }, ] +[[package]] +name = "ormsgpack" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/a7/462cf8ff5e29241868b82d3a5ec124d690eb6a6a5c6fa5bb1367b839e027/ormsgpack-1.9.1.tar.gz", hash = "sha256:3da6e63d82565e590b98178545e64f0f8506137b92bd31a2d04fd7c82baf5794", size = 56887 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/32/5f504c0695ff96aaaf0452bee522d79b5a3ee809f22fd77fdb0dd5756d86/ormsgpack-1.9.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1f804fd9c0fd84213a6022c34172f82323b34afa7052a4af18797582cf56365", size = 382793 }, + { url = "https://files.pythonhosted.org/packages/1e/c6/64fe1270271b61495611f1d3068baedb57d76e0f93ce7156f3763fb79b32/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab5cec99c46276b37071d570aab98603f3d0309b3818da3247eb64bb95e5cfc", size = 213974 }, + { url = "https://files.pythonhosted.org/packages/13/56/6666d6a9b82c7d2021fce6823ff823bc373a4e7280979c1b453317678fbc/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c12c6bb30e6df6fc0213b77f0a5e143f371d618be2e8eb4d555340ce01c6900", size = 217200 }, + { url = "https://files.pythonhosted.org/packages/5f/fb/b844ed1e69d8615163525a8403d7abd3548b3fbfa0f3a973808f36145a0f/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994d4bbb7ee333264a3e55e30ccee063df6635d785f21a08bf52f67821454a51", size = 223648 }, + { url = "https://files.pythonhosted.org/packages/f2/26/c40c3e300f9c61a5ed7a6921656dd0d2907a8174936e1e643677585e497c/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a668a584cf4bb6e1a6ef5a35f3f0d0fdae80cfb7237344ad19a50cce8c79317b", size = 394197 }, + { url = "https://files.pythonhosted.org/packages/2d/4c/7a4ae187f18e7abf7ab0662b473264a60a5aa4e9bff266f541a8855df163/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:aaf77699203822638014c604d100f132583844d4fd01eb639a2266970c02cfdf", size = 480550 }, + { url = "https://files.pythonhosted.org/packages/b1/33/5c465dfd5571f816835bb9e371987bf081b529c64ef28a72d18b0b59902d/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:003d7e1992b447898caf25a820b3037ec68a57864b3e2f34b64693b7d60a9984", size = 396955 }, + { url = "https://files.pythonhosted.org/packages/8f/fd/8f64f477b5c6d66e9c6343d7d3f32d7063ba20ab151dd36884e6504899ab/ormsgpack-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:67fefc77e4ba9469f79426769eb4c78acf21f22bef3ab1239a72dd728036ffc2", size = 125102 }, + { url = "https://files.pythonhosted.org/packages/d8/3b/388e7915a28db6ab3daedfd4937bd7b063c50dd1543068daa31c0a3b70ed/ormsgpack-1.9.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:16eaf32c33ab4249e242181d59e2509b8e0330d6f65c1d8bf08c3dea38fd7c02", size = 382794 }, + { url = "https://files.pythonhosted.org/packages/0f/b4/3f4afba058822bf69b274e0defe507056be0340e65363c3ebcd312b01b84/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c70f2e5b2f9975536e8f7936a9721601dc54febe363d2d82f74c9b31d4fe1c65", size = 213974 }, + { url = "https://files.pythonhosted.org/packages/bf/be/f0e21366d51b6e28fc3a55425be6a125545370d3479bf25be081e83ee236/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:17c9e18b07d69e3db2e0f8af4731040175e11bdfde78ad8e28126e9e66ec5167", size = 217200 }, + { url = "https://files.pythonhosted.org/packages/cc/90/67a23c1c880a6e5552acb45f9555b642528f89c8bcf75283a2ea64ef7175/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73538d749096bb6470328601a2be8f7bdec28849ec6fd19595c232a5848d7124", size = 223649 }, + { url = "https://files.pythonhosted.org/packages/80/ad/116c1f970b5b4453e4faa52645517a2e5eaf1ab385ba09a5c54253d07d0e/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:827ff71de228cfd6d07b9d6b47911aa61b1e8dc995dec3caf8fdcdf4f874bcd0", size = 394200 }, + { url = "https://files.pythonhosted.org/packages/c5/a2/b224a5ef193628a15205e473179276b87e8290d321693e4934a05cbd6ccf/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7307f808b3df282c8e8ed92c6ebceeb3eea3d8eeec808438f3f212226b25e217", size = 480551 }, + { url = "https://files.pythonhosted.org/packages/b5/f4/a0f528196af6ab46e6c3f3051cf7403016bdc7b7d3e673ea5b04b145be98/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f30aad7fb083bed1c540a3c163c6a9f63a94e3c538860bf8f13386c29b560ad5", size = 396959 }, + { url = "https://files.pythonhosted.org/packages/bc/6b/60c6f4787e3e93f5eb34fccb163753a8771465983a579e3405152f2422fd/ormsgpack-1.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:829a1b4c5bc3c38ece0c55cf91ebc09c3b987fceb24d3f680c2bcd03fd3789a4", size = 125100 }, + { url = "https://files.pythonhosted.org/packages/dd/f1/155a598cc8030526ccaaf91ba4d61530f87900645559487edba58b0a90a2/ormsgpack-1.9.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1ede445fc3fdba219bb0e0d1f289df26a9c7602016b7daac6fafe8fe4e91548f", size = 383225 }, + { url = "https://files.pythonhosted.org/packages/23/1c/ef3097ba550fad55c79525f461febdd4e0d9cc18d065248044536f09488e/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db50b9f918e25b289114312ed775794d0978b469831b992bdc65bfe20b91fe30", size = 214056 }, + { url = "https://files.pythonhosted.org/packages/27/77/64d0da25896b2cbb99505ca518c109d7dd1964d7fde14c10943731738b60/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c7d8fc58e4333308f58ec720b1ee6b12b2b3fe2d2d8f0766ab751cb351e8757", size = 217339 }, + { url = "https://files.pythonhosted.org/packages/6c/10/c3a7fd0a0068b0bb52cccbfeb5656db895d69e895a3abbc210c4b3f98ff8/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeee6d08c040db265cb8563444aba343ecb32cbdbe2414a489dcead9f70c6765", size = 223816 }, + { url = "https://files.pythonhosted.org/packages/43/e7/aee1238dba652f2116c2523d36fd1c5f9775436032be5c233108fd2a1415/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fbb8181c198bdc413a4e889e5200f010724eea4b6d5a9a7eee2df039ac04aca", size = 394287 }, + { url = "https://files.pythonhosted.org/packages/c7/09/1b452a92376f29d7a2da7c18fb01cf09978197a8eccbb8b204e72fd5a970/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:16488f094ac0e2250cceea6caf72962614aa432ee11dd57ef45e1ad25ece3eff", size = 480709 }, + { url = "https://files.pythonhosted.org/packages/de/13/7fa9fee5a73af8a73a42bf8c2e69489605714f65f5a41454400a05e84a3b/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:422d960bfd6ad88be20794f50ec7953d8f7a0f2df60e19d0e8feb994e2ed64ee", size = 397247 }, + { url = "https://files.pythonhosted.org/packages/a1/2d/2e87cb28110db0d3bb750edd4d8719b5068852a2eef5e96b0bf376bb8a81/ormsgpack-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6e2f9eab527cf43fb4a4293e493370276b1c8716cf305689202d646c6a782ef", size = 125368 }, + { url = "https://files.pythonhosted.org/packages/b8/54/0390d5d092831e4df29dbafe32402891fc14b3e6ffe5a644b16cbbc9d9bc/ormsgpack-1.9.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ac61c18d9dd085e8519b949f7e655f7fb07909fd09c53b4338dd33309012e289", size = 383226 }, + { url = "https://files.pythonhosted.org/packages/47/64/8b15d262d1caefead8fb22ec144f5ff7d9505fc31c22bc34598053d46fbe/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134840b8c6615da2c24ce77bd12a46098015c808197a9995c7a2d991e1904eec", size = 214057 }, + { url = "https://files.pythonhosted.org/packages/57/00/65823609266bad4d5ed29ea753d24a3bdb01c7edaf923da80967fc31f9c5/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38fd42618f626394b2c7713c5d4bcbc917254e9753d5d4cde460658b51b11a74", size = 217340 }, + { url = "https://files.pythonhosted.org/packages/a0/51/e535c50f7f87b49110233647f55300d7975139ef5e51f1adb4c55f58c124/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d36397333ad07b9eba4c2e271fa78951bd81afc059c85a6e9f6c0eb2de07cda", size = 223815 }, + { url = "https://files.pythonhosted.org/packages/0c/ee/393e4a6de2a62124bf589602648f295a9fb3907a0e2fe80061b88899d072/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:603063089597917d04e4c1b1d53988a34f7dc2ff1a03adcfd1cf4ae966d5fba6", size = 394287 }, + { url = "https://files.pythonhosted.org/packages/c6/d8/e56d7c3cb73a0e533e3e2a21ae5838b2aa36a9dac1ca9c861af6bae5a369/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:94bbf2b185e0cb721ceaba20e64b7158e6caf0cecd140ca29b9f05a8d5e91e2f", size = 480707 }, + { url = "https://files.pythonhosted.org/packages/e6/e0/6a3c6a6dc98583a721c54b02f5195bde8f801aebdeda9b601fa2ab30ad39/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38f380b1e8c96a712eb302b9349347385161a8e29046868ae2bfdfcb23e2692", size = 397246 }, + { url = "https://files.pythonhosted.org/packages/b0/60/0ee5d790f13507e1f75ac21fc82dc1ef29afe1f520bd0f249d65b2f4839b/ormsgpack-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a4bc63fb30db94075611cedbbc3d261dd17cf2aa8ff75a0fd684cd45ca29cb1b", size = 125371 }, +] + [[package]] name = "packaging" version = "24.2" @@ -837,6 +937,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] +[[package]] +name = "xxhash" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8a/0e9feca390d512d293afd844d31670e25608c4a901e10202aa98785eab09/xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212", size = 31970 }, + { url = "https://files.pythonhosted.org/packages/16/e6/be5aa49580cd064a18200ab78e29b88b1127e1a8c7955eb8ecf81f2626eb/xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520", size = 30801 }, + { url = "https://files.pythonhosted.org/packages/20/ee/b8a99ebbc6d1113b3a3f09e747fa318c3cde5b04bd9c197688fadf0eeae8/xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680", size = 220927 }, + { url = "https://files.pythonhosted.org/packages/58/62/15d10582ef159283a5c2b47f6d799fc3303fe3911d5bb0bcc820e1ef7ff4/xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da", size = 200360 }, + { url = "https://files.pythonhosted.org/packages/23/41/61202663ea9b1bd8e53673b8ec9e2619989353dba8cfb68e59a9cbd9ffe3/xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23", size = 428528 }, + { url = "https://files.pythonhosted.org/packages/f2/07/d9a3059f702dec5b3b703737afb6dda32f304f6e9da181a229dafd052c29/xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196", size = 194149 }, + { url = "https://files.pythonhosted.org/packages/eb/58/27caadf78226ecf1d62dbd0c01d152ed381c14c1ee4ad01f0d460fc40eac/xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c", size = 207703 }, + { url = "https://files.pythonhosted.org/packages/b1/08/32d558ce23e1e068453c39aed7b3c1cdc690c177873ec0ca3a90d5808765/xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482", size = 216255 }, + { url = "https://files.pythonhosted.org/packages/3f/d4/2b971e2d2b0a61045f842b622ef11e94096cf1f12cd448b6fd426e80e0e2/xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296", size = 202744 }, + { url = "https://files.pythonhosted.org/packages/19/ae/6a6438864a8c4c39915d7b65effd85392ebe22710412902487e51769146d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415", size = 210115 }, + { url = "https://files.pythonhosted.org/packages/48/7d/b3c27c27d1fc868094d02fe4498ccce8cec9fcc591825c01d6bcb0b4fc49/xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198", size = 414247 }, + { url = "https://files.pythonhosted.org/packages/a1/05/918f9e7d2fbbd334b829997045d341d6239b563c44e683b9a7ef8fe50f5d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442", size = 191419 }, + { url = "https://files.pythonhosted.org/packages/08/29/dfe393805b2f86bfc47c290b275f0b7c189dc2f4e136fd4754f32eb18a8d/xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da", size = 30114 }, + { url = "https://files.pythonhosted.org/packages/7b/d7/aa0b22c4ebb7c3ccb993d4c565132abc641cd11164f8952d89eb6a501909/xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9", size = 30003 }, + { url = "https://files.pythonhosted.org/packages/69/12/f969b81541ee91b55f1ce469d7ab55079593c80d04fd01691b550e535000/xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6", size = 26773 }, + { url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8", size = 30800 }, + { url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166", size = 221566 }, + { url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7", size = 201214 }, + { url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623", size = 429433 }, + { url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a", size = 194822 }, + { url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88", size = 208538 }, + { url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c", size = 216953 }, + { url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2", size = 203594 }, + { url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084", size = 210971 }, + { url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d", size = 415050 }, + { url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839", size = 192216 }, + { url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da", size = 30120 }, + { url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58", size = 30003 }, + { url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3", size = 26777 }, + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787 }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959 }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006 }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326 }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380 }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934 }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301 }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351 }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294 }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674 }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022 }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170 }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040 }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796 }, + { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795 }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792 }, + { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950 }, + { url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980 }, + { url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324 }, + { url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370 }, + { url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911 }, + { url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352 }, + { url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410 }, + { url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322 }, + { url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725 }, + { url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070 }, + { url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172 }, + { url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041 }, + { url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801 }, + { url = "https://files.pythonhosted.org/packages/ab/9a/233606bada5bd6f50b2b72c45de3d9868ad551e83893d2ac86dc7bb8553a/xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c", size = 29732 }, + { url = "https://files.pythonhosted.org/packages/0c/67/f75276ca39e2c6604e3bee6c84e9db8a56a4973fde9bf35989787cf6e8aa/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986", size = 36214 }, + { url = "https://files.pythonhosted.org/packages/0f/f8/f6c61fd794229cc3848d144f73754a0c107854372d7261419dcbbd286299/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6", size = 32020 }, + { url = "https://files.pythonhosted.org/packages/79/d3/c029c99801526f859e6b38d34ab87c08993bf3dcea34b11275775001638a/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b", size = 40515 }, + { url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da", size = 30064 }, +] + [[package]] name = "zstandard" version = "0.23.0"