From 1be2d5eb8ef04f8b82212deb21097fd41f77b71f Mon Sep 17 00:00:00 2001 From: isaacbmiller Date: Fri, 31 Jul 2026 14:45:09 +0000 Subject: [PATCH 1/2] refactor(interpreter): support backend-specific code execution Route CodeAct tools through the CodeInterpreter capability boundary, pass backend guidance through call-local signatures, support custom Parameter state loading, and make binary RLM payload transport portable across constrained interpreters. Amp-Thread-ID: https://ampcode.com/threads/T-019fb0dc-8d11-7510-abc9-4bc180e4e6e4 Co-authored-by: Isaac Miller --- dspy/predict/code_act.py | 33 +++++++++++++++--- dspy/predict/rlm.py | 46 ++++++++++++++----------- dspy/primitives/base_module.py | 14 +++++++- dspy/primitives/code_interpreter.py | 6 ++++ dspy/primitives/python_interpreter.py | 4 +++ tests/predict/test_code_act.py | 48 +++++++++++++++++++++------ tests/predict/test_rlm.py | 29 +++++++++++++--- tests/primitives/test_base_module.py | 41 +++++++++++++++++++++++ 8 files changed, 180 insertions(+), 41 deletions(-) diff --git a/dspy/predict/code_act.py b/dspy/predict/code_act.py index f53023993e..4a89a4923f 100644 --- a/dspy/predict/code_act.py +++ b/dspy/predict/code_act.py @@ -1,3 +1,4 @@ +import functools import inspect import logging from typing import Callable @@ -6,7 +7,7 @@ from dspy.adapters.types.tool import Tool from dspy.predict.program_of_thought import ProgramOfThought from dspy.predict.react import ReAct -from dspy.primitives.code_interpreter import CodeInterpreter, _validate_interpreter_factory +from dspy.primitives.code_interpreter import CodeInterpreter, _execution_instructions, _validate_interpreter_factory from dspy.primitives.python_interpreter import PythonInterpreter from dspy.signatures.signature import Signature, ensure_signature @@ -87,7 +88,7 @@ def _build_instructions(self, signature, tools): "For each iteration, you will generate a code snippet that either solves the task or progresses towards the solution.\n" "Ensure any output you wish to extract from the code is printed to the console. The code should be enclosed in a fenced code block.\n" f"When all information for producing the outputs ({outputs}) are available to be extracted, mark `finished=True` besides the final Python code.\n" - "You have access to the Python Standard Library and the following functions:" + "You have access to the execution environment and the following approved functions:" ) for idx, tool in enumerate(tools.values()): @@ -95,6 +96,19 @@ def _build_instructions(self, signature, tools): return instructions + @staticmethod + def _make_interpreter_tool(tool: Tool): + if inspect.iscoroutinefunction(tool.func): + async def invoke(**kwargs): + return await tool.acall(**kwargs) + else: + def invoke(**kwargs): + return tool(**kwargs) + + functools.update_wrapper(invoke, tool.func) + invoke.__signature__ = inspect.signature(tool.func) + return invoke + def forward(self, interpreter: CodeInterpreter | None = None, /, **kwargs): """Run the program with a fresh interpreter or a caller-owned override. @@ -110,14 +124,23 @@ def forward(self, interpreter: CodeInterpreter | None = None, /, **kwargs): "To use a caller-owned interpreter, pass it as the first positional argument when calling the module." ) with self._interpreter_context(interpreter) as interpreter: - # Define the tool functions in the interpreter + # Register approved host functions instead of copying their source + # into the guest, which breaks closures and bypasses host policy. for tool in self.tools.values(): - interpreter.execute(inspect.getsource(tool.func)) + interpreter.tools[tool.name] = self._make_interpreter_tool(tool) + if hasattr(interpreter, "_tools_registered"): + interpreter._tools_registered = False trajectory = {} max_iters = kwargs.pop("max_iters", self.max_iters) for idx in range(max_iters): - code_data = self.codeact(trajectory=trajectory, **kwargs) + codeact_args = {"trajectory": trajectory, **kwargs} + if instructions := _execution_instructions(interpreter): + if codeact_signature := getattr(self.codeact, "signature", None): + codeact_args["signature"] = codeact_signature.append_instructions( + f"\n\nExecution environment:\n{instructions}" + ) + code_data = self.codeact(**codeact_args) output = None code, error = self._parse_code(code_data) diff --git a/dspy/predict/rlm.py b/dspy/predict/rlm.py index 09ec51f381..138a67a4fa 100644 --- a/dspy/predict/rlm.py +++ b/dspy/predict/rlm.py @@ -10,7 +10,6 @@ from __future__ import annotations -import base64 import contextvars import functools import inspect @@ -32,6 +31,7 @@ CodeInterpreter, FinalOutput, _create_interpreter, + _execution_instructions, _validate_interpreter, _validate_interpreter_factory, ) @@ -62,7 +62,6 @@ - `llm_query_batched(prompts)` - query multiple prompts concurrently (much faster for multiple queries) - `print()` - ALWAYS print to see results - `SUBMIT({final_output_names})` - submit final output when done -- Standard libraries: re, json, collections, math, etc. IMPORTANT: This is ITERATIVE. Each code block you write will execute, you'll see the output, then you decide what to do next. Do NOT try to solve everything in one step. @@ -457,12 +456,9 @@ def _prepare_serializable_vars( try: payload_vars[raw_var_name] = payload.decode("utf-8") except UnicodeDecodeError: - encoded_var_name = f"{raw_var_name}_base64" - payload_vars[encoded_var_name] = base64.b64encode(payload).decode("ascii") - code_lines.extend([ - "import base64", - f"{raw_var_name} = base64.b64decode({encoded_var_name})", - ]) + encoded_var_name = f"{raw_var_name}_hex" + payload_vars[encoded_var_name] = payload.hex() + code_lines.append(f"{raw_var_name} = bytes.fromhex({encoded_var_name})") else: payload_vars[raw_var_name] = str(payload) @@ -479,7 +475,7 @@ def _prepare_serializable_vars( def _make_interpreter_tool(self, tool: Tool) -> Callable: """Preserve function metadata while routing execution through Tool.""" - if inspect.iscoroutinefunction(tool.func) or inspect.iscoroutinefunction(getattr(tool.func, "__call__", None)): + if inspect.iscoroutinefunction(tool.func) or inspect.iscoroutinefunction(type(tool.func).__call__): async def invoke(**kwargs): return await tool.acall(**kwargs) else: @@ -668,11 +664,17 @@ def _execute_iteration( ) -> Prediction | REPLHistory: """Execute one iteration. Returns Prediction if done, else updated REPLHistory.""" variables_info = [variable.format() for variable in variables] - action = self.generate_action( - variables_info=variables_info, - repl_history=history, - iteration=f"{iteration + 1}/{self.max_iters}", - ) + action_args = { + "variables_info": variables_info, + "repl_history": history, + "iteration": f"{iteration + 1}/{self.max_iters}", + } + if instructions := _execution_instructions(repl): + if action_signature := getattr(self.generate_action, "signature", None): + action_args["signature"] = action_signature.append_instructions( + f"\n\nExecution environment:\n{instructions}" + ) + action = self.generate_action(**action_args) if self.verbose: logger.info( f"RLM iteration {iteration + 1}/{self.max_iters}\n" @@ -761,11 +763,17 @@ async def _aexecute_iteration( ) -> Prediction | REPLHistory: """Async version: Execute one iteration.""" variables_info = [variable.format() for variable in variables] - pred = await self.generate_action.acall( - variables_info=variables_info, - repl_history=history, - iteration=f"{iteration + 1}/{self.max_iters}", - ) + action_args = { + "variables_info": variables_info, + "repl_history": history, + "iteration": f"{iteration + 1}/{self.max_iters}", + } + if instructions := _execution_instructions(repl): + if action_signature := getattr(self.generate_action, "signature", None): + action_args["signature"] = action_signature.append_instructions( + f"\n\nExecution environment:\n{instructions}" + ) + pred = await self.generate_action.acall(**action_args) if self.verbose: logger.info( f"RLM iteration {iteration + 1}/{self.max_iters}\n" diff --git a/dspy/primitives/base_module.py b/dspy/primitives/base_module.py index ab2214a69b..de2d89e34c 100644 --- a/dspy/primitives/base_module.py +++ b/dspy/primitives/base_module.py @@ -157,11 +157,23 @@ def dump_state(self, json_mode=True): return {name: param.dump_state(json_mode=json_mode) for name, param in self.named_parameters()} def load_state(self, state, *, allow_unsafe_lm_state=False): + import inspect + from dspy.predict.predict import Predict + def _accepts_unsafe_state(param): + try: + parameters = inspect.signature(param.load_state).parameters + except (TypeError, ValueError): + return isinstance(param, Predict) + explicit = parameters.get("allow_unsafe_lm_state") + if explicit is not None: + return explicit.kind in {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY} + return any(value.kind == inspect.Parameter.VAR_KEYWORD for value in parameters.values()) + def _apply(module): for name, param in module.named_parameters(): - if isinstance(param, Predict): + if _accepts_unsafe_state(param): param.load_state(state[name], allow_unsafe_lm_state=allow_unsafe_lm_state) else: param.load_state(state[name]) diff --git a/dspy/primitives/code_interpreter.py b/dspy/primitives/code_interpreter.py index ce43810aef..56db3984bd 100644 --- a/dspy/primitives/code_interpreter.py +++ b/dspy/primitives/code_interpreter.py @@ -169,6 +169,12 @@ def _create_interpreter(factory: Callable[[], CodeInterpreter]) -> CodeInterpret return interpreter +def _execution_instructions(interpreter: CodeInterpreter) -> str | None: + """Return optional generated-code guidance without expanding the required protocol.""" + instructions = getattr(interpreter, "execution_instructions", None) + return instructions.strip() if isinstance(instructions, str) and instructions.strip() else None + + def _validate_interpreter(interpreter: Any) -> None: """Validate a caller-owned interpreter.""" if not isinstance(interpreter, CodeInterpreter): diff --git a/dspy/primitives/python_interpreter.py b/dspy/primitives/python_interpreter.py index 66061b6347..3a46b65f03 100644 --- a/dspy/primitives/python_interpreter.py +++ b/dspy/primitives/python_interpreter.py @@ -157,6 +157,10 @@ def my_tool(question: str) -> str: ``` """ + execution_instructions = """This is CPython running in Pyodide. The Python standard library and +Pyodide-compatible packages are available, but subprocesses and native system interfaces are unavailable. Host +filesystem, environment, and network access exist only when explicitly enabled by the interpreter configuration.""" + def __init__( self, deno_command: list[str] | None = None, diff --git a/tests/predict/test_code_act.py b/tests/predict/test_code_act.py index 471b16a07a..faebb3cef1 100644 --- a/tests/predict/test_code_act.py +++ b/tests/predict/test_code_act.py @@ -1,4 +1,3 @@ -import threading from unittest.mock import Mock import pytest @@ -82,7 +81,7 @@ def test_codeact_support_multiple_fields(pooled_interpreter): assert res.maximum == "6" assert res.minimum == "2" assert res.trajectory == { - "code_output_0": '"{\'maximum\': 6.0, \'minimum\': 2.0}\\n"', + "code_output_0": '"{\'maximum\': 6, \'minimum\': 2}\\n"', "generated_code_0": "result = extract_maximum_minimum('2, 3, 5, 6')\nprint(result)", } @@ -144,12 +143,7 @@ def test_codeact_code_execution_failure(pooled_interpreter): def test_codeact_evaluate_creates_one_interpreter_per_example(): - tool_registration_barrier = threading.Barrier(4) - def execute(code, variables): - if code.startswith("def add"): - tool_registration_barrier.wait(timeout=30) - return "" return "2\n" factory = MockInterpreterFactory(execute_fn=execute) @@ -172,13 +166,14 @@ def execute(code, variables): assert len(factory.instances) == 4 assert len({id(interpreter) for interpreter in factory.instances}) == 4 for interpreter in factory.instances: - assert interpreter.call_count == 2 + assert interpreter.call_count == 1 + assert interpreter.tools["add"](a=1, b=1) == 2 with pytest.raises(CodeInterpreterError, match="shutdown"): interpreter.execute("print('closed')") def test_codeact_factory_creates_fresh_interpreter_per_sequential_call(): - factory = MockInterpreterFactory(responses=["", "2\n"]) + factory = MockInterpreterFactory(responses=["2\n"]) program = CodeAct(BasicQA, tools=[add], interpreter_factory=factory) program.codeact = StaticPredictor(generated_code="print(add(1, 1))", finished=True) program.extractor = StaticPredictor(answer="2") @@ -194,6 +189,37 @@ def test_codeact_factory_creates_fresh_interpreter_per_sequential_call(): interpreter.execute("print('closed')") +def test_codeact_uses_interpreter_instructions_without_changing_predictor_schema(): + seen = {} + + class CapturingPredictor: + def __init__(self, signature): + self.signature = signature + + def __call__(self, **kwargs): + seen.update(kwargs) + return dspy.Prediction(generated_code="print(add(1, 1))", finished=True) + + interpreter = MockInterpreter(responses=["2\n"]) + interpreter.execution_instructions = "Use the constrained test dialect." + program = CodeAct(BasicQA, tools=[add]) + program.codeact = CapturingPredictor(program.codeact.signature) + program.extractor = StaticPredictor(answer="2") + + assert program(interpreter, question="What is 1+1?").answer == "2" + assert "execution_instructions" not in seen + assert interpreter.execution_instructions in seen["signature"].instructions + + +@pytest.mark.asyncio +async def test_codeact_preserves_async_host_tool(): + async def async_add(a: int, b: int) -> int: + return a + b + + wrapped = CodeAct._make_interpreter_tool(dspy.Tool(async_add)) + assert await wrapped(a=20, b=22) == 42 + + def test_codeact_allows_interpreter_as_signature_input(): factory = MockInterpreterFactory(responses=["", "CPython\n"]) program = CodeAct("interpreter -> answer", tools=[add], interpreter_factory=factory) @@ -229,7 +255,7 @@ def test_codeact_does_not_shutdown_caller_owned_interpreter(): program = CodeAct(BasicQA, tools=[add], interpreter_factory=factory) program.codeact = StaticPredictor(generated_code="print(add(1, 1))", finished=True) program.extractor = StaticPredictor(answer="2") - interpreter = MockInterpreter(responses=["", "2\n"]) + interpreter = MockInterpreter(responses=["2\n"]) try: result = program(interpreter, question="What is 1+1?") @@ -256,7 +282,7 @@ def test_codeact_shuts_down_factory_interpreter_when_extractor_raises(): def test_codeact_propagates_terminal_interpreter_failure_and_shuts_down(): - factory = MockInterpreterFactory(responses=["", CodeInterpreterError("protocol corrupt")]) + factory = MockInterpreterFactory(responses=[CodeInterpreterError("protocol corrupt")]) program = CodeAct(BasicQA, tools=[add], interpreter_factory=factory) program.codeact = StaticPredictor(generated_code="print(add(1, 1))", finished=True) diff --git a/tests/predict/test_rlm.py b/tests/predict/test_rlm.py index 09c0ef8ac8..c4c2785ac0 100644 --- a/tests/predict/test_rlm.py +++ b/tests/predict/test_rlm.py @@ -6,7 +6,6 @@ - Integration tests (@pytest.mark.deno): PythonInterpreter with Deno """ -import base64 from contextlib import contextmanager import pytest @@ -556,6 +555,26 @@ def test_action_signature_has_iteration_field(self): rlm = RLM("context -> answer") action_sig = rlm.generate_action.signature assert "iteration" in action_sig.input_fields + assert "execution_instructions" not in action_sig.input_fields + + def test_action_receives_interpreter_execution_instructions(self): + seen = {} + + class CapturingPredictor: + def __call__(self, **kwargs): + seen.update(kwargs) + return Prediction(reasoning="done", code="SUBMIT(answer='ok')") + + mock = MockInterpreter(responses=[FinalOutput({"answer": "ok"})]) + mock.execution_instructions = "Use only the constrained test dialect." + rlm = RLM("context -> answer", max_iters=1) + predictor = CapturingPredictor() + predictor.signature = rlm.generate_action.signature + rlm.generate_action = predictor + + assert rlm.forward(mock, context="test").answer == "ok" + assert "execution_instructions" not in seen + assert mock.execution_instructions in seen["signature"].instructions def test_format_output(self): """Test output formatting.""" @@ -1620,8 +1639,8 @@ def test_no_serializable_returns_all(self): assert regular == {"query": "hello"} assert mock.call_count == 0 - def test_binary_payload_uses_base64_transport(self): - """Non-UTF8 bytes should be transported via base64 and decoded in sandbox code.""" + def test_binary_payload_uses_portable_hex_transport(self): + """Non-UTF8 bytes should use a transport supported by constrained interpreters.""" mock = MockInterpreter(responses=[""]) rlm = RLM("data, query -> answer") @@ -1631,8 +1650,8 @@ def test_binary_payload_uses_base64_transport(self): assert mock.call_count == 1 code, variables = mock.call_history[0] - assert "_raw_data = base64.b64decode(_raw_data_base64)" in code - assert variables["_raw_data_base64"] == base64.b64encode(b"\xff\xfe\xfd").decode("ascii") + assert "_raw_data = bytes.fromhex(_raw_data_hex)" in code + assert variables["_raw_data_hex"] == "fffefd" def test_large_payload_not_inlined_in_code(self): """Large payloads should ride in the variables kwarg, not the code string. diff --git a/tests/primitives/test_base_module.py b/tests/primitives/test_base_module.py index fb538f906b..d6af5af684 100644 --- a/tests/primitives/test_base_module.py +++ b/tests/primitives/test_base_module.py @@ -9,10 +9,51 @@ from litellm.types.utils import Usage import dspy +from dspy.predict.parameter import Parameter from dspy.primitives.prediction import Prediction from dspy.utils.dummies import DummyLM +def test_load_state_forwards_safe_flag_to_custom_parameter_kwargs(): + class CustomParameter(Parameter): + def __init__(self): + self.received = None + + def dump_state(self, json_mode=True): + return {} + + def load_state(self, state, **kwargs): + self.received = kwargs + + class CustomModule(dspy.Module): + def __init__(self): + self.parameter = CustomParameter() + + module = CustomModule() + module.load_state({"parameter": {}}, allow_unsafe_lm_state=True) + assert module.parameter.received == {"allow_unsafe_lm_state": True} + + +def test_load_state_preserves_custom_parameter_without_safe_flag(): + class CustomParameter(Parameter): + def __init__(self): + self.loaded = False + + def dump_state(self, json_mode=True): + return {} + + def load_state(self, state): + self.loaded = True + + class CustomModule(dspy.Module): + def __init__(self): + self.parameter = CustomParameter() + + module = CustomModule() + module.load_state({"parameter": {}}, allow_unsafe_lm_state=True) + assert module.parameter.loaded is True + + def test_deepcopy_basic(): signature = dspy.Signature("q -> a") cot = dspy.ChainOfThought(signature) From ed014e24407685edfc8bd4e5eba87011ab20d7c9 Mon Sep 17 00:00:00 2001 From: isaacbmiller Date: Fri, 31 Jul 2026 14:45:15 +0000 Subject: [PATCH 2/2] feat(experimental): add capability-limited Monty programs Add an optimizable MontyProgram and persistent MontyInterpreter with real DSPy syntax, approved host capabilities, shared budgets, resource limits, and depth-two nested RLM execution. Amp-Thread-ID: https://ampcode.com/threads/T-019fb0dc-8d11-7510-abc9-4bc180e4e6e4 Co-authored-by: Isaac Miller --- dspy/experimental/__init__.py | 14 + dspy/experimental/monty/__init__.py | 5 + dspy/experimental/monty/_bridge.py | 269 +++++++++++++++++++ dspy/experimental/monty/_interpreter.py | 140 ++++++++++ dspy/experimental/monty/_shim.py | 33 +++ dspy/experimental/monty/_source.py | 60 +++++ dspy/experimental/monty/program.py | 186 +++++++++++++ pyproject.toml | 2 + tests/experimental/test_monty.py | 340 ++++++++++++++++++++++++ uv.lock | 148 ++++++++++- 10 files changed, 1196 insertions(+), 1 deletion(-) create mode 100644 dspy/experimental/monty/__init__.py create mode 100644 dspy/experimental/monty/_bridge.py create mode 100644 dspy/experimental/monty/_interpreter.py create mode 100644 dspy/experimental/monty/_shim.py create mode 100644 dspy/experimental/monty/_source.py create mode 100644 dspy/experimental/monty/program.py create mode 100644 tests/experimental/test_monty.py diff --git a/dspy/experimental/__init__.py b/dspy/experimental/__init__.py index 651c7d97f2..35c92089f1 100644 --- a/dspy/experimental/__init__.py +++ b/dspy/experimental/__init__.py @@ -1,7 +1,21 @@ +from typing import TYPE_CHECKING, Any + from dspy.adapters.types.citation import Citations from dspy.adapters.types.document import Document +if TYPE_CHECKING: + from dspy.experimental.monty import MontyProgram + __all__ = [ "Citations", "Document", + "MontyProgram", ] + + +def __getattr__(name: str) -> Any: + if name == "MontyProgram": + from dspy.experimental.monty import MontyProgram + + return MontyProgram + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/dspy/experimental/monty/__init__.py b/dspy/experimental/monty/__init__.py new file mode 100644 index 0000000000..cffde16e90 --- /dev/null +++ b/dspy/experimental/monty/__init__.py @@ -0,0 +1,5 @@ +"""Experimental Monty Flex runtime.""" + +from .program import MontyProgram + +__all__ = ["MontyProgram"] diff --git a/dspy/experimental/monty/_bridge.py b/dspy/experimental/monty/_bridge.py new file mode 100644 index 0000000000..539f8ec89e --- /dev/null +++ b/dspy/experimental/monty/_bridge.py @@ -0,0 +1,269 @@ +"""Capability bridge and immutable policy for Monty invocations.""" + +from __future__ import annotations + +import ast +import inspect +import keyword +import threading +from dataclasses import dataclass +from typing import Any + +from pydantic_core import to_jsonable_python + +from dspy.adapters.types.tool import Tool +from dspy.primitives.example import Example + +RESERVED = { + "dspy", + "SUBMIT", + "print", + "llm_query", + "llm_query_batched", + "__dspy_construct__", + "__dspy_call__", + "__dspy_inputs__", +} +DEFAULT_LIMITS = {"max_duration_secs": 30.0, "max_memory": 64 * 1024 * 1024, "max_recursion_depth": 200} +CODE_KINDS = {"RLM", "CodeAct", "ProgramOfThought"} +RLM_DSPY_INSTRUCTIONS = """ + +This interpreter also provides a capability-limited `dspy` object. You may compose +real DSPy modules in your code, for example: +`child = dspy.RLM("context, question -> answer", max_iters=1)` followed by +`result = child(context=context, question=question)`. Available constructors are +Predict, ChainOfThought, RLM, CodeAct, ProgramOfThought, ReAct, and ReActV2. +Predictor results expose fields as attributes, such as `result.answer`. +""" + +_ANNOTATION_NAMES = { + "Any", + "Literal", + "NoneType", + "Optional", + "Union", + "bool", + "dict", + "float", + "int", + "list", + "set", + "str", + "tuple", +} + + +def _validate_signature_annotations(signature: str) -> None: + """Reject guest annotations that could resolve imports in DSPy's parser.""" + for fields in signature.split("->"): + function = ast.parse(f"def __dspy_signature({fields}): pass").body[0] + if function.args.defaults or function.args.vararg or function.args.kwarg or function.args.kwonlyargs: + raise ValueError("predictor signatures support only named fields without defaults") + for argument in function.args.args: + if argument.annotation is None: + continue + for node in ast.walk(argument.annotation): + if isinstance(node, ast.Name) and node.id not in _ANNOTATION_NAMES: + raise ValueError(f"unsupported predictor annotation: {node.id}") + if isinstance(node, (ast.Attribute, ast.Call)): + raise ValueError("predictor annotations cannot reference host modules or call functions") + + +def resolve_signature(value): + if isinstance(value, str): + _validate_signature_annotations(value) + return value + expected = {"__dspy_signature__", "signature", "instructions"} + if not isinstance(value, dict) or set(value) != expected or value["__dspy_signature__"] is not True: + raise ValueError("predictor signature must be a string or dspy.Signature marker") + if not isinstance(value["signature"], str) or not isinstance(value["instructions"], (str, type(None))): + raise ValueError("invalid dspy.Signature marker") + _validate_signature_annotations(value["signature"]) + from dspy.signatures.signature import make_signature + + return make_signature(value["signature"], value["instructions"]) + + +def jsonable(value: Any) -> Any: + if isinstance(value, Example): + value = value.toDict() + return to_jsonable_python(value) + + +@dataclass(frozen=True) +class Policy: + max_predictor_calls: int + max_tool_calls: int + max_nested_depth: int + limits: dict[str, Any] + request_timeout: float | None + + +class Budget: + def __init__(self, policy: Policy): + self.policy, self.predictors, self.tools, self.lock = policy, 0, 0, threading.Lock() + + def count(self, kind: str) -> None: + with self.lock: + attr, maximum = ( + ("predictors", self.policy.max_predictor_calls) + if kind == "predictor" + else ("tools", self.policy.max_tool_calls) + ) + setattr(self, attr, getattr(self, attr) + 1) + if getattr(self, attr) > maximum: + raise RuntimeError(f"Monty program exceeded its limit of {maximum} {kind} calls") + + +def normalize_tools(values: list[Any]) -> dict[str, Tool]: + result = {} + for value in values: + tool = value if isinstance(value, Tool) else Tool(value) + if inspect.iscoroutinefunction(tool.func): + raise ValueError("MontyProgram does not support asynchronous tools") + if not tool.name.isidentifier() or keyword.iskeyword(tool.name): + raise ValueError(f"invalid Monty tool name: {tool.name!r}") + if tool.name in RESERVED or tool.name.startswith("__dspy"): + raise ValueError(f"Monty tool name {tool.name!r} is reserved") + if tool.name in result: + raise ValueError(f"Duplicate Monty tool name: {tool.name!r}") + result[tool.name] = tool + return result + + +class Invocation: + def __init__( + self, *, policy: Policy, tools: dict[str, Tool], lm: Any, depth: int = 0, budget: Budget | None = None + ): + self.policy, self.tools, self.lm, self.depth = policy, tools, lm, depth + self.budget = budget or Budget(policy) + self.registry = {} + + def _int(self, config, name, default, maximum): + value = config.pop(name, default) + if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= maximum: + raise ValueError(f"{name} must be an integer between 1 and {maximum}") + return value + + def _tools(self, config, signature): + values = config.pop("tools", []) + if not isinstance(values, list): + raise ValueError("tools must be a list of approved tools") + selected = {} + for value in values: + # Monty represents an external callable by its lookup name when it + # crosses back into a host callback. + name = value if isinstance(value, str) else None + if name not in self.tools: + raise ValueError("tools may only reference approved host tools") + selected[name] = self.tools[name] + from dspy.signatures.signature import ensure_signature + + collisions = set(ensure_signature(signature).input_fields) & set(selected) + if collisions: + raise ValueError(f"predictor inputs conflict with selected tools: {sorted(collisions)}") + return selected + + def _budgeted_tools(self, selected): + result = [] + for tool in selected.values(): + + def invoke(_tool=tool, **kwargs): + self.budget.count("tool") + return _tool(**kwargs) + + result.append( + Tool( + invoke, + name=tool.name, + desc=tool.desc, + args=tool.args, + arg_types=tool.arg_types, + arg_desc=tool.arg_desc, + ) + ) + return result + + def construct(self, kind, signature, config): + if kind not in {"Predict", "ChainOfThought", "RLM", "CodeAct", "ProgramOfThought", "ReAct", "ReActV2"}: + raise ValueError(f"unsupported predictor kind: {kind}") + resolved_signature = resolve_signature(signature) + if isinstance(resolved_signature, str) and resolved_signature.count("->") != 1: + raise ValueError("predictor signature string must contain one '->'") + if not isinstance(config, dict): + raise ValueError("predictor configuration must be a dictionary") + config = dict(config) + from dspy import RLM, ChainOfThought, CodeAct, Predict, ProgramOfThought, ReAct, ReActV2 + + types = { + "Predict": Predict, + "ChainOfThought": ChainOfThought, + "RLM": RLM, + "CodeAct": CodeAct, + "ProgramOfThought": ProgramOfThought, + "ReAct": ReAct, + "ReActV2": ReActV2, + } + kwargs, selected_tools = {}, {} + if kind in {"ReAct", "ReActV2", "RLM", "CodeAct"}: + selected_tools = self._tools(config, resolved_signature) + kwargs["tools"] = self._budgeted_tools(selected_tools) + if kind in {"ReAct", "ReActV2", "RLM", "CodeAct", "ProgramOfThought"}: + kwargs["max_iters"] = self._int(config, "max_iters", 20 if kind in {"ReAct", "ReActV2", "RLM"} else 5, 100) + if kind == "RLM": + kwargs["max_llm_calls"] = self._int(config, "max_llm_calls", 50, 100) + kwargs["max_output_chars"] = self._int(config, "max_output_chars", 10_000, 100_000) + verbose = config.pop("verbose", False) + if not isinstance(verbose, bool): + raise ValueError("verbose must be boolean") + kwargs.update(verbose=verbose, sub_lm=self.lm) + if config: + raise ValueError(f"unsupported {kind} configuration: {sorted(config)}") + if kind in CODE_KINDS: + if self.depth >= self.policy.max_nested_depth: + raise RuntimeError(f"code execution nesting exceeds maximum depth of {self.policy.max_nested_depth}") + kwargs["interpreter_factory"] = self.interpreter_factory(selected_tools) + predictor = types[kind](resolved_signature, **kwargs) + if kind == "RLM": + instructions = predictor.generate_action.signature.instructions + RLM_DSPY_INSTRUCTIONS + predictor.generate_action.signature = predictor.generate_action.signature.with_instructions(instructions) + if self.lm is not None: + predictor.set_lm(self.lm) + handle = f"predictor_{len(self.registry)}" + self.registry[handle] = (kind, predictor) + return handle + + def interpreter_factory(self, tools): + def factory(): + from ._interpreter import MontyInterpreter + + return MontyInterpreter(self.child(tools)) + + return factory + + def child(self, tools): + return Invocation(policy=self.policy, tools=tools, lm=self.lm, depth=self.depth + 1, budget=self.budget) + + def call(self, handle, inputs): + if set(inputs) & {"config", "demos", "lm", "new_signature", "signature", "sub_lm", "interpreter_factory"}: + raise ValueError("predictor call cannot override control arguments") + try: + kind, predictor = self.registry[handle] + except KeyError as error: + raise ValueError("unknown or expired predictor handle") from error + if kind in {"ReAct", "ReActV2", "CodeAct"} and "max_iters" in inputs: + raise ValueError(f"{kind} max_iters can only be configured at construction") + self.budget.count("predictor") + return jsonable(predictor(**inputs)) + + def lookup(self): + result = {"__dspy_construct__": self.construct, "__dspy_call__": self.call} + for name, tool in self.tools.items(): + + def invoke(*args, _tool=tool, **kwargs): + self.budget.count("tool") + arguments = inspect.signature(_tool.func).bind(*args, **kwargs).arguments + return jsonable(_tool(**arguments)) + + result[name] = invoke + return result diff --git a/dspy/experimental/monty/_interpreter.py b/dspy/experimental/monty/_interpreter.py new file mode 100644 index 0000000000..be59b3454d --- /dev/null +++ b/dspy/experimental/monty/_interpreter.py @@ -0,0 +1,140 @@ +"""Private persistent Monty CodeInterpreter adapter.""" + +from __future__ import annotations + +import ast +import inspect +import textwrap +from typing import Any + +from dspy.primitives.code_interpreter import CodeExecutionError, CodeInterpreterError, FinalOutput + +from ._bridge import Invocation, jsonable +from ._shim import SHIM + + +class MontyInterpreter: + execution_instructions = """This is a constrained Monty Python environment, not CPython. +Use plain functions, loops, comprehensions, and built-in containers. Available modules include json, math, re, +datetime, pathlib, typing, and limited asyncio. Do not use package installation, subprocesses, threads, sockets, +eval/exec, runtime introspection, class inheritance, properties, generators, or custom magic methods. Prefer +approved host tools and llm_query_batched for I/O and concurrent semantic work. The capability-limited dspy facade +supports Predict, ChainOfThought, RLM, CodeAct, ProgramOfThought, ReAct, and ReActV2.""" + + def __init__(self, invocation: Invocation): + self.invocation = invocation + self.tools = {} + self.output_fields = None + self._pool = self._checkout = self._session = None + self._closed = False + self._terminal_error = None + + def start(self): + if self._session is not None: + return + if self._terminal_error is not None: + raise CodeInterpreterError("MontyInterpreter session has ended; create a new interpreter for a fresh session.") + if self._closed: + raise CodeInterpreterError("interpreter has been shut down") + try: + from pydantic_monty import Monty + except ImportError as error: + raise ImportError("Monty support requires `pip install 'dspy[monty]'`") from error + pool = Monty(request_timeout=self.invocation.policy.request_timeout) + try: + pool.__enter__() + checkout = pool.checkout(limits=self.invocation.policy.limits) + session = checkout.__enter__() + except Exception as error: + try: + pool.__exit__(None, None, None) + except Exception: + pass + raise CodeInterpreterError(f"failed to start Monty interpreter: {error}") from error + self._pool, self._checkout, self._session = pool, checkout, session + + def execute(self, code: str, variables: dict[str, Any] | None = None): + self.start() + names = [field["name"] for field in (self.output_fields or [])] + tree = ast.parse(code) + if tree.body and isinstance(tree.body[-1], ast.Expr): + tree.body[-1] = ast.copy_location( + ast.Assign(targets=[ast.Name(id="__dspy_result", ctx=ast.Store())], value=tree.body[-1].value), + tree.body[-1], + ) + ast.fix_missing_locations(tree) + code = ast.unparse(tree) + wrapper = f"""\nimport json as __dspy_json +def SUBMIT(*args, **kwargs): + names = {names!r} + if names: + if args: + if kwargs or len(args) != len(names): raise ValueError("SUBMIT arguments do not match output fields") + kwargs = dict(zip(names, args)) + elif set(kwargs) != set(names): raise ValueError("SUBMIT arguments do not match output fields") + else: + if kwargs or len(args) != 1: raise ValueError("SUBMIT requires exactly one positional output") + kwargs = {{"output": args[0]}} + raise BaseException("__DSPY_FINAL__" + __dspy_json.dumps(kwargs)) +__dspy_result = None +try: +{textwrap.indent(code, " ")} +except BaseException as error: + __dspy_message = str(error) + if not __dspy_message.startswith("__DSPY_FINAL__"): raise + __dspy_result = {{"__dspy_final__": __dspy_json.loads(__dspy_message[14:])}} +__dspy_result +""" + printed = [] + lookup = self.invocation.lookup() + for name, tool in self.tools.items(): + if name in lookup: + # Invocation-owned tools retain their aggregate-budget wrappers. + continue + if name in {"llm_query", "llm_query_batched"}: + lookup[name] = tool + continue + + def invoke(*args, _tool=tool, **kwargs): + self.invocation.budget.count("tool") + arguments = inspect.signature(_tool).bind(*args, **kwargs).arguments + return jsonable(_tool(**arguments)) + + lookup[name] = invoke + try: + from pydantic_monty import MontyCrashedError, MontyError + + result = self._session.feed_run( + SHIM + wrapper, + inputs=jsonable(variables or {}), + external_lookup=lookup, + print_callback=lambda _stream, value: printed.append(value), + ) + except MontyCrashedError as error: + message = f"{error}; interpreter state was lost. Create a new interpreter for a fresh session." + try: + self.shutdown() + except Exception: + pass + self._terminal_error = message + raise CodeInterpreterError(message) from error + except MontyError as error: + raise CodeExecutionError(str(error)) from error + except Exception as error: + raise CodeInterpreterError(str(error)) from error + if isinstance(result, dict) and "__dspy_final__" in result: + return FinalOutput(result["__dspy_final__"]) + return "".join(printed).rstrip("\n") if printed else result + + def shutdown(self): + if self._closed: + return + checkout, pool = self._checkout, self._pool + self._pool = self._checkout = self._session = None + self._closed = True + try: + if checkout is not None: + checkout.__exit__(None, None, None) + finally: + if pool is not None: + pool.__exit__(None, None, None) diff --git a/dspy/experimental/monty/_shim.py b/dspy/experimental/monty/_shim.py new file mode 100644 index 0000000000..77c734ff93 --- /dev/null +++ b/dspy/experimental/monty/_shim.py @@ -0,0 +1,33 @@ +"""The deliberately small DSPy facade installed in Monty guests.""" + +SHIM = r""" +class _Module: pass +class _PredictorResult: + def __init__(self, fields): + self._fields = fields + for name, value in fields.items(): setattr(self, name, value) + def get(self, name, default=None): return self._fields.get(name, default) +def _dspy_fields(value): + return value._fields if isinstance(value, _PredictorResult) else value +def _predictor(kind, signature, config): + handle = __dspy_construct__(kind, signature, config) + def call(**inputs): return _PredictorResult(__dspy_call__(handle, inputs)) + return call +class _DSPy: + Module = _Module + def Signature(self, signature, instructions=None): + return {"__dspy_signature__": True, "signature": signature, "instructions": instructions} + def Prediction(self, **fields): return fields + def Tool(self, tool): return tool +dspy = _DSPy() +def _constructor(kind): + def construct(signature, **config): return _predictor(kind, signature, config) + return construct +dspy.Predict = _constructor("Predict") +dspy.ChainOfThought = _constructor("ChainOfThought") +dspy.RLM = _constructor("RLM") +dspy.CodeAct = _constructor("CodeAct") +dspy.ProgramOfThought = _constructor("ProgramOfThought") +dspy.ReAct = _constructor("ReAct") +dspy.ReActV2 = _constructor("ReActV2") +""" diff --git a/dspy/experimental/monty/_source.py b/dspy/experimental/monty/_source.py new file mode 100644 index 0000000000..d1ee7e2e4a --- /dev/null +++ b/dspy/experimental/monty/_source.py @@ -0,0 +1,60 @@ +"""Validation and execution-only lowering for Flex module source.""" + +import ast + + +def compile_source(source: str) -> tuple[str, str]: + """Return the module class name and Monty-compatible source.""" + if not isinstance(source, str) or not source.strip(): + raise ValueError("module_src must be a non-empty string") + try: + tree = ast.parse(source) + except SyntaxError as error: + raise ValueError(f"Invalid module_src: {error}") from error + classes = [node for node in tree.body if isinstance(node, ast.ClassDef)] + if len(classes) != 1 or any(not isinstance(node, (ast.ClassDef, ast.Pass)) for node in tree.body): + raise ValueError("module_src must contain exactly one top-level class and no imports or executable statements") + cls = classes[0] + if cls.decorator_list or cls.keywords: + raise ValueError("the module class cannot use decorators or class keywords") + if len(cls.bases) != 1 or not ( + isinstance(cls.bases[0], ast.Attribute) + and isinstance(cls.bases[0].value, ast.Name) + and cls.bases[0].value.id == "dspy" + and cls.bases[0].attr == "Module" + ): + raise ValueError("the top-level class must subclass dspy.Module") + methods = {node.name: node for node in cls.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))} + allowed_members = (ast.FunctionDef, ast.AsyncFunctionDef, ast.Pass) + for index, node in enumerate(cls.body): + is_docstring = ( + index == 0 + and isinstance(node, ast.Expr) + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + ) + if not isinstance(node, allowed_members) and not is_docstring: + raise ValueError("the module class can contain only methods and a docstring") + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.decorator_list: + raise ValueError("module methods cannot use decorators") + if "__init__" not in methods or "forward" not in methods: + raise ValueError("the module class must define __init__ and forward") + if any(isinstance(node, ast.AsyncFunctionDef) for node in ast.walk(cls)): + raise ValueError("async methods are not supported by MontyProgram") + cls.bases = [] + init = methods["__init__"] + for index, statement in enumerate(init.body): + if ( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and not statement.value.args + and not statement.value.keywords + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "__init__" + and isinstance(statement.value.func.value, ast.Call) + and isinstance(statement.value.func.value.func, ast.Name) + and statement.value.func.value.func.id == "super" + ): + init.body[index] = ast.copy_location(ast.Pass(), statement) + ast.fix_missing_locations(tree) + return cls.name, ast.unparse(tree) diff --git a/dspy/experimental/monty/program.py b/dspy/experimental/monty/program.py new file mode 100644 index 0000000000..577c396297 --- /dev/null +++ b/dspy/experimental/monty/program.py @@ -0,0 +1,186 @@ +"""Public Flex-style source program runtime.""" + +from __future__ import annotations + +import pydantic + +from dspy.adapters.utils import parse_value +from dspy.predict.parameter import Parameter +from dspy.primitives.module import Module +from dspy.primitives.prediction import Prediction +from dspy.signatures.signature import ensure_signature +from dspy.utils.annotation import experimental + +from ._bridge import DEFAULT_LIMITS, Invocation, Policy, jsonable, normalize_tools +from ._shim import SHIM +from ._source import compile_source + + +@experimental +class MontyProgram(Module, Parameter): + """Execute optimizable DSPy orchestration source in a Monty sandbox. + + ``module_src`` must define one ``dspy.Module`` subclass with ``__init__`` + and synchronous ``forward`` methods. The guest can compose ``Predict``, + ``ChainOfThought``, ``RLM``, ``CodeAct``, ``ProgramOfThought``, ``ReAct``, + and ``ReActV2`` using normal DSPy constructor and call syntax. Real modules, + LMs, credentials, and tools remain in the host process; Monty receives only + JSON-compatible values and opaque predictor/tool capabilities. + + A fresh invocation owns each outer sandbox and predictor registry. Nested + code-executing modules receive distinct Monty sessions, selected tools only, + and the invocation's shared predictor/tool budgets. Those budgets count + bridged module and host-tool calls, not every internal LM turn in a compound + module; ``max_iters`` and RLM's ``max_llm_calls`` bound those turns. + + If source is omitted, the baseline uses ``Predict`` or, when tools are + supplied, ``RLM``. The source is this module's single opaque ``Parameter``. + Async execution and custom signature annotations inside guest-authored + predictors are not currently supported. + """ + + def __init__( + self, + signature, + module_src=None, + *, + tools=None, + max_predictor_calls=100, + max_tool_calls=100, + max_nested_depth=2, + limits=None, + request_timeout=120.0, + callbacks=None, + ): + super().__init__(callbacks=callbacks) + self._signature = ensure_signature(signature) + self._tools = normalize_tools(tools or []) + collisions = set(self.signature.input_fields) & set(self._tools) + if collisions: + raise ValueError(f"program inputs conflict with tools: {sorted(collisions)}") + if module_src is None: + module_src = self._baseline(bool(tools)) + self._module_src = module_src + self._class_name, self._execution_src = compile_source(module_src) + if not isinstance(max_predictor_calls, int) or max_predictor_calls < 1: + raise ValueError("max_predictor_calls must be positive") + if not isinstance(max_tool_calls, int) or max_tool_calls < 1: + raise ValueError("max_tool_calls must be positive") + if not isinstance(max_nested_depth, int) or not 0 <= max_nested_depth <= 2: + raise ValueError("max_nested_depth must be between 0 and 2") + self._policy = Policy( + max_predictor_calls, max_tool_calls, max_nested_depth, {**DEFAULT_LIMITS, **(limits or {})}, request_timeout + ) + self._lm = None + + @property + def signature(self): + return self._signature + + @property + def module_src(self): + return self._module_src + + def _baseline(self, tools): + inputs = ", ".join(self._signature.input_fields) + kwargs = ", ".join(f"{name}={name}" for name in self._signature.input_fields) + outputs = ", ".join(f"{name}=result.{name}" for name in self._signature.output_fields) + kind = "RLM" if tools else "Predict" + tool_config = f", tools=[{', '.join(self._tools)}]" if tools else "" + predictor_signature = self._render_signature() + signature_arg = f"dspy.Signature({predictor_signature!r}, {self._signature.instructions!r})" + forward_args = f", {inputs}" if inputs else "" + return ( + f"class MontyModule(dspy.Module):\n def __init__(self):\n" + f" super().__init__()\n self.p = dspy.{kind}({signature_arg}{tool_config})\n\n" + f" def forward(self{forward_args}):\n result = self.p({kwargs})\n" + f" return dspy.Prediction({outputs})\n" + ) + + def _render_signature(self): + from dspy.adapters.utils import get_annotation_name + + def render(fields): + values = [] + for name, field in fields.items(): + annotation = get_annotation_name(field.annotation) + try: + ensure_signature(f"value: {annotation} -> output") + except Exception: + values.append(name) + else: + values.append(f"{name}: {annotation}") + return ", ".join(values) + + return f"{render(self.signature.input_fields)} -> {render(self.signature.output_fields)}" + + def _bind_code(self, source): + name, execution = compile_source(source) + self._module_src, self._class_name, self._execution_src = source, name, execution + + def named_predictors(self): + return [] + + def set_lm(self, lm): + self._lm = lm + + def get_lm(self): + return self._lm + + def reset(self): + self._lm = None + + def dump_state(self, json_mode=True): + return {"module_src": self.module_src, "lm": self._lm.dump_state() if self._lm is not None else None} + + def load_state(self, state, *, allow_unsafe_lm_state=False): + self._bind_code(state["module_src"]) + lm_state = state.get("lm") + if lm_state is None: + self._lm = None + else: + from dspy.clients.base_lm import BaseLM + from dspy.predict.predict import _sanitize_lm_state + + safe = _sanitize_lm_state(lm_state, allow_unsafe_lm_state) + self._lm = BaseLM.load_state(safe, allow_custom_lm_class=allow_unsafe_lm_state) + return self + + def forward(self, **input_args): + missing = set(self.signature.input_fields) - set(input_args) + if missing: + raise ValueError(f"Missing required inputs: {sorted(missing)}") + try: + from pydantic_monty import Monty + except ImportError as error: + raise ImportError("MontyProgram requires `pip install 'dspy[monty]'`") from error + invocation = Invocation(policy=self._policy, tools=self._tools, lm=self._lm) + source = ( + SHIM + "\n" + self._execution_src + f"\n__dspy_module = {self._class_name}()\n" + "_dspy_fields(__dspy_module.forward(**__dspy_inputs__))\n" + ) + inputs = {name: input_args[name] for name in self.signature.input_fields} + with Monty(request_timeout=self._policy.request_timeout) as pool: + with pool.checkout(limits=self._policy.limits) as session: + output = session.feed_run( + source, + inputs={"__dspy_inputs__": jsonable(inputs)}, + external_lookup=invocation.lookup(), + ) + if not isinstance(output, dict): + raise TypeError("Monty forward() must return dspy.Prediction or dict") + missing = set(self.signature.output_fields) - set(output) + if missing: + raise ValueError(f"Monty forward() did not return output fields: {sorted(missing)}") + result, errors = {}, [] + for name, field in self.signature.output_fields.items(): + try: + result[name] = parse_value(output[name], field.annotation) + except (TypeError, ValueError, pydantic.ValidationError) as error: + errors.append(f"{name}: {error}") + if errors: + raise ValueError("Monty returned invalid output fields: " + "; ".join(errors)) + return Prediction(**result) + + async def aforward(self, **input_args): + raise NotImplementedError("MontyProgram supports synchronous forward only") diff --git a/pyproject.toml b/pyproject.toml index 39353ed93f..3c479dc0fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ anthropic = ["anthropic>=0.18.0,<1.0.0"] weaviate = ["weaviate-client>=4.5.4,<4.22.0"] mcp = ["mcp; python_version >= '3.10'"] +monty = ["pydantic-monty>=0.0.19"] langchain = ["langchain_core>=0.3.0"] optuna = ["optuna>=3.4.0"] numpy = ["numpy>=1.26.0"] @@ -61,6 +62,7 @@ dev = [ ] test_extras = [ "mcp; python_version >= '3.10'", + "pydantic-monty>=0.0.19", "datasets>=2.14.6", "pandas>=2.1.1", "optuna>=3.4.0", diff --git a/tests/experimental/test_monty.py b/tests/experimental/test_monty.py new file mode 100644 index 0000000000..ada38adc58 --- /dev/null +++ b/tests/experimental/test_monty.py @@ -0,0 +1,340 @@ +from unittest.mock import Mock, patch + +import pytest + +from dspy.experimental import MontyProgram +from dspy.experimental.monty._bridge import DEFAULT_LIMITS, Invocation, Policy, normalize_tools +from dspy.experimental.monty._interpreter import MontyInterpreter +from dspy.predict.rlm import RLM +from dspy.primitives.code_interpreter import CodeExecutionError, CodeInterpreterError, FinalOutput +from dspy.primitives.sandbox_serializable import SandboxSerializable +from dspy.utils import DummyLM + +pytest.importorskip("pydantic_monty") + + +def source(body, init="pass"): + return f"""class TestModule(dspy.Module): + def __init__(self): + super().__init__() + {init} + + def forward(self, **kwargs): + {body} +""" + + +def invocation(tools=(), *, max_tools=100, depth=2): + policy = Policy(100, max_tools, depth, DEFAULT_LIMITS, 120.0) + return Invocation(policy=policy, tools=normalize_tools(list(tools)), lm=None) + + +def test_baseline_predict_and_typed_output(): + program = MontyProgram("question -> answer: int") + program.set_lm(DummyLM([{"answer": "4"}])) + assert program(question="2+2?").answer == 4 + + +def test_baseline_without_inputs_is_valid_source(): + program = MontyProgram("-> answer") + program.set_lm(DummyLM([{"answer": "ready"}])) + assert program().answer == "ready" + + +def test_flex_source_predict(): + program = MontyProgram( + "question -> answer", + source( + "result = self.p(question=kwargs['question'])\n return dspy.Prediction(answer=result.answer)", + "self.p = dspy.ChainOfThought('question -> answer')", + ), + ) + program.set_lm(DummyLM([{"reasoning": "add", "answer": "4"}])) + assert program(question="2+2?").answer == "4" + + +def test_dspy_signature_syntax_preserves_instructions(): + program = MontyProgram( + "question -> answer", + source( + "return self.p(question=kwargs['question'])", + "self.p = dspy.Predict(dspy.Signature('question -> answer', 'Answer very briefly.'))", + ), + ) + program.set_lm(DummyLM([{"answer": "brief"}])) + assert program(question="question").answer == "brief" + + +def test_depth_two_sub_rlm_uses_real_dspy_syntax(): + program = MontyProgram( + "context, question -> answer", + source("return self.root(context=kwargs['context'], question=kwargs['question'])", "self.root = dspy.RLM('context, question -> answer', max_iters=1)"), + ) + program.set_lm( + DummyLM( + [ + { + "reasoning": "Delegate to a child RLM", + "code": ( + "child = dspy.RLM('context, question -> answer', max_iters=1)\n" + "result = child(context=context, question=question)\n" + "SUBMIT(answer=result.answer)" + ), + }, + {"reasoning": "Answer in the child", "code": "SUBMIT(answer='nested answer')"}, + ] + ) + ) + assert program(context="facts", question="question").answer == "nested answer" + + +def test_direct_tool_and_budget(): + def add(a, b): + return a + b + + program = MontyProgram( + "a, b -> answer: int", + source("return dspy.Prediction(answer=add(a=kwargs['a'], b=kwargs['b']))"), + tools=[add], + max_tool_calls=1, + ) + assert program(a=20, b=22).answer == 42 + + +def test_interpreter_tool_is_callable_and_delegatable(): + def add(a, b): + return a + b + + current = invocation([add]) + interpreter = MontyInterpreter(current) + interpreter.tools["add"] = lambda **kwargs: -1 + try: + assert interpreter.execute("add(20, b=22)") == 42 + interpreter.execute("child = dspy.RLM('question -> answer', tools=[dspy.Tool(add)], max_iters=1)") + finally: + interpreter.shutdown() + + _, child = current.registry["predictor_0"] + assert set(child.tools) == {"add"} + + +def test_interpreter_honors_dynamically_injected_tools_and_budget(): + interpreter = MontyInterpreter(invocation(max_tools=1)) + interpreter.tools["add"] = lambda a, b: a + b + try: + assert interpreter.execute("add(20, 22)") == 42 + with pytest.raises(CodeExecutionError, match="limit of 1 tool calls"): + interpreter.execute("add(a=1, b=1)") + finally: + interpreter.shutdown() + + +def test_rlm_binary_sandbox_serializable_setup_uses_monty_compatible_transport(): + class BinaryValue(SandboxSerializable): + def sandbox_setup(self): + return "" + + def to_sandbox(self): + return b"\xff\x00" + + def sandbox_assignment(self, var_name, data_expr): + return f"{var_name} = {data_expr}" + + def rlm_preview(self, max_chars=500): + return "binary" + + interpreter = MontyInterpreter(invocation()) + try: + RLM("data -> answer")._prepare_serializable_vars({"data": BinaryValue()}, interpreter) + assert interpreter.execute("data.hex()") == "ff00" + finally: + interpreter.shutdown() + + +def test_compound_predictors_share_tool_budget(): + calls = [] + + def record(value: str) -> str: + calls.append(value) + return value + + current = invocation([record], max_tools=1) + first = current.construct("ReAct", "question -> answer", {"tools": ["record"], "max_iters": 1}) + current.registry[first][1].tools["record"](value="first") + second = current.construct("ReActV2", "question -> answer", {"tools": ["record"], "max_iters": 1}) + with pytest.raises(RuntimeError, match="limit of 1 tool calls"): + current.registry[second][1].tools["record"](value="second") + assert calls == ["first"] + + +def test_llm_query_accepts_advertised_positional_call(): + interpreter = MontyInterpreter(invocation()) + interpreter.tools["llm_query"] = lambda prompt: f"answer: {prompt}" + try: + assert interpreter.execute("llm_query('question')") == "answer: question" + finally: + interpreter.shutdown() + + +def test_nested_interpreter_exposes_only_selected_tools(): + def first(): + return "first" + + def second(): + return "second" + + current = invocation([first, second]) + handle = current.construct("RLM", "question -> answer", {"tools": ["first"], "max_iters": 1}) + child = current.registry[handle][1]._interpreter_factory().invocation + assert set(child.tools) == {"first"} + + +def test_third_code_execution_level_is_rejected(): + root = invocation() + outer_handle = root.construct("RLM", "question -> answer", {"max_iters": 1}) + depth_one = root.registry[outer_handle][1]._interpreter_factory().invocation + child_handle = depth_one.construct("RLM", "question -> answer", {"max_iters": 1}) + depth_two = depth_one.registry[child_handle][1]._interpreter_factory().invocation + with pytest.raises(RuntimeError, match="maximum depth of 2"): + depth_two.construct("RLM", "question -> answer", {"max_iters": 1}) + + +def test_call_time_max_iters_cannot_bypass_constructor_limit(): + current = invocation() + handle = current.construct("ReAct", "question -> answer", {"max_iters": 1}) + with pytest.raises(ValueError, match="construction"): + current.call(handle, {"question": "q", "max_iters": 1_000_000}) + + +def test_untyped_submit_matches_code_interpreter_contract(): + interpreter = MontyInterpreter(invocation()) + try: + assert interpreter.execute("SUBMIT({'answer': 42})") == FinalOutput({"output": {"answer": 42}}) + with pytest.raises(CodeExecutionError, match="exactly one positional"): + interpreter.execute("SUBMIT(answer=42)") + finally: + interpreter.shutdown() + + +def test_submit_cannot_be_swallowed_by_guest_exception_handler(): + interpreter = MontyInterpreter(invocation()) + interpreter.output_fields = [{"name": "answer"}] + try: + result = interpreter.execute( + "try:\n SUBMIT(answer='ok')\nexcept Exception:\n pass\nraise RuntimeError('must not execute')" + ) + assert result == FinalOutput({"answer": "ok"}) + finally: + interpreter.shutdown() + + +@pytest.mark.parametrize( + "signature", + ["value: unknown_package.Type -> answer", "value: UnknownType -> answer"], +) +def test_guest_predictor_annotations_cannot_import_host_modules(signature): + current = invocation() + with patch("dspy.signatures.signature.importlib.import_module") as import_module: + with pytest.raises(ValueError, match=r"annotation|host modules"): + current.construct("Predict", signature, {}) + import_module.assert_not_called() + + +def test_interpreter_is_persistent_and_shutdown_is_terminal(): + interpreter = MontyInterpreter(invocation()) + interpreter.execute("value = 41") + assert interpreter.execute("value + 1") == 42 + with pytest.raises(CodeExecutionError): + interpreter.execute("missing_name") + assert interpreter.execute("value") == 41 + interpreter.shutdown() + interpreter.shutdown() + with pytest.raises(CodeInterpreterError, match="shut down"): + interpreter.execute("value") + + +def test_worker_timeout_is_terminal(): + policy = Policy(100, 100, 2, DEFAULT_LIMITS, 0.1) + interpreter = MontyInterpreter(Invocation(policy=policy, tools={}, lm=None)) + with pytest.raises(CodeInterpreterError, match="state was lost"): + interpreter.execute("while True:\n pass") + with pytest.raises(CodeInterpreterError, match="session has ended"): + interpreter.execute("1 + 1") + + +def test_startup_failure_is_wrapped_and_pool_is_cleaned_up(): + pool = Mock() + pool.__enter__ = Mock(side_effect=RuntimeError("worker unavailable")) + pool.__exit__ = Mock() + interpreter = MontyInterpreter(invocation()) + + with patch("pydantic_monty.Monty", return_value=pool): + with pytest.raises(CodeInterpreterError, match=r"failed to start.*worker unavailable"): + interpreter.start() + + pool.__exit__.assert_called_once() + assert interpreter._pool is interpreter._checkout is interpreter._session is None + + +def test_tool_and_input_names_cannot_collide(): + def question(): + return "question" + + with pytest.raises(ValueError, match="conflict"): + MontyProgram("question -> answer", tools=[question]) + + +def test_reserved_rlm_tool_names_are_rejected(): + def query(prompt): + return prompt + + query.__name__ = "llm_query" + with pytest.raises(ValueError, match="reserved"): + MontyProgram("question -> answer", tools=[query]) + + +@pytest.mark.parametrize( + "bad", + [ + "import os", + "class A(dspy.Module): pass\nclass B(dspy.Module): pass", + "class A(object):\n def __init__(self): pass\n def forward(self): pass", + "class A(dspy.Module):\n def __init__(self): pass\n async def forward(self): pass", + "@decorator\nclass A(dspy.Module):\n def __init__(self): pass\n def forward(self): pass", + "class A(dspy.Module):\n value = side_effect()\n def __init__(self): pass\n def forward(self): pass", + ], +) +def test_source_validation(bad): + with pytest.raises(ValueError): + MontyProgram("-> answer", bad) + + +@pytest.mark.asyncio +async def test_async_is_explicitly_unsupported(): + program = MontyProgram("-> answer", source("return {'answer': 'ok'}")) + with pytest.raises(NotImplementedError, match="synchronous"): + await program.acall() + + +def test_state_reset_and_opaque_parameter(): + program = MontyProgram("-> answer", source("return {'answer': 'ok'}")) + assert program.named_predictors() == [] + state = program.dump_state() + program._bind_code(source("return {'answer': 'changed'}")) + program.load_state(state) + assert program.module_src == state["module_src"] + program.reset() + assert program.get_lm() is None + + +def test_read_only_source_and_signature(): + program = MontyProgram("-> answer") + with pytest.raises(AttributeError): + program.module_src = "changed" + with pytest.raises(AttributeError): + program.signature = "changed" + + +def test_depth_configuration_is_hard_capped_at_two(): + with pytest.raises(ValueError, match="between 0 and 2"): + MontyProgram("-> answer", max_nested_depth=3) diff --git a/uv.lock b/uv.lock index 07d5de5d3f..0e182e4ca2 100644 --- a/uv.lock +++ b/uv.lock @@ -812,6 +812,9 @@ mcp = [ { name = "mcp", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, { name = "mcp", version = "1.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform == 'win32'" }, ] +monty = [ + { name = "pydantic-monty" }, +] numpy = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -828,6 +831,7 @@ test-extras = [ { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "optuna" }, { name = "pandas" }, + { name = "pydantic-monty" }, ] weaviate = [ { name = "weaviate-client" }, @@ -864,6 +868,8 @@ requires-dist = [ { name = "pillow", marker = "extra == 'dev'", specifier = ">=10.1.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7.0" }, { name = "pydantic", specifier = ">=2.0" }, + { name = "pydantic-monty", marker = "extra == 'monty'", specifier = ">=0.0.19" }, + { name = "pydantic-monty", marker = "extra == 'test-extras'", specifier = ">=0.0.19" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.2.5" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.26.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" }, @@ -875,7 +881,7 @@ requires-dist = [ { name = "tqdm", specifier = ">=4.66.1" }, { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4.5.4,<4.22.0" }, ] -provides-extras = ["anthropic", "weaviate", "mcp", "langchain", "optuna", "numpy", "litellm", "dev", "test-extras"] +provides-extras = ["anthropic", "weaviate", "mcp", "monty", "langchain", "optuna", "numpy", "litellm", "dev", "test-extras"] [[package]] name = "email-validator" @@ -2713,6 +2719,146 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pydantic-monty" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-monty-runtime" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/f8/c04df414086488834d102ab85cf8172c114108f842fb142ac92a490213fd/pydantic_monty-0.0.19.tar.gz", hash = "sha256:f3f9e256058b4085349dd4ad347795d5203a8310e9eaad9a2bff93890ac5b86d", size = 1484032, upload-time = "2026-07-24T10:00:13.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/da/a8d30b24a1eba8bd870d3eac4c7e01945a3b524bfe6dc49811307dd55ac4/pydantic_monty-0.0.19-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d8f8623adf1bf5bd037605d64b1ccb0949b3d25c93e477db625e170a01fe14fc", size = 2495751, upload-time = "2026-07-24T09:56:13.376Z" }, + { url = "https://files.pythonhosted.org/packages/1c/55/82270b373085b63121f97f11f81dc7ee0ee84b325e2e9aab29e263e0cf3b/pydantic_monty-0.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2be7262da51c04da8f7229870f43c9ded11e03efff7679642e862d4d4cfe1efe", size = 2254803, upload-time = "2026-07-24T09:56:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/be/be/7536409aef31c0d789c5baa4ec4964546c4443729a88c847578a6df1e372/pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:e1bda4fc890be717a18a49b35f96ae603da2bca4b91f9af035808387cbb14405", size = 2307377, upload-time = "2026-07-24T09:56:16.887Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/4ecbe4498189e312ceaedf8d63ef244a85eca7e61d4218979a2545fa4532/pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_armv7l.whl", hash = "sha256:bc3f63f356be4bd2f9dee27782a5c921d12720a407c5a2f51fcc238923745812", size = 2007646, upload-time = "2026-07-24T09:56:18.267Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8b/0faa9f1f0bc1598ddae7b67d08e4440f89728bfe4d513e6b99f036a8dd19/pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:b2872cb5712dc4c5d0e4ced447975c331c8aee4a34247caadda22e9571ee3ff5", size = 2154227, upload-time = "2026-07-24T09:56:19.632Z" }, + { url = "https://files.pythonhosted.org/packages/f1/2d/bb6e35b598847ad5dca191485a4f88ddfee382e4731593a5d8b97bc6d5ed/pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:eb40b75ee26204826b3201edbc20776f365a3d81c0824c9c14ad37deab3d8161", size = 2304923, upload-time = "2026-07-24T09:56:21.25Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/a48df8d4f84a59b833da61040d6247c9ad173069d60ffb5ed0e576576f86/pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:e7094f92f033288ffca192e214c401e01cdb47d270076e1e0d8507e8426fd3ce", size = 2165734, upload-time = "2026-07-24T09:56:22.659Z" }, + { url = "https://files.pythonhosted.org/packages/32/3a/2d0a8d8605d2ab6e6203c8d9a6c648e8090f725b956fb54e5301d96c32ee/pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:09e4de15c7b4c4e9e09f99f538fb15a00ac25cdbaa5bc5cb2a9ba03bcf405254", size = 2384315, upload-time = "2026-07-24T09:56:23.976Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/cc64ca2cb278ddf1edef2af702368d7ad91c2a3774b916dfd34a12b4af2b/pydantic_monty-0.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a83449e220075b1f447975dc43ab5b21aa09943158cefc507220a79862a9ba02", size = 2502605, upload-time = "2026-07-24T09:56:25.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/56/3f9b9fb4d0fb479ead5728703ebd2944f748e6fda3eb9a4e4f0f562674ce/pydantic_monty-0.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3516d03ff28b12e8e1312a4e36afb17021300bdf1ee2f1b736aca544af4731b5", size = 2740924, upload-time = "2026-07-24T09:56:27.108Z" }, + { url = "https://files.pythonhosted.org/packages/16/22/e9e4a5385eae8e843929443d7c7a14ca08a5e6e13aea2f9e90a7df9d9bf2/pydantic_monty-0.0.19-cp310-cp310-win32.whl", hash = "sha256:f739585b8b30cfe14c10914d9d5407d5e1be407f71b47bac9e7bf648ad469a6a", size = 1914331, upload-time = "2026-07-24T09:56:28.668Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f3/f4a8d50a3e2fb9a95de7171036da9caecc700949dd889f019ab237f08146/pydantic_monty-0.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:0adcb51fd32e8cd40c5a57ec6019267d6fa63a6cac70688a5a7bee435b9df19f", size = 2132497, upload-time = "2026-07-24T09:56:30.296Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/890a292f96f3a9363419ce01a89cc98be1f95115e6ee04010c8a8a6b6f26/pydantic_monty-0.0.19-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:435b0ae337f701ee83adc94eee6f2983378cb336bd8ebffa8282c74ea2b39d6b", size = 2495091, upload-time = "2026-07-24T09:56:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/8fa600ade713a762eabe0fd5915a60d4395eb75fd6d8d1e0e8e4487d67ed/pydantic_monty-0.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:472d4a775d71a8bfe66bd67c926feb0ea77c0a5ba515c72be21702ab96995106", size = 2253609, upload-time = "2026-07-24T09:56:33.234Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/6f3b9142345f7d7b23043f2bc2340e6bcdb5206b7e2c3e1e4e2ffb0b7fac/pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0e94031cc2d81a5e7923f38ffaa8e8ec3ae7f001a7fcd4bdbe1cbf99a048bc6d", size = 2306671, upload-time = "2026-07-24T09:56:34.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d4/893044608c29bc55eaf1001d419729c1ce36e0ba468bc5419c59f3c72706/pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:d51f460ecefb77bc6efbf8a18ba9d63ce08fed7388fe5481d34c01210cf97c5b", size = 2006044, upload-time = "2026-07-24T09:56:36.068Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/8b3921b23429f15d9dafda8d37a6fafc0b8240c9eedcbbeeac56da01fad1/pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:87d43fa5831a177513535f022c590561b38f91fadd535aa18f8e45b294d08afc", size = 2152953, upload-time = "2026-07-24T09:56:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/b792e1d5348fb1ee4209c81800a0aada5c5d81651fb2c050d1b0bd19dbe1/pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:c96fda04408e01c7becb4a7918f1459a0d9d8d4a41e96f5ed846162d1e9f5c80", size = 2303787, upload-time = "2026-07-24T09:56:39.114Z" }, + { url = "https://files.pythonhosted.org/packages/73/58/c11eb27a42302423cbb9adfab3de4ef83d4d4a8bbdf07c9d7d054ba9d5d5/pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:f6268bed8489cc8ff13c656c4ec7cac0121e58502e1519f1875cf21fa3f29f62", size = 2164908, upload-time = "2026-07-24T09:56:40.846Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/6129bbfa55daf42175436535984273b199cc0c290de5349bf1781d3f001c/pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:655b09207e3218a356b5298ac676536e0ed80c96001b4619748d0722c5d4f55a", size = 2382867, upload-time = "2026-07-24T09:56:42.436Z" }, + { url = "https://files.pythonhosted.org/packages/89/b4/af66ca38439b2712faf186b9e0341f179e1dd06ede791f5ae8e0738f5bf2/pydantic_monty-0.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:77884bf6047e6df2edc02089aa3b5f3fe0030028dd88ed61ff99d75d1b367199", size = 2501367, upload-time = "2026-07-24T09:56:43.794Z" }, + { url = "https://files.pythonhosted.org/packages/95/9f/8f6107b2ac31b89539317d9054947fccd0a6ab7ff7f4e4110aec53542efc/pydantic_monty-0.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0c6e4d78af78b23521d9dc15a6c191217942291d357b4b80ad5234d51633cd2", size = 2740027, upload-time = "2026-07-24T09:56:45.308Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b4/2ded2cc56c3d667fdc116c73e99176a588b212dc5a81aa84d8b9e2d72b23/pydantic_monty-0.0.19-cp311-cp311-win32.whl", hash = "sha256:49a979b0902eb0965521726e90826ff8d48fac5f51bfeda5e1b131f203c1790f", size = 1913668, upload-time = "2026-07-24T09:56:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/00/d8/ef1147a176b7ced141f878034ac7ede71b7af519d721c4b80dba830af8b4/pydantic_monty-0.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:1b3a4db9b7972068dfa80c91f814318f2fd4102a7716420051c79178b56e4ad7", size = 2131998, upload-time = "2026-07-24T09:56:48.261Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/152bbb3315dfa46d4e4aae71779230e50c67a34d859a3470fd75c01b795c/pydantic_monty-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b073e64edfd62cca918d792d6fe559512472f981f949e53a7aec673201f5f554", size = 2492733, upload-time = "2026-07-24T09:56:49.612Z" }, + { url = "https://files.pythonhosted.org/packages/46/16/9d37f1bf94c47c593a06ecb918bb64a2c8a38af24d6fa2b0d0e031938edf/pydantic_monty-0.0.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5825ae6c40270166f0f31b6e62d59b0fe47201d12b1be5842efc22d3b7dadcee", size = 2234610, upload-time = "2026-07-24T09:56:51.257Z" }, + { url = "https://files.pythonhosted.org/packages/82/4f/31732f4b9c2b6574eb564e420593b9be3f80d92440211970fab23e882bc5/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5b6821c0035ff2c02cf0f37823fb905890f7238ce923bbc3ca937740f9554836", size = 2303116, upload-time = "2026-07-24T09:56:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/21/2c/c21e6179361100dd8b9ad410df775d176a29e0b85f2ffc3144fd29840ee9/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:5fb4514d99a10e304237a82baeb6072e934ce8462dcd5fb5c3fea0ee0ef16aeb", size = 2007947, upload-time = "2026-07-24T09:56:54.006Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/ad64f4894334499f689bdce7e5b5dda6b680d98989424092dcbf21666564/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:2b0f154ac2e6450befa337a99a8519e2f1195cc59f88bd73d780067ace0c4c97", size = 2151468, upload-time = "2026-07-24T09:56:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/4ff5f9170e4865ec28fb152bc6ebaa8bd1873e695ee98af2103607ba42e8/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:6fcb4e3a312397432f7c5a0aa04a765670d9f37f4e23f37cb580b3faa2f218b1", size = 2300164, upload-time = "2026-07-24T09:56:57.318Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/c9bfb6708bc5d9f6b040db46156c6e3f16de68ab22f07e2de4f25782414b/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:955d308171ba0260f75d2dede18c46d9732647f94b8f7fba3076bb539f155dc8", size = 2164984, upload-time = "2026-07-24T09:56:58.66Z" }, + { url = "https://files.pythonhosted.org/packages/35/2d/8c1492216632f53e229cb2886c7fd09613d5990f4856f93f7ae0364da9bc/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3d7e1a6cc1977b24e01b5322888a5ba3f05b112a04a1646ba51f2c34c562a3d9", size = 2357823, upload-time = "2026-07-24T09:57:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/ae4adaaf343de00748fc3598402c1523e48db7094a9c1e0fa26177699440/pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4904785a34f71f59aa1eb6dc081bafd09e4caa2f028cd379aa3aa846b8a1c27d", size = 2497387, upload-time = "2026-07-24T09:57:01.686Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/eaef87ed6cbffed9c720dd3cb850e748b0aab11f5ec1ecffb56250973f7d/pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce15a5326e24cf7045ec9c1456ddb92abd09df91708771c3ae5e72d8e6374c81", size = 2738391, upload-time = "2026-07-24T09:57:03.3Z" }, + { url = "https://files.pythonhosted.org/packages/63/af/58be6fd6ea87e27bd57435013ca1f63d645a0c990c2f23708bcdd048af24/pydantic_monty-0.0.19-cp312-cp312-win32.whl", hash = "sha256:600eb259415e8b2dfef4be38d030c945b3fbb4fb85e727cb97131e7329ab017f", size = 1908274, upload-time = "2026-07-24T09:57:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/20/b7/1cb54e43113cb69c40fb765cfee3be1c222d81153b432131f50508569aee/pydantic_monty-0.0.19-cp312-cp312-win_amd64.whl", hash = "sha256:2c98b1c99994f92ab487a762b107067ab70036f64231e05aa3f6d2b16018688e", size = 2111335, upload-time = "2026-07-24T09:57:06.614Z" }, + { url = "https://files.pythonhosted.org/packages/24/17/0926da051f34ccaa45bf528777dc99e5ea611669cdd7b715be1e086c1fe0/pydantic_monty-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c01fb1162cf87dbf145b875450eabfde6b35b26f27ed63468398cb4c37732064", size = 2496669, upload-time = "2026-07-24T09:57:07.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/c38f79e24971b4d2205ee156df6e5c6ccdcc720152f54a956cc26e7488b0/pydantic_monty-0.0.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e97dc97de32410003fb5517b72ab47799b4546a3ab48a75e60260cf73734da76", size = 2234599, upload-time = "2026-07-24T09:57:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/89/fb2ed1677ad2c3e2801aa119fdc280d1c64f3480e1015811e0942c9a7d20/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:06318e6add780f66259067829ab16062ce7a556dba0884372a881881b4a7c3bf", size = 2305696, upload-time = "2026-07-24T09:57:10.97Z" }, + { url = "https://files.pythonhosted.org/packages/24/ec/e36ff1c2c97f46420d57680199e7ae2e1eb306d2cfda22be2448e53e7484/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:5a765a36141bbeb074d89b424d5488bed4e358c603c72606cc9d81ee44d83de2", size = 2007600, upload-time = "2026-07-24T09:57:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/b47670d3e28f99dc2f4c2686dc42233843dce1a607f3d3cc8a387f3b9b74/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:48f5ebc048779c854f993834586ba372df3b65500d1ed7f147023abc29aeb2c4", size = 2151382, upload-time = "2026-07-24T09:57:13.8Z" }, + { url = "https://files.pythonhosted.org/packages/9f/18/559d71fc66a769c22f6b2a8470515aa6e69a141ab617900fe28a806080b7/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2eb7502f93d8bb568d255fccca95e3b9daca05bb674b9c5323fcba14d088932c", size = 2302643, upload-time = "2026-07-24T09:57:15.42Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/171be42e2ec211bd01fe4be7b6de9ab0c346081edc33830f9f9b781341ab/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:608994600a839dd863940ac84810c48d378390a4fea8c77e5145feb84037e610", size = 2169103, upload-time = "2026-07-24T09:57:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/8c47253c8f3f528f0fa2d87dde85623dc3f211189a372e2d69cb6ad896b1/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:13a652f9dffa6d25ea458fec83108f60f29682caf42cecef91955b5b8cb04365", size = 2358155, upload-time = "2026-07-24T09:57:18.51Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/239107b83bae3a20e4f5424f6c6f3f88bae1fd1e734603c298167985f8be/pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6167c966db7d8d2fe03940f8da596de6dcd184dcf5cf6209f0a1a9af8792550d", size = 2500858, upload-time = "2026-07-24T09:57:19.878Z" }, + { url = "https://files.pythonhosted.org/packages/48/55/02a7059f20e7198e511ac0b88a3957a2c01b8991326359b7c1bb590a09b8/pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3102b44307d04f41897ae51d4daeab4fc9b5bf84a78c036781b488876ef998c3", size = 2742212, upload-time = "2026-07-24T09:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/a8/28/b632fe0e8eeba3f2b4dbec6ebd4c9b569c20e9597007f2d0fbbf04101307/pydantic_monty-0.0.19-cp313-cp313-win32.whl", hash = "sha256:e43da52776796a894f40533a7e5a322e98d9aaf7d8f6fbb7dc21a0de60a93f41", size = 1908553, upload-time = "2026-07-24T09:57:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d3/b90872f017871339ceb03e70fa4915ef8682128a476a66adffedfff874d8/pydantic_monty-0.0.19-cp313-cp313-win_amd64.whl", hash = "sha256:fd8c195875f8f44d55bc7d1d53c4e43248b184b3ac803d8131c92d4cc05a1aef", size = 2111345, upload-time = "2026-07-24T09:57:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/eb/11/c2aed55502bfc9620837312f0e2fca7a3d4bc959824a66d81b72144d7256/pydantic_monty-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2692dc4452937cf2200afd257297e9ac3ccff122b80aa7a69935e8275d684193", size = 2497017, upload-time = "2026-07-24T09:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d0/d4b44a81c71308109cfa642a24b803057615ca1609530c5e66c376780efe/pydantic_monty-0.0.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a4717f829b35c4bc5d9f6f52a52d19b90729bd494bcb69133bd4c0afcab9c76", size = 2247468, upload-time = "2026-07-24T09:57:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/a9/89/52848dce3acbb1d58df34496db3c4718814cc39f6db01a5025bfdc12b530/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:99f7282213ad6ebf7daf149a88d1517e6328afbf6780180512b9de62f30d292b", size = 2306181, upload-time = "2026-07-24T09:57:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/90/05/f7791c79c7be2240c43a9287196ed49034f773e3f4158492f028e486ebc2/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:978788b1c56fa0c49927e0a35151f0a635ef2f8d947d16915541ff864d2112f5", size = 2008738, upload-time = "2026-07-24T09:57:30.423Z" }, + { url = "https://files.pythonhosted.org/packages/93/a1/d714258eeb2583acaab2035834acc54ac6baa65bac456f897d901bc0c03a/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:8b821cac39deeb1abb2994d5a65f117ce26e55c586d0f570d425ff469ff48e3c", size = 2152275, upload-time = "2026-07-24T09:57:31.725Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/cdb1fa761e992a489b07a17adb80c21bf99eabd1286ca62f9e73acda1f4b/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:b43c4ffa5651f0eca97458dc673d7952064ae5eb5b836d23967a7d41483bb8f4", size = 2303533, upload-time = "2026-07-24T09:57:33.131Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/e6685cf82521e68e0dcb94e0c97a1a20410b1266fbce7008c0caa3484039/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:4054610601358943a3dd740a8d59a6812cc682e94d6903cb72baadae1ef5d2ac", size = 2169585, upload-time = "2026-07-24T09:57:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/6f031a628d3de72d95bedbb18292ccf998d9a422aff58eedf37347a0b367/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0b2a34c320968a3cef3d933737e99c932f13c55667315128f366afdaeea2be04", size = 2375171, upload-time = "2026-07-24T09:57:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d3/4367bccdf2c06a977c0d5ddf816190d570a07199f011034df16d51c84723/pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ffaf950f4284bb193a54f18fa4e3e8c225b5b52265813abffe492074e90c65fb", size = 2501475, upload-time = "2026-07-24T09:57:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/7a0e1d5c016848afc9a8605aa4f16e6960d68806e775865b986aacaf8f88/pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:bb1e5ee6762f9494bfb0f4cc316ad3e9a8c7917e3c984a23eb2e2322d401e5cb", size = 2742547, upload-time = "2026-07-24T09:57:39.284Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/92f77cf088f1df72a5dbab109c8c0e82f7ddeb18e65835a8dffcf967bb4a/pydantic_monty-0.0.19-cp314-cp314-win32.whl", hash = "sha256:b68ef6503b39f2f014162e3d8e7f48b9722a43ceb7b6d7199dc6d545f4c37b34", size = 1907832, upload-time = "2026-07-24T09:57:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3e/85e1a914f81659ea25c34916fdef27fcda2b00323dafb76cf2a5321f7c11/pydantic_monty-0.0.19-cp314-cp314-win_amd64.whl", hash = "sha256:14ef37b43c5bf90966ca51bcad0fae892a3c2546cd151fadc039e0a25ca74073", size = 2123187, upload-time = "2026-07-24T09:57:42.352Z" }, +] + +[[package]] +name = "pydantic-monty-runtime" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/29/e44460fd934584ddecc6b8a8acddbc0605e86ea9d027910acdbf526c8f14/pydantic_monty_runtime-0.0.19.tar.gz", hash = "sha256:717e11349d7234575750cec881ac390961de02a40bd09f875dc5e097705b896b", size = 1322628, upload-time = "2026-07-24T10:00:14.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/48/6ace8242da7daafa4520156d123f25aba25cced5118dcdbc69021107ce49/pydantic_monty_runtime-0.0.19-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c469530f5b9cdcabeef71a978253e7409b875e13d0a7081b6a63d7f6712307d", size = 9449108, upload-time = "2026-07-24T09:57:44.327Z" }, + { url = "https://files.pythonhosted.org/packages/98/fa/adc98e5b6339b84af647e6398fb85233378e5f91dffc81cd73868e6f30b5/pydantic_monty_runtime-0.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16b6edb5b7f28e493a1c2e89652400db1074483906eaf1b0ce9775d6873a6b34", size = 9735875, upload-time = "2026-07-24T09:57:46.674Z" }, + { url = "https://files.pythonhosted.org/packages/61/c3/c7d8a6a823c08c96a5ae8a6981170d992e3985cc718aed17563651de2837/pydantic_monty_runtime-0.0.19-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:03abdc743d141a63df677568cea00e7d26b6777c15026a5072701d5bb7dba6d7", size = 9171497, upload-time = "2026-07-24T09:57:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/a4/80/0652fdc1315db0a2ffeba2cb888d4a9a2e0adb50a92bf99295a3c2566f7b/pydantic_monty_runtime-0.0.19-cp310-cp310-manylinux_2_28_armv7l.whl", hash = "sha256:f7533a17fe2fec81ce063d05298516045ed796f8362470a13bca08a7fc870fb9", size = 9565494, upload-time = "2026-07-24T09:57:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/89/9b/9428aab338c3b561e069ba83610fc5fe58a7dbcb4a125632bfe32224a8d5/pydantic_monty_runtime-0.0.19-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:893a718ed52b103a0de44958eb2b032d432357fd5498b83f8b6497d3a75aae60", size = 10199598, upload-time = "2026-07-24T09:57:53.667Z" }, + { url = "https://files.pythonhosted.org/packages/53/d9/7932925db34e7d4cfbeb529dc60c057343df2c68023d4d2a075a81c48c49/pydantic_monty_runtime-0.0.19-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:5578c0dfe2fa831132a76528626ce5ada016dd38474a3082cfe3f3ceea6cd9db", size = 10355700, upload-time = "2026-07-24T09:57:56.385Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1d/67ffd8a5802971ab2568b6cb61124dea09530a94d4e98b2aa34d63708ed2/pydantic_monty_runtime-0.0.19-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:935ad929d8936302ab43eaff8795edb3a6e24051f76a8a487528160e758bbded", size = 10197859, upload-time = "2026-07-24T09:57:59.01Z" }, + { url = "https://files.pythonhosted.org/packages/a2/de/cc8473d0ff5f1acac920b7836e501f4f11e33083930bcf2708b20c14c909/pydantic_monty_runtime-0.0.19-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:836f7ff399556970f8d5d2a5ecc590c238d7005b8007a7569fc5352abaf627a2", size = 10661717, upload-time = "2026-07-24T09:58:01.426Z" }, + { url = "https://files.pythonhosted.org/packages/08/c0/32ec1f5f8ff9ef086eb9f6d205249a389613445ecacc39535b04c339b381/pydantic_monty_runtime-0.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a465466b1f90f2f8fd03de479a1cb69b2d2c3008c032356f9b0a14b10b8759fd", size = 9143211, upload-time = "2026-07-24T09:58:03.809Z" }, + { url = "https://files.pythonhosted.org/packages/67/1a/25d590aae41e34ef2c17abf8dec9f0bc7a03e89a34f10925dfeaa7792359/pydantic_monty_runtime-0.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0599f910f542d30559dbfc6f716a7eaded367114994f81acc8b95923ac7f3cd9", size = 9731588, upload-time = "2026-07-24T09:58:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/dae8b5b02dbcf6b0547797e75a11c949618158ed4ddd81b67eae7ff27ece/pydantic_monty_runtime-0.0.19-cp310-cp310-win32.whl", hash = "sha256:8b8c8c2483e9a8859d8965d110ccffdebc3590a7cf4a51ecb11e8c07b4637fb1", size = 9227832, upload-time = "2026-07-24T09:58:08.778Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/69a576007bf36f0df124d7a9f63cf21b5280434e1eae79dc31a6ca2d2562/pydantic_monty_runtime-0.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:48403baba83ab9a5c0383a092b9074c91c676ec2612c0c0f35803b9a6c36a76f", size = 10941520, upload-time = "2026-07-24T09:58:11.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/3e661a611e74be1ebd607cb8aa4d6098812cf02267032dd29ab36c97af50/pydantic_monty_runtime-0.0.19-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3f512175460f7d6c3c14c06368962c8ffbe5a4d0f5446dc8c7865e3296c8b14e", size = 9449108, upload-time = "2026-07-24T09:58:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/a4/71/0e7d258aa755ed993e4f3a0a9a74af5bfcb2cbb5ca6266061a8f65bef0b7/pydantic_monty_runtime-0.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6da3f1c9cb810fdfc5a3878ac1271945906f09b238e8034dca8bd0554120f21b", size = 9735875, upload-time = "2026-07-24T09:58:16.355Z" }, + { url = "https://files.pythonhosted.org/packages/cf/de/7252d0212cf630f3d93029584fef41cfbea9771f09f74543b74f3a09b114/pydantic_monty_runtime-0.0.19-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:84ef70970561c4e75877d478d6262db9761bfaf56242be304198e2be2b1e5ff4", size = 9171499, upload-time = "2026-07-24T09:58:18.788Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8b/0902cb24c51d50d8e8fe2ffda4c78a49cdc1b43959432f6609da7478a69d/pydantic_monty_runtime-0.0.19-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:aa3b392025457ea8e261a4671952419d51540dc72171927e046734fd610ab908", size = 9565495, upload-time = "2026-07-24T09:58:21.103Z" }, + { url = "https://files.pythonhosted.org/packages/12/29/1795d33147d68f5d0acacc45c118583d8607ec2df2b7c82888728ca6faa3/pydantic_monty_runtime-0.0.19-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f1d83baaed567e6ea6b6647642dcda139df9aa33aa47d0fb4772fa01a0956082", size = 10199598, upload-time = "2026-07-24T09:58:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/23/36/77182137b297e53cb60c779b10102a3225bd05bd24c832a6e8cae34c60fb/pydantic_monty_runtime-0.0.19-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:e74ddfcec96e09a54e2b5b9ab5e197b3eb6d86ec1f21147dd689c7b8d21a1af3", size = 10355699, upload-time = "2026-07-24T09:58:26.141Z" }, + { url = "https://files.pythonhosted.org/packages/db/78/f9f1d4d9dbea085de7ebb713e4ce93a30e638560fdd12eccd874fb4185d6/pydantic_monty_runtime-0.0.19-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:3b4fff2d14d397205aa73ae294fd9bf8e985f37efb49d0047a989fe38441db8b", size = 10197860, upload-time = "2026-07-24T09:58:28.517Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/cad347f3109f48bd7e0371c0cd3d81b7090c309d734bea0632d4c6087808/pydantic_monty_runtime-0.0.19-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:df7b3ae6fc2f4560685aef1d321feabc00ada6f61d9622201fd1c92ce1a969d6", size = 10661716, upload-time = "2026-07-24T09:58:30.979Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0b/401b8ad8d77cb55417e06a75549e98190109c2adff2c02a58f6a6520958d/pydantic_monty_runtime-0.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c0f505f44ac4fea5fe714a44b32bb7c157285c3627433f0361795070ae4ee47", size = 9143209, upload-time = "2026-07-24T09:58:33.615Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c4/b5a5fe26c2209b0e07de1671ebb8e595cd32d0620d709f06ed879f47f2af/pydantic_monty_runtime-0.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:358364545a90d2f4229cedfa3346f3559ddd3e7b897dff2ef30c5c2ee3919eb1", size = 9731588, upload-time = "2026-07-24T09:58:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/52/8a/7319feb23909047283c0891f59a621a3b7747e8a40afcc99ba7b65ec8d0d/pydantic_monty_runtime-0.0.19-cp311-cp311-win32.whl", hash = "sha256:3447e0cabdbdd3ed6d07327b0f23fb342915a27b3e5378d1e68e73a92a2007fe", size = 9227833, upload-time = "2026-07-24T09:58:38.482Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f8/40661b44c19b9c02b625ae1549e74712d23485b6e6cae59a05c2c9cc43b2/pydantic_monty_runtime-0.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:aa2e84f08e65d022b107f49392486a502c2a894346ac31ec9d5841081a9bd466", size = 10941520, upload-time = "2026-07-24T09:58:41.429Z" }, + { url = "https://files.pythonhosted.org/packages/fd/11/a6f4e12982b2232b9036db334fbcfecbacf46b9acaf311f9c3110e431c53/pydantic_monty_runtime-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:be34905548e31237fc683f5a34986489c127727e8f65481e8c87c4ad0b3a4dc2", size = 9449108, upload-time = "2026-07-24T09:58:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a0/1e720b1bba457c6a1ab4fca20c571ae058238c7df3e1892b5efebad05a8b/pydantic_monty_runtime-0.0.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f402f20672c1e25d4915de7e97023519461b9ffbedacd08c427d40daa77bdd6", size = 9735875, upload-time = "2026-07-24T09:58:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/97/4439b312d9463881630dd9d998d2c8fe2b8ef457b451202c71816e25688c/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ec0d25dfbe65b9a5dfe77ff86353e95a86774d786a1d496206163f828f072fd", size = 9171496, upload-time = "2026-07-24T09:58:49.504Z" }, + { url = "https://files.pythonhosted.org/packages/1e/67/fa7b99afe1917b89eec3b4b6375f468c76eeea8227dd48361b7f2db90d97/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:7ecc2f8eeabf6483908db4cc69b7d4c156a95675dcbdda2a7540a22ed904bfb2", size = 9565495, upload-time = "2026-07-24T09:58:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a6/83a9dceb8d9f5dffd9a082b60590bb61b0ac48dca19aa02847ebbab1ad46/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:fc1e508b9dc2cab64e27004d0f4ce44c7e1d255e85bafba390884fa07d696319", size = 10199598, upload-time = "2026-07-24T09:58:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/bafd9dfa9a9a0d26b9882053aaea2280d791225c1e3b33b4b48f23fd5f92/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:9e3f6e366f62e54d7bd0fb995f007f42d23c26eee560d57c04d75d5adde3b45c", size = 10355698, upload-time = "2026-07-24T09:58:56.713Z" }, + { url = "https://files.pythonhosted.org/packages/0a/00/ef55cdc9f5ade20475622bb8dba617efce00ef9da2b94b36a76172edadce/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:2ddf4f2d1d063f330bb54b2dc00f6d5bcfbdd3defdc5988b856d350f62160e26", size = 10197859, upload-time = "2026-07-24T09:58:59.158Z" }, + { url = "https://files.pythonhosted.org/packages/a8/67/d1c359658458cf97296fda4359bfc71de8c9f968fb3db68eb7ce00136d43/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ce2c9550c30a5c94b63dfe578243d0823511feeecb020723f5140f9737505410", size = 10661715, upload-time = "2026-07-24T09:59:01.576Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/9841d93f956bfbd0bbbac75edf42bb3b13b5bad7d25b09d9a221b5e54d71/pydantic_monty_runtime-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:05563e178b6be783de088abe4a25cef9cfbc04811f7c1e1873860252e711edac", size = 9143212, upload-time = "2026-07-24T09:59:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/0cd3643cc5051456b7baad6f16d85fb1d05daefd0556e4c82ea8f22cc9a2/pydantic_monty_runtime-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de74aa4df47147d104bccdac4b39c1f3fd543091be3fd0156f77eeea3e483bc3", size = 9731590, upload-time = "2026-07-24T09:59:06.323Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5a/69f4eabec2538df364242395ab3ef77b30a124a0e4b461231589651a1e97/pydantic_monty_runtime-0.0.19-cp312-cp312-win32.whl", hash = "sha256:5208056d9e23d951768ba4b94df3caf7fd84bfe951f68ec4a1803eb03377bbeb", size = 9227834, upload-time = "2026-07-24T09:59:08.694Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4b/221a21f477aef0c488cbe1467111b0988658bc4a42cfc6b404201bc432af/pydantic_monty_runtime-0.0.19-cp312-cp312-win_amd64.whl", hash = "sha256:e4bba0c6024a3a8bd8c8a8cba25233a19cf686218e97afb4c059aa0c625a4b8b", size = 10941520, upload-time = "2026-07-24T09:59:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a8/1ba497c35eca33273f2144b8e78d94832cc33aec5f69d8bef56968a61933/pydantic_monty_runtime-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:84dd041652581335503af7c60ee7362a462d946a62e8c4a44a939984034d0d25", size = 9449108, upload-time = "2026-07-24T09:59:13.497Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/08c05c9729a35d6c9831bfd57d535bd1257f601d15b62936c27f1c0cfb47/pydantic_monty_runtime-0.0.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:263ab33dce2008ca6c83b4013b0280a98a1991d4071c55413d00b539940fe8aa", size = 9735874, upload-time = "2026-07-24T09:59:15.918Z" }, + { url = "https://files.pythonhosted.org/packages/c0/64/63fbdb4c069ceb2af6261fe5923864b5be309799087f16a5dccc96141c12/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:64f56d48097d8a158125534b20e8b22003c5e727082a303907da8e61aae0c7e3", size = 9171498, upload-time = "2026-07-24T09:59:18.799Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cf/82390fe7f0e1100662e6cac7a1c0de716ea4bf851a53aa2b0ef324fc4e3e/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:a4d80fb4598524cd5039a596f8e6900adb2a5a2da66d012a3734a2b441ad06c3", size = 9565494, upload-time = "2026-07-24T09:59:21.232Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/caa8bf32ba0c0e8ce31ef2773b1a1f60d688e0237cd396e48bbef9f7161f/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:1c5cd0b140c765772e0606a7047fbf95cc49d62d0d8b79b4f520dae0e38b3ba7", size = 10199597, upload-time = "2026-07-24T09:59:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/48/25/ae9bb52518b2ce8fe7509d6cad4f4916b4458b748fecb180bef2d78165e9/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:7d80975c6e792088285f49a91e26de483ef95e5bffc4939b02d4c5ea1059749b", size = 10355699, upload-time = "2026-07-24T09:59:26.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/0875b1239b8ea57e4ea6de421cd240fc5a88c02e381f88d858f00439b1e9/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f826cb911f78fe4fddf09fa3f38518484041908d08de927fa7dd134c002a2c77", size = 10197858, upload-time = "2026-07-24T09:59:28.889Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/9365473fe31e53a6dc4d6fea406d5d8f3f03a9b7d84d1f29a9e6d664c862/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:fdf56c6cd8c6163737d1ce284f9acb00feff77b7e952f041dc281b94336c4fc9", size = 10661715, upload-time = "2026-07-24T09:59:31.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/f9/8d85b8f77d4006a8d80517a0781c49e6ca026f68747f801b63298e64197e/pydantic_monty_runtime-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a675f165773af0a2e0209dd96014aa9e613115cdc6dad297a7bb96cf87f83315", size = 9143210, upload-time = "2026-07-24T09:59:33.843Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/ded73742ee9fa2160c711141c28ea2ee083f073019e89724049c5e67cd84/pydantic_monty_runtime-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9e42ab0287c0497d9d5529e8a53dd2f63f9d1f92f6592170baab894ea9956da6", size = 9731590, upload-time = "2026-07-24T09:59:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/4b/06/67be8320dc592b8c4caa8c4c1544c8ddf2760029d46843d078aa5a4cdb14/pydantic_monty_runtime-0.0.19-cp313-cp313-win32.whl", hash = "sha256:1f5ff1b9585e648304096705045fb6bd90d43b561568b0d373265e8d201b1234", size = 9227833, upload-time = "2026-07-24T09:59:39.022Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/bab34897974d83640f8773f03d2001142bc13e80e517bfce8c8c4a57157e/pydantic_monty_runtime-0.0.19-cp313-cp313-win_amd64.whl", hash = "sha256:7181a2153ff257fe34109685148167b6e8219d4acfe8f346115b58152fd26aa8", size = 10941519, upload-time = "2026-07-24T09:59:41.538Z" }, + { url = "https://files.pythonhosted.org/packages/63/8d/f46ac4778b2ac64183607bc63ecc783f16ca9981de30eb8ec9aa5e7132cd/pydantic_monty_runtime-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4cc92139b476469c0d7e5caa147d38c2929de39bf1724cb22bbab80297823963", size = 9449107, upload-time = "2026-07-24T09:59:44.139Z" }, + { url = "https://files.pythonhosted.org/packages/a5/14/34a7bb4630d1bac0d055049568ecc888a87954c176428bde5764a7ff6ed7/pydantic_monty_runtime-0.0.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a97e00bd46305f34a85f2e2e3ac4c92dbd3340e7af694aafb192ff58c4fb40e", size = 9735874, upload-time = "2026-07-24T09:59:46.546Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/977de0b258c0858f52b23bd32afbfdf8a8c4614daff5d0b7d5c86332ce6e/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:46ad28b1b3c41113c9e89373da18bcc883a24da371d4eeb9b32d3d194286f1a5", size = 9171497, upload-time = "2026-07-24T09:59:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/39/39/4374200ee4b938fc8b5026056f13bb677e15796bca1323a16210738431ad/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:dc61844187a32c2f9c2b69846c5d55679eb838b4f7e495b4d03509f89fbf41f9", size = 9565495, upload-time = "2026-07-24T09:59:51.149Z" }, + { url = "https://files.pythonhosted.org/packages/04/55/c4ee4b0a10610359e09cad9d327db19095914a3bf427fb3d8164ec2bcae0/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:b23b52a79ee6be0a943e4b8e5d996e6c489a37b23a337838164f22c9e8ed11a7", size = 10199598, upload-time = "2026-07-24T09:59:53.619Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/cc9df084e9f930bbb2873b6dd832b377f76071aec309b1545589d818fd90/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:484496817f5f238c42aaea8a1945b545a17d8ef2a21e9e81799d55e481d25485", size = 10355699, upload-time = "2026-07-24T09:59:56.043Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/bb13e6f655fcaee340032ad6a3cd1524957d0fa471ddc7e27bdb1f4c240b/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c3e7cbad58ae9bd3402581faa7d54d719f9c140dc1888f5c9439d45bff528ea1", size = 10197859, upload-time = "2026-07-24T09:59:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/c532715987383668ee835337e1485f51585bc8bf189f033370a05eff17f1/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5534067dc7ffdae809293da95d0e95b6d8481f4c88aff59385e19f466ba3c0f0", size = 10661715, upload-time = "2026-07-24T10:00:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/9a0ab28a061efc184398a0b4db34e459572bb2316cf766f0d6ce65b475e3/pydantic_monty_runtime-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c1ae32b06a4456ab223bafa54d29a349066d625d09063c28ba14ee9019f9b7c2", size = 9143211, upload-time = "2026-07-24T10:00:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/af5b3e6395834572975d98f4d1a00a57ee8029bf68ca5732347550f32b35/pydantic_monty_runtime-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:497cf8c3f30992f9aafc8707084eaffe2391b7b5dec067d04d5715f9a562c56b", size = 9731591, upload-time = "2026-07-24T10:00:06.351Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/96797fd269342cfdb49f03c22fd9a05ef91f711089081c4b3861b9c520e2/pydantic_monty_runtime-0.0.19-cp314-cp314-win32.whl", hash = "sha256:942feb948df8edb61ae7ba6ae77dc655e6985be06d72eef886562fe573ba3086", size = 9227832, upload-time = "2026-07-24T10:00:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/39/fc/02d15281c8e00b48df9af8f75a4fe06f3f8f33ef6a910507a45a19f2b61b/pydantic_monty_runtime-0.0.19-cp314-cp314-win_amd64.whl", hash = "sha256:91d93339c70483ed9256b3b15e3375f6597ae65be280f9b89ba9ca0355f95f54", size = 10941519, upload-time = "2026-07-24T10:00:11.226Z" }, +] + [[package]] name = "pydantic-settings" version = "2.9.1"