From 833f68f88b11b4cbd4149c14c2a18b22f2a60343 Mon Sep 17 00:00:00 2001 From: Will Kurt Date: Mon, 1 Jul 2024 09:00:00 -0700 Subject: [PATCH 001/505] Add abridged version of the .txt article on Coding For Structured Generation (#1012) I've added an abridged version this [post on the .txt blog](https://blog.dottxt.co/coding-for-structured-generation.html) to the cookbook that should provide a good overview of a basic workflow for developing code when working with structured generation. --- .../images/coding_structure_diagram.png | Bin 0 -> 73586 bytes .../structured_generation_workflow.md | 215 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 docs/cookbook/images/coding_structure_diagram.png create mode 100644 docs/cookbook/structured_generation_workflow.md diff --git a/docs/cookbook/images/coding_structure_diagram.png b/docs/cookbook/images/coding_structure_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c30e04a5b07a9bd962f4e91d1cfccf22d22dc702 GIT binary patch literal 73586 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
I&R~go+_PhN3X5IeejFkny{nuHsS%A?S|_4yD0 zZA`r4v_}Cki8a_APY!@hhjbqE#R>UBtXxzu?`26tTUZ0E=N$~2`rY(%jk$hu6}3JB zd2cA(Ro!q)gYxcln641#u>rff$OM>W}w|@-X?QxDoFPBi7T~lmj@s!w0KTa z1BXR-$9HpRwL8pHIsgu_Ycc>8uQLwQ>sS`^KY$s4&~r`o+PBXSLphW9?Af7I=0&YV z2|(zTeF-+Q2`fOLTT&L}lf7*N@Jb1wJd-KMX6t${gn!Qj5+)j

ajw)@VBfas>Kh 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 G1OE$ I'd be happy to help you generate a realistic phone\ +I cannot generate a real phone number as I'm just\ +I'm an AI and don't have the ability\ +Sure! Here is a randomly generated phone number in the format\ +Here's a phone number that fits the format for a\ +In Washington State, phone numbers typically have a three-dig\ +Here are a few examples of phone numbers that could be considered\ +I'd be happy to help generate a realistic phone number\ +I'd be happy to help you generate a random phone\ +Based on the format you provided, a realistic phone number for\ + +As we can see, none of these outputs are even phone numbers! + +Let's see if we can improve this using structured generation. + +## The Structured Generation Workflow + +In order to solve this problem we're going to introduce a *Structured Generation Workflow* outlined in this image: + +!["Visual of Structured Generation Workflow"](./images/coding_structure_diagram.png) + +Let's step through this: + +### Real example + +We start with a real example phone number, in this case for the Seattle Public Library, that we can use to verify the structure we are creating. + +```python +phone_number = "(206) 386-4636" +``` + +For a simple example like this, we'll just be using a single phone number, for more complex examples it can be helpful to have more examples. + +### Draft Structure + +The next step in the process is for use to define a simple regex that we feel correctly models our real data. + +```python +phone_regex_1 = r'\([0-9]{3}\) [0-9]{3}-[0-9]{4}' +``` + +Next we need to validate this regex against our real data. + +### Validate by matching examples + +Whenever writing non-trivial code with structured generation it is *essential* that you first validate the code against your real data example(s). + +We'll start with a simple method of validation: just checking that our regex matches the data. + +``` +import re +re.match(phone_regex_1, phone_number) + +# + +``` + +Now that we have a match, we can move on to generating structured output! + +### Generate Structure + +We're ready to see if structured generation can make an improvement over our initial unstructured approach: + +```python +phone_generator_v1 = outlines.generate.regex(model, phone_regex_1) +for _ in range(10): + print(phone_generator_v1(prompt_phone)) +``` +> (206) 555-1234\ +(206) 555-1234\ +(206) 555-1234\ +(206) 555-1234\ +(206) 555-1234\ +(206) 555-1234\ +(206) 123-4567\ +(206) 555-1234\ +(206) 555-1234\ +(206) 555-1234 + +At least we have phone numbers! But I think we can do better! + +### Inspect output + +In this case the model *did* create phone numbers and, impressively, got the area code correct. So using structured generation did improve things. However these numbers are pretty boring. Let's improve that structure! + +## Iteration + +We've walked through the loop once, so we can go quickly now through each iteration. + +We start by improving our structure: + +```python +phone_regex_2 = r'\([0-9]{3}\) [2-46-9]{3}-[02-9]{4}' +``` + +Before rushing to another round of generation, let's validate this new regex. We'll add just a bit more sophistication over our last check: + +```python +re.match(phone_regex_2, phone_number)[0] == phone_number +# True +``` +Now that we've validated, let's generate with this new regex! + +```python +phone_generator_v2 = outlines.generate.regex(model, + phone_regex_2) +for _ in range(10): + print(phone_generator_v2(prompt_phone)) +``` + +> (206) 867-5309\ +(206) 666-7777\ +(206) 444-3333\ +(206) 444-3333\ +(206) 943-2222\ +(206) 323-6789\ +(206) 444-3333\ +(206) 867-5309\ +(206) 466-2255\ +(206) 222-3333 + +Better, but I don't like those repeated sequences. Like good software developers, let's iterate again! + +## Reiteration - with debugging + +Here's a fancier regex that should give us more interesting results: + +```python +phone_regex_3_error = r'\([0-9]{3}\) [2-4][7-9][4-6]-[3-6][2-8][1-4]' +``` + +This looks good to me, but there's a subtle bug, that's why we *always* need to validate our structure against real data. This time we'll make our validator do a bit more work to verify the correct string is matched: + +```python +if not re.match(phone_regex_3_error, phone_number): + print("Regex fails match") +else: + matched_string = re.match(phone_regex_3_error, phone_number)[0] + if matched_string == phone_number: + print("Successful match") + else: + print(f"Error {matched_string} != {phone_number}") +``` +This prints out: +> Error (206) 386-463 != (206) 386-4636 + +Ah! We were missing the last digit, let's fix that and regenerate: + +```python +phone_regex_3_fixed = r'\([0-9]{3}\) [2-4][7-9][4-6]-[3-6][2-8][1-4][6-9]' +phone_generator_v3 = outlines.generate.regex(model, + phone_regex_3_fixed) +for _ in range(10): + print(phone_generator_v3(prompt_phone)) +``` + +>(206) 494-3216\ +(206) 374-6218\ +(206) 494-3337\ +(206) 476-3216\ +(206) 484-3548\ +(206) 495-3218\ +(206) 494-5517\ +(206) 375-4636\ +(206) 384-6216\ +(206) 385-6218 + +Much better! + +Now you've seen a quick example of the structured generation workflow that can be used at the basis for building and iteration on much larger structured generation tasks! From 25b6bcd498d0f335683f0026e9baeb591061d001 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Tue, 2 Jul 2024 09:20:14 -0500 Subject: [PATCH 002/505] Cache Legal-Token Mask as torch.tensor --- benchmarks/bench_processors.py | 106 +++++++++++++++++++++++++-------- docs/community/contribute.md | 10 +++- outlines/fsm/guide.py | 16 +++-- tests/fsm/test_fsm.py | 57 ++++++++++-------- tests/fsm/test_guide.py | 61 ++++++++++--------- 5 files changed, 166 insertions(+), 84 deletions(-) diff --git a/benchmarks/bench_processors.py b/benchmarks/bench_processors.py index b47bb4ea5..5b4901540 100644 --- a/benchmarks/bench_processors.py +++ b/benchmarks/bench_processors.py @@ -1,8 +1,13 @@ -import mlx.core as mx import numpy as np import torch -from outlines.processors import OutlinesLogitsProcessor +import outlines.models as models +from outlines.processors import OutlinesLogitsProcessor, RegexLogitsProcessor + +try: + import mlx.core as mx +except ImportError: + pass def is_mlx_lm_allowed(): @@ -13,6 +18,37 @@ def is_mlx_lm_allowed(): return mx.metal.is_available() +def get_mock_processor_inputs(array_library, num_tokens=30000): + """ + logits: (4, 30,000 ) dtype=float + input_ids shape: (4, 2048) dtype=int + """ + if array_library == "torch": + logits = torch.rand((4, num_tokens), dtype=torch.float) + input_ids = torch.randint( + low=0, high=num_tokens, size=(4, 2048), dtype=torch.int + ) + elif array_library == "torch_cuda": + logits = torch.rand((4, num_tokens), dtype=torch.float, device="cuda") + input_ids = torch.randint( + low=0, high=num_tokens, size=(4, 2048), dtype=torch.int, device="cuda" + ) + elif array_library == "numpy": + logits = np.random.rand(4, num_tokens).astype(np.float32) + input_ids = np.random.randint(low=0, high=num_tokens, size=(4, 2048)) + elif array_library == "mlx": + logits = mx.random.uniform( + low=-1e9, high=1e9, shape=(4, num_tokens), dtype=mx.float32 + ) + input_ids = mx.random.randint( + low=0, high=num_tokens, shape=(4, 2048), dtype=mx.int32 + ) + else: + raise ValueError + + return logits, input_ids + + class HalvingLogitsProcessor(OutlinesLogitsProcessor): """Simply halve the passed logits""" @@ -20,33 +56,53 @@ def process_logits(self, input_ids, logits): return logits / 2 -class LogitsProcessorBenchmark: +class LogitsProcessorPassthroughBenchmark: + """ + Benchmark the time it takes to convert between array frameworks + This should be on the order of microseconds + """ + params = ["torch", "numpy"] - if mx.metal.is_available(): + if is_mlx_lm_allowed(): params += ["mlx"] + if torch.cuda.is_available(): + params += ["torch_cuda"] def setup(self, array_library): self.logits_processor = HalvingLogitsProcessor() - # logits: (4, 30,000 ) dtype=float - # input_ids shape: (4, 2048) dtype=int - if array_library == "torch": - self.logits = torch.rand((4, 30000), dtype=torch.float) - self.input_ids = torch.randint( - low=0, high=30000, size=(4, 2048), dtype=torch.int - ) - elif array_library == "numpy": - self.logits = np.random.rand(4, 30000).astype(np.float32) - self.input_ids = np.random.randint(low=0, high=30000, size=(4, 2048)) - elif array_library == "mlx": - self.logits = mx.random.uniform( - low=-1e9, high=1e9, shape=(4, 30000), dtype=mx.float32 - ) - self.input_ids = mx.random.randint( - low=0, high=30000, shape=(4, 2048), dtype=mx.int32 - ) - else: - raise ValueError - - def time_logits_processor(self, array_library): + self.logits, self.input_ids = get_mock_processor_inputs(array_library) + + def time_passthrough(self, *params): + self.logits_processor(self.input_ids, self.logits) + + +class LogitsProcessorStructuredBenchmark: + """ + Benchmark structured generation mask application for single decoder pass + """ + + array_libraries = ["torch", "numpy"] + if is_mlx_lm_allowed(): + array_libraries += ["mlx"] + # PR TODO + if torch.cuda.is_available(): + array_libraries += ["torch_cuda"] + + # accept very many or very few tokens, respectively + patterns = [r"[^Z]*", "Z*"] + + params = [array_libraries, patterns] + param_names = ["array_library, pattern"] + + def setup(self, array_library, pattern): + tokenizer = models.transformers("facebook/opt-125m", device="cpu").tokenizer + + self.logits_processor = RegexLogitsProcessor(pattern, tokenizer) + + self.logits, self.input_ids = get_mock_processor_inputs( + array_library, len(tokenizer.vocabulary) + ) + + def time_structured_generation(self, array_library, pattern): self.logits_processor(self.input_ids, self.logits) diff --git a/docs/community/contribute.md b/docs/community/contribute.md index b336eacad..d5568f47c 100644 --- a/docs/community/contribute.md +++ b/docs/community/contribute.md @@ -66,17 +66,21 @@ You can run the benchmark test suite locally with the following command: asv run --config benchmarks/asv.conf.json ``` -Run a specific test: +Caveats: +- If you're on a device with CUDA, you must add the argument `--launch-method spawn` +- Uncommitted code will not be benchmarked, you must first commit your changes. + +#### Run a specific test: ``` asv run --config benchmarks/asv.conf.json -b bench_json_schema.JsonSchemaBenchmark.time_json_schema_to_fsm ``` -Profile a specific test: +#### Profile a specific test: ``` asv run --config benchmarks/asv.conf.json --profile -b bench_json_schema.JsonSchemaBenchmark.time_json_schema_to_fsm ``` -Compare to `origin/main` +#### Compare to `origin/main` ``` get fetch origin asv continuous origin/main HEAD --config benchmarks/asv.conf.json diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index d247db62b..792eba34f 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, List, Optional, Protocol, Tuple, Union import interegular +import torch from lark import Lark from outlines import grammars @@ -146,6 +147,13 @@ def __init__(self, regex_string: str, tokenizer): self.eos_token_id = tokenizer.eos_token_id self.final_states = fsm_finals | {-1} + # cache returned masks token masks + # this increases performance of the mask substantially + self.states_to_token_mask = { + state: torch.tensor(list(next_tokens_to_end_states.keys())) + for state, next_tokens_to_end_states in self.states_to_token_maps.items() + } + def get_next_instruction(self, state: int) -> Instruction: """Return the next instruction for guided generation. @@ -169,11 +177,11 @@ def get_next_instruction(self, state: int) -> Instruction: A `Generate` instance that contains the model and the allowed token ids. """ - next_tokens_to_end_states = self.states_to_token_maps.get(state) - if next_tokens_to_end_states is None: - return Write([self.eos_token_id]) + next_tokens_mask = self.states_to_token_mask.get(state) + if next_tokens_mask is None: + return Write(torch.tensor([self.eos_token_id])) - return Generate(list(next_tokens_to_end_states.keys())) + return Generate(next_tokens_mask) def get_next_state(self, state: int, token_id: int) -> int: """Update the state of the guide. diff --git a/tests/fsm/test_fsm.py b/tests/fsm/test_fsm.py index 97d120f20..21163b70d 100644 --- a/tests/fsm/test_fsm.py +++ b/tests/fsm/test_fsm.py @@ -3,6 +3,13 @@ from outlines.fsm.fsm import CFGFSM, RegexFSM, StopAtEosFSM +def assert_expected_tensor_ids(tensor, ids): + assert len(tensor) == len(ids) + norm_tensor = sorted(map(int, tensor)) + norm_ids = sorted(map(int, tensor)) + assert norm_tensor == norm_ids, (norm_tensor, norm_ids) + + def test_stop_at_eos(): class MockTokenizer: vocabulary = {"a": 1, "eos": 2} @@ -50,7 +57,7 @@ def convert_token_to_string(self, token): fsm = RegexFSM(regex_str, tokenizer) assert fsm.states_to_token_maps == {0: {1: 1}} - assert fsm.allowed_token_ids(state=0) == [1] + assert_expected_tensor_ids(fsm.allowed_token_ids(state=0), [1]) assert fsm.next_state(state=0, token_id=1) == 1 assert fsm.next_state(state=0, token_id=tokenizer.eos_token_id) == -1 @@ -111,27 +118,27 @@ def decode(self, token_ids): with pytest.warns(UserWarning): fsm = CFGFSM(cfg_str, tokenizer) - assert set(fsm.allowed_token_ids(state=fsm.start_state)) == {1, 3, 5} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1, 3, 5]) state = fsm.next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "{" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) state = fsm.next_state(state=state, token_id=3) assert fsm.generation == "{[" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 3, 4} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 3, 4]) state = fsm.next_state(state=state, token_id=4) assert fsm.generation == "{[]" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {2} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [2]) state = fsm.next_state(state=state, token_id=2) assert fsm.generation == "{[]}" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {5} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [5]) state = fsm.next_state(state=state, token_id=5) assert fsm.generation == "{[]}" assert fsm.is_final_state(state) @@ -164,24 +171,24 @@ def decode(self, token_ids): with pytest.warns(UserWarning): fsm = CFGFSM(cfg_str, tokenizer) - assert set(fsm.allowed_token_ids(state=fsm.start_state)) == {1} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1]) state = fsm.next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "(" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 2} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2]) state = fsm.next_state(state=state, token_id=2) assert fsm.generation == "()" assert not fsm.is_final_state(state) # possible to continue or terminate - assert set(fsm.allowed_token_ids(state=state)) == {1, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 3]) state = fsm.next_state(state=state, token_id=3) # feed eos assert fsm.generation == "()" assert fsm.is_final_state(state) # once eos generated, can only terminate - assert set(fsm.allowed_token_ids(state=state)) == {3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [3]) def test_cfg_ignore_directive(): @@ -214,38 +221,38 @@ def decode(self, token_ids): state = 0 - assert set(fsm.allowed_token_ids(state=0)) == {1, 2} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=0), [1, 2]) state = fsm.next_state(state=0, token_id=2) assert fsm.generation == " " assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=0)) == {1, 2} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=0), [1, 2]) state = fsm.next_state(state=0, token_id=1) assert fsm.generation == " a" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) state = fsm.next_state(state=state, token_id=2) assert fsm.generation == " a " assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) state = fsm.next_state(state=state, token_id=2) assert fsm.generation == " a " assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) state = fsm.next_state(state=state, token_id=1) assert fsm.generation == " a a" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) state = fsm.next_state(state=state, token_id=3) assert fsm.generation == " a a" assert fsm.is_final_state(state) # once eos generated, can only terminate - assert set(fsm.allowed_token_ids(state=state)) == {3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [3]) def test_cfg_multitoken_terminal(): @@ -274,19 +281,19 @@ def decode(self, token_ids): with pytest.warns(UserWarning): fsm = CFGFSM(cfg_str, tokenizer) - assert set(fsm.allowed_token_ids(state=fsm.start_state)) == {1, 2} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1, 2]) assert fsm.reset_state # starting new regex state = fsm.next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "a" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1]) assert not fsm.reset_state # continuing current regex state = fsm.next_state(state=state, token_id=1) assert fsm.generation == "aa" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [3]) assert not fsm.reset_state # completing current regex state = fsm.next_state(state=state, token_id=3) assert fsm.generation == "aa" @@ -319,27 +326,27 @@ def decode(self, token_ids): with pytest.warns(UserWarning): fsm = CFGFSM(cfg_str, tokenizer) - assert set(fsm.allowed_token_ids(state=fsm.start_state)) == {1, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1, 3]) state = fsm.next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "(" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {1, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 3]) state = fsm.next_state(state=state, token_id=3) assert fsm.generation == "(a" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [2, 3]) state = fsm.next_state(state=state, token_id=3) assert fsm.generation == "(aa" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {2, 3} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [2, 3]) state = fsm.next_state(state=state, token_id=2) assert fsm.generation == "(aa)" assert not fsm.is_final_state(state) - assert set(fsm.allowed_token_ids(state=state)) == {4} + assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [4]) state = fsm.next_state(state=state, token_id=4) assert fsm.generation == "(aa)" assert fsm.is_final_state(state) diff --git a/tests/fsm/test_guide.py b/tests/fsm/test_guide.py index 68f7e33ee..20ba75893 100644 --- a/tests/fsm/test_guide.py +++ b/tests/fsm/test_guide.py @@ -3,6 +3,13 @@ from outlines.fsm.guide import CFGGuide, Generate, RegexGuide, StopAtEOSGuide, Write +def assert_expected_tensor_ids(tensor, ids): + assert len(tensor) == len(ids) + norm_tensor = sorted(map(int, tensor)) + norm_ids = sorted(map(int, tensor)) + assert norm_tensor == norm_ids, (norm_tensor, norm_ids) + + def test_stop_at_eos(): class MockTokenizer: vocabulary = {"a": 1, "eos": 2} @@ -56,7 +63,7 @@ def convert_token_to_string(self, token): instruction = fsm.get_next_instruction(0) assert isinstance(instruction, Generate) - assert instruction.tokens == [1] + assert_expected_tensor_ids(instruction.tokens, [1]) assert fsm.get_next_state(state=0, token_id=1) == 1 assert fsm.get_next_state(state=0, token_id=tokenizer.eos_token_id) == -1 @@ -102,7 +109,7 @@ def convert_token_to_string(self, token): instruction = fsm.get_next_instruction(0) assert isinstance(instruction, Generate) - assert instruction.tokens == [5, 4] + assert_expected_tensor_ids(instruction.tokens, [5, 4]) assert fsm.get_next_state(state=0, token_id=5) == 1 assert fsm.get_next_state(state=0, token_id=tokenizer.eos_token_id) == -1 @@ -149,7 +156,7 @@ def convert_token_to_string(self, token): instruction = fsm.get_next_instruction(0) assert isinstance(instruction, Generate) - assert instruction.tokens == [5, 10] + assert_expected_tensor_ids(instruction.tokens, [5, 10]) assert fsm.get_next_state(state=0, token_id=5) == 1 assert fsm.get_next_state(state=0, token_id=tokenizer.eos_token_id) == -1 @@ -209,35 +216,35 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(fsm.start_state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 3, 5} + assert_expected_tensor_ids(instruction.tokens, [1, 3, 5]) state = fsm.get_next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "{" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) state = fsm.get_next_state(state=state, token_id=3) assert fsm.generation == "{[" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 3, 4} + assert_expected_tensor_ids(instruction.tokens, [1, 3, 4]) state = fsm.get_next_state(state=state, token_id=4) assert fsm.generation == "{[]" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {2} + assert_expected_tensor_ids(instruction.tokens, [2]) state = fsm.get_next_state(state=state, token_id=2) assert fsm.generation == "{[]}" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Write) - assert set(instruction.tokens) == {5} + assert_expected_tensor_ids(instruction.tokens, [5]) state = fsm.get_next_state(state=state, token_id=5) assert fsm.generation == "{[]}" assert fsm.is_final_state(state) @@ -270,14 +277,14 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(fsm.start_state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1} + assert_expected_tensor_ids(instruction.tokens, [1]) state = fsm.get_next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "(" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2} + assert_expected_tensor_ids(instruction.tokens, [1, 2]) state = fsm.get_next_state(state=state, token_id=2) assert fsm.generation == "()" assert not fsm.is_final_state(state) @@ -285,7 +292,7 @@ def decode(self, token_ids): # possible to continue or terminate instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 3]) state = fsm.get_next_state(state=state, token_id=3) # feed eos assert fsm.generation == "()" assert fsm.is_final_state(state) @@ -293,7 +300,7 @@ def decode(self, token_ids): # once eos generated, can only terminate instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Write) - assert set(instruction.tokens) == {3} + assert_expected_tensor_ids(instruction.tokens, [3]) def test_cfg_ignore_directive(): @@ -326,42 +333,42 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(0) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2} + assert_expected_tensor_ids(instruction.tokens, [1, 2]) state = fsm.get_next_state(state=0, token_id=2) assert fsm.generation == " " assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(0) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2} + assert_expected_tensor_ids(instruction.tokens, [1, 2]) state = fsm.get_next_state(state=0, token_id=1) assert fsm.generation == " a" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) state = fsm.get_next_state(state=state, token_id=2) assert fsm.generation == " a " assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) state = fsm.get_next_state(state=state, token_id=2) assert fsm.generation == " a " assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) state = fsm.get_next_state(state=state, token_id=1) assert fsm.generation == " a a" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) state = fsm.get_next_state(state=state, token_id=3) assert fsm.generation == " a a" assert fsm.is_final_state(state) @@ -369,7 +376,7 @@ def decode(self, token_ids): # once eos generated, can only terminate instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Write) - assert set(instruction.tokens) == {3} + assert_expected_tensor_ids(instruction.tokens, [3]) def test_cfg_multitoken_terminal(): @@ -398,7 +405,7 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(fsm.start_state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 2} + assert_expected_tensor_ids(instruction.tokens, [1, 2]) assert fsm.reset_state # starting new regex state = fsm.get_next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "a" @@ -406,7 +413,7 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1} + assert_expected_tensor_ids(instruction.tokens, [1]) assert not fsm.reset_state # continuing current regex state = fsm.get_next_state(state=state, token_id=1) assert fsm.generation == "aa" @@ -414,7 +421,7 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Write) - assert set(instruction.tokens) == {3} + assert_expected_tensor_ids(instruction.tokens, [3]) assert not fsm.reset_state # completing current regex state = fsm.get_next_state(state=state, token_id=3) assert fsm.generation == "aa" @@ -447,35 +454,35 @@ def decode(self, token_ids): instruction = fsm.get_next_instruction(fsm.start_state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 3]) state = fsm.get_next_state(state=fsm.start_state, token_id=1) assert fsm.generation == "(" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {1, 3} + assert_expected_tensor_ids(instruction.tokens, [1, 3]) state = fsm.get_next_state(state=state, token_id=3) assert fsm.generation == "(a" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {2, 3} + assert_expected_tensor_ids(instruction.tokens, [2, 3]) state = fsm.get_next_state(state=state, token_id=3) assert fsm.generation == "(aa" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Generate) - assert set(instruction.tokens) == {2, 3} + assert_expected_tensor_ids(instruction.tokens, [2, 3]) state = fsm.get_next_state(state=state, token_id=2) assert fsm.generation == "(aa)" assert not fsm.is_final_state(state) instruction = fsm.get_next_instruction(state) assert isinstance(instruction, Write) - assert set(instruction.tokens) == {4} + assert_expected_tensor_ids(instruction.tokens, [4]) state = fsm.get_next_state(state=state, token_id=4) assert fsm.generation == "(aa)" assert fsm.is_final_state(state) From ea5fbdbbb035abe2b46afe7d16f134a4ef973cde Mon Sep 17 00:00:00 2001 From: Rohan Shah <57906961+rshah713@users.noreply.github.com> Date: Wed, 10 Jul 2024 10:25:22 -0400 Subject: [PATCH 003/505] fix json prompt example --- docs/reference/prompting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/prompting.md b/docs/reference/prompting.md index 34860fce0..8ea9c0243 100644 --- a/docs/reference/prompting.md +++ b/docs/reference/prompting.md @@ -260,7 +260,8 @@ pretty print a dictionary from within an Outlines prompt function def my_prompt(response_model): """{{ response_model | schema }}""" - my_prompt(MyResponse) + prompt = my_prompt(MyResponse) + print(prompt) # { # "field1": "an int", # "field2": "" From 497ed9fdeaa1583a46fdbdd6eedb04d052fc44cf Mon Sep 17 00:00:00 2001 From: Rin Date: Thu, 11 Jul 2024 14:36:32 +0100 Subject: [PATCH 004/505] Fix broken link in README.md regarding Serving with vLLM updated the link to https://outlines-dev.github.io/outlines/reference/serve/vllm/ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aeb1126b6..4ed1e9237 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ First time here? Go to our [setup guide](https://outlines-dev.github.io/outlines - [x] 💾 Caching of generations - [x] 🗂️ Batch inference - [x] 🎲 Sample with the greedy, multinomial and beam search algorithms (and more to come!) -- [x] 🚀 [Serve with vLLM](https://outlines-dev.github.io/outlines/reference/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! +- [x] 🚀 [Serve with vLLM](https://outlines-dev.github.io/outlines/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! Outlines 〰 has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date! From 0383ce1c17114cf5031e3a95d8781eb6a42e6365 Mon Sep 17 00:00:00 2001 From: Jules Gagnon-Marchand Date: Sun, 7 Jul 2024 20:37:27 -0700 Subject: [PATCH 005/505] Fix bug in batched multi sample generation --- outlines/generate/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index 51a995664..8b2561075 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -372,7 +372,7 @@ def token_generator() -> Iterator[Union[List[str], str, List[List[str]]]]: previously_generated_sequences = generated_sequences # We reshape the output to (batch_size, sample_size) output: List[List[str]] = list() - for i in range(batch_size): + for i in range(0, batch_size * num_samples, num_samples): output.append(next_tokens[i : i + num_samples]) # We remove leading dimensions for the output From 8c5f1d86889cf9ea110b5736eb676ba2ef4f4353 Mon Sep 17 00:00:00 2001 From: Jules Gagnon-Marchand Date: Sun, 7 Jul 2024 21:38:53 -0700 Subject: [PATCH 006/505] Fixed __call__ as well --- outlines/generate/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index 8b2561075..f739dc98c 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -231,8 +231,8 @@ def __call__( # We reshape the output to (batch_size, sample_size) output: List[List[FormattedOutput]] = list() - for i in range(batch_size): - output.append(formatted[i : i + num_samples]) + for i in range(0, batch_size * num_samples, num_samples): + output.append(formatted[i : i + num_samples] # We remove leading dimensions for the output if batch_size == 1 and num_samples == 1: From b54a96404e880383ad50071af5e9a5560680c4a3 Mon Sep 17 00:00:00 2001 From: Jules Gagnon-Marchand Date: Mon, 8 Jul 2024 09:35:32 -0700 Subject: [PATCH 007/505] Update outlines/generate/api.py Co-authored-by: Patrice Bechard --- outlines/generate/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index f739dc98c..4104e3080 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -232,7 +232,7 @@ def __call__( # We reshape the output to (batch_size, sample_size) output: List[List[FormattedOutput]] = list() for i in range(0, batch_size * num_samples, num_samples): - output.append(formatted[i : i + num_samples] + output.append(formatted[i : i + num_samples]) # We remove leading dimensions for the output if batch_size == 1 and num_samples == 1: From 59d88dc02df78d7810e8e51cc7860197392a89ca Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Fri, 12 Jul 2024 17:21:19 +0300 Subject: [PATCH 008/505] Update file contributors style for docs --- docs/stylesheets/extra.css | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 8611360d2..fdf3acb9e 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -9,9 +9,6 @@ --md-code-fg-color: #FFFFFF; --md-text-font-family: "Inter"; --md-code-font: "Source Code Pro Custom"; - --md-default-fg-color--light: #D8DEE9; - --md-default-fg-color--lighter: #E5E9F0; - --md-default-fg-color--lightest: #ECEFF4; } .index-pre-code { @@ -23,6 +20,19 @@ text-align: left; } +.md-clipboard::after { + color: #FFFFFF; + transition: color 0.3s ease-in-out; +} + +.md-clipboard:hover::after { + color: #D8DEE9; +} + +.md-source-file { + text-align: center; + padding: 24px 0; +} .md-typeset pre>code { border-radius: .2rem; From a48f86ffb4b149ba2237dae72647e8adc40e778f Mon Sep 17 00:00:00 2001 From: Tal Date: Sat, 13 Jul 2024 21:47:09 +0300 Subject: [PATCH 009/505] Add missing blank space (#1036) --- outlines/generate/format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outlines/generate/format.py b/outlines/generate/format.py index d87a3fe70..d7b7e164d 100644 --- a/outlines/generate/format.py +++ b/outlines/generate/format.py @@ -41,5 +41,5 @@ def format(model, python_type, sampler: Sampler = multinomial()) -> SequenceGene def format_openai(model, python_type, sampler: Sampler = multinomial()): raise NotImplementedError( "Cannot use Python type-structured generation with an OpenAI model" - + "due to the limitations of the OpenAI API." + + " due to the limitations of the OpenAI API." ) From 62b7601d3a6aa56cd11d51b1404a6afd6e7efb91 Mon Sep 17 00:00:00 2001 From: Franz Louis Cesista Date: Mon, 15 Jul 2024 01:02:35 +0800 Subject: [PATCH 010/505] Adds support for custom regex parsers (for multimodal structured generation) (#1039) As [discussed in our Discord server](https://discord.com/channels/1182316225284554793/1182317446225481788/1261998326077984802) This PR adds support for custom regex parsers. This doesn't change the behavior of Outlines by default. But this allows us to write custom `Guide` classes that uses custom regex parsers for e.g. multimodal generation. Also improves documentation --- outlines/fsm/guide.py | 54 ++++++++++++++--- outlines/fsm/regex.py | 112 ++++++++++++++++++++++++++++++----- outlines/models/llamacpp.py | 2 +- outlines/models/tokenizer.py | 2 +- tests/fsm/test_regex.py | 2 + 5 files changed, 148 insertions(+), 24 deletions(-) diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index 792eba34f..aa694232e 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -1,5 +1,15 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, List, Optional, Protocol, Tuple, Union +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + List, + Optional, + Protocol, + Set, + Tuple, + Union, +) import interegular import torch @@ -108,16 +118,44 @@ def copy(self): @cache() def create_states_mapping( - regex_string: str, tokenizer: "Tokenizer" -) -> Tuple[dict, set, set]: + regex_string: str, + tokenizer: "Tokenizer", + regex_parser: Callable[[str], interegular.Pattern] = interegular.parse_pattern, + frozen_tokens: List[str] = [], +) -> Tuple[Dict[int, Dict[int, int]], Set[int], set]: """Create the variables related to the mapping between states and tokens - The parameters of the function are used for caching purpose + The parameters of the function are used for caching purpose. + + Parameters + ---------- + regex_string: (`str`): + The regular expression string to generate a states mapping for. + tokenizer: (`Tokenizer`): + The model's tokenizer. + regex_parser: (`Callable[[str], interegular.Pattern]`, *optional*): + A function that parses a regex string into an `interegular` Pattern object. + frozen_tokens: (`List[str]`, *optional*): + A list of tokens that should be kept as-is when expanding the token-level FSM + into a byte-level FSM. Defaults to an empty list. + + Returns + ------- + states_to_token_maps: (`Dict[int, Dict[int, int]]`): + A mapping from states to a mapping from token ids originating from that state + to the next state to transition to given that token. The structure is as follows: + (origin_state -> (token_id -> next_state)) + empty_token_ids: (`Set[int]`): + A set of token ids that correspond to empty strings. + final_states: (`set`): + A set of final states in the FSM. """ - regex_pattern = interegular.parse_pattern(regex_string) - byte_fsm = make_byte_level_fsm(regex_pattern.to_fsm().reduce(), keep_utf8=True) + regex_pattern = regex_parser(regex_string) + byte_fsm = make_byte_level_fsm( + regex_pattern.to_fsm().reduce(), keep_utf8=True, frozen_tokens=frozen_tokens + ) regex_fsm, _ = make_deterministic_fsm(byte_fsm) states_to_token_maps, empty_token_ids = create_fsm_index_tokenizer( - regex_fsm, tokenizer + regex_fsm, tokenizer, frozen_tokens=frozen_tokens ) # We make sure that it is possible to generate strings in the language @@ -138,7 +176,7 @@ class RegexGuide(Guide): initial_state = 0 - def __init__(self, regex_string: str, tokenizer): + def __init__(self, regex_string: str, tokenizer: "Tokenizer"): ( self.states_to_token_maps, self.empty_token_ids, diff --git a/outlines/fsm/regex.py b/outlines/fsm/regex.py index ff8dd9131..8cfd81ead 100644 --- a/outlines/fsm/regex.py +++ b/outlines/fsm/regex.py @@ -199,11 +199,27 @@ def byte_symbol(byte: int) -> str: return f"\x00{byte:02X}" if byte >= 0x80 else chr(byte) -def make_byte_level_fsm(fsm: FSM, keep_utf8=False) -> FSM: +def make_byte_level_fsm( + fsm: FSM, keep_utf8: bool = False, frozen_tokens: List[str] = [] +) -> FSM: """Convert an FSM to a byte-level FSM, expanding multi-byte characters as - sequences of single-byte transitions. If keep_utf8 is set, the original - utf-8 characters are kept in the alphabet. - NOTE: we're representing bytes as strings to keep it type-compatible. + sequences of single-byte transitions. + + Parameters + ---------- + fsm: (`interegular.FSM`): + The token-level FSM to convert to a byte-level FSM. + keep_utf8: (`bool`, *optional*): + If set to True, the original utf-8 characters are kept as-is. Defaults to + False. NOTE: we're representing bytes as strings to keep it type-compatible. + frozen_tokens: (`List[str]`, *optional*): + A list of tokens that should be kept as-is in the byte-level FSM. That is, + these tokens will not be expanded into byte-level transitions. Defaults to + an empty list. + + Returns + ------- + `interegular.FSM`: A byte-level FSM. """ anything_else_key = fsm.alphabet[anything_else] @@ -218,8 +234,8 @@ def make_byte_level_fsm(fsm: FSM, keep_utf8=False) -> FSM: all_bytes: Set[int] = set() max_key = max(fsm.alphabet.values()) for symbol, transition_key in fsm.alphabet.items(): - assert symbol == anything_else or len(symbol) == 1 - if symbol == anything_else or ord(symbol) < 0x80: + assert symbol == anything_else or symbol in frozen_tokens or len(symbol) == 1 + if symbol == anything_else or symbol in frozen_tokens or ord(symbol) < 0x80: symbol_mapping[symbol] = transition_key else: if keep_utf8: @@ -714,15 +730,40 @@ def get_vocabulary_transition_keys( alphabet_symbol_mapping: Dict[str, int], alphabet_anything_value: int, vocabulary: List[Tuple[str, Sequence[int]]], + frozen_tokens: List[str] = numba.typed.List.empty_list(numba.types.unicode_type), ) -> List[Sequence[int]]: """ Calculate the sequence transition keys for each token str within a vocabulary + + Parameters + ---------- + alphabet_symbol_mapping: (`Dict[str, int]`): + A mapping from an alphabet symbol in a FSM to its corresponding transition key. + alphabet_anything_value: (`int`): + The transition key for the anything_else symbol in the FSM. + vocabulary: (`List[Tuple[str, Sequence[int]]]`): + A list of tuples, each containing a token and a list of equivalent token ids. + frozen_tokens: (`List[str]`, *optional*): + A list of tokens that are kept as-is when transforming the FSM. + Defaults to an empty list. + + Returns + ------- + `List[Sequence[int]]`: + A list of token transition keys for each token in the vocabulary. """ vocab_transition_keys = numba.typed.List.empty_list(numba.int64[:]) for token_str, _ in vocabulary: - token_transition_keys = get_token_transition_keys( - alphabet_symbol_mapping, alphabet_anything_value, token_str - ) + # Since these tokens are not expanded into byte-level transitions, we can + # simply get their transition keys directly. + if token_str in frozen_tokens: + token_transition_keys = np.array( + [alphabet_symbol_mapping[token_str]], dtype=np.int64 + ) + else: + token_transition_keys = get_token_transition_keys( + alphabet_symbol_mapping, alphabet_anything_value, token_str + ) vocab_transition_keys.append(token_transition_keys) return vocab_transition_keys @@ -731,8 +772,26 @@ def get_vocabulary_transition_keys( def create_fsm_index_end_to_end( fsm_info: FSMInfo, vocabulary: List[Tuple[str, Sequence[int]]], + frozen_tokens: List[str] = [], ) -> Dict[int, Set[Tuple[int, int]]]: - """Create an FSM state-to-vocabulary map/index through end-to-end token parsing.""" + """Create an FSM state-to-vocabulary map/index through end-to-end token parsing. + + Parameters + ---------- + fsm_info: (`interegular.FSMInfo`): + The FSM information object containing the FSM's alphabet, transitions, initial + and final states, and other relevant information. + vocabulary: (`List[Tuple[str, Sequence[int]]]`): + A list of tuples, each containing a token and a list of equivalent token ids. + frozen_tokens: (`List[str]`, *optional*): + A list of tokens that are kept as-is when transforming the FSM. + + Returns + ------- + `Dict[int, Set[Tuple[int, int]]]`: + A mapping from FSM states to sets of tuples containing token ids and the end + states of the FSM after parsing the token. + """ # TODO: Consider using a `List` of `Set`s instead; that way we can JIT this # code, too. @@ -750,6 +809,11 @@ def create_fsm_index_end_to_end( fsm_info.alphabet_symbol_mapping, fsm_info.alphabet_anything_value, vocabulary, + frozen_tokens=( + numba.typed.List(frozen_tokens) + if len(frozen_tokens) > 0 + else numba.typed.List.empty_list(numba.types.unicode_type) + ), ) while next_states: @@ -883,21 +947,41 @@ def reduced_vocabulary( def create_fsm_index_tokenizer( - fsm: BetterFSM, - tokenizer: "Tokenizer", + fsm: BetterFSM, tokenizer: "Tokenizer", frozen_tokens: List[str] = [] ) -> Tuple[Dict[int, Dict[int, int]], Set[int]]: """Construct an FMS index from a tokenizer. This uses the end-to-end approach of `create_fsm_index_end_to_end`. + Parameters + ---------- + fsm: (`BetterFSM`): + A cache-friendly FSM. Other interegular FSMs can also be used, but caching + may not work as expected. + tokenizer: (`Tokenizer`): + The model's tokenizer. + frozen_tokens: (`List[str]`, *optional*): + A list of tokens that should be kept as-is when expanding the token-level + FSM into a byte-level FSM. Defaults to an empty list. + + Returns + ------- + states_to_token_maps: (`Dict[int, Dict[int, int]]`): + A mapping from states to a mapping from token ids originating from that state + to the next state to transition to given that token. The structure is as follows: + (origin_state -> (token_id -> next_state)) + empty_token_ids: (`Set[int]`): + A set of token ids that correspond to empty strings. + .. warning:: `fsm` needs to be deterministically ordered so that future caching makes sense. - """ vocabulary, empty_token_ids = reduced_vocabulary(tokenizer) - states_to_token_subsets = create_fsm_index_end_to_end(fsm.fsm_info, vocabulary) + states_to_token_subsets = create_fsm_index_end_to_end( + fsm.fsm_info, vocabulary, frozen_tokens + ) # Allow transitions to EOS from all terminals FSM states that are # reachable diff --git a/outlines/models/llamacpp.py b/outlines/models/llamacpp.py index aa260a457..7e223a9fa 100644 --- a/outlines/models/llamacpp.py +++ b/outlines/models/llamacpp.py @@ -27,7 +27,7 @@ def __init__(self, model: "Llama"): self.eos_token_id = model.token_eos() self.eos_token = model.tokenizer().decode([self.eos_token_id]) self.pad_token_id = self.eos_token_id - self.special_tokens: Set[int] = set() + self.special_tokens: Set[str] = set() self.vocabulary: Dict[str, int] = dict() diff --git a/outlines/models/tokenizer.py b/outlines/models/tokenizer.py index 949414a44..1a5708d85 100644 --- a/outlines/models/tokenizer.py +++ b/outlines/models/tokenizer.py @@ -9,7 +9,7 @@ class Tokenizer(Hashable, Protocol): eos_token_id: int pad_token_id: int vocabulary: Dict[str, int] - special_tokens: Set[int] + special_tokens: Set[str] def encode( self, prompt: Union[str, List[str]] diff --git a/tests/fsm/test_regex.py b/tests/fsm/test_regex.py index 7ba4f9caa..fa72ad0dc 100644 --- a/tests/fsm/test_regex.py +++ b/tests/fsm/test_regex.py @@ -600,6 +600,7 @@ def convert_token_to_string(self, token): regex_fsm.fsm_info.alphabet_symbol_mapping, regex_fsm.fsm_info.alphabet_anything_value, vocabulary, + numba.typed.List.empty_list(numba.types.unicode_type), ) token_str_to_tranition_keys = { @@ -637,6 +638,7 @@ def convert_token_to_string(self, token): regex_fsm.fsm_info.alphabet_symbol_mapping, regex_fsm.fsm_info.alphabet_anything_value, vocabulary, + numba.typed.List.empty_list(numba.types.unicode_type), ) token_str_trans_key_seq = { From 82248550b3aed82f8d95271686759368437fc9aa Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 1 Jul 2024 08:56:49 -0500 Subject: [PATCH 011/505] Use LogitsProcessors for models.transformers, apply in outlines.generate.* --- README.md | 5 +- docs/reference/models/transformers.md | 53 ++- docs/reference/text.md | 5 +- examples/llamacpp_example.py | 6 +- outlines/generate/cfg.py | 42 +-- outlines/generate/regex.py | 6 +- outlines/generate/text.py | 5 +- outlines/models/__init__.py | 2 +- outlines/models/transformers.py | 184 +++++++++- tests/generate/test_integration_llamacpp.py | 9 +- .../generate/test_integration_transformers.py | 320 +++++------------- tests/models/test_transformers.py | 9 +- 12 files changed, 358 insertions(+), 288 deletions(-) diff --git a/README.md b/README.md index 4ed1e9237..96d02dbe1 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,9 @@ model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") generator = outlines.generate.json(model, Character) # Draw a sample -rng = torch.Generator(device="cuda") -rng.manual_seed(789001) +seed = 789001 -character = generator("Give me a character description", rng=rng) +character = generator("Give me a character description", seed=seed) print(repr(character)) # Character(name='Anderson', age=28, armor=, weapon=, strength=8) diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index 286df4367..7c1febd02 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -15,7 +15,7 @@ Outlines provides an integration with the `torch` implementation of causal model ```python from outlines import models -model = models.transformers("mistralai/Mistral-7B-v0.1", device="cuda") +model = models.transformers("mistralai/Mistral-7B-v0.3", device="cuda") ``` If you need more fine-grained control you can also initialize the model and tokenizer separately: @@ -30,4 +30,55 @@ tokenizer = AutoTokenizer.from_pretrained("gpt2") model = models.Transformers(llm, tokenizer) ``` +# Using Logits Processors + +There are two ways to use Outlines Structured Generation with HuggingFace Transformers: +- 1) Use Outlines generation wrapper, `outlines.models.transformers` +- 2) Use `OutlinesLogitsProcessor` with `transformers.AutoModelForCausalLM` + +Outlines supports a myriad of logits processors for structured generation. In these example, we will use the `RegexLogitsProcessor` which guarantees generated text matches the specified pattern. + +## Example: `outlines.models.transformers` + +``` +import outlines + +time_regex_pattern = r"(0?[1-9]|1[0-2]):[0-5]\d\s?(am|pm)?" + +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct", device="cuda") +generator = outlines.generate.regex(model, time_regex_pattern) + +output = generator("The the best time to visit a dentist is at ") +print(output) +# 2:30 pm +``` + +## Example: Direct `transformers` library use + +``` +import outlines +import transformers + + +model_uri = "microsoft/Phi-3-mini-4k-instruct" + +outlines_tokenizer = outlines.models.TransformerTokenizer( + transformers.AutoTokenizer.from_pretrained(model_uri) +) +phone_number_logits_processor = outlines.processors.RegexLogitsProcessor( + "\\+?[1-9][0-9]{7,14}", # phone number pattern + outlines_tokenizer, +) + +generator = transformers.pipeline('text-generation', model=model_uri) + +output = generator( + "Jenny gave me her number it's ", + logits_processor=transformers.LogitsProcessorList([phone_number_logits_processor]) +) +print(output) +# [{'generated_text': "Jenny gave me her number it's 2125550182"}] +# not quite 8675309 what we expected, but it is a valid phone number +``` + [transformers]: https://github.com/huggingface/transformers diff --git a/docs/reference/text.md b/docs/reference/text.md index f364c3d2e..423a26ba6 100644 --- a/docs/reference/text.md +++ b/docs/reference/text.md @@ -80,8 +80,7 @@ from outlines import models, generate model = models.transformers("mistralai/Mistral-7B-v0.1") -rng = torch.Generator(device="cuda") -rng.manual_seed(789001) +seed = 789001 -answer = generator("What is 2+2?", rng=rng) +answer = generator("What is 2+2?", seed=seed) ``` diff --git a/examples/llamacpp_example.py b/examples/llamacpp_example.py index 0b478217f..22d0da3ba 100644 --- a/examples/llamacpp_example.py +++ b/examples/llamacpp_example.py @@ -1,6 +1,5 @@ from enum import Enum -import torch from pydantic import BaseModel, constr import outlines @@ -37,10 +36,9 @@ class Character(BaseModel): generator = outlines.generate.json(model, Character) # Draw a sample - rng = torch.Generator(device="cpu") - rng.manual_seed(789005) + seed = 789005 prompt = "Instruct: You are a leading role play gamer. You have seen thousands of different characters and their attributes.\nPlease return a JSON object with common attributes of an RPG character. Give me a character description\nOutput:" - sequence = generator(prompt, rng=rng, max_tokens=512) + sequence = generator(prompt, seed=seed, max_tokens=512) print(sequence) diff --git a/outlines/generate/cfg.py b/outlines/generate/cfg.py index e473c26a6..0df833067 100644 --- a/outlines/generate/cfg.py +++ b/outlines/generate/cfg.py @@ -1,16 +1,14 @@ from functools import singledispatch -from outlines.fsm.guide import CFGGuide -from outlines.generate.api import SequenceGenerator, SequenceGeneratorAdapter +from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI -from outlines.models.llamacpp import LlamaCpp -from outlines.models.mlxlm import MLXLM -from outlines.models.vllm import VLLM from outlines.samplers import Sampler, multinomial @singledispatch -def cfg(model, cfg_str: str, sampler: Sampler = multinomial()) -> SequenceGenerator: +def cfg( + model, cfg_str: str, sampler: Sampler = multinomial() +) -> SequenceGeneratorAdapter: """Generate text in the language of a Context-Free Grammar Arguments @@ -24,40 +22,16 @@ def cfg(model, cfg_str: str, sampler: Sampler = multinomial()) -> SequenceGenera Returns ------- - A `SequenceGenerator` instance that generates text. + A `SequenceGeneratorAdapter` instance that generates text. """ - fsm = CFGGuide(cfg_str, model.tokenizer) - device = model.device - generator = SequenceGenerator(fsm, model, sampler, device) - - return generator - - -@cfg.register(MLXLM) -@cfg.register(VLLM) -def cfg_unimplemented( - model, - cfg_str: str, - sampler: Sampler = multinomial(), -): raise NotImplementedError( - f"The CFG Logits processor is not available for {type(model)}." + f"The CFG Logits processor is not available for {type(model)}. " + + "Please subscribe to https://github.com/outlines-dev/outlines/issues/684" + + " for updates on the fix." ) -@cfg.register(LlamaCpp) -def cfg_llamacpp( - model: LlamaCpp, - cfg_str: str, - sampler: Sampler = multinomial(), -): - from outlines.integrations.llamacpp import CFGLogitsProcessor - - logits_processor = CFGLogitsProcessor(cfg_str, model.model) - return SequenceGeneratorAdapter(model, logits_processor, sampler) - - @cfg.register(OpenAI) def cfg_openai(model, cfg_str: str, sampler: Sampler = multinomial()): raise NotImplementedError( diff --git a/outlines/generate/regex.py b/outlines/generate/regex.py index 6b6656fe9..cdf64a21f 100644 --- a/outlines/generate/regex.py +++ b/outlines/generate/regex.py @@ -5,6 +5,7 @@ from outlines.models import OpenAI from outlines.models.llamacpp import LlamaCpp from outlines.models.mlxlm import MLXLM +from outlines.models.transformers import Transformers from outlines.models.vllm import VLLM from outlines.samplers import Sampler, multinomial @@ -39,8 +40,9 @@ def regex(model, regex_str: str, sampler: Sampler = multinomial()): @regex.register(MLXLM) -def regex_mlxlm( - model: MLXLM, +@regex.register(Transformers) +def regex_unified( + model, regex_str: str, sampler: Sampler = multinomial(), ): diff --git a/outlines/generate/text.py b/outlines/generate/text.py index 081ba0920..b8feb7659 100644 --- a/outlines/generate/text.py +++ b/outlines/generate/text.py @@ -2,7 +2,7 @@ from outlines.fsm.guide import StopAtEOSGuide from outlines.generate.api import SequenceGenerator, SequenceGeneratorAdapter -from outlines.models import MLXLM, VLLM, LlamaCpp, OpenAI +from outlines.models import MLXLM, VLLM, LlamaCpp, OpenAI, Transformers from outlines.samplers import Sampler, multinomial @@ -37,7 +37,8 @@ def text(model, sampler: Sampler = multinomial()) -> SequenceGenerator: @text.register(MLXLM) -def text_mlxlm(model: MLXLM, sampler: Sampler = multinomial()): +@text.register(Transformers) +def text_unified(model, sampler: Sampler = multinomial()): return SequenceGeneratorAdapter(model, None, sampler) diff --git a/outlines/models/__init__.py b/outlines/models/__init__.py index 4d74ebf60..fde913e2c 100644 --- a/outlines/models/__init__.py +++ b/outlines/models/__init__.py @@ -12,7 +12,7 @@ from .mamba import Mamba, mamba from .mlxlm import MLXLM, mlxlm from .openai import OpenAI, azure_openai, openai -from .transformers import Transformers, transformers +from .transformers import Transformers, TransformerTokenizer, transformers from .vllm import VLLM, vllm LogitsGenerator = Union[Transformers, LlamaCpp, ExLlamaV2Model, Mamba, MLXLM, VLLM] diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index fae9b8e74..4c475617a 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -1,13 +1,17 @@ -from typing import TYPE_CHECKING, List, Optional, Tuple, Union +import dataclasses +from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union from datasets.fingerprint import Hasher +from outlines.generate.api import GenerationParameters, SamplingParameters from outlines.models.tokenizer import Tokenizer if TYPE_CHECKING: import torch from transformers import PreTrainedModel, PreTrainedTokenizer + from outlines.processors import OutlinesLogitsProcessor + __all__ = ["transformers"] @@ -129,7 +133,6 @@ def __init__( model: "PreTrainedModel", tokenizer: "PreTrainedTokenizer", ): - self.device = model.device self.model = model self.tokenizer = TransformerTokenizer(tokenizer) @@ -190,6 +193,183 @@ def __call__( return next_token_logits, kv_cache + def generate( + self, + prompts: Union[str, List[str]], + generation_parameters: GenerationParameters, + logits_processor: Optional["OutlinesLogitsProcessor"], + sampling_parameters: SamplingParameters, + ) -> Union[str, List[str], List[List[str]]]: + """Generate text using `transformers`. + + Arguments + --------- + prompts + A prompt or list of prompts. + generation_parameters + An instance of `GenerationParameters` that contains the prompt, + the maximum number of tokens, stop sequences and seed. All the + arguments to `SequenceGeneratorAdapter`'s `__cal__` method. + logits_processor + The logits processor to use when generating text. + sampling_parameters + An instance of `SamplingParameters`, a dataclass that contains + the name of the sampler to use and related parameters as available + in Outlines. + + Returns + ------- + The generated text + """ + if isinstance(prompts, str): + # convert to 2d + input_ids, attention_mask = self.tokenizer.encode([prompts]) + else: + input_ids, attention_mask = self.tokenizer.encode(prompts) + inputs = { + "input_ids": input_ids.to(self.model.device), + "attention_mask": attention_mask.to(self.model.device), + } + + generation_kwargs = self._get_generation_kwargs( + prompts, + generation_parameters, + logits_processor, + sampling_parameters, + ) + generated_ids = self._generate_output_seq(prompts, inputs, **generation_kwargs) + + # if single str input and single sample per input, convert to a 1D output + if isinstance(prompts, str): + generated_ids = generated_ids.squeeze(0) + + return self._decode_generation(generated_ids) + + def stream( + self, + prompts: Union[str, List[str]], + generation_parameters: GenerationParameters, + logits_processor: Optional["OutlinesLogitsProcessor"], + sampling_parameters: SamplingParameters, + ) -> Iterator[Union[str, List[str]]]: + """ + Temporary stream stand-in which implements stream() signature + and equivalent behaviour but isn't yielded until generation completes. + + TODO: implement following completion of https://github.com/huggingface/transformers/issues/30810 + """ + if isinstance(prompts, str): + # convert to 2d + input_ids, attention_mask = self.tokenizer.encode([prompts]) + else: + input_ids, attention_mask = self.tokenizer.encode(prompts) + inputs = { + "input_ids": input_ids.to(self.model.device), + "attention_mask": attention_mask.to(self.model.device), + } + + generation_kwargs = self._get_generation_kwargs( + prompts, + generation_parameters, + logits_processor, + sampling_parameters, + ) + generated_ids = self._generate_output_seq(prompts, inputs, **generation_kwargs) + + # if single str input and single sample per input, convert to a 1D output + if isinstance(prompts, str): + generated_ids = generated_ids.squeeze(0) + + for i in range(generated_ids.size(-1)): + output_group_ids = generated_ids.select(-1, i).unsqueeze(-1) + yield self._decode_generation(output_group_ids) + + def _get_generation_kwargs( + self, + prompts: Union[str, List[str]], + generation_parameters: GenerationParameters, + logits_processor: Optional["OutlinesLogitsProcessor"], + sampling_parameters: SamplingParameters, + ) -> dict: + """ + Conert outlines generation parameters into model.generate kwargs + """ + from transformers import GenerationConfig, LogitsProcessorList, set_seed + + max_new_tokens, stop_at, seed = dataclasses.astuple(generation_parameters) + sampler, num_samples, top_p, top_k, temperature = dataclasses.astuple( + sampling_parameters + ) + if max_new_tokens is None: + max_new_tokens = int(2**30) + + # global seed, not desirable + if seed is not None: + set_seed(seed) + + if logits_processor is not None: + logits_processor_list = LogitsProcessorList([logits_processor]) + else: + logits_processor_list = None + + generation_config = GenerationConfig( + max_new_tokens=max_new_tokens, + stop_strings=stop_at, + num_return_sequences=(num_samples or 1), + top_p=top_p, + top_k=top_k, + temperature=temperature, + do_sample=(sampler == "multinomial"), + num_beams=(num_samples if sampler == "beam_search" else 1), + eos_token_id=self.tokenizer.eos_token_id, + pad_token_id=self.tokenizer.pad_token_id, + ) + + return dict( + logits_processor=logits_processor_list, + generation_config=generation_config, + tokenizer=self.tokenizer.tokenizer, + ) + + def _generate_output_seq( + self, prompts, inputs, generation_config, **generation_kwargs + ): + input_ids = inputs["input_ids"] + output_ids = self.model.generate( + generation_config=generation_config, **inputs, **generation_kwargs + ) + + # encoder-decoder returns output_ids only, decoder-only returns full seq ids + if self.model.config.is_encoder_decoder: + generated_ids = output_ids + else: + generated_ids = output_ids[:, input_ids.shape[1] :] + + # if batch list inputs AND multiple samples per input, convert generated_id to 3D view + num_samples = generation_config.num_return_sequences or 1 + + if num_samples > 1 and isinstance(prompts, list): + batch_size = input_ids.size(0) + num_return_sequences = generation_config.num_return_sequences or 1 + generated_ids = generated_ids.view(batch_size, num_return_sequences, -1) + + return generated_ids + + def _decode_generation(self, generated_ids: "torch.Tensor"): + if len(generated_ids.shape) == 1: + return self.tokenizer.decode([generated_ids])[0] + elif len(generated_ids.shape) == 2: + return self.tokenizer.decode(generated_ids) + elif len(generated_ids.shape) == 3: + return [ + self.tokenizer.decode(generated_ids[i]) + for i in range(len(generated_ids)) + ] + else: + raise TypeError( + f"Generated outputs aren't 1D, 2D or 3D, but instead are {generated_ids.shape}" + ) + def transformers( model_name: str, diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py index 452d22c36..0d1908ebd 100644 --- a/tests/generate/test_integration_llamacpp.py +++ b/tests/generate/test_integration_llamacpp.py @@ -25,7 +25,7 @@ def model(tmp_path_factory): ( (generate.text, []), (generate.regex, ("[0-9]",)), - (generate.cfg, (grammars.arithmetic,)), + # (generate.cfg, (grammars.arithmetic,)), # Awaiting CFG fix ), ) def test_llamacpp_generation_api(model, generator_type, params): @@ -245,8 +245,11 @@ def test_llamacpp_json_schema(model): def test_llamacpp_cfg(model): prompt = "<|im_start|>user\nOutput a short and valid JSON object with two keys.<|im_end|>\n><|im_start|>assistant\n" - result = generate.cfg(model, grammars.arithmetic)(prompt, seed=11) - assert isinstance(result, str) + + # remove this statement once cfg is implemented + with pytest.raises(NotImplementedError): + result = generate.cfg(model, grammars.arithmetic)(prompt, seed=11) + assert isinstance(result, str) @pytest.mark.parametrize( diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index da08bed71..f3fb9682e 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -14,44 +14,50 @@ from outlines.samplers import beam_search, greedy, multinomial -def test_transformers_integration_text(): - rng = torch.Generator() - rng.manual_seed(10000) # Choosen so is generated +@pytest.fixture(scope="session") +def model(tmp_path_factory): + return models.transformers( + "hf-internal-testing/tiny-random-GPTJForCausalLM", device="cpu" + ) - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") - sequence = generate.text(model)("Write a short sentence ", rng=rng) + +def test_transformers_integration_text(model): + sequence = generate.text(model)( + "Write a short sentence ", seed=10000, max_tokens=10 + ) assert isinstance(sequence, str) assert model.tokenizer.eos_token not in sequence sequence = generate.text(model)( - "Write a short sentence ", max_tokens=10, stop_at=".", rng=rng + "Write a short sentence ", + max_tokens=20, + stop_at="a", + seed=10000, ) assert isinstance(sequence, str) prompts = ["Write a short sentence ", "And another one "] - sequence = generate.text(model)(prompts, max_tokens=10, stop_at=[".", ","], rng=rng) + sequence = generate.text(model)( + prompts, max_tokens=10, stop_at=[".", ","], seed=10000 + ) assert isinstance(sequence, list) assert len(sequence) == 2 assert isinstance(sequence[0], str) -def test_transformers_integration_text_multiple_samples(): - rng = torch.Generator() - rng.manual_seed(10000) # Choosen so is generated - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_integration_text_multiple_samples(model): sampler = multinomial(2) - sequence = generate.text(model, sampler=sampler)("Write a short sentence ", rng=rng) + sequence = generate.text(model, sampler=sampler)( + "Write a short sentence ", seed=10000 + ) assert isinstance(sequence, list) assert len(sequence) == 2 assert model.tokenizer.eos_token not in sequence prompts = ["Write a short sentence ", "And another one "] sequence = generate.text(model, sampler=sampler)( - prompts, max_tokens=10, stop_at=[".", ","], rng=rng + prompts, max_tokens=10, stop_at=[".", ","], seed=10000 ) assert isinstance(sequence, list) assert len(sequence) == 2 @@ -60,14 +66,9 @@ def test_transformers_integration_text_multiple_samples(): assert isinstance(sequence[0][0], str) -def test_transformers_integration_streaming(): - rng = torch.Generator() - rng.manual_seed(10000) # Choosen so is generated - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_integration_streaming(model): sequence = generate.text(model).stream( - "Write a short sentence ", max_tokens=10, stop_at=[".", ","], rng=rng + "Write a short sentence ", max_tokens=10, stop_at=[".", ","], seed=10000 ) token = next(sequence) @@ -77,7 +78,7 @@ def test_transformers_integration_streaming(): assert isinstance(remaining, str) sequence = generate.text(model).stream( - ["Prompt1", "Prompt2"], max_tokens=10, stop_at=[".", ","], rng=rng + ["Prompt1", "Prompt2"], max_tokens=10, stop_at=[".", ","], seed=10000 ) tokens = next(sequence) assert isinstance(tokens, list) @@ -85,19 +86,11 @@ def test_transformers_integration_streaming(): assert isinstance(tokens[1], str) -def test_transformers_integration_streaming_batch_samples(): - rng = torch.Generator() - rng.manual_seed(10000) # Choosen so is generated - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_integration_streaming_batch_samples(model): sampler = multinomial(samples=2) sequence = generate.text(model, sampler=sampler).stream( - ["Prompt1", "Prompt2"], - max_tokens=10, - stop_at=[".", ","], - rng=rng, + ["Prompt1", "Prompt2"], max_tokens=10, stop_at=[".", ","], seed=10000 ) tokens = next(sequence) assert isinstance(tokens, list) @@ -108,19 +101,11 @@ def test_transformers_integration_streaming_batch_samples(): assert len(tokens[1]) == 2 -def test_transformers_integration_streaming_batch_beam_search(): - rng = torch.Generator() - rng.manual_seed(10000) # Choosen so is generated - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_integration_streaming_batch_beam_search(model): sampler = beam_search(beams=2) - sequence = generate.text(model, sampler=sampler).stream( - ["Prompt1", "Prompt2"], - max_tokens=10, - stop_at=[".", ","], - rng=rng, + sequence = generate.regex(model, r"ab[cd]e", sampler=sampler).stream( + ["Prompt1", "Prompt2"], max_tokens=10, stop_at=["c", "d"], seed=10000 ) tokens = next(sequence) assert isinstance(tokens, list) @@ -131,61 +116,41 @@ def test_transformers_integration_streaming_batch_beam_search(): assert len(tokens[1]) == 2 -def test_transformers_integration_text_stop(): - rng = torch.Generator() - rng.manual_seed(10000) # Choosen so is generated - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_integration_text_stop(model): prompt = "Write a short sentence " - sequence = generate.text(model)(prompt, stop_at="a", rng=rng) + sequence = generate.text(model)(prompt, stop_at="a", seed=10000) assert sequence[len(prompt) :].find("a") == -1 -def test_transformers_various_regexes(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_various_regexes(model): prompt = "Write an email address" regex_str = r"([a-z]{10})@([a-z]{5})\.([a-z]{3})" generator = generate.regex(model, regex_str) # One prompt - sequence = generator(prompt, rng=rng) + sequence = generator(prompt, seed=0) assert re.fullmatch(regex_str, sequence) is not None -def test_transformers_various_regexes_prompt_list(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_various_regexes_prompt_list(model): prompt = "Write an email address" regex_str = r"([a-z]{10})@([a-z]{5})\.([a-z]{3})" generator = generate.regex(model, regex_str) # Two prompts - sequence = generator([prompt, prompt], rng=rng) + sequence = generator([prompt, prompt], seed=0) assert re.fullmatch(regex_str, sequence[0]) is not None assert re.fullmatch(regex_str, sequence[1]) is not None -def test_transformers_various_regexes_prompt_list_multiple_samples(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_various_regexes_prompt_list_multiple_samples(model): sampler = multinomial(samples=2) prompt = "Write an email address" regex_str = r"([a-z]{10})@([a-z]{5})\.([a-z]{3})" generator = generate.regex(model, regex_str, sampler=sampler) # Two prompts - sequence = generator([prompt, prompt], rng=rng) + sequence = generator([prompt, prompt], seed=0, max_tokens=500) assert isinstance(sequence, list) assert len(sequence) == 2 assert re.fullmatch(regex_str, sequence[0][0]) is not None @@ -194,12 +159,7 @@ def test_transformers_various_regexes_prompt_list_multiple_samples(): assert re.fullmatch(regex_str, sequence[1][1]) is not None -def test_transformers_various_regexes_prompt_list_beam_search(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_various_regexes_prompt_list_beam_search(model): sampler = beam_search(5) prompt_1 = "Write an email address" prompt_2 = "Random" @@ -207,7 +167,7 @@ def test_transformers_various_regexes_prompt_list_beam_search(): generator = generate.regex(model, regex_str, sampler=sampler) # Two prompts - sequence = generator([prompt_1, prompt_2], rng=rng) + sequence = generator([prompt_1, prompt_2], seed=0) assert isinstance(sequence, list) assert len(sequence) == 2 assert len(sequence[0]) == 5 @@ -217,105 +177,65 @@ def test_transformers_various_regexes_prompt_list_beam_search(): assert re.fullmatch(regex_str, sequence[1][1]) is not None -def test_transformers_integration_integer(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_integer(model): prompt = "Write a short sentence" - sequence = generate.format(model, int)(prompt, max_tokens=10, rng=rng) + sequence = generate.format(model, int)(prompt, max_tokens=10, seed=0) assert isinstance(sequence, int) -def test_transformers_integration_integer_array(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_integer_array(model): prompts = ["Give me a number", "And another one"] - sequence = generate.format(model, int)(prompts, max_tokens=10, rng=rng) + sequence = generate.format(model, int)(prompts, max_tokens=10, seed=0) assert isinstance(sequence, list) assert len(sequence) == 2 assert isinstance(sequence[0], int) assert isinstance(sequence[1], int) -def test_transformers_integration_float(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_float(model): prompt = "Write a short sentence" - sequence = generate.format(model, float)(prompt, max_tokens=10, rng=rng) + sequence = generate.format(model, float)(prompt, max_tokens=10, seed=0) assert sequence != "" assert isinstance(sequence, float) -def test_transformers_integration_bool(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_bool(model): prompt = "Is this True or False?" - sequence = generate.format(model, bool)(prompt, max_tokens=10, rng=rng) + sequence = generate.format(model, bool)(prompt, max_tokens=10, seed=0) assert sequence != "" assert isinstance(sequence, bool) -def test_transformers_integration_date(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_date(model): prompt = "What day is it today?" - sequence = generate.format(model, datetime.date)(prompt, max_tokens=10, rng=rng) + sequence = generate.format(model, datetime.date)(prompt, max_tokens=10, seed=0) assert sequence != "" assert isinstance(sequence, datetime.date) -def test_transformers_integration_time(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_time(model): prompt = "What time is it?" - sequence = generate.format(model, datetime.time)(prompt, max_tokens=10, rng=rng) + sequence = generate.format(model, datetime.time)(prompt, max_tokens=10, seed=0) assert sequence != "" assert isinstance(sequence, datetime.time) -def test_transformers_integration_datetime(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_integration_datetime(model): prompt = "What time is it?" - sequence = generate.format(model, datetime.datetime)(prompt, max_tokens=20, rng=rng) + sequence = generate.format(model, datetime.datetime)(prompt, max_tokens=20, seed=0) assert sequence != 0 assert isinstance(sequence, datetime.datetime) -def test_transformers_integration_choice(): - rng = torch.Generator() - rng.manual_seed(0) - - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_integration_choice(model): prompt = "Write a short sentence " - sequence = generate.choice(model, ["test", "choice"])(prompt, rng=rng) + sequence = generate.choice(model, ["test", "choice"])(prompt, seed=0) assert sequence == "test" or sequence == "choice" @@ -327,9 +247,7 @@ def test_transformers_integration_with_pad_token(): assert model.tokenizer.pad_token == "" -def test_transformers_json_basic(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_basic(model): prompt = "Output some JSON " class Spam(BaseModel): @@ -338,10 +256,7 @@ class Spam(BaseModel): spam: constr(max_length=10) fuzz: bool - rng = torch.Generator() - rng.manual_seed(0) # make sure that `bar` is not an int - - result = generate.json(model, Spam)(prompt, max_tokens=500, rng=rng) + result = generate.json(model, Spam)(prompt, max_tokens=500, seed=0) assert isinstance(result, BaseModel) assert isinstance(result.foo, int) assert isinstance(result.bar, float) @@ -350,9 +265,7 @@ class Spam(BaseModel): assert len(result.spam) <= 10 -def test_transformers_json_schema(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_schema(model): prompt = "Output some JSON " schema = """{ @@ -366,18 +279,13 @@ def test_transformers_json_schema(): } """ - rng = torch.Generator() - rng.manual_seed(0) # make sure that `bar` is not an int - - result = generate.json(model, schema)(prompt, max_tokens=500, rng=rng) + result = generate.json(model, schema)(prompt, max_tokens=500, seed=0) assert isinstance(result, dict) assert isinstance(result["foo"], int) assert isinstance(result["bar"], str) -def test_transformers_json_batch(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_batch(model): prompts = ["Output some JSON ", "Output more JSON"] class Spam(BaseModel): @@ -386,17 +294,12 @@ class Spam(BaseModel): spam: constr(max_length=10) fuzz: bool - rng = torch.Generator() - rng.manual_seed(0) # make sure that `bar` is not an int - - result = generate.json(model, Spam)(prompts, max_tokens=500, rng=rng) + result = generate.json(model, Spam)(prompts, max_tokens=500, seed=0) assert isinstance(result[0], BaseModel) assert isinstance(result[1], BaseModel) -def test_transformers_json_batch_multiple_samples(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_batch_multiple_samples(model): sampler = multinomial(samples=2) prompts = ["Output some JSON ", "Output more JSON"] @@ -406,11 +309,8 @@ class Spam(BaseModel): spam: constr(max_length=10) fuzz: bool - rng = torch.Generator() - rng.manual_seed(0) # make sure that `bar` is not an int - result = generate.json(model, Spam, sampler=sampler)( - prompts, max_tokens=500, rng=rng + prompts, max_tokens=500, seed=0 ) assert isinstance(result, list) assert len(result) == 2 @@ -420,14 +320,9 @@ class Spam(BaseModel): assert isinstance(result[1][1], BaseModel) -def test_transformers_json_str_enum(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_str_enum(model): prompt = "Output some JSON " - rng = torch.Generator() - rng.manual_seed(0) - class Name(str, Enum): john = "John" marc = "Marc" @@ -437,20 +332,15 @@ class User(BaseModel): user_id: int name: Name - result = generate.json(model, User)(prompt, rng=rng) + result = generate.json(model, User)(prompt, seed=0) assert isinstance(result, BaseModel) assert isinstance(result.user_id, int) assert result.name in ["John", "Marc", "Michel"] -def test_transformers_json_int_enum(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_int_enum(model): prompt = "Output some JSON " - rng = torch.Generator() - rng.manual_seed(0) - class Id(int, Enum): one = 1 two = 2 @@ -458,25 +348,20 @@ class Id(int, Enum): class User(BaseModel): user_id: Id - result = generate.json(model, User)(prompt, rng=rng) + result = generate.json(model, User)(prompt, seed=0) assert isinstance(result, BaseModel) assert isinstance(result.user_id, int) assert result.user_id in [1, 2] -def test_transformers_json_array(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_array(model): prompt = "Output some JSON " class User(BaseModel): user_id: int value: List[float] - rng = torch.Generator() - rng.manual_seed(0) - - result = generate.json(model, User)(prompt, rng=rng) + result = generate.json(model, User)(prompt, seed=0) assert isinstance(result, BaseModel) assert isinstance(result.user_id, int) assert isinstance(result.value, list) @@ -485,19 +370,14 @@ class User(BaseModel): @pytest.mark.xfail(reason="The implementation of `anyOf` is incorrect") -def test_transformers_json_union(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_union(model): prompt = "Output some JSON " class Spam(BaseModel): foo: int bar: Union[constr(max_length=10), float] - rng = torch.Generator() - rng.manual_seed(4) - - result = generate.json(model, Spam)(prompt, max_tokens=100, rng=rng) + result = generate.json(model, Spam)(prompt, max_tokens=100, seed=4) assert isinstance(result, BaseModel) assert ( isinstance(result.bar, int) @@ -506,26 +386,18 @@ class Spam(BaseModel): ) -def test_transformers_json_function(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name) +def test_transformers_json_function(model): prompt = "Output arguments for the function" def function(foo: int, bar: List[int]): return foo + sum(bar) - rng = torch.Generator() - rng.manual_seed(4) - - sequence = generate.json(model, function)(prompt, max_tokens=100, rng=rng) + sequence = generate.json(model, function)(prompt, max_tokens=100, seed=4) assert isinstance(sequence, dict) assert isinstance(function(**sequence), int) -def test_transformers_logits_vocab_size(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") - +def test_transformers_logits_vocab_size(model): # Artificially increase the weights/logits size relative # to the vocabulary model.model.resize_token_embeddings(pad_to_multiple_of=3) @@ -535,16 +407,11 @@ def test_transformers_logits_vocab_size(): generator = generate.choice(model, ["True", "False"]) - rng = torch.Generator() - rng.manual_seed(101) - - sequence = generator("blah", rng=rng) + sequence = generator("blah", seed=101) assert sequence == "False" -def test_transformers_json_custom_ws(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - model = models.transformers(model_name, device="cpu") +def test_transformers_json_custom_ws(model): prompt = "Output some JSON with newlines" # try to force model to use newlines schema = """{ @@ -558,12 +425,9 @@ def test_transformers_json_custom_ws(): } """ - rng = torch.Generator() - rng.manual_seed(0) - generator = generate.json(model, schema, whitespace_pattern=r"[ ]?") generator.format_sequence = lambda x: x # patch to return raw text - assert "\n" not in generator(prompt, max_tokens=500, rng=rng) + assert "\n" not in generator(prompt, max_tokens=500, seed=0) def test_transformers_reduced_vocabulary_caching(): @@ -582,11 +446,8 @@ def test_transformers_reduced_vocabulary_caching(): assert vocab2 is vocab -def test_custom_sampler(): - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" - - model = models.transformers(model_name) - +@pytest.mark.skip(reason="Custom Sampler Disabled in Transformers Integration") +def test_custom_sampler(model): seen = False target_token_ids = model.tokenizer.encode(["c"])[0] @@ -623,14 +484,11 @@ def __call__( def test_transformers_use_existing_model_and_tokenizer(): from transformers import AutoModelForCausalLM, AutoTokenizer - rng = torch.Generator() - rng.manual_seed(10000) - model_name = "hf-internal-testing/tiny-random-GPTJForCausalLM" hf_tokenizer = AutoTokenizer.from_pretrained(model_name) hf_model = AutoModelForCausalLM.from_pretrained(model_name) model = Transformers(hf_model, hf_tokenizer) - sequence = generate.text(model)("Write a short sentence ", rng=rng) + sequence = generate.text(model)("Write a short sentence ", seed=10000) assert isinstance(sequence, str) @@ -652,22 +510,30 @@ def test_RegexGuide_caching(temp_cache_dir): assert create_states_mapping.__memory__ is cache model = models.transformers( - "hf-internal-testing/tiny-random-XLMRobertaXLForCausalLM" + "hf-internal-testing/tiny-random-XLMRobertaXLForCausalLM", device="cpu" ) generator = generate.regex(model, regex, sampler=greedy()) assert cache.stats() == (0, 1) - model_2 = models.transformers("hf-internal-testing/tiny-random-GPTJForCausalLM") + model_2 = models.transformers( + "hf-internal-testing/tiny-random-GPTJForCausalLM", device="cpu" + ) generator_2 = generate.regex(model_2, regex, sampler=greedy()) assert cache.stats() == (0, 2) # These two different models and tokenizers should not have the same state # mapping results - assert generator.fsm.states_to_token_maps != generator_2.fsm.states_to_token_maps + assert ( + generator.logits_processor.fsm.states_to_token_maps + != generator_2.logits_processor.fsm.states_to_token_maps + ) generator_3 = generate.regex(model_2, regex, sampler=greedy()) assert cache.stats() == (1, 2) - assert generator_2.fsm.states_to_token_maps == generator_3.fsm.states_to_token_maps + assert ( + generator_2.logits_processor.fsm.states_to_token_maps + == generator_3.logits_processor.fsm.states_to_token_maps + ) # Just for fun... structured = generator(prompt, max_tokens=30) diff --git a/tests/models/test_transformers.py b/tests/models/test_transformers.py index f4596a2df..1404d287a 100644 --- a/tests/models/test_transformers.py +++ b/tests/models/test_transformers.py @@ -63,20 +63,17 @@ def test_llama_tokenizer(): def test_model(): - with pytest.raises(ValueError, match="When passing device_map as a string"): - transformers(TEST_MODEL, device="non_existent") - model = transformers(TEST_MODEL, device="cpu") assert isinstance(model.tokenizer, TransformerTokenizer) - assert model.device.type == "cpu" + assert model.model.device.type == "cpu" model = transformers(TEST_MODEL, model_kwargs={"device_map": "cpu"}) assert isinstance(model.tokenizer, TransformerTokenizer) - assert model.device.type == "cpu" + assert model.model.device.type == "cpu" model = transformers(TEST_MODEL, device="cpu", model_kwargs={"device_map": "cuda"}) assert isinstance(model.tokenizer, TransformerTokenizer) - assert model.device.type == "cpu" + assert model.model.device.type == "cpu" input_ids = torch.tensor([[0, 1, 2]]) logits, kv_cache = model(input_ids, torch.ones_like(input_ids)) From 517ff5321db5f6d96bba293ef3d0130148be7034 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 21 Jun 2024 09:59:03 -0500 Subject: [PATCH 012/505] Use LogitsProcessors for models.transformers -> outlines.generate.* --- outlines/models/transformers.py | 4 ++++ outlines/processors/base_logits_processor.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index 4c475617a..70f19a0eb 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -182,6 +182,10 @@ def forward( return output.logits, output.past_key_values + @property + def device(self): + return self.model.device + def __call__( self, input_ids: "torch.LongTensor", diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index f829844a6..1d141a2f0 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -42,7 +42,7 @@ def process_logits( input_ids and logits are always 2D tensors for handling a batch of sequences. - input_ids -> List[List[tokens]] - - logits.shape[0] -> 2D_Tensor[logits] + - logits -> 2D_Tensor[logit floats] Important to keep in mind when designing universal logits processors - logits processors are only used once and never re-applied for a new sequence generator From 5a7f08205f9d9bff997db01401c731afe4b25501 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 15 Jul 2024 03:48:09 -0500 Subject: [PATCH 013/505] enable generate.fsm with llamacpp by using outlines.processors --- outlines/fsm/guide.py | 19 ++++++++++++------- outlines/generate/fsm.py | 19 ++++++++++++++++++- outlines/generate/regex.py | 13 +------------ outlines/generate/text.py | 6 +----- outlines/models/llamacpp.py | 4 ++++ outlines/models/transformers.py | 4 ---- tests/generate/test_generate.py | 11 +++++++++++ 7 files changed, 47 insertions(+), 29 deletions(-) diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index aa694232e..2e4415148 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -184,13 +184,7 @@ def __init__(self, regex_string: str, tokenizer: "Tokenizer"): ) = create_states_mapping(regex_string, tokenizer) self.eos_token_id = tokenizer.eos_token_id self.final_states = fsm_finals | {-1} - - # cache returned masks token masks - # this increases performance of the mask substantially - self.states_to_token_mask = { - state: torch.tensor(list(next_tokens_to_end_states.keys())) - for state, next_tokens_to_end_states in self.states_to_token_maps.items() - } + self._cache_state_to_token_tensor() def get_next_instruction(self, state: int) -> Instruction: """Return the next instruction for guided generation. @@ -285,8 +279,19 @@ def create_states_mapping_from_interegular_fsm( from_interegular_instance.empty_token_ids, ) = create_states_mapping_from_interegular_fsm(interegular_fsm) from_interegular_instance.eos_token_id = tokenizer.eos_token_id + from_interegular_instance._cache_state_to_token_tensor() return from_interegular_instance + def _cache_state_to_token_tensor(self): + """ + cache state -> token int tensor + this increases performance of mask construction substantially + """ + self.states_to_token_mask = { + state: torch.tensor(list(next_tokens_to_end_states.keys())) + for state, next_tokens_to_end_states in self.states_to_token_maps.items() + } + def is_final_state(self, state: int) -> bool: """Determine whether the current state of the guide is a final state.""" return state in self.final_states diff --git a/outlines/generate/fsm.py b/outlines/generate/fsm.py index 80db350f0..832a154bd 100644 --- a/outlines/generate/fsm.py +++ b/outlines/generate/fsm.py @@ -1,10 +1,14 @@ +from functools import singledispatch + import interegular from outlines.fsm.guide import RegexGuide -from outlines.generate.api import SequenceGenerator +from outlines.generate.api import SequenceGenerator, SequenceGeneratorAdapter +from outlines.models import MLXLM, LlamaCpp, Transformers from outlines.samplers import Sampler, multinomial +@singledispatch def fsm( model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() ) -> SequenceGenerator: @@ -12,3 +16,16 @@ def fsm( device = model.device generator = SequenceGenerator(fsm, model, sampler, device) return generator + + +@fsm.register(MLXLM) +@fsm.register(Transformers) +@fsm.register(LlamaCpp) +def fsm_unified( + model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() +) -> SequenceGeneratorAdapter: + from outlines.processors import FSMLogitsProcessor + + fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) + logits_processor = FSMLogitsProcessor(tokenizer=model.tokenizer, fsm=fsm) + return SequenceGeneratorAdapter(model, logits_processor, sampler) diff --git a/outlines/generate/regex.py b/outlines/generate/regex.py index cdf64a21f..52b8c7dad 100644 --- a/outlines/generate/regex.py +++ b/outlines/generate/regex.py @@ -41,6 +41,7 @@ def regex(model, regex_str: str, sampler: Sampler = multinomial()): @regex.register(MLXLM) @regex.register(Transformers) +@regex.register(LlamaCpp) def regex_unified( model, regex_str: str, @@ -52,18 +53,6 @@ def regex_unified( return SequenceGeneratorAdapter(model, logits_processor, sampler) -@regex.register(LlamaCpp) -def regex_llamacpp( - model: LlamaCpp, - regex_str: str, - sampler: Sampler = multinomial(), -): - from outlines.integrations.llamacpp import RegexLogitsProcessor - - logits_processor = RegexLogitsProcessor(regex_str, llm=model.model) - return SequenceGeneratorAdapter(model, logits_processor, sampler) - - @regex.register(VLLM) def regex_vllm( model: VLLM, diff --git a/outlines/generate/text.py b/outlines/generate/text.py index b8feb7659..6da187e0b 100644 --- a/outlines/generate/text.py +++ b/outlines/generate/text.py @@ -38,6 +38,7 @@ def text(model, sampler: Sampler = multinomial()) -> SequenceGenerator: @text.register(MLXLM) @text.register(Transformers) +@text.register(LlamaCpp) def text_unified(model, sampler: Sampler = multinomial()): return SequenceGeneratorAdapter(model, None, sampler) @@ -47,11 +48,6 @@ def text_vllm(model: VLLM, sampler: Sampler = multinomial()): return SequenceGeneratorAdapter(model, None, sampler) -@text.register(LlamaCpp) -def text_llamacpp(model: LlamaCpp, sampler: Sampler = multinomial()): - return SequenceGeneratorAdapter(model, None, sampler) - - @text.register(OpenAI) def text_openai(model: OpenAI, sampler: Sampler = multinomial()) -> OpenAI: if not isinstance(sampler, multinomial): diff --git a/outlines/models/llamacpp.py b/outlines/models/llamacpp.py index 7e223a9fa..904b193c4 100644 --- a/outlines/models/llamacpp.py +++ b/outlines/models/llamacpp.py @@ -141,6 +141,10 @@ class LlamaCpp: def __init__(self, model: "Llama"): self.model = model + @property + def tokenizer(self): + return LlamaCppTokenizer(self.model) + def prepare_generation_parameters( self, generation_parameters: GenerationParameters, diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index 70f19a0eb..4c475617a 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -182,10 +182,6 @@ def forward( return output.logits, output.past_key_values - @property - def device(self): - return self.model.device - def __call__( self, input_ids: "torch.LongTensor", diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index c6d023929..9432c0e4e 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -101,6 +101,17 @@ def test_generate_text_stream(request, model_fixture): assert isinstance(token, str) +@pytest.mark.parametrize("pattern", REGEX_PATTERNS) +@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +def test_generate_fsm(request, model_fixture, pattern): + import interegular + + model = request.getfixturevalue(model_fixture) + generator = generate.fsm(model, interegular.parse_pattern(pattern).to_fsm()) + res = generator("test") + assert re.fullmatch(pattern, res) is not None, res + + @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) def test_generate_regex(request, model_fixture, pattern): From 08a3e540551f2ae2f36a89d137553e155a00ef09 Mon Sep 17 00:00:00 2001 From: Alonso Silva Allende Date: Tue, 16 Jul 2024 12:04:45 +0200 Subject: [PATCH 014/505] Add QA with Citations example to the Cookbook (#1042) As requested by @rlouf this PR adds a question answering with citations example to the Cookbook using llama-cpp-python. --- docs/cookbook/qa-with-citations.md | 228 +++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 229 insertions(+) create mode 100644 docs/cookbook/qa-with-citations.md diff --git a/docs/cookbook/qa-with-citations.md b/docs/cookbook/qa-with-citations.md new file mode 100644 index 000000000..b8e034a29 --- /dev/null +++ b/docs/cookbook/qa-with-citations.md @@ -0,0 +1,228 @@ +# Generate Synthetic Data and Q&A with Citations + +This tutorial is adapted from the [instructor-ollama notebook](https://github.com/alonsosilvaallende/Hermes-Function-Calling/blob/main/examples/instructor_ollama.ipynb). We start with a simple example to generate synthetic data and then we approach the problem of question answering by providing citations. We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](library). + +We pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): + +```python +!wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +``` + +We initialize the model: + +```python +from llama_cpp import Llama +from outlines import generate, models + +llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + n_gpu_layers=-1, + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + flash_attn=True, + n_ctx=8192, + verbose=False +) +model = models.LlamaCpp(llm) +``` + +## Generate Synthetic Data + +We first need to define our Pydantic class for a user: + +```python +from pydantic import BaseModel, Field + +class UserDetail(BaseModel): + id: int = Field(..., description="Unique identifier") # so the model keeps track of the number of users + first_name: str + last_name: str + age: int +``` + +We then define a Pydantic class for a list of users: + +```python +from typing import List + +class Users(BaseModel): + users: List[UserDetail] +``` + +We can use a `generate.json` by passing this Pydantic class we just defined, and call the generator: + +```python +generator = generate.json(model, Users) +response = generator("Create 5 fake users", max_tokens=1024, temperature=0, seed=42) +print(response.users) +# [UserDetail(id=1, first_name='John', last_name='Doe', age=25), +# UserDetail(id=2, first_name='Jane', last_name='Doe', age=30), +# UserDetail(id=3, first_name='Bob', last_name='Smith', age=40), +# UserDetail(id=4, first_name='Alice', last_name='Smith', age=35), +# UserDetail(id=5, first_name='John', last_name='Smith', age=20)] +``` + +```python +for user in response.users: + print(user.first_name) + print(user.last_name) + print(user.age) + print(#####) +# John +# Doe +# 25 +# ##### +# Jane +# Doe +# 30 +# ##### +# Bob +# Smith +# 40 +# ##### +# Alice +# Smith +# 35 +# ##### +# John +# Smith +# 20 +# ##### +``` + +## QA with Citations + +We first need to define our Pydantic class for QA with citations: + +```python +from typing import List +from pydantic import BaseModel + +class QuestionAnswer(BaseModel): + question: str + answer: str + citations: List[str] + +schema = QuestionAnswer.model_json_schema() +``` + +We then need to adapt our prompt to the [Hermes prompt format for JSON schema](https://github.com/NousResearch/Hermes-Function-Calling?tab=readme-ov-file#prompt-format-for-json-mode--structured-outputs): + +```python +def generate_hermes_prompt(question, context, schema=schema): + return ( + "<|im_start|>system\n" + "You are a world class AI model who answers questions in JSON with correct and exact citations " + "extracted from the `Context`. " + f"Here's the json schema you must adhere to:\n\n{schema}\n<|im_end|>\n" + "<|im_start|>user\n" + + "`Context`: " + + context + + "\n`Question`: " + + question + "<|im_end|>" + + "\n<|im_start|>assistant\n" + "" + ) +``` + +We can use `generate.json` by passing the Pydantic class we previously defined, and call the generator with Hermes prompt: + +```python +question = "What did the author do during college?" +context = """ +My name is Jason Liu, and I grew up in Toronto Canada but I was born in China. +I went to an arts high school but in university I studied Computational Mathematics and physics. +As part of coop I worked at many companies including Stitchfix, Facebook. +I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years. +""" +generator = generate.json(model, QuestionAnswer) +prompt = generate_hermes_prompt(question, context) +response = generator(prompt, max_tokens=1024, temperature=0, seed=42) +print(response) +# QuestionAnswer(question='What did Jason Liu do during college?', answer='During college, Jason Liu studied Computational Mathematics and Physics. He also worked at companies such as Stitchfix and Facebook, and started the Data Science club at the University of Waterloo, serving as its president for two years.', citations=['I went to an arts high school but in university I studied Computational Mathematics and physics.', 'As part of coop I worked at many companies including Stitchfix, Facebook.', 'I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years.']) +``` + +We can do the same for a list of question-context pairs: + +```python +question1 = "Where was John born?" +context1 = """ +John Doe is a software engineer who was born in New York, USA. +He studied Computer Science at the Massachusetts Institute of Technology. +During his studies, he interned at Google and Microsoft. +He also founded the Artificial Intelligence club at his university and served as its president for three years. +""" + +question2 = "What did Emily study in university?" +context2 = """ +Emily Smith is a data scientist from London, England. +She attended the University of Cambridge where she studied Statistics and Machine Learning. +She interned at IBM and Amazon during her summer breaks. +Emily was also the head of the Women in Tech society at her university. +""" + +question3 = "Which companies did Robert intern at?" +context3 = """ +Robert Johnson, originally from Sydney, Australia, is a renowned cybersecurity expert. +He studied Information Systems at the University of Melbourne. +Robert interned at several cybersecurity firms including NortonLifeLock and McAfee. +He was also the leader of the Cybersecurity club at his university. +""" + +question4 = "What club did Alice start at her university?" +context4 = """ +Alice Williams, a native of Dublin, Ireland, is a successful web developer. +She studied Software Engineering at Trinity College Dublin. +Alice interned at several tech companies including Shopify and Squarespace. +She started the Web Development club at her university and was its president for two years. +""" + +question5 = "What did Michael study in high school?" +context5 = """ +Michael Brown is a game developer from Tokyo, Japan. +He attended a specialized high school where he studied Game Design. +He later attended the University of Tokyo where he studied Computer Science. +Michael interned at Sony and Nintendo during his university years. +He also started the Game Developers club at his university. +""" + +for question, context in [ + (question1, context1), + (question2, context2), + (question3, context3), + (question4, context4), + (question5, context5), +]: + final_prompt = my_final_prompt(question, context) + generator = generate.json(model, QuestionAnswer) + response = generator(final_prompt, max_tokens=1024, temperature=0, seed=42) + display(question) + display(response.answer) + display(response.citations) + print("\n\n") + +# 'Where was John born?' +# 'John Doe was born in New York, USA.' +# ['John Doe is a software engineer who was born in New York, USA.'] +# +# +# 'What did Emily study in university?' +# 'Emily studied Statistics and Machine Learning in university.' +# ['She attended the University of Cambridge where she studied Statistics and Machine Learning.'] +# +# +# 'Which companies did Robert intern at?' +# 'Robert interned at NortonLifeLock and McAfee.' +# ['Robert Johnson, originally from Sydney, Australia, is a renowned cybersecurity expert. He interned at several cybersecurity firms including NortonLifeLock and McAfee.'] +# +# +# 'What club did Alice start at her university?' +# 'Alice started the Web Development club at her university.' +# ['Alice Williams, a native of Dublin, Ireland, is a successful web developer. She started the Web Development club at her university and was its president for two years.'] +# +# +# 'What did Michael study in high school?' +# 'Michael studied Game Design in high school.' +# ['Michael Brown is a game developer from Tokyo, Japan. He attended a specialized high school where he studied Game Design.'] +``` diff --git a/mkdocs.yml b/mkdocs.yml index 12e79fd75..bc06c4993 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,6 +106,7 @@ nav: - Summarize a document: cookbook/chain_of_density.md - Playing chess: cookbook/models_playing_chess.md - Perspective-taking prompting: cookbook/simtom.md + - Question-answering with citations: cookbook/qa-with-citations.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From e684af2d5dacf9b0d06c2d41054f40488c30da6d Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 15 Jul 2024 19:08:41 -0500 Subject: [PATCH 015/505] Fix mamba integration by making it a variant of outlines.models.transformers --- docs/reference/models/mamba.md | 7 --- docs/reference/models/transformers.md | 67 +++++++++++++++++++++++++++ outlines/models/__init__.py | 5 +- outlines/models/mamba.py | 61 ------------------------ outlines/models/transformers.py | 60 ++++++++++++++++++++---- tests/generate/test_generate.py | 38 ++++++++++++++- 6 files changed, 157 insertions(+), 81 deletions(-) delete mode 100644 docs/reference/models/mamba.md delete mode 100644 outlines/models/mamba.py diff --git a/docs/reference/models/mamba.md b/docs/reference/models/mamba.md deleted file mode 100644 index ac6db3682..000000000 --- a/docs/reference/models/mamba.md +++ /dev/null @@ -1,7 +0,0 @@ -# Mamba - -```bash -pip install mamba_ssm transformers torch -``` - -*Coming soon* diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index 7c1febd02..80b70845f 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -82,3 +82,70 @@ print(output) ``` [transformers]: https://github.com/huggingface/transformers + + +# Alternative Model Classes + +`outlines.models.transformers` defaults to `transformers.AutoModelForCausalLM`, which is the appropriate class for most standard large language models, including Llama 3, Mistral, Phi-3, etc. + +However other variants with unique behavior can be used as well by passing the appropriate class. + +### Mamba + +[Mamba](https://github.com/state-spaces/mamba) is a transformers alternative which employs memory efficient, linear-time decoding. + +To use Mamba with outlines you must first install the necessary requirements: +``` +pip install causal-conv1d>=1.2.0 mamba-ssm torch transformers +``` + +Then you can either create an Mamba-2 Outlines model via +```python +import outlines + +model = outlines.models.mamba("state-spaces/mamba-2.8b-hf") +``` + +or explicitly with +```python +import outlines +from transformers import MambaForCausalLM + +model = outlines.models.transformers( + "state-spaces/mamba-2.8b-hf", + model_class=MambaForCausalLM +) +``` + +Further Reading: +- https://huggingface.co/docs/transformers/en/model_doc/mamba + +### Encoder-Decoder Models + +You can use encoder-decoder (seq2seq) models like T5 and BART with Outlines. + +Be cautious with model selection though, some models such as `t5-base` don't include certain characters (`{`) and you may get an error when trying to perform structured generation. + +T5 Example: +```python +import outlines +from transformers import AutoModelForSeq2SeqLM + +model_pile_t5 = models.transformers( + model_name="EleutherAI/pile-t5-large", + model_class=AutoModelForSeq2SeqLM, +) +``` + +Bart Example: +```python +model_bart = models.transformers( + model_name="facebook/bart-large", + model_class=AutoModelForSeq2SeqLM, +) +``` + + +### Multi-Modal Models + +/Coming soon/ diff --git a/outlines/models/__init__.py b/outlines/models/__init__.py index fde913e2c..c161215d1 100644 --- a/outlines/models/__init__.py +++ b/outlines/models/__init__.py @@ -9,10 +9,9 @@ from .exllamav2 import ExLlamaV2Model, exl2 from .llamacpp import LlamaCpp, llamacpp -from .mamba import Mamba, mamba from .mlxlm import MLXLM, mlxlm from .openai import OpenAI, azure_openai, openai -from .transformers import Transformers, TransformerTokenizer, transformers +from .transformers import Transformers, TransformerTokenizer, mamba, transformers from .vllm import VLLM, vllm -LogitsGenerator = Union[Transformers, LlamaCpp, ExLlamaV2Model, Mamba, MLXLM, VLLM] +LogitsGenerator = Union[Transformers, LlamaCpp, ExLlamaV2Model, MLXLM, VLLM] diff --git a/outlines/models/mamba.py b/outlines/models/mamba.py deleted file mode 100644 index d3dabf669..000000000 --- a/outlines/models/mamba.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -from .transformers import TransformerTokenizer - -if TYPE_CHECKING: - import torch - from mamba_ssm.models.mixer_seq_simple import MambaLMHeadModel - from transformers import PreTrainedTokenizer - - -TOKENIZER_MODEL = "EleutherAI/gpt-neox-20b" - - -class Mamba: - """Represent a `mamba` model.""" - - def __init__( - self, model: "MambaLMHeadModel", tokenizer: "PreTrainedTokenizer", device - ): - self.device = device - self.model = model - self.tokenizer = TransformerTokenizer(tokenizer) - - def forward(self, input_ids: "torch.LongTensor", *_): - """Compute a forward pass through the mamba model.""" - - output = self.model(input_ids) - next_token_logits = output.logits[..., -1, :] - return next_token_logits, None - - def __call__(self, input_ids: "torch.LongTensor", *_) -> "torch.FloatTensor": - return self.forward(input_ids) - - -def mamba( - model_name: str, - device: Optional[str] = None, - model_kwargs: dict = {}, - tokenizer_kwargs: dict = {}, -): - try: - import torch - from mamba_ssm import MambaLMHeadModel - from transformers import AutoTokenizer - except ImportError: - raise ImportError( - "The `mamba_ssm`, `torch` and `transformer` libraries needs to be installed in order to use Mamba people." - ) - - if not torch.cuda.is_available(): - raise NotImplementedError("Mamba models can only run on GPU.") - else: - if device is None: - device = "cuda" - - model = MambaLMHeadModel.from_pretrained(model_name, device=device) - - tokenizer_kwargs.setdefault("padding_side", "left") - tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_MODEL, **tokenizer_kwargs) - - return Mamba(model, tokenizer, device) diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index 4c475617a..1d3be511a 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -1,4 +1,5 @@ import dataclasses +import inspect from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union from datasets.fingerprint import Hasher @@ -226,10 +227,16 @@ def generate( input_ids, attention_mask = self.tokenizer.encode([prompts]) else: input_ids, attention_mask = self.tokenizer.encode(prompts) + inputs = { "input_ids": input_ids.to(self.model.device), "attention_mask": attention_mask.to(self.model.device), } + if ( + "attention_mask" + not in inspect.signature(self.model.forward).parameters.keys() + ): + del inputs["attention_mask"] generation_kwargs = self._get_generation_kwargs( prompts, @@ -267,6 +274,11 @@ def stream( "input_ids": input_ids.to(self.model.device), "attention_mask": attention_mask.to(self.model.device), } + if ( + "attention_mask" + not in inspect.signature(self.model.forward).parameters.keys() + ): + del inputs["attention_mask"] generation_kwargs = self._get_generation_kwargs( prompts, @@ -336,7 +348,7 @@ def _generate_output_seq( ): input_ids = inputs["input_ids"] output_ids = self.model.generate( - generation_config=generation_config, **inputs, **generation_kwargs + **inputs, generation_config=generation_config, **generation_kwargs ) # encoder-decoder returns output_ids only, decoder-only returns full seq ids @@ -376,6 +388,8 @@ def transformers( device: Optional[str] = None, model_kwargs: dict = {}, tokenizer_kwargs: dict = {}, + model_class=None, + tokenizer_class=None, ): """Instantiate a model from the `transformers` library and its tokenizer. @@ -398,19 +412,47 @@ def transformers( A `TransformersModel` model instance. """ - try: - from transformers import AutoModelForCausalLM, AutoTokenizer - except ImportError: - raise ImportError( - "The `transformers` library needs to be installed in order to use `transformers` models." - ) + if model_class is None or tokenizer_class is None: + try: + from transformers import AutoModelForCausalLM, AutoTokenizer + except ImportError: + raise ImportError( + "The `transformers` library needs to be installed in order to use `transformers` models." + ) + if model_class is None: + model_class = AutoModelForCausalLM + if tokenizer_class is None: + tokenizer_class = AutoTokenizer if device is not None: model_kwargs["device_map"] = device - model = AutoModelForCausalLM.from_pretrained(model_name, **model_kwargs) + model = model_class.from_pretrained(model_name, **model_kwargs) tokenizer_kwargs.setdefault("padding_side", "left") - tokenizer = AutoTokenizer.from_pretrained(model_name, **tokenizer_kwargs) + tokenizer = tokenizer_class.from_pretrained(model_name, **tokenizer_kwargs) return Transformers(model, tokenizer) + + +def mamba( + model_name: str, + device: Optional[str] = None, + model_kwargs: dict = {}, + tokenizer_kwargs: dict = {}, +): + try: + from transformers import MambaForCausalLM + + except ImportError: + raise ImportError( + "The `mamba_ssm`, `torch` and `transformer` libraries needs to be installed in order to use Mamba." + ) + + return transformers( + model_name=model_name, + device=device, + model_kwargs=model_kwargs, + tokenizer_kwargs=tokenizer_kwargs, + model_class=MambaForCausalLM, + ) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index 9432c0e4e..06d311f5b 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -36,17 +36,53 @@ def model_transformers_opt125m(tmp_path_factory): return models.transformers("facebook/opt-125m", device="cpu") +@pytest.fixture(scope="session") +def model_mamba(tmp_path_factory): + return models.mamba(model_name="state-spaces/mamba-130m-hf", device="cpu") + + +@pytest.fixture(scope="session") +def model_bart(tmp_path_factory): + from transformers import AutoModelForSeq2SeqLM + + return models.transformers( + "facebook/bart-base", device="cpu", model_class=AutoModelForSeq2SeqLM + ) + + +# TODO: exllamav2 failing in main, address in https://github.com/outlines-dev/outlines/issues/808 +# TODO: t5 tokenizer doesn't work with streaming +""" +@pytest.fixture(scope="session") +def model_exllamav2(tmp_path_factory): + return models.exllamav2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + device="cpu" + ) + +@pytest.fixture(scope="session") +def model_t5(tmp_path_factory): + from transformers import AutoModelForSeq2SeqLM + + return models.transformers( + "EleutherAI/pile-t5-base", device="cpu", model_class=AutoModelForSeq2SeqLM + ) +""" + + ALL_MODEL_FIXTURES = ( "model_llamacpp", "model_mlxlm", "model_mlxlm_phi3", "model_transformers_random", "model_transformers_opt125m", + "model_mamba", + "model_bart", ) NOT_IMPLEMENTED = { - "stream": ["model_vllm"], + "stream": [], "batch": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], "beam_search": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], "multiple_samples": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], From cd8c79aa6ee1ee4f731ea08865f43d4a50029310 Mon Sep 17 00:00:00 2001 From: Daniel van Strien Date: Tue, 16 Jul 2024 13:18:35 +0100 Subject: [PATCH 016/505] fix PyPI name for autoawq --- docs/reference/models/vllm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/models/vllm.md b/docs/reference/models/vllm.md index b5221e582..735ab4cc5 100644 --- a/docs/reference/models/vllm.md +++ b/docs/reference/models/vllm.md @@ -80,7 +80,7 @@ model = models.vllm("https://huggingface.co/squeeze-ai-lab/sq-llama-30b-w4-s5", !!! Warning "Dependencies" - To use AWQ model you need to install the auto-awq library `pip install auto-awq`. + To use AWQ model you need to install the autoawq library `pip install autoawq`. To use GPTQ models you need to install the autoGTPQ and optimum libraries `pip install auto-gptq optimum`. From d64bfc29a1518cd3a84b9ded6d58adfddcf59e3f Mon Sep 17 00:00:00 2001 From: JerryKwan Date: Wed, 17 Jul 2024 21:36:33 +0800 Subject: [PATCH 017/505] add fallback tokenizer (#1046) add fallback tonenizer if tiktoken can not get encoding from model name support llm services which provide openai compatibility api such as ollama --- outlines/models/openai.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/outlines/models/openai.py b/outlines/models/openai.py index 8625f4d3d..692e64ac5 100644 --- a/outlines/models/openai.py +++ b/outlines/models/openai.py @@ -1,5 +1,6 @@ """Integration with OpenAI's API.""" import functools +import warnings from dataclasses import asdict, dataclass, field, replace from itertools import zip_longest from typing import Callable, Dict, List, Optional, Set, Tuple, Union @@ -439,7 +440,14 @@ def openai_model( config = OpenAIConfig(model=model_name) client = AsyncOpenAI(**openai_client_params) - tokenizer = tiktoken.encoding_for_model(model_name) + + try: + tokenizer = tiktoken.encoding_for_model(model_name) + except KeyError: + warnings.warn( + f"Could not find a tokenizer for model {model_name}. Using default cl100k_base." + ) + tokenizer = tiktoken.get_encoding("cl100k_base") return OpenAI(client, config, tokenizer) @@ -464,6 +472,13 @@ def azure_openai( config = OpenAIConfig(model=deployment_name) client = AsyncAzureOpenAI(**azure_openai_client_params) - tokenizer = tiktoken.encoding_for_model(model_name or deployment_name) + + try: + tokenizer = tiktoken.encoding_for_model(model_name or deployment_name) + except KeyError: + warnings.warn( + f"Could not find a tokenizer for model {model_name or deployment_name}. Using default cl100k_base." + ) + tokenizer = tiktoken.get_encoding("cl100k_base") return OpenAI(client, config, tokenizer) From 9ce0df3cfea68c2b06edf4847a08b4153cd7a2ee Mon Sep 17 00:00:00 2001 From: Michael Louis Date: Wed, 17 Jul 2024 10:38:34 -0400 Subject: [PATCH 018/505] Update cerebrium instructions --- docs/cookbook/deploy-using-cerebrium.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/cookbook/deploy-using-cerebrium.md b/docs/cookbook/deploy-using-cerebrium.md index 79d703d32..b7609424c 100644 --- a/docs/cookbook/deploy-using-cerebrium.md +++ b/docs/cookbook/deploy-using-cerebrium.md @@ -24,6 +24,9 @@ cerebrium init outlines-project You set up your environment and hardware in the cerebrium.toml file that was created using the init function above. ```toml +[cerebrium.deployment] +docker_base_image_url = "nvidia/cuda:12.1.1-runtime-ubuntu22.04" + [cerebrium.hardware] cpu = 2 memory = 14.0 From 1bf23be48f3df1f02b2dd879ecd3cdceba8e4f86 Mon Sep 17 00:00:00 2001 From: Alonso Silva Date: Thu, 18 Jul 2024 10:29:36 +0200 Subject: [PATCH 019/505] Add Knowledge Graph Extraction example --- .../images/knowledge-graph-extraction.png | Bin 0 -> 33127 bytes docs/cookbook/knowledge_graph_extraction.md | 137 ++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 138 insertions(+) create mode 100644 docs/cookbook/images/knowledge-graph-extraction.png create mode 100644 docs/cookbook/knowledge_graph_extraction.md diff --git a/docs/cookbook/images/knowledge-graph-extraction.png b/docs/cookbook/images/knowledge-graph-extraction.png new file mode 100644 index 0000000000000000000000000000000000000000..a24b1c61a9c371e4fdad95dd5ec9fc31cd71c9cd GIT binary patch literal 33127 zcmbTebzD{3w?4dRC8Z@*x)CI06Dr-^p$G^fC6ZDW9Ri!~1}W*3kOm3q?ndd(cdm2p z{oU_-|9RsNKj-M)d#)K{j5)^hJYxl@D$C$uQ({99geNB}sSZJ?s^E|FEllt)U&-rj z!GBO3)n%SSMSav8;4kQ}o+v$mppr10%NH2n@3-w`^&BCHj1m5WBB#!<3qd}Ya*|Ip zU5)?F;1~{TU5j5=*^)b5F|FdsE0Jgn??;gCj*#5p&W($W{(7bPuGjv;)_Akgs?I#O z&bM-_J04@48$;n!DJu7^+4B!kX_YMTx1yO`4RZOL54YCDcUs~W9<|^;eE8^>hgk92 z5|2}#UwDs4pIiU?zB7+LtL(dXz_g%Y1IIIO_wd(hrKve^v^BQT9zykQYm{?udbKxU=x^1WvdgR+ zbH$s92>mKsC6^f|ayK_OwL0g&xt6%AQeC38fK{_QR)1re!(B*RRZ(I+vkj(f5*c8ZiPkld3zjKiUrCQc_Z4 z)$4W|E^-xOxJbJnj?gTJrFJz-%UlFD zq)JT00?|y>xo~?P{+2;TM>BDuR<+H|nJBv8QSeGCdhfMOCi~0exl-;2aW~uP+YaZd@x5J^9rE4jf;Qm#3m&z9mpyVLBO;h?=AS<8+^a>Lq^2;&sUjW zdlmA;Op14gbP^F8gv@G)kQ$pQ0SB8X{tTJ0?2VzEAq^E##xTf-_Q40#f$u@&+=J&w zTdWTrJSpfh3)t~b;5847&BnfcJ6z_2`Kwn!8@-$cmHr|(9D9=*Wfr~Z!j7w|9icQ} z_5(QJ>6EQod~F>aL$yv@%Y=@IK@w6@sYqs(Xqi+2gDTss+lvtdasYG8UqC1D-L+=rbCmrI~Y8)Kc(BwyS z>?X9U{T}Yab}G98lbdrLw@gYClIz7F?m$k3CMVaRgsNG01;cl?8!Is;**YekQC=5Q zbKUgN{04A5*Vw8#uLR@wfB9?e7d|4?%@8f*^7j2ol+Ht~H4E3*c4zx5rrb{|UbOhC zy?IkmEr16h$iWs0x*V)3@nDp@AH8jQeANEtwIE_v{#iOEF0N2o?@I?abO?a~ zkKdZ1T%UZ0+ZXKg=w7*MikRmz;Vwf**gf*0!f2O2dvuBQUgXjl5CRMSc1yxk!|qR< z#okf>D#Hb*WQ$+155jzyr@p`Vq{}S-LkgF<&3d$eFhLN_=faxnh5)kliD8-#Y98q# zF1GD8D7)`Ah%Xl7p?o$?xhK5l*lz(Y#z6x1U zg-P%EFOB5utPrN}Ep-HG^?7=DfZeng1iKkO-j^iOR&>4Amjr$z9Y)KkUA6r?!w-R~ z?+ijl$?HoHAeRZ#!M8M?K1B_o&Kfm}YaB7B%y*>xOCGnJE1@$3FhRnfZ*5{iv^ z^qoypIV8+iHrt>>(*$5v@tp_#;woFCg^~Q}V5~t9tAY$BokC-ZVnyEz7}dL~(LRV5 zapAf;NV!4sL-tr;R)&A(Zmz4UU3Eo`TJi%9e4A!!og!r<`uh@v=>_Pti%t77)VjL5 z-kr?2^w2{C$cJCB=AU}3k=m`Kf$1#n{rU5TLFM^Rn-5dyRe1EakEWfg@lYg&o}psQ z(}A0SlwRrV>}>zmZ2Ohj$ zl1)x+s4lcmSg35N=5$C{ti*tn(14c_Gcqv^mRk*(g>?4!tJD+ve*WA^4;{7G_)g(h zJ)pNWm~Mxfj^HIR)H5{ic%6vo(NjKr4tZYgbyVw8Q-0YQSo)mHea+c=xbd{e5} z2G5JY$B+;$5sQ!Xq7})kX-oCbYKNp8a4iQt_F< zC-ONB{B_dwk*Tsb6Qvkk#MRVQ!nW_Fr=!@(|A=%wi&@w z992e;JuaA>v+GibU~46Zbp4Bmqwc6KZ8nipxr9|lwk^6g)rNpeLKH|64gFNdx`y1Agy*+H&vuxqLF5s9BE{k(xs)%{I|fTuYU|Y$*R7 zkw)z5P$MubEQUXw-+ELvj8@bPuj!F}!b+uet4;ir0K|t2T4i9)eTD*s*@4m^{9}(qn#Gf@&Dd z{PFQy^zQ^9h~qR~?gvev#??P(GLgkkWt`5gu>o;!t&#vb`UH#{)y~Ob;U|XL@xejM z>!nn^>B^drN45v!s~D-*z;b-#U%W`Gu9&i1Y7begw1Wy4&4sg-5}2&DTvjO}*`V)u z^r{dWyLQ1>#`O95=L{;TZqw~Gg;aSL5H#*f$O5r)bBEEouRqYE2bm$?6ymN#zN_QH z_wNt&=+9RZ>?{XTpOj)kqtDT?CDe;U9)Yj~#-N%Wt?310phCKSV2n?|!G5<8h;vv= z*3{IH+_`h#8k%(3oqvLji;Lt>FK#&RE>MAdgh6b(d->PSJCZS-RU`Yp8|uu0K)=o0 z`i}Qa9S4dJ|J@I$Ga#M13(|U((TC^g=jOuLQ7I`ZAajr6hg4f`k*phgc+_)f*-U&~ zzI_-tC3b>B?=%NNj^LL@b*_8q6PV4_2l$75`=JQq=Vc&&1ap^$KQw6c`oTd6@U4dTXFt7`Fo`& zMv4M@{d%rJ)CdGJ3vUzQ8|u07;-? zJU#%~eUJJG_`a&3lZhJmS5XkXYa^1AxfCRUMMg40xgoX1xfgeQk{7rT10i;eSJCGu zJKWtiljUjq`)}1SeQH5{_FF~#ilJL3j5Z7Fo`)(DcSN9z z+>`dYgFcas$qMV?dSS6opS;qJK5_k-Yn($O&Mq$K-B7<1pHOUz1YZ!Hu>L!H`}rmm zJv#S4B^_qBp+9NcBAa3usWP&u66D}MGYhcn{Q9629XeJ$p>aw<9iTTv!>Apov@sYe z#mG@f$=;r+3*v=_P1Mu~kJf)bL%QjGDlO$z!yKRwmiVwrBjWrEiI@R(gKRQH=?`yY zyJz49nU68JM|wrxt$zhZN{i5qhEg94d&uR|9p8!US573z9oNA-Fk`A z%cQO+Uy~4{HjE}V{3pcFZ6MmTJNBNuSZ}=A3G3dw({Bh|+Yc9aP8i!ggD&p=Q@JRK zxb0_h5FTyM7(}OPM@^Ksz9P#qiawHN5f6W|*B_R(&FRU)^7eMta!hyexc2n~8|8(z zz}};vs=kbM7Xlqmlxg2u?mRGBGMJ^(#dh6}S-?Nmk6yt#m;AIxo|tv7%+xLO0UPU5 zl&XF#@)^b}>|CC?+tXqH@w=rjH^IR;&{La0)}t9owQk)=X}o(ER!@tfiPAXXid{2YK_$*5~#)7!N6f5S?Nu{N8^6-7H09(>B7Be)(wkd|0$_x%* z$m?_gOOLJ=)XiqM3)pjlLwZ^(WVGb1CTjV#Xi@|JW7Z-r+ld^6P8)+#(M{ieYRXUF z?~yiGVxh2R{D|RXqJF{yi2^hr-ue2{4Gg%J{2mM_eCdENACS=BH7!T{#MhXPe)G0T zlGf7ye}?S^We+NdDGJD`DmxXIX$L=+PoyzJEZ12rk}pkJEZi74kE1*AD)AaP0KA$(S2s>)tNgR9cRRo z2paa3pBaM=QGqRv0vrY$no*r|DLOw*b~!(VBpR9ep)0q8*?cf#W^$+XbOh#cJ|aee z)lO3FUW!HepHzfy&Re$GWLcb7zUr#|7Q+P;;vTswogDhr4?YF;PYTX3=wO_K&V~ji%qetZEMIKqgQ) z5SmfCyuPO#?ycFoB>YKW_FXuNqj`!< zQ3|l@OD_?B1Dz)@TqT6B#Tpu`UyYu1H|cgr=!KP$_j=@86vvYa`R-YwSF%iWmJ6EJQ(33#!|83BNs-kSa-YbqKWW}p z;be2P8kuJFMf!JMgyFD!b9W(hOuDlA`ncq=y55o<%S`=OivY>D52W5jV-@b=1*u=z zW|1^zt4@&uKG}iqj43fUv(N*G7*N+VO`MdBn_PhDiunEeeWxu^U$a+%B@}@t# z<+0H>qnVsxPdo{3kV{z1R69J2_P1@ka@Ir5Xh~RN2|=}5Kz`Ma4D%=dmQy(WO74?- z_qbuj29Fv#-hR~;`NO$A2c(z$gREsC#(o;h>l3n7HDATnXgvPeAFs9uB^ACUvyrNn zC=>tZd72waM)KQ*X4_`-CaCM&V~Q{Nnyy8!IC`pVEPS3kT zrgEQZ4Y{1gD`cW{6oAW{8*uOMm+t&t6gUe*kBs6|PWRhKR==J%baC_G z^kwE&G~agvVJ$wo6n3tH+UtOo4JWZ8l!rfY-2Jo(;=fq)735Dx$~$1WE&S3AG%EKY z8(u3xj5;Y$j&mm9lk<59e;_N&*Jvqk=_qk{E4o1WY(NBD`+C(1o$+tG$E$w|? z^2z%Og1D993umX>_`y$yCxbl9!rQ#xbac;uVJ)|Rd2|{VR<-HmN=(H(M!x+P;|EAh z;zvr&6+m=#{u|G+VfccL(RSV`l8$w{+cdmlkCIp}NQ*pDa3ccK_?qvbV)TTsq&z2b zMPEz(Sj4((?ZakfSKZqE5q+Hik}9sM*?@%w?_U|nN2TeH{v$-;urnI)G7!iquzdQ- z>TF@7Kt4Y3Pb@8t97ZZoo(`t+Y|b%$isn$tJE$$_{*-Six5|L6Hs?0Z4GuBj#dZoE zi@T%fbdYQY z*#C+#TQBr_og+Of{H-_Qs@XU))dk#ZWv670^u}c}b8M_GMjU&Ck%TXvXkK-XcX7Aw z?wH5X=sk5XNU$X7=8iYjNyy=@VSm~sZbsJTov473wW4WktlU;VH@Ab^anD$mVrJ2@ zOY^9KOAE8lSgHi2ln$xh&YrfsBp$tb#Tq_%Y^G7U5MKG>b}!D3vdz6%Db^WNR_A!p zv2d%PT9v#DN*`Mmh!WHR#rDK&evOTdh;J&SH6?)$M(z~1eKaE7w|s_V>59y62-{eo z$zvez;WmiMX~k0&^;U~k-?Fn8Ql`i~zvfAa?&8+f$jt29Exews5WP%~8lLp(Uo>MJ zoa$~YJI+0@8x`BuXdn9o9{5iV#m33Wk8yhZcd94j@j(jFimP-1QIRq(swwjGG+ovo z@qOVV^HXCtR-SChUw;u}ZKr-zGoGcy>5x*sV^6WpL@y&rdjX8=lYS z|C*1I^=y3&Yo|%0ml5qqKv|dk=gTP@Q95<`7&!gE1JRzmGY+hg5$ zjUzKgnShF8r!tw(5gyuNGl79+dh$QhEg3UY6n*NiqP|k#AV2?!n3So1ovSMUHZp3Q z;(>f>%HFFBwhxgnkrjyy`U;otd@myQ>h15L4P;nc-h0Lj4P*jZW1z-yO|quCI=0qn zXoFJ&!hg5=i(>z%u=YCNajoy&@gF^q@e&(K;>Wi)&)j-K+r%^MDL2yr9ioE_LLcxJ z<^k&$JB=}nkR|Saa0T(*yLWFB zBs?rPheIlh$JUUg(Dt+Gn7b#O+aN9c4AP8Zu*;>Ahk#826~_N9vEG_%Y~&!M5q>NA zzf*L;?3e|xaC58t?+@miKY`T(Hs(Kb_i-G8cWEGx0j|Q;UGIxGT7>fb9(U?Qh75t) z@&11L|I`dz=;-L3L~6>)p`b(-9rl!&$$<`Uf$Hq3sw$`!Jonnj|Fawcff5Il<57(1 zfKuuqg2tcCxD0z@ON0TiVEre6Aak|ywclNxJE^UOe5rHVV(JF9o3X@DwZksBAmzZ( zWUTh?QBWV^6yAIH0nO9i{(AwTxw(1QzonMF$TsDy0on|3D3a##+i zngJpV7p)zhZLtW4dti7#&b_nvGhPBt0In~=gQaJ5hCOdd^uyJ9FqNQkw11_|6q5p^ zO7&k=XA8e-s>sQ-Bf&rA>YU!c<*KTNv$Mb#U*EveurPuoac^T#S>l7rQUnk}VDR5E z_rR)jGq&kB8_-f+VDw?4&ZXE&DfjQ#t8z7p0no1yjgfo~NVG;j5GF84@xwCTmHUeFbQHHzjlz;Aihb=I*ZcC9zve z0>Cbc5%N?APzn{OWeZ?@VYcLDM=tC-(#V7a1r!vNQq0tQSOm10O-&L2U+VAA0f-qG zDHk3oZ0>$~db(N2dSTUU1Q9mUsSrwr3RNI4TasZkPgY$*djg4tV4+ox=`iksJh;!`J zU;Hm5Yf&&b?$Ij%zWj^GO%2;5T|g1cKDA(C5?SBBe+BF#tFWH;$isN7G7 zCPly|zJL%AnSqy=mu7g-8-VTjU^hC{U+(t+bo6`+B$++}c~e4Fo(wAgf_FdjiyL0+ zCnd^c!ni3geM;M@>2``LH`@zHRUFvE_eBt61Psy=$*2Ad<%9vGv0I z;xD!T!nm6RV7Dvg1*5C0h4kq6+CSwXb~UG+L%N8UDnpiUr@jEVm?{C_STjyfUDa%7 zYWYFr{Oo?X{$LYIo(FN9?|Bmh$oy})9j&bcAeMkY@CQIrskE+a#ojm9rnR3!Xhq{I z{|87ef7yYrwraHjXjRk0+o^=`TnR3v0Y^#+CLsM8L!ep1-hiJ;C3uP#94O4MtE!}> zrIo+iP}j?!L|p=09KaG!T5U9c7~#OHh6aS-gBV&06IV|0JdR^=`7<8gaJjFrIa-*I zi&h=^=vCi9Mwpm9@Dq|Fzf|EHzSjhIJ=>31_p-D z$A2d1yOTs*vH|uP=;7f3Aa6=64Ohr{Q84xMXLK-C%rBy?O*8fheDG8`BAB5LFx3Gl zLpmfKFl;@PzX3nIQS5z{Ux32LX&W@n6jELIHUR_bXeObRphw(X9+;B9r#H@g$jhrf z3K?=UGD;Q1+VQz;8=eE4o)wTSEP!mGl>5GvMWF}*Gw_i< zKWV&CYVpO(xV-dGvl}Z)l1+DAP=a(8zVHaU)xKQ`%str}3j+y|jLV#7RW2Yq7ARpd zL@*by!9GCMaDVd>^B558fd-u@h99OVtAt+=#4~$9@?`>cYU*wkV8J*G z&@krB5(I40t%kC@^hX}6dBl*AO`*TvrHj+ZR^;K-YC4T)R-8iN!vizp+69ckK&D*e zIsh#9Z&iAJI4_${{u0ABU-#F(Sx90C9I_A2E)`9&vaR8p$UQ1Lf&0F-_Ec)jD$~Bz zoDbYkE%iYZv`+&F_itd)Hj~F+dgC7X&3awf=%M8vPdoO*_G4c7`aPNP3+f;;E`SQsjob8W&mn!)t@Z7FFVrSg!miuR}Y#-%Le3S@Eqjk zcL}mRK>5wG-19tCI|oVA3gG~|E!s5Ma2IhEsqG^WwWk3g?ZdR+?|mJqU2Mz=?hW8A zQ*Y~!C$0Byt>dMDbQj?Jl|eWeXg$T-2Vs@yBA#N8Y6I5O02AK7Wl5UyQfzE< zIbaI4>NbeK+c<#Cq1pvZ*jNLHQLQHJ131o9e~l%VZxwT&Y*|D0bHcv>W>w8;p8!E8 zUsw@Z~6GTuzK)8%-{qKEQ4DRGy<$|WN-I^uG&NcNU#9qy+W9ahjCPx5#+CQtBaZafA z_W;_SVZcwhAtjT@)moF>ll}TBQbff3q&;kF`qtw6g&|jbaBjAL_B!s7XOO`$TjpE+ z_4(FfZ;T)t=1KdSe?Mu;BYv?Q58E?pG@rF*epQGTC?^_H85N=jz-_x6&Q>3I#$H_i zlwLact4tHkpV=P?(cC7smG_yb087=@-ah#C!82GBAql(zfxDB?G4v$bWmPFrDBRe6 zgAx1It*6)W$WdOb-@A5(=FeaaMyiR|*_T<@CA<=i#w6Ye6VvO{2=K~Bk-c$9#&_8EnL^cAM3=B|$g<&~AAWxl}(Y_Pv#idYh$ zWSPP~iUKjfvz;F&$#?fe-X9kr^f}qs0>ri^#$w%LyGnJHw5CB_rtW8 zI89H}513OtBAz~dx~%qmVqyYLLIZ++j^z7N!Nih9Z#-i*EGc3ML-jF+Cm#TQs;VfS z=W?a9#41J`M=5Aa{f~HJBnEiSAYh)kq~i;eewP5vS&-F0PO>|!s}KcTaq1&b6p@f4 z55BBb+0I1B^j266Ni(PczMtfx%<3qfwIMqINOM2}FAD@b8_Z{KtLLv!dWicju z98ywJi*_p_YsWitD*sBb&Q4GL8~OS9wT6Gx*S8|qLC(Xq38mTrgL~)8?Z3K}99vvm zEX#H`tabNL>%Y`u)MYDxP|IrmgcUS(h=qGs0$e(&Qz7bm>w`e?qaYJXO_*Wh;kOtj ze$CSOnr25&(D{!pPe>QNU2tUE;-abAs&%^10y@l7r9MiewvT9y<}_eB{$17@AY);X zt;-N`^0p4}M3;d!h`(cXE_OU^BcS>Ucbx{e-QwgoAQkf4u3o}FQ|vee+MbQevx7zb ztewUiV+P^+?<^{6$wCf4SKlR0!noow(Q=z~%g>*Zbd~`pg-cqk6phaRBok;p4dB8W z2nPgWcvV$Z^>LnFnFXvc44)qMP3ajj!osJ%Gh|Y49^i1^PYcv9Irub>f$(dx(nbLm zFIL#j#OqhQW+b9Wrd+ZPxqG9S{Cc_dFG!W%9c^lzgZe11pg?h&u*16pkbiBCf18Kw z>)Y^Ddhuu;Ybq*GdS`PcX2QAvA=cgxS9H zPT8*V!8skFw4(Ar(?XZ$z?`^z2=0HFuFIxy>n=&#;@h)s>!JJ(@{^z+m?#o+uxZ>f z!R;F|2;O0FTt4RVAbhNn;t9kwi?wXY>Pr4JxHYXd>wV*SE`GCivg_-j^z^5&CNhbqYjN2>=g{?av3XCl>9??)OIPgB9LVkQcQ{hj z6shXzv3jin?w@Y+Qhz^}3|fcfTuJkylcNQoGI?b2>&`{ZldC=JcAW51@yi7QSh5ko z3$6d7Is9&TVKvv_xrq8YxADjOlR%N;%vuoN!XM2U+Yd#s!agEIP%y;;~o?8RG0Z0L|63P8#=#o-eQnHisH&_UuKo8MDl26%|?|#tU z+%CGw`{)s@2T8{bDFgaQAWlKDyi{U=JfDVP4dj9#3AVww(O<9?qvIA1&LacjKw>s% zfE?Unlvxd~9zX?RN<(jt>%|G&Lr2n!Y{51X;>#YW`# z_&^LQ%1th_qWhin>Azf-U%*cSnPuve=pmPR>di5-Ay?=eL)16 z+Uo(g2*~4qLU0TLL%o^OE@vAjjV^76c|V%VV09`(x;Psp zb~GqZYyIAoVXe!h8y^)V_29t+K=4m~>BLTbu-R+lW`^Dsd9Y`Vu$ko3YiU~sv=n?a zO-xlvSk_Nb)(OOtQ7v2j?s-|tC&RJ|1{JQ>;v(2;*fwZqa^1V2i{R=COg z_7nwTA|gpJ8mza(%q8z{Jv16pQ(4N}@kfXL!UOsNLhv5mJuyS1+ww+t6uWLiYQSzZ z7ceQiy`T8hLK4q>aS6?#0WwKFh~Smmo=`+icFS0b=)tc?rM^L3DCM}|Bxr-};?^os zfaOHT#K@pw;ggtKQo=H4utZ&?8z+k~ap0IQDrD0vqXiMPZ{blyG8+fo>e3jZLb!gn z!s8MXw6d~-cMz^(Kjr28>jDGA7JZ4V3bE~I18hL0U-QT^?5t>EV4+qzs$3>U+mJbPF}eAErUcvE)22n4;*I=1{<){i{FYmZE7;8 z*!stC`3D9@@E0iJr^0fS-G_H4cB${Oq7QHZ@rr9KKb0Fhi_EgdZbY^4u9*W`xRHfLPs0Lrp!27<)yo$-}9N4;5Lx6|Dh zK*r|-rvD1Ibw5=TpI_GE58U>tJ&KrAk~yq(iLoVc;H(tIcDnir@JW)YpJm|e20*4K zNr7kqd7&k&pZoRS)&{vU9U4OfyA!Wc;(+_=fGD=+1_Tg_ z9&!QE@+;#mL;auD0=+UbJm7)J_+X*7_}1c9GC)KR!?j2^1@hU+FIBE6P(K{J1Y;5t z-{@ahjemb3n~b=t(g?0OP2^c^rBZ?bh_kM=aGI+u>M7`X{!R~7CCiu?+n^3|Y5>V;>j!(_C#>v{hB_3w*(Z`ai-CIqnnL0a6|$tHxJ^ z6_nBfRF_@xXPf)byfC~S*{8M44X%mYbgw|>MM6!@(LFx_hqL9*@bqB*GzdZL&f${) zWQh+MRXi4|9SEg&JwV$0(17?gz(s)(C&RMK1odKKKA@g1`HW(TmeTVo#Z!CqI}?cy z^H1P(REt_2Zd+sxBT1m6Ai!Y$iO8_^(*?}`-UVbtgsFV;^+n?iRaYeDo-nLrmx|lz zTsmri{;M?DWTiQ(eS-JD#2I&{pF|elb}CE6uIw19-`yU5Baa=`-2$*%c;kG>yv_ms zBo&wAOh->IHDSEM%*%3pRx|m|!V_wmMQUgI}dL6rBW=XXd!89(o-38MdzTUMK z-#{ytC<5k0g=_FF^G_xt@_{w94TKCn3G+%8;y@d&M;|ZdS2N zCV;mXXLz6@rOD9&wQcNK3Bc`xaJrB36H85Qr=^N7{B*E40h#i>~eJ2chWo>5{2u zYIZx015*QE&{MR!prDDfg#!r|C3i2*GE=3e-vioI#&(Liu?J5CBm&`i~##v>kU-cii!(p7DBg@Xv(EH!z^wVdC) zEnAZvG7Z^bz(7QgsPTx{6AhBdTQXkV`Jx;rlP|EQzM*txh_m8{d&AY5@>EB+JX{<@ z%lqD*u7xF!!1VISiO%nO*7muSdo!%Lp(%LJ$&InFq4+*fG9lpvd+a4~SijhXqE{QC zJaV~9Yfc9dz1q1{QH{)KRXQf^KcOMcXwAxG*PGCje?5rGSejC`@!k!KTU9-1rLja) zEqghumE}WwiuI-7P()NZVA?7T<#oth4-S4rb-L^$d8zt~S!7kyuW;weOfpOFvua(2 zZm>GYzq3Euk)3_ey=Y{By)kRz{4CY-dJVuix5}Dqqz1ouY+e^d|DU7auP5 z^CK*ZqdG#umVD>RvrCPFqV7DqD0x#yxbMPy{pa&X>@#kN5@@?5h)6F_7WJFUw!cYRk}~zvQy1BaQG;|BPtfRUpC$A>PV-pr>EzzZfCC1#H&sC zHIu58{3fnRStg&8Qn{?GVt>Xh#<#DA^@Pyt!hFZ(5lQDjJN{FNq2e9i6kw zjZcNI@v8)>Y)*XJ`UT2oD*RT)AW9%jlpub8Jn>5D$yFP<2~vipEmSb=W|^e~MXUe7 z7-`Pv#`{gRlq_$~pDxPGfL@K4f>&&8>g(?CS1%h z)`7^+JR{;U^)Hc!3i}GS+W@8fv2hX#)aEitEgT=>t91jC2#Z=Xbtxuz7^Df?*C=Qg zIz@*_1>!mCYf^MXk;ZJARrbaFVN{;rX{j_I(SWcEf%d|<@S{1lnr@~;a6 zf*%N9WGpC+uXcXk+sV6d;cz#JqP|O8IeR$M3n&Z4#sfR=3xllm^gd)+^DiP6Y)w;w zamu`<%$oBGhWw0A=JMx;BaUw>D#}*7bl(&qj6c!b*8K9|j6A^I=x@*0e0CBVxon{lNp&HNkv);~V`?A-SYLTu!>Z=N~xG#z8NE1hIPYNro|2 z=SZOVBL;y|#b=&fGGx||9&!YJ)5vWhIf)B-pRWITfI+J?s)VU`_sv%8&ccsax(;1$ zdhYAT&#tG;Nz9yOqN8NyiS`sDy-76!IBM(G*DdwlrW6|K&obfsVN(0T@vY*PuEa(T ziMn!@`a8}P4E^pK5oyL18vkN8FKVfo!U~L$7Un?@y}rRR#^oo1)*q>0v|Z9QDFq;4A$UF@z{9*o& z?v2m>ngQGD0YMyV&c~Ma5NC1?cRh-ziGy+C4EfXP1b3xefQZ!1#jD;UpAwJPVT~cX z6-E~{Tp5nNZlBGeteCnq+UIE#w({V}dIur{7Y1K=YpZ0*ECH2>bAgqN*BE>D$B@#_ z>wHGNUK5UrUB;V-dKPh6m2;X`OlrRjhY+-k4GH@66W?GljZC^#7p{SoGGmlUwA5=k zn;-S9lX6Gi!&}8~ai+P4^UAIJh{`V8_>Q%{55^-T8RW(8MUCi!XyC`)e z)c0mD;%hv4R`e}NXy81_5nQuY&PQ#(v)C6|m4fNC)SU;3y6+M{BdnO!TYDxZYX1BA z$SSUq$z_j?Da`t=iUDLGdw-r68!u5Z{}`!0KgcIg&hr&iMgR`XGH6g05SM)xLi7 zoWYoi`B4t79oskSh}U}VO^QJhfga88#FH0#xuO;Y#~R*!i1(J@>nD*1fq z*<2aZ`K7iRjON9`*{0$q5f;~+obR)uZR5FIZ)(FUET=9Q~Db^^PUb^52FVQF`d?r%0Tl8^|5sqfod z5(z2*HS!(onY&UvOCvXTn($3U-$M2Q01H>M8ZC$$m;QL;4XX7s(pM!fMR1R#zx(=~ z@L4A`Q);mY7vmSW+e#Efyc)B;`?ij3X-9HF{wM2MmV0hz2r1QVotukqSUU}ky0$(& zB4^C?lo_9UXilwX?)COMQ1Mj80o6Y{K#`FqR`uY18~Yyp*ERO6lDj0(Tw(FY`nN_~ z*IqoE-hF=V3!T!^`?{XnasktH-rv%$+9-P>CU#zx{80IQ5pFT{UDwvo=xA5_GKWib zG&@tLA}H9*{BOE!n@PFj&SnbU00eKhB|ze{=V<})ANDx%%%VYai>aAEGO|fLo1IZ` znOy6%SD{`Hqm^YtK#-`YZ9!y?wzRmsEr>{bA!|4|%Et53gLFSgn+SU_N_WJO<-We& zkt{^s(DzyVX~3kjG;VnEVy{V*^KrbZh|D=KjNu4vM5F%Z`Xaj#?%vq9q`x~U zt=4WD6ppZC+)qMe{gT&iws|W0xGStFOOHR`SH9_`L zDkmap-77Kkf^8uPH$clKXHz}2)Y;Z`BUx#zy|Fwk<+AaT72T*&|sff0`4p!GRhfLEKf&%E}r}dxoWk1&zUAO5_jtz?ZE+ zlLu(KY_9%`j)4)tTb1`OOi>M*2O(w!S=q0QF5^OX5lOXpKR|a0&|ek#0t}M5yQe1u z@F%iJgL2CU*#~gv>?VZg0Fe4xfJe3^CH?6Pryp+g_6Dfc7%U$l&f`8z5^|u6V7BD5 zeFn>tq_Zp1pr#7A?q!I!7XLe5@<}>`5_GDClz3h^V-tX8o(BI!Zl<-MZj>gcfh8!B zK~!`GiP%e+24hQ6q?S~3QtUJ=S-r3Y8UTIZ)rpd#o*jVreJlor?k3D#`X>K_yzgyPJZcIffzA{rx z#w4H(UcOBoIyKz%0Zl4yO8;LF+8RD!)FUMJs2A!hwJJ`hkPP;_?P4jVgr2@{DA4!r0)M~6o5-j#Ep zRQsNrz6I+Rx(BaY;>R)Zj)u0ny6NN}(k4>1c(qIxKOzSZiD16xI$Ozc&o&s_z)}tJb*G_ zY=#2H-vPa+U(nX#@)QMVJS6%uQb0c$=@CdiH9!w7K<-`wSyO5_K<)q;0^CT+cWM?u zWM+8ek!V81U;>m201_oksM%-sJ_beHfcMRbcMz|H#(1N*I3AUNA`)Kd0XVQ!Lh#N6 z#V+Exz0b?H<0oH+i867qyqXrMdNTpK!{&9t$xuEKmEwKbj;jYG3Y$)y4@0o#sR4Rv znF=Vm&DkGyEP~df!JvQ6`h1yS06A$qh?5PHwl+ zAeTefF|$=txU4Hwmf-f!eor4*!M9BXn(lz8@}$V9&cZ1_2PUI~#pSc0v z%@8AGq2T5w3^U22kOc72by^c$>w4u@TA&BaijBPs_Xd04T+D3{c%ObM7`f;De4dI4 zN@aq(fT){j9Hy29&b!%*JL!*M;X)-W3)}(dS}F*ZDx;Lyz>KrE zvbGyp2xX;;RO^ zH`CSOaF??+6xwDJG!?DcVnAN5tK5 zN_TaA(ij6iU94=k>HGbEKK*S+RG%^qunKd$CjRPPhbcz}-~{lyIc6M}6IwqJh^-G> zt3$6_Y{I5eGycyLGq#_<268+2Haj%AIWpP5_A}XQ(q-MYzu=P>0)s=812PNwQ?|c*Xra7{QDo19vc@7a64EP zUcqPaX`dJ9vMJ;F?+F+4*b%|+ZSlTbx(9Gj<7%6?R5mRCJ!R|l1oWMh4)Q;!)sJj5 z@Mg9ML72CJ4U%%7>36ymjziiu@O92GsG1pWkFV*Sgo~vA{zkK%EP?3Z*8-GkLEC)1 znn!TM_CKHf@r*p(GP#jg5j)ESf=5i-vT5hPAHWR*7?srb$Mtdu5M$uif^<>(yjrO2 zv8itOKND`SBLbR;OptfMA1(^~vZ)b+KO9x$`D*SXT?hK>j2b-RiVSO>uB@yaf|pPX z0R}uTJ^f>mNh25PKEN_t9pO|0$Pz?m+}4tYL61qvRjZN9jFhpl@ofgY? z&s(?$T6w)aE>Yg^ViARbtwDVrqBKK9xvYro%3JD12P zX)|CEYXrISakJSQZ-OW)ao+di<5gka0nz2$r`kIwfuYlCp#$l@d>D;dEPhW3;!w~O z1C>bTzrRjL(vq^WvhLs=KaR&q@G4FYHK|QQ){RwdR=#Nvti8v_KB_HjPsc5FjAh>8 z&3X+^9**+K%qy^ zoR@28Wp9u{=>mx-QOfQ0?(YRV2>+LxK1ds-L@^2TZ2kP|EOPSXgIf!OI^I8jFp?CC zBbH@keRRbIETqRJ{Qw(fkR)!|6t~H9o5-I&&GCBY*AtErCD35|g1W(aV;2Oj)^bBI zEQ2OVspZC(ws2+*AyNh!A;)D|-o>Q;mp-h;q~Dg-_3GT}fTZ zGWIhGr?#}VW`Wc2ad%J8Y;C|Q0#j1^iolc_iQLMFnCbNlFG4mUmD6J%8y}A-3`Vx7 zetmLv2Oh8QgEa&eip5nuvZDwifHKj}6kqeaAo|p~^3L@Q1NZznEOcNi{%hTMRez+^JJ}6Hkbe)3VSMxJ$c0Ry)gGr>Co1y4GL;V1tK@5TkSGN6vU(q^{-eRZ5}98GL6l zBqyL3R$KLw!i9hz?r~@6ufewUb%2LfR?o&&P`BdeULeOkT28C1)yprj{{IvMf zkcM~7#)V@pQ&Zz97qdLUBgj5*Wbw<9#Ep-MAYlt1zFt?guV_h%&j*e7`l1>=5I0=b z@#y5fd$vJ&y{uRcy&Ee7jMxvZ#G$r>Rj~e<-+q3VkV$_gL!P6onu(9k+0C1Ar|Es; z(jJ(po~!N{-Z^t(cV(5wzo8;@p&zt@Z@i8Vb0Uum(Q}DKiw^I~_+KaXTV3^=+vG-h#)mec z+v-TF_;FJprKMj6YLhV7>MQy_%3gcVU;}f{@}tYKh<+*2eq;cD>4HO~Z&p^_qT3)Y%96SWg=2K+UDaTo zc=7CsS=kW;s?qlmYtzaih{c*IGG7e2R1JyZJ|A(>4*K+yinmcZB(9CG_;I#0Ou|?!o?yo<}RP~ta+ zbY?|t;^e&!{&a1WnKE%vG#v9KfV@?Q2%gJFO8$P)bmNngVfPvpQ0W4SW{H!r9U|!$ zP&eR6$8by^uwe)1JnO^w5#`VZhx@Z?oV$4TKONjq$B*QcKNWiuCp z!3m)3!!a45gkirpW!cZbl4OP!eaj{o)?tD%uVP58n_NyAW+e{ z|M+yA3wH7543AIVpaXi1096omH3g2DC>=#;M>N&szk&!PYw#{$5|;f018%d>TM($< zxjQA{ze%$(>!!$}eymF+qeb$zve{ zX8;c%{(JuRYd}{4t)cTBpMTUn#Y}?@UCa*4ow4_faCaah%>4ZPMjMysi6*oQWfOWp zm9&rQ@wX1hO81BJg7O+Pff!^J!AoLG0OejM^v;O_||l{puyxRUeH~* z4hs>;FCE}wW7CAzn>7ewm}jYKuz1kmAtKLOun9e>LTaKX&)up$jxpOWT{_T>#7_3P zgg^_6ydQPctTfyUmL9h7orjb+jlwZQbPU+XWV6bB%w?-ZgV(Qs@|Y@U=q7Q3Fvsbd zVqB(5J5S+CMkE;tbvqDP6b50dLC0Xd!RsXVzN!3++vFkvr_yD>axY-ZINzT*zk~ps z%5B;J;1t|}h~c@Edpw5%_Z5KJumT1F110w<2G|sZl9G{W@FcU1P!a^1(C-?g^eaCB zK#&mp#unz~Pdq%tR<{m?(=gX0RTk+bfY~En3VA$3^YB|_)OfU@py29zf!IBJW$Hp! z%g2vwHmKVTYAZfw=vkmpBC))&(NTe!Aq)7H2m_Xjen9zieL7dP%2QaOL5~FV(R?Vv zo_dR*lqAtj=tY)rOmrA|Yb18iQ_ZpLaudeix}D9%j41sgn#=X;xHN5P2!SMx+% z9HqiBKf8QM*N}h+vi*vlLO6(fM!7$`gwNq%_406`Z9;b*Y(j5M`R#WGQB~dr88EIW zr~52N2$R{qfssZ9RQ}B4fD-J2!AygbYad(!t!uWQy8>zRmJw(0feUp$FGNXfgKKo$ zF1$-~j1D7^ScDM_lxlP`{5Hqg4a@jjX}&K}WTGq@fE-oDh}g$EkHIDG3&*Twe%|{d(D6!$HcBjX#DerEf(QqoSK!O3*(u&OF{CQzqm*f|MYc|1`S^3Gv z&tHG6Dm>C}hp5(q;mBZu!70la<&yKyR1?F4f`b{!RaOw@6&7561$=Oyn5O72(>U@Y z|7J$zFbZhC=L)zr2gZVih6V`Mq&U}QssvNEaP?BUlCAD{ZIu2Ntz0-jcnE>cpaWBs z)gHJN&6zq>r5X|SN|pFj$4o%~Z%JW)IHK8uyb*Zr7Qi{n!oeZzpnl1BEx|lJAejNH zyug4h?&+iIMfCjU2a`|O?1xj*A&*28i96U(JF380)re1Zu>we>Iot2|uNE#vjUYdB zzvK^pTM!HQaVTRIV!>HBXlZGw+_-X$6wbq1?K@8fmqs$dfJGG@!1dc|6_T`!#nRhw^ zq1x`nF0w`->IOcxVv>3|_Wi(sS${Z0y;Kl!zm%^k5mf;Fj8!LAIX5Tgy^lX)LG-R z35sN89Yo5GjL*w}j@=U*7GuLPsI~lcLgr`20@!t)Ni6zVMvD5n$D3M(mXfiv%8Qju zQHF>-V6e_)FId_H!nam<5=gJ zSAzyja+&YdJG9PWeMGbOv5Ao3gSr#!h?|F;Q zNWkm-kv~bueDQJ-!Q{?>&s(?}Ms$eWLGW;9Vtiaf>G$dbVZOh8%>8mr=z!232Vy6Y z$`}jjSV8s+&JSE=bQAF6cV>4Cwsw z4tRov<{%ZRBm?p$qPAGp#Y&+4xX>yzqXpUzAsswkFbkpsZGSuJ6Or|)OpMh?=>6+V z1qg8f=QX$mE!cSb=CRvAs=rhJ?@lr)H_Vt@d%7 z5A=fR*V_xW@Sbu$CcOQ}6`IgeI*0W4AOu-3%C|A~y0~ykopk>hD(}NfmD_p$f$d=W zh=`3INrGAH!n8fMt?;t9e7koo{iger+SNX{MI=mtV2DcV`_~iSMQ=BVO!UcEq{Qoj zv2?wN@7`h2RSeyc z|Cy`aI0vh-s(B{_Ywiw$bM|~*G;}2u9COWkwMpNRGwCEf2~c3V}FoPtG#?r8lS3V zHgb3?*@}MY;p5`-vnGJnD4Ns|Zn-heE^n!kSg!ngeSnod;h>`H2n7g4{biHc<*4R<$Wai(KHNk4I>@V9d0#Z(R1h12nNi$$G_Es`$$HZQ$)zL(Kc zv>cQPbG_}6H-FV4vzcJ9{aJqhQf!uqw-)+^kxa9%rLy@G0lUlIo8*|5}E4@EWfT_6PcBG!^p^zjpAeGy!MP@9;?MYOmj-M@YIrLQO!+& zU@7V5QUt$JFY&9j9lBSDuQK*~DyM($OC34KW0hG~H63Q^-WP6t)q(DNXtO&O`a{o; zvxqwsyK8XSn9YTgaQ%JWNK8UlqJir5kzI4B6QRo_@q{^ShCdrIFLf=#LUE;og z9_1j7@U2@{4Ic-xJa;F#W5j84zAfX+mj2oybbxHhJG4}+UWWKZf-)q`n8v}_+=E2! zT8A7J$9jy|az~hGRqUwO1?P>Twm8bpC}ERRyb}1g?8B!pN=h2_x=m1rhEHfSb2M5a71R{n&mxx(f8Gn6FLSG zKL%2RG(TpAU``ZdJo{ZDnPfQlP(wxG2|MlMHBGHPF6?FYJ*1OfegeVZg;2uCho6q< z`~&-(rYBFGiaB_yP0KfdT|YTh(e6{(uwV@`uP*$99J6; zC9hvs*m@e>*VMLRWP1fv0CT)`aVI{$mylE)myMA!I2a>l6iwPclNY^#wc#!K^7z8S zKx?X<6t4F`(#gg1Fm;?Pkv$Gd;yWxeg4q7mFA=9xXVW+mM5f)MXFnJ#P;%`!g=`l& z0VM}dp%q>|{M~cw=>2<4_}(9)f=30+BT_C@rfK!9ub3R{ALI1Xa=M8>-`9aHB{NyI zi()E}!|hNRTX#n`tz`HS^f)gm%{y3RCJ^pF@2%zgV)3#i^W)*8jCtwiujq_Fmo}UD zUiiKBK5V_>T5f;ZditnV1MLes6Xl9>=~2MHo95vYxXrrFG#@sHPKou`8HE^{#+4RJ z-4h56i?TBvRjs!eCu|fMM8wORuX$WpdYYx)ayG&wi|oTddt`gUrLvGEspY>F%Ap%c z$rZ{Gi$=dMzP1(2%$T>G`B=e)6}_`f)%$Ne+zP&DxH?PND|IIOva)FNYmPIO-&c&z zzBu~U@XXke*7>T3;J(2NEhUCh?EgNz{XI+rl?TpQhYgi5Lu+#}Blp97lbg&wY z;P2pVt9*@QZE9IGv<)4L5BptX)lf*g5^lt16u++xz1yv8cG8x*?7fw~s$kMwgg}Dv zB93J<_S~U`+;`KSZ!Ij2Nj+AjU6QcY z7eRLj@h)1t6>qIWmXuQjE=&mM^Y)QsnASP!0-?c@jd0vw} zbV)x^ha(PDsA60NPMeGFQjT67nvvg>5-R5iw5XN0px*T@zgervEqsEni}S%{UzS#e zup(Qg4BfnleyG=%Xk7wUq`mtD4sBMrYBtYbKrTwi!t$iQDSS{f$>Lfi52*j z4jX)>Vo^%keOo_%r|R^~?t@%W{n#OjCe9tMj+{Qim6p-d8ApyB0kRsBm?cM-TZ>jY zeFdHV6W;3ToGL{?X*Mjj#G1)me?ucITb*N1F<4W|qm-nnJ<%p*e_g%j(DBx5!IvbP zOG~Zz71s5RSIk`grtN+`eK}V!?71*b^T4K8%d@9nE#jP&B}>}nb*bO#-3}Ty8cU|# zQu3XNL*QD;`911H8MAW~<+fw9a{}6>@tU8p*&=sz4s8yElhv^t_YM@rI4Yf^FqO01 z%#HW@;V`Z3Yy9MHKI<2*-_HoAHlk%(Q(i`9d7CwpBjCT^juf1V!iFE8ctj@;l!ojc zIB_W6_)#i9>ey5ir?RZuKWh8*h3zjm7V)4%^uedhGE|sMn=WmzZz6f z>fkfcWt>cx%qzr1mI z+-5s&oOJwAOU2#%(Ziv=FLGnbUdss{&20)!6e+*a`PY~ZmM)lcA&rzIXd72A94YO7 zGcBpi@2=6<0=xc2LpC0X!i@MIGLdltXL2_=S#}%F&&=~_X`gVqCXnzMU-Ro}F~i!l z_0cv1J3iInlh+F+CwCbe>JpwH%udNY;0 zMQih>D|50pUTshD-cBE#u!y_8NV-D9Yi=hkhT+t0E7qwTpES&}^HIE8JG(({+I)|a zvgko7Xm4$|^{W;dhp%7IA7dR*aLu^A$HPn0Wp>SeE@6(Fb?~-tvp3UzwvHdk(xRh5q^TRFA#C4KUeD~VaO7r<@A@9N((d8ef4 z_^z98J|76CrW!x_zl$^CJ*C6@{VY{9UQkzf+8@}g!s{&`nv4|DW3zL3b~+v7Z7YR4 zzr6AKoP=A+{QFZbhS4weM`C;Zx~_iq=aOa}Xjrz`zi-hlTJN?;*G#3VMFfT{IQv6h zZlPpCxq|EOjFe`)v=V;XJ0_RV?Gh;=o>_d`S~%x<*m_sLRS~_@XLq9G?;~^FZBn7| zKBvd+{i}Q17pc#)IW5^c*Lhmr@>82X!g(gxOK!-pvvvQG2O2wRb(Zo=Go%CEGvDl! z;pYwtUyl-{2@{ch{woh1>mrqu7)_gX*4f6$PFXb9m+Lg+49y#bN1qFBOE>%fxH6d+ zxOy|`$vYu^u?wcLvsN!}=_J+*wmMij5BsKU3Td`g8q(dE_^cGAa3E5Z``PP(7uJnm z?~N5K6uSP^PENTtCS_TfJ4)~O*vLJvW%tAK$&YjU+r0!+9=~KV5VVnq%@Dll`(i4| zTKH+Du>IX(wFyacNts%?7{i&D`@WCUT5M>RKDgvR`B&#Np6%~@H8*2&^j=iJ%43;x zh2`g)Q?kfJ-{#1L8R5Oo2F=;wEn~0z=(g@jAY<84y}K!?L6+c`dS!$5e6zuKquD*S zt9)wWh~_D04+vq&zss`#+D-MvS-G9{IWAF+aO-$Rc`gvDn?7b_$pO_{88Phq@q_devkW z$Isth32$$n?;G*ttQbqJQS3`J8YFqa8+n_yIjZ_oW04A=N4Yi?$N-C2Bv6io)Fi?i&me!(#J+bHKRCdFJ@Ph1=0;XV31$!9D}At|lL zTz&h{S=jN+ed}Gus~*Pr%|jFw_I4ioO@2+pf2qD9`#>dPmN2Aq-yu=oQ=vcR$m@$= zUzFt#vDUW3Dr#M);^VOrd4arsPDM{(vs_blN3Oj&x<9bO(>unTDPb%B0GFg-+D(Zz zvNip*^fS+SW3omzTPkGgHHBo&&P}u_NUM4~8~WoTF5xzO#ZKR7r4hFA6V@n+^SGsj z+r0@pM$?=^#rFwD<`JSNZWEL;d}YMWI}vHkV=r@eg--ocgmJKdnzh?CFCrhNi zNaQc~rOanXBhk;C`9nOXhN4YJHPJWq=k@=MrDs0mrN@}%6@pR|l0 zV^=dy@n06ukJ#*;k3fq~TaLF{H-2YW zw~u0)7xItk8{0qA;M=sZH{Uz_B<9}3!jx`)SexLq(0E6J6bBez>(?_4&l_+`{ zYs`{=^j^CnA#OxLOr_=JH9<;_c-iOlI@?s?Cgq~(7akX@_lIjNGcsf!YTGc3XS%ZF zp=O<{n|6ejFL5yTxVHTU;|44>r-12M#bG@H_)Z6(V;3JAwHCn| zVp=z8TMVO^7>l*uZ+Lf{&p31qV4=kn#b1cRMlm;pj=H)D7&}u9M*9Dy@V1*;D0Cwx zMNOt{J?x+2#~HIsiWHs=rB`~;*AoBC-*|2OZiReqPO11!Y4eCZ#w45 z!|2;doJ{c=j9M8R^^IrKG8al)47GZnWn`T>n`X&+(&wG1e!PjmC)w-SMpe9QN=8MG z9-7~Is5$9|Kksk}>-~PJ#=b zpcVgo2JtAs-k2nYBgMraw)a+lUC@KCXFn3#+Y6$oliF~LUz2B{|Az^B?kEx#)cX^7 zFa}PcO+>*{eW909q%xnO(3VY0ZLA%|2xY+NU%`(WkiY@jVyxu>$h4#X3PnIHG$wzI zkeL@jUIZ$`a&>noX!gJV{`lyq8jqYi!+5&iud_r=Y83k2zvG1{Zf#IPKn#QhvU=&j zqw~5N`(Vqb-#OpCLq0A&20? z{i+Q z;F+605_Vu6p;R877Q-EDk5YaHa1HpmmX`a1sHb5y3ot;NA|d3o!4V{c%*w)YNLwBB zX(6J2V@E6YLr8mRl@0qakEsB(V69ADP&)^TUHmDM@IOMxy`YNw4`~#w`VVQ8s)Im+ z*+xU5P)XMaq(h`;b zgypfM%|fM+VafU++dA5yMuQ$T{IbHBLdY;AYm%|0a?CCtJ`$ZV&2{ za&@HY3%u1Y1zV9d^wUW5`@PDImAs9{cPXqB)gY}4^b)GO25b7TnlY0OW2C>(Roy^kcwEK30APg2PrO0}%1UFDBPG1IpD3 z8bUAHRjtv;^9OXCUZ5kHH}aaH5_`SRBQyqmsD5}&CmlMXJtQhMdIa=+RN)Z=_6^}^ ziISjOD~NBp5q*nvXTq5{x5oI)@|q=pjR^P@*xF+sh$Bq7Bn^ivN}_`e~W{*zW@7toK=b2m1|7c;(E>%UM=o*^*FeZ?OVs{QgvKV?cDywK&O|>W z-Qi23@)X2te-5!Hj|5?Amm0moAqT>9%GJhYx@70N%}|7a=Ae&(yzS8@^jCK&(&mk( zj>ob_{o$Z|6xT+5$-~VO_c4&q(*Gw-`wS6E1DLm%(FxSdX?G?#&B;1>trR@JRM?ek zx`Q+f1TPPy3P7&(cQ!cEl?+LxLxLP6Yd~~%GOTvRZBYgNMAEyEV@(xj7XCT&q%7H# zb3!k%?A6a?NQ|SAJbTnv`D$NV4mcG}Q*_Pr&HWt+8ch0v-9z^F*?;3u2g?7!p>o08 z0l76upQkE61C7c5MG>Y-}q(L?R2(&JPoa9^o{VvwAF z5aEhalyc#R6l^Sj<}_Y_A{m)25Z!tKnP(96AEbOJ;#<$+WoA~}E1Bg&2SN<@a10t` zl_6ncTL9|{75fYbVLK>b1}c7P&~m6D%66MHD><)KVMPr&7~4FdFNMBgN!yQo2xYb2 zb45^sdaND+r&6lCb>dX%++@JkKj6nn5}j!!c)e=0V49uAzTQO7eSUtr_f82r6m*1X zGmCC01`%jH51CICx70#9xRi_Z-^vgXc??q9@Yj#$9qKPYoPrdNGla;yI!}EnvR*(W zO*K%nsUVh5$%eJbC$5Ol&{qL_a8=}X*i@cQ!#fjFCc4mGBr~A_^{#zS9(oQcuJyhK3y}TiazWGU z6xEA}ZBaf7!B?_11m}S!W}eoyG0<~Ay*(DNs%aCF3h@sfjGLj7DXUdHQpTkB?`et9 zBcZ@WE$}idd+?^byu69~M8K0JDJdA>%R@cFi14%tka~Wn212G?koY4696Y2eXTYk& z{3edQlgx$HYA7an2yy`GuK=DrlDG+#4KMV*c?B1J3ND)NqPmqH$Ot!v$Pj9Ift#Ao za*I%NNd~I49tf(Bk5k>eFeV*wV5*J*n^6ezERcVmTAllv1gcck_7t;Mj-akWNLWaL z%mOkQ8ib}woZzTdL9nuKKyRsRq(XENKcs()>mU4Tv>Jk64V@wRANbWE5L2Se)QAXP z6}{NRtk((EcWt7Z>>Fz%j|TC}a8-*S#)v4woGXUB0|aoTL4;wTta619wChVeO!@<2 zO?N+#ibzkq6M+nsY3 zsVscwuCT1^BjD*GwJK=ayGS;=x z)R{hDpg5xsbIF4B5z^kBBJqpGBm{X*<_}nHw+`mL|?46U^H&eo# z3x?DeRs-H3OH?>FEK6P1_$HmU7ZX9bZ=aE&ArlM|>xvLuCC&YOJUb1o}u%82&m!$y?jGWn7dwr5(>l z)3I`M7hk)6z4b=m%OSUh$+ycsOR{5b)I*^AOQUdA19I4lLU^wtKo&3RdIR94g0YE- z)*GNlbgQ3iSl(Wg9d+B+$-;mog4(9)K;X@*F!Y*Ofi#ROHJT6L!Yay+q}$0W4$gCr z7hjCHQFh|tSo)DrY5u@v6lkptp{>|et}Iz%II?al`bC#~<8P!_#QMabh}0lwRU>wS zONMt0WLpMECEq8ht{`P1*!EIyNR1Ele{uLY8Pq`?a0QJ5%QL>L6zd&6{TG@K$<=U&&dvG zH9<`srp`sd3+1veffWK5ttaz#A3cxr=W-22%uGXBLdozgzeR`kGT8tjxByXTG7VIC z%8Rw0_xsB(=}}WtBOaLd*?mC7JPpqx^S|h-_xY*1VQd}ivImWJE*ON7N`D0~A1w<9 zOsWxWWtz~J|1uGL301QGlg#Csb%(*`>0X)ylegA>J^$NmOl4(#F7 zQ&_{xlV(_kV?1D~qYWpSfyFW)zEnXil?R@emh1^wHVFg`gK0)YQquVOfZ}2$HB`lh zwkys^4y!r@x(b@<99V)IUb}W^ZKh9~mjbll(IC&(2MCAUQz-9uocK5~Q4{tM%eFc@ zJ3FhNuErwT9bsYNa73Q-dc+)tB&2e9-Np#b7w~Mt-tB!^FX%)ON3nIXHOLZ|g;57^ z2XM-Bamv%Q{#heerdC(oQ__z`?^#aeJ&&QeB*Q8HrmXsY8PkjS?zbg|;M&0^*p#XX zq^#`iB9H9|SJ)3(%LOT^kRdC37}Y@5FZY(@(0|M_nMl2*vQKXt*Fl5Erq-w6waSn# zz3_5D(KWgW*$yF9%6#&!ZUeH+9jcwVw!c~b?ym=aCS%Yj=5c{Q?TXuZ%Q=9#fGJ*X2g{S!L2hcu;qkQJRNFjCPe{ZjJ7()T|dB+#-(@fy>GM69V1>Y7@ z#xjpNl#b^}9($A$Gm0B@o2vi|`GiQ=k5{xe5|K?MDAO4q6RG!o1DT=0?#}OKcM?4w zn0(FrSWy?z@C_i1eF~8{Jbq;nG~l`y-CNW~YePSPhPWwC)?d#3&<^=~wQX3@usJ;0 z088-jE*Kjxo!htA{1`Pf2system\n" + "You are a world class AI model who answers questions in JSON " + f"Here's the json schema you must adhere to:\n\n{schema}\n<|im_end|>\n" + "<|im_start|>user\n" + + user_prompt + + "<|im_end|>" + + "\n<|im_start|>assistant\n" + "" + ) +``` + +For a given user prompt, for example: + +```python +user_prompt = "Alice loves Bob and she hates Charlie." +``` + +We can use `generate.json` by passing the Pydantic class we previously defined, and call the generator with the Hermes prompt: + +```python +from outlines import generate, models + +model = models.LlamaCpp(llm) +generator = generate.json(model, KnowledgeGraph) +prompt = generate_hermes_prompt(user_prompt) +response = generator(prompt, max_tokens=1024, temperature=0, seed=42) +``` + +We obtain the nodes and edges of the knowledge graph: + +```python +print(response.nodes) +print(response.edges) +# [Node(id=1, label='Alice', property='Person'), +# Node(id=2, label='Bob', property='Person'), +# Node(id=3, label='Charlie', property='Person')] +# [Edge(source=1, target=2, label='love', property='Relationship'), +# Edge(source=1, target=3, label='hate', property='Relationship')] +``` + +## (Optional) Visualizing the Knowledge Graph + +We can use the [Graphviz library](https://graphviz.readthedocs.io/en/stable/) to visualize the generated knowledge graph. For detailed installation instructions, see [here](https://graphviz.readthedocs.io/en/stable/#installation). + +```python +from graphviz import Digraph + +dot = Digraph() +for node in response.nodes: + dot.node(str(node.id), node.label, shape='circle', width='1', height='1') +for edge in response.edges: + dot.edge(str(edge.source), str(edge.target), label=edge.label) + +dot.render('knowledge-graph.gv', view=True) +``` + +![Image of the Extracted Knowledge Graph](./images/knowledge-graph-extraction.png) + +This example was originally contributed by [Alonso Silva](https://github.com/alonsosilvaallende). diff --git a/mkdocs.yml b/mkdocs.yml index bc06c4993..7de4a6350 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,6 +107,7 @@ nav: - Playing chess: cookbook/models_playing_chess.md - Perspective-taking prompting: cookbook/simtom.md - Question-answering with citations: cookbook/qa-with-citations.md + - Knowledge Graph Extraction: cookbook/knowledge_graph_extraction.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From f6a6c297df8219f00bc7233a5a6e990d070ecae5 Mon Sep 17 00:00:00 2001 From: Alonso Silva Allende Date: Thu, 18 Jul 2024 15:22:30 +0200 Subject: [PATCH 020/505] Correct link and add llama-cpp-python installation instructions (#1051) - Correct link for llama-cpp-python - Add installation instructions for llama-cpp-python - Correct first question-answer --- docs/cookbook/qa-with-citations.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/cookbook/qa-with-citations.md b/docs/cookbook/qa-with-citations.md index b8e034a29..cb39befe9 100644 --- a/docs/cookbook/qa-with-citations.md +++ b/docs/cookbook/qa-with-citations.md @@ -1,11 +1,17 @@ # Generate Synthetic Data and Q&A with Citations -This tutorial is adapted from the [instructor-ollama notebook](https://github.com/alonsosilvaallende/Hermes-Function-Calling/blob/main/examples/instructor_ollama.ipynb). We start with a simple example to generate synthetic data and then we approach the problem of question answering by providing citations. We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](library). +This tutorial is adapted from the [instructor-ollama notebook](https://github.com/alonsosilvaallende/Hermes-Function-Calling/blob/main/examples/instructor_ollama.ipynb). We start with a simple example to generate synthetic data and then we approach the problem of question answering by providing citations. + +We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: + +```shell +pip install llama-cpp-python +``` We pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -```python -!wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +```shell +wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf ``` We initialize the model: @@ -16,15 +22,14 @@ from outlines import generate, models llm = Llama( "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", - n_gpu_layers=-1, tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( "NousResearch/Hermes-2-Pro-Llama-3-8B" ), + n_gpu_layers=-1, flash_attn=True, n_ctx=8192, verbose=False ) -model = models.LlamaCpp(llm) ``` ## Generate Synthetic Data @@ -53,6 +58,7 @@ class Users(BaseModel): We can use a `generate.json` by passing this Pydantic class we just defined, and call the generator: ```python +model = models.LlamaCpp(llm) generator = generate.json(model, Users) response = generator("Create 5 fake users", max_tokens=1024, temperature=0, seed=42) print(response.users) @@ -140,7 +146,7 @@ generator = generate.json(model, QuestionAnswer) prompt = generate_hermes_prompt(question, context) response = generator(prompt, max_tokens=1024, temperature=0, seed=42) print(response) -# QuestionAnswer(question='What did Jason Liu do during college?', answer='During college, Jason Liu studied Computational Mathematics and Physics. He also worked at companies such as Stitchfix and Facebook, and started the Data Science club at the University of Waterloo, serving as its president for two years.', citations=['I went to an arts high school but in university I studied Computational Mathematics and physics.', 'As part of coop I worked at many companies including Stitchfix, Facebook.', 'I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years.']) +# QuestionAnswer(question='What did the author do during college?', answer='The author studied Computational Mathematics and physics in university and was also involved in starting the Data Science club, serving as its president for 2 years.', citations=['I went to an arts high school but in university I studied Computational Mathematics and physics.', 'I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years.']) ``` We can do the same for a list of question-context pairs: @@ -226,3 +232,5 @@ for question, context in [ # 'Michael studied Game Design in high school.' # ['Michael Brown is a game developer from Tokyo, Japan. He attended a specialized high school where he studied Game Design.'] ``` + +This example was originally contributed by [Alonso Silva](https://github.com/alonsosilvaallende). From a7e3381a6aa6d4209f1f3aea8a048c1fb6ee10bc Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 19 Jul 2024 11:55:30 -0500 Subject: [PATCH 021/505] Introduce `outlines.models.transformers_vision` (#1052) Rendered Docs: https://github.com/lapp0/outlines/blob/multimodal-models/docs/reference/models/transformers_vision.md - Fixes https://github.com/outlines-dev/outlines/issues/787 - Fixes https://github.com/outlines-dev/outlines/issues/662 # Changes - Introduce `models.transformers_vision` which subclasses `models.transformers` and overrides its behavior so it applies, instead of `AutoTokenizer`, `AutoProcessor` to handle the text AND `PIL.Images` media - Introduce `VisionSequenceGeneratorAdapter`, handling and validating the `media` argument. - Update `outlines.generate` to dispatch `TransformersVision` models to `VisionSequenceGeneratorAdapter` # Tests - `tests/generate/test_api.py`: Test `prompt` / `media` validation - `tests/generate/test_generate.py`: - Add `model_transformers_vision` fixture. **tests pass locally, but disabled because a model small enough for CI isn't available** - Test all `outlines.generate` generators to ensure dispatchers for this new sequence generator is handled correctly. --- docs/reference/models/transformers_vision.md | 112 ++++++++++ outlines/generate/api.py | 109 +++++++++- outlines/generate/fsm.py | 17 +- outlines/generate/regex.py | 31 ++- outlines/generate/text.py | 20 +- outlines/models/__init__.py | 2 + outlines/models/transformers_vision.py | 139 ++++++++++++ pyproject.toml | 1 + tests/generate/test_api.py | 33 +++ tests/generate/test_generate.py | 211 +++++++++++++------ 10 files changed, 596 insertions(+), 79 deletions(-) create mode 100644 docs/reference/models/transformers_vision.md create mode 100644 outlines/models/transformers_vision.py create mode 100644 tests/generate/test_api.py diff --git a/docs/reference/models/transformers_vision.md b/docs/reference/models/transformers_vision.md new file mode 100644 index 000000000..b0ad6533b --- /dev/null +++ b/docs/reference/models/transformers_vision.md @@ -0,0 +1,112 @@ +# Transformers Vision + +Outlines allows seamless use of [vision models](https://huggingface.co/learn/computer-vision-course/en/unit4/multimodal-models/tasks-models-part1). + +`outlines.models.transformers_vision` has shares interfaces with, and is based on [`outlines.models.transformers`](./transformers.md). + +Tasks supported include +- image + text -> text +- video + text -> text + + + +## Example: Using [Llava-Next](https://huggingface.co/docs/transformers/en/model_doc/llava_next) Vision Models + +Install dependencies +`pip install torchvision pillow flash-attn` + +Create the model +```python +import outlines + +model = outlines.models.transformers_vision( + "llava-hf/llava-v1.6-mistral-7b-hf", + device="cuda", +) +``` + +Create convenience function to load a `PIL.Image` from URL +```python +from PIL import Image +from io import BytesIO +from urllib.request import urlopen + +def img_from_url(url): + img_byte_stream = BytesIO(urlopen(url).read()) + return Image.open(img_byte_stream).convert("RGB") +``` + +### Describing an image + +```python +description_generator = outlines.generate.text(model) +description_generator( + " detailed description:", + [img_from_url("https://upload.wikimedia.org/wikipedia/commons/2/25/Siam_lilacpoint.jpg")] +) +``` + +> This is a color photograph featuring a Siamese cat with striking blue eyes. The cat has a creamy coat and a light eye color, which is typical for the Siamese breed. Its features include elongated ears, a long, thin tail, and a striking coat pattern. The cat is sitting in an indoor setting, possibly on a cat tower or a similar raised platform, which is covered with a beige fabric, providing a comfortable and soft surface for the cat to rest or perch. The surface of the wall behind the cat appears to be a light-colored stucco or plaster. + +#### Multiple Images + +To include multiple images in your prompt you simply add more `` tokens to the prompt + +```python +image_urls = [ + "https://cdn1.byjus.com/wp-content/uploads/2020/08/ShapeArtboard-1-copy-3.png", # triangle + "https://cdn1.byjus.com/wp-content/uploads/2020/08/ShapeArtboard-1-copy-11.png", # hexagon +] +description_generator = outlines.generate.text(model) +description_generator( + "What shapes are present?", + list(map(img_from_url, image_urls)), +) +``` + +> There are two shapes present. One shape is a hexagon and the other shape is an triangle. ' + + +### Classifying an Image + +```python +pattern = "Mercury|Venus|Earth|Mars|Saturn|Jupiter|Neptune|Uranus|Pluto" +planet_generator = outlines.generate.regex(model, pattern) + +planet_generator( + "What planet is this: ", + [img_from_url("https://upload.wikimedia.org/wikipedia/commons/e/e3/Saturn_from_Cassini_Orbiter_%282004-10-06%29.jpg")] +) +``` + +> Saturn + + +### Extracting Structured Image data + +```python +from pydantic import BaseModel +from typing import List, Optional + +class ImageData(BaseModel): + caption: str + tags_list: List[str] + object_list: List[str] + is_photo: bool + +image_data_generator = outlines.generate.json(model, ImageData) + +image_data_generator( + " detailed JSON metadata:", + [img_from_url("https://upload.wikimedia.org/wikipedia/commons/9/98/Aldrin_Apollo_11_original.jpg")] +) +``` + +> `ImageData(caption='An astronaut on the moon', tags_list=['moon', 'space', 'nasa', 'americanflag'], object_list=['moon', 'moon_surface', 'space_suit', 'americanflag'], is_photo=True)` + + +## Resources + +### Chosing a model +- https://mmbench.opencompass.org.cn/leaderboard +- https://huggingface.co/spaces/WildVision/vision-arena diff --git a/outlines/generate/api.py b/outlines/generate/api.py index 4104e3080..ad01377c0 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -1,6 +1,6 @@ import datetime from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterator, List, Optional, Union +from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union from outlines.generate.generator import sequence_generator from outlines.samplers import BeamSearchSampler, GreedySampler, MultinomialSampler @@ -479,6 +479,13 @@ def format_sequence(self, sequence: str) -> FormattedOutput: """ return sequence + def _format(self, sequences): + """Apply formatting to every string in a completion.""" + if isinstance(sequences, list): + return [self._format(sequence) for sequence in sequences] + else: + return self.format_sequence(sequences) + def __call__( self, prompts: Union[str, List[str]], @@ -489,13 +496,6 @@ def __call__( ): """Generate text from a prompt of list of prompts.""" - def format(sequences): - """Apply formatting to every string in a completion.""" - if isinstance(sequences, list): - return [format(sequence) for sequence in sequences] - else: - return self.format_sequence(sequences) - generation_params = self.prepare_generation_parameters( max_tokens, stop_at, seed ) @@ -508,7 +508,7 @@ def format(sequences): **model_specific_params, ) - return format(completions) + return self._format(completions) def stream( self, @@ -529,3 +529,94 @@ def stream( self.sampling_params, **model_specific_params, ) + + +class VisionSequenceGeneratorAdapter(SequenceGeneratorAdapter): + def __call__( # type: ignore + self, + prompts: Union[str, List[str]], + media: Union[str, Any], + max_tokens: Optional[int] = None, + stop_at: Optional[Union[str, List[str]]] = None, + seed: Optional[int] = None, + **model_specific_params, + ): + """ + Generate text from a prompt of list of prompts. + + Media: A URI to construct media or media object itself. Used as AutoProcessor argument. + """ + prompts, media = self._validate_prompt_media_types(prompts, media) + + generation_params = self.prepare_generation_parameters( + max_tokens, stop_at, seed + ) + + completions = self.model.generate( + prompts, + media, + generation_params, + self.logits_processor, + self.sampling_params, + **model_specific_params, + ) + + return self._format(completions) + + def stream( # type: ignore + self, + prompts: Union[str, List[str]], + media: List[Union[str, Any, List[Union[str, Any]]]], + max_tokens: Optional[int] = None, + stop_at: Optional[Union[str, List[str]]] = None, + seed: Optional[int] = None, + **model_specific_params, + ): + """Return a text generator from a prompt or a list of prompts.""" + prompts, media = self._validate_prompt_media_types(prompts, media) + generation_params = self.prepare_generation_parameters( + max_tokens, stop_at, seed + ) + return self.model.stream( + prompts, + media, + generation_params, + self.logits_processor, + self.sampling_params, + **model_specific_params, + ) + + @classmethod + def _validate_prompt_media_types( + cls, + prompts: Union[str, List[str]], + media: Union[str, Any, List[Union[str, Any]]], + ) -> Union[Any, List[Any]]: + """ + Prepare media as PIL.Image and ensure for every prompt str there is one List[PIL.Image] + """ + + def valid_types(prompts, media): + from PIL import Image # type: ignore + + if isinstance(prompts, list): + if not isinstance(media, list) or len(prompts) != len(media): + return False + for subprompt, submedia in zip(prompts, media): + if not isinstance(subprompt, str) or not all( + isinstance(m, Image.Image) for m in submedia + ): + return False + elif isinstance(prompts, str): + if not all(isinstance(m, Image.Image) for m in media): + return False + return True + + if not valid_types(prompts, media): + raise TypeError( + "Expected (prompts, media) to be of type " + "(str, List[Image])), or (List[str], List[List[Image]]) " + f"instead got prompts={prompts}, media={media}" + ) + + return prompts, media diff --git a/outlines/generate/fsm.py b/outlines/generate/fsm.py index 832a154bd..47661b47f 100644 --- a/outlines/generate/fsm.py +++ b/outlines/generate/fsm.py @@ -3,8 +3,12 @@ import interegular from outlines.fsm.guide import RegexGuide -from outlines.generate.api import SequenceGenerator, SequenceGeneratorAdapter -from outlines.models import MLXLM, LlamaCpp, Transformers +from outlines.generate.api import ( + SequenceGenerator, + SequenceGeneratorAdapter, + VisionSequenceGeneratorAdapter, +) +from outlines.models import MLXLM, LlamaCpp, Transformers, TransformersVision from outlines.samplers import Sampler, multinomial @@ -29,3 +33,12 @@ def fsm_unified( fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) logits_processor = FSMLogitsProcessor(tokenizer=model.tokenizer, fsm=fsm) return SequenceGeneratorAdapter(model, logits_processor, sampler) + + +@fsm.register(TransformersVision) +def fsm_vision(model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial()): + from outlines.processors import FSMLogitsProcessor + + fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) + logits_processor = FSMLogitsProcessor(tokenizer=model.tokenizer, fsm=fsm) + return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) diff --git a/outlines/generate/regex.py b/outlines/generate/regex.py index 52b8c7dad..3aebcd429 100644 --- a/outlines/generate/regex.py +++ b/outlines/generate/regex.py @@ -1,12 +1,19 @@ from functools import singledispatch from outlines.fsm.guide import RegexGuide -from outlines.generate.api import SequenceGenerator, SequenceGeneratorAdapter -from outlines.models import OpenAI -from outlines.models.llamacpp import LlamaCpp -from outlines.models.mlxlm import MLXLM -from outlines.models.transformers import Transformers -from outlines.models.vllm import VLLM +from outlines.generate.api import ( + SequenceGenerator, + SequenceGeneratorAdapter, + VisionSequenceGeneratorAdapter, +) +from outlines.models import ( + MLXLM, + VLLM, + LlamaCpp, + OpenAI, + Transformers, + TransformersVision, +) from outlines.samplers import Sampler, multinomial @@ -53,6 +60,18 @@ def regex_unified( return SequenceGeneratorAdapter(model, logits_processor, sampler) +@regex.register(TransformersVision) +def regex_vision( + model, + regex_str: str, + sampler: Sampler = multinomial(), +): + from outlines.processors import RegexLogitsProcessor + + logits_processor = RegexLogitsProcessor(regex_str, tokenizer=model.tokenizer) + return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) + + @regex.register(VLLM) def regex_vllm( model: VLLM, diff --git a/outlines/generate/text.py b/outlines/generate/text.py index 6da187e0b..b0b4e10c7 100644 --- a/outlines/generate/text.py +++ b/outlines/generate/text.py @@ -1,8 +1,19 @@ from functools import singledispatch from outlines.fsm.guide import StopAtEOSGuide -from outlines.generate.api import SequenceGenerator, SequenceGeneratorAdapter -from outlines.models import MLXLM, VLLM, LlamaCpp, OpenAI, Transformers +from outlines.generate.api import ( + SequenceGenerator, + SequenceGeneratorAdapter, + VisionSequenceGeneratorAdapter, +) +from outlines.models import ( + MLXLM, + VLLM, + LlamaCpp, + OpenAI, + Transformers, + TransformersVision, +) from outlines.samplers import Sampler, multinomial @@ -43,6 +54,11 @@ def text_unified(model, sampler: Sampler = multinomial()): return SequenceGeneratorAdapter(model, None, sampler) +@text.register(TransformersVision) +def text_vision(model, sampler: Sampler = multinomial()): + return VisionSequenceGeneratorAdapter(model, None, sampler) + + @text.register(VLLM) def text_vllm(model: VLLM, sampler: Sampler = multinomial()): return SequenceGeneratorAdapter(model, None, sampler) diff --git a/outlines/models/__init__.py b/outlines/models/__init__.py index c161215d1..d28fcb2d7 100644 --- a/outlines/models/__init__.py +++ b/outlines/models/__init__.py @@ -5,6 +5,7 @@ codebase. """ + from typing import Union from .exllamav2 import ExLlamaV2Model, exl2 @@ -12,6 +13,7 @@ from .mlxlm import MLXLM, mlxlm from .openai import OpenAI, azure_openai, openai from .transformers import Transformers, TransformerTokenizer, mamba, transformers +from .transformers_vision import TransformersVision, transformers_vision from .vllm import VLLM, vllm LogitsGenerator = Union[Transformers, LlamaCpp, ExLlamaV2Model, MLXLM, VLLM] diff --git a/outlines/models/transformers_vision.py b/outlines/models/transformers_vision.py new file mode 100644 index 000000000..876f9bff5 --- /dev/null +++ b/outlines/models/transformers_vision.py @@ -0,0 +1,139 @@ +from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union + +from outlines.generate.api import GenerationParameters, SamplingParameters +from outlines.models import Transformers + +if TYPE_CHECKING: + from outlines.processors import OutlinesLogitsProcessor + + +class TransformersVision(Transformers): + def __init__(self, model, tokenizer, processor): + super().__init__(model, tokenizer) + self.processor = processor + + def generate( # type: ignore + self, + prompts: Union[str, List[str]], + media: Union[List[Any], List[List[Any]]], + generation_parameters: GenerationParameters, + logits_processor: Optional["OutlinesLogitsProcessor"], + sampling_parameters: SamplingParameters, + ) -> Union[str, List[str], List[List[str]]]: + """Generate text using `transformers`. + + Arguments + --------- + prompts + A prompt or list of prompts. + media + A List[PIL.Image] or List[List[PIL.Image]] + generation_parameters + An instance of `GenerationParameters` that contains the prompt, + the maximum number of tokens, stop sequences and seed. All the + arguments to `SequenceGeneratorAdapter`'s `__cal__` method. + logits_processor + The logits processor to use when generating text. + sampling_parameters + An instance of `SamplingParameters`, a dataclass that contains + the name of the sampler to use and related parameters as available + in Outlines. + + Returns + ------- + The generated text + """ + inputs = self.processor(prompts, media, padding=True, return_tensors="pt").to( + self.model.device + ) + + generation_kwargs = self._get_generation_kwargs( + prompts, + generation_parameters, + logits_processor, + sampling_parameters, + ) + generated_ids = self._generate_output_seq(prompts, inputs, **generation_kwargs) + + # if single str input and single sample per input, convert to a 1D output + if isinstance(prompts, str): + # Should always be true until NotImplementedError above is fixed + generated_ids = generated_ids.squeeze(0) + + return self._decode_generation(generated_ids) + + def stream( # type: ignore + self, + prompts: Union[str, List[str]], + media: Union[Any, List[Any]], # TODO: docstring + generation_parameters: GenerationParameters, + logits_processor: Optional["OutlinesLogitsProcessor"], + sampling_parameters: SamplingParameters, + ) -> Iterator[Union[str, List[str]]]: + raise NotImplementedError + + +def transformers_vision( + model_name: str, + device: Optional[str] = None, + model_kwargs: dict = {}, + processor_kwargs: dict = {}, + model_class=None, + tokenizer_class=None, + processor_class=None, +): + """Instantiate a model from the `transformers` library and its tokenizer. + + Parameters + ---------- + model_name + The name of the model as listed on Hugging Face's model page. + device + The device(s) on which the model should be loaded. This overrides + the `device_map` entry in `model_kwargs` when provided. + model_kwargs + A dictionary that contains the keyword arguments to pass to the + `from_pretrained` method when loading the model. + processor_kwargs + A dictionary that contains the keyword arguments to pass to the + `from_pretrained` method when loading the processor. + + Returns + ------- + A `TransformersModel` model instance. + + """ + if model_class is None or tokenizer_class is None: + try: + from transformers import ( + AutoTokenizer, + LlavaNextForConditionalGeneration, + LlavaNextProcessor, + ) + except ImportError: + raise ImportError( + "The `transformers` library needs to be installed in order to use `transformers` models." + ) + if model_class is None: + model_class = LlavaNextForConditionalGeneration + if processor_class is None: + processor_class = LlavaNextProcessor + + if device is not None: + model_kwargs["device_map"] = device + + model = model_class.from_pretrained(model_name, **model_kwargs) + + processor_kwargs.setdefault("padding_side", "left") + processor_kwargs.setdefault("pad_token", "[PAD]") + processor = processor_class.from_pretrained(model_name, **processor_kwargs) + + if tokenizer_class is None: + if getattr(processor, "tokenizer", None): + tokenizer = processor.tokenizer + else: + tokenizer = AutoTokenizer.from_pretrained(model_name, **processor_kwargs) + else: + tokenizer = tokenizer_class.from_pretrained(model_name, **processor_kwargs) + + return TransformersVision(model, tokenizer, processor) diff --git a/pyproject.toml b/pyproject.toml index aa88fcbbc..f94b3c84d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ test = [ "vllm; sys_platform != 'darwin'", "torch", "transformers", + "pillow", ] serve = [ "vllm>=0.3.0", diff --git a/tests/generate/test_api.py b/tests/generate/test_api.py new file mode 100644 index 000000000..7188022f5 --- /dev/null +++ b/tests/generate/test_api.py @@ -0,0 +1,33 @@ +from io import BytesIO +from urllib.request import urlopen + +import pytest +from PIL import Image # type: ignore + +from outlines.generate.api import VisionSequenceGeneratorAdapter + +IMG_URI = "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg" +PIL_IMG = Image.open(BytesIO(urlopen(IMG_URI).read())).convert("RGB") + + +@pytest.mark.parametrize( + "prompts,media,type_error", + [ + ("single prompt", [PIL_IMG], False), + (["prompt0", "prompt1"], [[PIL_IMG], [PIL_IMG]], False), + ("single prompt", [PIL_IMG, PIL_IMG], False), + (["prompt0", "prompt1"], [[PIL_IMG, PIL_IMG], [PIL_IMG]], False), + ("single prompt", "this isn't an image, it's a string", True), + ("single prompt", PIL_IMG, True), + (["prompt0", "prompt1"], [PIL_IMG], True), + (["prompt0", "prompt1"], [[PIL_IMG]], True), + (["prompt0", "prompt1"], [[[PIL_IMG]], [[PIL_IMG]]], True), + ], +) +def test_vision_sequence_generator_validate_types(prompts, media, type_error): + """Ensure inputs are validated correctly""" + if type_error: + with pytest.raises(TypeError): + VisionSequenceGeneratorAdapter._validate_prompt_media_types(prompts, media) + else: + VisionSequenceGeneratorAdapter._validate_prompt_media_types(prompts, media) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index 06d311f5b..b1202f396 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -7,6 +7,10 @@ import outlines.models as models import outlines.samplers as samplers +########################################## +# Model Fixtures +########################################## + @pytest.fixture(scope="session") def model_llamacpp(tmp_path_factory): @@ -50,6 +54,17 @@ def model_bart(tmp_path_factory): ) +@pytest.fixture(scope="session") +def model_transformers_vision(tmp_path_factory): + import torch + + return models.transformers_vision( + "llava-hf/llava-v1.6-mistral-7b-hf", + device="cuda", + model_kwargs=dict(torch_dtype=torch.bfloat16), + ) + + # TODO: exllamav2 failing in main, address in https://github.com/outlines-dev/outlines/issues/808 # TODO: t5 tokenizer doesn't work with streaming """ @@ -78,15 +93,42 @@ def model_t5(tmp_path_factory): "model_transformers_opt125m", "model_mamba", "model_bart", + # "model_transformers_vision", # tests pass, but awaiting a tiny model for CI ) -NOT_IMPLEMENTED = { - "stream": [], - "batch": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], - "beam_search": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], - "multiple_samples": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], -} +########################################## +# Stuctured Generation Inputs +########################################## + + +@pytest.fixture() +def sample_schema(): + from pydantic import BaseModel, conint, conlist, constr + + class SampleSchema(BaseModel): + title: constr(max_length=10) + numbers: conlist(conint(strict=True), min_length=3, max_length=3) + labels: conlist(constr(min_length=1, max_length=5), min_length=3, max_length=3) + + return SampleSchema + + +@pytest.fixture() +def sample_choices(): + return ["foo", "bar", "baz"] + + +REGEX_PATTERNS = [ + "a b c d e", # ensure proper tokenizer whitespace prefix handling + "(123456789)|(abcdefghijklmnop)", # ensure consistent correct sequence handling during batch + r"([a-z]{10})@([a-z]{5})\.([a-z]{3})", # email example +] + + +########################################### +# Model/Generator Pair Behavior Definitions +########################################### def enforce_not_implemented(model_fixture, *task_names): @@ -94,6 +136,12 @@ def enforce_not_implemented(model_fixture, *task_names): Per `NOT_IMPLEMENTED`, mapping, if a model hasn't implemented a task, assert an NotImplementedError is raised. Otherwise, run normally """ + NOT_IMPLEMENTED = { + "stream": ["model_transformers_vision"], + "batch": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], + "beam_search": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], + "multiple_samples": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], + } for task_name in task_names: if model_fixture in NOT_IMPLEMENTED.get(task_name, []): return pytest.raises(NotImplementedError) @@ -101,11 +149,35 @@ def enforce_not_implemented(model_fixture, *task_names): return contextlib.nullcontext() -REGEX_PATTERNS = [ - "a b c d e", # ensure proper tokenizer whitespace prefix handling - "(123456789)|(abcdefghijklmnop)", # ensure consistent correct sequence handling during batch - r"([a-z]{10})@([a-z]{5})\.([a-z]{3})", # email example -] +def get_inputs(fixture_name, batch_size=None): + """Get generator kwargs, just the prompt by default, but include images for transformers_visian""" + from io import BytesIO + from urllib.request import urlopen + + from PIL import Image # type: ignore + + prompts = ["abcd", "efgh", "1234", "5678", "foo", "bar", "baz", "bif"] + prompts = prompts[0] if batch_size is None else prompts[:batch_size] + + if fixture_name.endswith("_vision"): + img_url = "https://python-pillow.org/pillow-perf/static/space_pil_lanczos.png" + img = Image.open(BytesIO(urlopen(img_url).read())).convert("RGB") + + if batch_size is None: + return {"prompts": f" {prompts}", "media": [img]} + else: + return { + "prompts": [f" {p}" for p in prompts], + "media": [[img] for _ in range(batch_size)], + } + + else: + return {"prompts": prompts} + + +########################################### +# Tests +########################################### @pytest.mark.parametrize("sampler_name", ("greedy", "multinomial", "beam_search")) @@ -114,27 +186,17 @@ def test_generate_text(request, model_fixture, sampler_name): model = request.getfixturevalue(model_fixture) generator = generate.text(model, getattr(samplers, sampler_name)()) with enforce_not_implemented(model_fixture, sampler_name): - res = generator("test", max_tokens=10) + res = generator(**get_inputs(model_fixture), max_tokens=10) assert isinstance(res, str) +@pytest.mark.parametrize("pattern", REGEX_PATTERNS) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_batch_text(request, model_fixture): - model = request.getfixturevalue(model_fixture) - generator = generate.text(model) - with enforce_not_implemented(model_fixture, "batch"): - res = generator(["test", "test2"], max_tokens=10) - assert isinstance(res, list) - assert isinstance(res[0], str) - - -@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_text_stream(request, model_fixture): +def test_generate_regex(request, model_fixture, pattern): model = request.getfixturevalue(model_fixture) - generator = generate.text(model) - with enforce_not_implemented(model_fixture, "stream"): - for token in generator.stream("a b c ", max_tokens=10): - assert isinstance(token, str) + generator = generate.regex(model, pattern) + res = generator(**get_inputs(model_fixture), max_tokens=20) + assert re.fullmatch(pattern, res) is not None, res @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @@ -144,17 +206,44 @@ def test_generate_fsm(request, model_fixture, pattern): model = request.getfixturevalue(model_fixture) generator = generate.fsm(model, interegular.parse_pattern(pattern).to_fsm()) - res = generator("test") + res = generator(**get_inputs(model_fixture)) assert re.fullmatch(pattern, res) is not None, res -@pytest.mark.parametrize("pattern", REGEX_PATTERNS) +@pytest.mark.skip( + "Fix issues with JSON, some models fail this test https://github.com/outlines-dev/outlines/issues/985" +) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_regex(request, model_fixture, pattern): +def test_generate_json(request, model_fixture, sample_schema): model = request.getfixturevalue(model_fixture) - generator = generate.regex(model, pattern) - res = generator("foobarbaz", max_tokens=20) - assert re.fullmatch(pattern, res) is not None, res + generator = generate.json(model, sample_schema) + # asserts valid within call + generator(**get_inputs(model_fixture), max_tokens=100) + + +@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +def test_generate_choice(request, model_fixture, sample_choices): + model = request.getfixturevalue(model_fixture) + generator = generate.choice(model, sample_choices) + res = generator(**get_inputs(model_fixture)) + assert res in sample_choices + + +@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +def test_generate_format_bool(request, model_fixture): + model = request.getfixturevalue(model_fixture) + generator = generate.format(model, bool) + res = generator(**get_inputs(model_fixture)) + assert isinstance(res, bool) + + +@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +def test_generate_text_stream(request, model_fixture): + model = request.getfixturevalue(model_fixture) + generator = generate.text(model) + with enforce_not_implemented(model_fixture, "stream"): + for token in generator.stream(**get_inputs(model_fixture), max_tokens=10): + assert isinstance(token, str) @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @@ -164,23 +253,19 @@ def test_generate_regex_stream(request, model_fixture, pattern): generator = generate.regex(model, pattern) with enforce_not_implemented(model_fixture, "stream"): output = "" - for token in generator.stream("output:", max_tokens=20): + for token in generator.stream(**get_inputs(model_fixture), max_tokens=20): output += token assert re.fullmatch(pattern, output) is not None, output -@pytest.mark.parametrize("pattern", REGEX_PATTERNS) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_regex_batch_stream(request, model_fixture, pattern): +def test_generate_batch_text(request, model_fixture): model = request.getfixturevalue(model_fixture) - generator = generate.regex(model, pattern) - with enforce_not_implemented(model_fixture, "batch", "stream"): - outputs = ["", ""] - for tokens in generator.stream(["input 0", "input 1"], max_tokens=20): - outputs[0] += tokens[0] - outputs[1] += tokens[1] - for output in outputs: - assert re.fullmatch(pattern, output) is not None, output + generator = generate.text(model) + with enforce_not_implemented(model_fixture, "batch"): + res = generator(**get_inputs(model_fixture, 2), max_tokens=10) + assert isinstance(res, list) + assert isinstance(res[0], str) @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @@ -190,44 +275,50 @@ def test_generate_regex_batch(request, model_fixture, pattern): model = request.getfixturevalue(model_fixture) generator = generate.regex(model, pattern) with enforce_not_implemented(model_fixture, "batch"): - outputs = generator(["abc", "123", "123bce", "33aa"], max_tokens=20) + outputs = generator(**get_inputs(model_fixture, 4), max_tokens=20) for output in outputs: assert re.fullmatch(pattern, output) is not None, output @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_regex_single_multinomial(request, model_fixture, pattern): - """Ensure batch requests work and fsm order is maintained""" +def test_generate_regex_batch_stream(request, model_fixture, pattern): model = request.getfixturevalue(model_fixture) - generator = generate.regex(model, pattern, sampler=samplers.multinomial(4)) - with enforce_not_implemented(model_fixture, "multiple_samples"): - output_sample_groups = generator("single input", max_tokens=40) - for output in output_sample_groups: + generator = generate.regex(model, pattern) + with enforce_not_implemented(model_fixture, "batch", "stream"): + outputs = ["", ""] + for tokens in generator.stream(**get_inputs(model_fixture, 2), max_tokens=20): + outputs[0] += tokens[0] + outputs[1] += tokens[1] + for output in outputs: assert re.fullmatch(pattern, output) is not None, output @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_regex_batch_multinomial(request, model_fixture, pattern): +def test_generate_regex_single_multinomial(request, model_fixture, pattern): """Ensure batch requests work and fsm order is maintained""" model = request.getfixturevalue(model_fixture) generator = generate.regex(model, pattern, sampler=samplers.multinomial(4)) - with enforce_not_implemented(model_fixture, "batch", "multiple_samples"): - output_batch_groups = generator(["abc", "123", "123bce", "33aa"], max_tokens=40) - for output_sample_groups in output_batch_groups: - for output in output_sample_groups: - assert re.fullmatch(pattern, output) is not None, output + with enforce_not_implemented(model_fixture, "multiple_samples"): + output_sample_groups = generator(**get_inputs(model_fixture), max_tokens=40) + for output in output_sample_groups: + assert re.fullmatch(pattern, output) is not None, output @pytest.mark.parametrize("pattern", REGEX_PATTERNS) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) -def test_generate_regex_batch_beam_search(request, model_fixture, pattern): +@pytest.mark.parametrize("sampler_name", ("multinomial", "beam_search")) +def test_generate_regex_batch_multi_sample( + request, model_fixture, pattern, sampler_name +): """Ensure batch requests work and fsm order is maintained""" model = request.getfixturevalue(model_fixture) - generator = generate.regex(model, pattern, sampler=samplers.beam_search(4)) + generator = generate.regex( + model, pattern, sampler=getattr(samplers, sampler_name)(4) + ) with enforce_not_implemented(model_fixture, "batch", "multiple_samples"): - output_batch_groups = generator(["abc", "123", "123bce", "33aa"], max_tokens=40) + output_batch_groups = generator(**get_inputs(model_fixture, 4), max_tokens=40) for output_sample_groups in output_batch_groups: for output in output_sample_groups: assert re.fullmatch(pattern, output) is not None, output From 47dfa4bd530a333bce0978f930cc9eb006a1a589 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 20 Jul 2024 03:44:38 -0500 Subject: [PATCH 022/505] Use outlines.processors for vLLM --- outlines/generate/choice.py | 4 +- outlines/generate/format.py | 6 ++- outlines/generate/fsm.py | 24 +++++------ outlines/generate/json.py | 4 +- outlines/generate/regex.py | 43 +++++--------------- outlines/generate/text.py | 34 ++++------------ outlines/models/vllm.py | 22 ++++++++++ outlines/processors/base_logits_processor.py | 20 ++++----- outlines/processors/structured.py | 27 +++++------- tests/generate/conftest.py | 26 +++++++++++- tests/generate/test_generate.py | 16 ++++++-- 11 files changed, 119 insertions(+), 107 deletions(-) diff --git a/outlines/generate/choice.py b/outlines/generate/choice.py index 6718f26b2..931409a59 100644 --- a/outlines/generate/choice.py +++ b/outlines/generate/choice.py @@ -1,7 +1,7 @@ from functools import singledispatch from typing import Callable, List -from outlines.generate.api import SequenceGenerator +from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial @@ -11,7 +11,7 @@ @singledispatch def choice( model, choices: List[str], sampler: Sampler = multinomial() -) -> SequenceGenerator: +) -> SequenceGeneratorAdapter: regex_str = r"(" + r"|".join(choices) + r")" generator = regex(model, regex_str, sampler) diff --git a/outlines/generate/format.py b/outlines/generate/format.py index d7b7e164d..88acec75f 100644 --- a/outlines/generate/format.py +++ b/outlines/generate/format.py @@ -1,7 +1,7 @@ from functools import singledispatch from outlines.fsm.types import python_types_to_regex -from outlines.generate.api import SequenceGenerator +from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial @@ -9,7 +9,9 @@ @singledispatch -def format(model, python_type, sampler: Sampler = multinomial()) -> SequenceGenerator: +def format( + model, python_type, sampler: Sampler = multinomial() +) -> SequenceGeneratorAdapter: """Generate structured data that can be parsed as a Python type. Parameters diff --git a/outlines/generate/fsm.py b/outlines/generate/fsm.py index 47661b47f..03fe512b9 100644 --- a/outlines/generate/fsm.py +++ b/outlines/generate/fsm.py @@ -8,25 +8,13 @@ SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import MLXLM, LlamaCpp, Transformers, TransformersVision +from outlines.models import ExLlamaV2Model, TransformersVision from outlines.samplers import Sampler, multinomial @singledispatch def fsm( model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() -) -> SequenceGenerator: - fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) - device = model.device - generator = SequenceGenerator(fsm, model, sampler, device) - return generator - - -@fsm.register(MLXLM) -@fsm.register(Transformers) -@fsm.register(LlamaCpp) -def fsm_unified( - model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() ) -> SequenceGeneratorAdapter: from outlines.processors import FSMLogitsProcessor @@ -42,3 +30,13 @@ def fsm_vision(model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) logits_processor = FSMLogitsProcessor(tokenizer=model.tokenizer, fsm=fsm) return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) + + +@fsm.register(ExLlamaV2Model) +def fsm_exllamav2( + model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() +) -> SequenceGenerator: + fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) + device = model.device + generator = SequenceGenerator(fsm, model, sampler, device) + return generator diff --git a/outlines/generate/json.py b/outlines/generate/json.py index 3837f72b6..6209840e2 100644 --- a/outlines/generate/json.py +++ b/outlines/generate/json.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from outlines.fsm.json_schema import build_regex_from_schema, get_schema_from_signature -from outlines.generate.api import SequenceGenerator +from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial @@ -18,7 +18,7 @@ def json( schema_object: Union[str, object, Callable], sampler: Sampler = multinomial(), whitespace_pattern: Optional[str] = None, -) -> SequenceGenerator: +) -> SequenceGeneratorAdapter: """ Generate structured JSON data with a `Transformer` model based on a specified JSON Schema. diff --git a/outlines/generate/regex.py b/outlines/generate/regex.py index 3aebcd429..815a8b1b9 100644 --- a/outlines/generate/regex.py +++ b/outlines/generate/regex.py @@ -6,14 +6,7 @@ SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import ( - MLXLM, - VLLM, - LlamaCpp, - OpenAI, - Transformers, - TransformersVision, -) +from outlines.models import ExLlamaV2Model, OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial @@ -34,26 +27,10 @@ def regex(model, regex_str: str, sampler: Sampler = multinomial()): Returns ------- - A `SequenceGenerator` instance that generates text constrained by the + A `SequenceGeneratorAdapter` instance that generates text constrained by the regular expression. """ - fsm = RegexGuide(regex_str, model.tokenizer) - - device = model.device - generator = SequenceGenerator(fsm, model, sampler, device) - - return generator - - -@regex.register(MLXLM) -@regex.register(Transformers) -@regex.register(LlamaCpp) -def regex_unified( - model, - regex_str: str, - sampler: Sampler = multinomial(), -): from outlines.processors import RegexLogitsProcessor logits_processor = RegexLogitsProcessor(regex_str, tokenizer=model.tokenizer) @@ -72,16 +49,18 @@ def regex_vision( return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) -@regex.register(VLLM) -def regex_vllm( - model: VLLM, +@regex.register(ExLlamaV2Model) +def regex_exllamav2( + model, regex_str: str, sampler: Sampler = multinomial(), -): - from outlines.integrations.vllm import RegexLogitsProcessor +) -> SequenceGenerator: + fsm = RegexGuide(regex_str, model.tokenizer) - logits_processor = RegexLogitsProcessor(regex_str, model.model) - return SequenceGeneratorAdapter(model, logits_processor, sampler) + device = model.device + generator = SequenceGenerator(fsm, model, sampler, device) + + return generator @regex.register(OpenAI) diff --git a/outlines/generate/text.py b/outlines/generate/text.py index b0b4e10c7..3fe3dc553 100644 --- a/outlines/generate/text.py +++ b/outlines/generate/text.py @@ -6,19 +6,12 @@ SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import ( - MLXLM, - VLLM, - LlamaCpp, - OpenAI, - Transformers, - TransformersVision, -) +from outlines.models import ExLlamaV2Model, OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial @singledispatch -def text(model, sampler: Sampler = multinomial()) -> SequenceGenerator: +def text(model, sampler: Sampler = multinomial()) -> SequenceGeneratorAdapter: """Generate text with a `Transformer` model. Note @@ -37,21 +30,17 @@ def text(model, sampler: Sampler = multinomial()) -> SequenceGenerator: Returns ------- - A `SequenceGenerator` instance that generates text. + A `SequenceGeneratorAdapter` instance that generates text. """ - fsm = StopAtEOSGuide(model.tokenizer) - device = model.device - generator = SequenceGenerator(fsm, model, sampler, device) - - return generator + return SequenceGeneratorAdapter(model, None, sampler) -@text.register(MLXLM) -@text.register(Transformers) -@text.register(LlamaCpp) -def text_unified(model, sampler: Sampler = multinomial()): - return SequenceGeneratorAdapter(model, None, sampler) +@text.register(ExLlamaV2Model) +def text_exllamav2(model, sampler: Sampler = multinomial()) -> SequenceGenerator: + fsm = StopAtEOSGuide(model.tokenizer) + device = model.device + return SequenceGenerator(fsm, model, sampler, device) @text.register(TransformersVision) @@ -59,11 +48,6 @@ def text_vision(model, sampler: Sampler = multinomial()): return VisionSequenceGeneratorAdapter(model, None, sampler) -@text.register(VLLM) -def text_vllm(model: VLLM, sampler: Sampler = multinomial()): - return SequenceGeneratorAdapter(model, None, sampler) - - @text.register(OpenAI) def text_openai(model: OpenAI, sampler: Sampler = multinomial()) -> OpenAI: if not isinstance(sampler, multinomial): diff --git a/outlines/models/vllm.py b/outlines/models/vllm.py index 7c1f5ad7c..2ae3d99d4 100644 --- a/outlines/models/vllm.py +++ b/outlines/models/vllm.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, List, Optional, Union from outlines.generate.api import GenerationParameters, SamplingParameters +from outlines.integrations.utils import adapt_tokenizer if TYPE_CHECKING: from vllm import LLM @@ -22,6 +23,23 @@ def __init__(self, model: "LLM"): self.model = model self.lora_request = None + self.tokenizer = self._get_tokenizer() + + def _get_tokenizer(self): + if hasattr(self.model, "get_tokenizer"): + tokenizer = self.model.get_tokenizer() + elif hasattr(self.model, "tokenizer"): + if hasattr(self.model.tokenizer, "tokenizer"): + tokenizer = self.model.tokenizer.tokenizer + else: + tokenizer = self.model.tokenizer + else: + raise ValueError( + "The provided LLM instance neither has a " + "`tokenizer` attribute or a `get_tokenizer` method." + ) + return adapt_tokenizer(tokenizer=tokenizer) + def generate( self, prompts: Union[str, List[str]], @@ -100,6 +118,10 @@ def generate( sampling_params.top_p = top_p if top_k is not None and sampling_params.top_k == -1: sampling_params.top_k = top_k + # TODO: remove this if statement once fixed + # https://github.com/vllm-project/vllm/issues/5404#issuecomment-2175972897 + if top_k == 1: + sampling_params.repetition_penalty = 0 if temperature is not None and sampling_params.temperature == 1.0: sampling_params.temperature = temperature if sampler == "beam_search": diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index 1d141a2f0..feedf5253 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -67,22 +67,18 @@ def __call__( 3) Call self.process_logits() to perform business logic 4) Cast logits back to original array library type """ - # ensure logits are torch Tensors torch_logits = self._to_torch(logits) + input_ids = self._to_torch(input_ids) - assert torch_logits.shape[:-1] == self._to_torch(input_ids).shape[:-1] - - # ensure input_ids are List - if not isinstance(input_ids, list): - input_ids = input_ids.tolist() # compatible with numpy, torch, and mlx + assert torch_logits.shape[:-1] == input_ids.shape[:-1] # Guarantee passed as 2D Tensors, then covert back to original (1D or 2D) shape if len(torch_logits.shape) == 2: - processed_logits = self.process_logits(input_ids, torch_logits) + processed_logits = self.process_logits(input_ids.tolist(), torch_logits) elif len(torch_logits.shape) == 1: processed_logits = self.process_logits( - [input_ids], torch_logits.unsqueeze(0) + [input_ids.tolist()], torch_logits.unsqueeze(0) ).squeeze(0) # return logits as passed array type @@ -97,7 +93,7 @@ def _to_torch(tensor_like: Array) -> torch.Tensor: elif isinstance(tensor_like, np.ndarray): return torch.from_numpy(tensor_like) - elif isinstance(tensor_like, list): + elif isinstance(tensor_like, (list, tuple)): return torch.tensor(tensor_like) elif is_mlx_array_type(type(tensor_like)): @@ -108,7 +104,8 @@ def _to_torch(tensor_like: Array) -> torch.Tensor: else: raise TypeError( "LogitsProcessor must be called with either np.NDArray, " - "torch.Tensor, list, or mlx.core.array typed logits" + "torch.Tensor, list, or mlx.core.array typed logits. " + f"Logits type: `{type(tensor_like)}`" ) @staticmethod @@ -123,6 +120,9 @@ def _from_torch(tensor: torch.Tensor, target_type: Type) -> Array: elif target_type == list: return tensor.detach().tolist() + elif target_type == tuple: + return tuple(tensor.detach().tolist()) + elif is_mlx_array_type(target_type): import mlx.core as mx diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index d037c679f..0966a90db 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -61,9 +61,8 @@ def __init__(self, tokenizer: "Tokenizer", fsm: Guide): The finite state machine which is used to bias the logits. """ self.tokenizer = tokenizer - self._fsm_states: Dict[int, int] = {} + self._fsm_states: Dict[int, int] = {hash(tuple([])): 0} self.fsm: Guide = fsm - self._is_first_token = True self._seq_start_idx: Optional[int] = None def process_logits( @@ -83,25 +82,21 @@ def process_logits( torch.Tensor The biased logits. """ - sequence_states: List[int] = [] # vector of states corresponding to `input_ids` - - if self._is_first_token: - self._is_first_token = False + if self._seq_start_idx is None: self._seq_start_idx = len(input_ids[0]) - self._fsm_states = {hash(tuple([])): 0} - sequence_states = [0] * len(input_ids) - - else: - for seq_ids in input_ids: - prev_state_key = hash(tuple(seq_ids[self._seq_start_idx : -1])) - prev_state = self._fsm_states[prev_state_key] + sequence_states: List[int] = [] # vector of states corresponding to `input_ids` - curr_state_key = hash(tuple(seq_ids[self._seq_start_idx :])) - curr_state = self.fsm.get_next_state(prev_state, seq_ids[-1]) + for seq_ids in input_ids: + gen_ids = seq_ids[self._seq_start_idx :] + curr_state_key = hash(tuple(gen_ids)) + if curr_state_key not in self._fsm_states: + prev_state = self._fsm_states[hash(tuple(gen_ids[:-1]))] + curr_state = self.fsm.get_next_state(prev_state, gen_ids[-1]) self._fsm_states[curr_state_key] = curr_state - sequence_states.append(curr_state) + + sequence_states.append(self._fsm_states[curr_state_key]) mask = torch.full_like(logits, -math.inf) for i, fsm_state in enumerate(sequence_states): diff --git a/tests/generate/conftest.py b/tests/generate/conftest.py index fe6d1e1a7..ed8830119 100644 --- a/tests/generate/conftest.py +++ b/tests/generate/conftest.py @@ -1,16 +1,38 @@ from importlib import reload import pytest +import torch -def pytest_collection_modifyitems(config, items): - """If mlxlm and Metal aren't available, skip mlxlm tests""" +def is_metal_available(): try: import mlx.core as mx import mlx_lm # noqa: F401 assert mx.metal.is_available() except (ImportError, AssertionError): + return False + return True + + +def pytest_collection_modifyitems(config, items): + """ + If mlxlm and Metal aren't available, skip mlxlm tests + If CUDA isn't available, skip vllm and transformers_vision + """ + if not torch.cuda.is_available(): + skip_marker = pytest.mark.skip( + reason="Skipping test because CUDA is not available" + ) + for item in items: + if "model_fixture" in item.fixturenames: + model_param = item.callspec.params.get("model_fixture", None) + if model_param.startswith( + "model_transformers_vision" + ) or model_param.startswith("model_vllm"): + item.add_marker(skip_marker) + + if not is_metal_available(): skip_marker = pytest.mark.skip( reason="Skipping test because mlx-lm or Metal are not available" ) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index b1202f396..a86d3c253 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -61,10 +61,19 @@ def model_transformers_vision(tmp_path_factory): return models.transformers_vision( "llava-hf/llava-v1.6-mistral-7b-hf", device="cuda", - model_kwargs=dict(torch_dtype=torch.bfloat16), + model_kwargs=dict( + torch_dtype=torch.bfloat16, + load_in_4bit=True, + low_cpu_mem_usage=True, + ), ) +@pytest.fixture(scope="session") +def model_vllm(tmp_path_factory): + return models.vllm("facebook/opt-125m", gpu_memory_utilization=0.1) + + # TODO: exllamav2 failing in main, address in https://github.com/outlines-dev/outlines/issues/808 # TODO: t5 tokenizer doesn't work with streaming """ @@ -93,7 +102,8 @@ def model_t5(tmp_path_factory): "model_transformers_opt125m", "model_mamba", "model_bart", - # "model_transformers_vision", # tests pass, but awaiting a tiny model for CI + "model_transformers_vision", + "model_vllm", ) @@ -137,7 +147,7 @@ def enforce_not_implemented(model_fixture, *task_names): assert an NotImplementedError is raised. Otherwise, run normally """ NOT_IMPLEMENTED = { - "stream": ["model_transformers_vision"], + "stream": ["model_transformers_vision", "model_vllm"], "batch": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], "beam_search": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], "multiple_samples": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], From 42c55bc0db821254cfac76a1e540d80e2642746e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lestin=20Matte?= Date: Sat, 20 Jul 2024 18:14:48 +0200 Subject: [PATCH 023/505] Fix deprecated memory= parameter (#1057) memory= parameter is deprecated in favor of size= See https://modal.com/docs/reference/changelog#062174-2024-05-17 Current doc example produces the following error: ``` /path/test_modal.py:56: DeprecationError: 2024-05-16: The `memory` parameter is deprecated. Use the `size='80GB'` parameter instead. @app.function(image=outlines_image, gpu=gpu.A100(memory=80)) ``` --- docs/cookbook/deploy-using-modal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/deploy-using-modal.md b/docs/cookbook/deploy-using-modal.md index 8da39a3f5..d4058bbee 100644 --- a/docs/cookbook/deploy-using-modal.md +++ b/docs/cookbook/deploy-using-modal.md @@ -73,7 +73,7 @@ schema = """{ To make the inference work on Modal we need to wrap the corresponding function in a `@app.function` decorator. We pass to this decorator the image and GPU on which we want this function to run (here an A100 with 80GB memory): ```python -@app.function(image=outlines_image, gpu=gpu.A100(memory=80)) +@app.function(image=outlines_image, gpu=gpu.A100(size='80GB')) def generate( prompt: str = "Amiri, a 53 year old warrior woman with a sword and leather armor.", ): From abe689c1a2723ee797974e32e545dadabb872734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lestin=20Matte?= Date: Sat, 20 Jul 2024 18:49:39 +0200 Subject: [PATCH 024/505] Fix failing Modal example (#1058) It seems modal deletes environment variables, which makes outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") fail even after login. This workaround instructs user to manually add a key before importing the model. Fixes #1024 --- docs/cookbook/deploy-using-modal.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/cookbook/deploy-using-modal.md b/docs/cookbook/deploy-using-modal.md index d4058bbee..835924d2b 100644 --- a/docs/cookbook/deploy-using-modal.md +++ b/docs/cookbook/deploy-using-modal.md @@ -6,7 +6,7 @@ In this guide we will show you how you can use Modal to run programs written wit ## Build the image -First we need to define our container image. We download the Mistral-7B-v0.1 model from HuggingFace as part of the definition of the image so it only needs to be done once. +First we need to define our container image. We download the Mistral-7B-v0.1 model from HuggingFace as part of the definition of the image so it only needs to be done once (you need to provide an [access token](https://huggingface.co/settings/tokens)) ```python from modal import Image, App, gpu @@ -21,6 +21,8 @@ outlines_image = Image.debian_slim(python_version="3.11").pip_install( ) def import_model(): + import os + os.environ["HF_TOKEN"] = "YOUR_HUGGINGFACE_TOKEN" import outlines outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") From fcfef33c84f519af5aa24b1ca52c9908fd97d101 Mon Sep 17 00:00:00 2001 From: Alonso Silva Allende Date: Mon, 22 Jul 2024 17:39:21 +0200 Subject: [PATCH 025/505] Add links to the two examples (#1062) Add links to the two examples: - Q&A with Citations - Knowledge Graph Generation --- docs/cookbook/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 5e84cabab..24849d5ee 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -6,3 +6,5 @@ - [Chain Of Density](chain_of_density.md): Summarize documents using chain of density prompting and JSON-structured generation. - [Playing Chess](models_playing_chess.md): Make Mistral-7B play chess against itself using regex-structured generation. - [SimToM](simtom.md): Improve LLMs' Theory of Mind capabilities with perspective-taking prompting and JSON-structured generation. +- [Q&A with Citations](qa-with-citations.md): Answer questions and provide citations using JSON-structured generation. +- [Knowledge Graph Generation](knowledge_graph_extraction.md): Generate a Knowledge Graph from unstructured text using JSON-structured generation. From d78041e81f355ad9f83174cc8ac879443df95e76 Mon Sep 17 00:00:00 2001 From: Franz Louis Cesista Date: Tue, 23 Jul 2024 15:48:05 +0800 Subject: [PATCH 026/505] add link to MMSG --- docs/community/examples.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community/examples.md b/docs/community/examples.md index e606ca019..2ebaf2765 100644 --- a/docs/community/examples.md +++ b/docs/community/examples.md @@ -2,6 +2,8 @@ Publishing examples and articles about Outlines are a meaningful way to contrinute to the community. Here is a list of projects we are aware of. Drop us a line if we forgot yours! +[MMSG](https://github.com/leloykun/mmsg) is a Python library for generating interleaved text and image content in a structured format you can directly pass to downstream APIs. + [Multimodal Structured Generation: CVPR's 2nd MMFM Challenge Technical Report](https://arxiv.org/abs/2406.11403) shows that Structured Generation can outperform finetuning, and maybe even multimodality, in document-image understanding tasks as part of CVPR's 2nd MMFM Challenge. [Chess LLM Arena](https://huggingface.co/spaces/mlabonne/chessllm) is a HuggingFace Space where you can make LLMs compete in a chess match. From 6e026a7b35bf717fbe67565d934b1d2f74a5d438 Mon Sep 17 00:00:00 2001 From: Alonso Silva Allende Date: Sat, 27 Jul 2024 11:04:27 +0200 Subject: [PATCH 027/505] Correct documentation to disable caching --- outlines/caching.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outlines/caching.py b/outlines/caching.py index 95392c7e8..6fdda6214 100644 --- a/outlines/caching.py +++ b/outlines/caching.py @@ -153,8 +153,8 @@ def disable_cache(): `outlines.cache.disable` should be called right after importing outlines: - >>> import outlines.cache as cache - >>> cache.disable() + >>> import outlines.caching as cache + >>> cache.disable_cache() """ global _caching_enabled From bb927455112c976e3a777338057534d2dddd5171 Mon Sep 17 00:00:00 2001 From: ispobock Date: Sat, 27 Jul 2024 11:14:10 +0800 Subject: [PATCH 028/505] fix pad token id reset --- outlines/models/transformers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index 1d3be511a..7ecc9013f 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -69,7 +69,7 @@ def __init__(self, tokenizer: "PreTrainedTokenizer", **kwargs): self.eos_token_id = self.tokenizer.eos_token_id self.eos_token = self.tokenizer.eos_token - if not self.tokenizer.pad_token_id: + if self.tokenizer.pad_token_id is None: self.tokenizer.pad_token_id = self.tokenizer.eos_token_id self.pad_token_id = self.eos_token_id else: From 26e293445cbfe00b782cc83e47cb50124acaddb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 29 Jul 2024 16:56:12 +0200 Subject: [PATCH 029/505] Fix link to `mamba` model reference --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 7de4a6350..d69d1e718 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -138,7 +138,7 @@ nav: - TGI: reference/models/tgi.md - ExllamaV2: reference/models/exllamav2.md - MLX: reference/models/mlxlm.md - - Mamba: reference/models/mamba.md + - Mamba: reference/models/transformers.md - API: - OpenAI: reference/models/openai.md - API Reference: From 951b0204bcf555a7734b4083568849f9fba52888 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 29 Jul 2024 17:08:59 -0500 Subject: [PATCH 030/505] More detailed mlxlm documentation --- docs/reference/models/mlxlm.md | 69 +++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/reference/models/mlxlm.md b/docs/reference/models/mlxlm.md index e738abdce..cf7bb7443 100644 --- a/docs/reference/models/mlxlm.md +++ b/docs/reference/models/mlxlm.md @@ -2,24 +2,83 @@ Outlines provides an integration with [mlx-lm](https://github.com/ml-explore/mlx-examples/tree/main/llms), allowing models to be run quickly on Apple Silicon via the [mlx](https://ml-explore.github.io/mlx/build/html/index.html) library. -## Installation +!!! Note "Installation" -In addition to `outlines`, you must install `mlx-lm` and `mlx` libraries. You must use a device which [supports Metal](https://support.apple.com/en-us/102894). + You need to install the `mlx` and `mlx-lm` libraries on a device which [supports Metal](https://support.apple.com/en-us/102894) to use the mlx-lm integration. -## Using `models.mlxlm` + +## Load the model + +You can initialize the model by passing the name of the repository on the HuggingFace Hub. The official repository for mlx-lm supported models is [mlx-community](https://huggingface.co/mlx-community). ```python from outlines import models -model = models.mlxlm("mlx-community/Meta-Llama-3-8B-Instruct-8bit") +model = models.mlxlm("mlx-community/Meta-Llama-3.1-8B-Instruct-8bit") +``` + +This will download the model files to the hub cache folder and load the weights in memory. + +The arguments `model_config` and `tokenizer_config` are available to modify loading behavior. For example, per the `mlx-lm` [documentation](https://github.com/ml-explore/mlx-examples/tree/main/llms#supported-models), you must set an eos_token for `qwen/Qwen-7B`. In outlines you may do so via + ``` +model = models.mlxlm( + "mlx-community/Meta-Llama-3.1-8B-Instruct-8bit", + tokenizer_config={"eos_token": "<|endoftext|>", "trust_remote_code": True}, +) +``` + +**Main parameters:** + +(Subject to change. Table based on [mlx-lm.load docstring](https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/utils.py#L429)) + +| Parameters | Type | Description | Default | +|--------------------|--------|--------------------------------------------------------------------------------------------------|---------| +| `tokenizer_config` | `dict` | Configuration parameters specifically for the tokenizer. Defaults to an empty dictionary. | `{}` | +| `model_config` | `dict` | Configuration parameters specifically for the model. Defaults to an empty dictionary. | `{}` | +| `adapter_path` | `str` | Path to the LoRA adapters. If provided, applies LoRA layers to the model. | `None` | +| `lazy` | `bool` | If False, evaluate the model parameters to make sure they are loaded in memory before returning. | `False` | + + +## Generate text + +You may generate text using the parameters described in the [text generation documentation](../text.md). With the loaded model, you can generate text or perform structured generation, e.g. ```python from outlines import models, generate -model = models.mlxlm("mlx-community/Meta-Llama-3-8B-Instruct-8bit") +model = models.mlxlm("mlx-community/Meta-Llama-3.1-8B-Instruct-8bit") +generator = generate.text(model) + +answer = generator("A prompt", temperature=2.0) +``` + +## Streaming + +You may creating a streaming iterable with minimal changes + +```python +from outlines import models, generate + +model = models.mlxlm("mlx-community/Meta-Llama-3.1-8B-Instruct-8bit") +generator = generate.text(model) + +for token_str in generator.text("A prompt", temperature=2.0): + print(token_str) +``` + +## Structured + +You may perform structured generation with mlxlm to guarantee your output will match a regex pattern, json schema, or lark grammar. + +Example: Phone number generation with pattern `"\\+?[1-9][0-9]{7,14}"`: + +```python +from outlines import models, generate + +model = models.mlxlm("mlx-community/Meta-Llama-3.1-8B-Instruct-8bit") phone_number_pattern = "\\+?[1-9][0-9]{7,14}" generator = generate.regex(model, phone_number_pattern) From e3247fe633ca5c958c6d65be6803ace772547f72 Mon Sep 17 00:00:00 2001 From: Alonso Silva Date: Thu, 8 Aug 2024 17:28:20 +0200 Subject: [PATCH 031/505] Add chain of thought example --- docs/cookbook/chain_of_thought.md | 123 ++++++++++++++++++++++++++++++ docs/cookbook/index.md | 1 + mkdocs.yml | 1 + 3 files changed, 125 insertions(+) create mode 100644 docs/cookbook/chain_of_thought.md diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md new file mode 100644 index 000000000..2ab68ff7a --- /dev/null +++ b/docs/cookbook/chain_of_thought.md @@ -0,0 +1,123 @@ +# Chain of thought + + +Chain of thought is a prompting technique introduced in the paper [``Chain-of-Thought Prompting Elicits Reasoning in Large Language Models''](https://arxiv.org/abs/2201.11903) where throught prompting the authors generate a series of intermediate reasoning steps which improves the ability of LLMs to perform complex reasoning. + +In this guide, we use [outlines](https://outlines-dev.github.io/outlines/) to apply chain of thought through structured output. + +We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: + +```shell +pip install llama-cpp-python +``` + +We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): + +```shell +wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +``` + +We initialize the model: + +```python +from llama_cpp import Llama +from outlines import generate, models + +llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False +) +model = models.LlamaCpp(llm) +``` + +## Chain of thought + +We first define our Pydantic class for a reasoning step: + +```python +from pydantic import BaseModel, Field + +class Reasoning_Step(BaseModel): + reasoning_step: str = Field(..., description="Reasoning step") +``` + +We then define the Pydantic class for reasoning which will consist on a list of reasoning steps and a conclusion, and we get its JSON schema: + +```python +from typing import List + +from typing import List + +class Reasoning(BaseModel): + reasoning: List[Reasoning_Step] = Field(..., description="List of reasoning steps") + conclusion: str = Field(..., description="Conclusion") + +json_schema = Reasoning.model_json_schema() +``` + +We could generate using the json schema but for a change we will use the regex: + +```python +from outlines.integrations.utils import convert_json_schema_to_str +from outlines.fsm.json_schema import build_regex_from_schema + +json_schema = Reasoning.model_json_schema() +schema_str = convert_json_schema_to_str(json_schema=json_schema) +regex_str = build_regex_from_schema(schema_str) +``` + +We then need to adapt our prompt to the [Hermes prompt format for JSON schema](https://github.com/NousResearch/Hermes-Function-Calling?tab=readme-ov-file#prompt-format-for-json-mode--structured-outputs): + +```python +def generate_hermes_prompt(user_prompt): + return ( + "<|im_start|>system\n" + "You are a world class AI model who answers questions in JSON " + f"Here's the json schema you must adhere to:\n\n{schema}\n<|im_end|>\n" + "<|im_start|>user\n" + + user_prompt + + "<|im_end|>" + + "\n<|im_start|>assistant\n" + "" + ) +``` + +For a given user prompt, for example: + +```python +user_prompt = "9.11 and 9.9 -- which is bigger?" +``` + +We can use `generate.regex` by passing the Pydantic class we previously defined, and call the generator with the Hermes prompt: + +```python +generator = generate.regex(model, regex_str) +prompt = generate_hermes_prompt(user_prompt) +response = generator(prompt, max_tokens=1024, temperature=0, seed=42) +``` + +We obtain the reasoning steps as well as the conclusion + +```python +import json + +json_response = json.loads(response) + +print(json_response["reasoning"]) +print(json_response["conclusion"]) +# [{'reasoning_step': 'Both 9.11 and 9.9 are decimal numbers.'}, +# {'reasoning_step': 'When comparing decimal numbers, we look at the numbers after the decimal point.'}, +# {'reasoning_step': 'In this case, 9.11 has the number 1 after the decimal point, while 9.9 has the number 9.'}, +# {'reasoning_step': 'Since 1 is greater than 9, 9.11 is greater than 9.9.'}] +# '9.11 is bigger.' +``` + +We notice that the 4th reasoning step is wrong ``Since 1 is greater than 9, 9.11 is greater than 9.9.'', so we could probably give the model some examples for this particular task. + +This example was originally contributed by [Alonso Silva](https://github.com/alonsosilvaallende). diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 24849d5ee..e8816125a 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -8,3 +8,4 @@ - [SimToM](simtom.md): Improve LLMs' Theory of Mind capabilities with perspective-taking prompting and JSON-structured generation. - [Q&A with Citations](qa-with-citations.md): Answer questions and provide citations using JSON-structured generation. - [Knowledge Graph Generation](knowledge_graph_extraction.md): Generate a Knowledge Graph from unstructured text using JSON-structured generation. +- [Chain Of Thought (CoT)](chain_of_thought.md): Generate a series of intermediate reasoning steps using regex-structured generation. diff --git a/mkdocs.yml b/mkdocs.yml index d69d1e718..bb47ab45b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,6 +108,7 @@ nav: - Perspective-taking prompting: cookbook/simtom.md - Question-answering with citations: cookbook/qa-with-citations.md - Knowledge Graph Extraction: cookbook/knowledge_graph_extraction.md + - Chain of Thought (CoT): cookbook/chain_of_thought.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From 2a2a5d3b77a5810459c7d5c35357578fcb3078ac Mon Sep 17 00:00:00 2001 From: Alonso Silva Date: Thu, 8 Aug 2024 17:36:55 +0200 Subject: [PATCH 032/505] Fix typos --- docs/cookbook/chain_of_thought.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md index 2ab68ff7a..d320feb8d 100644 --- a/docs/cookbook/chain_of_thought.md +++ b/docs/cookbook/chain_of_thought.md @@ -1,7 +1,7 @@ # Chain of thought -Chain of thought is a prompting technique introduced in the paper [``Chain-of-Thought Prompting Elicits Reasoning in Large Language Models''](https://arxiv.org/abs/2201.11903) where throught prompting the authors generate a series of intermediate reasoning steps which improves the ability of LLMs to perform complex reasoning. +Chain of thought is a prompting technique introduced in the paper ["Chain-of-Thought Prompting Elicits Reasoning in Large Language Models"](https://arxiv.org/abs/2201.11903) where throught prompting the authors generate a series of intermediate reasoning steps which improves the ability of LLMs to perform complex reasoning. In this guide, we use [outlines](https://outlines-dev.github.io/outlines/) to apply chain of thought through structured output. @@ -52,8 +52,6 @@ We then define the Pydantic class for reasoning which will consist on a list of ```python from typing import List -from typing import List - class Reasoning(BaseModel): reasoning: List[Reasoning_Step] = Field(..., description="List of reasoning steps") conclusion: str = Field(..., description="Conclusion") @@ -61,13 +59,12 @@ class Reasoning(BaseModel): json_schema = Reasoning.model_json_schema() ``` -We could generate using the json schema but for a change we will use the regex: +We could generate a response using the json schema but for a change we will use the regex: ```python from outlines.integrations.utils import convert_json_schema_to_str from outlines.fsm.json_schema import build_regex_from_schema -json_schema = Reasoning.model_json_schema() schema_str = convert_json_schema_to_str(json_schema=json_schema) regex_str = build_regex_from_schema(schema_str) ``` @@ -88,13 +85,13 @@ def generate_hermes_prompt(user_prompt): ) ``` -For a given user prompt, for example: +For a given user prompt: ```python user_prompt = "9.11 and 9.9 -- which is bigger?" ``` -We can use `generate.regex` by passing the Pydantic class we previously defined, and call the generator with the Hermes prompt: +we can use `generate.regex` by passing the Pydantic class we previously defined, and call the generator with the Hermes prompt: ```python generator = generate.regex(model, regex_str) @@ -102,7 +99,7 @@ prompt = generate_hermes_prompt(user_prompt) response = generator(prompt, max_tokens=1024, temperature=0, seed=42) ``` -We obtain the reasoning steps as well as the conclusion +We obtain a series of intermediate reasoning steps as well as the conclusion: ```python import json @@ -118,6 +115,6 @@ print(json_response["conclusion"]) # '9.11 is bigger.' ``` -We notice that the 4th reasoning step is wrong ``Since 1 is greater than 9, 9.11 is greater than 9.9.'', so we could probably give the model some examples for this particular task. +We notice that the 4th reasoning step is wrong ``Since 1 is greater than 9, 9.11 is greater than 9.9.'', so we should probably give the model some examples for this particular task. This example was originally contributed by [Alonso Silva](https://github.com/alonsosilvaallende). From 2d1a06dc13f321598197acd9191d466620ce2727 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Thu, 8 Aug 2024 17:08:13 -0500 Subject: [PATCH 033/505] Fix coverage exclude_lines setting for ellipsis Closes #1085 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f94b3c84d..a41e29772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,7 @@ omit = [ exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", - "...", + "\\.\\.\\.", ] show_missing = true From 5e8f7709e3cecd02943120ed01420f00159cedbc Mon Sep 17 00:00:00 2001 From: Alonso Silva Date: Sat, 10 Aug 2024 19:01:51 +0200 Subject: [PATCH 034/505] Add ReAct agent example --- docs/cookbook/index.md | 1 + docs/cookbook/react_agent.md | 257 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 259 insertions(+) create mode 100644 docs/cookbook/react_agent.md diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index e8816125a..56334302a 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -9,3 +9,4 @@ - [Q&A with Citations](qa-with-citations.md): Answer questions and provide citations using JSON-structured generation. - [Knowledge Graph Generation](knowledge_graph_extraction.md): Generate a Knowledge Graph from unstructured text using JSON-structured generation. - [Chain Of Thought (CoT)](chain_of_thought.md): Generate a series of intermediate reasoning steps using regex-structured generation. +- [ReAct Agent](react_agent.md): Build an agent with open weights models using regex-structured generation. diff --git a/docs/cookbook/react_agent.md b/docs/cookbook/react_agent.md new file mode 100644 index 000000000..74930b70b --- /dev/null +++ b/docs/cookbook/react_agent.md @@ -0,0 +1,257 @@ +# ReAct Agent + +This example shows how to use [outlines](https://outlines-dev.github.io/outlines/) to build your own agent with open weights local models and structured outputs. It is inspired by the blog post [A simple Python implementation of the ReAct pattern for LLMs](https://til.simonwillison.net/llms/python-react-pattern) by [Simon Willison](https://simonwillison.net/). + +The ReAct pattern (for Reason+Act) is described in the paper [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629). It's a pattern where you implement additional actions that an LLM can take - searching Wikipedia or running calculations for example - and then teach it how to request the execution of those actions, and then feed their results back into the LLM. + +Additionally, we give the LLM the possibility of using a scratchpad described in the paper [Show Your Work: Scratchpads for Intermediate Computation with Language Models](https://arxiv.org/abs/2112.00114) which improves the ability of LLMs to perform multi-step computations. + +We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: + +```shell +pip install llama-cpp-python +``` + +We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): + +```shell +wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +``` + +We initialize the model: + +```python +import llama_cpp +from llama_cpp import Llama +from outlines import generate, models + +llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False +) +model = models.LlamaCpp(llm) +``` + +## Build a ReAct agent + +In this example, we use two tools: + +- wikipedia: \ - search Wikipedia and returns the snippet of the first result +- calculate: \ - evaluate an expression using Python's eval() function + +```python +import httpx + +def wikipedia(q): + return httpx.get("https://en.wikipedia.org/w/api.php", params={ + "action": "query", + "list": "search", + "srsearch": q, + "format": "json" + }).json()["query"]["search"][0]["snippet"] +``` + +```python +def calculate(numexp): + return eval(numexp) +``` + +We define the logic of the agent through a Pydantic class. First, we want the LLM to decide only between the two previously defined tools: + +```python +from enum import Enum + +class Action(str, Enum): + wikipedia = "wikipedia" + calculate = "calculate" +``` + +Our agent will loop through Thought and Action. We explicitly give the Action Input field so it doesn't forget to add the arguments of the Action. We also add a scratchpad (optional). + +```python +from pydantic import BaseModel, Field + +class Reason_and_Act(BaseModel): + Scratchpad: str = Field(..., description="Information from the Observation useful to answer the question") + Thought: str = Field(..., description="It describes your thoughts about the question you have been asked") + Action: Action + Action_Input: str = Field(..., description="The arguments of the Action.") +``` + +Our agent will reach a Final Answer. We also add a scratchpad (optional). + +```python +class Final_Answer(BaseModel): + Scratchpad: str = Field(..., description="Information from the Observation useful to answer the question") + Final_Answer: str = Field(..., description="Answer to the question grounded on the Observation") +``` + +Our agent will decide when it has reached a Final Answer and therefore to stop the loop of Thought and Action. + +```python +from typing import Union + +class Decision(BaseModel): + Decision: Union[Reason_and_Act, Final_Answer] +``` + +We could generate a response using the json schema but we will use the regex and check that everything is working as expected: + +```python +from outlines.integrations.utils import convert_json_schema_to_str +from outlines.fsm.json_schema import build_regex_from_schema + +json_schema = Decision.model_json_schema() +schema_str = convert_json_schema_to_str(json_schema=json_schema) +regex_str = build_regex_from_schema(schema_str) +print(regex_str) +# '\\{[ ]?"Decision"[ ]?:[ ]?(\\{[ ]?"Scratchpad"[ ]?:[ ]?"([^"\\\\\\x00-\\x1F\\x7F-\\x9F]|\\\\["\\\\])*"[ ]?,[ ]?"Thought"[ ]?:[ ]?"([^"\\\\\\x00-\\x1F\\x7F-\\x9F]|\\\\["\\\\])*"[ ]?,[ ]?"Action"[ ]?:[ ]?("wikipedia"|"calculate")[ ]?,[ ]?"Action_Input"[ ]?:[ ]?"([^"\\\\\\x00-\\x1F\\x7F-\\x9F]|\\\\["\\\\])*"[ ]?\\}|\\{[ ]?"Scratchpad"[ ]?:[ ]?"([^"\\\\\\x00-\\x1F\\x7F-\\x9F]|\\\\["\\\\])*"[ ]?,[ ]?"Final_Answer"[ ]?:[ ]?"([^"\\\\\\x00-\\x1F\\x7F-\\x9F]|\\\\["\\\\])*"[ ]?\\})[ ]?\\}' +``` + +We then need to adapt our prompt to the [Hermes prompt format for JSON schema](https://github.com/NousResearch/Hermes-Function-Calling?tab=readme-ov-file#prompt-format-for-json-mode--structured-outputs) and explain the agent logic: + +```python +import datetime + +def generate_hermes_prompt(question, schema=""): + return ( + "<|im_start|>system\n" + "You are a world class AI model who answers questions in JSON with correct Pydantic schema. " + f"Here's the json schema you must adhere to:\n\n{schema}\n\n" + "Today is " + datetime.datetime.today().strftime('%Y-%m-%d') + ".\n" + + "You run in a loop of Scratchpad, Thought, Action, Action Input, PAUSE, Observation. " + "At the end of the loop you output a Final Answer. " + "Use Scratchpad to store the information from the Observation useful to answer the question " + "Use Thought to describe your thoughts about the question you have been asked " + "and reflect carefully about the Observation if it exists. " + "Use Action to run one of the actions available to you. " + "Use Action Input to input the arguments of the selected action - then return PAUSE. " + "Observation will be the result of running those actions. " + "Your available actions are:\n" + "calculate:\n" + "e.g. calulate: 4**2 / 3\n" + "Runs a calculation and returns the number - uses Python so be sure to use floating point syntax if necessary\n" + "wikipedia:\n" + "e.g. wikipedia: Django\n" + "Returns a summary from searching Wikipedia\n" + "DO NOT TRY TO GUESS THE ANSWER. Begin! <|im_end|>" + "\n<|im_start|>user\n" + question + "<|im_end|>" + "\n<|im_start|>assistant\n" + ) +``` + +We define a ChatBot class + +```python +class ChatBot: + def __init__(self, prompt=""): + self.prompt = prompt + + def __call__(self, user_prompt): + self.prompt += user_prompt + result = self.execute() + return result + + def execute(self): + generator = generate.regex(model, regex_str) + result = generator(self.prompt, max_tokens=1024, temperature=0, seed=42) + return result +``` + +We define a query function: + +```python +import json + +def query(question, max_turns=5): + i = 0 + next_prompt = ( + "\n<|im_start|>user\n" + question + "<|im_end|>" + "\n<|im_start|>assistant\n" + ) + previous_actions = [] + while i < max_turns: + i += 1 + prompt = generate_hermes_prompt(question=question, schema=Decision.model_json_schema()) + bot = ChatBot(prompt=prompt) + result = bot(next_prompt) + json_result = json.loads(result)['Decision'] + if "Final_Answer" not in list(json_result.keys()): + scratchpad = json_result['Scratchpad'] if i == 0 else "" + thought = json_result['Thought'] + action = json_result['Action'] + action_input = json_result['Action_Input'] + print(f"\x1b[34m Scratchpad: {scratchpad} \x1b[0m") + print(f"\x1b[34m Thought: {thought} \x1b[0m") + print(f"\x1b[36m -- running {action}: {str(action_input)}\x1b[0m") + if action + ": " + str(action_input) in previous_actions: + observation = "You already run that action. **TRY A DIFFERENT ACTION INPUT.**" + else: + if action=="calculate": + try: + observation = eval(str(action_input)) + except Exception as e: + observation = f"{e}" + elif action=="wikipedia": + try: + observation = wikipedia(str(action_input)) + except Exception as e: + observation = f"{e}" + print() + print(f"\x1b[33m Observation: {observation} \x1b[0m") + print() + previous_actions.append(action + ": " + str(action_input)) + next_prompt += ( + "\nScratchpad: " + scratchpad + + "\nThought: " + thought + + "\nAction: " + action + + "\nAction Input: " + action_input + + "\nObservation: " + str(observation) + ) + else: + scratchpad = json_result["Scratchpad"] + final_answer = json_result["Final_Answer"] + print(f"\x1b[34m Scratchpad: {scratchpad} \x1b[0m") + print(f"\x1b[34m Final Answer: {final_answer} \x1b[0m") + return final_answer + print(f"\nFinal Answer: I am sorry, but I am unable to answer your question. Please provide more information or a different question.") + return "No answer found" +``` + +We can now test our ReAct agent: + +```python +print(query("What's 2 to the power of 10?")) +# Scratchpad: +# Thought: I need to perform a mathematical calculation to find the result of 2 to the power of 10. +# -- running calculate: 2**10 +# +# Observation: 1024 +# +# Scratchpad: 2 to the power of 10 is 1024. +# Final Answer: 2 to the power of 10 is 1024. +# 2 to the power of 10 is 1024. +``` + +```python +print(query("What does England share borders with?")) +# Scratchpad: +# Thought: To answer this question, I will use the 'wikipedia' action to gather information about England's geographical location and its borders. +# -- running wikipedia: England borders +# +# Observation: Anglo-Scottish border (Scottish Gaelic: Crìochan Anglo-Albannach) is an internal border of the United Kingdom separating Scotland and England which runs for +# +# Scratchpad: Anglo-Scottish border (Scottish Gaelic: Crìochan Anglo-Albannach) is an internal border of the United Kingdom separating Scotland and England which runs for +# Final Answer: England shares a border with Scotland. +# England shares a border with Scotland. +``` + +As mentioned in Simon's blog post, this is not a very robust implementation at all and there's a ton of room for improvement. But it is lovely how simple it is with a few lines of Python to make these extra capabilities available to the LLM. And now you can run it locally with an open weights LLM. + +This example was originally contributed by [Alonso Silva](https://github.com/alonsosilvaallende). diff --git a/mkdocs.yml b/mkdocs.yml index bb47ab45b..e5f3417ac 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -109,6 +109,7 @@ nav: - Question-answering with citations: cookbook/qa-with-citations.md - Knowledge Graph Extraction: cookbook/knowledge_graph_extraction.md - Chain of Thought (CoT): cookbook/chain_of_thought.md + - ReAct Agent: cookbook/react_agent.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From 3072b8f535cdfc2587979aa0821a1ded5796827a Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 13 Aug 2024 10:47:58 +0200 Subject: [PATCH 035/505] Include SPDX license info in project metadata A license entry in the project metadata makes it easier to detect the license of a project. Before: ```shell $ pip install pip-licenses outlines $ pip-licenses | grep outlines outlines 0.0.46 UNKNOWN ``` After: ```shell $ pip install . $ pip-licenses | grep outlines outlines 0.0.47.dev43+g8b78f5d Apache-2.0 ``` Signed-off-by: Christian Heimes --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a41e29772..ea0ac11c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ name = "outlines" authors= [{name = "Outlines Developers"}] description = "Probabilistic Generative Model Programming" requires-python = ">=3.8" +license = {text = "Apache-2.0"} keywords=[ "machine learning", "deep learning", From d0be920f46b0132d87732b94e6861ef872ca558c Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 22 Jul 2024 17:41:25 -0500 Subject: [PATCH 036/505] Restructure documentation, use phi-3-mini, include structured generation example, add feature matrix to models --- .../images/logits_processing_diagram.svg | 157 +++++++++++++ docs/quickstart.md | 214 +++--------------- docs/reference/{ => generation}/cfg.md | 0 docs/reference/{ => generation}/choices.md | 0 .../{ => generation}/custom_fsm_ops.md | 0 docs/reference/{ => generation}/format.md | 0 docs/reference/generation/generation.md | 210 +++++++++++++++++ docs/reference/{ => generation}/json.md | 3 +- docs/reference/{ => generation}/regex.md | 0 .../structured_generation_explanation.md | 58 +++++ docs/reference/{ => generation}/types.md | 0 docs/reference/json_mode.md | 17 -- docs/reference/models/models.md | 93 ++++++++ docs/reference/models/transformers_vision.md | 3 +- docs/stylesheets/extra.css | 4 + mkdocs.yml | 25 +- requirements-doc.txt | 1 + 17 files changed, 573 insertions(+), 212 deletions(-) create mode 100644 docs/assets/images/logits_processing_diagram.svg rename docs/reference/{ => generation}/cfg.md (100%) rename docs/reference/{ => generation}/choices.md (100%) rename docs/reference/{ => generation}/custom_fsm_ops.md (100%) rename docs/reference/{ => generation}/format.md (100%) create mode 100644 docs/reference/generation/generation.md rename docs/reference/{ => generation}/json.md (98%) rename docs/reference/{ => generation}/regex.md (100%) create mode 100644 docs/reference/generation/structured_generation_explanation.md rename docs/reference/{ => generation}/types.md (100%) delete mode 100644 docs/reference/json_mode.md create mode 100644 docs/reference/models/models.md diff --git a/docs/assets/images/logits_processing_diagram.svg b/docs/assets/images/logits_processing_diagram.svg new file mode 100644 index 000000000..92668e6c6 --- /dev/null +++ b/docs/assets/images/logits_processing_diagram.svg @@ -0,0 +1,157 @@ + + + + + + +%3 + + + +inputTokens + +Input Tokens + +7 + +4 + +8 + + + +TransformerDecoder + +Transformer Decoder Pass + + + +inputTokens->TransformerDecoder + + + + + +logitsTable + +Model Output Logits + +Token + +Probability + ++ + +3% + +foo + +4% + +. + +7% + +1 + +11% + +2 + +13% + +3 + +17% + + + +TransformerDecoder->logitsTable + + + + + +OutlinesLogitsProcessor + +Outlines Regex Logits Processor + + + +logitsTable->OutlinesLogitsProcessor + + + + + +logitsProcessorTable + +Processed Logits + +Token + +Probability + ++ + +0% + +foo + +0% + +. + +7% + +1 + +11% + +2 + +13% + +3 + +17% + + + +OutlinesLogitsProcessor->logitsProcessorTable + + + + + +sampler + +Sampler + + + +logitsProcessorTable->sampler + + + + + +sampledTokenTable + +Sampled Token + +Token + +3 + + + +sampler->sampledTokenTable + + + + + diff --git a/docs/quickstart.md b/docs/quickstart.md index 7d4e86fbc..fcb524d8a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -6,6 +6,7 @@ title: Quickstart After [installing Outlines](installation.md), the fastest way to get to up to speed with the library is to get acquainted with its few core elements. We advise you to take a quick look at this page to see everything Outlines has to offer before diving in the [documentation](reference/index.md). + ## Core elements ### Models @@ -15,88 +16,57 @@ The first step when writing a program with Outlines is to initialize a model. We ```python import outlines -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers( + "microsoft/Phi-3-mini-4k-instruct", + device="cuda" # optional device argument, default is cpu +) ``` -### Text generator - -Once the model is initialized you can build a text generator. This generator can be called with a prompt directly, or you can use the `stream` method to generate text token by token: - -=== "Generate" - - ```python - import outlines +Outlines supports a wide variety of inference engines and model weight types. More details on different models can be found in the [Outlines Models](./reference/models/models.md) documentation page. - model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") - generator = outlines.generate.text(model) +### Generation - result = generator("What's 2+2?", max_tokens=100) +Once the model is initialized you can build an `outlines.generate` generator. This generator can be called with a prompt directly. - print(result) - # That's right, it's 4! But remember, a delicious and nutrient dense 4, - # according to YEARS BUILT ON SOLID SCIENCE. This column presents additional - # findings from the fifteen-year study that produced the 2+2=4 conclusion. - ``` +([Outlines Structured Generation Full Documentation](./reference/generation/generation.md)) -=== "Stream" +=== "Text" ```python - import outlines - - model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") generator = outlines.generate.text(model) + result = generator("Question: What's 2+2? Answer:", max_tokens=100) + print(result) + # The answer is 4 + + # Outlines also supports streaming output stream = generator.stream("What's 2+2?", max_tokens=4) for i in range(5): token = next(stream) - print(token) - # ['Is'] - # [' this'] - # [' even'] - # [' a'] - # [' question'] + print(repr(token)) + # '2' + # '+' + # '2' + # ' equals' + # '4' ``` -### Multi-label classification - -Outlines allows you to do multi-label classification by guiding the model so it can only output either of the specified choices: - -```python -import outlines - -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") -generator = outlines.generate.choice(model, ["Blue", "Red", "Yellow"]) - -color = generator("What is the closest color to Indigo? ") -print(color) -# Blue -``` +=== "Structured" -### JSON-structured generation + Along with typical language model generation behavior via, `outlines.generate.text`, Outlines supports structured generation, which guarantees the tokens generated by the model will follow a predefined structure. Structures can be defined by a regex pattern, JSON schema, python object type, or a Lark grammar defining a parsable language such as SQL or Python. -Outlines can guide models so that they output valid JSON **100%** of the time. You can either specify the structure using [Pydantic][pydantic]{:target="_blank"} or a string that contains a [JSON Schema][jsonschema]{:target="_blank"}: - -=== "Pydantic" + Example: using pydantic to enforce a JSON schema ```python from enum import Enum from pydantic import BaseModel, constr, conint - import outlines - - class Armor(str, Enum): - leather = "leather" - chainmail = "chainmail" - plate = "plate" - - class Character(BaseModel): name: constr(max_length=10) age: conint(gt=18, lt=99) - armor: Armor + armor: (Enum('Armor', {'leather': 'leather', 'chainmail': 'chainmail', 'plate': 'plate'})) strength: conint(gt=1, lt=100) - model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") generator = outlines.generate.json(model, Character) character = generator( @@ -104,135 +74,10 @@ Outlines can guide models so that they output valid JSON **100%** of the time. Y + "name, age (between 1 and 99), armor and strength. " ) print(character) - # name='Orla' age=21 armor= strength=8 - ``` - -=== "JSON Schema" - - ```python - import outlines - - schema = """{ - "$defs": { - "Armor": { - "enum": ["leather", "chainmail", "plate"], - "title": "Armor", - "type": "string" - } - }, - "properties": { - "name": {"maxLength": 10, "title": "Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "armor": {"$ref": "#/$defs/Armor"}, - "strength": {"title": "Strength", "type": "integer"}\ - }, - "required": ["name", "age", "armor", "strength"], - "title": "Character", - "type": "object" - }""" - - model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") - generator = outlines.generate.json(model, schema) - character = generator( - "Generate a new character for my awesome game: " - + "name, age (between 1 and 99), armor and strength. " - ) - print(character) - # {'name': 'Yuki', 'age': 24, 'armor': 'plate', 'strength': 3} - ``` - -!!! Note - - We advise you to constrain the length of the strings fields when first testing your schema, especially with small models. + # Character(name='Zara', age=25, armor=, strength=85) + ``` -### Grammar-structured generation - -Outlines also allows to generate text that is valid to any [context-free grammar][cfg]{:target="_blank"} (CFG) in the [EBNF format][ebnf]{:target="_blank"}. Grammars can be intimidating, but they are a very powerful tool! Indeed, they determine the syntax of every programming language, valid chess moves, molecule structure, can help with procedural graphics generation, etc. - -Here we show a simple example of a grammar that defines arithmetic operations: - -```python -from outlines import models, generate - -arithmetic_grammar = """ - ?start: sum - - ?sum: product - | sum "+" product -> add - | sum "-" product -> sub - - ?product: atom - | product "*" atom -> mul - | product "/" atom -> div - - ?atom: NUMBER -> number - | "-" atom -> neg - | "(" sum ")" - - %import common.NUMBER - %import common.WS_INLINE - - %ignore WS_INLINE -""" - -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") -generator = generate.cfg(model, arithmetic_grammar, max_tokens=100) - -result = generator("Question: How can you write 5*5 using addition?\nAnswer:") -print(result) -# 5+5+5+5+5 -``` - - -EBNF grammars can be cumbersome to write. This is why Outlines provides grammar definitions in the `outlines.grammars.` module - -```python -from outlines import models, generate, grammars - -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") -generator = generate.cfg(model, grammars.arithmetic, max_tokens=100) - -result = generator("Question: How can you write 5*5 using addition?\nAnswer:") -print(result) -# 5+5+5+5+5 -``` - -The available grammars are listed [here](https://github.com/outlines-dev/outlines/tree/main/outlines/grammars). - - -### Regex-structured generation - -Slightly simpler, but no less useful, Outlines can generate text that is in the language of a [regular expression](https://www.regular-expressions.info/tutorial.html). For instance to force the model to generate IP addresses: - -```python -from outlines import models, generate - -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") - -regex_str = r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)" -generator = generate.regex(model, regex_str) - -result = generator("What is the IP address of localhost?\nIP: ") -print(result) -# 127.0.0.100 -``` - -### Generate a given Python type - -We provide a shortcut to regex-structured generation for simple use cases. Pass a Python type to the `outlines.generate.format` function and the LLM will output text that matches this type: - -```python -from outlines import models, generate - -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") -generator = generate.format(model, int) - -result = generator("What is 2+2?") -print(result) -# 4 -``` - -## Deploy using vLLM and FastAPI +## [Deploy using vLLM and FastAPI](./reference/serve/vllm.md) Outlines can be deployed as a LLM service using [vLLM][vllm]{:target="_blank"} and [FastAPI][fastapi]{:target="_blank"}. The server supports asynchronous processing of incoming requests, and benefits from the performance of vLLM. @@ -265,7 +110,7 @@ Or use the [requests][requests]{:target="_blank"} library from another python pr ## Utilities -### Prompt templates +### [Prompt templates](./reference/prompting.md) Prompting can lead to messy code. Outlines' prompt functions are python functions that contain a template for the prompt in their docstring. We use a powerful templating language to allow you to loop over lists, dictionaries, add conditionals, etc. directly from the prompt. When called, a prompt function returns the rendered template: @@ -368,6 +213,7 @@ Once you are done experimenting with a prompt and an output structure, it is use ``` It make it easier for the community to collaborate on the infinite number of use cases enabled by these models! + ## Going further If you need more inspiration you can take a look at the [cookbook](cookbook/index.md). If you have any question, or requests for documentation please reach out to us on [GitHub](https://github.com/outlines-dev/outlines/discussions), [Twitter](https://twitter.com/remilouf) or [Discord](https://discord.gg/UppQmhEpe8). diff --git a/docs/reference/cfg.md b/docs/reference/generation/cfg.md similarity index 100% rename from docs/reference/cfg.md rename to docs/reference/generation/cfg.md diff --git a/docs/reference/choices.md b/docs/reference/generation/choices.md similarity index 100% rename from docs/reference/choices.md rename to docs/reference/generation/choices.md diff --git a/docs/reference/custom_fsm_ops.md b/docs/reference/generation/custom_fsm_ops.md similarity index 100% rename from docs/reference/custom_fsm_ops.md rename to docs/reference/generation/custom_fsm_ops.md diff --git a/docs/reference/format.md b/docs/reference/generation/format.md similarity index 100% rename from docs/reference/format.md rename to docs/reference/generation/format.md diff --git a/docs/reference/generation/generation.md b/docs/reference/generation/generation.md new file mode 100644 index 000000000..88b963c72 --- /dev/null +++ b/docs/reference/generation/generation.md @@ -0,0 +1,210 @@ +--- +title: Generation +--- + +# Generation + +Once an [Outlines model](../models) is constructed you can use `outlines.generate` to generate text. Standard LLM generation is possible via `outlines.generate.text`, along with a variety of structured generation methods described below. (For a detailed technical explanation of how structured generation works, you may review the [Structured Generation Explanation](./structured_generation_explanation.md) page) + +Before generating text, you must construct an `outlines.model`. Example: + +```python +import outlines + +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct", device="cuda") +``` +### Text generator + +```python +generator = outlines.generate.text(model) + +result = generator("Question: What's 2+2? Answer:", max_tokens=100) +print(result) +# The answer is 4 + +# Outlines also supports streaming output +stream = generator.stream("What's 2+2?", max_tokens=4) +for i in range(5): + token = next(stream) + print(repr(token)) +# '2' +# '+' +# '2' +# ' equals' +# '4' +``` + +### [Multi-label classification](./choices.md) + +Outlines allows you to do multi-label classification by guiding the model so it can only output either of the specified choices: + +```python +import outlines + +model = outlines.models.transformers("microsoft/Phi-3-mini-128k-instruct") +generator = outlines.generate.choice(model, ["Blue", "Red", "Yellow"]) + +color = generator("What is the closest color to Indigo? ") +print(color) +# Blue +``` + +### [JSON-structured generation](./json.md) + +Outlines can guide models so that they output valid JSON **100%** of the time. You can either specify the structure using [Pydantic][pydantic]{:target="_blank"} or a string that contains a [JSON Schema][jsonschema]{:target="_blank"}: + +=== "Pydantic" + + ```python + from enum import Enum + from pydantic import BaseModel, constr, conint + + import outlines + + class Armor(str, Enum): + leather = "leather" + chainmail = "chainmail" + plate = "plate" + + + class Character(BaseModel): + name: constr(max_length=10) + age: conint(gt=18, lt=99) + armor: Armor + strength: conint(gt=1, lt=100) + + model = outlines.models.transformers("microsoft/Phi-3-mini-128k-instruct") + generator = outlines.generate.json(model, Character) + + character = generator( + "Generate a new character for my awesome game: " + + "name, age (between 1 and 99), armor and strength. " + ) + print(character) + # name='Orla' age=21 armor= strength=8 + ``` + +=== "JSON Schema" + + ```python + import outlines + + schema = """{ + "$defs": { + "Armor": { + "enum": ["leather", "chainmail", "plate"], + "title": "Armor", + "type": "string" + } + }, + "properties": { + "name": {"maxLength": 10, "title": "Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "armor": {"$ref": "#/$defs/Armor"}, + "strength": {"title": "Strength", "type": "integer"}\ + }, + "required": ["name", "age", "armor", "strength"], + "title": "Character", + "type": "object" + }""" + + model = outlines.models.transformers("microsoft/Phi-3-mini-128k-instruct") + generator = outlines.generate.json(model, schema) + character = generator( + "Generate a new character for my awesome game: " + + "name, age (between 1 and 99), armor and strength. " + ) + print(character) + # {'name': 'Yuki', 'age': 24, 'armor': 'plate', 'strength': 3} + ``` + +!!! Note + + We advise you to constrain the length of the strings fields when first testing your schema, especially with small models. + +### [Grammar-structured generation](./cfg.md) + +Outlines also allows to generate text that is valid to any [context-free grammar][cfg]{:target="_blank"} (CFG) in the [EBNF format][ebnf]{:target="_blank"}. Grammars can be intimidating, but they are a very powerful tool! Indeed, they determine the syntax of every programming language, valid chess moves, molecule structure, can help with procedural graphics generation, etc. + +Here we show a simple example of a grammar that defines arithmetic operations: + +```python +from outlines import models, generate + +arithmetic_grammar = """ + ?start: sum + + ?sum: product + | sum "+" product -> add + | sum "-" product -> sub + + ?product: atom + | product "*" atom -> mul + | product "/" atom -> div + + ?atom: NUMBER -> number + | "-" atom -> neg + | "(" sum ")" + + %import common.NUMBER + %import common.WS_INLINE + + %ignore WS_INLINE +""" + +model = models.transformers("microsoft/Phi-3-mini-128k-instruct") +generator = generate.cfg(model, arithmetic_grammar, max_tokens=100) + +result = generator("Question: How can you write 5*5 using addition?\nAnswer:") +print(result) +# 5+5+5+5+5 +``` + + +EBNF grammars can be cumbersome to write. This is why Outlines provides grammar definitions in the `outlines.grammars.` module + +```python +from outlines import models, generate, grammars + +model = models.transformers("microsoft/Phi-3-mini-128k-instruct") +generator = generate.cfg(model, grammars.arithmetic, max_tokens=100) + +result = generator("Question: How can you write 5*5 using addition?\nAnswer:") +print(result) +# 5+5+5+5+5 +``` + +The available grammars are listed [here](https://github.com/outlines-dev/outlines/tree/main/outlines/grammars). + + +### [Regex-structured generation](./regex.md) + +Slightly simpler, but no less useful, Outlines can generate text that is in the language of a [regular expression](https://www.regular-expressions.info/tutorial.html). For instance to force the model to generate IP addresses: + +```python +from outlines import models, generate + +model = models.transformers("microsoft/Phi-3-mini-128k-instruct") + +regex_str = r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)" +generator = generate.regex(model, regex_str) + +result = generator("What is the IP address of localhost?\nIP: ") +print(result) +# 127.0.0.100 +``` + +### [Generate a given Python type](./types.md) + +We provide a shortcut to regex-structured generation for simple use cases. Pass a Python type to the `outlines.generate.format` function and the LLM will output text that matches this type: + +```python +from outlines import models, generate + +model = models.transformers("microsoft/Phi-3-mini-128k-instruct") +generator = generate.format(model, int) + +result = generator("What is 2+2?") +print(result) +# 4 +``` diff --git a/docs/reference/json.md b/docs/reference/generation/json.md similarity index 98% rename from docs/reference/json.md rename to docs/reference/generation/json.md index 85e1a846a..a342395bd 100644 --- a/docs/reference/json.md +++ b/docs/reference/generation/json.md @@ -70,7 +70,8 @@ schema = """ "name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "integer"} - } + }, + "required": ["name", "last_name", "id"] } """ diff --git a/docs/reference/regex.md b/docs/reference/generation/regex.md similarity index 100% rename from docs/reference/regex.md rename to docs/reference/generation/regex.md diff --git a/docs/reference/generation/structured_generation_explanation.md b/docs/reference/generation/structured_generation_explanation.md new file mode 100644 index 000000000..0dedf060b --- /dev/null +++ b/docs/reference/generation/structured_generation_explanation.md @@ -0,0 +1,58 @@ +--- +title: Structured Generation Explanation +--- + +# Structured Generation Explanation + + +Language models generate text token by token, using the previous token sequence as input and sampled logits as output. This document explains the structured generation process, where only legal tokens are considered for the next step based on a predefined automata, e.g. a regex-defined [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM) or [Lark](https://lark-parser.readthedocs.io/en/stable/) grammar.` + + +## Worked Example + +Let's consider a worked example with a pattern for whole and decimal numbers: + +`^\d*(\.\d+)?$`. + +### Creating Automata + +The pattern is first converted into an automata. Below is a brief explanation of the automata conversion and its representation. + +**Automata Diagram:** + +```mermaid +graph LR + node0("1-9") --> node1("1-9") + node1 --> node1 + node1 --> nodeEND{{END}} + node1 --> nodePeriod(".") + nodePeriod --> node2("1-9") + node2 --> node2 + node2 --> nodeEND{{END}} +``` + +### Generating a Token + +Let's assume that we're in the middle of generation, and so far "748" has been generated. Here is the automata with the current state highlighted in green, with the legal next characters being another number (1-9), a dot (.), or end of sequence. + +```mermaid +graph LR + node0("1-9") --> node1("1-9") + node1 --> node1 + node1 --> nodeEND{{END}} + node1 --> nodePeriod(".") + nodePeriod --> node2("1-9") + node2 --> node2 + node2 --> nodeEND{{END}} + + style node1 fill:#090 +``` + +Generating a token requires the following steps: + +- Feed the previous input sequence ("748") into the language model. +- Language model runs a forward pass and produces token logits. +- Outlines logits processor sets the probability of illegal tokens to 0%. +- A token is sampled from the set of legal tokens. + +![Generation and Logits Processing Flow Chart](../../assets/images/logits_processing_diagram.svg) diff --git a/docs/reference/types.md b/docs/reference/generation/types.md similarity index 100% rename from docs/reference/types.md rename to docs/reference/generation/types.md diff --git a/docs/reference/json_mode.md b/docs/reference/json_mode.md deleted file mode 100644 index bb1c1c7a8..000000000 --- a/docs/reference/json_mode.md +++ /dev/null @@ -1,17 +0,0 @@ -# JSON mode - -Outlines can guarantee that the LLM will generate valid JSON, using [Grammar-structured generation](cfg.md): - -```python -from outlines import models, generate - -json_grammar = outlines.grammars.json - -model = models.transformers("mistralai/Mistral-7b-v0.1") -generator = generate.cfg(model, json_grammar) -sequence = generator("Generate valid JSON") -``` - -!!! Note "JSON that follows a schema" - - If you want to guarantee that the generated JSON follows a given schema, consult [this section](json.md) instead. diff --git a/docs/reference/models/models.md b/docs/reference/models/models.md new file mode 100644 index 000000000..dadfd34ad --- /dev/null +++ b/docs/reference/models/models.md @@ -0,0 +1,93 @@ +--- +title: Models +--- + +# Models + +Outlines supports generation using a number of inference engines (`outlines.models`) + +Loading a model using outlines follows a similar interface between inference engines. + +```python +import outlines +``` + +## [Transformers](./transformers.md) + +```python +model = outlines.models.transformers("microsoft/Phi-3-mini-128k-instruct", model_kwargs={}) +``` + +For additional arguments and use of other Huggingface Transformers model types see [Outlines' Transformers documentation](./transformers.md). + + +## [Transformers Vision](./transformers_vision.md) + +```python +model = outlines.models.transformers_vision("llava-hf/llava-v1.6-mistral-7b-hf") +``` + +For examples of generation and other details, see [Outlines' Transformers Vision documentation](./transformers_vision.md). + +## [vLLM](./vllm.md) + +```python +model = outlines.models.vllm("microsoft/Phi-3-mini-128k-instruct") +``` + +## [llama.cpp](./llamacpp.md) + +```python +model = outlines.models.llamacpp("microsoft/Phi-3-mini-4k-instruct-gguf", "Phi-3-mini-4k-instruct-q4.gguf") +``` + +Additional llama.cpp parameters can be found in the [Outlines' llama.cpp documentation](./llamacpp.md). + +## [ExLlamaV2](./exllamav2.md) + +```python +model = outlines.models.exllamav2("bartowski/Phi-3-mini-128k-instruct-exl2") +``` + +## [MLXLM](./mlxlmx.md) + +```python +model = outlines.models.mlxlm("mlx-community/Phi-3-mini-4k-instruct-4bit") +``` + +## [OpenAI](./openai.md) + +```python +model = outlines.models.openai( + "gpt-4o-mini", + api_key=os.environ["OPENAI_API_KEY"] +) +``` + + +# Feature Matrix +| | Transformers | Transformers Vision | vLLM | llama.cpp | ExLlamaV2 | MLXLM | OpenAI* | +|-------------------|--------------|---------------------|------|-----------|-----------|-------|---------| +| **Device** | | | | | | | | +| Cuda | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | N/A | +| Apple Silicon | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | N/A | +| x86 / AMD64 | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | N/A | +| **Sampling** | | | | | | | | +| Greedy | ✅ | ✅ | ✅ | ✅* | ✅ | ✅ | ❌ | +| Multinomial | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Multiple Samples | ✅ | ✅ | | ❌ | | ❌ | ✅ | +| Beam Search | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| **Generation** | | | | | | | | +| Batch | ✅ | ✅ | ✅ | ❌ | ? | ❌ | ❌ | +| Stream | ✅ | ❌ | ❌ | ✅ | ? | ✅ | ❌ | +| **`outlines.generate`** | | | | | | | | +| Text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Structured* | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | + + +## Caveats + +- OpenAI doesn't support structured generation due to limitations in their API and server implementation. +- `outlines.generate` ["Structured"](../generation/generation.md) includes methods such as `outlines.generate.regex`, `outlines.generate.json`, `outlines.generate.cfg`, etc. +- MLXLM only supports Apple Silicon. +- llama.cpp greedy sampling available via multinomial with `temperature = 0.0`. diff --git a/docs/reference/models/transformers_vision.md b/docs/reference/models/transformers_vision.md index b0ad6533b..e878f5866 100644 --- a/docs/reference/models/transformers_vision.md +++ b/docs/reference/models/transformers_vision.md @@ -2,9 +2,10 @@ Outlines allows seamless use of [vision models](https://huggingface.co/learn/computer-vision-course/en/unit4/multimodal-models/tasks-models-part1). -`outlines.models.transformers_vision` has shares interfaces with, and is based on [`outlines.models.transformers`](./transformers.md). +`outlines.models.transformers_vision` has shares interfaces with, and is based on [outlines.models.transformers](./transformers.md). Tasks supported include + - image + text -> text - video + text -> text diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index fdf3acb9e..4078215af 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -9,6 +9,10 @@ --md-code-fg-color: #FFFFFF; --md-text-font-family: "Inter"; --md-code-font: "Source Code Pro Custom"; + + /* don't inherit white fg color for mermaid diagrams from --md-code-fg-color */ + --md-mermaid-label-fg-color: #000000; + --md-mermaid-edge-color: #000000; } .index-pre-code { diff --git a/mkdocs.yml b/mkdocs.yml index e5f3417ac..d24ca9a63 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,7 +58,11 @@ markdown_extensions: pygments_lang_class: true noclasses: True pygments_style: nord - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true - pymdownx.inlinehilite @@ -117,24 +121,27 @@ nav: - Docs: - reference/index.md - Generation: + - Generation Overview: reference/generation/generation.md - Text: reference/text.md - Samplers: reference/samplers.md - - Structured generation: - - Classification: reference/choices.md - - Regex: reference/regex.md - - Type constraints: reference/format.md - - JSON (function calling): reference/json.md - - JSON mode: reference/json_mode.md - - Grammar: reference/cfg.md - - Custom FSM operations: reference/custom_fsm_ops.md + - Structured generation: + - Classification: reference/generation/choices.md + - Regex: reference/generation/regex.md + - Type constraints: reference/generation/format.md + - JSON (function calling): reference/generation/json.md + - Grammar: reference/generation/cfg.md + - Custom FSM operations: reference/generation/custom_fsm_ops.md + - Structured Generation Technical Explanation: reference/generation/structured_generation_explanation.md - Utilities: - Serve with vLLM: reference/serve/vllm.md - Custom types: reference/types.md - Prompt templating: reference/prompting.md - Outlines functions: reference/functions.md - Models: + - Models Overview: reference/models/models.md - Open source: - Transformers: reference/models/transformers.md + - Transformers Vision: reference/models/transformers_vision.md - Llama.cpp: reference/models/llamacpp.md - vLLM: reference/models/vllm.md - TGI: reference/models/tgi.md diff --git a/requirements-doc.txt b/requirements-doc.txt index b7bd2efbb..cf8c674a7 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -1,6 +1,7 @@ mkdocs mkdocs-material mkdocs-material[imaging] +mkdocs-mermaid2-plugin mkdocs-section-index mkdocstrings[python] mkdocs-git-committers-plugin-2 From b10c78fe08142dd6930f1d3b030f7dd5d1a651c6 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 27 Jul 2024 12:08:57 -0500 Subject: [PATCH 037/505] don't use gated models (mistral) in outlines-supported documentation --- README.md | 16 +++++----------- docs/cookbook/deploy-using-cerebrium.md | 2 +- docs/cookbook/index.md | 2 +- docs/cookbook/models_playing_chess.md | 2 +- docs/cookbook/structured_generation_workflow.md | 4 ++-- docs/quickstart.md | 6 +++--- docs/reference/generation/choices.md | 2 +- docs/reference/generation/custom_fsm_ops.md | 2 +- docs/reference/generation/format.md | 2 +- docs/reference/generation/json.md | 6 +++--- docs/reference/generation/regex.md | 2 +- docs/reference/generation/types.md | 4 ++-- docs/reference/models/transformers.md | 2 +- docs/reference/models/vllm.md | 10 +++++----- docs/reference/samplers.md | 10 +++++----- docs/reference/serve/vllm.md | 6 +++--- docs/reference/text.md | 10 +++++----- docs/welcome.md | 1 - 18 files changed, 41 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 96d02dbe1..364e77244 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,6 @@ is to ensure that there is a well-defined interface between their output and user-defined code. **Outlines** provides ways to control the generation of language models to make their output more predictable. -Before using mistral models, request access on huggingface [here](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2). -``` python -# login to access mistral model -from huggingface_hub import login -login() -``` ### Multiple choices You can reduce the completion to a choice between multiple possibilities: @@ -79,7 +73,7 @@ You can reduce the completion to a choice between multiple possibilities: ``` python import outlines -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") prompt = """You are a sentiment-labelling assistant. Is the following review positive or negative? @@ -122,7 +116,7 @@ hood: ``` python import outlines -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") prompt = "What is the IP address of the Google DNS servers? " @@ -185,7 +179,7 @@ class Character(BaseModel): strength: int -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") # Construct structured sequence generator generator = outlines.generate.json(model, Character) @@ -250,7 +244,7 @@ schema = '''{ } }''' -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = outlines.generate.json(model, schema) character = generator("Give me a character description") ``` @@ -338,7 +332,7 @@ def labelling(to_label, examples): {{ to_label }} // """ -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") prompt = labelling("Just awesome", examples) answer = outlines.generate.text(model)(prompt, max_tokens=100) ``` diff --git a/docs/cookbook/deploy-using-cerebrium.md b/docs/cookbook/deploy-using-cerebrium.md index b7609424c..6fbaab390 100644 --- a/docs/cookbook/deploy-using-cerebrium.md +++ b/docs/cookbook/deploy-using-cerebrium.md @@ -50,7 +50,7 @@ Running code in Cerebrium is like writing normal python with no special syntax. import outlines -model = outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") schema = """{ "title": "Character", diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 56334302a..58e84ae96 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -4,7 +4,7 @@ - [Named Entity Extraction](extraction.md): Extract information from pizza orders. - [Dating Profile](dating_profiles.md): Build dating profiles from descriptions using prompt templating and JSON-structured generation. - [Chain Of Density](chain_of_density.md): Summarize documents using chain of density prompting and JSON-structured generation. -- [Playing Chess](models_playing_chess.md): Make Mistral-7B play chess against itself using regex-structured generation. +- [Playing Chess](models_playing_chess.md): Make Phi-3 Mini play chess against itself using regex-structured generation. - [SimToM](simtom.md): Improve LLMs' Theory of Mind capabilities with perspective-taking prompting and JSON-structured generation. - [Q&A with Citations](qa-with-citations.md): Answer questions and provide citations using JSON-structured generation. - [Knowledge Graph Generation](knowledge_graph_extraction.md): Generate a Knowledge Graph from unstructured text using JSON-structured generation. diff --git a/docs/cookbook/models_playing_chess.md b/docs/cookbook/models_playing_chess.md index 3b4f10865..a0df2eb2d 100644 --- a/docs/cookbook/models_playing_chess.md +++ b/docs/cookbook/models_playing_chess.md @@ -23,7 +23,7 @@ Phi-2 will be playing against itself: ```python from outlines import models -model = models.transformers("microsoft/phi-2") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") ``` diff --git a/docs/cookbook/structured_generation_workflow.md b/docs/cookbook/structured_generation_workflow.md index c627ab9a8..b9d7c28a4 100644 --- a/docs/cookbook/structured_generation_workflow.md +++ b/docs/cookbook/structured_generation_workflow.md @@ -12,7 +12,7 @@ Before diving into how to use structure generation for this task let's start wit ```python import outlines -model_name = 'mistralai/Mistral-7B-Instruct-v0.2' +model_name = 'microsoft/Phi-3-mini-4k-instruct' model = outlines.models.transformers(model_name) ``` @@ -31,7 +31,7 @@ messages_phone = [ ] # This allows us to properly format our prompt for -# Mistrals 'Instruct' interface. +# Phi-3 Mini's 'Instruct' interface. prompt_phone = tokenizer.apply_chat_template(messages_phone, tokenize=False) ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index fcb524d8a..8ffffd2ed 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -84,13 +84,13 @@ Outlines can be deployed as a LLM service using [vLLM][vllm]{:target="_blank"} a First start the server: ```python -python -m outlines.serve.serve --model="mistralai/Mistral-7B-Instruct-v0.2" +python -m outlines.serve.serve --model="microsoft/Phi-3-mini-4k-instruct" ``` Or you can start the server with Outlines' official Docker image: ```bash -docker run -p 8000:8000 outlinesdev/outlines --model="mistralai/Mistral-7B-Instruct-v0.2" +docker run -p 8000:8000 outlinesdev/outlines --model="microsoft/Phi-3-mini-4k-instruct" ``` This will by default start a server at `http://127.0.0.1:8000` (check what the console says, though). Without the `--model` argument set, the OPT-125M model is used. @@ -186,7 +186,7 @@ Once you are done experimenting with a prompt and an output structure, it is use generate_joke = outlines.Function( tell_a_joke, Joke, - "mistralai/Mistral-7B-Instruct-v0.2" + "microsoft/Phi-3-mini-4k-instruct" ) ``` diff --git a/docs/reference/generation/choices.md b/docs/reference/generation/choices.md index 275b4a9a0..aed5af5ab 100644 --- a/docs/reference/generation/choices.md +++ b/docs/reference/generation/choices.md @@ -5,7 +5,7 @@ Oultines allows you to make sure the generated text is chosen between different ```python from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.choice(model, ["skirt", "dress", "pen", "jacket"]) answer = generator("Pick the odd word out: skirt, dress, pen, jacket") diff --git a/docs/reference/generation/custom_fsm_ops.md b/docs/reference/generation/custom_fsm_ops.md index 22f5ca6bb..5c4be96fa 100644 --- a/docs/reference/generation/custom_fsm_ops.md +++ b/docs/reference/generation/custom_fsm_ops.md @@ -29,7 +29,7 @@ difference_fsm_fsm.accepts('["a","blue","donkey"]') # True -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.fsm(model, difference_fsm) response = generator("Don't talk about pink elephants") ``` diff --git a/docs/reference/generation/format.md b/docs/reference/generation/format.md index 52b0dc5ec..749baa8b6 100644 --- a/docs/reference/generation/format.md +++ b/docs/reference/generation/format.md @@ -5,7 +5,7 @@ We can ask completions to be restricted to valid python types: ```python from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.format(model, int) answer = generator("When I was 6 my sister was half my age. Now I’m 70 how old is my sister?") print(answer) diff --git a/docs/reference/generation/json.md b/docs/reference/generation/json.md index a342395bd..da9f14729 100644 --- a/docs/reference/generation/json.md +++ b/docs/reference/generation/json.md @@ -25,7 +25,7 @@ class User(BaseModel): id: int -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.json(model, User) result = generator( "Create a user profile with the fields name, last_name and id" @@ -60,7 +60,7 @@ from pydantic import BaseModel from outlines import models from outlines import generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") schema = """ { @@ -94,7 +94,7 @@ from outlines import generate def add(a: int, b: int): return a + b -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.json(model, add) result = generator("Return two integers named a and b respectively. a is odd and b even.") diff --git a/docs/reference/generation/regex.md b/docs/reference/generation/regex.md index 093dd0e18..23df35567 100644 --- a/docs/reference/generation/regex.md +++ b/docs/reference/generation/regex.md @@ -5,7 +5,7 @@ Outlines can guarantee that the text generated by the LLM will be valid to a reg ```python from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.regex( model, diff --git a/docs/reference/generation/types.md b/docs/reference/generation/types.md index 2e02f45f1..5b83a5916 100644 --- a/docs/reference/generation/types.md +++ b/docs/reference/generation/types.md @@ -46,7 +46,7 @@ class Client(BaseModel): zip_code: locale.ZipCode -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.json(model, Client) result = generator( "Create a client profile with the fields name, phone_number and zip_code" @@ -63,7 +63,7 @@ from pydantic import BaseModel from outlines import models, generate, types -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.format(model, types.locale("us").PhoneNumber) result = generator( "Return a US Phone number: " diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index 80b70845f..15eabb682 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -15,7 +15,7 @@ Outlines provides an integration with the `torch` implementation of causal model ```python from outlines import models -model = models.transformers("mistralai/Mistral-7B-v0.3", device="cuda") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct", device="cuda") ``` If you need more fine-grained control you can also initialize the model and tokenizer separately: diff --git a/docs/reference/models/vllm.md b/docs/reference/models/vllm.md index 735ab4cc5..1380d3d2c 100644 --- a/docs/reference/models/vllm.md +++ b/docs/reference/models/vllm.md @@ -13,7 +13,7 @@ Outlines supports models available via vLLM's offline batched inference interfac ```python from outlines import models -model = models.vllm("mistralai/Mistral-7B-v0.1") +model = models.vllm("microsoft/Phi-3-mini-4k-instruct") ``` Or alternatively: @@ -22,7 +22,7 @@ Or alternatively: import vllm from outlines import models -llm = vllm.LLM("mistralai/Mistral-7B-v0.1") +llm = vllm.LLM("microsoft/Phi-3-mini-4k-instruct") model = models.VLLM(llm) ``` @@ -41,7 +41,7 @@ You can pass any parameter that you would normally pass to `vllm.LLM`, as keywor from outlines import models model = models.vllm( - "mistralai/Mistral-7B-v0.1", + "microsoft/Phi-3-mini-4k-instruct", trust_remote_code=True, gpu_memory_utilization=0.7 ) @@ -94,7 +94,7 @@ To run multi-GPU inference with vLLM you need to set the `tensor_parallel_size` from outlines import models model = models.vllm( - "mistralai/Mistral-7B-v0.1" + "microsoft/Phi-3-mini-4k-instruct" tensor_parallel_size=2 ) ``` @@ -120,7 +120,7 @@ from vllm.sampling_params import SamplingParams from outlines import models, generate -model = models.vllm("mistralai/Mistral-7b-v0.1") +model = models.vllm("microsoft/Phi-3-mini-4k-instruct") generator = generate.text(model) params = SamplingParams(n=2, frequency_penalty=1., min_tokens=2) diff --git a/docs/reference/samplers.md b/docs/reference/samplers.md index a0f652bbf..56e4e1f7e 100644 --- a/docs/reference/samplers.md +++ b/docs/reference/samplers.md @@ -10,7 +10,7 @@ Outlines defaults to the multinomial sampler without top-p or top-k sampling, an from outlines import models, generate, samplers -model = models.transformers("mistralai/Mistral-7B-0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") sampler = samplers.multinomial() generator = generate.text(model, sampler) @@ -26,7 +26,7 @@ You can ask the generator to take multiple samples by passing the number of samp from outlines import models, generate, samplers -model = models.transformers("mistralai/Mistral-7B-0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") sampler = samplers.multinomial(3) generator = generate.text(model, sampler) @@ -42,7 +42,7 @@ If you ask multiple samples for a batch of prompt the returned array will be of from outlines import models, generate, samplers -model = models.transformers("mistralai/Mistral-7B-0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") sampler = samplers.multinomial(3) generator = generate.text(model, sampler) @@ -78,7 +78,7 @@ You can also use the greedy sampler. For this you need to initialize the generat from outlines import models, generate, samplers -model = models.transformers("mistralai/Mistral-7B-0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") sampler = samplers.greedy() generator = generate.text(model, sampler) @@ -99,7 +99,7 @@ Outlines also comes with the Beam Search sampling algorithm: from outlines import models, generate, samplers -model = models.transformers("mistralai/Mistral-7B-Instruct-v0.2") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") sampler = samplers.beam_search(beams=5) generator = generate.text(model, sampler) diff --git a/docs/reference/serve/vllm.md b/docs/reference/serve/vllm.md index 1b4d4bf14..0b584568b 100644 --- a/docs/reference/serve/vllm.md +++ b/docs/reference/serve/vllm.md @@ -13,7 +13,7 @@ pip install outlines[serve] You can then start the server with: ```bash -python -m outlines.serve.serve --model="mistralai/Mistral-7B-Instruct-v0.2" +python -m outlines.serve.serve --model="microsoft/Phi-3-mini-4k-instruct" ``` This will by default start a server at `http://127.0.0.1:8000` (check what the console says, though). Without the `--model` argument set, the OPT-125M model is used. The `--model` argument allows you to specify any model of your choosing. @@ -22,7 +22,7 @@ To run inference on multiple GPUs you must pass the `--tensor-parallel-size` arg ```bash -python -m outlines.serve.serve --model="mistralai/Mistral-7B-Instruct-v0.2" --tensor-parallel-size 2 +python -m outlines.serve.serve --model="microsoft/Phi-3-mini-4k-instruct" --tensor-parallel-size 2 ``` @@ -31,7 +31,7 @@ python -m outlines.serve.serve --model="mistralai/Mistral-7B-Instruct-v0.2" --te You can install and run the server with Outlines' official Docker image using the command ```bash -docker run -p 8000:8000 outlinesdev/outlines --model="mistralai/Mistral-7B-Instruct-v0.2" +docker run -p 8000:8000 outlinesdev/outlines --model="microsoft/Phi-3-mini-4k-instruct" ``` ## Querying Endpoint diff --git a/docs/reference/text.md b/docs/reference/text.md index 423a26ba6..3d0d130f1 100644 --- a/docs/reference/text.md +++ b/docs/reference/text.md @@ -13,7 +13,7 @@ model = models.openai("gpt-4") generator = generate.text(model) answer = generator("What is 2+2?") -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.text(model) answer = generator("What is 2+2?") ``` @@ -28,7 +28,7 @@ Outlines allows you to stream the model's response by calling the `.stream` meth ```python from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.text(model) tokens = generator.stream("What is 2+2?") @@ -45,7 +45,7 @@ To limit the number of tokens generated you can pass the `max_tokens` positional ```python from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.text(model) answer = generator("What is 2+2?", 5) @@ -60,7 +60,7 @@ You can also ask the model to stop generating text after a given string has been ```python from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") generator = generate.text(model) answer = generator("What is 2+2?", stop_at=".") @@ -78,7 +78,7 @@ It can be useful to seed the generation in order to get reproducible results: import torch from outlines import models, generate -model = models.transformers("mistralai/Mistral-7B-v0.1") +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") seed = 789001 diff --git a/docs/welcome.md b/docs/welcome.md index 80632aff6..4c327c020 100644 --- a/docs/welcome.md +++ b/docs/welcome.md @@ -170,7 +170,6 @@ Outlines would not be what it is today without a community of dedicated develope Outlines was originally developed at [@NormalComputing](https://twitter.com/NormalComputing) by [@remilouf](https://twitter.com/remilouf) and [@BrandonTWillard](https://twitter.com/BrandonTWillard). It is now maintained by [.txt](https://dottxt.co). - [discord]: https://discord.gg/R9DSu34mGd [aesara]: https://github.com/aesara-devs [blackjax]: https://github.com/blackjax-devs/blackjax From 7218cdd0a5e69ac4267b9a17a9d900b41d95c83b Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 27 Jul 2024 12:10:37 -0500 Subject: [PATCH 038/505] =?UTF-8?q?Add=20Remi's=20AI=20Engineer=20World?= =?UTF-8?q?=E2=80=99s=20Fair=20Presentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 8ffffd2ed..2e1f9a6bb 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -216,7 +216,7 @@ Once you are done experimenting with a prompt and an output structure, it is use ## Going further -If you need more inspiration you can take a look at the [cookbook](cookbook/index.md). If you have any question, or requests for documentation please reach out to us on [GitHub](https://github.com/outlines-dev/outlines/discussions), [Twitter](https://twitter.com/remilouf) or [Discord](https://discord.gg/UppQmhEpe8). +If you need more inspiration you can take a look at the [cookbook](cookbook/index.md) or watch [Remi Louf's AI Engineer World’s Fair Presentation on Outlines](https://www.youtube.com/live/R0X7mPagRiE?t=775s). If you have any question, or requests for documentation please reach out to us on [GitHub](https://github.com/outlines-dev/outlines/discussions), [Twitter](https://twitter.com/remilouf) or [Discord](https://discord.gg/UppQmhEpe8). [pydantic]: https://docs.pydantic.dev/latest From 0c9f5bb3ce60825735c3d2d4b6952a0dbc7d11f8 Mon Sep 17 00:00:00 2001 From: parkervg Date: Tue, 30 Jul 2024 20:58:18 -0400 Subject: [PATCH 039/505] Make `model_class` required arg, default `processor_class` to `AutoProcessor` --- docs/reference/models/transformers_vision.md | 2 ++ outlines/models/transformers_vision.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/reference/models/transformers_vision.md b/docs/reference/models/transformers_vision.md index e878f5866..b488bca1a 100644 --- a/docs/reference/models/transformers_vision.md +++ b/docs/reference/models/transformers_vision.md @@ -19,9 +19,11 @@ Install dependencies Create the model ```python import outlines +from transformers import LlavaNextForConditionalGeneration model = outlines.models.transformers_vision( "llava-hf/llava-v1.6-mistral-7b-hf", + model_class=LlavaNextForConditionalGeneration, device="cuda", ) ``` diff --git a/outlines/models/transformers_vision.py b/outlines/models/transformers_vision.py index 876f9bff5..db4ba3e43 100644 --- a/outlines/models/transformers_vision.py +++ b/outlines/models/transformers_vision.py @@ -75,10 +75,10 @@ def stream( # type: ignore def transformers_vision( model_name: str, + model_class, device: Optional[str] = None, model_kwargs: dict = {}, processor_kwargs: dict = {}, - model_class=None, tokenizer_class=None, processor_class=None, ): @@ -88,6 +88,9 @@ def transformers_vision( ---------- model_name The name of the model as listed on Hugging Face's model page. + model_class + The `PreTrainedModel` class from transformers to use in initializing the vision model from `model_name`. + https://huggingface.co/docs/transformers/main/en/main_classes/model#transformers.PreTrainedModel device The device(s) on which the model should be loaded. This overrides the `device_map` entry in `model_kwargs` when provided. @@ -103,21 +106,20 @@ def transformers_vision( A `TransformersModel` model instance. """ - if model_class is None or tokenizer_class is None: + if processor_class is None or tokenizer_class is None: try: from transformers import ( AutoTokenizer, - LlavaNextForConditionalGeneration, - LlavaNextProcessor, + AutoProcessor, ) except ImportError: raise ImportError( "The `transformers` library needs to be installed in order to use `transformers` models." ) - if model_class is None: - model_class = LlavaNextForConditionalGeneration if processor_class is None: - processor_class = LlavaNextProcessor + processor_class = AutoProcessor + if tokenizer_class is None: + tokenizer_class = AutoTokenizer if device is not None: model_kwargs["device_map"] = device From a2dddeac41314ff50375aa48ec8830b744f8db67 Mon Sep 17 00:00:00 2001 From: parkervg Date: Wed, 31 Jul 2024 09:26:19 -0400 Subject: [PATCH 040/505] Running pre-commit hooks --- outlines/models/transformers_vision.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/outlines/models/transformers_vision.py b/outlines/models/transformers_vision.py index db4ba3e43..9709c26ae 100644 --- a/outlines/models/transformers_vision.py +++ b/outlines/models/transformers_vision.py @@ -108,10 +108,7 @@ def transformers_vision( """ if processor_class is None or tokenizer_class is None: try: - from transformers import ( - AutoTokenizer, - AutoProcessor, - ) + from transformers import AutoProcessor, AutoTokenizer except ImportError: raise ImportError( "The `transformers` library needs to be installed in order to use `transformers` models." From c6dd805abe6b4918fb74bbcd849c2d2dde75e044 Mon Sep 17 00:00:00 2001 From: parkervg Date: Wed, 31 Jul 2024 15:53:56 -0400 Subject: [PATCH 041/505] Pass `model_class` to transformers_vision in test_generate.py --- tests/generate/test_generate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index a86d3c253..8d5daa37e 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -57,9 +57,11 @@ def model_bart(tmp_path_factory): @pytest.fixture(scope="session") def model_transformers_vision(tmp_path_factory): import torch + from transformers import LlavaNextForConditionalGeneration return models.transformers_vision( "llava-hf/llava-v1.6-mistral-7b-hf", + model_class=LlavaNextForConditionalGeneration, device="cuda", model_kwargs=dict( torch_dtype=torch.bfloat16, From c5a1788e1f42e1e952369b496e547604f74771af Mon Sep 17 00:00:00 2001 From: parkervg Date: Wed, 31 Jul 2024 15:56:49 -0400 Subject: [PATCH 042/505] Add types-Pillow to mypy hook --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b528f0e8e..a9039b605 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,4 +30,4 @@ repos: - id: mypy args: [--allow-redefinition] exclude: ^examples/ - additional_dependencies: [types-tqdm] + additional_dependencies: [types-tqdm, types-Pillow] From eca98a8cf4875712bba51bfe63fcd7c07c97005b Mon Sep 17 00:00:00 2001 From: parkervg Date: Wed, 31 Jul 2024 15:56:57 -0400 Subject: [PATCH 043/505] Add CPU-based test for transformers_vision models --- .../test_integration_transformers_vision.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/generate/test_integration_transformers_vision.py diff --git a/tests/generate/test_integration_transformers_vision.py b/tests/generate/test_integration_transformers_vision.py new file mode 100644 index 000000000..62bc6da53 --- /dev/null +++ b/tests/generate/test_integration_transformers_vision.py @@ -0,0 +1,41 @@ +from io import BytesIO +from urllib.request import urlopen + +import pytest +from PIL import Image +from transformers import LlavaForConditionalGeneration + +import outlines +from outlines.models.transformers_vision import transformers_vision + + +@pytest.fixture(scope="session") +def model(tmp_path_factory): + return transformers_vision( + "trl-internal-testing/tiny-random-LlavaForConditionalGeneration", + model_class=LlavaForConditionalGeneration, + device="cpu", + ) + + +@pytest.fixture(scope="session") +def image(tmp_path_factory): + def img_from_url(url): + img_byte_stream = BytesIO(urlopen(url).read()) + return Image.open(img_byte_stream).convert("RGB") + + return img_from_url( + "https://upload.wikimedia.org/wikipedia/commons/2/25/Siam_lilacpoint.jpg" + ) + + +def test_text_gen(model, image): + assert model.model.device.type == "cpu" + description_generator = outlines.generate.text(model) + sequence = description_generator( + "<|im_start|>user\n\nWhat is this?<|im_end|>\n<|im_start|>assistant\n", + [image], + seed=10000, + max_tokens=10, + ) + assert isinstance(sequence, str) From 894f240b02e9bd113153fde548570800b84d49ac Mon Sep 17 00:00:00 2001 From: parkervg Date: Thu, 1 Aug 2024 09:19:10 -0400 Subject: [PATCH 044/505] Adding `apply_chat_template` argument to VisionSequenceGeneratorAdapter --- outlines/generate/api.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index ad01377c0..dea3108d5 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -1,6 +1,6 @@ import datetime from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union +from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union, cast from outlines.generate.generator import sequence_generator from outlines.samplers import BeamSearchSampler, GreedySampler, MultinomialSampler @@ -534,8 +534,9 @@ def stream( class VisionSequenceGeneratorAdapter(SequenceGeneratorAdapter): def __call__( # type: ignore self, - prompts: Union[str, List[str]], + prompts: Union[str, List[str], List[dict], List[List[dict]]], media: Union[str, Any], + apply_chat_template: bool = False, max_tokens: Optional[int] = None, stop_at: Optional[Union[str, List[str]]] = None, seed: Optional[int] = None, @@ -546,6 +547,17 @@ def __call__( # type: ignore Media: A URI to construct media or media object itself. Used as AutoProcessor argument. """ + if apply_chat_template: + # Transform the huggingface conversation object into the string that this + # model expects. + # https://huggingface.co/docs/transformers/main/en/chat_templating + prompts = ( + [self.model.processor.apply_chat_template(p) for p in prompts] + if isinstance(prompts[0], list) + else self.model.processor.apply_chat_template(prompts) + ) + prompts = cast(Union[str, List[str]], prompts) + prompts, media = self._validate_prompt_media_types(prompts, media) generation_params = self.prepare_generation_parameters( @@ -565,14 +577,26 @@ def __call__( # type: ignore def stream( # type: ignore self, - prompts: Union[str, List[str]], + prompts: Union[str, List[str], List[dict], List[List[dict]]], media: List[Union[str, Any, List[Union[str, Any]]]], + apply_chat_template: bool = False, max_tokens: Optional[int] = None, stop_at: Optional[Union[str, List[str]]] = None, seed: Optional[int] = None, **model_specific_params, ): """Return a text generator from a prompt or a list of prompts.""" + if apply_chat_template: + # Transform the huggingface conversation object into the string that this + # model expects. + # https://huggingface.co/docs/transformers/main/en/chat_templating + prompts = ( + [self.model.processor.apply_chat_template(p) for p in prompts] + if isinstance(prompts[0], list) + else self.model.processor.apply_chat_template(prompts) + ) + prompts = cast(Union[str, List[str]], prompts) + prompts, media = self._validate_prompt_media_types(prompts, media) generation_params = self.prepare_generation_parameters( max_tokens, stop_at, seed From 3a98e44ebbaa501d6bceedacedc10432b39b92ef Mon Sep 17 00:00:00 2001 From: parkervg Date: Thu, 1 Aug 2024 09:19:53 -0400 Subject: [PATCH 045/505] More comprehensive TransformersVision test cases Multi-image context, using `apply_chat_template` --- .../test_integration_transformers_vision.py | 132 ++++++++++++++++-- 1 file changed, 119 insertions(+), 13 deletions(-) diff --git a/tests/generate/test_integration_transformers_vision.py b/tests/generate/test_integration_transformers_vision.py index 62bc6da53..c976f04a9 100644 --- a/tests/generate/test_integration_transformers_vision.py +++ b/tests/generate/test_integration_transformers_vision.py @@ -3,11 +3,22 @@ import pytest from PIL import Image -from transformers import LlavaForConditionalGeneration +from transformers import AutoProcessor, LlavaForConditionalGeneration import outlines from outlines.models.transformers_vision import transformers_vision +IMAGE_URLS = [ + "https://upload.wikimedia.org/wikipedia/commons/2/25/Siam_lilacpoint.jpg", + "https://upload.wikimedia.org/wikipedia/commons/7/71/2010-kodiak-bear-1.jpg", + "https://upload.wikimedia.org/wikipedia/commons/b/be/Tamias-rufus-001.jpg", +] + + +def img_from_url(url): + img_byte_stream = BytesIO(urlopen(url).read()) + return Image.open(img_byte_stream).convert("RGB") + @pytest.fixture(scope="session") def model(tmp_path_factory): @@ -19,23 +30,118 @@ def model(tmp_path_factory): @pytest.fixture(scope="session") -def image(tmp_path_factory): - def img_from_url(url): - img_byte_stream = BytesIO(urlopen(url).read()) - return Image.open(img_byte_stream).convert("RGB") +def larger_model_with_processor(tmp_path_factory): + return transformers_vision( + "llava-hf/llava-interleave-qwen-0.5b-hf", + model_class=LlavaForConditionalGeneration, + device="cpu", + ) + + +@pytest.fixture(scope="session") +def processor(tmp_path_factory): + return AutoProcessor.from_pretrained("llava-hf/llava-interleave-qwen-0.5b-hf") + + +def test_single_image_text_gen(model, processor): + conversation = [ + { + "role": "user", + "content": [{"type": "text", "text": "What is this?"}, {"type": "image"}], + }, + ] + generator = outlines.generate.text(model) + sequence = generator( + processor.apply_chat_template(conversation), + [img_from_url(IMAGE_URLS[0])], + seed=10000, + max_tokens=10, + ) + assert isinstance(sequence, str) + + +def test_multi_image_text_gen(model, processor): + """If the length of image tags and number of images we pass are > 1 and equal, + we should yield a successful generation. + """ + conversation = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What do all these have in common?"}, + ] + + [{"type": "image"} for _ in range(len(IMAGE_URLS))], + }, + ] + generator = outlines.generate.text(model) + sequence = generator( + processor.apply_chat_template(conversation), + [img_from_url(i) for i in IMAGE_URLS], + seed=10000, + max_tokens=10, + ) + assert isinstance(sequence, str) + + +def test_mismatched_image_text_gen(model, processor): + """If the length of image tags and number of images we pass are unequal, + we should raise an error. + """ + conversation = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "I'm passing 3 images, but only 1 image tag"}, + {"type": "image"}, + ], + }, + ] + generator = outlines.generate.text(model) + with pytest.raises(ValueError): + _ = generator( + processor.apply_chat_template(conversation), + [img_from_url(i) for i in IMAGE_URLS], + seed=10000, + max_tokens=10, + ) + - return img_from_url( - "https://upload.wikimedia.org/wikipedia/commons/2/25/Siam_lilacpoint.jpg" +def test_default_apply_chat_template(larger_model_with_processor): + """If we pass in a list of dicts in the format that huggingface chat_template expects, + and specify `apply_chat_template=True` when calling the generator, + we should yield a successful generation. + """ + conversation = [ + { + "role": "user", + "content": [{"type": "text", "text": "What is this?"}, {"type": "image"}], + }, + ] + generator = outlines.generate.text(larger_model_with_processor) + sequence = generator( + conversation, + [img_from_url(IMAGE_URLS[0])], + seed=10000, + max_tokens=10, + apply_chat_template=True, ) + assert isinstance(sequence, str) -def test_text_gen(model, image): - assert model.model.device.type == "cpu" - description_generator = outlines.generate.text(model) - sequence = description_generator( - "<|im_start|>user\n\nWhat is this?<|im_end|>\n<|im_start|>assistant\n", - [image], +def test_single_image_choice(model, processor): + conversation = [ + { + "role": "user", + "content": [{"type": "text", "text": "What is this?"}, {"type": "image"}], + }, + ] + choices = ["cat", "dog"] + generator = outlines.generate.choice(model, choices) + sequence = generator( + processor.apply_chat_template(conversation), + [img_from_url(IMAGE_URLS[0])], seed=10000, max_tokens=10, ) assert isinstance(sequence, str) + assert sequence in choices From a363568e9bc0e08cbc4e23161024a9e4c9772d28 Mon Sep 17 00:00:00 2001 From: parkervg Date: Thu, 1 Aug 2024 17:43:30 -0400 Subject: [PATCH 046/505] Revert "Adding `apply_chat_template` argument to VisionSequenceGeneratorAdapter" This reverts commit 6200d402601a744ce483ffaec286f01b468fb507. --- outlines/generate/api.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index dea3108d5..ad01377c0 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -1,6 +1,6 @@ import datetime from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union from outlines.generate.generator import sequence_generator from outlines.samplers import BeamSearchSampler, GreedySampler, MultinomialSampler @@ -534,9 +534,8 @@ def stream( class VisionSequenceGeneratorAdapter(SequenceGeneratorAdapter): def __call__( # type: ignore self, - prompts: Union[str, List[str], List[dict], List[List[dict]]], + prompts: Union[str, List[str]], media: Union[str, Any], - apply_chat_template: bool = False, max_tokens: Optional[int] = None, stop_at: Optional[Union[str, List[str]]] = None, seed: Optional[int] = None, @@ -547,17 +546,6 @@ def __call__( # type: ignore Media: A URI to construct media or media object itself. Used as AutoProcessor argument. """ - if apply_chat_template: - # Transform the huggingface conversation object into the string that this - # model expects. - # https://huggingface.co/docs/transformers/main/en/chat_templating - prompts = ( - [self.model.processor.apply_chat_template(p) for p in prompts] - if isinstance(prompts[0], list) - else self.model.processor.apply_chat_template(prompts) - ) - prompts = cast(Union[str, List[str]], prompts) - prompts, media = self._validate_prompt_media_types(prompts, media) generation_params = self.prepare_generation_parameters( @@ -577,26 +565,14 @@ def __call__( # type: ignore def stream( # type: ignore self, - prompts: Union[str, List[str], List[dict], List[List[dict]]], + prompts: Union[str, List[str]], media: List[Union[str, Any, List[Union[str, Any]]]], - apply_chat_template: bool = False, max_tokens: Optional[int] = None, stop_at: Optional[Union[str, List[str]]] = None, seed: Optional[int] = None, **model_specific_params, ): """Return a text generator from a prompt or a list of prompts.""" - if apply_chat_template: - # Transform the huggingface conversation object into the string that this - # model expects. - # https://huggingface.co/docs/transformers/main/en/chat_templating - prompts = ( - [self.model.processor.apply_chat_template(p) for p in prompts] - if isinstance(prompts[0], list) - else self.model.processor.apply_chat_template(prompts) - ) - prompts = cast(Union[str, List[str]], prompts) - prompts, media = self._validate_prompt_media_types(prompts, media) generation_params = self.prepare_generation_parameters( max_tokens, stop_at, seed From c2d92b78fc0d5e1afbeadcb59e624f9e2b210a7b Mon Sep 17 00:00:00 2001 From: parkervg Date: Thu, 1 Aug 2024 17:45:37 -0400 Subject: [PATCH 047/505] Remove `apply_chat_template` test --- .../test_integration_transformers_vision.py | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/tests/generate/test_integration_transformers_vision.py b/tests/generate/test_integration_transformers_vision.py index c976f04a9..28b516c57 100644 --- a/tests/generate/test_integration_transformers_vision.py +++ b/tests/generate/test_integration_transformers_vision.py @@ -29,15 +29,6 @@ def model(tmp_path_factory): ) -@pytest.fixture(scope="session") -def larger_model_with_processor(tmp_path_factory): - return transformers_vision( - "llava-hf/llava-interleave-qwen-0.5b-hf", - model_class=LlavaForConditionalGeneration, - device="cpu", - ) - - @pytest.fixture(scope="session") def processor(tmp_path_factory): return AutoProcessor.from_pretrained("llava-hf/llava-interleave-qwen-0.5b-hf") @@ -106,28 +97,6 @@ def test_mismatched_image_text_gen(model, processor): ) -def test_default_apply_chat_template(larger_model_with_processor): - """If we pass in a list of dicts in the format that huggingface chat_template expects, - and specify `apply_chat_template=True` when calling the generator, - we should yield a successful generation. - """ - conversation = [ - { - "role": "user", - "content": [{"type": "text", "text": "What is this?"}, {"type": "image"}], - }, - ] - generator = outlines.generate.text(larger_model_with_processor) - sequence = generator( - conversation, - [img_from_url(IMAGE_URLS[0])], - seed=10000, - max_tokens=10, - apply_chat_template=True, - ) - assert isinstance(sequence, str) - - def test_single_image_choice(model, processor): conversation = [ { From 90e588c7f30fbcaa2bff8aa50e2c11e480d3fbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Tue, 13 Aug 2024 17:44:23 +0200 Subject: [PATCH 048/505] Fix details in the documentation --- docs/cookbook/chain_of_thought.md | 4 +- docs/cookbook/dating_profiles.md | 4 +- docs/cookbook/index.md | 2 + docs/cookbook/knowledge_graph_extraction.md | 4 +- docs/cookbook/qa-with-citations.md | 4 +- docs/cookbook/react_agent.md | 7 ++- docs/reference/generation/generation.md | 6 +++ .../structured_generation_explanation.md | 6 +-- docs/reference/models/models.md | 52 +++---------------- docs/reference/models/transformers.md | 23 ++++---- docs/stylesheets/extra.css | 8 +++ docs/welcome.md | 6 +-- mkdocs.yml | 8 +-- 13 files changed, 51 insertions(+), 83 deletions(-) diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md index d320feb8d..cc079a7ff 100644 --- a/docs/cookbook/chain_of_thought.md +++ b/docs/cookbook/chain_of_thought.md @@ -7,13 +7,13 @@ In this guide, we use [outlines](https://outlines-dev.github.io/outlines/) to ap We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: -```shell +```bash pip install llama-cpp-python ``` We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -```shell +```bash wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf ``` diff --git a/docs/cookbook/dating_profiles.md b/docs/cookbook/dating_profiles.md index f839b65fe..d0fb9b576 100644 --- a/docs/cookbook/dating_profiles.md +++ b/docs/cookbook/dating_profiles.md @@ -170,7 +170,7 @@ parsed_profile = DatingProfile.model_validate_json(profile) Here are a couple of results: -``` +```json { "bio": """I'm an ambitious lawyer with a casual and fashionable style. I love games and sports, but my true passion is preparing refreshing cocktails at @@ -199,7 +199,7 @@ Here are a couple of results: } ``` -``` +```json { "bio": """I’m a sexy lawyer with time on my hands. I love to game and play ping pong, but the real reason you should swipe to the right diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 58e84ae96..a844ce240 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -1,5 +1,7 @@ # Examples +This part of the documentation provides a few cookbooks that you can browse to get acquainted with the library and get some inspiration about what you could do with structured generation. Remember that you can easily change the model that is being used! + - [Classification](classification.md): Classify customer requests. - [Named Entity Extraction](extraction.md): Extract information from pizza orders. - [Dating Profile](dating_profiles.md): Build dating profiles from descriptions using prompt templating and JSON-structured generation. diff --git a/docs/cookbook/knowledge_graph_extraction.md b/docs/cookbook/knowledge_graph_extraction.md index c7e347dd4..c4c1dc75c 100644 --- a/docs/cookbook/knowledge_graph_extraction.md +++ b/docs/cookbook/knowledge_graph_extraction.md @@ -4,13 +4,13 @@ In this guide, we use [outlines](https://outlines-dev.github.io/outlines/) to ex We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: -```shell +```bash pip install llama-cpp-python ``` We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -```shell +```bash wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf ``` diff --git a/docs/cookbook/qa-with-citations.md b/docs/cookbook/qa-with-citations.md index cb39befe9..c2111617f 100644 --- a/docs/cookbook/qa-with-citations.md +++ b/docs/cookbook/qa-with-citations.md @@ -4,13 +4,13 @@ This tutorial is adapted from the [instructor-ollama notebook](https://github.co We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: -```shell +```bash pip install llama-cpp-python ``` We pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -```shell +```bash wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf ``` diff --git a/docs/cookbook/react_agent.md b/docs/cookbook/react_agent.md index 74930b70b..15fb964a0 100644 --- a/docs/cookbook/react_agent.md +++ b/docs/cookbook/react_agent.md @@ -8,13 +8,13 @@ Additionally, we give the LLM the possibility of using a scratchpad described in We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: -```shell +```bash pip install llama-cpp-python ``` We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -```shell +```bash wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf ``` @@ -55,9 +55,8 @@ def wikipedia(q): "srsearch": q, "format": "json" }).json()["query"]["search"][0]["snippet"] -``` -```python + def calculate(numexp): return eval(numexp) ``` diff --git a/docs/reference/generation/generation.md b/docs/reference/generation/generation.md index 88b963c72..0c090f8a7 100644 --- a/docs/reference/generation/generation.md +++ b/docs/reference/generation/generation.md @@ -208,3 +208,9 @@ result = generator("What is 2+2?") print(result) # 4 ``` + + +[jsonschema]: https://json-schema.org/learn/getting-started-step-by-step +[pydantic]: https://docs.pydantic.dev/latest +[cfg]: https://en.wikipedia.org/wiki/Context-free_grammar +[ebnf]: https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form diff --git a/docs/reference/generation/structured_generation_explanation.md b/docs/reference/generation/structured_generation_explanation.md index 0dedf060b..aa27a7a85 100644 --- a/docs/reference/generation/structured_generation_explanation.md +++ b/docs/reference/generation/structured_generation_explanation.md @@ -1,8 +1,4 @@ ---- -title: Structured Generation Explanation ---- - -# Structured Generation Explanation +# How does Outlines work? Language models generate text token by token, using the previous token sequence as input and sampled logits as output. This document explains the structured generation process, where only legal tokens are considered for the next step based on a predefined automata, e.g. a regex-defined [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM) or [Lark](https://lark-parser.readthedocs.io/en/stable/) grammar.` diff --git a/docs/reference/models/models.md b/docs/reference/models/models.md index dadfd34ad..34b5be4cf 100644 --- a/docs/reference/models/models.md +++ b/docs/reference/models/models.md @@ -4,60 +4,20 @@ title: Models # Models -Outlines supports generation using a number of inference engines (`outlines.models`) - -Loading a model using outlines follows a similar interface between inference engines. +Outlines supports generation using a number of inference engines (`outlines.models`). Loading a model using outlines follows a similar interface between inference engines: ```python import outlines -``` - -## [Transformers](./transformers.md) - -```python -model = outlines.models.transformers("microsoft/Phi-3-mini-128k-instruct", model_kwargs={}) -``` - -For additional arguments and use of other Huggingface Transformers model types see [Outlines' Transformers documentation](./transformers.md). - -## [Transformers Vision](./transformers_vision.md) - -```python +model = outlines.models.transformers("microsoft/Phi-3-mini-128k-instruct") model = outlines.models.transformers_vision("llava-hf/llava-v1.6-mistral-7b-hf") -``` - -For examples of generation and other details, see [Outlines' Transformers Vision documentation](./transformers_vision.md). - -## [vLLM](./vllm.md) - -```python model = outlines.models.vllm("microsoft/Phi-3-mini-128k-instruct") -``` - -## [llama.cpp](./llamacpp.md) - -```python -model = outlines.models.llamacpp("microsoft/Phi-3-mini-4k-instruct-gguf", "Phi-3-mini-4k-instruct-q4.gguf") -``` - -Additional llama.cpp parameters can be found in the [Outlines' llama.cpp documentation](./llamacpp.md). - -## [ExLlamaV2](./exllamav2.md) - -```python +model = outlines.models.llamacpp( + "microsoft/Phi-3-mini-4k-instruct-gguf", "Phi-3-mini-4k-instruct-q4.gguf" +) model = outlines.models.exllamav2("bartowski/Phi-3-mini-128k-instruct-exl2") -``` - -## [MLXLM](./mlxlmx.md) - -```python model = outlines.models.mlxlm("mlx-community/Phi-3-mini-4k-instruct-4bit") -``` -## [OpenAI](./openai.md) - -```python model = outlines.models.openai( "gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"] @@ -66,7 +26,7 @@ model = outlines.models.openai( # Feature Matrix -| | Transformers | Transformers Vision | vLLM | llama.cpp | ExLlamaV2 | MLXLM | OpenAI* | +| | [Transformers](transformers.md) | [Transformers Vision](transformers_vision.md) | [vLLM](vllm.md) | [llama.cpp](llamacpp.md) | [ExLlamaV2](exllamav2.md) | [MLXLM](mlxlm.md) | [OpenAI](openai.md)* | |-------------------|--------------|---------------------|------|-----------|-----------|-------|---------| | **Device** | | | | | | | | | Cuda | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | N/A | diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index 15eabb682..2a13e28ec 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -33,14 +33,15 @@ model = models.Transformers(llm, tokenizer) # Using Logits Processors There are two ways to use Outlines Structured Generation with HuggingFace Transformers: -- 1) Use Outlines generation wrapper, `outlines.models.transformers` -- 2) Use `OutlinesLogitsProcessor` with `transformers.AutoModelForCausalLM` + +1. Use Outlines generation wrapper, `outlines.models.transformers` +2. Use `OutlinesLogitsProcessor` with `transformers.AutoModelForCausalLM` Outlines supports a myriad of logits processors for structured generation. In these example, we will use the `RegexLogitsProcessor` which guarantees generated text matches the specified pattern. -## Example: `outlines.models.transformers` +## Using `outlines.models.transformers` -``` +```python import outlines time_regex_pattern = r"(0?[1-9]|1[0-2]):[0-5]\d\s?(am|pm)?" @@ -53,9 +54,9 @@ print(output) # 2:30 pm ``` -## Example: Direct `transformers` library use +## Using models initialized via the `transformers` library -``` +```python import outlines import transformers @@ -117,8 +118,9 @@ model = outlines.models.transformers( ) ``` -Further Reading: -- https://huggingface.co/docs/transformers/en/model_doc/mamba + + +Read [`transformers`'s documentation](https://huggingface.co/docs/transformers/en/model_doc/mamba) for more information. ### Encoder-Decoder Models @@ -144,8 +146,3 @@ model_bart = models.transformers( model_class=AutoModelForSeq2SeqLM, ) ``` - - -### Multi-Modal Models - -/Coming soon/ diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 4078215af..c4539ab80 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -96,6 +96,14 @@ background: #FFFFFF ! important } +.language-text { + background: #FFFFFF ! important +} + +.language-json { + background: #FFFFFF ! important +} + h1.title { color: #FFFFFF; margin: 0px 0px 5px; diff --git a/docs/welcome.md b/docs/welcome.md index 4c327c020..a7800f7ad 100644 --- a/docs/welcome.md +++ b/docs/welcome.md @@ -6,7 +6,7 @@ Outlines〰 is a Python library that allows you to use Large Language Model in a ## What models do you support? -We support [Openai](reference/models/openai.md), but the true power of Outlines〰 is unleashed with Open Source models available via the [transformers](reference/models/transformers.md), [llama.cpp](reference/models/llamacpp.md), [exllama2](reference/models/exllamav2.md) and [mamba_ssm](reference/models/mamba.md) libraries. If you want to build and maintain an integration with another library, [get in touch][discord]. +We support [Openai](reference/models/openai.md), but the true power of Outlines〰 is unleashed with Open Source models available via the [transformers](reference/models/transformers.md), [llama.cpp](reference/models/llamacpp.md), [exllama2](reference/models/exllamav2.md), [mlx-lm](reference/models/mlxlm.md) and [vllm](reference/models/vllm.md) models. If you want to build and maintain an integration with another library, [get in touch][discord]. ## What are the main features? @@ -17,7 +17,7 @@ We support [Openai](reference/models/openai.md), but the true power of Outlines No more invalid JSON outputs, 100% guaranteed - [:octicons-arrow-right-24: Generate JSON](reference/json.md) + [:octicons-arrow-right-24: Generate JSON](reference/generation/json.md) - :material-keyboard-outline:{ .lg .middle } __JSON mode for vLLM__ @@ -34,7 +34,7 @@ We support [Openai](reference/models/openai.md), but the true power of Outlines Generate text that parses correctly 100% of the time - [:octicons-arrow-right-24: Guide LLMs](reference/regex.md) + [:octicons-arrow-right-24: Guide LLMs](reference/generation/regex.md) - :material-chat-processing-outline:{ .lg .middle } __Powerful Prompt Templating__ diff --git a/mkdocs.yml b/mkdocs.yml index d24ca9a63..afc56528b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -121,24 +121,24 @@ nav: - Docs: - reference/index.md - Generation: - - Generation Overview: reference/generation/generation.md + - Overview: reference/generation/generation.md - Text: reference/text.md - Samplers: reference/samplers.md - Structured generation: + - How does it work?: reference/generation/structured_generation_explanation.md - Classification: reference/generation/choices.md - Regex: reference/generation/regex.md - Type constraints: reference/generation/format.md - JSON (function calling): reference/generation/json.md - Grammar: reference/generation/cfg.md - Custom FSM operations: reference/generation/custom_fsm_ops.md - - Structured Generation Technical Explanation: reference/generation/structured_generation_explanation.md - Utilities: - Serve with vLLM: reference/serve/vllm.md - - Custom types: reference/types.md + - Custom types: reference/generation/types.md - Prompt templating: reference/prompting.md - Outlines functions: reference/functions.md - Models: - - Models Overview: reference/models/models.md + - Overview: reference/models/models.md - Open source: - Transformers: reference/models/transformers.md - Transformers Vision: reference/models/transformers_vision.md From ce49ccd495e379518fb59f33cea252d903781733 Mon Sep 17 00:00:00 2001 From: Alonso Silva Allende Date: Tue, 13 Aug 2024 23:03:01 +0200 Subject: [PATCH 049/505] Change cookbook examples: Download model weights in the hub cache folder (#1097) Change cookbook examples: Download model weights in the hub cache folder --- docs/cookbook/chain_of_thought.md | 56 +++++++++++++------- docs/cookbook/knowledge_graph_extraction.md | 56 +++++++++++++------- docs/cookbook/qa-with-citations.md | 55 +++++++++++++------- docs/cookbook/react_agent.md | 57 +++++++++++++-------- 4 files changed, 148 insertions(+), 76 deletions(-) diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md index cc079a7ff..17c362696 100644 --- a/docs/cookbook/chain_of_thought.md +++ b/docs/cookbook/chain_of_thought.md @@ -11,30 +11,48 @@ We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp- pip install llama-cpp-python ``` -We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): +We download the model weights by passing the name of the repository on the HuggingFace Hub, and the filenames (or glob pattern): +```python +import llama_cpp +from outlines import generate, models -```bash -wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +model = models.llamacpp("NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF", + "Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False) ``` -We initialize the model: +??? note "(Optional) Store the model weights in a custom folder" -```python -from llama_cpp import Llama -from outlines import generate, models + By default the model weights are downloaded to the hub cache but if we want so store the weights in a custom folder, we pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -llm = Llama( - "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", - tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( - "NousResearch/Hermes-2-Pro-Llama-3-8B" - ), - n_gpu_layers=-1, - flash_attn=True, - n_ctx=8192, - verbose=False -) -model = models.LlamaCpp(llm) -``` + ```bash + wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf + ``` + + We initialize the model: + + ```python + import llama_cpp + from llama_cpp import Llama + from outlines import generate, models + + llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False + ) + ``` ## Chain of thought diff --git a/docs/cookbook/knowledge_graph_extraction.md b/docs/cookbook/knowledge_graph_extraction.md index c4c1dc75c..e25166bca 100644 --- a/docs/cookbook/knowledge_graph_extraction.md +++ b/docs/cookbook/knowledge_graph_extraction.md @@ -8,30 +8,48 @@ We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama pip install llama-cpp-python ``` -We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): +We download the model weights by passing the name of the repository on the HuggingFace Hub, and the filenames (or glob pattern): +```python +import llama_cpp +from outlines import generate, models -```bash -wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +model = models.llamacpp("NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF", + "Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False) ``` -We initialize the model: +??? note "(Optional) Store the model weights in a custom folder" -```python -from llama_cpp import Llama -from outlines import generate, models + By default the model weights are downloaded to the hub cache but if we want so store the weights in a custom folder, we pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -llm = Llama( - "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", - tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( - "NousResearch/Hermes-2-Pro-Llama-3-8B" - ), - n_gpu_layers=-1, - flash_attn=True, - n_ctx=8192, - verbose=False -) -model = models.LlamaCpp(llm) -``` + ```bash + wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf + ``` + + We initialize the model: + + ```python + import llama_cpp + from llama_cpp import Llama + from outlines import generate, models + + llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False + ) + ``` ## Knowledge Graph Extraction diff --git a/docs/cookbook/qa-with-citations.md b/docs/cookbook/qa-with-citations.md index c2111617f..79a2214c3 100644 --- a/docs/cookbook/qa-with-citations.md +++ b/docs/cookbook/qa-with-citations.md @@ -8,29 +8,48 @@ We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama pip install llama-cpp-python ``` -We pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): +We download the model weights by passing the name of the repository on the HuggingFace Hub, and the filenames (or glob pattern): +```python +import llama_cpp +from outlines import generate, models -```bash -wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf +model = models.llamacpp("NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF", + "Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False) ``` -We initialize the model: +??? note "(Optional) Store the model weights in a custom folder" -```python -from llama_cpp import Llama -from outlines import generate, models + By default the model weights are downloaded to the hub cache but if we want so store the weights in a custom folder, we pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): -llm = Llama( - "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", - tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( - "NousResearch/Hermes-2-Pro-Llama-3-8B" - ), - n_gpu_layers=-1, - flash_attn=True, - n_ctx=8192, - verbose=False -) -``` + ```bash + wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf + ``` + + We initialize the model: + + ```python + import llama_cpp + from llama_cpp import Llama + from outlines import generate, models + + llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False + ) + ``` ## Generate Synthetic Data diff --git a/docs/cookbook/react_agent.md b/docs/cookbook/react_agent.md index 15fb964a0..ca4829d5f 100644 --- a/docs/cookbook/react_agent.md +++ b/docs/cookbook/react_agent.md @@ -12,32 +12,49 @@ We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp- pip install llama-cpp-python ``` -We pull a quantized GGUF model, in this guide we pull [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): - -```bash -wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf -``` - -We initialize the model: - +We download the model weights by passing the name of the repository on the HuggingFace Hub, and the filenames (or glob pattern): ```python import llama_cpp -from llama_cpp import Llama from outlines import generate, models -llm = Llama( - "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", - tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( - "NousResearch/Hermes-2-Pro-Llama-3-8B" - ), - n_gpu_layers=-1, - flash_attn=True, - n_ctx=8192, - verbose=False -) -model = models.LlamaCpp(llm) +model = models.llamacpp("NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF", + "Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False) ``` +??? note "(Optional) Store the model weights in a custom folder" + + By default the model weights are downloaded to the hub cache but if we want so store the weights in a custom folder, we pull a quantized GGUF model [Hermes-2-Pro-Llama-3-8B](https://huggingface.co/NousResearch/Hermes-2-Theta-Llama-3-8B-GGUF) by [NousResearch](https://nousresearch.com/) from [HuggingFace](https://huggingface.co/): + + ```bash + wget https://hf.co/NousResearch/Hermes-2-Pro-Llama-3-8B-GGUF/resolve/main/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf + ``` + + We initialize the model: + + ```python + import llama_cpp + from llama_cpp import Llama + from outlines import generate, models + + llm = Llama( + "/path/to/model/Hermes-2-Pro-Llama-3-8B-Q4_K_M.gguf", + tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( + "NousResearch/Hermes-2-Pro-Llama-3-8B" + ), + n_gpu_layers=-1, + flash_attn=True, + n_ctx=8192, + verbose=False + ) + ``` + ## Build a ReAct agent In this example, we use two tools: From 8e94488d4ee3c5a29a919d0b9e19f7ea4170b1f4 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 14 Aug 2024 16:42:32 -0700 Subject: [PATCH 050/505] Correct variable name in chain-of-thought example --- docs/cookbook/chain_of_thought.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md index 17c362696..bd76f40b7 100644 --- a/docs/cookbook/chain_of_thought.md +++ b/docs/cookbook/chain_of_thought.md @@ -94,7 +94,7 @@ def generate_hermes_prompt(user_prompt): return ( "<|im_start|>system\n" "You are a world class AI model who answers questions in JSON " - f"Here's the json schema you must adhere to:\n\n{schema}\n<|im_end|>\n" + f"Here's the json schema you must adhere to:\n\n{json_schema}\n<|im_end|>\n" "<|im_start|>user\n" + user_prompt + "<|im_end|>" From e3ecbccfcfc4e7ed9d2243828c60fdeb10ea3df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 22 Jul 2024 15:37:09 +0200 Subject: [PATCH 051/505] Remove unused `llama.cpp` and `transformers` integrations --- outlines/integrations/llamacpp.py | 191 -------------------------- outlines/integrations/transformers.py | 159 --------------------- 2 files changed, 350 deletions(-) delete mode 100644 outlines/integrations/llamacpp.py delete mode 100644 outlines/integrations/transformers.py diff --git a/outlines/integrations/llamacpp.py b/outlines/integrations/llamacpp.py deleted file mode 100644 index 8e18c33e7..000000000 --- a/outlines/integrations/llamacpp.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Make LlamaCpp compatible with Outlines' structured generation. - - _______________________________ -/ Don't want to self-host? \ -\\ Try .json at http://dottxt.co / - ------------------------------- - \\ ^__^ - \\ (oo)\\_______ - (__)\\ )\\/\ - ||----w | - || || - -Copyright 2024- the Outlines developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import math -from typing import TYPE_CHECKING, Optional, Type, Union - -import numpy as np -import torch -from numpy.typing import NDArray -from pydantic import BaseModel - -from outlines.fsm.guide import CFGGuide, Guide, RegexGuide -from outlines.fsm.json_schema import build_regex_from_schema -from outlines.integrations.utils import convert_json_schema_to_str -from outlines.models.llamacpp import LlamaCppTokenizer - -if TYPE_CHECKING: - from llama_cpp import Llama - - -class LogitsProcessor: - """Bias LlamaCpp generation using a finite state machine. - - Attributes - ---------- - tokenizer - The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__(self, tokenizer: LlamaCppTokenizer, fsm: Guide): - """A FSM-based logits processor. - - Parameters - ---------- - tokenizer - The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. - """ - self.tokenizer = tokenizer - self._fsm_state = 0 - self.fsm: Guide = fsm - self._is_first_token = True - - def __call__( - self, input_ids: NDArray[np.int64], scores: NDArray[np.float32] - ) -> NDArray[np.float32]: - """Use the FSM to bias the logits before sampling the next token. - - Parameters - ---------- - input_ids - The input token ids. - scores - The logits. - - Returns - ------- - NDArray[np.float32] - The biased logits. - """ - if self._is_first_token: - self._is_first_token = False - else: - last_token = input_ids[-1] - self._fsm_state = self.fsm.get_next_state(self._fsm_state, last_token) - - allowed_tokens = self.fsm.get_next_instruction(self._fsm_state).tokens - - mask = torch.full((scores.shape[-1],), -math.inf, device="cpu").numpy() - mask[allowed_tokens] = 0 - biased_scores = scores + mask - - return biased_scores - - def copy(self) -> "LogitsProcessor": - """Return a copy of the logits processor.""" - return LogitsProcessor(tokenizer=self.tokenizer, fsm=self.fsm.copy()) - - -class RegexLogitsProcessor(LogitsProcessor): - """Bias LlamaCpp generation based on a regular expression. - - Attributes - ---------- - tokenizer - The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__(self, regex_string: str, llm: "Llama"): - """Compile the FSM that drives the regex-guided generation. - - Parameters - ---------- - regex_string - A string that represents a regular expression - llm - The Llama model. - """ - tokenizer = LlamaCppTokenizer(model=llm) - fsm = RegexGuide(regex_string, tokenizer) - super().__init__(tokenizer=tokenizer, fsm=fsm) - - -class JSONLogitsProcessor(RegexLogitsProcessor): - """Bias LlamaCpp generation based on a JSON schema. - - Attributes - ---------- - tokenizer - The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__( - self, - schema: Union[dict, Type[BaseModel], str], - llm: "Llama", - whitespace_pattern: Optional[str] = None, - ): - """Compile the FSM that drives the JSON-guided generation. - - Parameters - ---------- - schema - A JSON schema that encodes the structure we want the model to generate. - llm - The Llama model. - whitespace_pattern - Pattern to use for JSON syntactic whitespace (doesn't impact string - literals). For example, to allow only a single space or newline with - `whitespace_pattern=r"[\n ]?"` - """ - schema_str = convert_json_schema_to_str(json_schema=schema) - regex_string = build_regex_from_schema(schema_str, whitespace_pattern) - super().__init__(regex_string=regex_string, llm=llm) - - -class CFGLogitsProcessor(LogitsProcessor): - """Bias LlamaCpp generation based on a context-free grammar. - - Attributes - ---------- - llm - The Llama model. - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__(self, cfg_str: str, llm: "Llama"): - """Compile the FSM that drives the CFG-guided generation. - - Parameters - ---------- - cfg_str - A string that represents a grammar - llm - The Llama model. - """ - tokenizer = LlamaCppTokenizer(model=llm) - fsm = CFGGuide(cfg_string=cfg_str, tokenizer=tokenizer) - super().__init__(tokenizer=tokenizer, fsm=fsm) diff --git a/outlines/integrations/transformers.py b/outlines/integrations/transformers.py deleted file mode 100644 index 7c1bafd22..000000000 --- a/outlines/integrations/transformers.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Make Hugging Face transformers compatible with Outlines' structured generation. - - _______________________________ -/ Don't want to self-host? \ -\\ Try .json at http://dottxt.co / - ------------------------------- - \\ ^__^ - \\ (oo)\\_______ - (__)\\ )\\/\ - ||----w | - || || - -Copyright 2024- the Outlines developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -from collections import defaultdict -from typing import DefaultDict, Iterable, Optional, Type, Union - -import torch -from pydantic import BaseModel -from transformers import Pipeline, PreTrainedTokenizerBase - -from outlines.fsm.guide import RegexGuide -from outlines.fsm.json_schema import build_regex_from_schema -from outlines.integrations.utils import adapt_tokenizer, convert_json_schema_to_str - - -class RegexPrefixAllowedTokens: - """Bias transformers generation based on a regular expression. - - Attributes - ---------- - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__( - self, - regex_string: str, - tokenizer_or_pipe: Union[PreTrainedTokenizerBase, Pipeline], - ): - """Compile the FSM that drives the regex-structured generation. - - Parameters - ---------- - regex_string - A string that represents a regular expression. - tokenizer_or_pipe - The tokenizer of the model, or the pipeline object. - - Raises - ------ - ValueError - If the `tokenizer_or_pipe` parameter is not a tokenizer or a pipeline. - """ - if isinstance(tokenizer_or_pipe, Pipeline): - tokenizer = tokenizer_or_pipe.tokenizer - elif isinstance(tokenizer_or_pipe, PreTrainedTokenizerBase): - tokenizer = tokenizer_or_pipe - else: - raise ValueError( - "The tokenizer_or_pipe parameter must be a tokenizer or a pipeline." - ) - assert isinstance(tokenizer, PreTrainedTokenizerBase) - tokenizer = adapt_tokenizer(tokenizer=tokenizer) - self.fsm = RegexGuide(regex_string=regex_string, tokenizer=tokenizer) - self._fsm_state: DefaultDict[int, int] = defaultdict(int) - - # The generated text with `transformers` include the input token IDs as well, - # so we use this attribute to keep track of the input token IDs. This allows us - # to reset the FSM state when the input token IDs change, as well as to only - # apply the FSM to the generated tokens. - self._prefix = [-1] - - def __call__(self, batch_id: int, sent: torch.Tensor) -> Optional[Iterable[int]]: - """Use the FSM to bias the logits before sampling the next token. - - Parameters - ---------- - batch_id - The index of the current batch. - sent - The tokens of the current sentence. - - Returns - ------- - List[int] - The indices of the tokens that are allowed to be sampled next. - """ - input_ids = sent.tolist() - - # If the prefix token IDs have changed we assume that we are dealing with a new - # sample and reset the FSM state - if input_ids[: len(self._prefix)] != self._prefix: - self._fsm_state = defaultdict(int) - self._prefix = input_ids - seq_id = hash(tuple([])) - - else: - # Remove the prefix token IDs from the input token IDs, as the FSM should - # only be applied to the generated tokens - input_ids = input_ids[len(self._prefix) :] - - last_token = input_ids[-1] - last_seq_id = hash(tuple(input_ids[:-1])) - seq_id = hash(tuple(input_ids)) - self._fsm_state[seq_id] = self.fsm.get_next_state( - state=self._fsm_state[last_seq_id], token_id=last_token - ) - - allowed_tokens = self.fsm.get_next_instruction( - state=self._fsm_state[seq_id] - ).tokens - return allowed_tokens - - -class JSONPrefixAllowedTokens(RegexPrefixAllowedTokens): - """Bias transformers generation based on a JSON schema. - - Attributes - ---------- - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__( - self, - schema: Union[dict, Type[BaseModel], str], - tokenizer_or_pipe: Union[PreTrainedTokenizerBase, Pipeline], - whitespace_pattern: Optional[str] = None, - ): - """Compile the FSM that drives the JSON-guided generation. - - Parameters - ---------- - schema - A schema that encodes the structure we want the model to generate. - tokenizer_or_pipe - The tokenizer of the model, or the pipeline object. - whitespace_pattern - Pattern to use for JSON syntactic whitespace (doesn't impact string - literals). For example, to allow only a single space or newline with - `whitespace_pattern=r"[\n ]?"` - """ - schema_str = convert_json_schema_to_str(json_schema=schema) - regex_string = build_regex_from_schema(schema_str, whitespace_pattern) - super().__init__(regex_string=regex_string, tokenizer_or_pipe=tokenizer_or_pipe) From 03b61c5085a6f52c37513aaf47fc5a85f22170ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 22 Jul 2024 15:50:22 +0200 Subject: [PATCH 052/505] Move BaseModel conversion to `outlines.fsm` --- outlines/fsm/json_schema.py | 37 +++++++++++++++++++++++++++++-- outlines/integrations/utils.py | 33 --------------------------- outlines/processors/structured.py | 3 +-- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/outlines/fsm/json_schema.py b/outlines/fsm/json_schema.py index b29243001..98d2de59c 100644 --- a/outlines/fsm/json_schema.py +++ b/outlines/fsm/json_schema.py @@ -2,10 +2,10 @@ import json import re import warnings -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Type, Union from jsonschema.protocols import Validator -from pydantic import create_model +from pydantic import BaseModel, create_model from referencing import Registry, Resource from referencing._core import Resolver from referencing.jsonschema import DRAFT202012 @@ -86,6 +86,39 @@ def build_regex_from_schema(schema: str, whitespace_pattern: Optional[str] = Non return to_regex(resolver, content, whitespace_pattern) +def convert_json_schema_to_str(json_schema: Union[dict, str, Type[BaseModel]]) -> str: + """Convert a JSON schema to a string. + + Parameters + ---------- + json_schema + The JSON schema. + + Returns + ------- + str + The JSON schema converted to a string. + + Raises + ------ + ValueError + If the schema is not a dictionary, a string or a Pydantic class. + """ + if isinstance(json_schema, dict): + schema_str = json.dumps(json_schema) + elif isinstance(json_schema, str): + schema_str = json_schema + elif issubclass(json_schema, BaseModel): + schema_str = json.dumps(json_schema.model_json_schema()) + else: + raise ValueError( + f"Cannot parse schema {json_schema}. The schema must be either " + + "a Pydantic class, a dictionary or a string that contains the JSON " + + "schema specification" + ) + return schema_str + + def _get_num_items_pattern(min_items, max_items, whitespace_pattern): # Helper function for arrays and objects min_items = int(min_items or 0) diff --git a/outlines/integrations/utils.py b/outlines/integrations/utils.py index 9ac4e2a4f..edf77e5c7 100644 --- a/outlines/integrations/utils.py +++ b/outlines/integrations/utils.py @@ -68,36 +68,3 @@ def convert_token_to_string(token: Union[str, bytes]) -> str: tokenizer.convert_token_to_string = convert_token_to_string return tokenizer - - -def convert_json_schema_to_str(json_schema: Union[dict, str, Type[BaseModel]]) -> str: - """Convert a JSON schema to a string. - - Parameters - ---------- - json_schema - The JSON schema. - - Returns - ------- - str - The JSON schema converted to a string. - - Raises - ------ - ValueError - If the schema is not a dictionary, a string or a Pydantic class. - """ - if isinstance(json_schema, dict): - schema_str = json.dumps(json_schema) - elif isinstance(json_schema, str): - schema_str = json_schema - elif issubclass(json_schema, BaseModel): - schema_str = json.dumps(json_schema.model_json_schema()) - else: - raise ValueError( - f"Cannot parse schema {json_schema}. The schema must be either " - + "a Pydantic class, a dictionary or a string that contains the JSON " - + "schema specification" - ) - return schema_str diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index 0966a90db..f8d63d7b5 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -30,8 +30,7 @@ from pydantic import BaseModel from outlines.fsm.guide import CFGGuide, Guide, RegexGuide -from outlines.fsm.json_schema import build_regex_from_schema -from outlines.integrations.utils import convert_json_schema_to_str +from outlines.fsm.json_schema import build_regex_from_schema, convert_json_schema_to_str from .base_logits_processor import OutlinesLogitsProcessor From 0a70c42fea61fd8c8d1679f687315081f9ee8377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 22 Jul 2024 15:53:02 +0200 Subject: [PATCH 053/505] Move function that adapts the tokenizer --- outlines/integrations/__init__.py | 1 - outlines/integrations/utils.py | 70 ------------- outlines/integrations/vllm.py | 158 ------------------------------ outlines/models/vllm.py | 41 +++++++- outlines/serve/serve.py | 9 +- outlines/serve/vllm.py | 4 - tests/fsm/test_regex.py | 3 +- 7 files changed, 47 insertions(+), 239 deletions(-) delete mode 100644 outlines/integrations/__init__.py delete mode 100644 outlines/integrations/utils.py delete mode 100644 outlines/integrations/vllm.py delete mode 100644 outlines/serve/vllm.py diff --git a/outlines/integrations/__init__.py b/outlines/integrations/__init__.py deleted file mode 100644 index b0a90d5ea..000000000 --- a/outlines/integrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Utility functions and classes used to integrate `outlines` into other packages.""" diff --git a/outlines/integrations/utils.py b/outlines/integrations/utils.py deleted file mode 100644 index edf77e5c7..000000000 --- a/outlines/integrations/utils.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Utility functions used in integrations with other packages. - - _______________________________ -/ Don't want to self-host? \ -\\ Try .json at http://dottxt.co / - ------------------------------- - \\ ^__^ - \\ (oo)\\_______ - (__)\\ )\\/\ - ||----w | - || || - -Copyright 2024- the Outlines developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import json -from typing import Type, Union - -from pydantic import BaseModel -from transformers import SPIECE_UNDERLINE, PreTrainedTokenizerBase - - -def adapt_tokenizer(tokenizer: PreTrainedTokenizerBase) -> PreTrainedTokenizerBase: - """Adapt a tokenizer to use to compile the FSM. - - The API of Outlines tokenizers is slightly different to that of `transformers`. In - addition we need to handle the missing spaces to Llama's tokenizer to be able to - compile FSMs for this model. - - Parameters - ---------- - tokenizer - The tokenizer of the model. - - Returns - ------- - PreTrainedTokenizerBase - The adapted tokenizer. - """ - tokenizer.vocabulary = tokenizer.get_vocab() - tokenizer.special_tokens = set(tokenizer.all_special_tokens) - - def convert_token_to_string(token: Union[str, bytes]) -> str: - string = tokenizer.convert_tokens_to_string([token]) - - # A hack to handle missing spaces to HF's Llama tokenizers - if ( - type(token) is str - and token.startswith(SPIECE_UNDERLINE) - or token == "<0x20>" - ): - return " " + string - - return string - - tokenizer.convert_token_to_string = convert_token_to_string - - return tokenizer diff --git a/outlines/integrations/vllm.py b/outlines/integrations/vllm.py deleted file mode 100644 index 2a5f26e35..000000000 --- a/outlines/integrations/vllm.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Make vLLM compatible with Outlines' structured generation. - - _______________________________ -/ Don't want to self-host? \ -\\ Try .json at http://dottxt.co / - ------------------------------- - \\ ^__^ - \\ (oo)\\_______ - (__)\\ )\\/\ - ||----w | - || || - -Copyright 2024- the Outlines developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import math -from collections import defaultdict -from typing import TYPE_CHECKING, DefaultDict, Dict, List, Optional, Type, Union - -import torch -from pydantic import BaseModel - -from outlines.fsm.guide import RegexGuide -from outlines.fsm.json_schema import build_regex_from_schema -from outlines.integrations.utils import adapt_tokenizer, convert_json_schema_to_str - -if TYPE_CHECKING: - from vllm import LLM - - -class RegexLogitsProcessor: - """Bias vLLM generation based on a regular expression. - - Attributes - ---------- - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__(self, regex_string: str, llm: "LLM"): - """Compile the FSM that drives the regex-structured generation. - - Parameters - ---------- - regex_string - A string that represents a regular expression. - llm - The vLLM model. - - Raises - ------ - ValueError - If the provided LLM instance in `RegexLogitsProcessor` neither has a - `tokenizer` attribute or a `get_tokenizer` method. - """ - if hasattr(llm, "get_tokenizer"): - tokenizer = llm.get_tokenizer() - elif hasattr(llm, "tokenizer"): - if hasattr(llm.tokenizer, "tokenizer"): - tokenizer = llm.tokenizer.tokenizer - else: - tokenizer = llm.tokenizer - else: - raise ValueError( - "The provided LLM instance in `RegexLogitsProcessor` neither has a " - "`tokenizer` attribute or a `get_tokenizer` method." - ) - tokenizer = adapt_tokenizer(tokenizer=tokenizer) - self.mask_cache: Dict[int, torch.Tensor] = {} - self.fsm = RegexGuide(regex_string, tokenizer) - self._fsm_state: DefaultDict[int, int] = defaultdict(int) - - def __call__(self, input_ids: List[int], scores: torch.Tensor) -> torch.Tensor: - """Use the FSM to bias the logits before sampling the next token. - - Parameters - ---------- - input_ids - The tokens of the current sentence. - scores - The logits of the current sentence. - - Returns - ------- - torch.Tensor - The biased logits. - """ - seq_id = hash(tuple(input_ids)) - - # Initialize the FSM state dictionary if the input_ids are empty, as this means - # that the input_ids are the first tokens of the sequence. - if len(input_ids) > 0: - last_token = input_ids[-1] - last_seq_id = hash(tuple(input_ids[:-1])) - self._fsm_state[seq_id] = self.fsm.get_next_state( - state=self._fsm_state[last_seq_id], token_id=last_token - ) - - state_id = self._fsm_state[seq_id] - if state_id not in self.mask_cache: - allowed_tokens = self.fsm.get_next_instruction( - state=self._fsm_state[seq_id] - ).tokens - mask = torch.full((scores.shape[-1],), -math.inf) - mask[allowed_tokens] = 0 - mask = mask.pin_memory() - self.mask_cache[state_id] = mask - else: - mask = self.mask_cache[state_id] - mask = mask.to(device=scores.device, non_blocking=True) - biased_scores = scores + mask - - return biased_scores - - -class JSONLogitsProcessor(RegexLogitsProcessor): - """Bias vLLM generation based on a JSON schema. - - Attributes - ---------- - fsm - The finite state machine which is used to bias the logits. - """ - - def __init__( - self, - schema: Union[dict, Type[BaseModel], str], - llm: "LLM", - whitespace_pattern: Optional[str] = None, - ): - """Compile the FSM that drives the JSON-guided generation. - - Parameters - ---------- - schema - A JSON schema that encodes the structure we want the model to generate. - llm - The vLLM model. - whitespace_pattern - Pattern to use for JSON syntactic whitespace (doesn't impact string - literals). For example, to allow only a single space or newline with - `whitespace_pattern=r"[\n ]?"` - """ - schema_str = convert_json_schema_to_str(json_schema=schema) - regex_string = build_regex_from_schema(schema_str, whitespace_pattern) - super().__init__(regex_string=regex_string, llm=llm) diff --git a/outlines/models/vllm.py b/outlines/models/vllm.py index 2ae3d99d4..d1f97bde2 100644 --- a/outlines/models/vllm.py +++ b/outlines/models/vllm.py @@ -1,8 +1,9 @@ import dataclasses from typing import TYPE_CHECKING, List, Optional, Union +from transformers import SPIECE_UNDERLINE, PreTrainedTokenizerBase + from outlines.generate.api import GenerationParameters, SamplingParameters -from outlines.integrations.utils import adapt_tokenizer if TYPE_CHECKING: from vllm import LLM @@ -185,3 +186,41 @@ def vllm(model_name: str, **vllm_model_params): model = LLM(model_name, **vllm_model_params) return VLLM(model) + + +def adapt_tokenizer(tokenizer: PreTrainedTokenizerBase) -> PreTrainedTokenizerBase: + """Adapt a tokenizer to use to compile the FSM. + + The API of Outlines tokenizers is slightly different to that of `transformers`. In + addition we need to handle the missing spaces to Llama's tokenizer to be able to + compile FSMs for this model. + + Parameters + ---------- + tokenizer + The tokenizer of the model. + + Returns + ------- + PreTrainedTokenizerBase + The adapted tokenizer. + """ + tokenizer.vocabulary = tokenizer.get_vocab() + tokenizer.special_tokens = set(tokenizer.all_special_tokens) + + def convert_token_to_string(token: Union[str, bytes]) -> str: + string = tokenizer.convert_tokens_to_string([token]) + + # A hack to handle missing spaces to HF's Llama tokenizers + if ( + type(token) is str + and token.startswith(SPIECE_UNDERLINE) + or token == "<0x20>" + ): + return " " + string + + return string + + tokenizer.convert_token_to_string = convert_token_to_string + + return tokenizer diff --git a/outlines/serve/serve.py b/outlines/serve/serve.py index fb8c80139..998fbc459 100644 --- a/outlines/serve/serve.py +++ b/outlines/serve/serve.py @@ -35,12 +35,14 @@ from vllm.sampling_params import SamplingParams from vllm.utils import random_uuid -from outlines.integrations.vllm import JSONLogitsProcessor, RegexLogitsProcessor +from outlines.models.vllm import adapt_tokenizer +from outlines.processors import JSONLogitsProcessor, RegexLogitsProcessor TIMEOUT_KEEP_ALIVE = 5 # seconds. TIMEOUT_TO_PREVENT_DEADLOCK = 1 # seconds. app = FastAPI() engine = None +tokenizer = None @app.get("/health") @@ -69,9 +71,9 @@ async def generate(request: Request) -> Response: json_schema = request_dict.pop("schema", None) regex_string = request_dict.pop("regex", None) if json_schema is not None: - logits_processors = [JSONLogitsProcessor(json_schema, engine.engine)] + logits_processors = [JSONLogitsProcessor(json_schema, tokenizer)] elif regex_string is not None: - logits_processors = [RegexLogitsProcessor(regex_string, engine.engine)] + logits_processors = [RegexLogitsProcessor(regex_string, tokenizer)] else: logits_processors = [] @@ -124,6 +126,7 @@ async def stream_results() -> AsyncGenerator[bytes, None]: # Sets default for the model (`facebook/opt-125m`) engine = AsyncLLMEngine.from_engine_args(engine_args) + tokenizer = adapt_tokenizer(tokenizer=engine.engine.tokenizer.tokenizer) uvicorn.run( app, diff --git a/outlines/serve/vllm.py b/outlines/serve/vllm.py deleted file mode 100644 index ddc50b47d..000000000 --- a/outlines/serve/vllm.py +++ /dev/null @@ -1,4 +0,0 @@ -from outlines.integrations.vllm import ( # noqa[F401] - JSONLogitsProcessor, - RegexLogitsProcessor, -) diff --git a/tests/fsm/test_regex.py b/tests/fsm/test_regex.py index fa72ad0dc..f7aa4ae67 100644 --- a/tests/fsm/test_regex.py +++ b/tests/fsm/test_regex.py @@ -18,7 +18,6 @@ reduced_vocabulary, walk_fsm, ) -from outlines.integrations.utils import adapt_tokenizer from outlines.models.transformers import TransformerTokenizer @@ -714,6 +713,6 @@ def test_reduced_vocabulary_with_rare_tokens(rare_token): [2]: https://github.com/outlines-dev/outlines/pull/948 """ tokenizer = AutoTokenizer.from_pretrained("openai-community/gpt2") - tokenizer = adapt_tokenizer(tokenizer=tokenizer) + tokenizer = TransformerTokenizer(tokenizer=tokenizer) tokenizer.vocabulary[rare_token] = max(tokenizer.vocabulary.values()) + 1 reduced_vocabulary(tokenizer) From 900762b0f240c6220549d7d8594a221e6c9845e8 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Fri, 23 Aug 2024 13:17:51 -0500 Subject: [PATCH 054/505] Use relative coverage source paths --- .github/workflows/tests.yml | 13 +++++-------- pyproject.toml | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10879c78f..b76d723f2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,11 +36,13 @@ jobs: - name: Run tests run: | pytest --cov=outlines + env: + COVERAGE_FILE: .coverage.${{ steps.matrix-id.outputs.id }} - name: Upload coverage data uses: actions/upload-artifact@v3 with: name: coverage-data - path: .coverage* + path: .coverage.* if-no-files-found: ignore coverage: @@ -66,19 +68,14 @@ jobs: with: name: coverage-data - - name: Fetch master for coverage diff + - name: Fetch main for coverage diff run: | git fetch --no-tags --prune origin main - name: Combine coverage & fail if it's <100%. run: | - # Combine coverage files (not needed now, but maybe later) - # python -m coverage combine - - # Produce an html report with absolute coverage information + python -m coverage combine python -m coverage html --skip-covered --skip-empty - - # Report relative coverage and write to the workflow's summary python -m coverage xml diff-cover coverage.xml --markdown-report=coverage.md --fail-under=100 || (cat coverage.md >> $GITHUB_STEP_SUMMARY && exit 1) diff --git a/pyproject.toml b/pyproject.toml index ea0ac11c8..99d4f94e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ omit = [ "tests/*", ] branch = true +relative_files = true [tool.coverage.report] omit = [ From ce029a8052c4d631a7e4786d6228411c616d9e8f Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Wed, 28 Aug 2024 18:54:42 -0500 Subject: [PATCH 055/505] Add missing CI matrix step and use dev installs --- .github/workflows/tests.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b76d723f2..344dfb55e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,16 @@ jobs: - name: Set up test environment run: | python -m pip install --upgrade pip - pip install .[test] + pip install -e .[test] + - name: Create matrix id + id: matrix-id + env: + MATRIX_CONTEXT: ${{ toJson(matrix) }} + run: | + echo $MATRIX_CONTEXT + export MATRIX_ID=`echo $MATRIX_CONTEXT | md5sum | cut -c 1-32` + echo $MATRIX_ID + echo "::set-output name=id::$MATRIX_ID" - name: Run tests run: | pytest --cov=outlines From 067aedb49dc6eb530af5cbef8c1d0da553382e61 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 28 Aug 2024 23:27:57 -0700 Subject: [PATCH 056/505] Update modal example (#1111) I updated some of the text of the modal example to add more information on what's happening where, since Modal's general structure may be unfamiliar to new users. List of changes: - Removed version constraints on the image. I think this is a questionable choice so I'd love input -- in general, this should probably better be addressed by versioning the docs #999. There's an open PR #1059 here that can help with this too -- I'm happy to revert to pinned versions, or at least update to the current version of outlines (0.0.46). - Added a brief note that the Mistral model is gated and requires you to request access on HuggingFace. - Moved the environment variable setting from `import_model` to a `.env` call when the image is created. This is more idiomatic Modal code. It was previously addressed in #1058, but this should be more current. - More comments and prep. --- docs/cookbook/deploy-using-modal.md | 104 +++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/docs/cookbook/deploy-using-modal.md b/docs/cookbook/deploy-using-modal.md index 835924d2b..998e5d835 100644 --- a/docs/cookbook/deploy-using-modal.md +++ b/docs/cookbook/deploy-using-modal.md @@ -4,36 +4,83 @@ In this guide we will show you how you can use Modal to run programs written with Outlines on GPU in the cloud. +## Requirements + +We recommend installing `modal` and `outlines` in a virtual environment. You can create one with: + +```bash +python -m venv venv +source venv/bin/activate +``` + +Then install the required packages: + +```bash +pip install modal outlines +``` + ## Build the image -First we need to define our container image. We download the Mistral-7B-v0.1 model from HuggingFace as part of the definition of the image so it only needs to be done once (you need to provide an [access token](https://huggingface.co/settings/tokens)) +First we need to define our container image. If you need to access a gated model, you will need to provide an [access token](https://huggingface.co/settings/tokens). See the `.env` call below for how to provide a HuggingFace token. + +Setting a token is best done by setting an environment variable `HF_TOKEN` with your token. If you do not wish to do this, we provide a commented-out line in the code to set the token directly in the code. ```python from modal import Image, App, gpu +import os +# This creates a modal App object. Here we set the name to "outlines-app". +# There are other optional parameters like modal secrets, schedules, etc. +# See the documentation here: https://modal.com/docs/reference/modal.App app = App(name="outlines-app") +# Specify a language model to use. +# Another good model to use is "NousResearch/Hermes-2-Pro-Mistral-7B" +language_model = "mistral-community/Mistral-7B-v0.2" + +# Please set an environment variable HF_TOKEN with your Hugging Face API token. +# The code below (the .env({...}) part) will copy the token from your local +# environment to the container. +# More info on Image here: https://modal.com/docs/reference/modal.Image outlines_image = Image.debian_slim(python_version="3.11").pip_install( - "outlines==0.0.37", - "transformers==4.38.2", - "datasets==2.18.0", - "accelerate==0.27.2", -) + "outlines", + "transformers", + "datasets", + "accelerate", + "sentencepiece", +).env({ + # This will pull in your HF_TOKEN environment variable if you have one. + 'HF_TOKEN':os.environ['HF_TOKEN'] + + # To set the token directly in the code, uncomment the line below and replace + # 'YOUR_TOKEN' with the HuggingFace access token. + # 'HF_TOKEN':'YOUR_TOKEN' +}) +``` + +## Setting the container up +When running longer Modal apps, it's recommended to download your language model when the container starts, rather than when the function is called. This will cache the model for future runs. + +```python +# This function imports the model from Hugging Face. The modal container +# will call this function when it starts up. This is useful for +# downloading models, setting up environment variables, etc. def import_model(): - import os - os.environ["HF_TOKEN"] = "YOUR_HUGGINGFACE_TOKEN" import outlines - outlines.models.transformers("mistralai/Mistral-7B-Instruct-v0.2") + outlines.models.transformers(language_model) +# This line tells the container to run the import_model function when it starts. outlines_image = outlines_image.run_function(import_model) ``` -We will run the JSON-structured generation example [in the README](https://github.com/outlines-dev/outlines?tab=readme-ov-file#efficient-json-generation-following-a-json-schema), with the following schema: +## Define a schema -## Run inference +We will run the JSON-structured generation example [in the README](https://github.com/outlines-dev/outlines?tab=readme-ov-file#efficient-json-generation-following-a-json-schema), with the following schema: ```python +# Specify a schema for the character description. In this case, +# we want to generate a character with a name, age, armor, weapon, and strength. schema = """{ "title": "Character", "type": "object", @@ -72,48 +119,71 @@ schema = """{ }""" ``` -To make the inference work on Modal we need to wrap the corresponding function in a `@app.function` decorator. We pass to this decorator the image and GPU on which we want this function to run (here an A100 with 80GB memory): +To make the inference work on Modal we need to wrap the corresponding function in a `@app.function` decorator. We pass to this decorator the image and GPU on which we want this function to run. + +Let's choose an A100 with 80GB memory. Valid GPUs can be found [here](https://modal.com/docs/reference/modal.gpu). ```python +# Define a function that uses the image we chose, and specify the GPU +# and memory we want to use. @app.function(image=outlines_image, gpu=gpu.A100(size='80GB')) def generate( prompt: str = "Amiri, a 53 year old warrior woman with a sword and leather armor.", ): + # Remember, this function is being executed in the container, + # so we need to import the necessary libraries here. You should + # do this with any other libraries you might need. import outlines + # Load the model into memory. The import_model function above + # should have already downloaded the model, so this call + # only loads the model into GPU memory. model = outlines.models.transformers( - "mistralai/Mistral-7B-v0.1", device="cuda" + language_model, device="cuda" ) + # Generate a character description based on the prompt. + # We use the .json generation method -- we provide the + # - model: the model we loaded above + # - schema: the JSON schema we defined above generator = outlines.generate.json(model, schema) + + # Make sure you wrap your prompt in instruction tags ([INST] and [/INST]) + # to indicate that the prompt is an instruction. Instruction tags can vary + # by models, so make sure to check the model's documentation. character = generator( f"[INST]Give me a character description. Describe {prompt}.[/INST]" ) + # Print out the generated character. print(character) ``` -We then need to define a `local_entrypoint` to call our function `generate` remotely: +We then need to define a `local_entrypoint` to call our function `generate` remotely. ```python @app.local_entrypoint() def main( prompt: str = "Amiri, a 53 year old warrior woman with a sword and leather armor.", ): + # We use the "generate" function defined above -- note too that we are calling + # .remote() on the function. This tells modal to run the function in our cloud + # machine. If you want to run the function locally, you can call .local() instead, + # though this will require additional setup. generate.remote(prompt) ``` -Here `@app.local_entrypoint()` decorator defines `main` as the function to start from locally when running the Modal CLI. You can save above code to `example.py` (or use [this implementation](https://github.com/outlines-dev/outlines/blob/main/examples/modal_example.py)). Let's now see how to run the code on the cloud using the Modal CLI. +Here `@app.local_entrypoint()` decorator defines `main` as the function to start from locally when using the Modal CLI. You can save above code to `example.py` (or use [this implementation](https://github.com/outlines-dev/outlines/blob/main/examples/modal_example.py)). Let's now see how to run the code on the cloud using the Modal CLI. ## Run on the cloud -First install the Modal client from PyPi: +First install the Modal client from PyPi, if you have not already: ```bash pip install modal ``` -You then need to obtain a token from Modal. To do so easily, run the following command: +You then need to obtain a token from Modal. Run the following command: ```bash modal setup From 7cf63d6b7ed5d27863234fafbfe812fbd706c6bc Mon Sep 17 00:00:00 2001 From: Bill Metangmo <25366207+billmetangmo@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:45:57 +0200 Subject: [PATCH 057/505] Fix typo (#1120) --- docs/reference/serve/vllm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/serve/vllm.md b/docs/reference/serve/vllm.md index 0b584568b..8e2886c96 100644 --- a/docs/reference/serve/vllm.md +++ b/docs/reference/serve/vllm.md @@ -2,7 +2,7 @@ !!! tip "Would rather not self-host?" - If you want to get started quickly with JSON-structured generaton you can call instead [.json](https://h1xbpbfsf0w.typeform.com/to/ZgBCvJHF), a [.txt](http://dottxt.co) API that guarantees valid JSON. + If you want to get started quickly with JSON-structured generation you can call instead [.json](https://h1xbpbfsf0w.typeform.com/to/ZgBCvJHF), a [.txt](http://dottxt.co) API that guarantees valid JSON. Outlines can be deployed as an LLM service using the vLLM inference engine and a FastAPI server. vLLM is not installed by default so will need to install Outlines with: From 18eafb0944a7de6c8e801ec5454c84e88c4ddd0d Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 30 Aug 2024 13:59:35 -0400 Subject: [PATCH 058/505] pass text and images as kwargs to VLM processor --- outlines/models/transformers_vision.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/outlines/models/transformers_vision.py b/outlines/models/transformers_vision.py index 9709c26ae..772645b80 100644 --- a/outlines/models/transformers_vision.py +++ b/outlines/models/transformers_vision.py @@ -43,9 +43,9 @@ def generate( # type: ignore ------- The generated text """ - inputs = self.processor(prompts, media, padding=True, return_tensors="pt").to( - self.model.device - ) + inputs = self.processor( + text=prompts, images=media, padding=True, return_tensors="pt" + ).to(self.model.device) generation_kwargs = self._get_generation_kwargs( prompts, From f941fc79a5b7e2068705b4375027526af5f73509 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 27 Jul 2024 10:55:30 -0500 Subject: [PATCH 059/505] Update CFGGuide to use outlines.fsm.parsing. Enable generate.cfg --- benchmarks/bench_cfg_guide.py | 61 +++ .../reference/generation/creating_grammars.md | 99 ++++ mkdocs.yml | 1 + outlines/fsm/fsm.py | 24 +- outlines/fsm/guide.py | 282 +++++------ outlines/fsm/parsing.py | 67 ++- outlines/generate/cfg.py | 35 +- outlines/generate/fsm.py | 12 +- outlines/grammars/common.lark | 9 +- outlines/grammars/json.lark | 6 +- outlines/processors/__init__.py | 2 +- outlines/processors/structured.py | 112 +++-- .../arithmetic/lots_of_ops.arithmetic.test | 1 + .../arithmetic/simple_math.arithmetic.test | 1 + .../outlines.generate.samplers.mypy.json.test | 372 ++++++++++++++ tests/cfg_samples/json/simple_fruit.json.test | 20 + .../json/simple_fruit_no_indent.json.test | 1 + tests/fsm/test_cfg_guide.py | 457 ++++++++++++++++++ tests/fsm/test_fsm.py | 262 +--------- tests/fsm/test_guide.py | 292 ++++++----- tests/generate/test_generate.py | 34 ++ tests/generate/test_integration_llamacpp.py | 18 +- .../generate/test_integration_transformers.py | 8 +- 23 files changed, 1520 insertions(+), 656 deletions(-) create mode 100644 benchmarks/bench_cfg_guide.py create mode 100644 docs/reference/generation/creating_grammars.md create mode 100644 tests/cfg_samples/arithmetic/lots_of_ops.arithmetic.test create mode 100644 tests/cfg_samples/arithmetic/simple_math.arithmetic.test create mode 100644 tests/cfg_samples/json/outlines.generate.samplers.mypy.json.test create mode 100644 tests/cfg_samples/json/simple_fruit.json.test create mode 100644 tests/cfg_samples/json/simple_fruit_no_indent.json.test create mode 100644 tests/fsm/test_cfg_guide.py diff --git a/benchmarks/bench_cfg_guide.py b/benchmarks/bench_cfg_guide.py new file mode 100644 index 000000000..8f6de914a --- /dev/null +++ b/benchmarks/bench_cfg_guide.py @@ -0,0 +1,61 @@ +import random + +from transformers import AutoTokenizer + +import outlines.grammars +from outlines.caching import cache_disabled +from outlines.fsm.guide import CFGGuide +from outlines.models.transformers import TransformerTokenizer + +from .common import ensure_numba_compiled + +random.seed(42) + + +def get_tiny_tokenizer(): + """1000 tokens in vocabulary""" + return TransformerTokenizer( + AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-gpt2") + ) + + +benched_grammars = { + "json": outlines.grammars.json, + "arithmetic": outlines.grammars.arithmetic, +} + + +class CFGGuideBenchmark: + params = benched_grammars.keys() + + def setup(self, grammar_name): + self.tokenizer = get_tiny_tokenizer() + ensure_numba_compiled( + self.tokenizer + ) # numba not currently used, but will be in the future + self.prebuilt_cfg_guide = CFGGuide( + benched_grammars[grammar_name], self.tokenizer + ) + + @staticmethod + def _run_random_cfg(guide): + state = guide.initial_state + token_ids = list(guide.tokenizer.vocabulary.values()) + for i in range(40): + # simulate ordering of logits top prob to lowest prob + random.shuffle(token_ids) + # simulate sampling and state update + next_token_id = next(guide.iter_valid_token_ids(state, token_ids)) + state = guide.get_next_state(state, next_token_id) + + @cache_disabled() + def time_cfg_guide_setup(self, grammar_name): + CFGGuide(benched_grammars[grammar_name], self.tokenizer) + + @cache_disabled() + def time_cfg_guide_run(self, grammar): + self._run_random_cfg(self.prebuilt_cfg_guide) + + @cache_disabled() + def peakmem_cfg_guide_run(self, grammar): + self._run_random_cfg(self.prebuilt_cfg_guide) diff --git a/docs/reference/generation/creating_grammars.md b/docs/reference/generation/creating_grammars.md new file mode 100644 index 000000000..78e41282a --- /dev/null +++ b/docs/reference/generation/creating_grammars.md @@ -0,0 +1,99 @@ +# Overview + +Outlines allows the use of [Lark](https://github.com/lark-parser/lark) grammars to guide generation. These grammars are used to construct parsers that filter out incompatible tokens during the generation process The result is a generation that adheres to the grammar's production rules. + +# Primer on Creating Grammars + +To create grammars for Outlines, a solid understanding of Lark grammars is necessary. Here's how you can get started: + +- Read Lark's grammars documentations [here](https://lark-parser.readthedocs.io/en/latest/grammar.html). +- Review Outlines' existing grammars [here](/outlines/grammars). + + +# Compatibility With Outlines + +It's important to note that not all Lark grammars work with Outlines. Changes may be necessary to ensure compatability. + +### LALR(1) Parser + +Outlines utilizes Larks LALR(1) parser, meaning the grammar must be unambiguous at least up to the next token (one token lookahead). Read Lark's official LALR(1) parser documentation [here](https://lark-parser.readthedocs.io/en/stable/parsers.html#lalr-1). + +If your grammar is ambiguous, you will recieve the following error at runtime: + +``` +GrammarError: Reduce/Reduce collision in Terminal('B') between the following rules: +``` + +### Regex Terminal Restrictions + +Outlines converts terminals to finite state machines using the [Interegular](https://github.com/MegaIng/interegular/) library. Not all regular expressions work with Interegular, mitigation is described in the subsections which follow. + + +#### Avoid Lookarounds + +Examples of removing lookaround while maintaining the same functionality + +##### Example: Escaped String + +From Outlines' modified `ESCAPED_STRING` in [common.lark](/outlines/grammars/common.lark). + +Before: +``` +_STRING_INNER: /.*?/ +_STRING_ESC_INNER: _STRING_INNER /(?) +print(result) +``` + +# Converting +There are a few tools available for converting from other grammars to lark. These tools serve as a starting point. However, you will typically need to make additional adjustments to ensure full compatibility and proper functioning within Outlines. + +Tools: +- Larks built in "Nearley-to-Lark" converter https://lark-parser.readthedocs.io/en/latest/tools.html +- Convert ANTLR4 to Lark (Note, most antlr4 grammars are not LALR(1) compatible, so will require additional tweaking) https://github.com/kaby76/Domemtech.Trash/blob/main/src/trconvert/readme.md +- Extract EBNF from Yacc files https://www.bottlecaps.de/rr/ui + +Reference Grammars: +- Github Lark Grammars https://github.com/search?q=path%3A*.lark&type=code +- Github Nearley Grammars https://github.com/search?q=path%3A*.ne+%22-%3E%22&type=code +- Antlr4 grammars https://github.com/antlr/grammars-v4/ +- Grammar zoo https://slebok.github.io/zoo/index.html#html diff --git a/mkdocs.yml b/mkdocs.yml index afc56528b..bf863085a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -131,6 +131,7 @@ nav: - Type constraints: reference/generation/format.md - JSON (function calling): reference/generation/json.md - Grammar: reference/generation/cfg.md + - Creating Grammars: reference/generation/creating_grammars.md - Custom FSM operations: reference/generation/custom_fsm_ops.md - Utilities: - Serve with vLLM: reference/serve/vllm.md diff --git a/outlines/fsm/fsm.py b/outlines/fsm/fsm.py index 4a7fce8c9..bfcf55c03 100644 --- a/outlines/fsm/fsm.py +++ b/outlines/fsm/fsm.py @@ -1,7 +1,7 @@ import warnings from typing import TYPE_CHECKING, Iterable, NewType, Optional -from outlines.fsm.guide import CFGGuide, RegexGuide, StopAtEOSGuide +from outlines.fsm.guide import RegexGuide, StopAtEOSGuide if TYPE_CHECKING: from outlines.models.tokenizer import Tokenizer @@ -45,25 +45,3 @@ def allowed_token_ids(self, state: FSMState) -> Optional[Iterable[int]]: def next_state(self, state: FSMState, token_id: int) -> FSMState: return FSMState(self.get_next_state(state, token_id)) - - -class CFGFSM(CFGGuide): - """FSM to generate text that is in the language of a context-free grammar.""" - - def __init__(self, cfg_string: str, tokenizer): - warnings.warn( - UserWarning( - "The `CFGFSM` interface is deprecated and will be removed on 2024-06-01. Please use `CFGGuide` instead." - ) - ) - super().__init__(cfg_string, tokenizer) - - def allowed_token_ids(self, state: FSMState) -> Optional[Iterable[int]]: - return self.get_next_instruction(state).tokens - - def next_state(self, state: FSMState, token_id: int) -> FSMState: - return FSMState(self.get_next_state(state, token_id)) - - def copy(self) -> "CFGFSM": - """Create a copy of the FSM.""" - return CFGFSM(self.cfg_string, self.tokenizer) diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index 2e4415148..aa073d107 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -1,8 +1,12 @@ +import collections +import copy from dataclasses import dataclass from typing import ( TYPE_CHECKING, + Any, Callable, Dict, + Generator, List, Optional, Protocol, @@ -13,10 +17,12 @@ import interegular import torch -from lark import Lark +from lark.indenter import DedentError +from lark.lexer import UnexpectedCharacters, UnexpectedToken from outlines import grammars from outlines.caching import cache +from outlines.fsm.parsing import PartialLark, PartialParserState from outlines.fsm.regex import ( create_fsm_index_tokenizer, make_byte_level_fsm, @@ -69,13 +75,15 @@ class Guide(Protocol): """ - def get_next_instruction(self, state: int) -> Instruction: + initial_state: Any + + def get_next_instruction(self, state: Any) -> Instruction: ... - def get_next_state(self, state: int, token_id: int) -> int: + def get_next_state(self, state: Any, token_id: int) -> Any: ... - def is_final_state(self, state: int) -> bool: + def is_final_state(self, state: Any) -> bool: ... def copy(self) -> "Guide": @@ -86,7 +94,8 @@ class StopAtEOSGuide(Guide): """Guide to generate tokens until the EOS token has been generated.""" final_state = 1 - start_state = 0 + start_state = 0 # TODO: remove start_state, use only initial_state + initial_state = 0 def __init__(self, tokenizer: "Tokenizer"): """Initialize the generation guide. @@ -107,7 +116,7 @@ def get_next_state(self, state: int, token_id: int) -> int: if token_id == self.eos_token_id or state == self.final_state: return self.final_state - return self.start_state + return self.initial_state def is_final_state(self, state: int): return state == self.final_state @@ -300,178 +309,175 @@ def copy(self): return self +CFGState = collections.namedtuple("CFGState", ["parser_state", "prev_token"]) + + class CFGGuide(Guide): - """Guide to generate text that is in the language of a context-free grammar.""" + """Guide to generate text that is in the language of a context-free Lark grammar.""" def __init__(self, cfg_string: str, tokenizer): + """ + Construct the PartialLark parser and set the empty initial_state (PartialParserState) + """ self.cfg_string = cfg_string self.tokenizer = tokenizer - - self.parser = Lark( + self.eos_token_id = self.tokenizer.eos_token_id + self.parser = PartialLark( cfg_string, parser="lalr", - lexer="contextual", - propagate_positions=False, - maybe_placeholders=False, - regex=True, import_paths=[grammars.GRAMMAR_PATH], ) - self.terminal_regexps = dict() - for terminal in self.parser.terminals: - if terminal.pattern is not None: - self.terminal_regexps[terminal.name] = terminal.pattern.to_regexp() - self.terminal_regexps["$END"] = tokenizer.eos_token - - self.generation = "" - self.reset_state = False - self.allow_eos = False - self.regex_fsm: RegexGuide - - self.check_last = False - self.proposal_last: List[int] = [] - self.regex_fsm_last: RegexGuide - - self.start_state = 0 - self.final_state = -1 - - def get_next_instruction(self, state: int) -> Instruction: - """Generate an instruction for the next step. - - Upon initialization, the CFG incremental parser is used to determine the - first regex and construct the first FSM to generate the first terminal. + self.initial_state = CFGState( + parser_state=self.parser.parse(""), prev_token=None + ) - This FSM is used for proposals until either: + def get_next_instruction(self, state: CFGState) -> Instruction: + """Return the next instruction for guided generation. - - The FSM is exhausted, and its only remaining option is the EOS token, - in which case we feed the generated terminal to the - CFG incremental parser and allow it to propose the next regex - corresponding to the next set of valid terminals. - - The current FSM can be exhausted, but the EOS token is not the only - remaining option. In this case we allow proposal of current terminal - extensions, store the current FSM and its state, then also use the CFG - parser to propose a new regex corresponding to terminating the current - terminal and starting the next one. The model can then sample from - either of these sets to determine whether to extend the current - terminal or terminate it and start the next one. + Current lazy approach: + - For each token in the vocabulary + - create a copy of the parsers state + - add the tokens to the parsers input text + - if valid, add token to returned tokens - The CFG incremental parser is allowed to propose the EOS token from any accepting state, - and once it is generated, the FSM will continue to always generate the EOS token. + Further refinements are necessary for performant text processing. Parameters ---------- state - The current state of the FSM. + The guides current PartialParserState, or None if complete Returns ------- - A list that contains the tokens to mask. + A `Generate` instance that contains the model and the allowed token ids. """ - if self.is_final_state(state): - return Write([self.tokenizer.eos_token_id]) - proposal: List[int] = [] - if self.generation != "": - if self.check_last: - proposer = self.regex_fsm_last - else: - proposer = self.regex_fsm + if state.parser_state is None: + return Write(torch.tensor([self.eos_token_id])) - instruction = proposer.get_next_instruction(state) + valid_tokens = list( + self.iter_valid_token_ids(state, self.tokenizer.vocabulary.values()) + ) + if len(valid_tokens) == 1: + return Write(torch.tensor(valid_tokens)) + return Generate(torch.tensor(valid_tokens)) - assert instruction.tokens is not None + def iter_valid_token_ids( + self, state: CFGState, candidate_token_ids: list + ) -> Generator[int, None, None]: + """ + Iterate over the given token_ids and yield those that are valid for the current parser state. - if isinstance(instruction, Write): - proposal += instruction.tokens + Parameters + ---------- + parser_state + The current state of the parser, or None if complete. + token_ids + The list of token ids to check for validity. + + Yields + ------ + int + Valid token ids. + """ + if state.parser_state is None: + yield self.eos_token_id + return + + for token_id in candidate_token_ids: + if token_id == self.eos_token_id: + if self.can_terminate_state(state): + yield token_id else: - proposal += instruction.tokens - - if self.tokenizer.eos_token_id not in proposal: - return Generate(proposal) - - self.check_last = False - proposal = [x for x in proposal if x != self.tokenizer.eos_token_id] - if len(proposal) > 0: - self.check_last = True - self.proposal_last = proposal.copy() - self.regex_fsm_last = proposer - - interactive = self.parser.parse_interactive(self.generation) - interactive.exhaust_lexer() - - options = {self.terminal_regexps[x] for x in interactive.accepts()} - # add %ignore terminals - options |= {self.terminal_regexps[x] for x in self.parser.lexer_conf.ignore} - - if self.terminal_regexps["$END"] in options: - options.remove(self.terminal_regexps["$END"]) - if len(options) == 0: - return Write([self.tokenizer.eos_token_id]) - self.allow_eos = True - options.add("") - assert len(options) > 1 - - regex_string = r"(" + r"|".join([r"(" + x + r")" for x in options]) + r")" - self.regex_fsm = RegexGuide(regex_string, self.tokenizer) - self.reset_state = True - - instruction = self.regex_fsm.get_next_instruction(self.start_state) - - assert instruction.tokens is not None - - if isinstance(instruction, Write): - proposal += instruction.tokens - else: - proposal += instruction.tokens - - if self.allow_eos: - self.allow_eos = False - else: - proposal = [x for x in proposal if x != self.tokenizer.eos_token_id] - assert len(proposal) > 0 - - return Generate(proposal) - - def get_next_state(self, state: int, token_id: int) -> int: - """Update the state of the guide. - - Transitions the underlying regex FSM to its next state. - If at max tokens or EOS token, transition permanently to the final state. - Update stored partial generations for subsequent incremental parsing. + try: + self._get_parser_state_token_applied(state, int(token_id)) + yield token_id + except ( + ValueError, + EOFError, + UnexpectedToken, + UnexpectedCharacters, + DedentError, + ): + pass + + def get_next_state(self, state: CFGState, token_id: int) -> CFGState: + """ + Update the state of the guide. + Decode the token_id, and calculate the new parser_state with the token applied. Parameters ---------- state - The current state of the FSM. + The guides current PartialParserState, or None if complete token_id The id of the token that was just generated. Returns ------- - The new state of the FSM. - """ + The guides new PartialParserState - # We need to return the final state when in the final state because we - # then generate EOS tokens instead of stopping the generation. - if token_id == self.tokenizer.eos_token_id or state == self.final_state: - return self.final_state - - self.generation += self.tokenizer.decode([token_id])[0] + """ + if state.parser_state is None or token_id == self.eos_token_id: + parser_state = None + else: + parser_state = self._get_parser_state_token_applied(state, int(token_id)) + return CFGState(parser_state=parser_state, prev_token=token_id) - if self.check_last: - if token_id in self.proposal_last: - return self.regex_fsm_last.get_next_state(state, token_id) - self.check_last = False + def _get_parser_state_token_applied( + self, state: CFGState, token_id: int + ) -> PartialParserState: + """ + Don't mutate `parser_state`, copy to protect - if self.reset_state: - self.reset_state = False - state = self.start_state + Get the token string + - if first token in generation: tokenizer.decode (no leading whitespace) + - else: normalized (with possibly leading whitespace) - return self.regex_fsm.get_next_state(state, token_id) + Don't allow empty ("") tokens, raise ValueError + """ + parser_state = copy.copy(state.parser_state) # prevent side effects - def is_final_state(self, state: int) -> bool: - return state == self.final_state + # normalize + if state.prev_token is None: + new_token_str = self.tokenizer.decode([token_id])[0] + else: + prev_token_str = self.tokenizer.decode([[state.prev_token]])[0] + combined_token_str = self.tokenizer.decode([[state.prev_token, token_id]])[ + 0 + ] + new_token_str = combined_token_str[len(prev_token_str) :] + + if new_token_str == "": + raise ValueError("empty next token") + + # update parser with new token + parser_state.lexer.state.text += new_token_str + self.parser.parse_from_state(parser_state, is_end=False) + + return parser_state + + def is_final_state(self, state: CFGState) -> bool: + # TODO: remove this method, use can_terminate_state and must_terminate_state + # here and in RegexGuide per https://github.com/outlines-dev/outlines/issues/885 + return self.can_terminate_state(state) + + def can_terminate_state(self, state: CFGState) -> bool: + """Generation is allowed to terminate""" + if state.parser_state is not None: + try: + copy.copy(state.parser_state).feed_eof() + except UnexpectedToken: + return False + return True + + def must_terminate_state(self, state: CFGState) -> bool: + """Generation must terminate, no legal continuations""" + return state.parser_state is None or set(state.parser_state.accepts()).issubset( + {"$END"} + ) def copy(self) -> "CFGGuide": - """Create a copy of the FSM.""" + """Create a copy of the Guide.""" return CFGGuide(self.cfg_string, self.tokenizer) diff --git a/outlines/fsm/parsing.py b/outlines/fsm/parsing.py index e4fa7b764..f780fb46e 100644 --- a/outlines/fsm/parsing.py +++ b/outlines/fsm/parsing.py @@ -447,6 +447,49 @@ def feed_token_no_stack(self, token, is_end=False): if is_end and state_stack[-1] == end_state: return + def feed_eof(self): + last_token = self.lexer.state.last_token + + if last_token is None: + eof_token = self.lexer._Token("$END", "", 0, 1, 1) + else: + eof_token = Token.new_borrow_pos("$END", "", last_token) + + new_token_is_legal = ( + last_token is None + or last_token.type != "partial" + or any(ti.is_final for ti in last_token.value.terminals_and_info) + ) + if new_token_is_legal: + self.feed_token(eof_token, is_end=True) + else: + raise UnexpectedToken(eof_token, [], state=self, interactive_parser=None) + + def choices(self): + return self.parse_conf.parse_table.states[self.position] + + def accepts(self): + """ + Adapted from https://github.com/lark-parser/lark/blob/be542c2ff6d968817df019b8bf03f37b3111c08c/lark/parsers/lalr_interactive_parser.py#L95 + Returns the set of possible tokens that will advance the parser into a new valid state. + """ + accepts = set() + conf_no_callbacks = copy(self.parse_conf) + # We don't want to call callbacks here since those might have arbitrary side effects + # and are unnecessarily slow. + conf_no_callbacks.callbacks = {} + for t in self.choices(): + if t.isupper(): # is terminal? + new_state = copy(self) + new_state.parse_conf = conf_no_callbacks + try: + new_state.feed_token(new_state.lexer._Token(t, "")) + except UnexpectedToken: + pass + else: + accepts.add(t) + return accepts + def __copy__(self): return type(self)( self.parse_conf, @@ -483,12 +526,7 @@ def parse_from_state(self, state, last_token=None, is_end=False): state.feed_token(token) if is_end and (not token or token.type != "partial"): - end_token = ( - Token.new_borrow_pos("$END", "", token) - if token - else Token("$END", "", 0, 1, 1) - ) - state.feed_token(end_token, True) + state.feed_eof() return state except UnexpectedInput as e: @@ -614,6 +652,8 @@ def __init__(self, conf: "LexerConf", states, always_accept=()): lexer_conf.terminals = [ terminals_by_name[n] for n in accepts if n in terminals_by_name ] + if not lexer_conf.terminals: + continue lexer = PartialBasicLexer(lexer_conf) lexer_by_symbols[key] = lexer @@ -626,9 +666,22 @@ def lex(self, lexer_state: LexerState, parser_state: Any) -> Iterator[Token]: try: while True: lexer = self.lexers[parser_state.position] - yield lexer.next_token(lexer_state, parser_state) + next_tok = lexer.next_token(lexer_state, parser_state) + yield next_tok except EOFError: pass + except KeyError: + if len(lexer_state.text) > lexer_state.line_ctr.char_pos: + raise UnexpectedCharacters( + lexer_state.text, + lexer_state.line_ctr.char_pos, + lexer_state.line_ctr.line, + lexer_state.line_ctr.column, + allowed=False, + token_history=lexer_state.last_token and [lexer_state.last_token], + state=parser_state, + terminals_by_name=self.root_lexer.terminals, + ) class PartialBasicLexer(BasicLexer): diff --git a/outlines/generate/cfg.py b/outlines/generate/cfg.py index 0df833067..4f372f209 100644 --- a/outlines/generate/cfg.py +++ b/outlines/generate/cfg.py @@ -1,7 +1,10 @@ from functools import singledispatch -from outlines.generate.api import SequenceGeneratorAdapter -from outlines.models import OpenAI +from outlines.generate.api import ( + SequenceGeneratorAdapter, + VisionSequenceGeneratorAdapter, +) +from outlines.models import ExLlamaV2Model, LlamaCpp, OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial @@ -14,8 +17,7 @@ def cfg( Arguments --------- model: - An instance of `Transformer` that represents a model from the - `transformers` library. + An `outlines.model` instance. sampler: The sampling algorithm to use to generate token ids from the logits distribution. @@ -25,13 +27,32 @@ def cfg( A `SequenceGeneratorAdapter` instance that generates text. """ + from outlines.processors import CFGLogitsProcessor + + logits_processor = CFGLogitsProcessor(cfg_str, tokenizer=model.tokenizer) + return SequenceGeneratorAdapter(model, logits_processor, sampler) + + +@cfg.register(TransformersVision) +def cfg_vision(model, cfg_str: str, sampler: Sampler = multinomial()): + from outlines.processors import CFGLogitsProcessor + + logits_processor = CFGLogitsProcessor(cfg_str, tokenizer=model.tokenizer) + return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) + + +@cfg.register(ExLlamaV2Model) +def cfg_exllamav2(model, cfg_str: str, sampler: Sampler = multinomial()): raise NotImplementedError( - f"The CFG Logits processor is not available for {type(model)}. " - + "Please subscribe to https://github.com/outlines-dev/outlines/issues/684" - + " for updates on the fix." + "Not yet available, track progress in https://github.com/outlines-dev/outlines/pull/1010" ) +@cfg.register(LlamaCpp) +def cfg_llamacpp(model, cfg_str: str, sampler: Sampler = multinomial()): + raise NotImplementedError("Not yet available due to bug in llama_cpp tokenizer") + + @cfg.register(OpenAI) def cfg_openai(model, cfg_str: str, sampler: Sampler = multinomial()): raise NotImplementedError( diff --git a/outlines/generate/fsm.py b/outlines/generate/fsm.py index 03fe512b9..a9338836a 100644 --- a/outlines/generate/fsm.py +++ b/outlines/generate/fsm.py @@ -16,19 +16,19 @@ def fsm( model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() ) -> SequenceGeneratorAdapter: - from outlines.processors import FSMLogitsProcessor + from outlines.processors import GuideLogitsProcessor - fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) - logits_processor = FSMLogitsProcessor(tokenizer=model.tokenizer, fsm=fsm) + guide = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) + logits_processor = GuideLogitsProcessor(tokenizer=model.tokenizer, guide=guide) return SequenceGeneratorAdapter(model, logits_processor, sampler) @fsm.register(TransformersVision) def fsm_vision(model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial()): - from outlines.processors import FSMLogitsProcessor + from outlines.processors import GuideLogitsProcessor - fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) - logits_processor = FSMLogitsProcessor(tokenizer=model.tokenizer, fsm=fsm) + guide = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) + logits_processor = GuideLogitsProcessor(tokenizer=model.tokenizer, guide=guide) return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) diff --git a/outlines/grammars/common.lark b/outlines/grammars/common.lark index 801c27e97..ee5e00c50 100644 --- a/outlines/grammars/common.lark +++ b/outlines/grammars/common.lark @@ -43,11 +43,14 @@ SIGNED_FLOAT: ["+"|"-"] FLOAT NUMBER: FLOAT | INT SIGNED_NUMBER: ["+"|"-"] NUMBER -// -// TODO: Working escaped_string -// UNESCAPED_STRING: /\"[^"]*\"/ +// based on `outlines/fsm/json_schema.py` +_NON_CONTROL_CHAR: /([^"\\\x00-\x1F\x7F-\x9F])/ +_ESCAPED_CHAR: /\\/ (_NON_CONTROL_CHAR | /\\/ | /"/) +ESCAPED_STRING_INNER: _NON_CONTROL_CHAR | _ESCAPED_CHAR +ESCAPED_STRING: /"/ ESCAPED_STRING_INNER* /"/ + // diff --git a/outlines/grammars/json.lark b/outlines/grammars/json.lark index 72af448ce..7429fa558 100644 --- a/outlines/grammars/json.lark +++ b/outlines/grammars/json.lark @@ -2,7 +2,7 @@ ?value: object | array -| UNESCAPED_STRING +| ESCAPED_STRING | SIGNED_NUMBER -> number | "true" -> true | "false" -> false @@ -10,9 +10,9 @@ array : "[" [value ("," value)*] "]" object : "{" [pair ("," pair)*] "}" -pair : UNESCAPED_STRING ":" value +pair : ESCAPED_STRING ":" value -%import common.UNESCAPED_STRING +%import common.ESCAPED_STRING %import common.SIGNED_NUMBER %import common.WS diff --git a/outlines/processors/__init__.py b/outlines/processors/__init__.py index 22c10d905..f0f0f829b 100644 --- a/outlines/processors/__init__.py +++ b/outlines/processors/__init__.py @@ -1,6 +1,6 @@ from .structured import ( CFGLogitsProcessor, - FSMLogitsProcessor, + GuideLogitsProcessor, JSONLogitsProcessor, OutlinesLogitsProcessor, RegexLogitsProcessor, diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index f8d63d7b5..93d68727f 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -24,7 +24,7 @@ limitations under the License. """ import math -from typing import TYPE_CHECKING, Dict, List, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union import torch from pydantic import BaseModel @@ -38,36 +38,41 @@ from outlines.models.tokenizer import Tokenizer -class FSMLogitsProcessor(OutlinesLogitsProcessor): - """Bias generation using a finite state machine. +class GuideLogitsProcessor(OutlinesLogitsProcessor): + """Bias generation using a finite Attributes ---------- tokenizer The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. + guide + The `outlines.fsm.Guide` which is used to bias the logits. """ - def __init__(self, tokenizer: "Tokenizer", fsm: Guide): - """A FSM-based logits processor. + tokenizer: "Tokenizer" + guide: Guide + _guide_states: Dict[int, Any] + _seq_start_idx: Optional[int] + + def __init__(self, tokenizer: "Tokenizer", guide: Guide): + """A Guide-based logits processor. Parameters ---------- tokenizer The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. + guide + The `outlines.fsm.Guide. which is used to bias the logits. """ self.tokenizer = tokenizer - self._fsm_states: Dict[int, int] = {hash(tuple([])): 0} - self.fsm: Guide = fsm - self._seq_start_idx: Optional[int] = None + self.guide = guide + self._guide_states = {hash(tuple([])): self.guide.initial_state} + self._seq_start_idx = None def process_logits( self, input_ids: List[List[int]], logits: torch.Tensor ) -> torch.Tensor: - """Use the FSM to bias the logits before sampling the next token. + """Use the Guide to bias the logits before sampling the next token. Parameters ---------- @@ -90,38 +95,38 @@ def process_logits( gen_ids = seq_ids[self._seq_start_idx :] curr_state_key = hash(tuple(gen_ids)) - if curr_state_key not in self._fsm_states: - prev_state = self._fsm_states[hash(tuple(gen_ids[:-1]))] - curr_state = self.fsm.get_next_state(prev_state, gen_ids[-1]) - self._fsm_states[curr_state_key] = curr_state + if curr_state_key not in self._guide_states: + prev_state = self._guide_states[hash(tuple(gen_ids[:-1]))] + curr_state = self.guide.get_next_state(prev_state, gen_ids[-1]) + self._guide_states[curr_state_key] = curr_state - sequence_states.append(self._fsm_states[curr_state_key]) + sequence_states.append(self._guide_states[curr_state_key]) mask = torch.full_like(logits, -math.inf) - for i, fsm_state in enumerate(sequence_states): - allowed_tokens = self.fsm.get_next_instruction(fsm_state).tokens + for i, guide_state in enumerate(sequence_states): + allowed_tokens = self.guide.get_next_instruction(guide_state).tokens mask[i, allowed_tokens] = logits[i, allowed_tokens] return mask - def copy(self) -> "FSMLogitsProcessor": + def copy(self) -> "GuideLogitsProcessor": """Return a copy of the logits processor.""" - return FSMLogitsProcessor(tokenizer=self.tokenizer, fsm=self.fsm.copy()) + return GuideLogitsProcessor(tokenizer=self.tokenizer, guide=self.guide.copy()) -class RegexLogitsProcessor(FSMLogitsProcessor): +class RegexLogitsProcessor(GuideLogitsProcessor): """Bias generation based on a regular expression. Attributes ---------- tokenizer The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. + guide + The `outlines.fsm.RegexGuide. which is used to bias the logits. """ def __init__(self, regex_string: str, tokenizer: "Tokenizer"): - """Compile the FSM that drives the regex-guided generation. + """Compile the RegexGuide that drives the regex-guided generation. Parameters ---------- @@ -130,8 +135,8 @@ def __init__(self, regex_string: str, tokenizer: "Tokenizer"): tokenizer An Outlines tokenizer """ - fsm = RegexGuide(regex_string, tokenizer) - super().__init__(tokenizer=tokenizer, fsm=fsm) + guide = RegexGuide(regex_string, tokenizer) + super().__init__(tokenizer=tokenizer, guide=guide) class JSONLogitsProcessor(RegexLogitsProcessor): @@ -141,8 +146,8 @@ class JSONLogitsProcessor(RegexLogitsProcessor): ---------- tokenizer The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. + guide + The `outlines.fsm.RegexGuide. which is used to bias the logits. """ def __init__( @@ -151,7 +156,7 @@ def __init__( tokenizer: "Tokenizer", whitespace_pattern: Optional[str] = None, ): - """Compile the FSM that drives the JSON-guided generation. + """Compile the Guide that drives the JSON-guided generation. Parameters ---------- @@ -169,19 +174,21 @@ def __init__( super().__init__(regex_string=regex_string, tokenizer=tokenizer) -class CFGLogitsProcessor(FSMLogitsProcessor): +class CFGLogitsProcessor(GuideLogitsProcessor): """Bias generation based on a context-free grammar. Attributes ---------- tokenizer The tokenizer used to convert tokens to ids. - fsm - The finite state machine which is used to bias the logits. + guide + The `outlines.fsm.CFGGuide. which is used to bias the logits. """ + guide: CFGGuide + def __init__(self, cfg_str: str, tokenizer: "Tokenizer"): - """Compile the FSM that drives the CFG-guided generation. + """Compile the CFGGuide that drives the CFG-guided generation. Parameters ---------- @@ -190,5 +197,36 @@ def __init__(self, cfg_str: str, tokenizer: "Tokenizer"): tokenizer The tokenizer used to convert tokens to ids. """ - cfg_automata = CFGGuide(cfg_string=cfg_str, tokenizer=tokenizer) - super().__init__(tokenizer=tokenizer, fsm=cfg_automata) + cfg_guide = CFGGuide(cfg_string=cfg_str, tokenizer=tokenizer) + super().__init__(tokenizer=tokenizer, guide=cfg_guide) + + def process_logits( + self, input_ids: List[List[int]], logits: torch.Tensor + ) -> torch.Tensor: + """Same behavior as GuideLogitsProcessor, but uses rejection sampling""" + if self._seq_start_idx is None: + self._seq_start_idx = len(input_ids[0]) + + sequence_states: List = [] # vector of states corresponding to `input_ids` + + for seq_ids in input_ids: + gen_ids = seq_ids[self._seq_start_idx :] + curr_state_key = hash(tuple(gen_ids)) + + if curr_state_key not in self._guide_states: + prev_state = self._guide_states[hash(tuple(gen_ids[:-1]))] + curr_state = self.guide.get_next_state(prev_state, gen_ids[-1]) + self._guide_states[curr_state_key] = curr_state + + sequence_states.append(self._guide_states[curr_state_key]) + + mask = torch.full_like(logits, -math.inf) + for i, guide_state in enumerate(sequence_states): + first_legal_token = next( + self.guide.iter_valid_token_ids( + guide_state, torch.argsort(logits[i], descending=True) + ) + ) + mask[i, [first_legal_token]] = logits[i, [first_legal_token]] + + return mask diff --git a/tests/cfg_samples/arithmetic/lots_of_ops.arithmetic.test b/tests/cfg_samples/arithmetic/lots_of_ops.arithmetic.test new file mode 100644 index 000000000..1489aebc7 --- /dev/null +++ b/tests/cfg_samples/arithmetic/lots_of_ops.arithmetic.test @@ -0,0 +1 @@ +5+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1 diff --git a/tests/cfg_samples/arithmetic/simple_math.arithmetic.test b/tests/cfg_samples/arithmetic/simple_math.arithmetic.test new file mode 100644 index 000000000..882f05c8d --- /dev/null +++ b/tests/cfg_samples/arithmetic/simple_math.arithmetic.test @@ -0,0 +1 @@ +(1 * 2) - (0.1 * 2 * 9.42) diff --git a/tests/cfg_samples/json/outlines.generate.samplers.mypy.json.test b/tests/cfg_samples/json/outlines.generate.samplers.mypy.json.test new file mode 100644 index 000000000..1a328a9b6 --- /dev/null +++ b/tests/cfg_samples/json/outlines.generate.samplers.mypy.json.test @@ -0,0 +1,372 @@ +{ + ".class": "MypyFile", + "_fullname": "outlines.generate.samplers", + "future_import_flags": [], + "is_partial_stub_package": false, + "is_stub": false, + "names": { + ".class": "SymbolTable", + "Protocol": { + ".class": "SymbolTableNode", + "cross_ref": "typing.Protocol", + "kind": "Gdef" + }, + "Sampler": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "TypeInfo", + "_promote": [], + "abstract_attributes": [ + [ + "__call__", + 2 + ] + ], + "alt_promote": null, + "bases": [ + "builtins.object" + ], + "dataclass_transform_spec": null, + "declared_metaclass": null, + "defn": { + ".class": "ClassDef", + "fullname": "outlines.generate.samplers.Sampler", + "name": "Sampler", + "type_vars": [] + }, + "deletable_attributes": [], + "flags": [ + "is_abstract", + "is_protocol" + ], + "fullname": "outlines.generate.samplers.Sampler", + "has_param_spec_type": false, + "metaclass_type": "abc.ABCMeta", + "metadata": {}, + "module_name": "outlines.generate.samplers", + "mro": [ + "outlines.generate.samplers.Sampler", + "builtins.object" + ], + "names": { + ".class": "SymbolTable", + "__call__": { + ".class": "SymbolTableNode", + "kind": "Mdef", + "node": { + ".class": "FuncDef", + "abstract_status": 2, + "arg_kinds": [ + 0, + 0, + 0, + 0 + ], + "arg_names": [ + "self", + "logits", + "samples", + "rng" + ], + "dataclass_transform_spec": null, + "flags": [ + "is_trivial_body" + ], + "fullname": "outlines.generate.samplers.Sampler.__call__", + "name": "__call__", + "type": { + ".class": "CallableType", + "arg_kinds": [ + 0, + 0, + 0, + 0 + ], + "arg_names": [ + "self", + "logits", + "samples", + "rng" + ], + "arg_types": [ + "outlines.generate.samplers.Sampler", + { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + }, + "builtins.int", + { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + } + ], + "bound_args": [], + "def_extras": { + "first_arg": "self" + }, + "fallback": "builtins.function", + "from_concatenate": false, + "implicit": false, + "is_ellipsis_args": false, + "name": "__call__ of Sampler", + "ret_type": { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + }, + "type_guard": null, + "unpack_kwargs": false, + "variables": [] + } + } + } + }, + "self_type": null, + "slots": null, + "tuple_type": null, + "type_vars": [], + "typeddict_type": null + } + }, + "__annotations__": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "Var", + "flags": [ + "is_ready" + ], + "fullname": "outlines.generate.samplers.__annotations__", + "name": "__annotations__", + "type": { + ".class": "Instance", + "args": [ + "builtins.str", + { + ".class": "AnyType", + "missing_import_name": null, + "source_any": null, + "type_of_any": 6 + } + ], + "type_ref": "builtins.dict" + } + } + }, + "__doc__": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "Var", + "flags": [ + "is_ready" + ], + "fullname": "outlines.generate.samplers.__doc__", + "name": "__doc__", + "type": "builtins.str" + } + }, + "__file__": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "Var", + "flags": [ + "is_ready" + ], + "fullname": "outlines.generate.samplers.__file__", + "name": "__file__", + "type": "builtins.str" + } + }, + "__name__": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "Var", + "flags": [ + "is_ready" + ], + "fullname": "outlines.generate.samplers.__name__", + "name": "__name__", + "type": "builtins.str" + } + }, + "__package__": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "Var", + "flags": [ + "is_ready" + ], + "fullname": "outlines.generate.samplers.__package__", + "name": "__package__", + "type": "builtins.str" + } + }, + "greedy": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "FuncDef", + "abstract_status": 0, + "arg_kinds": [ + 0, + 0, + 2 + ], + "arg_names": [ + "logits", + "samples", + "_" + ], + "dataclass_transform_spec": null, + "flags": [], + "fullname": "outlines.generate.samplers.greedy", + "name": "greedy", + "type": { + ".class": "CallableType", + "arg_kinds": [ + 0, + 0, + 2 + ], + "arg_names": [ + "logits", + "samples", + "_" + ], + "arg_types": [ + { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + }, + "builtins.int", + { + ".class": "AnyType", + "missing_import_name": null, + "source_any": null, + "type_of_any": 1 + } + ], + "bound_args": [], + "def_extras": { + "first_arg": null + }, + "fallback": "builtins.function", + "from_concatenate": false, + "implicit": false, + "is_ellipsis_args": false, + "name": "greedy", + "ret_type": { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + }, + "type_guard": null, + "unpack_kwargs": false, + "variables": [] + } + } + }, + "multinomial": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "FuncDef", + "abstract_status": 0, + "arg_kinds": [ + 0, + 0, + 0 + ], + "arg_names": [ + "logits", + "samples", + "rng" + ], + "dataclass_transform_spec": null, + "flags": [], + "fullname": "outlines.generate.samplers.multinomial", + "name": "multinomial", + "type": { + ".class": "CallableType", + "arg_kinds": [ + 0, + 0, + 0 + ], + "arg_names": [ + "logits", + "samples", + "rng" + ], + "arg_types": [ + { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + }, + "builtins.int", + { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + } + ], + "bound_args": [], + "def_extras": { + "first_arg": null + }, + "fallback": "builtins.function", + "from_concatenate": false, + "implicit": false, + "is_ellipsis_args": false, + "name": "multinomial", + "ret_type": { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + }, + "type_guard": null, + "unpack_kwargs": false, + "variables": [] + } + } + }, + "torch": { + ".class": "SymbolTableNode", + "kind": "Gdef", + "node": { + ".class": "Var", + "flags": [ + "is_suppressed_import", + "is_ready", + "is_inferred" + ], + "fullname": "outlines.generate.samplers.torch", + "name": "torch", + "type": { + ".class": "AnyType", + "missing_import_name": "outlines.generate.samplers.torch", + "source_any": null, + "type_of_any": 3 + } + } + } + }, + "path": "/home/andrew/p/outlines/outlines/generate/samplers.py" +} diff --git a/tests/cfg_samples/json/simple_fruit.json.test b/tests/cfg_samples/json/simple_fruit.json.test new file mode 100644 index 000000000..bffa952c7 --- /dev/null +++ b/tests/cfg_samples/json/simple_fruit.json.test @@ -0,0 +1,20 @@ +[ + { + "ID": "1", + "Name": "Andrew \"The Escaper\" Lapp", + "Age": "30", + "FavFruit": "Banana" + }, + { + "ID": "2", + "Name": "Mohammad", + "Age": "40", + "FavFruit": "\"Any Fruit As Long as It's In Quotes!\"" + }, + { + "ID": "3", + "Name": "Alice", + "Age": "61", + "FavFruit": "Peaches, but only \n newline separated peaches" + } +] diff --git a/tests/cfg_samples/json/simple_fruit_no_indent.json.test b/tests/cfg_samples/json/simple_fruit_no_indent.json.test new file mode 100644 index 000000000..9b7d319da --- /dev/null +++ b/tests/cfg_samples/json/simple_fruit_no_indent.json.test @@ -0,0 +1 @@ +[{"ID": "1", "Name": "Andrew", "Age": "30", "FavFruit": "Banana"}, {"ID": "2", "Name": "Mohammad", "Age": "40", "FavFruit": "Apple"}, {"ID": "3", "Name": "Alice", "Age": "61", "FavFruit": "Peach"}] diff --git a/tests/fsm/test_cfg_guide.py b/tests/fsm/test_cfg_guide.py new file mode 100644 index 000000000..d92afa625 --- /dev/null +++ b/tests/fsm/test_cfg_guide.py @@ -0,0 +1,457 @@ +from collections import namedtuple +from pathlib import Path + +import pytest +from transformers import AutoTokenizer + +from outlines import grammars, models +from outlines.fsm.guide import CFGGuide + + +@pytest.fixture +def cleanup_lark_import(): + import importlib + + import lark.lark + + yield + # Clean up lark.lark.LarkOptions._defaults + importlib.reload(lark.lark) + + +TestInputs = namedtuple( + "TestInputs", + [ + "grammar", # the lark grammar to validate against + "vocabulary", # the token strings which can be concatenated for a generation + "generated", # the tokens which have been generated so far + "legal_next_tokens", # the subset of the vocabulary which can legally be next in `generated` + ], +) + + +cfg_test_inputs = { + "Next Token Doesn't Complete Terminal": TestInputs( + grammar=r'?start: "a" "bc"', + vocabulary=["a", "ab", "b", "c"], + generated=["a"], + legal_next_tokens=["b"], + ), + "Ambiguous Terminal Completion": TestInputs( + grammar=r'?start: "ab" | "abc"', + vocabulary=["a", "ab", "abc", "abcd", "b", "c"], + generated=["a"], + legal_next_tokens=["b"], + ), + "Token is Substring of Another Token": TestInputs( + grammar=r'?start: "abc" | "abcd"', + vocabulary=["a", "b", "bc", "bcd", "bcde"], + generated=["a"], + legal_next_tokens=["b", "bc", "bcd"], + ), + "Multiple Valid Continuations": TestInputs( + grammar=r'?start: ("a" "b") | ("a" "c")', + vocabulary=["a", "b", "bc", "c"], + generated=["a"], + legal_next_tokens=["b", "c"], + ), + "Prefix Matches Multiple Terminals": TestInputs( + grammar=r'?start: "abcd" | "abef"', + vocabulary=["a", "b", "be", "bcd", "bef", "bed"], + generated=["a"], + legal_next_tokens=["b", "be", "bcd", "bef"], + ), + "Token Matches Multiple Paths in Grammar": TestInputs( + grammar=r'?start: ("a" "b" "c") | ("a" "b" "d")', + vocabulary=["a", "b", "c", "d"], + generated=["a", "b"], + legal_next_tokens=["c", "d"], + ), + "Incomplete Terminal at End of Prefix": TestInputs( + grammar=r'?start: "abc"', + vocabulary=["a", "ab", "c", "abc", "abcd"], + generated=["ab"], + legal_next_tokens=["c"], + ), + "Complex Grammar Rules": TestInputs( + grammar=r'?start: "a" "b" ["c"]', + vocabulary=["a", "b", "c"], + generated=["a", "b"], + legal_next_tokens=["c", None], # Allowing the document to end after "a" "b" + ), + "Empty Prefix String": TestInputs( + grammar=r'?start: "a" | "b"', + vocabulary=["a", "b", "c", "d"], + generated=[], + legal_next_tokens=["a", "b"], + ), + "Ambiguous Pattern Completion": TestInputs( + grammar=r'?start: /a+/ "b" /c?d/', + vocabulary=["a", "aa", "b", "cd", "d"], + generated=["a", "a", "b"], + legal_next_tokens=["cd", "d"], + ), + "Optional Patterns with Overlapping Tokens": TestInputs( + grammar=r'?start: "a" "b"? "c"', + vocabulary=["a", "b", "bc", "c"], + generated=["a"], + legal_next_tokens=["b", "bc", "c"], + ), + "Greedy vs. Non-Greedy Matching": TestInputs( + grammar=r'?start: /a+?/ "b" /c/', + vocabulary=["a", "aa", "aaa", "b", "c"], + generated=["a", "a", "b"], + legal_next_tokens=["c"], + ), + "Nested Optional Elements": TestInputs( + grammar=r'?start: "a" ["b" ["c"]]', + vocabulary=["a", "b", "bc", "c"], + generated=["a"], + legal_next_tokens=[ + "b", + "bc", + None, + ], # Allowing the document to end after "a" "b" + ), + "Recursive Patterns": TestInputs( + grammar=r'?start: /a(bc)*/ "d"', + vocabulary=["a", "bc", "bcbcbc", "d"], + generated=["a", "bc", "d"], + legal_next_tokens=[None], # Allowing the document to end after "a" "bc" "d" + ), + "Overlapping Character Classes": TestInputs( + grammar=r'?start: /[ab]+/ "d"', + vocabulary=["a", "b", "c", "aa", "bb", "cc", "d"], + generated=["a", "b"], + legal_next_tokens=["d", "a", "b", "aa", "bb"], + ), + "Conditional Patterns": TestInputs( + grammar=r'?start: "a" /b/ "c" (/d/)?', + vocabulary=["a", "b", "c", "d"], + generated=["a", "b", "c"], + legal_next_tokens=["d", None], # Allowing the document to end after "a" "b" "c" + ), + "Unicode and Special Characters": TestInputs( + grammar=r'?start: /[a-zA-Z]/ "é" /[0-9]+/', + vocabulary=["a", "b", "é", "1", "2", "12"], + generated=["a", "é"], + legal_next_tokens=["1", "2", "12"], + ), + "Unicode and Special Characters Are Choices": TestInputs( + grammar=r'?start: /[a-zA-Z]/ "é" /[0-9]+/', + vocabulary=["a", "b", "é", "é9", "2", "12"], + generated=["a"], + legal_next_tokens=["é", "é9"], + ), + "Whitespace and Ignored Characters": TestInputs( + grammar=r'?start: "a" / *\s*b/ "c"', + vocabulary=["a", " b", " c", "c"], + generated=["a", " b"], + legal_next_tokens=["c"], + ), + "Token Overlaps Multiple Terminals": TestInputs( + grammar=r'?start: "a" "b" "c" "ab"', + vocabulary=["a", "b", "bc", "cab", "abc"], + generated=["a"], + legal_next_tokens=["b", "bc"], + ), + "Interleaved Sequences": TestInputs( + grammar=r'?start: ("a" "b") | ("a" "c")', + vocabulary=["a", "b", "c", "ab", "ac"], + generated=["a"], + legal_next_tokens=["b", "c"], + ), + "Repeated and Nested Patterns": TestInputs( + grammar=r'?start: "a" ("b" "c")* "d"', + vocabulary=["a", "b", "c", "bc", "bcc", "cbc", "bcbc", "cbccccd", "d", "bcbcd"], + generated=["a", "b", "c"], + legal_next_tokens=["b", "bc", "bcbc", "d", "bcbcd"], + ), + "Ambiguous Ending Patterns": TestInputs( + grammar=r'?start: "a" (/b/)? (/c/)*', + vocabulary=["a", "b", "c"], + generated=["a", "b"], + legal_next_tokens=["c", None], # Allowing the document to end after "a" "b" + ), + "Whitespace Handling in Patterns": TestInputs( + grammar=r'?start: "a" / *\s*b/ /c /', + vocabulary=["a", " b", "c"], + generated=["a", " b"], + legal_next_tokens=["c"], + ), + "Token with Escape Characters": TestInputs( + grammar=r'?start: "a\n" ("\t")? "b"', + vocabulary=["a\nb", "a", "b", "\n", "\tb", "\t"], + generated=["a", "\n"], + legal_next_tokens=["b", "\t", "\tb"], + ), + "Complex Nesting": TestInputs( + grammar=r'?start: "a" ("b" ("c" "d"))', + vocabulary=["a", "b", "c", "d"], + generated=["a", "b", "c"], + legal_next_tokens=["d"], + ), + "Repeated Optional Patterns": TestInputs( + grammar=r'?start: ("a" ["b"])*', + vocabulary=["a", "b"], + generated=["a", "b", "a"], + legal_next_tokens=["a", "b", None], + ), + "Multiple Non-Terminal Symbols": TestInputs( + grammar=r""" + ?start: A B + A: "a" + B: "b" + """, + vocabulary=["a", "b"], + generated=["a"], + legal_next_tokens=["b"], + ), + "Recursive Definitions": TestInputs( + grammar=r""" + ?start: term_a + term_a: "a" term_a | "b" + """, + vocabulary=["a", "b"], + generated=["a", "a"], + legal_next_tokens=["a", "b"], + ), + "Ignored Patterns": TestInputs( + grammar=r""" + ?start: "a" "b" "c" + %ignore /\s+/ + """, + vocabulary=["a", "b", "c", " "], + generated=["a", " ", "b"], + legal_next_tokens=["c", " "], + ), + "Cross-References": TestInputs( + grammar=r""" + ?start: term_a + term_a : /a/ term_b + term_b : /b/ term_a | /c/ + """, + vocabulary=["a", "b", "c", "bac"], + generated=["a"], + legal_next_tokens=["b", "bac", "c"], + ), + "Multiple Complex Non-Terminal Rules": TestInputs( + grammar=r""" + ?start: S1 S2 S3 + S1: "a" | "b" + S2: "c" | "d" + S3: "e" "f" | "g" + """, + vocabulary=["a", "b", "c", "d", "e", "f", "g"], + generated=["a", "c"], + legal_next_tokens=["e", "g"], + ), + # + # + # TODO: fix + # parser incorrectly requires consumption of a or b in this case for unknown reasons + # "Nested Patterns with Repetition": TestInputs( + # grammar=r'?start: "a" ("b" | "c")* "d"', + # vocabulary=["a", "b", "bc", "bcc", "cbc", "bcbc", "cbccccd", "c", "d" "bcdcb"], + # generated=["a"], + # legal_next_tokens=["b", "bc", "bcc", "cbc", "bcbc", "cbccccd", "c", "d"], + # ), + # + # + # TODO: fix + # adjacent terminals with ambiguous starts and ends not handled properly + # ensure parser isn't greedy incorrectly + # "Ambiguous Overlapping Patterns": TestInputs( + # grammar=r"?start: /ab*/ /bc?/", + # vocabulary=["a", "ab", "abc", "b", "bc", "c"], + # generated=["a", "b", "b"], + # legal_next_tokens=["b", "c", "bc"], + # ), + # "Ambiguous Overlapping Patterns In Generation": TestInputs( + # grammar=r"?start: /ab*/ /bc?/", + # vocabulary=["a", "ab", "abc", "b", "bc", "c", "abbbc"], + # generated=["a", "b", "b"], + # legal_next_tokens=["b", "c", "bc"], + # ), + # + # + # SKIP: + # Awaiting negative lookarounds in interegular + # "Lookahead and Lookbehind with Nested Conditions": TestInputs( + # grammar=r'?start: /(?<=a)b(?=c)/ "d"', + # vocabulary=["a", "b", "c", "d"], + # generated=["a", "b", "c"], + # legal_next_tokens=["d"] + # ), + # "Lookbehind Patterns": TestInputs( + # grammar=r'?start: /(?<=a)b/ "c"', + # vocabulary=["a", "b", "c"], + # generated=["a", "b"], + # legal_next_tokens=["c"] + # ), +} + + +@pytest.mark.parametrize("name", cfg_test_inputs.keys()) +def test_cfg_next_token(name, cleanup_lark_import): + inputs = cfg_test_inputs[name] + + class MockTokenizer: + vocabulary = {token: i + 1 for i, token in enumerate(inputs.vocabulary)} + vocabulary[""] = 0 + reverse_vocab = {i: tok for tok, i in vocabulary.items()} + special_tokens = {""} + eos_token_id = 0 + + def convert_token_to_string(self, token): + return token + + def decode(self, token_ids): + if isinstance(token_ids[0], list): + return [ + "".join(map(self.reverse_vocab.get, token_ids_sublist)) + for token_ids_sublist in token_ids + ] + return [self.reverse_vocab[token_id] for token_id in token_ids] + + # create a guide and the appropriate state advanced + # per the inputs generated tokens + tokenizer = MockTokenizer() + guide = CFGGuide(inputs.grammar, tokenizer) + state = guide.initial_state + for token in inputs.generated: + state = guide.get_next_state(state, tokenizer.vocabulary[token]) + instruction = guide.get_next_instruction(state) + + # normalize expectations and returned tokens for simple comparison + returned_next_tokens = sorted( + {tokenizer.reverse_vocab[int(t)] for t in instruction.tokens} + ) + expected_next_tokens = sorted( + { + t + if t is not None + else tokenizer.reverse_vocab[tokenizer.eos_token_id] # None -> "" + for t in inputs.legal_next_tokens + } + ) + + assert returned_next_tokens == expected_next_tokens + + +@pytest.fixture(scope="session") +def tokenizer_sentencepiece_gpt2(): + return models.TransformerTokenizer(AutoTokenizer.from_pretrained("gpt2")) + + +@pytest.fixture(scope="session") +def tokenizer_sentencepiece_llama1(): + return models.TransformerTokenizer( + AutoTokenizer.from_pretrained( + "trl-internal-testing/tiny-random-LlamaForCausalLM" + ) + ) + + +@pytest.fixture(scope="session") +def tokenizer_tiktoken_llama3(): + return models.TransformerTokenizer( + AutoTokenizer.from_pretrained("yujiepan/llama-3-tiny-random") + ) + + +@pytest.fixture(scope="session") +def tokenizer_character_level_byt5(): + return models.TransformerTokenizer( + AutoTokenizer.from_pretrained("google/byt5-small") + ) + + +# Collects all samples within cfg_samples/ and makes adding +# a test case as easy as adding a valid sample to cfg_samples/ +all_samples = {} +examples_path = Path(__file__).parent.parent / "cfg_samples" +for sample_collection_path in examples_path.iterdir(): + grammar_name = sample_collection_path.name + grammar = getattr(grammars, grammar_name) + for sample_path in sample_collection_path.iterdir(): + test_name = f"{grammar_name}_{sample_path.name}" + with open(sample_path) as f: + all_samples[test_name] = (grammar_name, grammar, f.read().rstrip("\n")) + + +@pytest.mark.parametrize("sample_name", all_samples.keys()) +def test_cfg_test_sample_valid_with_lark(sample_name): + """assert the provided sample is valid (testing the test itself)""" + from lark import Lark, UnexpectedToken + + grammar_name, grammar_str, sample = all_samples[sample_name] + try: + parser = Lark(grammar_str, parser="lalr", import_paths=[grammars.GRAMMAR_PATH]) + parser = parser.parse_interactive(sample) + token = parser.exhaust_lexer()[-1] + parser.feed_eof(token) + except UnexpectedToken as e: + raise Exception( + f"Invalid test, sample '{sample_name}' isn't a legal generation of '{grammar_name}':\n{e}" + ) + + +@pytest.mark.parametrize("sample_name", all_samples.keys()) +@pytest.mark.parametrize( + "tokenizer_name", + [ + "tokenizer_sentencepiece_gpt2", + "tokenizer_sentencepiece_llama1", + "tokenizer_tiktoken_llama3", + "tokenizer_character_level_byt5", + ], +) +def test_cfg_grammar_sample(request, sample_name, tokenizer_name, cleanup_lark_import): + """Test whether CFG can generate the exact token sequence as tokenizer.encode(sample) produces""" + + # TODO: enable these tests once improvements are made + if ( + tokenizer_name != "tokenizer_character_level_byt5" + or sample_name == "json_outlines.generate.samplers.mypy.json.test" + ): + pytest.skip("CFG is too slow, skipping tests for this tokenizer") + elif sample_name == "arithmetic_lots_of_ops.arithmetic.test": + pytest.skip("CFG incorrectly handles this valid sample, skipping until bugfix") + + tokenizer = request.getfixturevalue(tokenizer_name) + + grammar_name, grammar_str, sample = all_samples[sample_name] + cfg_guide = CFGGuide(grammar_str, tokenizer) + + sample_token_ids = tokenizer.tokenizer.encode( + sample, add_special_tokens=False, return_tensors="pt" + )[0] + assert ( + len(sample_token_ids.shape) == 1 + ) # ensure we're encoding in the desired shape for this test + + state = cfg_guide.initial_state + for i, token_id in enumerate(sample_token_ids): + if tokenizer.decode([token_id])[0] == "": + continue + next_instruction = cfg_guide.get_next_instruction(state) + if token_id not in next_instruction.tokens: + processed_str = tokenizer.decode([sample_token_ids[:i]])[0] + remaining_str = tokenizer.decode([sample_token_ids[i:]])[0] + if next_instruction.tokens == [tokenizer.eos_token_id]: + error_label = "CFGGuide required EOS early" + else: + expected = tokenizer.decode(next_instruction.tokens) + error_label = ( + f"Mismatched expectations, Guide expected {sorted(expected)}" + ) + raise Exception( + f"{error_label}\n" + f"processed:\n```{processed_str}```\n" + f"remaining:\n```{remaining_str}```" + ) + next_instruction.tokens + state = cfg_guide.get_next_state(state, token_id) + final_instruction = cfg_guide.get_next_instruction(state) + assert tokenizer.eos_token_id in final_instruction.tokens diff --git a/tests/fsm/test_fsm.py b/tests/fsm/test_fsm.py index 21163b70d..94166fd95 100644 --- a/tests/fsm/test_fsm.py +++ b/tests/fsm/test_fsm.py @@ -1,6 +1,6 @@ import pytest -from outlines.fsm.fsm import CFGFSM, RegexFSM, StopAtEosFSM +from outlines.fsm.fsm import RegexFSM, StopAtEosFSM def assert_expected_tensor_ids(tensor, ids): @@ -90,263 +90,3 @@ def convert_token_to_string(self, token): state = fsm.next_state(state=5, token_id=103) assert fsm.is_final_state(state) - - -def test_cfg(): - class MockTokenizer: - vocabulary = {"{": 1, "}": 2, "[": 3, "]": 4, "eos": 5} - special_tokens = {"eos"} - eos_token = "eos" - eos_token_id = 5 - - def convert_token_to_string(self, token): - return token - - @property - def inverse_vocabulary(self): - return {v: k for k, v in self.vocabulary.items()} - - def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] - - cfg_str = """ - start: expr - expr: "{" expr "}" | "[" expr "]" | - """ - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = CFGFSM(cfg_str, tokenizer) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1, 3, 5]) - state = fsm.next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "{" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) - state = fsm.next_state(state=state, token_id=3) - assert fsm.generation == "{[" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 3, 4]) - state = fsm.next_state(state=state, token_id=4) - assert fsm.generation == "{[]" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [2]) - state = fsm.next_state(state=state, token_id=2) - assert fsm.generation == "{[]}" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [5]) - state = fsm.next_state(state=state, token_id=5) - assert fsm.generation == "{[]}" - assert fsm.is_final_state(state) - - -def test_cfg_early_termination(): - class MockTokenizer: - vocabulary = {"(": 1, ")": 2, "eos": 3} - special_tokens = {"eos"} - eos_token = "eos" - eos_token_id = 3 - - def convert_token_to_string(self, token): - return token - - @property - def inverse_vocabulary(self): - return {v: k for k, v in self.vocabulary.items()} - - def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] - - cfg_str = """ - start: expr+ - expr: "(" subexpr ")" - subexpr: expr | - """ - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = CFGFSM(cfg_str, tokenizer) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1]) - state = fsm.next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "(" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2]) - state = fsm.next_state(state=state, token_id=2) - assert fsm.generation == "()" - assert not fsm.is_final_state(state) - - # possible to continue or terminate - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 3]) - state = fsm.next_state(state=state, token_id=3) # feed eos - assert fsm.generation == "()" - assert fsm.is_final_state(state) - - # once eos generated, can only terminate - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [3]) - - -def test_cfg_ignore_directive(): - class MockTokenizer: - vocabulary = {"a": 1, " ": 2, "eos": 3} - special_tokens = {"eos"} - eos_token = "eos" - eos_token_id = 3 - - def convert_token_to_string(self, token): - return token - - @property - def inverse_vocabulary(self): - return {v: k for k, v in self.vocabulary.items()} - - def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] - - cfg_str = """ - start: LETTER+ - LETTER: "a" - WS: " " - %ignore WS - """ - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = CFGFSM(cfg_str, tokenizer) - - state = 0 - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=0), [1, 2]) - state = fsm.next_state(state=0, token_id=2) - assert fsm.generation == " " - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=0), [1, 2]) - state = fsm.next_state(state=0, token_id=1) - assert fsm.generation == " a" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) - state = fsm.next_state(state=state, token_id=2) - assert fsm.generation == " a " - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) - state = fsm.next_state(state=state, token_id=2) - assert fsm.generation == " a " - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) - state = fsm.next_state(state=state, token_id=1) - assert fsm.generation == " a a" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 2, 3]) - state = fsm.next_state(state=state, token_id=3) - assert fsm.generation == " a a" - assert fsm.is_final_state(state) - - # once eos generated, can only terminate - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [3]) - - -def test_cfg_multitoken_terminal(): - class MockTokenizer: - vocabulary = {"a": 1, "b": 2, "eos": 3} - special_tokens = {"eos"} - eos_token = "eos" - eos_token_id = 3 - - def convert_token_to_string(self, token): - return token - - @property - def inverse_vocabulary(self): - return {v: k for k, v in self.vocabulary.items()} - - def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] - - cfg_str = """ - start: S - S: "aa" | "bb" - """ - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = CFGFSM(cfg_str, tokenizer) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1, 2]) - assert fsm.reset_state # starting new regex - state = fsm.next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "a" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1]) - assert not fsm.reset_state # continuing current regex - state = fsm.next_state(state=state, token_id=1) - assert fsm.generation == "aa" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [3]) - assert not fsm.reset_state # completing current regex - state = fsm.next_state(state=state, token_id=3) - assert fsm.generation == "aa" - assert fsm.is_final_state(state) - - -def test_cfg_allow_both_extend_and_shift_terminal(): - class MockTokenizer: - vocabulary = {"(": 1, ")": 2, "a": 3, "eos": 4} - special_tokens = {"eos"} - eos_token = "eos" - eos_token_id = 4 - - def convert_token_to_string(self, token): - return token - - @property - def inverse_vocabulary(self): - return {v: k for k, v in self.vocabulary.items()} - - def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] - - cfg_str = """ - start: s - s: "(" s ")" | /a+/ - """ - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = CFGFSM(cfg_str, tokenizer) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=fsm.start_state), [1, 3]) - state = fsm.next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "(" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [1, 3]) - state = fsm.next_state(state=state, token_id=3) - assert fsm.generation == "(a" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [2, 3]) - state = fsm.next_state(state=state, token_id=3) - assert fsm.generation == "(aa" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [2, 3]) - state = fsm.next_state(state=state, token_id=2) - assert fsm.generation == "(aa)" - assert not fsm.is_final_state(state) - - assert_expected_tensor_ids(fsm.allowed_token_ids(state=state), [4]) - state = fsm.next_state(state=state, token_id=4) - assert fsm.generation == "(aa)" - assert fsm.is_final_state(state) diff --git a/tests/fsm/test_guide.py b/tests/fsm/test_guide.py index 20ba75893..67b4e0dd8 100644 --- a/tests/fsm/test_guide.py +++ b/tests/fsm/test_guide.py @@ -205,49 +205,47 @@ def inverse_vocabulary(self): return {v: k for k, v in self.vocabulary.items()} def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] + if isinstance(token_ids[0], list): + return [ + "".join(map(self.inverse_vocabulary.get, token_ids_sublist)) + for token_ids_sublist in token_ids + ] + return [self.inverse_vocabulary[token_id] for token_id in token_ids] cfg_str = """ start: expr expr: "{" expr "}" | "[" expr "]" | """ tokenizer = MockTokenizer() - fsm = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(fsm.start_state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 3, 5]) - state = fsm.get_next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "{" - assert not fsm.is_final_state(state) + guide = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) - state = fsm.get_next_state(state=state, token_id=3) - assert fsm.generation == "{[" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids( + guide.get_next_instruction(guide.initial_state).tokens, [1, 3, 5] + ) + state = guide.get_next_state(guide.initial_state, token_id=1) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 3, 4]) - state = fsm.get_next_state(state=state, token_id=4) - assert fsm.generation == "{[]" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2, 3]) + state = guide.get_next_state(state, token_id=3) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [2]) - state = fsm.get_next_state(state=state, token_id=2) - assert fsm.generation == "{[]}" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 3, 4]) + state = guide.get_next_state(state, token_id=4) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Write) - assert_expected_tensor_ids(instruction.tokens, [5]) - state = fsm.get_next_state(state=state, token_id=5) - assert fsm.generation == "{[]}" - assert fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [2]) + state = guide.get_next_state(state, token_id=2) + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) + + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [5]) + state = guide.get_next_state(state, token_id=5) + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) def test_cfg_early_termination(): @@ -265,7 +263,12 @@ def inverse_vocabulary(self): return {v: k for k, v in self.vocabulary.items()} def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] + if isinstance(token_ids[0], list): + return [ + "".join(map(self.inverse_vocabulary.get, token_ids_sublist)) + for token_ids_sublist in token_ids + ] + return [self.inverse_vocabulary[token_id] for token_id in token_ids] cfg_str = """ start: expr+ @@ -273,34 +276,29 @@ def decode(self, token_ids): subexpr: expr | """ tokenizer = MockTokenizer() - fsm = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(fsm.start_state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1]) - state = fsm.get_next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "(" - assert not fsm.is_final_state(state) + guide = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2]) - state = fsm.get_next_state(state=state, token_id=2) - assert fsm.generation == "()" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids( + guide.get_next_instruction(guide.initial_state).tokens, [1] + ) + state = guide.get_next_state(guide.initial_state, token_id=1) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) + + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2]) + state = guide.get_next_state(state, token_id=2) + assert not guide.must_terminate_state(state) + assert guide.can_terminate_state(state) # possible to continue or terminate - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 3]) - state = fsm.get_next_state(state=state, token_id=3) # feed eos - assert fsm.generation == "()" - assert fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 3]) + state = guide.get_next_state(state, token_id=3) # feed eos + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) # once eos generated, can only terminate - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Write) - assert_expected_tensor_ids(instruction.tokens, [3]) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [3]) def test_cfg_ignore_directive(): @@ -318,7 +316,12 @@ def inverse_vocabulary(self): return {v: k for k, v in self.vocabulary.items()} def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] + if isinstance(token_ids[0], list): + return [ + "".join(map(self.inverse_vocabulary.get, token_ids_sublist)) + for token_ids_sublist in token_ids + ] + return [self.inverse_vocabulary[token_id] for token_id in token_ids] cfg_str = """ start: LETTER+ @@ -327,56 +330,43 @@ def decode(self, token_ids): %ignore WS """ tokenizer = MockTokenizer() - fsm = CFGGuide(cfg_str, tokenizer) - state = 0 + guide = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(0) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2]) - state = fsm.get_next_state(state=0, token_id=2) - assert fsm.generation == " " - assert not fsm.is_final_state(state) + state = guide.initial_state - instruction = fsm.get_next_instruction(0) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2]) - state = fsm.get_next_state(state=0, token_id=1) - assert fsm.generation == " a" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2]) + state = guide.get_next_state(state, token_id=2) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) - state = fsm.get_next_state(state=state, token_id=2) - assert fsm.generation == " a " - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2]) + state = guide.get_next_state(state, token_id=1) + assert not guide.must_terminate_state(state) + assert guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) - state = fsm.get_next_state(state=state, token_id=2) - assert fsm.generation == " a " - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2, 3]) + state = guide.get_next_state(state, token_id=2) + assert not guide.must_terminate_state(state) + assert guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) - state = fsm.get_next_state(state=state, token_id=1) - assert fsm.generation == " a a" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2, 3]) + state = guide.get_next_state(state, token_id=2) + assert not guide.must_terminate_state(state) + assert guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2, 3]) - state = fsm.get_next_state(state=state, token_id=3) - assert fsm.generation == " a a" - assert fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2, 3]) + state = guide.get_next_state(state, token_id=1) + assert not guide.must_terminate_state(state) + assert guide.can_terminate_state(state) + + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 2, 3]) + state = guide.get_next_state(state, token_id=3) + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) # once eos generated, can only terminate - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Write) - assert_expected_tensor_ids(instruction.tokens, [3]) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [3]) def test_cfg_multitoken_terminal(): @@ -394,38 +384,37 @@ def inverse_vocabulary(self): return {v: k for k, v in self.vocabulary.items()} def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] + if isinstance(token_ids[0], list): + return [ + "".join(map(self.inverse_vocabulary.get, token_ids_sublist)) + for token_ids_sublist in token_ids + ] + return [self.inverse_vocabulary[token_id] for token_id in token_ids] cfg_str = """ start: S S: "aa" | "bb" """ tokenizer = MockTokenizer() - fsm = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(fsm.start_state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 2]) - assert fsm.reset_state # starting new regex - state = fsm.get_next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "a" - assert not fsm.is_final_state(state) + guide = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1]) - assert not fsm.reset_state # continuing current regex - state = fsm.get_next_state(state=state, token_id=1) - assert fsm.generation == "aa" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids( + guide.get_next_instruction(guide.initial_state).tokens, [1, 2] + ) + state = guide.get_next_state(guide.initial_state, token_id=1) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Write) - assert_expected_tensor_ids(instruction.tokens, [3]) - assert not fsm.reset_state # completing current regex - state = fsm.get_next_state(state=state, token_id=3) - assert fsm.generation == "aa" - assert fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1]) + state = guide.get_next_state(state, token_id=1) + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) + + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [3]) + state = guide.get_next_state(state, token_id=3) + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) def test_cfg_allow_both_extend_and_shift_terminal(): @@ -443,46 +432,45 @@ def inverse_vocabulary(self): return {v: k for k, v in self.vocabulary.items()} def decode(self, token_ids): - return [self.inverse_vocabulary[t] for t in token_ids] + if isinstance(token_ids[0], list): + return [ + "".join(map(self.inverse_vocabulary.get, token_ids_sublist)) + for token_ids_sublist in token_ids + ] + return [self.inverse_vocabulary[token_id] for token_id in token_ids] cfg_str = """ start: s s: "(" s ")" | /a+/ """ tokenizer = MockTokenizer() - fsm = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(fsm.start_state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 3]) - state = fsm.get_next_state(state=fsm.start_state, token_id=1) - assert fsm.generation == "(" - assert not fsm.is_final_state(state) + guide = CFGGuide(cfg_str, tokenizer) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [1, 3]) - state = fsm.get_next_state(state=state, token_id=3) - assert fsm.generation == "(a" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids( + guide.get_next_instruction(guide.initial_state).tokens, [1, 3] + ) + state = guide.get_next_state(guide.initial_state, token_id=1) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [2, 3]) - state = fsm.get_next_state(state=state, token_id=3) - assert fsm.generation == "(aa" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [1, 3]) + state = guide.get_next_state(state, token_id=3) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Generate) - assert_expected_tensor_ids(instruction.tokens, [2, 3]) - state = fsm.get_next_state(state=state, token_id=2) - assert fsm.generation == "(aa)" - assert not fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [2, 3]) + state = guide.get_next_state(state, token_id=3) + assert not guide.must_terminate_state(state) + assert not guide.can_terminate_state(state) - instruction = fsm.get_next_instruction(state) - assert isinstance(instruction, Write) - assert_expected_tensor_ids(instruction.tokens, [4]) - state = fsm.get_next_state(state=state, token_id=4) - assert fsm.generation == "(aa)" - assert fsm.is_final_state(state) + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [2, 3]) + state = guide.get_next_state(state, token_id=2) + + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) + + assert_expected_tensor_ids(guide.get_next_instruction(state).tokens, [4]) + state = guide.get_next_state(state, token_id=4) + assert guide.must_terminate_state(state) + assert guide.can_terminate_state(state) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index 8d5daa37e..fc4166535 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -131,6 +131,17 @@ def sample_choices(): return ["foo", "bar", "baz"] +@pytest.fixture() +def sample_lark_grammar(): + # from https://github.com/lark-parser/lark/blob/master/docs/grammar.md + return """ + ?start: hello_world "!" number + hello_world: ("hello" | "world") ~ 3 + number: ("0".."9") ~ 5 + thanks: "Thank"i " for testing!" + """ + + REGEX_PATTERNS = [ "a b c d e", # ensure proper tokenizer whitespace prefix handling "(123456789)|(abcdefghijklmnop)", # ensure consistent correct sequence handling during batch @@ -153,6 +164,7 @@ def enforce_not_implemented(model_fixture, *task_names): "batch": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], "beam_search": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], "multiple_samples": ["model_llamacpp", "model_mlxlm", "model_mlxlm_phi3"], + "cfg": ["model_llamacpp"], # TODO: fix llama_cpp tokenizer } for task_name in task_names: if model_fixture in NOT_IMPLEMENTED.get(task_name, []): @@ -249,6 +261,28 @@ def test_generate_format_bool(request, model_fixture): assert isinstance(res, bool) +@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +def test_generate_cfg(request, model_fixture, sample_lark_grammar): + from lark import Lark + + from outlines import grammars + + model = request.getfixturevalue(model_fixture) + with enforce_not_implemented(model_fixture, "cfg"): + generator = generate.cfg(model, sample_lark_grammar) + res = generator(**get_inputs(model_fixture)) + # validate legal with the grammar via lark + # TODO: cleanup PartialLark so doesn't modify Lark globally + import importlib + + import lark.lark + + importlib.reload(lark.lark) + Lark( + sample_lark_grammar, parser="lalr", import_paths=[grammars.GRAMMAR_PATH] + ).parse(res) + + @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) def test_generate_text_stream(request, model_fixture): model = request.getfixturevalue(model_fixture) diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py index 0d1908ebd..3469dcbc0 100644 --- a/tests/generate/test_integration_llamacpp.py +++ b/tests/generate/test_integration_llamacpp.py @@ -5,7 +5,6 @@ from pydantic import BaseModel, constr import outlines.generate as generate -import outlines.grammars as grammars import outlines.models as models import outlines.samplers as samplers @@ -243,15 +242,6 @@ def test_llamacpp_json_schema(model): assert isinstance(result["bar"], str) -def test_llamacpp_cfg(model): - prompt = "<|im_start|>user\nOutput a short and valid JSON object with two keys.<|im_end|>\n><|im_start|>assistant\n" - - # remove this statement once cfg is implemented - with pytest.raises(NotImplementedError): - result = generate.cfg(model, grammars.arithmetic)(prompt, seed=11) - assert isinstance(result, str) - - @pytest.mark.parametrize( "repo,model_path,hf_tokenizer_uri", [ @@ -319,15 +309,15 @@ def test_RegexGuide_caching(model, temp_cache_dir): # These two different models and tokenizers should not have the same state # mapping results assert ( - generator.logits_processor.fsm.states_to_token_maps - != generator_2.logits_processor.fsm.states_to_token_maps + generator.logits_processor.guide.states_to_token_maps + != generator_2.logits_processor.guide.states_to_token_maps ) generator_3 = generate.regex(model_2, regex, sampler=samplers.greedy()) assert cache.stats() == (1, 2) assert ( - generator_2.logits_processor.fsm.states_to_token_maps - == generator_3.logits_processor.fsm.states_to_token_maps + generator_2.logits_processor.guide.states_to_token_maps + == generator_3.logits_processor.guide.states_to_token_maps ) # Just for fun... diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index f3fb9682e..cdd57e0c6 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -524,15 +524,15 @@ def test_RegexGuide_caching(temp_cache_dir): # These two different models and tokenizers should not have the same state # mapping results assert ( - generator.logits_processor.fsm.states_to_token_maps - != generator_2.logits_processor.fsm.states_to_token_maps + generator.logits_processor.guide.states_to_token_maps + != generator_2.logits_processor.guide.states_to_token_maps ) generator_3 = generate.regex(model_2, regex, sampler=greedy()) assert cache.stats() == (1, 2) assert ( - generator_2.logits_processor.fsm.states_to_token_maps - == generator_3.logits_processor.fsm.states_to_token_maps + generator_2.logits_processor.guide.states_to_token_maps + == generator_3.logits_processor.guide.states_to_token_maps ) # Just for fun... From 6fb355901746dbd56634a0e1ebf34f94a9b5f360 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 27 Jul 2024 11:16:22 -0500 Subject: [PATCH 060/505] Add experimental-warning to CFGGuide, update warning-linked docs --- docs/reference/generation/cfg.md | 9 +++++++-- outlines/fsm/guide.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/reference/generation/cfg.md b/docs/reference/generation/cfg.md index 4f0285c11..e3b177800 100644 --- a/docs/reference/generation/cfg.md +++ b/docs/reference/generation/cfg.md @@ -30,10 +30,15 @@ print(sequence) # (8-2) ``` -!!! Note "Performance" +###### Disclaimer - The implementation of grammar-structured generation in Outlines is very naive. This does not reflect the performance of [.txt](https://dottxt.co)'s product, where we made grammar-structured generation as fast as regex-structured generation. +!!! Note "Experimental" + Outlines current **community-contributed** implementation of CFG-structured generation is experimental. This does not reflect the performance of [.txt](https://dottxt.co)'s product, where we have optimized grammar-structured generation to be as fast as regex-structured generation. Additionally, it does not fully align with the approach described in our [technical report](https://arxiv.org/pdf/2307.09702), aside from its use of incremental/partial parsing. This feature is still a work in progress, requiring performance enhancements and bug fixes for an ideal implementation. For more details, please see our [grammar-related open issues on GitHub](https://github.com/outlines-dev/outlines/issues?q=is%3Aissue+is%3Aopen+label%3Agrammar). + +!!! Note "Greedy" + + To mitigate performance issues, CFG-structured generation will use rejection sampling and iterate over the candidate tokens highest logit first,, completing once a single valid token ID is selected. This is effectively greedy generation. ## Ready-to-use grammars diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index aa073d107..44a918494 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -1,5 +1,6 @@ import collections import copy +import warnings from dataclasses import dataclass from typing import ( TYPE_CHECKING, @@ -319,6 +320,11 @@ def __init__(self, cfg_string: str, tokenizer): """ Construct the PartialLark parser and set the empty initial_state (PartialParserState) """ + warnings.warn( + "Outlines' public *community-contributed* CFG structured generation is experimental. " + "Please review https://outlines-dev.github.io/outlines/reference/cfg#disclaimer" + ) + self.cfg_string = cfg_string self.tokenizer = tokenizer self.eos_token_id = self.tokenizer.eos_token_id From 72377db17049fea7a8ec8e2a9bdac9b546e1d17c Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 27 Jul 2024 11:16:54 -0500 Subject: [PATCH 061/505] Add benchmark: CFG rejection sampling + CFG no rejection sampling --- benchmarks/bench_cfg_guide.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/benchmarks/bench_cfg_guide.py b/benchmarks/bench_cfg_guide.py index 8f6de914a..14dc31c73 100644 --- a/benchmarks/bench_cfg_guide.py +++ b/benchmarks/bench_cfg_guide.py @@ -38,23 +38,31 @@ def setup(self, grammar_name): ) @staticmethod - def _run_random_cfg(guide): + def _run_random_cfg(guide, rejection_sampling=True): state = guide.initial_state token_ids = list(guide.tokenizer.vocabulary.values()) for i in range(40): # simulate ordering of logits top prob to lowest prob random.shuffle(token_ids) # simulate sampling and state update - next_token_id = next(guide.iter_valid_token_ids(state, token_ids)) - state = guide.get_next_state(state, next_token_id) + if rejection_sampling: + next_token_id = next(guide.iter_valid_token_ids(state, token_ids)) + state = guide.get_next_state(state, next_token_id) + else: + next_token_id = random.choice(guide.get_next_instruction(state).tokens) + state = guide.get_next_state(state, next_token_id) @cache_disabled() def time_cfg_guide_setup(self, grammar_name): CFGGuide(benched_grammars[grammar_name], self.tokenizer) + @cache_disabled() + def time_cfg_guide_run_rejection_sampling(self, grammar): + self._run_random_cfg(self.prebuilt_cfg_guide, rejection_sampling=True) + @cache_disabled() def time_cfg_guide_run(self, grammar): - self._run_random_cfg(self.prebuilt_cfg_guide) + self._run_random_cfg(self.prebuilt_cfg_guide, rejection_sampling=False) @cache_disabled() def peakmem_cfg_guide_run(self, grammar): From 8730325fed857a2b55e153c7113b52de7ad811d2 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Fri, 6 Sep 2024 17:33:02 -0500 Subject: [PATCH 062/505] Include hidden files in coverage CI upload --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 344dfb55e..dd9d2836e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,7 @@ jobs: name: coverage-data path: .coverage.* if-no-files-found: ignore + include-hidden-files: true coverage: name: Combine & check coverage. From c3a1adf770b08aec2345ea005b51b2e30cfeb190 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Fri, 6 Sep 2024 17:35:50 -0500 Subject: [PATCH 063/505] Update artifact actions --- .github/workflows/tests.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dd9d2836e..53d20b70b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,12 +48,14 @@ jobs: env: COVERAGE_FILE: .coverage.${{ steps.matrix-id.outputs.id }} - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-data path: .coverage.* if-no-files-found: ignore include-hidden-files: true + # TODO FIXME: This is only using the last run + overwrite: true coverage: name: Combine & check coverage. @@ -74,7 +76,7 @@ jobs: run: | pip install --upgrade "coverage[toml]>=5.1" diff-cover - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: coverage-data @@ -90,10 +92,12 @@ jobs: diff-cover coverage.xml --markdown-report=coverage.md --fail-under=100 || (cat coverage.md >> $GITHUB_STEP_SUMMARY && exit 1) - name: Upload HTML report if check failed. - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: html-report path: htmlcov + # TODO FIXME: This is only using the last run + overwrite: true if: ${{ failure() }} build-wheel: From e28d42320df05ca4cc9ca9376cdbf8fff655a78f Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Tue, 10 Sep 2024 10:36:01 -0700 Subject: [PATCH 064/505] Add documentation request issue template (#1138) Didn't see a good issue template to add documentation requests, here's a simple one. --- .github/ISSUE_TEMPLATE/doc_request.md | 21 +++++++++++++++++++++ README.md | 3 +++ 2 files changed, 24 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/doc_request.md diff --git a/.github/ISSUE_TEMPLATE/doc_request.md b/.github/ISSUE_TEMPLATE/doc_request.md new file mode 100644 index 000000000..f9dd65f92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc_request.md @@ -0,0 +1,21 @@ +--- +name: 📚 Documentation +about: Help with documentation improvements. +title: '' +labels: '' +assignees: '' +--- + +### Documentation request + +Documentation is a high priority for outlines. If you would like to see something added to the documentation, please help us out by: + +- Checking to see if the documentation does not already exist. +- Providing a detailed explanation of what you'd like to understand better. +- Highlighting any specific difficulties you ran into. + +### Are you willing to open a PR? + +Tell us whether you are willing to submit a PR for this yourself, we'd greatly appreciate it! + +Thanks for contributing! diff --git a/README.md b/README.md index 364e77244..59a3722c0 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![.txt Twitter][dottxt-twitter-badge]][dottxt-twitter] [![Outlines Twitter][outlines-twitter-badge]][outlines-twitter] +[![Documentation][documentation-badge]][documentation] [![Contributors][contributors-badge]][contributors] [![Downloads][downloads-badge]][pypistats] [![Discord][discord-badge]][discord] @@ -355,6 +356,8 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) } ``` +[documentation]: https://outlines-dev.github.io/outlines/welcome/ +[documentation-badge]: https://img.shields.io/readthedocs/outlines [contributors]: https://github.com/outlines-dev/outlines/graphs/contributors [contributors-badge]: https://img.shields.io/github/contributors/outlines-dev/outlines?style=flat-square&logo=github&logoColor=white&color=ECEFF4 [dottxt-twitter]: https://twitter.com/dottxtai From d11048bea7d55dea7bac92be6b74d84cdf4c49e4 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Tue, 10 Sep 2024 12:30:18 -0500 Subject: [PATCH 065/505] Set tokenizer versions in tests --- tests/fsm/test_regex.py | 17 ++++++++++------- tests/generate/test_integration_llamacpp.py | 5 ++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/fsm/test_regex.py b/tests/fsm/test_regex.py index f7aa4ae67..824588b22 100644 --- a/tests/fsm/test_regex.py +++ b/tests/fsm/test_regex.py @@ -402,15 +402,18 @@ def test_create_fsm_index_end_to_end_multi_byte(): @pytest.mark.parametrize( - "hf_tokenizer_uri", + "hf_tokenizer_uri, revision", [ - "gpt2", - "microsoft/phi-2", - "Qwen/Qwen1.5-0.5B-Chat", - "NousResearch/Hermes-2-Pro-Llama-3-8B", + ("openai-community/gpt2", "607a30d783dfa663caf39e06633721c8d4cfcd7e"), + ("microsoft/phi-2", "ef382358ec9e382308935a992d908de099b64c23"), + ("Qwen/Qwen1.5-0.5B-Chat", "4d14e384a4b037942bb3f3016665157c8bcb70ea"), + ( + "NousResearch/Hermes-2-Pro-Llama-3-8B", + "783fd50eb82d7f57758de033861f54d62dde234f", + ), ], ) -def test_create_fsm_index_tokenizer(hf_tokenizer_uri): +def test_create_fsm_index_tokenizer(hf_tokenizer_uri, revision): # The combined regular expressions of a lexer state in a Python grammar regex_str = "(?:(?:[0-9](?:(?:_)?[0-9])*(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*|(?:[0-9](?:(?:_)?[0-9])*\\.(?:[0-9](?:(?:_)?[0-9])*)?|\\.[0-9](?:(?:_)?[0-9])*)(?:(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*)?)|[0-9](?:(?:_)?[0-9])*)(?:J|j)|(?:[0-9](?:(?:_)?[0-9])*(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*|(?:[0-9](?:(?:_)?[0-9])*\\.(?:[0-9](?:(?:_)?[0-9])*)?|\\.[0-9](?:(?:_)?[0-9])*)(?:(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*)?)|0(?:x|X)(?:(?:_)?(?:[0-9]|[a-f]|[A-F]))+|0(?:b|B)(?:(?:_)?[0-1])+|0(?:o|O)(?:(?:_)?[0-7])+|(?:(?i:([ubf]?r?|r[ubf])('([^\\\\']|.)*?'))|(?i:([ubf]?r?|r[ubf])(\"([^\\\"]|.)*?\")))|(?:(?:\r?\n[\t ]*|#[^\n]*))+|[1-9](?:(?:_)?[0-9])*|\\\\[\t \x0c]*\r?\n|continue|nonlocal|assert|global|import|lambda|return|async|await|break|class|False|match|raise|while|yield|case|from|None|pass|True|with|def|del|for|not|try|if|[^\\W\\d]\\w*|#[^\n]*|[\t \x0c]+|\\.\\.\\.|@|\\{|\\(|\\[|\\-|\\+|\\*|\\~" @@ -425,7 +428,7 @@ def test_create_fsm_index_tokenizer(hf_tokenizer_uri): num_bytes_fsm_states = len(bytes_fsm.states) assert num_bytes_fsm_states == 235 - tokenizer = AutoTokenizer.from_pretrained(hf_tokenizer_uri) + tokenizer = AutoTokenizer.from_pretrained(hf_tokenizer_uri, revision=revision) tokenizer = TransformerTokenizer(tokenizer) states_to_token_subsets, empty_token_ids = create_fsm_index_tokenizer( diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py index 3469dcbc0..0a98f0226 100644 --- a/tests/generate/test_integration_llamacpp.py +++ b/tests/generate/test_integration_llamacpp.py @@ -329,6 +329,9 @@ def test_RegexGuide_caching(model, temp_cache_dir): assert structured != structured_2 +@pytest.mark.xfail( + reason="Some versions of the Hermes-2-Pro-Llama-3 model have a broken config" +) def test_tokenizer_vocabulary_decode_sanity(): """Assert the decoded newline token (198) is the same as the normalized vocab token""" import llama_cpp @@ -337,7 +340,7 @@ def test_tokenizer_vocabulary_decode_sanity(): "bartowski/Meta-Llama-3-8B-Instruct-GGUF", "Meta-Llama-3-8B-Instruct-IQ1_M.gguf", tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained( - "NousResearch/Hermes-2-Pro-Llama-3-8B" + "NousResearch/Hermes-2-Pro-Llama-3-8B", ), ) tokenizer = generate.regex(model, "a").logits_processor.tokenizer From ae1524cf2a02b5a2e498eae45636e22eca92a194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Wed, 11 Sep 2024 12:52:18 +0200 Subject: [PATCH 066/505] Change Outlines' logo --- README.md | 2 - docs/assets/images/logo-square.svg | 123 ++++++++++++++++++++++++++ docs/assets/images/logo.png | Bin 372647 -> 6354 bytes docs/assets/images/logo.svg | 133 +++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 docs/assets/images/logo-square.svg create mode 100644 docs/assets/images/logo.svg diff --git a/README.md b/README.md index 59a3722c0..f02c126ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@
-# Outlines 〰️ - Outlines Logo [![.txt Twitter][dottxt-twitter-badge]][dottxt-twitter] diff --git a/docs/assets/images/logo-square.svg b/docs/assets/images/logo-square.svg new file mode 100644 index 000000000..1d1eb3a38 --- /dev/null +++ b/docs/assets/images/logo-square.svg @@ -0,0 +1,123 @@ + + diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png index 9a9f234a746510cb4bb42acd52e2f06cccf1dc74..14d1c727e132c9b07a9d922a46eece1af4e9a394 100644 GIT binary patch literal 6354 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 literal 372647 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

d64Q+0B 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 z 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�u5133s7o0XpLu@ueD`C%U$ehQ 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

5DDFF>jg~Tld~JeafJZ$!cmx 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

h3}f?U;t!oz>i)rLDj9y5$(mWa^@H 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 diff --git a/docs/assets/images/logo.svg b/docs/assets/images/logo.svg new file mode 100644 index 000000000..7d3b4f84b --- /dev/null +++ b/docs/assets/images/logo.svg @@ -0,0 +1,133 @@ + +outlines From 4ee420337dfccb9b75283f40f9917d77dd1b29a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Thu, 12 Sep 2024 12:47:57 +0200 Subject: [PATCH 067/505] Update logo size in documentation --- README.md | 2 +- docs/assets/images/logo-simple.png | Bin 0 -> 2196 bytes docs/index.md | 2 +- mkdocs.yml | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 docs/assets/images/logo-simple.png diff --git a/README.md b/README.md index f02c126ae..4988c77cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-Outlines Logo +Outlines Logo [![.txt Twitter][dottxt-twitter-badge]][dottxt-twitter] [![Outlines Twitter][outlines-twitter-badge]][outlines-twitter] diff --git a/docs/assets/images/logo-simple.png b/docs/assets/images/logo-simple.png new file mode 100644 index 0000000000000000000000000000000000000000..2c14b4d9f9d8e38431c44c004594d83ad6a111e8 GIT binary patch literal 2196 zcmb_edsNa_9Ea7cY-(l8=XAB1bt<(qEfs4EmmU{A2rTey}xi|Fy zY_Ea7xjqO4GVu0-9s+^D2Ef?8R1a9m7jLWvre%>{M`J*sjTtioEMB`E_yc4X>v<#= zPQb**1xFtT#l^+h{SbkVITjpw+>Q{9&3@``4gxJ*>ka+paQt;;uP~j8HEUFlkTB=W zS=G1G?0H=;ojO{)hnx&$mUosur#Kr2O{b$MXu>kpmR6V6%;D~2a` z`B^BrxWbCsP+f?FI*Uq=J;5SpP#JtUh$*7`$YoF~-JQqmea6g){|hUXsXR8)6Z z;`E?&a_D{pwU%C?HtNJEP0D(Ga#}G#8bf({t!yNATYthQZ_aurh@;&Vwv*`eB`LWu z8%LI;m?0@^YDx7CsLBf0BShdW$hn&HjEb11?+MEOGOCXtKmtn|S+-);yNY^!&_I3- zq_Yp@Ub0m@lu0Q~=`&y@+PLEA(_1Hqiia(avsns%raN8Rm7}z?_Qn{>yN$T*9vlWW z0xYN@V#mkCK9)oI&|^3qTh@IwkD>^qDmPoBC9oQX6ibvSH+c-EGSc!6)^ZaNF9>q- z&1oj1eVaMkc0Qix)_!?R*`!kS@}J}AItR+Igv1x6OkSY~r&L2_!m+@M;bQ|BN$M}O zgVHH0BPy=X;!;WuJBW(D^Qd90P3-2k-;zv6+A>3FE#(1%D!jIc`eHLuC2oZas0ru$ zTe6x>n2JA&v=0SUw712c3*wCnoQ>q^R{K6v{uvh6i7YoNk-uu2HtexZbVl~_yWq~WLdFvMWxOu^ zn1!J9*E8*XksbDjCF4cf9hU&@=SMc{^sf_%hcwc5g{{l!O!VE3$-sQsqm)ohGE&1v zSc!^!aW9sl9;H(9LXO@I}U8K#! zL3lMMR6{A!CV9V@5QP!OpNa97*l_wmR$?JTSP-yiy=bC~lqFhNi`!hnehlyGecu_Y zfC8HB-yahHN1mhI88%A77Aw;f*X=WGN8QrFiOse%q|Rm(Tm^=kq5!V(W(HE@cZBYo z+W8PO;-uO^JS8U9t!^k~DFr$}IHvup_P>8Otf72#p7}a2gqsV1o=ghn631VO(Uy%1 zQ*^J34=Iw-ijh2cF*x&ya=gg5k_O-ego;E!+&{7-yoeaSZZPCwA;X#Z)Cbd0>R*FZ zwQ1?Xq570tyLMadjGsd6R(ZeYR1fyHVOnLEx3 zk$+)(o;kcT%E{x|K<4x<#s<)m=Sm_F#I#RqNGy(;O)Wr9<&-TPPXoK`aGT5*Q{+2= P7X`@M69#2}8+7g;FTJ7w literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index b19ebd57a..e7409c0d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ hide: #
- ![Image title](assets/images/logo.png){ width="300" } + ![Image title](assets/images/logo.png){ width="600" }
diff --git a/mkdocs.yml b/mkdocs.yml index bf863085a..c95905914 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,8 +19,8 @@ theme: palette: - scheme: default primary: white - logo: assets/images/logo.png - favicon: assets/images/logo.png + logo: assets/images/logo-simple.png + favicon: assets/images/logo-simple.png icon: repo: fontawesome/brands/github features: From ba8a52bff3a07747bb2a1861c7c8e94539c27620 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 12 Sep 2024 11:14:07 -0700 Subject: [PATCH 068/505] Improve sampler docs (#1141) The docs did not fully convey how to set temperature, and I figured I'd update the sampler docs more generally. A user was having issues with this in the Discord. I update the documentation to include a few things: Changes: - Brief overview in layman's terms of what the samplers actually do - How multinomial sampling works - Added "Parameters" sections for beam search and multinomial sampling - Added more headers to break up the various parameters in multinomial sampling - Provided a simple comparison table to help users choose from the available samplers Misc doc convenience stuff: - gitignore for mkdocs-generated folders - Manually triggered build/publish GitHub Actions for the docs --- .github/workflows/build_documentation.yml | 1 + .github/workflows/publish_documentation.yml | 1 + .gitignore | 4 ++ docs/reference/samplers.md | 77 +++++++++++++++++++-- outlines/samplers.py | 4 +- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index 7902c1716..639e8c7a5 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -3,6 +3,7 @@ name: Build the documentation on: pull_request: branches: [main] + workflow_dispatch: jobs: build: diff --git a/.github/workflows/publish_documentation.yml b/.github/workflows/publish_documentation.yml index 4679121b2..d861424f3 100644 --- a/.github/workflows/publish_documentation.yml +++ b/.github/workflows/publish_documentation.yml @@ -1,6 +1,7 @@ name: Publish the documentation on: + workflow_dispatch: push: branches: - main diff --git a/.gitignore b/.gitignore index 9add6d8c4..4984b18cb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ docs/build *.gguf .venv benchmarks/results + +# Remove doc build folders +.cache/ +build/ diff --git a/docs/reference/samplers.md b/docs/reference/samplers.md index 56e4e1f7e..9cba5cefa 100644 --- a/docs/reference/samplers.md +++ b/docs/reference/samplers.md @@ -2,9 +2,33 @@ Outlines offers different sequence sampling algorithms, and we will integrate more in the future. You can read [this blog post](https://huggingface.co/blog/how-to-generate) for an overview of the different sampling algorithm. +Samplers provide control over the sampling process, allowing you to influence the output of the model. This can include controlling randomness (temperature), biasing towards certain tokens (top-k, top-p), or sequence generation (beam search). + ## Multinomial sampling -Outlines defaults to the multinomial sampler without top-p or top-k sampling, and temperature equal to 1. Not specifying a sampler is equivalent to: +[Multinomial sampling](https://en.wikipedia.org/wiki/Multinomial_distribution) is the default sampling algorithm in Outlines. + +As an example, suppose we have only two possible tokens: "H" and "T". For a fixed prompt such as "Flip a coin, did you get heads or tails?" The language model calculates probability for each token: + +| Token | Probability | +|-------|-------------| +| "H" | 0.5 | +| "T" | 0.5 | + +You'd expect to receive "H" 50% of the time and "T" 50% of the time. + +### Parameters + +- `samples`: Number of samples to generate (default: 1) +- `top_k`: Only consider the top k tokens (optional) +- `top_p`: Only consider the top tokens with cumulative probability >= p (optional) +- `temperature`: Controls randomness of sampling (optional) + +### Default behavior + +Outlines defaults to the multinomial sampler without top-p or top-k sampling, and temperature equal to 1. + +Not specifying a sampler is equivalent to: ```python from outlines import models, generate, samplers @@ -20,6 +44,8 @@ print(answer) # 4 ``` +### Batching + You can ask the generator to take multiple samples by passing the number of samples when initializing the sampler: ```python @@ -36,7 +62,7 @@ print(answer) # [4, 4, 4] ``` -If you ask multiple samples for a batch of prompt the returned array will be of shape `(num_samples, num_batches)`: +If you ask multiple samples for a batch of prompts the returned array will be of shape `(num_samples, num_batches)`: ```python from outlines import models, generate, samplers @@ -52,6 +78,25 @@ print(answer) # [[4, 4, 4], [6, 6, 6]] ``` +### Temperature + +You can control the temperature with + +```python +from outlines import models, generate, samplers + + +model = models.transformers("microsoft/Phi-3-mini-4k-instruct") +sampler = samplers.multinomial(3, temperature=0.5) + +generator = generate.text(model, sampler) +answer = generator(["What is 2+2?", "What is 3+3?"]) + +print(answer) +``` + +If you would like to use `temperature=0.0`, please use `sampler=samplers.greedy()` instead. + ### Top-k sampling You can ask Outlines to only consider the top-k logits at each step by specifying the value of the `top-k` keyword argument when initializing the sampler. @@ -71,7 +116,9 @@ sampler = samplers.multinomial(3, top_p=0.95) ## Greedy sampler -You can also use the greedy sampler. For this you need to initialize the generator with the sampler: +Greedy sampling selects the token with the highest probability at each step. It's deterministic and always produces the same output for a given input. + +To use the greedy sampler, initialize the generator with the sampler: ```python @@ -88,12 +135,14 @@ print(answer) # 4 ``` -You cannot ask for multiple samples with the greedy sampler since it does not clear what the result should be. +You cannot ask for multiple samples with the greedy sampler since it does not clear what the result should be. Only the most likely token can be returned. ## Beam Search -Outlines also comes with the Beam Search sampling algorithm: +Beam search maintains multiple candidate sequences at each step, potentially finding better overall sequences than greedy or multinomial sampling. + +To use Beam Search, initialize the generator with the sampler: ```python from outlines import models, generate, samplers @@ -108,8 +157,22 @@ answer = generator("What is 2+2?") print(answer) # 4 ``` - - !!! Warning "Compatibility" Only models from the `transformers` and `exllamav2 ` libraries are compatible with Beam Search. + +### Parameters + +- `beams`: Number of beams to use (default: 1) + +## Sampler Comparison + +Here's a table comparing the different samplers: + +| Sampler | Pros | Cons | Use Cases | +|---------|------|------|-----------| +| Greedy | Deterministic, fast | May produce repetitive text | When you need consistent, predictable output | +| Multinomial | Balances exploration and exploitation | Results may vary between runs | General-purpose text generation, creative tasks | +| Beam Search | Can find globally better sequences | More computationally expensive | When sequence quality is critical, e.g., translation | + +For most use cases, we recommend using the default [multinomial sampler](#multinomial-sampling). diff --git a/outlines/samplers.py b/outlines/samplers.py index 8b64ed768..b1421971f 100644 --- a/outlines/samplers.py +++ b/outlines/samplers.py @@ -250,8 +250,8 @@ class BeamSearchSampler: Attributes ---------- samples - The number of samples taken for each input sequence. - + The number of samples taken for each input sequence. Equivalent to the + number of beams. """ def __init__(self, beams: int = 1): From 2a4d72d5f8dc7c2603549c53b8e807ead4928ea1 Mon Sep 17 00:00:00 2001 From: Chris Sanchez <19364189+americanthinker@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:20:16 -0600 Subject: [PATCH 069/505] Update vllm.md (#1137) Minor typo that prevents model from being downloaded from Huggingface hub. Corrected the typo and verified that the download now works. --------- Co-authored-by: Cameron Pfiffer --- docs/reference/models/vllm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/models/vllm.md b/docs/reference/models/vllm.md index 1380d3d2c..fb1c830fa 100644 --- a/docs/reference/models/vllm.md +++ b/docs/reference/models/vllm.md @@ -73,7 +73,7 @@ vLLM supports AWQ, GPTQ and SqueezeLLM quantized models: ```python from outlines import models -model = models.vllm("TheBloke/Llama2-7b-Chat-AWQ", quantization="awq") +model = models.vllm("TheBloke/Llama-2-7B-Chat-AWQ", quantization="awq") model = models.vllm("TheBloke/Mistral-7B-Instruct-v0.2-GPTQ", quantization="gptq") model = models.vllm("https://huggingface.co/squeeze-ai-lab/sq-llama-30b-w4-s5", quantization="squeezellm") ``` From 0b9a3f1378c62449472cd6430d18eab9ac39c2bd Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Fri, 13 Sep 2024 11:44:14 -0700 Subject: [PATCH 070/505] Correct pathways, update site color, front page fixes (#1146) Follow up to #1143 and moving the repo to dottxt-ai. - A lot of our links were broken because they pointed to the old site. - We changed the logo and the color but it wasn't fully integrated across the site. I fixed these, and made some improvements to the front page so that it's a little cleaner. New mobile/desktop views ![image](https://github.com/user-attachments/assets/9c068733-9cbc-4864-a5cd-763cb7403fa5) ![image](https://github.com/user-attachments/assets/ddc5c638-6129-439d-8645-403af1ec9acf) Current for reference ![image](https://github.com/user-attachments/assets/2bcebe41-a4db-4fba-9a0e-d8adfaf8b1f9) GitHub contributors are still available off the welcome page: ![image](https://github.com/user-attachments/assets/ce4b21e6-2969-47f6-b351-19cf91759868) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .../pull_request_template.md | 2 +- Dockerfile | 2 +- README.md | 24 ++-- benchmarks/asv.conf.json | 4 +- docs/blog/posts/roadmap-2024.md | 10 +- docs/community/contribute.md | 8 +- docs/community/feedback.md | 4 +- docs/community/versioning.md | 2 +- docs/cookbook/chain_of_density.md | 2 +- docs/cookbook/chain_of_thought.md | 2 +- docs/cookbook/deploy-using-bentoml.md | 6 +- docs/cookbook/deploy-using-modal.md | 4 +- docs/cookbook/knowledge_graph_extraction.md | 2 +- docs/cookbook/react_agent.md | 2 +- docs/cookbook/simtom.md | 4 +- docs/index.md | 20 +-- docs/installation.md | 2 +- docs/overrides/home.html | 120 ++++++++++++++++++ docs/overrides/index.html | 11 -- docs/overrides/main.html | 20 --- docs/quickstart.md | 2 +- docs/reference/generation/cfg.md | 4 +- docs/reference/generation/generation.md | 2 +- docs/reference/generation/types.md | 2 +- docs/reference/serve/vllm.md | 2 +- docs/stylesheets/extra.css | 1 + docs/welcome.md | 20 +-- environment.yml | 4 +- examples/dating_profile.py | 2 +- mkdocs.yml | 4 +- outlines/fsm/guide.py | 4 +- outlines/generate/cfg.py | 2 +- pyproject.toml | 6 +- tests/fsm/test_json_schema.py | 2 +- tests/fsm/test_regex.py | 8 +- tests/generate/test_generate.py | 4 +- tests/generate/test_integration_llamacpp.py | 2 +- tests/test_function.py | 10 +- 39 files changed, 203 insertions(+), 131 deletions(-) create mode 100644 docs/overrides/home.html delete mode 100644 docs/overrides/index.html diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a396c1e76..90a4af686 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ contact_links: - name: 🤔 Questions & Help - url: https://github.com/outlines-dev/outlines/discussions/new + url: https://github.com/dottxt-ai/outlines/discussions/new about: "If you have a question about how to use Outlines, please start a discussion." diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index bc181466b..ce0e89999 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -18,6 +18,6 @@ A few important guidelines and requirements before we can merge your PR: Consider opening a **Draft PR** if your work is still in progress but you would like some feedback from other contributors. -[issues]: https://github.com/outlines-dev/outlines/issues +[issues]: https://github.com/dottxt-ai/outlines/issues [git-guidelines]: https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [docstring-guidelines]: https://numpydoc.readthedocs.io/en/latest/format.html diff --git a/Dockerfile b/Dockerfile index c6e5f0672..117e39e88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,5 +13,5 @@ COPY outlines ./outlines RUN --mount=source=.git,target=.git,type=bind \ pip install --no-cache-dir .[serve] -# https://outlines-dev.github.io/outlines/reference/vllm/ +# https://dottxt-ai.github.io/outlines/reference/vllm/ ENTRYPOINT ["python3", "-m", "outlines.serve.serve"] diff --git a/README.md b/README.md index 4988c77cc..87109d182 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ Made with ❤👷️ by the team at [.txt](https://dottxt.co). pip install outlines ``` -First time here? Go to our [setup guide](https://outlines-dev.github.io/outlines/welcome) +First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/welcome) ## Features -- [x] 🤖 [Multiple model integrations](https://outlines-dev.github.io/outlines/installation): OpenAI, transformers, llama.cpp, exllama2, mamba +- [x] 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/installation): OpenAI, transformers, llama.cpp, exllama2, mamba - [x] 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/) - [x] 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping - [x] ⚡ Fast [regex-structured generation](#efficient-regex-structured-generation) @@ -36,10 +36,10 @@ First time here? Go to our [setup guide](https://outlines-dev.github.io/outlines - [x] 💾 Caching of generations - [x] 🗂️ Batch inference - [x] 🎲 Sample with the greedy, multinomial and beam search algorithms (and more to come!) -- [x] 🚀 [Serve with vLLM](https://outlines-dev.github.io/outlines/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! +- [x] 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! -Outlines 〰 has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date! +Outlines has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date! ## Why should I use structured generation? @@ -145,7 +145,7 @@ as non-structured generation. ### Efficient JSON generation following a Pydantic model -Outlines 〰 allows to guide the generation process so the output is *guaranteed* to follow a [JSON schema](https://json-schema.org/) or [Pydantic model](https://docs.pydantic.dev/latest/): +Outlines allows to guide the generation process so the output is *guaranteed* to follow a [JSON schema](https://json-schema.org/) or [Pydantic model](https://docs.pydantic.dev/latest/): ```python from enum import Enum @@ -197,7 +197,7 @@ print(repr(character)) # Character(name='Vivian Thr', age=44, armor=, weapon=, strength=125) ``` -The method works with union types, optional types, arrays, nested schemas, etc. Some field constraints are [not supported yet](https://github.com/outlines-dev/outlines/issues/215), but everything else should work. +The method works with union types, optional types, arrays, nested schemas, etc. Some field constraints are [not supported yet](https://github.com/dottxt-ai/outlines/issues/215), but everything else should work. ### Efficient JSON generation following a JSON Schema @@ -277,7 +277,7 @@ print(sequence) # (8-2) ``` -This was a very simple grammar, and you can use `outlines.generate.cfg` to generate syntactically valid Python, SQL, and much more than this. Any kind of structured text, really. All you have to do is search for "X EBNF grammar" on the web, and take a look at the [Outlines `grammars` module](https://github.com/outlines-dev/outlines/tree/main/outlines/grammars). +This was a very simple grammar, and you can use `outlines.generate.cfg` to generate syntactically valid Python, SQL, and much more than this. Any kind of structured text, really. All you have to do is search for "X EBNF grammar" on the web, and take a look at the [Outlines `grammars` module](https://github.com/dottxt-ai/outlines/tree/main/outlines/grammars). ### Open functions @@ -339,8 +339,8 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) ## Join us - 💡 **Have an idea?** Come chat with us on [Discord][discord] -- 🔨 **Want to contribute?** Consult our [contribution guide](https://outlines-dev.github.io/outlines/community/contribute/). -- 🐞 **Found a bug?** Open an [issue](https://github.com/outlines-dev/outlines/issues) +- 🔨 **Want to contribute?** Consult our [contribution guide](https://dottxt-ai.github.io/outlines/community/contribute/). +- 🐞 **Found a bug?** Open an [issue](https://github.com/dottxt-ai/outlines/issues) ## Cite Outlines @@ -354,10 +354,10 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) } ``` -[documentation]: https://outlines-dev.github.io/outlines/welcome/ +[documentation]: https://dottxt-ai.github.io/outlines/welcome/ [documentation-badge]: https://img.shields.io/readthedocs/outlines -[contributors]: https://github.com/outlines-dev/outlines/graphs/contributors -[contributors-badge]: https://img.shields.io/github/contributors/outlines-dev/outlines?style=flat-square&logo=github&logoColor=white&color=ECEFF4 +[contributors]: https://github.com/dottxt-ai/outlines/graphs/contributors +[contributors-badge]: https://img.shields.io/github/contributors/dottxt-ai/outlines?style=flat-square&logo=github&logoColor=white&color=ECEFF4 [dottxt-twitter]: https://twitter.com/dottxtai [outlines-twitter]: https://twitter.com/OutlinesOSS [discord]: https://discord.gg/R9DSu34mGd diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json index f57db9a0b..92808ce83 100644 --- a/benchmarks/asv.conf.json +++ b/benchmarks/asv.conf.json @@ -1,7 +1,7 @@ { "version": 1, "project": "Outlines", - "project_url": "https://outlines-dev.github.io/outlines/", + "project_url": "https://dottxt-ai.github.io/outlines/", "repo": "..", "branches": [ "HEAD" @@ -11,7 +11,7 @@ "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}", ], "environment_type": "virtualenv", - "show_commit_url": "https://github.com/outlines-dev/outlines/commit/", + "show_commit_url": "https://github.com/dottxt-ai/outlines/commit/", "benchmark_dir": ".", "env_dir": "env", "results_dir": "results", diff --git a/docs/blog/posts/roadmap-2024.md b/docs/blog/posts/roadmap-2024.md index d1413b05f..2832660fc 100644 --- a/docs/blog/posts/roadmap-2024.md +++ b/docs/blog/posts/roadmap-2024.md @@ -23,13 +23,13 @@ Thanks to a refactor of the library, it is now possible to use our constrained g *We would like expand our work to the whole sampling layer*, and add new sampling methods that should make structured generation more accurate. This means we will keep the `transformers` integration as it is today and will expand our text generation logic around this library. -Making workflows re-usable and easy to share is difficult today. That is why *we are big believers in [outlines functions](https://github.com/outlines-dev/functions)*. We will keep improving the interface and adding examples. +Making workflows re-usable and easy to share is difficult today. That is why *we are big believers in [outlines functions](https://github.com/dottxt-ai/functions)*. We will keep improving the interface and adding examples. Finally, *we want to add a CLI tool*, `outlines serve`. This will allows you to either serve an API that does general constrained generation, or to serve Outlines function. ## Detailed roadmap -Here is a more detailed roadmap for the next 12 months. Outlines is a [community](https://discord.gg/ZxBxyWmW5n) effort, and we invite you to pick either topic and [contribute to the library](https://github.com/outlines-dev/outlines). I will progressively add related [issues](https://github.com/outlines-dev/outlines/issues) in the repository. +Here is a more detailed roadmap for the next 12 months. Outlines is a [community](https://discord.gg/ZxBxyWmW5n) effort, and we invite you to pick either topic and [contribute to the library](https://github.com/dottxt-ai/outlines). I will progressively add related [issues](https://github.com/dottxt-ai/outlines/issues) in the repository. ### Many more examples and tutorials @@ -44,7 +44,7 @@ Let's be honest, Outlines is lacking clear and thorough examples. We want to cha We want to keep the current integrations but lower the maintenance cost so we can focus on what we bring to the table. -* Deprecate every obsolete integration: `transformers` has recently integrated `autoawq` and `autogptq` for instance. ([PR](https://github.com/outlines-dev/outlines/pull/527)) +* Deprecate every obsolete integration: `transformers` has recently integrated `autoawq` and `autogptq` for instance. ([PR](https://github.com/dottxt-ai/outlines/pull/527)) * See if we can integrate to a library that provides state-space models via a logit processing function; * Integrate with llama.cpp via a logits processor; * Integrate with exllamav2 via a logits processor; @@ -55,14 +55,14 @@ We're just getting started! * Improve the performance of existing structured generation algorithms; * Improve the correctness of structured generation algorithms; -* Add ready-to-use grammars in the [grammars](https://github.com/outlines-dev/grammars) repository or in a submodule in Outlines. +* Add ready-to-use grammars in the [grammars](https://github.com/dottxt-ai/grammars) repository or in a submodule in Outlines. ### Keep developing Outlines functions Functions are awesome, use them! * Implement a CLI `outlines serve` that allows to serve Outlines functions locally; -* Add more functions to the [functions](https://github.com/outlines-dev/functions) repository. +* Add more functions to the [functions](https://github.com/dottxt-ai/functions) repository. ### Serve structured generation diff --git a/docs/community/contribute.md b/docs/community/contribute.md index d5568f47c..d29576b75 100644 --- a/docs/community/contribute.md +++ b/docs/community/contribute.md @@ -16,7 +16,7 @@ Note that the [issue tracker][issues] is only intended for actionable items. In ### Setup -First, [fork the repository on GitHub](https://github.com/outlines-dev/outlines/fork) and clone the fork locally: +First, [fork the repository on GitHub](https://github.com/dottxt-ai/outlines/fork) and clone the fork locally: ```bash git clone git@github.com/YourUserName/outlines.git @@ -127,6 +127,6 @@ Then you can [open a pull request][pull-requests] on GitHub. It should prompt yo Do not hesitate to open a draft PR before your contribution is ready, especially if you have questions and/or need feedback. If you need help, come tell us on [Discord][discord]. [discord]: https://discord.gg/R9DSu34mGd -[discussions]: https://github.com/outlines-dev/outlines/discussions -[issues]: https://github.com/outlines-dev/outlines/issues -[pull-requests]: https://github.com/outlines-dev/outlines/pulls +[discussions]: https://github.com/dottxt-ai/outlines/discussions +[issues]: https://github.com/dottxt-ai/outlines/issues +[pull-requests]: https://github.com/dottxt-ai/outlines/pulls diff --git a/docs/community/feedback.md b/docs/community/feedback.md index 942809542..033e91870 100644 --- a/docs/community/feedback.md +++ b/docs/community/feedback.md @@ -53,7 +53,7 @@ If Outlines has been helpful to you, let us know on [Discord][discord] or give u - + @@ -75,7 +75,7 @@ We highly value the insights of our users, and we would love to hear from you. I - What challenges are you facing? - What do you think could be improved? -To schedule an appointment follow [this link](https://cal.com/dottxt/outlines). This is exclusively intended to share your experience, please go on [Discord][discord] or [GitHub](https://github.com/outlines-dev/outlines/discussions) for support. +To schedule an appointment follow [this link](https://cal.com/dottxt/outlines). This is exclusively intended to share your experience, please go on [Discord][discord] or [GitHub](https://github.com/dottxt-ai/outlines/discussions) for support. [discord]: https://discord.gg/UppQmhEpe8 [twitter]: https://twitter.com/dottxtai diff --git a/docs/community/versioning.md b/docs/community/versioning.md index d64a56e7f..023b92537 100644 --- a/docs/community/versioning.md +++ b/docs/community/versioning.md @@ -15,7 +15,7 @@ Each part of the version number (`major.minor.patch`) conveys information about ## Releases -Releases along with release notes can be found on the [Outlines Releases GitHub Page](https://github.com/outlines-dev/outlines/releases). +Releases along with release notes can be found on the [Outlines Releases GitHub Page](https://github.com/dottxt-ai/outlines/releases). ## Version Pinning Recommendations diff --git a/docs/cookbook/chain_of_density.md b/docs/cookbook/chain_of_density.md index 16c2838f2..2a6b4eb39 100644 --- a/docs/cookbook/chain_of_density.md +++ b/docs/cookbook/chain_of_density.md @@ -122,4 +122,4 @@ print(result.model_dump()) Not bad, considering we used a smallish model to generate the summary! Chain of Density seems to be a very effective prompting technique to generate dense summaries, even with small quantized models. Its implementation in Outlines is also very short. -Note that this is the first article I tried and it worked out of the box. Try it out on other articles, and please share the results on Twitter, or by opening [a new discussion](https://github.com/outlines-dev/outlines/discussions/categories/show-and-tell) on the Outlines repository! +Note that this is the first article I tried and it worked out of the box. Try it out on other articles, and please share the results on Twitter, or by opening [a new discussion](https://github.com/dottxt-ai/outlines/discussions/categories/show-and-tell) on the Outlines repository! diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md index bd76f40b7..b814ae048 100644 --- a/docs/cookbook/chain_of_thought.md +++ b/docs/cookbook/chain_of_thought.md @@ -3,7 +3,7 @@ Chain of thought is a prompting technique introduced in the paper ["Chain-of-Thought Prompting Elicits Reasoning in Large Language Models"](https://arxiv.org/abs/2201.11903) where throught prompting the authors generate a series of intermediate reasoning steps which improves the ability of LLMs to perform complex reasoning. -In this guide, we use [outlines](https://outlines-dev.github.io/outlines/) to apply chain of thought through structured output. +In this guide, we use [outlines](https://dottxt-ai.github.io/outlines/) to apply chain of thought through structured output. We use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: diff --git a/docs/cookbook/deploy-using-bentoml.md b/docs/cookbook/deploy-using-bentoml.md index 6bee77441..2926df0f6 100644 --- a/docs/cookbook/deploy-using-bentoml.md +++ b/docs/cookbook/deploy-using-bentoml.md @@ -2,7 +2,7 @@ [BentoML](https://github.com/bentoml/BentoML) is an open-source model serving library for building performant and scalable AI applications with Python. It comes with tools that you need for serving optimization, model packaging, and production deployment. -In this guide, we will show you how to use BentoML to run programs written with Outlines on GPU locally and in [BentoCloud](https://www.bentoml.com/), an AI Inference Platform for enterprise AI teams. The example source code in this guide is also available in the [examples/bentoml/](https://github.com/outlines-dev/outlines/blob/main/examples/bentoml/) directory. +In this guide, we will show you how to use BentoML to run programs written with Outlines on GPU locally and in [BentoCloud](https://www.bentoml.com/), an AI Inference Platform for enterprise AI teams. The example source code in this guide is also available in the [examples/bentoml/](https://github.com/dottxt-ai/outlines/blob/main/examples/bentoml/) directory. ## Import a model @@ -56,7 +56,7 @@ mistralai--mistral-7b-v0.1:m7lmf5ac2cmubnnz 13.49 GiB 2024-04-25 06:5 As the model is ready, we can define a [BentoML Service](https://docs.bentoml.com/en/latest/guides/services.html) to wrap the capabilities of the model. -We will run the JSON-structured generation example [in the README](https://github.com/outlines-dev/outlines?tab=readme-ov-file#efficient-json-generation-following-a-json-schema), with the following schema: +We will run the JSON-structured generation example [in the README](https://github.com/dottxt-ai/outlines?tab=readme-ov-file#efficient-json-generation-following-a-json-schema), with the following schema: ```python DEFAULT_SCHEMA = """{ @@ -153,7 +153,7 @@ We then need to define an HTTP endpoint using `@bentoml.api` to decorate the met Here `@bentoml.api` decorator defines `generate` as an HTTP endpoint that accepts a JSON request body with two fields: `prompt` and `json_schema` (optional, which allows HTTP clients to provide their own JSON schema). The type hints in the function signature will be used to validate incoming JSON requests. You can define as many HTTP endpoints as you want by using `@bentoml.api` to decorate other methods of `Outlines` class. -Now you can save the above code to `service.py` (or use [this implementation](https://github.com/outlines-dev/outlines/blob/main/examples/bentoml/)), and run the code using the BentoML CLI. +Now you can save the above code to `service.py` (or use [this implementation](https://github.com/dottxt-ai/outlines/blob/main/examples/bentoml/)), and run the code using the BentoML CLI. ## Run locally for testing and debugging diff --git a/docs/cookbook/deploy-using-modal.md b/docs/cookbook/deploy-using-modal.md index 998e5d835..15e200cb5 100644 --- a/docs/cookbook/deploy-using-modal.md +++ b/docs/cookbook/deploy-using-modal.md @@ -76,7 +76,7 @@ outlines_image = outlines_image.run_function(import_model) ## Define a schema -We will run the JSON-structured generation example [in the README](https://github.com/outlines-dev/outlines?tab=readme-ov-file#efficient-json-generation-following-a-json-schema), with the following schema: +We will run the JSON-structured generation example [in the README](https://github.com/dottxt-ai/outlines?tab=readme-ov-file#efficient-json-generation-following-a-json-schema), with the following schema: ```python # Specify a schema for the character description. In this case, @@ -173,7 +173,7 @@ def main( generate.remote(prompt) ``` -Here `@app.local_entrypoint()` decorator defines `main` as the function to start from locally when using the Modal CLI. You can save above code to `example.py` (or use [this implementation](https://github.com/outlines-dev/outlines/blob/main/examples/modal_example.py)). Let's now see how to run the code on the cloud using the Modal CLI. +Here `@app.local_entrypoint()` decorator defines `main` as the function to start from locally when using the Modal CLI. You can save above code to `example.py` (or use [this implementation](https://github.com/dottxt-ai/outlines/blob/main/examples/modal_example.py)). Let's now see how to run the code on the cloud using the Modal CLI. ## Run on the cloud diff --git a/docs/cookbook/knowledge_graph_extraction.md b/docs/cookbook/knowledge_graph_extraction.md index e25166bca..6a6877756 100644 --- a/docs/cookbook/knowledge_graph_extraction.md +++ b/docs/cookbook/knowledge_graph_extraction.md @@ -1,6 +1,6 @@ # Knowledge Graph Extraction -In this guide, we use [outlines](https://outlines-dev.github.io/outlines/) to extract a knowledge graph from unstructured text. +In this guide, we use [outlines](https://dottxt-ai.github.io/outlines/) to extract a knowledge graph from unstructured text. We will use [llama.cpp](https://github.com/ggerganov/llama.cpp) using the [llama-cpp-python](https://github.com/abetlen/llama-cpp-python) library. Outlines supports llama-cpp-python, but we need to install it ourselves: diff --git a/docs/cookbook/react_agent.md b/docs/cookbook/react_agent.md index ca4829d5f..0597eab07 100644 --- a/docs/cookbook/react_agent.md +++ b/docs/cookbook/react_agent.md @@ -1,6 +1,6 @@ # ReAct Agent -This example shows how to use [outlines](https://outlines-dev.github.io/outlines/) to build your own agent with open weights local models and structured outputs. It is inspired by the blog post [A simple Python implementation of the ReAct pattern for LLMs](https://til.simonwillison.net/llms/python-react-pattern) by [Simon Willison](https://simonwillison.net/). +This example shows how to use [outlines](https://dottxt-ai.github.io/outlines/) to build your own agent with open weights local models and structured outputs. It is inspired by the blog post [A simple Python implementation of the ReAct pattern for LLMs](https://til.simonwillison.net/llms/python-react-pattern) by [Simon Willison](https://simonwillison.net/). The ReAct pattern (for Reason+Act) is described in the paper [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629). It's a pattern where you implement additional actions that an LLM can take - searching Wikipedia or running calculations for example - and then teach it how to request the execution of those actions, and then feed their results back into the LLM. diff --git a/docs/cookbook/simtom.md b/docs/cookbook/simtom.md index aa96005b4..f730d029a 100644 --- a/docs/cookbook/simtom.md +++ b/docs/cookbook/simtom.md @@ -17,9 +17,9 @@ SimToM calls an LLM with two consecutive prompts: To implement SimToM with Outlines, we will need to: -1. Write the prompts with [prompt functions](https://outlines-dev.github.io/outlines/reference/prompting/). +1. Write the prompts with [prompt functions](https://dottxt-ai.github.io/outlines/reference/prompting/). 2. Define the JSON object each prompt will return using Pydantic. -3. Generate responses with a Mistral model using the [transformers integration](https://outlines-dev.github.io/outlines/reference/models/transformers/). +3. Generate responses with a Mistral model using the [transformers integration](https://dottxt-ai.github.io/outlines/reference/models/transformers/). Let's dive into it! diff --git a/docs/index.md b/docs/index.md index e7409c0d3..3692d32c9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,26 +1,8 @@ --- title: Outlines +template: home.html # Note that this is managed in overrides/home.html hide: - navigation - toc - feedback --- - -# - -
- ![Image title](assets/images/logo.png){ width="600" } -
- -
-

Generate text with LLMs

-

Robust prompting & (structured) text generation

- [:fontawesome-solid-bolt: Get started](welcome.md){ .md-button .md-button--primary } - [:fontawesome-brands-discord: Join the Community](https://discord.gg/ZxBxyWmW5n){ .md-button } - -
-```bash -pip install outlines -``` -
-
diff --git a/docs/installation.md b/docs/installation.md index 1017b627e..bf2da86f9 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -28,7 +28,7 @@ If you encounter any problem using Outlines with these libraries, take a look at You can install the latest version of Outlines on the repository's `main` branch: ```python -pip install git+https://github.com/outlines-dev/outlines.git@main +pip install git+https://github.com/dottxt-ai/outlines.git@main ``` This can be useful, for instance, when a fix has been merged but not yet released. diff --git a/docs/overrides/home.html b/docs/overrides/home.html new file mode 100644 index 000000000..1114895e6 --- /dev/null +++ b/docs/overrides/home.html @@ -0,0 +1,120 @@ +{#- +This file overrides the home page to use HTML tooling +better. +-#} +{% extends "main.html" %} +{% block tabs %} +{{ super() }} + + + +
+
+
+
+ Outlines Logo +
+
+

+ Structured text generation and robust prompting for language models +

+ + + + + + +

Made with ❤️ by the team at .txt

+
+
+
+
+{% endblock %} +{% block content %}{% endblock %} +{% block footer %}{% endblock %} \ No newline at end of file diff --git a/docs/overrides/index.html b/docs/overrides/index.html deleted file mode 100644 index 74a4987f4..000000000 --- a/docs/overrides/index.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} - -{% block announce %} - For updates follow @remilouf on - - - Twitter - -{% endblock %} diff --git a/docs/overrides/main.html b/docs/overrides/main.html index b4183d71a..5cb6467e6 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,22 +1,2 @@ {% extends "base.html" %} -{% block announce %} - For updates follow @dottxtai on - - - Twitter - - and - - {% include ".icons/fontawesome/solid/star.svg" %} - - the repo on - - - {% include ".icons/fontawesome/brands/github.svg" %} - - Github - -{% endblock %} diff --git a/docs/quickstart.md b/docs/quickstart.md index 2e1f9a6bb..81a067ad6 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -216,7 +216,7 @@ Once you are done experimenting with a prompt and an output structure, it is use ## Going further -If you need more inspiration you can take a look at the [cookbook](cookbook/index.md) or watch [Remi Louf's AI Engineer World’s Fair Presentation on Outlines](https://www.youtube.com/live/R0X7mPagRiE?t=775s). If you have any question, or requests for documentation please reach out to us on [GitHub](https://github.com/outlines-dev/outlines/discussions), [Twitter](https://twitter.com/remilouf) or [Discord](https://discord.gg/UppQmhEpe8). +If you need more inspiration you can take a look at the [cookbook](cookbook/index.md) or watch [Remi Louf's AI Engineer World’s Fair Presentation on Outlines](https://www.youtube.com/live/R0X7mPagRiE?t=775s). If you have any question, or requests for documentation please reach out to us on [GitHub](https://github.com/dottxt-ai/outlines/discussions), [Twitter](https://twitter.com/remilouf) or [Discord](https://discord.gg/UppQmhEpe8). [pydantic]: https://docs.pydantic.dev/latest diff --git a/docs/reference/generation/cfg.md b/docs/reference/generation/cfg.md index e3b177800..f0104ec63 100644 --- a/docs/reference/generation/cfg.md +++ b/docs/reference/generation/cfg.md @@ -34,7 +34,7 @@ print(sequence) !!! Note "Experimental" - Outlines current **community-contributed** implementation of CFG-structured generation is experimental. This does not reflect the performance of [.txt](https://dottxt.co)'s product, where we have optimized grammar-structured generation to be as fast as regex-structured generation. Additionally, it does not fully align with the approach described in our [technical report](https://arxiv.org/pdf/2307.09702), aside from its use of incremental/partial parsing. This feature is still a work in progress, requiring performance enhancements and bug fixes for an ideal implementation. For more details, please see our [grammar-related open issues on GitHub](https://github.com/outlines-dev/outlines/issues?q=is%3Aissue+is%3Aopen+label%3Agrammar). + Outlines current **community-contributed** implementation of CFG-structured generation is experimental. This does not reflect the performance of [.txt](https://dottxt.co)'s product, where we have optimized grammar-structured generation to be as fast as regex-structured generation. Additionally, it does not fully align with the approach described in our [technical report](https://arxiv.org/pdf/2307.09702), aside from its use of incremental/partial parsing. This feature is still a work in progress, requiring performance enhancements and bug fixes for an ideal implementation. For more details, please see our [grammar-related open issues on GitHub](https://github.com/dottxt-ai/outlines/issues?q=is%3Aissue+is%3Aopen+label%3Agrammar). !!! Note "Greedy" @@ -65,7 +65,7 @@ The following grammars are currently available: - Arithmetic grammar via `outlines.grammars.arithmetic` - JSON grammar via `outlines.grammars.json` -If you would like more grammars to be added to the repository, please open an [issue](https://github.com/outlines-dev/outlines/issues) or a [pull request](https://github.com/outlines-dev/outlines/pulls). +If you would like more grammars to be added to the repository, please open an [issue](https://github.com/dottxt-ai/outlines/issues) or a [pull request](https://github.com/dottxt-ai/outlines/pulls). ## Grammar guide diff --git a/docs/reference/generation/generation.md b/docs/reference/generation/generation.md index 0c090f8a7..a14818514 100644 --- a/docs/reference/generation/generation.md +++ b/docs/reference/generation/generation.md @@ -174,7 +174,7 @@ print(result) # 5+5+5+5+5 ``` -The available grammars are listed [here](https://github.com/outlines-dev/outlines/tree/main/outlines/grammars). +The available grammars are listed [here](https://github.com/dottxt-ai/outlines/tree/main/outlines/grammars). ### [Regex-structured generation](./regex.md) diff --git a/docs/reference/generation/types.md b/docs/reference/generation/types.md index 5b83a5916..eb6d7382b 100644 --- a/docs/reference/generation/types.md +++ b/docs/reference/generation/types.md @@ -73,7 +73,7 @@ print(result) ``` -We plan on adding many more custom types. If you have found yourself writing regular expressions to generate fields of a given type, or if you could benefit from more specific types don't hesite to [submit a PR](https://github.com/outlines-dev/outlines/pulls) or [open an issue](https://github.com/outlines-dev/outlines/issues/new/choose). +We plan on adding many more custom types. If you have found yourself writing regular expressions to generate fields of a given type, or if you could benefit from more specific types don't hesite to [submit a PR](https://github.com/dottxt-ai/outlines/pulls) or [open an issue](https://github.com/dottxt-ai/outlines/issues/new/choose). [wiki-isbn]: https://en.wikipedia.org/wiki/ISBN#Check_digits diff --git a/docs/reference/serve/vllm.md b/docs/reference/serve/vllm.md index 8e2886c96..14277a526 100644 --- a/docs/reference/serve/vllm.md +++ b/docs/reference/serve/vllm.md @@ -65,7 +65,7 @@ curl http://127.0.0.1:8000/generate \ Instead of `curl`, you can also use the [requests][requests]{:target="_blank"} library from another python program. -Please consult the [vLLM documentation][vllm]{:target="_blank"} for details on additional request parameters. You can also [read the code](https://github.com/outlines-dev/outlines/blob/main/outlines/serve/serve.py) in case you need to customize the solution to your needs. +Please consult the [vLLM documentation][vllm]{:target="_blank"} for details on additional request parameters. You can also [read the code](https://github.com/dottxt-ai/outlines/blob/main/outlines/serve/serve.py) in case you need to customize the solution to your needs. [requests]: https://requests.readthedocs.io/en/latest/ [vllm]: https://docs.vllm.ai/en/latest/index.html diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index c4539ab80..7c5b5e808 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -9,6 +9,7 @@ --md-code-fg-color: #FFFFFF; --md-text-font-family: "Inter"; --md-code-font: "Source Code Pro Custom"; + --md-typeset-a-color: #d53135; /*this is the brand color*/ /* don't inherit white fg color for mermaid diagrams from --md-code-fg-color */ --md-mermaid-label-fg-color: #000000; diff --git a/docs/welcome.md b/docs/welcome.md index a7800f7ad..728a30a97 100644 --- a/docs/welcome.md +++ b/docs/welcome.md @@ -2,11 +2,11 @@ title: Welcome to Outlines! --- -Outlines〰 is a Python library that allows you to use Large Language Model in a simple and robust way (with structured generation). It is built by [.txt][.txt]{:target="_blank"}, and is already used in production by many companies. +Outlines is a Python library that allows you to use Large Language Model in a simple and robust way (with structured generation). It is built by [.txt][.txt]{:target="_blank"}, and is already used in production by many companies. ## What models do you support? -We support [Openai](reference/models/openai.md), but the true power of Outlines〰 is unleashed with Open Source models available via the [transformers](reference/models/transformers.md), [llama.cpp](reference/models/llamacpp.md), [exllama2](reference/models/exllamav2.md), [mlx-lm](reference/models/mlxlm.md) and [vllm](reference/models/vllm.md) models. If you want to build and maintain an integration with another library, [get in touch][discord]. +We support [Openai](reference/models/openai.md), but the true power of Outlines is unleashed with Open Source models available via the [transformers](reference/models/transformers.md), [llama.cpp](reference/models/llamacpp.md), [exllama2](reference/models/exllamav2.md), [mlx-lm](reference/models/mlxlm.md) and [vllm](reference/models/vllm.md) models. If you want to build and maintain an integration with another library, [get in touch][discord]. ## What are the main features? @@ -48,7 +48,7 @@ We support [Openai](reference/models/openai.md), but the true power of Outlines ## Why use Outlines? -Outlines〰 is built at [.txt][.txt] by engineers with decades of experience in software engineering, machine learning (Bayesian Statistics and NLP), and compilers. [.txt][.txt] is a VC-backed company fully focused on the topic of structured generation and is committed to make the community benefit from its experience. +Outlines is built at [.txt][.txt] by engineers with decades of experience in software engineering, machine learning (Bayesian Statistics and NLP), and compilers. [.txt][.txt] is a VC-backed company fully focused on the topic of structured generation and is committed to make the community benefit from its experience. We are also open source veterans and have authored/maintained many libraries over the years: the [Aesara][aesara]{:target="_blank"} and [Pythological][pythological]{:target="_blank"} ecosystems, [Blackjax][blackjax]{:target="_blank"} and [Hy][hy]{:target="_blank"} among many others. . @@ -119,33 +119,33 @@ Still not convinced, read [what people say about us](community/feedback.md). And ## Philosophy -**Outlines** 〰 is a library for neural text generation. You can think of it as a +**Outlines** is a library for neural text generation. You can think of it as a more flexible replacement for the `generate` method in the [transformers](https://github.com/huggingface/transformers) library. -**Outlines** 〰 helps developers *structure text generation* to build robust +**Outlines** helps developers *structure text generation* to build robust interfaces with external systems. It provides generation methods that guarantee that the output will match a regular expressions, or follow a JSON schema. -**Outlines** 〰 provides *robust prompting primitives* that separate the prompting +**Outlines** provides *robust prompting primitives* that separate the prompting from the execution logic and lead to simple implementations of few-shot generations, ReAct, meta-prompting, agents, etc. -**Outlines** 〰 is designed as a *library* that is meant to be compatible the +**Outlines** is designed as a *library* that is meant to be compatible the broader ecosystem, not to replace it. We use as few abstractions as possible, and generation can be interleaved with control flow, conditionals, custom Python functions and calls to other libraries. -**Outlines** 〰 is *compatible with every auto-regressive model*. It only interfaces with models +**Outlines** is *compatible with every auto-regressive model*. It only interfaces with models via the next-token logits distribution. ## Outlines people Outlines would not be what it is today without a community of dedicated developers: - - + + ## Acknowledgements diff --git a/environment.yml b/environment.yml index c267f86a0..6fc980fed 100644 --- a/environment.yml +++ b/environment.yml @@ -1,9 +1,9 @@ # To use: # # $ conda env create -f environment.yml # `mamba` works too for this command -# $ conda activate outlines-dev +# $ conda activate dottxt-ai # -name: outlines-dev +name: dottxt-ai channels: - conda-forge - huggingface diff --git a/examples/dating_profile.py b/examples/dating_profile.py index acc00126e..504ec943d 100644 --- a/examples/dating_profile.py +++ b/examples/dating_profile.py @@ -26,7 +26,7 @@ class QuestionAnswer: class DatingProfile(BaseModel): # It is possible put length constraints on these strings using constr- however, this appears to dramatically increase the generation time - # This may be resolved in the future with this PR: https://github.com/outlines-dev/outlines/pull/272 + # This may be resolved in the future with this PR: https://github.com/dottxt-ai/outlines/pull/272 bio: str job: str # Ignore mypy checks here because it still doesn't support conlist or constr: https://github.com/pydantic/pydantic/issues/975 diff --git a/mkdocs.yml b/mkdocs.yml index c95905914..4189df1c0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ # Site information -site_name: Outlines 〰️ +site_name: Outlines site_author: The Outlines developers site_description: >- Structured text generation with LLMs @@ -19,7 +19,7 @@ theme: palette: - scheme: default primary: white - logo: assets/images/logo-simple.png + logo: assets/images/logo-square.svg favicon: assets/images/logo-simple.png icon: repo: fontawesome/brands/github diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index 44a918494..b7b121fe6 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -322,7 +322,7 @@ def __init__(self, cfg_string: str, tokenizer): """ warnings.warn( "Outlines' public *community-contributed* CFG structured generation is experimental. " - "Please review https://outlines-dev.github.io/outlines/reference/cfg#disclaimer" + "Please review https://dottxt-ai.github.io/outlines/reference/cfg#disclaimer" ) self.cfg_string = cfg_string @@ -466,7 +466,7 @@ def _get_parser_state_token_applied( def is_final_state(self, state: CFGState) -> bool: # TODO: remove this method, use can_terminate_state and must_terminate_state - # here and in RegexGuide per https://github.com/outlines-dev/outlines/issues/885 + # here and in RegexGuide per https://github.com/dottxt-ai/outlines/issues/885 return self.can_terminate_state(state) def can_terminate_state(self, state: CFGState) -> bool: diff --git a/outlines/generate/cfg.py b/outlines/generate/cfg.py index 4f372f209..034a65ae5 100644 --- a/outlines/generate/cfg.py +++ b/outlines/generate/cfg.py @@ -44,7 +44,7 @@ def cfg_vision(model, cfg_str: str, sampler: Sampler = multinomial()): @cfg.register(ExLlamaV2Model) def cfg_exllamav2(model, cfg_str: str, sampler: Sampler = multinomial()): raise NotImplementedError( - "Not yet available, track progress in https://github.com/outlines-dev/outlines/pull/1010" + "Not yet available, track progress in https://github.com/dottxt-ai/outlines/pull/1010" ) diff --git a/pyproject.toml b/pyproject.toml index 99d4f94e1..dfda81acd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,9 +73,9 @@ serve = [ ] [project.urls] -homepage = "https://github.com/outlines-dev/outlines" -documentation = "https://outlines-dev.github.io/outlines/" -repository = "https://github.com/outlines-dev/outlines" +homepage = "https://github.com/dottxt-ai/outlines" +documentation = "https://dottxt-ai.github.io/outlines/" +repository = "https://github.com/dottxt-ai/outlines" [project.readme] file="README.md" diff --git a/tests/fsm/test_json_schema.py b/tests/fsm/test_json_schema.py index 21571da8d..7565ff642 100644 --- a/tests/fsm/test_json_schema.py +++ b/tests/fsm/test_json_schema.py @@ -1018,7 +1018,7 @@ class MockModel(BaseModel): def test_one_of_doesnt_produce_illegal_lookaround(): - """Reproduces failure in https://github.com/outlines-dev/outlines/issues/823""" + """Reproduces failure in https://github.com/dottxt-ai/outlines/issues/823""" class Cat(BaseModel): pet_type: Literal["cat"] diff --git a/tests/fsm/test_regex.py b/tests/fsm/test_regex.py index 824588b22..7418deca2 100644 --- a/tests/fsm/test_regex.py +++ b/tests/fsm/test_regex.py @@ -667,7 +667,7 @@ def convert_token_to_string(self, token): def test_numba_leading_null_byte_UnicodeCharSeq_remains_broken(): """Assert numba UnicodeCharSeq w/ leading \x00 is still broken""" # EXPLANATION: - # https://github.com/outlines-dev/outlines/pull/930#issuecomment-2143535968 + # https://github.com/dottxt-ai/outlines/pull/930#issuecomment-2143535968 # from https://github.com/numba/numba/issues/9542 d = numba.typed.typeddict.Dict.empty(numba.types.UnicodeCharSeq(1), numba.int64) @@ -685,7 +685,7 @@ def test_numba_leading_null_byte_UnicodeCharSeq_remains_broken(): def test_numba_leading_null_byte_unicode_type_sane(input_key): """Assert numba unicode_type w/ leading \x00 is working""" # EXPLANATION: - # https://github.com/outlines-dev/outlines/pull/930#issuecomment-2143535968 + # https://github.com/dottxt-ai/outlines/pull/930#issuecomment-2143535968 # from https://github.com/numba/numba/issues/9542 d = numba.typed.typeddict.Dict.empty(numba.types.unicode_type, numba.int64) @@ -712,8 +712,8 @@ def test_reduced_vocabulary_with_rare_tokens(rare_token): See [1] and [2] for context. - [1]: https://github.com/outlines-dev/outlines/pull/763 - [2]: https://github.com/outlines-dev/outlines/pull/948 + [1]: https://github.com/dottxt-ai/outlines/pull/763 + [2]: https://github.com/dottxt-ai/outlines/pull/948 """ tokenizer = AutoTokenizer.from_pretrained("openai-community/gpt2") tokenizer = TransformerTokenizer(tokenizer=tokenizer) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index fc4166535..ff247b0f4 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -76,7 +76,7 @@ def model_vllm(tmp_path_factory): return models.vllm("facebook/opt-125m", gpu_memory_utilization=0.1) -# TODO: exllamav2 failing in main, address in https://github.com/outlines-dev/outlines/issues/808 +# TODO: exllamav2 failing in main, address in https://github.com/dottxt-ai/outlines/issues/808 # TODO: t5 tokenizer doesn't work with streaming """ @pytest.fixture(scope="session") @@ -235,7 +235,7 @@ def test_generate_fsm(request, model_fixture, pattern): @pytest.mark.skip( - "Fix issues with JSON, some models fail this test https://github.com/outlines-dev/outlines/issues/985" + "Fix issues with JSON, some models fail this test https://github.com/dottxt-ai/outlines/issues/985" ) @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) def test_generate_json(request, model_fixture, sample_schema): diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py index 0a98f0226..08521c672 100644 --- a/tests/generate/test_integration_llamacpp.py +++ b/tests/generate/test_integration_llamacpp.py @@ -250,7 +250,7 @@ def test_llamacpp_json_schema(model): ], ) def test_byte_tokenizer_regression(repo, model_path, hf_tokenizer_uri): - """Reproduce https://github.com/outlines-dev/outlines/issues/820""" + """Reproduce https://github.com/dottxt-ai/outlines/issues/820""" import llama_cpp model = models.llamacpp( diff --git a/tests/test_function.py b/tests/test_function.py index 24e132d42..62f7ea29f 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -28,29 +28,29 @@ def test_download_from_github_invalid(): download_from_github("outlines/program") with pytest.raises(ValueError, match="Do not append"): - download_from_github("outlines-dev/outlines/program.py") + download_from_github("dottxt-ai/outlines/program.py") @responses.activate def test_download_from_github_success(): responses.add( responses.GET, - "https://raw.githubusercontent.com/outlines-dev/outlines/main/program.py", + "https://raw.githubusercontent.com/dottxt-ai/outlines/main/program.py", body="import outlines\n", status=200, ) - file = download_from_github("outlines-dev/outlines/program") + file = download_from_github("dottxt-ai/outlines/program") assert file == "import outlines\n" responses.add( responses.GET, - "https://raw.githubusercontent.com/outlines-dev/outlines/main/foo/bar/program.py", + "https://raw.githubusercontent.com/dottxt-ai/outlines/main/foo/bar/program.py", body="import outlines\n", status=200, ) - file = download_from_github("outlines-dev/outlines/foo/bar/program") + file = download_from_github("dottxt-ai/outlines/foo/bar/program") assert file == "import outlines\n" From cfde8b7faa02df89fbbe30c0f0ef8cee9824799f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 16 Sep 2024 18:31:52 +0200 Subject: [PATCH 071/505] Change the color of the logo (#1155) --- docs/assets/images/logo-square.png | Bin 0 -> 2233 bytes docs/assets/images/logo-square.svg | 45 +++++++++++++------------- docs/assets/images/logo.png | Bin 6354 -> 6386 bytes docs/assets/images/logo.svg | 49 +++++++++++++++-------------- docs/overrides/home.html | 2 +- docs/overrides/main.html | 1 - 6 files changed, 49 insertions(+), 48 deletions(-) create mode 100644 docs/assets/images/logo-square.png diff --git a/docs/assets/images/logo-square.png b/docs/assets/images/logo-square.png new file mode 100644 index 0000000000000000000000000000000000000000..0592b8fa9a2d43e08f7367e187514beabc685e2f GIT binary patch literal 2233 zcmcgue>9VO7=PXM8_DUO${Zw>>J%v?8>;KK?oKtHGj*_)CTBK|HTY6i|Ax(k?SPtS#=>2rO*I~#^P5WV zA)Cu(9Wx4vj4h&aEp&t(a+*7(d-|9$q^n*c;}WT3M&~0Hb%Wi3G2y(meZqdWHTwd5Zm2oM=_UUkaW0Md)lyT?dJ-ZhXs_zU#$`uDrM_s{?tvv zGt+|TxTQn4zEsY<_cqUtI&{blN_3_tO!^0x8EMXn>q9(Z42b87XW+Ex8Sfr7?a9Gj zt90X$+RFEnc;$vBZ$^|fA>maY>~%$3jwclB)rS>4?re`7c_p+)U%WF$40zO_nxp2l z-fdrVpNUwz%P~N4>nCv!*eQy|Nu*R?udxy4EkgE_+H94BGM^z_JH`s7-PaZ;d{9_x zG36Qt=9p)=t@s$W_%Yp9je|BBT1uX5Y1Yj`;fIa!@tp zI4e-uyAwOtNy?MH4}BR1GaiA|qTtkC6i>H)V?Tbi{slMc2c&s=%SFS?2x)s{-H%+e zOi@{;6l`&I4#l~X%vD!^}KE_ANRuooB3nrg5{d-;LMgy|y0{N~=1(8LJ>R&%09O((XAoD5 zy`;7YD&ex^G?LZNElMRkC%djkt!&xhJ#8q9G;(_OxE z@|4iG`$LS~$XuBp(nhJ<9QVSdSjL~&`9dJJOvVOglH{??i#IGa7NYgUCOf3JwIBTL O032)()+L8d{rWfXa;-}M literal 0 HcmV?d00001 diff --git a/docs/assets/images/logo-square.svg b/docs/assets/images/logo-square.svg index 1d1eb3a38..29b1c20f2 100644 --- a/docs/assets/images/logo-square.svg +++ b/docs/assets/images/logo-square.svg @@ -28,8 +28,8 @@ showgrid="false" showguides="true" inkscape:zoom="1.0083333" - inkscape:cx="280.66116" - inkscape:cy="25.785124" + inkscape:cx="280.66117" + inkscape:cy="25.785125" inkscape:window-width="3440" inkscape:window-height="1416" inkscape:window-x="0" @@ -46,78 +46,79 @@ inkscape:locked="false" /> + style="fill:#e35a26;fill-opacity:1" /> diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png index 14d1c727e132c9b07a9d922a46eece1af4e9a394..005b7c6d2195cf0af295c15fd2e167f3f30b1fad 100644 GIT binary patch literal 6386 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 diff --git a/docs/assets/images/logo.svg b/docs/assets/images/logo.svg index 7d3b4f84b..440b3f0dc 100644 --- a/docs/assets/images/logo.svg +++ b/docs/assets/images/logo.svg @@ -28,8 +28,8 @@ showgrid="false" showguides="true" inkscape:zoom="1.0083333" - inkscape:cx="280.66116" - inkscape:cy="25.785124" + inkscape:cx="280.66117" + inkscape:cy="25.785125" inkscape:window-width="3440" inkscape:window-height="1416" inkscape:window-x="0" @@ -45,7 +45,7 @@ id="guide315" inkscape:locked="false" />outlinesoutlines + style="fill:#e35a26;fill-opacity:1" /> diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 1114895e6..0c97d5ac6 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -117,4 +117,4 @@

{% endblock %} {% block content %}{% endblock %} -{% block footer %}{% endblock %} \ No newline at end of file +{% block footer %}{% endblock %} diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 5cb6467e6..94d9808cc 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,2 +1 @@ {% extends "base.html" %} - From 1894fa3ae297a75f992849f53cd85dbfeec2e9be Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 16 Sep 2024 15:11:27 -0400 Subject: [PATCH 072/505] Remove Broken pyairports Package, Replace with airportsdata --- outlines/types/airports.py | 9 +++++---- pyproject.toml | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/outlines/types/airports.py b/outlines/types/airports.py index b4ed6784d..934ae1844 100644 --- a/outlines/types/airports.py +++ b/outlines/types/airports.py @@ -1,10 +1,11 @@ """Generate valid airport codes.""" from enum import Enum -from pyairports.airports import AIRPORT_LIST +import airportsdata + +AIRPORT_IATA_LIST = [ + (v["iata"], v["iata"]) for v in airportsdata.load().values() if v["iata"] +] -AIRPORT_IATA_LIST = list( - {(airport[3], airport[3]) for airport in AIRPORT_LIST if airport[3] != ""} -) IATA = Enum("Airport", AIRPORT_IATA_LIST) # type:ignore diff --git a/pyproject.toml b/pyproject.toml index dfda81acd..4d87e1fca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "datasets", "typing_extensions", "pycountry", - "pyairports", + "airportsdata", ] dynamic = ["version"] @@ -137,7 +137,7 @@ module = [ "uvicorn.*", "fastapi.*", "pycountry.*", - "pyairports.*", + "airportsdata.*", ] ignore_missing_imports = true From 2b1aed0bc4464abf2f185b1d376430554a52514e Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 14 Sep 2024 16:40:33 -0400 Subject: [PATCH 073/505] enable actual-byte tokens in reduced_vocabulary --- outlines/fsm/regex.py | 12 +++++++++--- tests/fsm/test_regex.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/outlines/fsm/regex.py b/outlines/fsm/regex.py index 8cfd81ead..6b105a7b9 100644 --- a/outlines/fsm/regex.py +++ b/outlines/fsm/regex.py @@ -905,9 +905,15 @@ def reduced_vocabulary( ) if token_str: - # invalid utf-8 sequences are replaced with � (\ufffd), but there - # might also be tokens specifically for �, ��, ���, etc. - if "\ufffd" in token_str and not re_replacement_seq.match(token): + if isinstance(token, bytes): + # Handle BPE tokenizers where the tokens are directly stored as bytes + # https://github.com/QwenLM/Qwen/blob/main/tokenization_note.md#regular-tokens + token_str = "".join(byte_symbol(b) for b in token) + + elif "\ufffd" in token_str and not re_replacement_seq.match(token): + # invalid utf-8 sequences are replaced with � (\ufffd), but there + # might also be tokens specifically for �, ��, ���, etc. + if re_llama_byte_token.match(token): # llama-like tokenizers have <0xXX> tokens for all # bytes >= 0x80 and represent all incomplete utf-8 diff --git a/tests/fsm/test_regex.py b/tests/fsm/test_regex.py index 7418deca2..1789c4a7c 100644 --- a/tests/fsm/test_regex.py +++ b/tests/fsm/test_regex.py @@ -714,8 +714,29 @@ def test_reduced_vocabulary_with_rare_tokens(rare_token): [1]: https://github.com/dottxt-ai/outlines/pull/763 [2]: https://github.com/dottxt-ai/outlines/pull/948 + [3]: https://github.com/dottxt-ai/outlines/pull/1153 """ tokenizer = AutoTokenizer.from_pretrained("openai-community/gpt2") tokenizer = TransformerTokenizer(tokenizer=tokenizer) tokenizer.vocabulary[rare_token] = max(tokenizer.vocabulary.values()) + 1 reduced_vocabulary(tokenizer) + + +def test_reduced_vocabulary_with_byte_tokens(): + class MockTokenizer: + vocabulary = { + "string": 1, + b"\xa1": 2, # Qwen-Style + "eos": 3, + } + special_tokens = {"eos"} + eos_token_id = 3 + + def convert_token_to_string(self, token): + return b"\xef\xbf\xbd".decode() + + reduced_vocab = reduced_vocabulary(MockTokenizer()) + + # See fsm.regex.get_token_transition_keys() + # FSM transition keys represents bytes as + assert reduced_vocab[0][1][0] == "\x00A1" From 55919506da45cd9e0289a23d9689f3e783f67317 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sun, 15 Sep 2024 16:38:48 -0400 Subject: [PATCH 074/505] Use OpenAI API For Structured Generation (json, choice) --- outlines/generate/choice.py | 27 +++-- outlines/generate/json.py | 38 ++++++- outlines/models/openai.py | 213 +++--------------------------------- pyproject.toml | 1 - tests/models/test_openai.py | 116 ++++++++++++-------- 5 files changed, 134 insertions(+), 261 deletions(-) diff --git a/outlines/generate/choice.py b/outlines/generate/choice.py index 931409a59..595513d52 100644 --- a/outlines/generate/choice.py +++ b/outlines/generate/choice.py @@ -1,3 +1,4 @@ +import json as pyjson from functools import singledispatch from typing import Callable, List @@ -5,6 +6,7 @@ from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial +from .json import json from .regex import regex @@ -24,13 +26,22 @@ def choice( def choice_openai( model: OpenAI, choices: List[str], sampler: Sampler = multinomial() ) -> Callable: - if not isinstance(sampler, multinomial): - raise NotImplementedError( - r"The OpenAI API does not support any other sampling algorithm " - + "that the multinomial sampler." - ) - - def generate_choice(prompt: str, max_tokens: int = 1): - return model.generate_choice(prompt, choices, max_tokens) + """ + Call OpenAI API with response_format of a dict: + {"result": } + """ + + choices_schema = pyjson.dumps( + { + "type": "object", + "properties": {"result": {"type": "string", "enum": choices}}, + "additionalProperties": False, + "required": ["result"], + } + ) + generator = json(model, choices_schema, sampler) + + def generate_choice(*args, **kwargs): + return generator(*args, **kwargs)["result"] return generate_choice diff --git a/outlines/generate/json.py b/outlines/generate/json.py index 6209840e2..f75878d29 100644 --- a/outlines/generate/json.py +++ b/outlines/generate/json.py @@ -70,9 +70,39 @@ def json( @json.register(OpenAI) def json_openai( - model, schema_object: Union[str, object, Callable], sampler: Sampler = multinomial() + model, schema_object: Union[str, object], sampler: Sampler = multinomial() ): - raise NotImplementedError( - "Cannot use JSON Schema-structure generation with an OpenAI model " - + "due to the limitations of the OpenAI API" + if not isinstance(sampler, multinomial): + raise NotImplementedError( + r"The OpenAI API does not support any other sampling algorithm " + + "than the multinomial sampler." + ) + + if isinstance(schema_object, type(BaseModel)): + schema = pyjson.dumps(schema_object.model_json_schema()) + format_sequence = lambda x: schema_object.parse_raw(x) + elif isinstance(schema_object, str): + schema = schema_object + format_sequence = lambda x: pyjson.loads(x) + else: + raise ValueError( + f"Cannot parse schema {schema_object}. The schema must be either " + + "a Pydantic object, a function or a string that contains the JSON " + + "Schema specification" + ) + + # create copied, patched model with normalized json schema set + generator = model.new_with_replacements( + response_format={ + "type": "json_schema", + "json_schema": { + "name": "default", + "strict": True, + "schema": pyjson.loads(schema), + }, + } ) + + generator.format_sequence = format_sequence + + return generator diff --git a/outlines/models/openai.py b/outlines/models/openai.py index 692e64ac5..89c26f217 100644 --- a/outlines/models/openai.py +++ b/outlines/models/openai.py @@ -1,9 +1,8 @@ """Integration with OpenAI's API.""" +import copy import functools -import warnings from dataclasses import asdict, dataclass, field, replace -from itertools import zip_longest -from typing import Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union import numpy as np @@ -74,7 +73,6 @@ def __init__( self, client, config, - tokenizer=None, system_prompt: Optional[str] = None, ): """Create an `OpenAI` instance. @@ -89,13 +87,9 @@ def __init__( config An instance of `OpenAIConfig`. Can be useful to specify some parameters that cannot be set by calling this class' methods. - tokenizer - The tokenizer associated with the model the client connects to. - """ self.client = client - self.tokenizer = tokenizer self.config = config # We count the total number of prompt and generated tokens as returned @@ -104,6 +98,8 @@ def __init__( self.prompt_tokens = 0 self.completion_tokens = 0 + self.format_sequence = lambda seq: seq + def __call__( self, prompt: Union[str, List[str]], @@ -152,107 +148,17 @@ def __call__( self.prompt_tokens += prompt_tokens self.completion_tokens += completion_tokens - return response + return self.format_sequence(response) def stream(self, *args, **kwargs): raise NotImplementedError( "Streaming is currently not supported for the OpenAI API" ) - def generate_choice( - self, - prompt: str, - choices: List[str], - max_tokens: Optional[int] = None, - system_prompt: Optional[str] = None, - ) -> str: - """Call the OpenAI API to generate one of several choices. - - Parameters - ---------- - prompt - A string or list of strings that will be used to prompt the model - choices - The list of strings between which we ask the model to choose - max_tokens - The maximum number of tokens to generate - system_prompt - The content of the system message that precedes the user's prompt. - - """ - if self.tokenizer is None: - raise ValueError( - "You must initialize the `OpenAI` class with a tokenizer to use `outlines.generate.choice`" - ) - - config = replace(self.config, max_tokens=max_tokens) - - greedy = False - decoded: List[str] = [] - encoded_choices_left: List[List[int]] = [ - self.tokenizer.encode(word) for word in choices - ] - - while len(encoded_choices_left) > 0: - max_tokens_left = max([len(tokens) for tokens in encoded_choices_left]) - transposed_choices_left: List[Set] = [ - {item for item in subset if item is not None} - for subset in zip_longest(*encoded_choices_left) - ] - - if not greedy: - mask = build_optimistic_mask(transposed_choices_left) - else: - mask = {} - for token in transposed_choices_left[0]: # build greedy mask - mask[token] = 100 - - if len(mask) == 0: - break - - config = replace(config, logit_bias=mask, max_tokens=max_tokens_left) - - response, prompt_tokens, completion_tokens = generate_chat( - prompt, system_prompt, self.client, config - ) - self.prompt_tokens += prompt_tokens - self.completion_tokens += completion_tokens - - encoded_response = self.tokenizer.encode(response) - - if encoded_response in encoded_choices_left: - decoded.append(response) - break - else: - ( - encoded_response, - encoded_choices_left, - ) = find_response_choices_intersection( - encoded_response, encoded_choices_left - ) - - if len(encoded_response) == 0: - greedy = True # next iteration will be "greedy" - continue - else: - decoded.append("".join(self.tokenizer.decode(encoded_response))) - - if len(encoded_choices_left) == 1: # only one choice left - choice_left = self.tokenizer.decode(encoded_choices_left[0]) - decoded.append(choice_left) - break - - greedy = False # after each success, stay with (or switch to) "optimistic" approach - - prompt = prompt + "".join(decoded) - - choice = "".join(decoded) - - return choice - - def generate_json(self): - """Call the OpenAI API to generate a JSON object.""" - raise NotImplementedError + def new_with_replacements(self, **kwargs): + new_instance = copy.copy(self) + new_instance.config = replace(new_instance.config, **kwargs) + return new_instance def __str__(self): return self.__class__.__name__ + " API" @@ -313,81 +219,6 @@ async def call_api(prompt, system_prompt, config): return results, usage["prompt_tokens"], usage["completion_tokens"] -def find_longest_intersection(response: List[int], choice: List[int]) -> List[int]: - """Find the longest intersection between the response and the choice.""" - for i, (token_r, token_c) in enumerate(zip_longest(response, choice)): - if token_r != token_c: - return response[:i] - - return response - - -def find_response_choices_intersection( - response: List[int], choices: List[List[int]] -) -> Tuple[List[int], List[List[int]]]: - """Find the longest intersection between the response and the different - choices. - - Say the response is of the form `[1, 2, 3, 4, 5]` and we have the choices - `[[1, 2], [1, 2, 3], [6, 7, 8]` then the function will return `[1, 2, 3]` as the - intersection, and `[[]]` as the list of choices left. - - Parameters - ---------- - response - The model's response - choices - The remaining possible choices - - Returns - ------- - A tuple that contains the longest intersection between the response and the - different choices, and the choices which start with this intersection, with the - intersection removed. - - """ - max_len_prefix = 0 - choices_left = [] - longest_prefix = [] - for i, choice in enumerate(choices): - # Find the longest intersection between the response and the choice. - prefix = find_longest_intersection(response, choice) - - if len(prefix) > max_len_prefix: - max_len_prefix = len(prefix) - choices_left = [choice[len(prefix) :]] - longest_prefix = prefix - - elif len(prefix) == max_len_prefix: - choices_left.append(choice[len(prefix) :]) - - return longest_prefix, choices_left - - -def build_optimistic_mask( - transposed: List[Set[int]], max_mask_size: int = 300 -) -> Dict[int, int]: - """We build the largest mask possible. - - Tokens are added from left to right, so if the encoded choices are e.g. - `[[1,2], [3,4]]`, `1` and `3` will be added before `2` and `4`. - - Parameters - ---------- - transposed - A list of lists that contain the nth token of each choice. - - """ - mask: Dict[int, int] = {} - for tokens in transposed: - for token in tokens: - if len(mask) == max_mask_size: - return mask - mask[token] = 100 - - return mask - - def error_handler(api_call_fn: Callable) -> Callable: """Handle OpenAI API errors and missing API key.""" @@ -427,11 +258,10 @@ def openai_model( **openai_client_params, ): try: - import tiktoken from openai import AsyncOpenAI except ImportError: raise ImportError( - "The `openai` and `tiktoken` libraries needs to be installed in order to use Outlines' OpenAI integration." + "The `openai` library needs to be installed in order to use Outlines' OpenAI integration." ) if config is not None: @@ -441,15 +271,7 @@ def openai_model( client = AsyncOpenAI(**openai_client_params) - try: - tokenizer = tiktoken.encoding_for_model(model_name) - except KeyError: - warnings.warn( - f"Could not find a tokenizer for model {model_name}. Using default cl100k_base." - ) - tokenizer = tiktoken.get_encoding("cl100k_base") - - return OpenAI(client, config, tokenizer) + return OpenAI(client, config) def azure_openai( @@ -459,11 +281,10 @@ def azure_openai( **azure_openai_client_params, ): try: - import tiktoken from openai import AsyncAzureOpenAI except ImportError: raise ImportError( - "The `openai` and `tiktoken` libraries needs to be installed in order to use Outlines' Azure OpenAI integration." + "The `openai` library needs to be installed in order to use Outlines' Azure OpenAI integration." ) if config is not None: @@ -473,12 +294,4 @@ def azure_openai( client = AsyncAzureOpenAI(**azure_openai_client_params) - try: - tokenizer = tiktoken.encoding_for_model(model_name or deployment_name) - except KeyError: - warnings.warn( - f"Could not find a tokenizer for model {model_name or deployment_name}. Using default cl100k_base." - ) - tokenizer = tiktoken.get_encoding("cl100k_base") - - return OpenAI(client, config, tokenizer) + return OpenAI(client, config) diff --git a/pyproject.toml b/pyproject.toml index 4d87e1fca..937b0c9c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,6 @@ module = [ "pydantic.*", "pytest", "referencing.*", - "tiktoken.*", "torch.*", "transformers.*", "llama_cpp", diff --git a/tests/models/test_openai.py b/tests/models/test_openai.py index 8a7870efd..f4b97f36a 100644 --- a/tests/models/test_openai.py +++ b/tests/models/test_openai.py @@ -1,17 +1,14 @@ import importlib +import json +from contextlib import contextmanager from unittest import mock from unittest.mock import MagicMock import pytest from openai import AsyncOpenAI -from outlines.models.openai import ( - OpenAI, - OpenAIConfig, - build_optimistic_mask, - find_longest_intersection, - find_response_choices_intersection, -) +from outlines import generate +from outlines.models.openai import OpenAI, OpenAIConfig def module_patch(path): @@ -62,44 +59,67 @@ def test_openai_call(): assert mocked_generate_chat_arg_config.n == 3 -@pytest.mark.parametrize( - "response,choice,expected_intersection,expected_choices_left", - ( - ([1, 2, 3, 4], [[5, 6]], [], [[5, 6]]), - ([1, 2, 3, 4], [[5, 6], [7, 8]], [], [[5, 6], [7, 8]]), - ([1, 2, 3, 4], [[1, 2], [7, 8]], [1, 2], [[]]), - ([1, 2], [[1, 2, 3, 4], [1, 2]], [1, 2], [[3, 4], []]), - ([1, 2, 3], [[1, 2, 3, 4], [1, 2]], [1, 2, 3], [[4]]), - ), -) -def test_find_response_choices_intersection( - response, choice, expected_intersection, expected_choices_left -): - intersection, choices_left = find_response_choices_intersection(response, choice) - assert intersection == expected_intersection - assert choices_left == expected_choices_left - - -@pytest.mark.parametrize( - "response,choice,expected_prefix", - ( - ([1, 2, 3], [1, 2, 3, 4], [1, 2, 3]), - ([1, 2, 3], [1, 2, 3], [1, 2, 3]), - ([4, 5], [1, 2, 3], []), - ), -) -def test_find_longest_common_prefix(response, choice, expected_prefix): - prefix = find_longest_intersection(response, choice) - assert prefix == expected_prefix - - -@pytest.mark.parametrize( - "transposed,mask_size,expected_mask", - ( - ([{1, 2}, {3, 4}], 3, {1: 100, 2: 100, 3: 100}), - ([{1, 2}, {3, 4}], 4, {1: 100, 2: 100, 3: 100, 4: 100}), - ), -) -def test_build_optimistic_mask(transposed, mask_size, expected_mask): - mask = build_optimistic_mask(transposed, mask_size) - assert mask == expected_mask +@contextmanager +def patched_openai(completion, **oai_config): + """Create a patched openai whose chat completions always returns `completion`""" + with module_patch("outlines.models.openai.generate_chat") as mocked_generate_chat: + mocked_generate_chat.return_value = completion, 1, 2 + async_client = MagicMock(spec=AsyncOpenAI, api_key="key") + model = OpenAI( + async_client, + OpenAIConfig(max_tokens=10, temperature=0.5, n=2, stop=["."]), + ) + yield model + + +def test_openai_choice_call(): + with patched_openai(completion='{"result": "foo"}') as model: + generator = generate.choice(model, ["foo", "bar"]) + assert generator("hi") == "foo" + + +def test_openai_choice_call_invalid_server_response(): + with patched_openai(completion="not actual json") as model: + generator = generate.choice(model, ["foo", "bar"]) + with pytest.raises(json.decoder.JSONDecodeError): + generator("hi") + + +def test_openai_json_call_pydantic(): + from pydantic import BaseModel, ConfigDict, ValidationError + + class Person(BaseModel): + model_config = ConfigDict(extra="forbid") # required for openai + first_name: str + last_name: str + age: int + + completion = '{"first_name": "Usain", "last_name": "Bolt", "age": 38}' + + # assert success for valid response + with patched_openai(completion=completion) as model: + generator = generate.json(model, Person) + assert generator("fastest person") == Person.parse_raw(completion) + + # assert fail for non-json response + with patched_openai(completion="usain bolt") as model: + generator = generate.json(model, Person) + with pytest.raises(ValidationError): + assert generator("fastest person") + + +def test_openai_json_call_str(): + person_schema = '{"additionalProperties": false, "properties": {"first_name": {"title": "First Name", "type": "string"}, "last_name": {"title": "Last Name", "type": "string"}, "age": {"title": "Age", "type": "integer"}}, "required": ["first_name", "last_name", "age"], "title": "Person", "type": "object"}' + + output = {"first_name": "Usain", "last_name": "Bolt", "age": 38} + + # assert success for valid response + with patched_openai(completion=json.dumps(output)) as model: + generator = generate.json(model, person_schema) + assert generator("fastest person") == output + + # assert fail for non-json response + with patched_openai(completion="usain bolt") as model: + generator = generate.json(model, person_schema) + with pytest.raises(json.decoder.JSONDecodeError): + assert generator("fastest person") From 289ef5d7db674fc159f56fa14f5bcf45b4e4bec2 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sun, 15 Sep 2024 16:39:10 -0400 Subject: [PATCH 075/505] reflect in docs: models.openai with new interface, also remove references to gpt-3 and gpt-4 --- docs/reference/index.md | 2 +- docs/reference/models/models.md | 6 +++- docs/reference/models/openai.md | 50 ++++++++++++++++++++++++++------- docs/reference/text.md | 2 +- examples/babyagi.py | 2 +- examples/math_generate_code.py | 2 +- examples/meta_prompting.py | 2 +- examples/pick_odd_one_out.py | 2 +- examples/react.py | 21 +++++++------- examples/self_consistency.py | 2 +- 10 files changed, 63 insertions(+), 28 deletions(-) diff --git a/docs/reference/index.md b/docs/reference/index.md index a5357fd86..0cd42ba88 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -10,6 +10,6 @@ By default, language models stop generating tokens after and token was gen ```python import outlines.models as models -complete = models.openai("gpt-3.5-turbo") +complete = models.openai("gpt-4o-mini") expert = complete("Name an expert in quantum gravity.", stop_at=["\n", "."]) ``` diff --git a/docs/reference/models/models.md b/docs/reference/models/models.md index 34b5be4cf..4d2dda8c9 100644 --- a/docs/reference/models/models.md +++ b/docs/reference/models/models.md @@ -42,7 +42,11 @@ model = outlines.models.openai( | Stream | ✅ | ❌ | ❌ | ✅ | ? | ✅ | ❌ | | **`outlines.generate`** | | | | | | | | | Text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Structured* | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| __Structured__ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| JSON Schema | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Choice | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Regex | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| Grammar | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ## Caveats diff --git a/docs/reference/models/openai.md b/docs/reference/models/openai.md index 7f610c179..5c737c916 100644 --- a/docs/reference/models/openai.md +++ b/docs/reference/models/openai.md @@ -2,22 +2,21 @@ !!! Installation - You need to install the `openai` and `tiktoken` libraries to be able to use the OpenAI API in Outlines. + You need to install the `openai` library to be able to use the OpenAI API in Outlines. ## OpenAI models -Outlines supports models available via the OpenAI Chat API, e.g. ChatGPT and GPT-4. You can initialize the model by passing the model name to `outlines.models.openai`: +Outlines supports models available via the OpenAI Chat API, e.g. GPT-4o, ChatGPT and GPT-4. You can initialize the model by passing the model name to `outlines.models.openai`: ```python from outlines import models -model = models.openai("gpt-3.5-turbo") -model = models.openai("gpt-4-turbo") +model = models.openai("gpt-4o-mini") model = models.openai("gpt-4o") ``` -Check the [OpenAI documentation](https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4) for an up-to-date list of available models. You can pass any parameter you would pass to `openai.AsyncOpenAI` as keyword arguments: +Check the [OpenAI documentation](https://platform.openai.com/docs/models/gpt-4o) for an up-to-date list of available models. You can pass any parameter you would pass to `openai.AsyncOpenAI` as keyword arguments: ```python import os @@ -25,7 +24,7 @@ from outlines import models model = models.openai( - "gpt-3.5-turbo", + "gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"] ) ``` @@ -56,8 +55,8 @@ from outlines import models model = models.azure_openai( "azure-deployment-name", - "gpt-3.5-turbo", - api_version="2023-07-01-preview", + "gpt-4o-mini", + api_version="2024-07-18", azure_endpoint="https://example-endpoint.openai.azure.com", ) ``` @@ -111,6 +110,37 @@ model = models.openai(client, config) You need to pass the async client to be able to do batch inference. +## Structured Generation Support + +Outlines provides support for [OpenAI Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs/json-mode) via `outlines.generate.json`, `outlines.generate.choice` + +```python +from pydantic import BaseModel, ConfigDict +import outlines.models as models +from outlines import generate + +model = models.openai("gpt-4o-mini") + +class Person(BaseModel): + model_config = ConfigDict(extra='forbid') # required for openai + first_name: str + last_name: str + age: int + +generate.json(model, Person) +generator("current indian prime minister on january 1st 2023") +# Person(first_name='Narendra', last_name='Modi', age=72) + +generator = generate.choice(model, ["Chicken", "Egg"]) +print(generator("Which came first?")) +# Chicken +``` + +!!! Warning + + Structured generation support only provided to OpenAI-compatible endpoints which conform to OpenAI's standard. Additionally, `generate.regex` and `generate.cfg` are not supported. + + ## Advanced configuration For more advanced configuration option, such as support proxy, please consult the [OpenAI SDK's documentation](https://github.com/openai/openai-python): @@ -146,7 +176,7 @@ config = OpenAIConfig( top_p=.95, seed=0, ) -model = models.openai("gpt-3.5-turbo", config) +model = models.openai("gpt-4o-mini", config) ``` ## Monitoring API use @@ -158,7 +188,7 @@ from openai import AsyncOpenAI import outlines.models -model = models.openai("gpt-4") +model = models.openai("gpt-4o") print(model.prompt_tokens) # 0 diff --git a/docs/reference/text.md b/docs/reference/text.md index 3d0d130f1..f36a67da0 100644 --- a/docs/reference/text.md +++ b/docs/reference/text.md @@ -9,7 +9,7 @@ Outlines provides a unified interface to generate text with many language models ```python from outlines import models, generate -model = models.openai("gpt-4") +model = models.openai("gpt-4o-mini") generator = generate.text(model) answer = generator("What is 2+2?") diff --git a/examples/babyagi.py b/examples/babyagi.py index 6af2ead88..0a7a0b13b 100644 --- a/examples/babyagi.py +++ b/examples/babyagi.py @@ -10,7 +10,7 @@ import outlines import outlines.models as models -model = models.openai("gpt-3.5-turbo") +model = models.openai("gpt-4o-mini") complete = outlines.generate.text(model) diff --git a/examples/math_generate_code.py b/examples/math_generate_code.py index 5bf16e86a..7eb1651a7 100644 --- a/examples/math_generate_code.py +++ b/examples/math_generate_code.py @@ -35,7 +35,7 @@ def execute_code(code): prompt = answer_with_code_prompt(question, examples) -model = models.openai("gpt-3.5-turbo") +model = models.openai("gpt-4o-mini") answer = outlines.generate.text(model)(prompt) result = execute_code(answer) print(f"It takes Carla {result:.0f} minutes to download the file.") diff --git a/examples/meta_prompting.py b/examples/meta_prompting.py index 8f9ba5cee..cba18b5fe 100644 --- a/examples/meta_prompting.py +++ b/examples/meta_prompting.py @@ -140,7 +140,7 @@ def run_example(model_fn, question, model_name): parser.add_argument( "--model", type=str, - default="gpt-3.5-turbo-1106", + default="gpt-4o-mini", help="The Large Language Model to use to run the examples.", ) args = parser.parse_args() diff --git a/examples/pick_odd_one_out.py b/examples/pick_odd_one_out.py index 6c4d45927..6cd9f1daf 100644 --- a/examples/pick_odd_one_out.py +++ b/examples/pick_odd_one_out.py @@ -31,7 +31,7 @@ def build_ooo_prompt(options): options = ["sea", "mountains", "plains", "sock"] -model = models.openai("gpt-3.5-turbo") +model = models.openai("gpt-4o-mini") gen_text = outlines.generate.text(model) gen_choice = outlines.generate.choice(model, options) diff --git a/examples/react.py b/examples/react.py index ccd74dbbc..34b3c6eb2 100644 --- a/examples/react.py +++ b/examples/react.py @@ -13,6 +13,7 @@ import requests # type: ignore import outlines +import outlines.generate as generate import outlines.models as models @@ -45,25 +46,25 @@ def search_wikipedia(query: str): prompt = build_reAct_prompt("Where is Apple Computers headquarted? ") -model = models.openai("gpt-3.5-turbo") -complete = outlines.generate.text(model) +model = models.openai("gpt-4o-mini") + +mode_generator = generate.choice(model, choices=["Tho", "Act"]) +action_generator = generate.choice(model, choices=["Search", "Finish"]) +text_generator = generate.text(model) for i in range(1, 10): - mode = complete.generate_choice(prompt, choices=["Tho", "Act"], max_tokens=128) + mode = mode_generator(prompt, max_tokens=128) prompt = add_mode(i, mode, "", prompt) if mode == "Tho": - thought = complete(prompt, stop_at="\n", max_tokens=128) + thought = text_generator(prompt, stop_at="\n", max_tokens=128) prompt += f"{thought}" elif mode == "Act": - action = complete.generate_choice( - prompt, choices=["Search", "Finish"], max_tokens=128 - ) + action = action_generator(prompt, max_tokens=128) prompt += f"{action} '" - subject = complete( - prompt, stop_at=["'"], max_tokens=128 - ) # Apple Computers headquartered + subject = text_generator(prompt, stop_at=["'"], max_tokens=128) + # Apple Computers headquartered subject = " ".join(subject.split()[:2]) prompt += f"{subject}'" diff --git a/examples/self_consistency.py b/examples/self_consistency.py index e7468d5f9..f1bbe2a18 100644 --- a/examples/self_consistency.py +++ b/examples/self_consistency.py @@ -55,7 +55,7 @@ def few_shots(question, examples): """ -model = models.openai("gpt-3.5-turbo") +model = models.openai("gpt-4o-mini") generator = outlines.generate.text(model) prompt = few_shots(question, examples) answers = generator(prompt, samples=10) From e07f5500fb92acd3647f2aa13aebaf292b3fcbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sat, 21 Sep 2024 16:08:04 +0200 Subject: [PATCH 076/505] Remove link to Outlines twitter account The account is going to be deactivated soon. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 87109d182..9b01c5662 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Outlines Logo [![.txt Twitter][dottxt-twitter-badge]][dottxt-twitter] -[![Outlines Twitter][outlines-twitter-badge]][outlines-twitter] [![Documentation][documentation-badge]][documentation] [![Contributors][contributors-badge]][contributors] @@ -359,10 +358,8 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) [contributors]: https://github.com/dottxt-ai/outlines/graphs/contributors [contributors-badge]: https://img.shields.io/github/contributors/dottxt-ai/outlines?style=flat-square&logo=github&logoColor=white&color=ECEFF4 [dottxt-twitter]: https://twitter.com/dottxtai -[outlines-twitter]: https://twitter.com/OutlinesOSS [discord]: https://discord.gg/R9DSu34mGd [discord-badge]: https://img.shields.io/discord/1182316225284554793?color=81A1C1&logo=discord&logoColor=white&style=flat-square [downloads-badge]: https://img.shields.io/pypi/dm/outlines?color=89AC6B&logo=python&logoColor=white&style=flat-square [pypistats]: https://pypistats.org/packages/outlines [dottxt-twitter-badge]: https://img.shields.io/twitter/follow/dottxtai?style=social -[outlines-twitter-badge]: https://img.shields.io/twitter/follow/OutlinesOSS?style=social From 77c6d67913b24cef75fc7f40ab079c51715209e6 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Tue, 17 Sep 2024 16:50:08 -0400 Subject: [PATCH 077/505] Don't re-use logits processors in SequenceGeneratorAdapter, copy them --- outlines/generate/api.py | 9 +++++---- tests/generate/test_generate.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index ad01377c0..4919f2090 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -1,4 +1,5 @@ import datetime +from copy import copy from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union @@ -503,7 +504,7 @@ def __call__( completions = self.model.generate( prompts, generation_params, - self.logits_processor, + copy(self.logits_processor), self.sampling_params, **model_specific_params, ) @@ -525,7 +526,7 @@ def stream( return self.model.stream( prompts, generation_params, - self.logits_processor, + copy(self.logits_processor), self.sampling_params, **model_specific_params, ) @@ -556,7 +557,7 @@ def __call__( # type: ignore prompts, media, generation_params, - self.logits_processor, + copy(self.logits_processor), self.sampling_params, **model_specific_params, ) @@ -581,7 +582,7 @@ def stream( # type: ignore prompts, media, generation_params, - self.logits_processor, + copy(self.logits_processor), self.sampling_params, **model_specific_params, ) diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index ff247b0f4..a96ce8673 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -253,6 +253,16 @@ def test_generate_choice(request, model_fixture, sample_choices): assert res in sample_choices +@pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +def test_generate_choice_twice(request, model_fixture, sample_choices): + model = request.getfixturevalue(model_fixture) + generator = generate.choice(model, sample_choices) + res = generator(**get_inputs(model_fixture)) + assert res in sample_choices + res = generator(**get_inputs(model_fixture)) + assert res in sample_choices + + @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) def test_generate_format_bool(request, model_fixture): model = request.getfixturevalue(model_fixture) From 538e7147bf5085a4dc94bcfe854e3243ebca14f5 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Mon, 23 Sep 2024 16:09:12 -0500 Subject: [PATCH 078/505] Fix benchmark workflow triggers Closes #1164 --- .github/workflows/asv_benchmark_pr.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/asv_benchmark_pr.yml b/.github/workflows/asv_benchmark_pr.yml index 90fb47423..d7546f854 100644 --- a/.github/workflows/asv_benchmark_pr.yml +++ b/.github/workflows/asv_benchmark_pr.yml @@ -1,18 +1,29 @@ name: Benchmark PR on: + push: pull_request: - branches: [main] + types: [synchronize, labeled] workflow_dispatch: env: PYTHON_VERSION: "3.10" WORKING_DIR: ${{ github.workspace }}/benchmarks BENCHMARKS_OUTPUT: ${{ github.workspace }}/benchmarks_output +permissions: + contents: read + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + jobs: benchmark-pr: runs-on: ubuntu-latest - if: contains(github.event.pull_request.labels.*.name, 'run_benchmarks') || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run' + if: ${{ contains(github.event.pull_request.labels.*.name, 'run-benchmarks') || github.ref == 'refs/heads/main' }} defaults: run: From 6035e86ac8089d4f8aeab07ea116093a2ed0e03e Mon Sep 17 00:00:00 2001 From: Jan Trienes Date: Thu, 19 Sep 2024 13:55:08 +0200 Subject: [PATCH 079/505] Reuse jinja environment for a prompt --- outlines/prompts.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/outlines/prompts.py b/outlines/prompts.py index 01e900c96..a7824451a 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -24,6 +24,7 @@ class Prompt: def __post_init__(self): self.parameters: List[str] = list(self.signature.parameters.keys()) + self.jinja_environment = create_jinja_template(self.template) def __call__(self, *args, **kwargs) -> str: """Render and return the template. @@ -35,7 +36,7 @@ def __call__(self, *args, **kwargs) -> str: """ bound_arguments = self.signature.bind(*args, **kwargs) bound_arguments.apply_defaults() - return render(self.template, **bound_arguments.arguments) + return self.jinja_environment.render(**bound_arguments.arguments) def __str__(self): return self.template @@ -182,6 +183,11 @@ def render(template: str, **values: Optional[Dict[str, Any]]) -> str: A string that contains the rendered template. """ + jinja_template = create_jinja_template(template) + return jinja_template.render(**values) + + +def create_jinja_template(template: str): # Dedent, and remove extra linebreak cleaned_template = inspect.cleandoc(template) @@ -210,8 +216,7 @@ def render(template: str, **values: Optional[Dict[str, Any]]) -> str: env.filters["args"] = get_fn_args jinja_template = env.from_string(cleaned_template) - - return jinja_template.render(**values) + return jinja_template def get_fn_name(fn: Callable): From 30531e58f8e15fd4f47bed5d5718209cbad4b3e0 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 27 Sep 2024 19:34:00 -0400 Subject: [PATCH 080/505] Use more performant fsm backend --- .github/workflows/tests.yml | 2 +- benchmarks/bench_cfg_guide.py | 5 - benchmarks/bench_json_schema.py | 3 +- benchmarks/bench_numba_compile.py | 34 - benchmarks/bench_regex_guide.py | 4 +- benchmarks/common.py | 6 - outlines/fsm/fsm.py | 47 - outlines/fsm/guide.py | 247 +--- outlines/fsm/parsing.py | 3 +- outlines/fsm/regex.py | 1003 ----------------- pyproject.toml | 7 +- tests/fsm/test_fsm.py | 92 -- tests/fsm/test_regex.py | 742 ------------ .../generate/test_integration_transformers.py | 2 +- 14 files changed, 24 insertions(+), 2173 deletions(-) delete mode 100644 benchmarks/bench_numba_compile.py delete mode 100644 outlines/fsm/fsm.py delete mode 100644 outlines/fsm/regex.py delete mode 100644 tests/fsm/test_fsm.py delete mode 100644 tests/fsm/test_regex.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 53d20b70b..36e6f8526 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.10"] + python-version: ["3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/benchmarks/bench_cfg_guide.py b/benchmarks/bench_cfg_guide.py index 14dc31c73..477104435 100644 --- a/benchmarks/bench_cfg_guide.py +++ b/benchmarks/bench_cfg_guide.py @@ -7,8 +7,6 @@ from outlines.fsm.guide import CFGGuide from outlines.models.transformers import TransformerTokenizer -from .common import ensure_numba_compiled - random.seed(42) @@ -30,9 +28,6 @@ class CFGGuideBenchmark: def setup(self, grammar_name): self.tokenizer = get_tiny_tokenizer() - ensure_numba_compiled( - self.tokenizer - ) # numba not currently used, but will be in the future self.prebuilt_cfg_guide = CFGGuide( benched_grammars[grammar_name], self.tokenizer ) diff --git a/benchmarks/bench_json_schema.py b/benchmarks/bench_json_schema.py index 8d1ceeb24..8990b015c 100644 --- a/benchmarks/bench_json_schema.py +++ b/benchmarks/bench_json_schema.py @@ -2,7 +2,7 @@ from outlines.fsm.guide import RegexGuide from outlines.fsm.json_schema import build_regex_from_schema -from .common import ensure_numba_compiled, setup_tokenizer # noqa: E402 +from .common import setup_tokenizer # noqa: E402 simple_schema = """{ "$defs": { @@ -69,7 +69,6 @@ class JsonSchemaBenchmark: def setup(self, schema_name): self.tokenizer = setup_tokenizer() self.schema = schemas[schema_name] - ensure_numba_compiled(self.tokenizer) @cache_disabled() def time_json_schema_to_regex(self, schema_name): diff --git a/benchmarks/bench_numba_compile.py b/benchmarks/bench_numba_compile.py deleted file mode 100644 index 2713707e5..000000000 --- a/benchmarks/bench_numba_compile.py +++ /dev/null @@ -1,34 +0,0 @@ -import importlib - -import interegular -import numba - -from outlines.caching import cache_disabled -from outlines.fsm import regex - -from .common import setup_tokenizer - - -class NumbaCompileBenchmark: - def setup(self): - self.tokenizer = setup_tokenizer() - self.regex = regex - original_njit = numba.njit - - def mock_njit(*args, **kwargs): - kwargs["cache"] = False - return original_njit(*args, **kwargs) - - self.original_njit = original_njit - numba.njit = mock_njit - importlib.reload(self.regex) - self.regex_pattern, _ = self.regex.make_deterministic_fsm( - interegular.parse_pattern("a").to_fsm().reduce() - ) - - def teardown(self): - numba.njit = self.original_njit - - @cache_disabled() - def time_compile_numba(self): - self.regex.create_fsm_index_tokenizer(self.regex_pattern, self.tokenizer) diff --git a/benchmarks/bench_regex_guide.py b/benchmarks/bench_regex_guide.py index 099f94df2..7aaef6bac 100644 --- a/benchmarks/bench_regex_guide.py +++ b/benchmarks/bench_regex_guide.py @@ -1,7 +1,7 @@ from outlines.caching import cache_disabled from outlines.fsm.guide import RegexGuide -from .common import ensure_numba_compiled, setup_tokenizer +from .common import setup_tokenizer regex_samples = { "email": r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", @@ -21,7 +21,6 @@ class RegexGuideBenchmark: def setup(self, pattern_name): self.tokenizer = setup_tokenizer() - ensure_numba_compiled(self.tokenizer) self.pattern = regex_samples[pattern_name] @cache_disabled() @@ -34,7 +33,6 @@ class MemoryRegexGuideBenchmark: def setup(self, pattern_name): self.tokenizer = setup_tokenizer() - ensure_numba_compiled(self.tokenizer) self.pattern = regex_samples[pattern_name] @cache_disabled() diff --git a/benchmarks/common.py b/benchmarks/common.py index 7d999ea9b..e920888f5 100644 --- a/benchmarks/common.py +++ b/benchmarks/common.py @@ -1,14 +1,8 @@ from transformers import AutoTokenizer -from outlines.fsm.guide import RegexGuide from outlines.models.transformers import TransformerTokenizer def setup_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("gpt2") return TransformerTokenizer(tokenizer) - - -def ensure_numba_compiled(tokenizer): - RegexGuide("a", tokenizer) - return True diff --git a/outlines/fsm/fsm.py b/outlines/fsm/fsm.py deleted file mode 100644 index bfcf55c03..000000000 --- a/outlines/fsm/fsm.py +++ /dev/null @@ -1,47 +0,0 @@ -import warnings -from typing import TYPE_CHECKING, Iterable, NewType, Optional - -from outlines.fsm.guide import RegexGuide, StopAtEOSGuide - -if TYPE_CHECKING: - from outlines.models.tokenizer import Tokenizer - -FSMState = NewType("FSMState", int) - - -class StopAtEosFSM(StopAtEOSGuide): - """FSM to generate text until EOS has been generated.""" - - def __init__(self, tokenizer: "Tokenizer"): - warnings.warn( - UserWarning( - "The `StopAtTokenFSM` interface is deprecated and will be removed on 2024-06-01. Please use `StopAtEOSGuide` instead." - ) - ) - super().__init__(tokenizer) - - def allowed_token_ids(self, state: FSMState) -> Optional[Iterable[int]]: - next_instruction = self.get_next_instruction(state) - return next_instruction.tokens - - def next_state(self, state: FSMState, token_id: int) -> FSMState: - return FSMState(self.get_next_state(state, token_id)) - - -class RegexFSM(RegexGuide): - """FSM to generate text that is in the language of a regular expression.""" - - def __init__(self, regex_string: str, tokenizer): - warnings.warn( - UserWarning( - "The `RegexFSM` interface is deprecated and will be removed on 2024-06-01. Please use `RegexGuide` instead." - ) - ) - super().__init__(regex_string, tokenizer) - - def allowed_token_ids(self, state: FSMState) -> Optional[Iterable[int]]: - next_instruction = self.get_next_instruction(state) - return next_instruction.tokens - - def next_state(self, state: FSMState, token_id: int) -> FSMState: - return FSMState(self.get_next_state(state, token_id)) diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index b7b121fe6..a3a2c7369 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -1,72 +1,31 @@ import collections import copy import warnings -from dataclasses import dataclass -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generator, - List, - Optional, - Protocol, - Set, - Tuple, - Union, -) +from typing import TYPE_CHECKING, Any, Generator, Union -import interegular import torch from lark.indenter import DedentError from lark.lexer import UnexpectedCharacters, UnexpectedToken +from outlines_core.fsm.guide import Generate +from outlines_core.fsm.guide import Guide as CoreGuide +from outlines_core.fsm.guide import RegexGuide as CoreRegexGuide +from outlines_core.fsm.guide import Write +from outlines_core.fsm.guide import ( + create_states_mapping as uncached_create_states_mapping, +) from outlines import grammars from outlines.caching import cache from outlines.fsm.parsing import PartialLark, PartialParserState -from outlines.fsm.regex import ( - create_fsm_index_tokenizer, - make_byte_level_fsm, - make_deterministic_fsm, -) if TYPE_CHECKING: from outlines.models.tokenizer import Tokenizer -@dataclass(frozen=True) -class Write: - """Write instruction. - - Attributes - ---------- - tokens - The sequence of tokens to be added to the current sequence by the - generation process. - - """ - - tokens: List[int] - - -@dataclass(frozen=True) -class Generate: - """Generate instruction - - Attributes - ---------- - tokens - The tokens that lead to a valid completion if generated. A value - of ``None`` indicates that all tokens are allowed. - """ - - tokens: Optional[List[int]] - - Instruction = Union[Write, Generate] -class Guide(Protocol): +class Guide(CoreGuide): """Base definition of a generation guide. A generation guide defines the behavior of a finite-state machine that guides @@ -78,18 +37,6 @@ class Guide(Protocol): initial_state: Any - def get_next_instruction(self, state: Any) -> Instruction: - ... - - def get_next_state(self, state: Any, token_id: int) -> Any: - ... - - def is_final_state(self, state: Any) -> bool: - ... - - def copy(self) -> "Guide": - ... - class StopAtEOSGuide(Guide): """Guide to generate tokens until the EOS token has been generated.""" @@ -127,64 +74,15 @@ def copy(self): @cache() -def create_states_mapping( - regex_string: str, - tokenizer: "Tokenizer", - regex_parser: Callable[[str], interegular.Pattern] = interegular.parse_pattern, - frozen_tokens: List[str] = [], -) -> Tuple[Dict[int, Dict[int, int]], Set[int], set]: - """Create the variables related to the mapping between states and tokens - The parameters of the function are used for caching purpose. - - Parameters - ---------- - regex_string: (`str`): - The regular expression string to generate a states mapping for. - tokenizer: (`Tokenizer`): - The model's tokenizer. - regex_parser: (`Callable[[str], interegular.Pattern]`, *optional*): - A function that parses a regex string into an `interegular` Pattern object. - frozen_tokens: (`List[str]`, *optional*): - A list of tokens that should be kept as-is when expanding the token-level FSM - into a byte-level FSM. Defaults to an empty list. - - Returns - ------- - states_to_token_maps: (`Dict[int, Dict[int, int]]`): - A mapping from states to a mapping from token ids originating from that state - to the next state to transition to given that token. The structure is as follows: - (origin_state -> (token_id -> next_state)) - empty_token_ids: (`Set[int]`): - A set of token ids that correspond to empty strings. - final_states: (`set`): - A set of final states in the FSM. - """ - regex_pattern = regex_parser(regex_string) - byte_fsm = make_byte_level_fsm( - regex_pattern.to_fsm().reduce(), keep_utf8=True, frozen_tokens=frozen_tokens - ) - regex_fsm, _ = make_deterministic_fsm(byte_fsm) - states_to_token_maps, empty_token_ids = create_fsm_index_tokenizer( - regex_fsm, tokenizer, frozen_tokens=frozen_tokens - ) - - # We make sure that it is possible to generate strings in the language - # of the regular expression with the tokens present in the model's - # vocabulary. - if not any( - regex_fsm.finals.intersection(v.values()) for v in states_to_token_maps.values() - ): - raise ValueError( - "The vocabulary does not allow us to build a sequence that matches the input regex" - ) +def create_states_mapping(regex_string, tokenizer): + return uncached_create_states_mapping(regex_string, tokenizer) - return states_to_token_maps, empty_token_ids, regex_fsm.finals - -class RegexGuide(Guide): - """Guide to generate text in the language of a regular expression.""" - - initial_state = 0 +class RegexGuide(CoreRegexGuide): + """ + Guide to generate text in the language of a regular expression. + CoreRegexGuide with outlines cache + """ def __init__(self, regex_string: str, tokenizer: "Tokenizer"): ( @@ -196,119 +94,6 @@ def __init__(self, regex_string: str, tokenizer: "Tokenizer"): self.final_states = fsm_finals | {-1} self._cache_state_to_token_tensor() - def get_next_instruction(self, state: int) -> Instruction: - """Return the next instruction for guided generation. - - The initialization of the guide builds an index which maps FSM states to a - map from authorized tokens to the state in which the guide needs to move - if said token is generated. Therefore the authorized tokens at the - current state are the keys of the map returned by the value of the index - for current state. - - If the current state is not contained in the end this means that we are - in a final state of the guide. We only authorize EOS tokens in the final - state. - - Parameters - ---------- - state - The current state of the guide. - - Returns - ------- - A `Generate` instance that contains the model and the allowed token ids. - - """ - next_tokens_mask = self.states_to_token_mask.get(state) - if next_tokens_mask is None: - return Write(torch.tensor([self.eos_token_id])) - - return Generate(next_tokens_mask) - - def get_next_state(self, state: int, token_id: int) -> int: - """Update the state of the guide. - - We use the index to determine to which state the guide should transition - given the token that was just generated. - - Parameters - ---------- - state - The current state of the guide. - token_id - The id of the token that was just generated. - - Returns - ------- - The new state of the guide. - - """ - if token_id == self.eos_token_id or state not in self.states_to_token_maps: - return -1 - - last_token_to_end_state = self.states_to_token_maps[state] - next_state = last_token_to_end_state.get(token_id) - if next_state is None: - next_state = -1 - - return next_state - - @classmethod - def from_interegular_fsm( - cls, interegular_fsm: interegular.fsm.FSM, tokenizer: "Tokenizer" - ): - from_interegular_instance = cls.__new__(cls) - - def create_states_mapping_from_interegular_fsm( - fsm: interegular.fsm.FSM, - ) -> Tuple[dict, set]: - """Create the variables related to the mapping between states and tokens - The parameters of the function are used for caching purpose - """ - byte_fsm = make_byte_level_fsm(fsm.reduce(), keep_utf8=True) - regex_fsm, _ = make_deterministic_fsm(byte_fsm) - states_to_token_maps, empty_token_ids = create_fsm_index_tokenizer( - regex_fsm, tokenizer - ) - - # We make sure that it is possible to generate strings in the language - # of the regular expression with the tokens present in the model's - # vocabulary. - if not any( - regex_fsm.finals.intersection(v.values()) - for v in states_to_token_maps.values() - ): - raise ValueError( - "The vocabulary does not allow us to build a sequence that matches the input regex" - ) - - return states_to_token_maps, empty_token_ids - - ( - from_interegular_instance.states_to_token_maps, - from_interegular_instance.empty_token_ids, - ) = create_states_mapping_from_interegular_fsm(interegular_fsm) - from_interegular_instance.eos_token_id = tokenizer.eos_token_id - from_interegular_instance._cache_state_to_token_tensor() - return from_interegular_instance - - def _cache_state_to_token_tensor(self): - """ - cache state -> token int tensor - this increases performance of mask construction substantially - """ - self.states_to_token_mask = { - state: torch.tensor(list(next_tokens_to_end_states.keys())) - for state, next_tokens_to_end_states in self.states_to_token_maps.items() - } - - def is_final_state(self, state: int) -> bool: - """Determine whether the current state of the guide is a final state.""" - return state in self.final_states - - def copy(self): - return self - CFGState = collections.namedtuple("CFGState", ["parser_state", "prev_token"]) diff --git a/outlines/fsm/parsing.py b/outlines/fsm/parsing.py index f780fb46e..92d3cc166 100644 --- a/outlines/fsm/parsing.py +++ b/outlines/fsm/parsing.py @@ -34,8 +34,7 @@ ) from lark.parsers.lalr_interactive_parser import InteractiveParser from lark.parsers.lalr_parser import LALR_Parser, ParseConf, ParserState, _Parser - -from outlines.fsm.regex import ( +from outlines_core.fsm.regex import ( fsm_union, get_sub_fsms_from_seq, get_token_transition_keys, diff --git a/outlines/fsm/regex.py b/outlines/fsm/regex.py deleted file mode 100644 index 6b105a7b9..000000000 --- a/outlines/fsm/regex.py +++ /dev/null @@ -1,1003 +0,0 @@ -import re -from collections import namedtuple -from functools import lru_cache -from typing import ( - TYPE_CHECKING, - Dict, - FrozenSet, - Generator, - List, - Sequence, - Set, - Tuple, - Union, - cast, -) - -import numba -import numpy as np -from interegular.fsm import ( - FSM, - Alphabet, - OblivionError, - State, - TransitionKey, - _AnythingElseCls, - anything_else, -) -from numba.typed.typedobjectutils import _nonoptional -from tqdm import tqdm - -if TYPE_CHECKING: - from outlines.models.tokenizer import Tokenizer - - -class BetterAlphabet(Alphabet): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - assert anything_else in self._symbol_mapping - self.anything_value = self._symbol_mapping[anything_else] - - def __getitem__(self, item): - return self._symbol_mapping.get(item, self.anything_value) - - def copy(self): - return BetterAlphabet(self._symbol_mapping.copy()) - - -class BetterFSM(FSM): - flat_transition_map: Dict[Tuple[int, int], int] - trans_key_to_states: Dict[int, List[int]] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if not isinstance(self.alphabet, BetterAlphabet): - self.__dict__["alphabet"] = BetterAlphabet(self.alphabet._symbol_mapping) - - flat_transition_map = {} - trans_key_to_states = {} - for from_state, trans_map in self.map.items(): - for trans_key, to_state in trans_map.items(): - flat_transition_map[(from_state, trans_key)] = to_state - trans_key_to_states.setdefault(trans_key, set()).add(from_state) - - self.__dict__["trans_key_to_states"] = trans_key_to_states - self.__dict__["flat_transition_map"] = flat_transition_map - self.__dict__["_fsm_info"] = None - - def copy(self): - return BetterFSM( - alphabet=self.alphabet.copy(), - states=self.states.copy(), - initial=self.initial, - finals=self.finals.copy(), - map=self.map.copy(), - __no_validation__=True, - ) - - @property - def fsm_info(self): - if self._fsm_info is None: - flat_transition_map_items = np.fromiter( - ((a[0], a[1], b) for a, b in self.flat_transition_map.items()), - dtype=np.dtype("int64, int64, int64"), - ) - trans_key_to_states_items = np.fromiter( - ((k, z) for k, v in self.trans_key_to_states.items() for z in v), - dtype=np.dtype("int64, int64"), - ) - alphabet_symbol_mapping_items = [ - (k, v) - for k, v in self.alphabet._symbol_mapping.items() - if k != anything_else - ] - nb_finals = np.fromiter(self.finals, dtype=np.dtype("int64")) - self.__dict__["_fsm_info"] = create_fsm_info( - self.initial, - nb_finals, - flat_transition_map_items, - trans_key_to_states_items, - self.alphabet.anything_value, - alphabet_symbol_mapping_items, - ) - - return self._fsm_info - - -nb_int_list_type = numba.types.ListType(numba.int64) -nb_int_pair_type = numba.types.UniTuple(numba.int64, 2) -nb_unicode_type = numba.types.unicode_type - - -@numba.njit(cache=True) -def create_fsm_info( - py_initial, - py_finals, - flat_transition_map_items, - trans_key_to_states_items, - py_anything_value, - alphabet_symbol_mapping_items, -): - trans_key_to_states = numba.typed.Dict.empty(numba.int64, nb_int_list_type) - for trans_key_and_state in trans_key_to_states_items: - trans_key_to_states.setdefault( - trans_key_and_state[0], numba.typed.List.empty_list(numba.int64) - ).append(trans_key_and_state[1]) - - flat_transition_map = numba.typed.Dict.empty(nb_int_pair_type, numba.int64) - for trans_key_and_state in flat_transition_map_items: - flat_transition_map[ - (trans_key_and_state[0], trans_key_and_state[1]) - ] = trans_key_and_state[2] - - # use 2-char strings so that we can represent incomplete utf-8 sequences - # as 2-hex-digit pairs - alphabet_symbol_map = numba.typed.Dict.empty(nb_unicode_type, numba.int64) - for symbol_and_trans_key in alphabet_symbol_mapping_items: - alphabet_symbol_map[symbol_and_trans_key[0]] = symbol_and_trans_key[1] - - initial = numba.int64(py_initial) - - finals = set() - for final in py_finals: - finals.add(final) - - anything_value = numba.int64(py_anything_value) - - return FSMInfo( - initial, - finals, - flat_transition_map, - trans_key_to_states, - anything_value, - alphabet_symbol_map, - ) - - -FSMInfo = namedtuple( - "FSMInfo", - [ - "initial", - "finals", - "transitions", - "trans_key_to_states", - "alphabet_anything_value", - "alphabet_symbol_mapping", - ], -) - - -TransitionTrie = Dict[TransitionKey, "Union[TransitionTrie, State, None]"] - - -def add_to_transition_trie( - trie: TransitionTrie, - key_seq: Sequence[TransitionKey], - value: Union[State, None], -): - for key in key_seq[:-1]: - trie = cast(TransitionTrie, trie.setdefault(key, {})) - assert isinstance(trie, dict), "key sequence of incompatible length" - trie[key_seq[-1]] = value - - -# merge default_trie into the trie, only updating entries not present in the trie -def transition_trie_setdefault( - trie: TransitionTrie, - default_trie: TransitionTrie, -): - for key, default_value in default_trie.items(): - dest_value = trie.get(key) - if isinstance(dest_value, dict) and isinstance(default_value, dict): - transition_trie_setdefault(dest_value, default_value) - elif key not in trie: - trie[key] = default_value - - -def byte_symbol(byte: int) -> str: - return f"\x00{byte:02X}" if byte >= 0x80 else chr(byte) - - -def make_byte_level_fsm( - fsm: FSM, keep_utf8: bool = False, frozen_tokens: List[str] = [] -) -> FSM: - """Convert an FSM to a byte-level FSM, expanding multi-byte characters as - sequences of single-byte transitions. - - Parameters - ---------- - fsm: (`interegular.FSM`): - The token-level FSM to convert to a byte-level FSM. - keep_utf8: (`bool`, *optional*): - If set to True, the original utf-8 characters are kept as-is. Defaults to - False. NOTE: we're representing bytes as strings to keep it type-compatible. - frozen_tokens: (`List[str]`, *optional*): - A list of tokens that should be kept as-is in the byte-level FSM. That is, - these tokens will not be expanded into byte-level transitions. Defaults to - an empty list. - - Returns - ------- - `interegular.FSM`: A byte-level FSM. - """ - - anything_else_key = fsm.alphabet[anything_else] - symbol_mapping: Dict[Union[str, _AnythingElseCls], TransitionKey] = {} - map: Dict[State, Dict[TransitionKey, State]] = {} - states: List[State] = list(fsm.states) - - # identify all multi-byte characters in the alphabet and build a mapping - # from the original transition keys to sequences of new keys for each byte - key_to_key_seqs: Dict[TransitionKey, Set[Tuple[TransitionKey, ...]]] = {} - all_key_seqs: Set[Tuple[TransitionKey, ...]] = set() - all_bytes: Set[int] = set() - max_key = max(fsm.alphabet.values()) - for symbol, transition_key in fsm.alphabet.items(): - assert symbol == anything_else or symbol in frozen_tokens or len(symbol) == 1 - if symbol == anything_else or symbol in frozen_tokens or ord(symbol) < 0x80: - symbol_mapping[symbol] = transition_key - else: - if keep_utf8: - symbol_mapping[symbol] = transition_key - key_list: List[TransitionKey] = [] - for byte in symbol.encode("utf-8"): - symbol = byte_symbol(byte) - if symbol not in symbol_mapping: - symbol_mapping[symbol] = max_key = TransitionKey(max_key + 1) - all_bytes.add(byte) - key_list.append(symbol_mapping[symbol]) - key_seq = tuple(key_list) - key_to_key_seqs.setdefault(transition_key, set()).add(key_seq) - all_key_seqs.add(key_seq) - - # add all remaining multi-byte utf-8 bytes to the alphabet - # (this is required to represent `anything_else`) - utf8_ranges = { - 1: (0x80, 0xC0), # continuation bytes - 2: (0xC0, 0xE0), # 2-byte sequences - 3: (0xE0, 0xF0), # 3-byte sequences - 4: (0xF0, 0xF8), # 4-byte sequences - } - utf8_all_keys: Dict[int, Set[TransitionKey]] = { - n: set() for n in utf8_ranges.keys() - } - for n, (start, end) in utf8_ranges.items(): - range_key = max_key = TransitionKey(max_key + 1) - for byte in range(start, end): - byte_key = symbol_mapping.setdefault(byte_symbol(byte), range_key) - utf8_all_keys[n].add(byte_key) - - # cache of intermediate transition states by transitions from that state - state_cache: Dict[FrozenSet[Tuple[TransitionKey, State]], State] = {} - - # helper function to create multi-step transitions between states - max_state = max(fsm.states) - - def create_seq_transitions( - seq_transitions_trie: TransitionTrie, - ) -> Dict[TransitionKey, State]: - nonlocal max_state - result: Dict[TransitionKey, State] = {} - - for next_key, next_trie in seq_transitions_trie.items(): - if isinstance(next_trie, dict): - next_transitions = create_seq_transitions(next_trie) - if not next_transitions: - continue - cache_key = frozenset(next_transitions.items()) - next_state = state_cache.get(cache_key) - if next_state is None: - next_state = max_state = State(max_state + 1) - map[next_state] = next_transitions - state_cache[cache_key] = next_state - states.append(next_state) - result[next_key] = next_state - elif next_trie is not None: - result[next_key] = next_trie - - return result - - # create new states and transitions - for state, transitions in fsm.map.items(): - seq_transitions_trie: TransitionTrie = {} - state_map: Dict[TransitionKey, State] = {} - - for transition_key, to_state in transitions.items(): - if transition_key in key_to_key_seqs: - if keep_utf8: - state_map[transition_key] = to_state - for key_seq in key_to_key_seqs[transition_key]: - add_to_transition_trie(seq_transitions_trie, key_seq, to_state) - else: # keep single-byte transitions as is - state_map[transition_key] = to_state - - # handle multi-byte anything_else sequences - if anything_else_key in transitions: - for key_seq in all_key_seqs: - add_to_transition_trie(seq_transitions_trie, key_seq, None) - - anything_else_trie: TransitionTrie = {} - cont_trie: Union[TransitionTrie, State] = transitions[anything_else_key] - for n in range(2, 5): - cont_trie = {key: cont_trie for key in utf8_all_keys[1]} - for key in utf8_all_keys[n]: - anything_else_trie[key] = cont_trie - - transition_trie_setdefault(seq_transitions_trie, anything_else_trie) - - # create new states and transitions - next_transitions = create_seq_transitions(seq_transitions_trie) - state_map.update(next_transitions) - map[state] = state_map - - return FSM( - alphabet=Alphabet(symbol_mapping), - states=states, - initial=fsm.initial, - finals=fsm.finals, - map=map, - ) - - -def make_byte_level_better_fsm(fsm: BetterFSM, keep_utf8=False) -> BetterFSM: - new_fsm = make_byte_level_fsm(fsm, keep_utf8) - return BetterFSM( - alphabet=BetterAlphabet(new_fsm.alphabet._symbol_mapping), - states=new_fsm.states, - initial=new_fsm.initial, - finals=new_fsm.finals, - map=new_fsm.map, - ) - - -def make_deterministic_fsm(fsm: FSM) -> Tuple[BetterFSM, Dict[int, int]]: - """Construct an equivalent FSM with deterministic state labels.""" - old_to_new_trans_keys = { - trans_key: i - for i, (trans_key, _) in enumerate( - sorted(fsm.alphabet.by_transition.items(), key=lambda x: sorted(x[1])) - ) - } - - new_symbol_mapping = { - symbol: old_to_new_trans_keys[trans_key] - for symbol, trans_key in fsm.alphabet._symbol_mapping.items() - } - - new_alphabet = BetterAlphabet(new_symbol_mapping) - - new_map = { - from_state: { - old_to_new_trans_keys[trans_key]: to_state - for trans_key, to_state in trans_map.items() - } - for from_state, trans_map in fsm.map.items() - } - - old_to_new_states = {} - old_to_new_states[fsm.initial] = 0 - - i = 0 - seen = {fsm.initial} - old_state_queue = [fsm.initial] - while old_state_queue: - old_state = old_state_queue.pop(-1) - transitions = new_map[old_state] - sorted_transitions = sorted(transitions.items(), key=lambda v: v[0]) - for _, old_state in sorted_transitions: - if old_state not in seen: - old_state_queue.append(old_state) - seen.add(old_state) - if old_state not in old_to_new_states: - i += 1 - old_to_new_states[old_state] = i - - new_map = dict( - sorted( - ( - ( - old_to_new_states[from_state], - dict( - sorted( - ( - (trans_key, old_to_new_states[to_state]) - for trans_key, to_state in trans_map.items() - ), - key=lambda v: v[0], - ) - ), - ) - for from_state, trans_map in new_map.items() - ), - key=lambda v: v[0], - ) - ) - - new_initial = 0 - new_finals = frozenset( - sorted(old_to_new_states[old_state] for old_state in fsm.finals) - ) - new_states = frozenset(sorted(new_map.keys())) - - new_fsm = BetterFSM(new_alphabet, new_states, new_initial, new_finals, new_map) - - return new_fsm, old_to_new_states - - -@numba.njit(nogil=True, cache=True) -def _walk_fsm( - fsm_transitions: Dict[Tuple[int, int], int], - fsm_initial: int, - fsm_finals: Set[int], - token_transition_keys: Sequence[int], - start_state: int, - full_match: bool = True, -) -> List[int]: - state = start_state - accepted_states: List[int] = numba.typed.List.empty_list(numba.int64) - last_final_idx: int = numba.uint64(0) - - # Iterate over token transition key sequence. The transition key - # sequence represents the FSM traversal rules of the tokens symbols. - for i, trans_key in enumerate(token_transition_keys): - new_state = fsm_transitions.get((state, trans_key)) - - if new_state is None: - if not full_match and last_final_idx > 0: - return accepted_states[:last_final_idx] - - return numba.typed.List.empty_list(numba.int64) - - state = new_state - - if state in fsm_finals: - last_final_idx = numba.uint64(i + 1) - - accepted_states.append(_nonoptional(state)) - - if full_match and last_final_idx - 1 != i: - return numba.typed.List.empty_list(numba.int64) - - return accepted_states - - -def walk_fsm( - fsm: BetterFSM, - token_transition_keys: Sequence[int], - start_state: int, - full_match: bool = True, -) -> List[int]: - fsm_finals = fsm.finals - - state = start_state - accepted_states: List[int] = [] - last_final_idx: int = 0 - - fsm_transitions = fsm.flat_transition_map - - # Iterate over token transition key sequence. The transition key - # sequence represents the FSM traversal rules of the tokens symbols. - for i, trans_key in enumerate(token_transition_keys): - new_state = fsm_transitions.get((state, trans_key)) - - if new_state is None: - if not full_match and last_final_idx > 0: - return accepted_states[:last_final_idx] - - return [] - - state = new_state - - if state in fsm_finals: - last_final_idx = i + 1 - - accepted_states.append(state) - - if full_match and last_final_idx - 1 != i: - return [] - - return accepted_states - - -def fsm_union( - fsms: Sequence[FSM], -) -> Tuple[FSM, Dict[int, Tuple[Set[Tuple[int, int]], Set[int], Dict[int, Set[int]]]]]: - """Construct an FSM representing the union of the FSMs in `fsms`. - - This is an updated version of `interegular.fsm.FSM.union` made to return an - extra map of component FSMs to the sets of state transitions that - correspond to them in the new FSM. - - """ - - alphabet, new_to_old = Alphabet.union(*[fsm.alphabet for fsm in fsms]) - - indexed_fsms = tuple(enumerate(fsms)) - - initial = {i: fsm.initial for (i, fsm) in indexed_fsms} - - # Dedicated function accepting a "superset" and returning the next - # "superset" obtained by following this transition in the new FSM - def follow(current_state, new_transition: int): - next = {} - for i, f in indexed_fsms: - old_transition = new_to_old[i][new_transition] - if ( - i in current_state - and current_state[i] in f.map - and old_transition in f.map[current_state[i]] - ): - next[i] = f.map[current_state[i]][old_transition] - if not next: - raise OblivionError - return next - - states = [initial] - finals: Set[int] = set() - map: Dict[int, Dict[int, int]] = {} - - # Map component FSMs to their new state-to-state transitions, finals, and a - # map translating component FSM states to aggregate FSM states - fsms_to_trans_finals: Dict[ - int, Tuple[Set[Tuple[int, int]], Set[int], Dict[int, Set[int]]] - ] = {} - - i = 0 - while i < len(states): - state = states[i] - - # Add to the finals of the aggregate FSM whenever we hit a final in a - # component FSM - if any(state.get(j, -1) in fsm.finals for (j, fsm) in indexed_fsms): - finals.add(i) - - # Compute the map for this state - map[i] = {} - for transition in alphabet.by_transition: - try: - next = follow(state, transition) - except OblivionError: - # Reached an oblivion state; don't list it - continue - else: - try: - # TODO: Seems like this could--and should--be avoided - j = states.index(next) - except ValueError: - j = len(states) - states.append(next) - - map[i][transition] = j - - for fsm_id, fsm_state in next.items(): - ( - fsm_transitions, - fsm_finals, - fsm_old_to_new, - ) = fsms_to_trans_finals.setdefault(fsm_id, (set(), set(), {})) - old_from = state[fsm_id] - old_to = fsm_state - fsm_old_to_new.setdefault(old_from, set()).add(i) - fsm_old_to_new.setdefault(old_to, set()).add(j) - fsm_transitions.add((i, j)) - if fsm_state in fsms[fsm_id].finals: - fsm_finals.add(j) - - i += 1 - - fsm = FSM( - alphabet=alphabet, - states=range(len(states)), - initial=0, - finals=finals, - map=map, - __no_validation__=True, - ) - - fsm, old_to_new_states = make_deterministic_fsm(fsm) - _fsms_to_trans_finals = { - fsm_id: ( - {(old_to_new_states[s1], old_to_new_states[s2]) for s1, s2 in transitions}, - {old_to_new_states[s] for s in finals}, - { - old_state: {old_to_new_states[new_state] for new_state in new_states} - for old_state, new_states in old_to_new.items() - }, - ) - for fsm_id, (transitions, finals, old_to_new) in sorted( - fsms_to_trans_finals.items(), key=lambda x: x[0] - ) - } - - return ( - fsm, - _fsms_to_trans_finals, - ) - - -def get_sub_fsms_from_seq( - state_seq: Sequence[int], - fsms_to_trans_finals: Dict[ - int, Tuple[Set[Tuple[int, int]], Set[int], Dict[int, Set[int]]] - ], -) -> Generator[Tuple[int, bool, bool], None, None]: - """Get the indices of the sub-FSMs in `fsm` that could have matched the state sequence `state_seq`. - - Parameters - ---------- - state_seq - A state sequence. - fsms_to_trans_finals - A map from FSM indices to tuples containing sets of their state transitions - and sets of the final/accept states. - - Returns - ------- - A generator returning tuples containing each sub-FSM index (in the order - they were union-ed to construct `fsm`) and booleans indicating whether or - not there is another valid transition from the last state in the sequence - for the associated sub-FSM (i.e. if the FSM can continue - accepting/matching) and whether or not the sequence ends in a final state - of the sub-FSM. - """ - state_seq_transitions = set(zip(state_seq[:-1], state_seq[1:])) - last_fsm_state = state_seq[-1] - yield from ( - ( - # The sub-FMS index - fsm_idx, - # Is there another possible transition in this sub-FSM? - any(last_fsm_state == from_s for (from_s, to_s) in transitions), - # Is this sub-FSM in a final state? - state_seq[-1] in finals, - ) - for fsm_idx, (transitions, finals, _) in fsms_to_trans_finals.items() - if state_seq_transitions.issubset(transitions) - ) - - -@numba.njit(cache=True, nogil=True) -def state_scan_tokens( - fsm_transitions: Dict[Tuple[int, int], int], - alphabet_symbol_mapping: Dict[str, int], - alphabet_anything_value: int, - fsm_initial: int, - fsm_finals: Set[int], - vocabulary: List[Tuple[str, Sequence[int]]], - vocabulary_transition_keys: List[Sequence[int]], - start_state: int, -) -> Set[Tuple[int, int]]: - res = set() - - for (token, token_ids), token_transition_keys in zip( - vocabulary, vocabulary_transition_keys - ): - state_seq = _walk_fsm( - fsm_transitions, - fsm_initial, - fsm_finals, - token_transition_keys, - start_state, - False, - ) - - if state_seq is not None and len(state_seq) < len(token_transition_keys): - continue - - for token_id in token_ids: - res.add((token_id, state_seq[-1])) - - return res - - -@numba.njit(cache=True, nogil=True) -def get_token_transition_keys( - alphabet_symbol_mapping: Dict[str, int], - alphabet_anything_value: int, - token_str: str, -) -> Sequence[int]: - """ - Get the sequence of transition keys for an individual string - with respect to an FSMs alphabet symbol mapping - - This requires parsing the null-byte prefix rules of a byte-fsm: - - If two characters are prefixed by \x00, they are the grouped as a hex-byte - - Otherwise they are a standalone utf-8 character - """ - token_transition_keys = [] - i = 0 - while i < len(token_str): - if token_str[i] == "\x00" and i != len(token_str) - 1: - symbol = token_str[i : i + 3] - i += 3 - else: - symbol = token_str[i] - i += 1 - - token_transition_keys.append( - alphabet_symbol_mapping.get(symbol, alphabet_anything_value) - ) - - token_transition_keys_array = np.empty(len(token_transition_keys), dtype=np.int64) - for j in range(len(token_transition_keys)): - token_transition_keys_array[j] = token_transition_keys[j] - return token_transition_keys_array - - -@numba.njit(cache=True, nogil=True) -def get_vocabulary_transition_keys( - alphabet_symbol_mapping: Dict[str, int], - alphabet_anything_value: int, - vocabulary: List[Tuple[str, Sequence[int]]], - frozen_tokens: List[str] = numba.typed.List.empty_list(numba.types.unicode_type), -) -> List[Sequence[int]]: - """ - Calculate the sequence transition keys for each token str within a vocabulary - - Parameters - ---------- - alphabet_symbol_mapping: (`Dict[str, int]`): - A mapping from an alphabet symbol in a FSM to its corresponding transition key. - alphabet_anything_value: (`int`): - The transition key for the anything_else symbol in the FSM. - vocabulary: (`List[Tuple[str, Sequence[int]]]`): - A list of tuples, each containing a token and a list of equivalent token ids. - frozen_tokens: (`List[str]`, *optional*): - A list of tokens that are kept as-is when transforming the FSM. - Defaults to an empty list. - - Returns - ------- - `List[Sequence[int]]`: - A list of token transition keys for each token in the vocabulary. - """ - vocab_transition_keys = numba.typed.List.empty_list(numba.int64[:]) - for token_str, _ in vocabulary: - # Since these tokens are not expanded into byte-level transitions, we can - # simply get their transition keys directly. - if token_str in frozen_tokens: - token_transition_keys = np.array( - [alphabet_symbol_mapping[token_str]], dtype=np.int64 - ) - else: - token_transition_keys = get_token_transition_keys( - alphabet_symbol_mapping, alphabet_anything_value, token_str - ) - vocab_transition_keys.append(token_transition_keys) - - return vocab_transition_keys - - -def create_fsm_index_end_to_end( - fsm_info: FSMInfo, - vocabulary: List[Tuple[str, Sequence[int]]], - frozen_tokens: List[str] = [], -) -> Dict[int, Set[Tuple[int, int]]]: - """Create an FSM state-to-vocabulary map/index through end-to-end token parsing. - - Parameters - ---------- - fsm_info: (`interegular.FSMInfo`): - The FSM information object containing the FSM's alphabet, transitions, initial - and final states, and other relevant information. - vocabulary: (`List[Tuple[str, Sequence[int]]]`): - A list of tuples, each containing a token and a list of equivalent token ids. - frozen_tokens: (`List[str]`, *optional*): - A list of tokens that are kept as-is when transforming the FSM. - - Returns - ------- - `Dict[int, Set[Tuple[int, int]]]`: - A mapping from FSM states to sets of tuples containing token ids and the end - states of the FSM after parsing the token. - """ - - # TODO: Consider using a `List` of `Set`s instead; that way we can JIT this - # code, too. - states_to_token_subsets: Dict[int, Set[Tuple[int, int]]] = {} - seen: Set[int] = set() - next_states = {fsm_info.initial} - - pbar = tqdm( - total=len(set(fsm_info.transitions.values())) - + 1, # all transitions plus initial - desc="Compiling FSM index for all state transitions", - ) - - vocabulary_transition_keys = get_vocabulary_transition_keys( - fsm_info.alphabet_symbol_mapping, - fsm_info.alphabet_anything_value, - vocabulary, - frozen_tokens=( - numba.typed.List(frozen_tokens) - if len(frozen_tokens) > 0 - else numba.typed.List.empty_list(numba.types.unicode_type) - ), - ) - - while next_states: - start_state = next_states.pop() - - token_ids_end_states = state_scan_tokens( - fsm_info.transitions, - fsm_info.alphabet_symbol_mapping, - fsm_info.alphabet_anything_value, - fsm_info.initial, - fsm_info.finals, - vocabulary, - vocabulary_transition_keys, - start_state, - ) - - for token_id_and_end_state in token_ids_end_states: - states_to_token_subsets.setdefault(start_state, set()).add( - token_id_and_end_state - ) - end_state = token_id_and_end_state[1] - if end_state not in seen: - next_states.add(end_state) - - if start_state not in seen: - pbar.update(1) - seen.add(start_state) - - pbar.close() - - return states_to_token_subsets - - -re_llama_byte_token = re.compile(r"^<0x[0-9A-F]{2}>$") - -# The "▁*" prefix is required to handle Gemma and GPT-SW3 tokenizers, and the "\.*" -# suffix is required to handle the NorwAI tokenizer. -re_replacement_seq = re.compile(r"^▁*�+\.*$") - - -# Copied from transformers.models.gpt2.tokenization_gpt2.bytes_to_unicode -@lru_cache() -def gpt2_bytes_to_unicode(): - """ - Returns list of utf-8 byte and a mapping to unicode strings. We specifically avoids mapping to whitespace/control - characters the bpe code barfs on. - - The reversible bpe codes work on unicode strings. This means you need a large # of unicode characters in your vocab - if you want to avoid UNKs. When you're at something like a 10B token dataset you end up needing around 5K for - decent coverage. This is a significant percentage of your normal, say, 32K bpe vocab. To avoid that, we want lookup - tables between utf-8 bytes and unicode strings. - """ - bs = ( - list(range(ord("!"), ord("~") + 1)) - + list(range(ord("¡"), ord("¬") + 1)) - + list(range(ord("®"), ord("ÿ") + 1)) - ) - cs = bs[:] - n = 0 - for b in range(2**8): - if b not in bs: - bs.append(b) - cs.append(2**8 + n) - n += 1 - cs = [chr(n) for n in cs] - return dict(zip(bs, cs)) - - -@lru_cache() -def gpt2_unicode_to_bytes(): - return {v: k for k, v in gpt2_bytes_to_unicode().items()} - - -# TODO: Cannot cache typed collections to disk, yet. See -# https://github.com/numba/numba/issues/4698 -@lru_cache -def reduced_vocabulary( - tokenizer: "Tokenizer", -) -> Tuple[List[Tuple[str, Sequence[int]]], Set[int]]: - """Create a map from decoded vocabulary tokens to lists of equivalent token ids.""" - empty_token_ids = set() - vocabulary: Dict[Union[str, Tuple[str, ...]], List[int]] = {} - for token, token_idx in tokenizer.vocabulary.items(): - if token in tokenizer.special_tokens: - continue - - token_str: Union[str, Tuple[str, ...]] = tokenizer.convert_token_to_string( - token - ) - - if token_str: - if isinstance(token, bytes): - # Handle BPE tokenizers where the tokens are directly stored as bytes - # https://github.com/QwenLM/Qwen/blob/main/tokenization_note.md#regular-tokens - token_str = "".join(byte_symbol(b) for b in token) - - elif "\ufffd" in token_str and not re_replacement_seq.match(token): - # invalid utf-8 sequences are replaced with � (\ufffd), but there - # might also be tokens specifically for �, ��, ���, etc. - - if re_llama_byte_token.match(token): - # llama-like tokenizers have <0xXX> tokens for all - # bytes >= 0x80 and represent all incomplete utf-8 - # sequences using such tokens - token_bytes = [int(token[3:5], 16)] - else: - # gpt2-like tokenizers have multi-byte tokens that can - # have a mix of full and incomplete utf-8 characters, - # for example, b` \xf0` can be one token; these tokenizers - # map each byte to a valid utf-8 character - token_bytes = cast( - List[int], [gpt2_unicode_to_bytes().get(c) for c in token] - ) - if None in token_bytes: - raise RuntimeError( - f"Cannot convert token `{token}` ({token_idx}) to bytes: {token_str}" - ) - token_str = "".join(byte_symbol(b) for b in token_bytes) - - vocabulary.setdefault(token_str, []).append(token_idx) - else: - empty_token_ids.add(numba.int64(token_idx)) - - vocabulary_nb = numba.typed.List.empty_list( - numba.types.Tuple( - ( - nb_unicode_type, - numba.int64[:], - ) - ) - ) - for token_str, token_ids in vocabulary.items(): - token_ids_np = np.fromiter(token_ids, dtype=np.dtype("int64")) - vocabulary_nb.append((token_str, token_ids_np)) - - return vocabulary_nb, empty_token_ids - - -def create_fsm_index_tokenizer( - fsm: BetterFSM, tokenizer: "Tokenizer", frozen_tokens: List[str] = [] -) -> Tuple[Dict[int, Dict[int, int]], Set[int]]: - """Construct an FMS index from a tokenizer. - - This uses the end-to-end approach of `create_fsm_index_end_to_end`. - - Parameters - ---------- - fsm: (`BetterFSM`): - A cache-friendly FSM. Other interegular FSMs can also be used, but caching - may not work as expected. - tokenizer: (`Tokenizer`): - The model's tokenizer. - frozen_tokens: (`List[str]`, *optional*): - A list of tokens that should be kept as-is when expanding the token-level - FSM into a byte-level FSM. Defaults to an empty list. - - Returns - ------- - states_to_token_maps: (`Dict[int, Dict[int, int]]`): - A mapping from states to a mapping from token ids originating from that state - to the next state to transition to given that token. The structure is as follows: - (origin_state -> (token_id -> next_state)) - empty_token_ids: (`Set[int]`): - A set of token ids that correspond to empty strings. - - .. warning:: - - `fsm` needs to be deterministically ordered so that future caching makes sense. - """ - vocabulary, empty_token_ids = reduced_vocabulary(tokenizer) - - states_to_token_subsets = create_fsm_index_end_to_end( - fsm.fsm_info, vocabulary, frozen_tokens - ) - - # Allow transitions to EOS from all terminals FSM states that are - # reachable - # TODO: Do we really need this anymore? - for state in fsm.fsm_info.finals: - subset = states_to_token_subsets.get(state) - if subset is not None: - subset.add((tokenizer.eos_token_id, state)) - - # Convert to token-to-end-state maps - states_to_token_subsets = {k: dict(v) for k, v in states_to_token_subsets.items()} - - return states_to_token_subsets, empty_token_ids diff --git a/pyproject.toml b/pyproject.toml index 937b0c9c7..7229afa83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "outlines" authors= [{name = "Outlines Developers"}] description = "Probabilistic Generative Model Programming" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {text = "Apache-2.0"} keywords=[ "machine learning", @@ -32,7 +32,6 @@ dependencies = [ "cloudpickle", "diskcache", "pydantic>=2.0", - "numba", "referencing", "jsonschema", "requests", @@ -41,6 +40,7 @@ dependencies = [ "typing_extensions", "pycountry", "airportsdata", + "outlines_core", ] dynamic = ["version"] @@ -94,7 +94,6 @@ write_to = "outlines/_version.py" testpaths = ["tests"] filterwarnings = [ "error", - "ignore::numba.core.errors.NumbaPendingDeprecationWarning", "ignore::pydantic.warnings.PydanticDeprecatedSince20", "ignore::FutureWarning:transformers.*", "ignore::FutureWarning:huggingface_hub.*", @@ -129,7 +128,6 @@ module = [ "lark.*", "interegular.*", "datasets.*", - "numba.*", "requests.*", "responses.*", "vllm.*", @@ -137,6 +135,7 @@ module = [ "fastapi.*", "pycountry.*", "airportsdata.*", + "outlines_core.*", ] ignore_missing_imports = true diff --git a/tests/fsm/test_fsm.py b/tests/fsm/test_fsm.py deleted file mode 100644 index 94166fd95..000000000 --- a/tests/fsm/test_fsm.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytest - -from outlines.fsm.fsm import RegexFSM, StopAtEosFSM - - -def assert_expected_tensor_ids(tensor, ids): - assert len(tensor) == len(ids) - norm_tensor = sorted(map(int, tensor)) - norm_ids = sorted(map(int, tensor)) - assert norm_tensor == norm_ids, (norm_tensor, norm_ids) - - -def test_stop_at_eos(): - class MockTokenizer: - vocabulary = {"a": 1, "eos": 2} - eos_token_id = 2 - - with pytest.warns(UserWarning): - fsm = StopAtEosFSM(MockTokenizer()) - - assert fsm.allowed_token_ids(fsm.start_state) is None - assert fsm.allowed_token_ids(fsm.final_state) == [2] - assert fsm.next_state(fsm.start_state, 2) == fsm.final_state - assert fsm.next_state(fsm.start_state, 1) == fsm.start_state - assert fsm.is_final_state(fsm.start_state) is False - assert fsm.is_final_state(fsm.final_state) is True - - -def test_regex_vocabulary_error(): - class MockTokenizer: - vocabulary = {"a": 1} - special_tokens = {"eos"} - eos_token_id = 3 - - def convert_token_to_string(self, token): - return token - - regex_str = "[1-9]" - - with pytest.raises(ValueError, match="The vocabulary"): - RegexFSM(regex_str, MockTokenizer()) - - -def test_regex(): - class MockTokenizer: - vocabulary = {"1": 1, "a": 2, "eos": 3} - special_tokens = {"eos"} - eos_token_id = 3 - - def convert_token_to_string(self, token): - return token - - regex_str = "[1-9]" - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = RegexFSM(regex_str, tokenizer) - - assert fsm.states_to_token_maps == {0: {1: 1}} - assert_expected_tensor_ids(fsm.allowed_token_ids(state=0), [1]) - assert fsm.next_state(state=0, token_id=1) == 1 - assert fsm.next_state(state=0, token_id=tokenizer.eos_token_id) == -1 - - assert fsm.is_final_state(0) is False - - for state in fsm.final_states: - assert fsm.is_final_state(state) is True - - -def test_regex_final_state(): - """Make sure that the FSM stays in the final state as we keep generating""" - - class MockTokenizer: - vocabulary = {"`": 101, ".": 102, "\n": 103, "eos": 104} - special_tokens = {"eos"} - eos_token_id = 104 - - def convert_token_to_string(self, token): - return token - - regex_str = r"`\n(\.\n)?`\n" - tokenizer = MockTokenizer() - - with pytest.warns(UserWarning): - fsm = RegexFSM(regex_str, tokenizer) - - state = fsm.next_state(state=4, token_id=103) - assert state == 5 - assert fsm.is_final_state(state) - - state = fsm.next_state(state=5, token_id=103) - assert fsm.is_final_state(state) diff --git a/tests/fsm/test_regex.py b/tests/fsm/test_regex.py deleted file mode 100644 index 1789c4a7c..000000000 --- a/tests/fsm/test_regex.py +++ /dev/null @@ -1,742 +0,0 @@ -import interegular -import numba -import numpy as np -import pytest -from transformers import AutoTokenizer - -from outlines.fsm.regex import ( - _walk_fsm, - create_fsm_index_end_to_end, - create_fsm_index_tokenizer, - fsm_union, - get_sub_fsms_from_seq, - get_token_transition_keys, - get_vocabulary_transition_keys, - make_byte_level_better_fsm, - make_byte_level_fsm, - make_deterministic_fsm, - reduced_vocabulary, - walk_fsm, -) -from outlines.models.transformers import TransformerTokenizer - - -def identity(s): - return s - - -def to_bytes(s): - return [chr(b) if b < 0x80 else f"\x00{b:02X}" for b in s.encode("utf-8")] - - -def merge_symbols(byte_hexs): - return "".join(["\x00" + b if len(b) == 2 else b for b in byte_hexs]) - - -def token_str_to_trans_key(fsm, input_string): - return get_token_transition_keys( - fsm.fsm_info.alphabet_symbol_mapping, - fsm.fsm_info.alphabet_anything_value, - input_string, - ) - - -def walk_fsm_from_token_str( - fsm, - input_string: str, - start_state: int, - full_match: bool = True, -): - return walk_fsm( - fsm, - token_str_to_trans_key(fsm, input_string), - start_state, - full_match, - ) - - -def walk_fsm_from_token_str_numba( - fsm, - input_string: str, - start_state: int, - full_match: bool = True, -): - return _walk_fsm( - fsm.fsm_info.transitions, - fsm.fsm_info.initial, - fsm.fsm_info.finals, - token_str_to_trans_key(fsm, input_string), - start_state, - full_match=full_match, - ) - - -@pytest.mark.parametrize( - "function", - [ - walk_fsm_from_token_str, - walk_fsm_from_token_str_numba, - ], -) -def test_walk_fsm(function): - regex_pattern = interegular.parse_pattern("0|[1-9][2-9]*") - regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm().reduce()) - - res = tuple(function(regex_fsm, "0", regex_fsm.initial, full_match=True)) - assert res == (1,) - - res = tuple(function(regex_fsm, "00", regex_fsm.initial, full_match=False)) - assert res == (1,) - - res = tuple(function(regex_fsm, "!", regex_fsm.initial, full_match=True)) - assert res == tuple() - - res = tuple(function(regex_fsm, "00", regex_fsm.initial, full_match=True)) - assert res == tuple() - - # This should fail, because state `1` reads nothing - res = tuple(function(regex_fsm, "0", 1, full_match=True)) - assert res == tuple() - - regex_pattern = interegular.parse_pattern("0|[1-9][2-9]+") - regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm().reduce()) - - res = tuple(function(regex_fsm, "1", regex_fsm.initial, full_match=True)) - assert res == tuple() - - res = tuple(function(regex_fsm, "1", regex_fsm.initial, full_match=False)) - assert res == (2,) - - res = tuple(function(regex_fsm, "12", regex_fsm.initial, full_match=True)) - assert res == (2, 3) - - pattern = interegular.parse_pattern(r"(?:[^\W\d]\w*|[\t \x0c]+)") - fsm, _ = make_deterministic_fsm(pattern.to_fsm().reduce()) - - res = tuple(function(fsm, "x ", fsm.initial, full_match=False)) - assert res == (2,) - - start_state = list(fsm.finals)[0] - res = tuple(function(fsm, "!", start_state, full_match=False)) - assert res == tuple() - - -@pytest.mark.parametrize( - "function", - [ - walk_fsm_from_token_str, - walk_fsm_from_token_str_numba, - ], -) -@pytest.mark.parametrize( - "transform", - [ - identity, - to_bytes, - ], -) -def test_walk_fsm_multi_bytes(function, transform): - regex_pattern = interegular.parse_pattern("😂|[😇-😍][😈-😍]*") - str_regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm().reduce()) - regex_fsm = make_byte_level_better_fsm(str_regex_fsm, keep_utf8=True) - - res = tuple( - function( - regex_fsm, merge_symbols(transform("😂")), regex_fsm.initial, full_match=True - ) - ) - assert res[-1:] == (1,) - - res = tuple( - function( - regex_fsm, - merge_symbols(transform("😂😂")), - regex_fsm.initial, - full_match=False, - ) - ) - assert res[-1:] == (1,) - - res = tuple( - function( - regex_fsm, merge_symbols(transform("!")), regex_fsm.initial, full_match=True - ) - ) - assert res == tuple() - - res = tuple( - function( - regex_fsm, - merge_symbols(transform("😂😂")), - regex_fsm.initial, - full_match=True, - ) - ) - assert res == tuple() - - -def test_get_sub_fsms_from_seq(): - name_pattern = interegular.parse_pattern(r"[^\W\d]\w*") - name_fsm, _ = make_deterministic_fsm(name_pattern.to_fsm().reduce()) - - def_pattern = interegular.parse_pattern("def") - def_fsm, _ = make_deterministic_fsm(def_pattern.to_fsm().reduce()) - - match_pattern = interegular.parse_pattern("match") - match_fsm, _ = make_deterministic_fsm(match_pattern.to_fsm().reduce()) - - peq_pattern = interegular.parse_pattern(r"\+=") - peq_fsm, _ = make_deterministic_fsm(peq_pattern.to_fsm().reduce()) - - plus_pattern = interegular.parse_pattern(r"\+") - plus_fsm, _ = make_deterministic_fsm(plus_pattern.to_fsm().reduce()) - - fsms = [def_fsm, match_fsm, name_fsm, peq_fsm, plus_fsm] - - fsm, fsms_to_trans_finals = fsm_union(fsms) - - assert fsms_to_trans_finals == { - 0: ({(0, 3), (3, 9), (9, 10)}, {10}, {0: {0}, 1: {3}, 2: {9}, 3: {10}}), - 1: ( - {(0, 4), (4, 5), (5, 6), (6, 7), (7, 8)}, - {8}, - {0: {0}, 1: {4}, 2: {5}, 3: {6}, 4: {7}, 5: {8}}, - ), - 2: ( - { - (0, 2), - (0, 3), - (0, 4), - (2, 2), - (3, 2), - (3, 9), - (4, 2), - (4, 5), - (5, 2), - (5, 6), - (6, 2), - (6, 7), - (7, 2), - (7, 8), - (8, 2), - (9, 2), - (9, 10), - (10, 2), - }, - {2, 3, 4, 5, 6, 7, 8, 9, 10}, - {0: {0}, 1: {2, 3, 4, 5, 6, 7, 8, 9, 10}}, - ), - 3: ({(0, 1), (1, 11)}, {11}, {0: {0}, 1: {1}, 2: {11}}), - 4: ({(0, 1)}, {1}, {0: {0}, 1: {1}}), - } - - assert not fsm.accepts("1a") - assert fsm.accepts("a1") - assert fsm.accepts("def") - assert fsm.accepts("match") - assert fsm.accepts("+=") - assert fsm.accepts("+") - - state_seq = walk_fsm_from_token_str(fsm, "def", fsm.initial) - state_seq.insert(0, fsm.fsm_info.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(0, False, True), (2, True, True)] - - # Make sure the old-to-new state map is correct - def_state_seq = walk_fsm_from_token_str(def_fsm, "def", fsm.initial) - def_state_seq.insert(0, fsm.fsm_info.initial) - - def_old_to_new_states = fsms_to_trans_finals[0][2] - assert all( - new_state in def_old_to_new_states[old_state] - for old_state, new_state in zip(def_state_seq, state_seq) - ) - - state_seq = walk_fsm_from_token_str(fsm, "ef", fsm.initial) - state_seq.insert(0, fsm.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(2, True, True)] - - name_state_seq = walk_fsm_from_token_str(name_fsm, "ef", fsm.initial) - name_state_seq.insert(0, fsm.initial) - - name_old_to_new_states = fsms_to_trans_finals[2][2] - assert all( - new_state in name_old_to_new_states[old_state] - for old_state, new_state in zip(name_state_seq, state_seq) - ) - - state_seq = walk_fsm_from_token_str(fsm, "match", fsm.initial) - state_seq.insert(0, fsm.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(1, False, True), (2, True, True)] - - match_state_seq = walk_fsm_from_token_str(match_fsm, "match", fsm.initial) - match_state_seq.insert(0, fsm.initial) - - match_old_to_new_states = fsms_to_trans_finals[1][2] - assert all( - new_state in match_old_to_new_states[old_state] - for old_state, new_state in zip(match_state_seq, state_seq) - ) - - state_seq = walk_fsm_from_token_str(fsm, "defa", fsm.initial) - state_seq.insert(0, fsm.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(2, True, True)] - - state_seq = walk_fsm_from_token_str(fsm, "de", fsm.initial) - state_seq.insert(0, fsm.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(0, True, False), (2, True, True)] - - state_seq = walk_fsm_from_token_str(fsm, "+", fsm.initial, False) - state_seq.insert(0, fsm.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(3, True, False), (4, False, True)] - - state_seq = walk_fsm_from_token_str(fsm, "+=", fsm.initial) - state_seq.insert(0, fsm.initial) - - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(3, False, True)] - - # Test some overlapping patterns - join_fsms = [ - interegular.parse_pattern(r"JOIN").to_fsm().reduce(), - interegular.parse_pattern(r"JOIN LEFT").to_fsm().reduce(), - ] - fsm, fsms_to_trans_finals = fsm_union(join_fsms) - - # Matching "OI" - state_seq = [1, 2, 3] - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(0, True, False), (1, True, False)] - - # Matching "N" - state_seq = [3, 4] - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(0, False, True), (1, True, False)] - - # Matching " " - state_seq = [4, 5] - res = list(get_sub_fsms_from_seq(state_seq, fsms_to_trans_finals)) - assert res == [(1, True, False)] - - -def test_create_fsm_index_end_to_end(): - regex_str = "0|[1-9][0-9]*" - - regex_pattern = interegular.parse_pattern(regex_str) - regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm().reduce()) - - vocabulary = { - "blah": numba.typed.List([0]), - "1a": numba.typed.List([1]), - "2": numba.typed.List([2]), - "0": numba.typed.List([3]), - "": numba.typed.List([4]), - } - - vocabulary_nb = numba.typed.List.empty_list( - numba.types.Tuple( - ( - numba.types.unicode_type, - numba.int64[:], - ) - ) - ) - for token_tuple, token_ids in vocabulary.items(): - token = merge_symbols(token_tuple) - token_ids_np = np.fromiter(token_ids, dtype=np.dtype("int64")) - vocabulary_nb.append((token, token_ids_np)) - - res = create_fsm_index_end_to_end(regex_fsm.fsm_info, vocabulary_nb) - - assert res == {0: {(2, 2), (3, 1)}, 2: {(2, 2), (3, 2)}} - - -def test_create_fsm_index_end_to_end_multi_byte(): - regex_str = "😇| [😈-😍][😇-😎]*" - - regex_pattern = interegular.parse_pattern(regex_str) - regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm().reduce()) - byte_fsm = make_byte_level_better_fsm(regex_fsm, keep_utf8=True) - - vocabulary = { - "blah": numba.typed.List([0]), - "😈a": numba.typed.List([1]), - "😇": numba.typed.List([2]), - "😍": numba.typed.List([3]), - merge_symbols(("F0", "9F", "98", "8D")): numba.typed.List([4]), # '😍' - " 😍": numba.typed.List([5]), - merge_symbols((" ", "F0", "9F", "98", "8D")): numba.typed.List([6]), # ' 😍' - merge_symbols((" ", "F0", "9F", "98")): numba.typed.List( - [7] - ), # ' 😍' incomplete - "": numba.typed.List([8]), - } - - vocabulary_nb = numba.typed.List.empty_list( - numba.types.Tuple( - ( - numba.types.unicode_type, - numba.int64[:], - ) - ) - ) - for token_tuple, token_ids in vocabulary.items(): - token_tuple_np = merge_symbols(token_tuple) - token_ids_np = np.fromiter(token_ids, dtype=np.dtype("int64")) - vocabulary_nb.append((token_tuple_np, token_ids_np)) - - res = create_fsm_index_end_to_end(byte_fsm.fsm_info, vocabulary_nb) - - assert res == {0: {(5, 3), (6, 3), (7, 7), (2, 2)}, 3: {(2, 3), (3, 3), (4, 3)}} - - -@pytest.mark.parametrize( - "hf_tokenizer_uri, revision", - [ - ("openai-community/gpt2", "607a30d783dfa663caf39e06633721c8d4cfcd7e"), - ("microsoft/phi-2", "ef382358ec9e382308935a992d908de099b64c23"), - ("Qwen/Qwen1.5-0.5B-Chat", "4d14e384a4b037942bb3f3016665157c8bcb70ea"), - ( - "NousResearch/Hermes-2-Pro-Llama-3-8B", - "783fd50eb82d7f57758de033861f54d62dde234f", - ), - ], -) -def test_create_fsm_index_tokenizer(hf_tokenizer_uri, revision): - # The combined regular expressions of a lexer state in a Python grammar - regex_str = "(?:(?:[0-9](?:(?:_)?[0-9])*(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*|(?:[0-9](?:(?:_)?[0-9])*\\.(?:[0-9](?:(?:_)?[0-9])*)?|\\.[0-9](?:(?:_)?[0-9])*)(?:(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*)?)|[0-9](?:(?:_)?[0-9])*)(?:J|j)|(?:[0-9](?:(?:_)?[0-9])*(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*|(?:[0-9](?:(?:_)?[0-9])*\\.(?:[0-9](?:(?:_)?[0-9])*)?|\\.[0-9](?:(?:_)?[0-9])*)(?:(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*)?)|0(?:x|X)(?:(?:_)?(?:[0-9]|[a-f]|[A-F]))+|0(?:b|B)(?:(?:_)?[0-1])+|0(?:o|O)(?:(?:_)?[0-7])+|(?:(?i:([ubf]?r?|r[ubf])('([^\\\\']|.)*?'))|(?i:([ubf]?r?|r[ubf])(\"([^\\\"]|.)*?\")))|(?:(?:\r?\n[\t ]*|#[^\n]*))+|[1-9](?:(?:_)?[0-9])*|\\\\[\t \x0c]*\r?\n|continue|nonlocal|assert|global|import|lambda|return|async|await|break|class|False|match|raise|while|yield|case|from|None|pass|True|with|def|del|for|not|try|if|[^\\W\\d]\\w*|#[^\n]*|[\t \x0c]+|\\.\\.\\.|@|\\{|\\(|\\[|\\-|\\+|\\*|\\~" - - regex_pattern = interegular.parse_pattern(regex_str) - # Not reduced, so that there are many states - regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm()) - bytes_fsm = make_byte_level_better_fsm(regex_fsm, keep_utf8=True) - - num_fsm_states = len(regex_fsm.states) - assert num_fsm_states == 220 - - num_bytes_fsm_states = len(bytes_fsm.states) - assert num_bytes_fsm_states == 235 - - tokenizer = AutoTokenizer.from_pretrained(hf_tokenizer_uri, revision=revision) - tokenizer = TransformerTokenizer(tokenizer) - - states_to_token_subsets, empty_token_ids = create_fsm_index_tokenizer( - bytes_fsm, tokenizer - ) - - assert not empty_token_ids - assert len(states_to_token_subsets) / num_fsm_states > 0.94 - - -@pytest.mark.parametrize( - "regex,string,should_accept", - [ - ("[a-c]+", "😀", False), - ("[^a-c]+", "😀", True), - ("😀+", "😀😀😀", True), - ("😀+", "a", False), - ("[😀-😍]{2}", "😈😈", True), - ("[😀-😍]{2}", "aa", False), - ("[^😀-😍]{2}", "aa", True), - ("[^😀-😍]{2}", "😈😈", False), - ("[^😀-😍]{2}", "😎😎", True), - ("[^😀-😍]{2}", "😎😓", True), - ("[^😀-😍]{2}", "😎😈", False), - ("[😀-🙌]{2}", "😎😈", True), - ("[^😀-🙌]{2}", "😎😈", False), - ("[^😀-🙌]{2}", "🙏🙏", True), - ("[^😀-🙌]{2}", "🙏😎", False), - ], -) -def test_make_byte_level_fsm(regex, string, should_accept): - str_fsm = interegular.parse_pattern(regex).to_fsm() - str_accepts = str_fsm.accepts(string) - assert str_accepts == should_accept - - byte_fsm = make_byte_level_fsm(str_fsm) - byte_accepts = byte_fsm.accepts(to_bytes(string)) # type: ignore - assert byte_accepts == str_accepts - - mix_fsm = make_byte_level_fsm(str_fsm, keep_utf8=True) - mix_accepts = mix_fsm.accepts(to_bytes(string)) # type: ignore - assert mix_accepts == str_accepts - - mix_accepts_utf8 = mix_fsm.accepts(string) # type: ignore - assert mix_accepts_utf8 == str_accepts - - def advance(fsm, state, seq): - for symbol in seq: - if state is None: - return None - key = fsm.alphabet[symbol] - state = fsm.map[state].get(key) - return state - - # verify each state along the pattern - str_state = str_fsm.initial - byte_state = byte_fsm.initial - mix_state = byte_fsm.initial - for symbol in string: - str_state = advance(str_fsm, str_state, symbol) - byte_state = advance(byte_fsm, byte_state, to_bytes(symbol)) - mix_state_utf8 = advance(mix_fsm, mix_state, symbol) - mix_state = advance(mix_fsm, mix_state, to_bytes(symbol)) - assert byte_state == str_state - assert mix_state == str_state - assert mix_state_utf8 == str_state - - -@pytest.mark.skip(reason="Only for local profiling") -def test_regex_index_performance(): - from line_profiler import LineProfiler # type: ignore [import] - - regex_str = "(?:(?:[0-9](?:(?:_)?[0-9])*(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*|(?:[0-9](?:(?:_)?[0-9])*\\.(?:[0-9](?:(?:_)?[0-9])*)?|\\.[0-9](?:(?:_)?[0-9])*)(?:(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*)?)|[0-9](?:(?:_)?[0-9])*)(?:J|j)|(?:[0-9](?:(?:_)?[0-9])*(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*|(?:[0-9](?:(?:_)?[0-9])*\\.(?:[0-9](?:(?:_)?[0-9])*)?|\\.[0-9](?:(?:_)?[0-9])*)(?:(?:e|E)(?:(?:\\+|\\-))?[0-9](?:(?:_)?[0-9])*)?)|0(?:x|X)(?:(?:_)?(?:[0-9]|[a-f]|[A-F]))+|0(?:b|B)(?:(?:_)?[0-1])+|0(?:o|O)(?:(?:_)?[0-7])+|(?:(?i:([ubf]?r?|r[ubf])('([^\\\\']|.)*?'))|(?i:([ubf]?r?|r[ubf])(\"([^\\\"]|.)*?\")))|(?:(?:\r?\n[\t ]*|#[^\n]*))+|[1-9](?:(?:_)?[0-9])*|\\\\[\t \x0c]*\r?\n|continue|nonlocal|assert|global|import|lambda|return|async|await|break|class|False|match|raise|while|yield|case|from|None|pass|True|with|def|del|for|not|try|if|[^\\W\\d]\\w*|#[^\n]*|[\t \x0c]+|\\.\\.\\.|@|\\{|\\(|\\[|\\-|\\+|\\*|\\~" - - regex_pattern = interegular.parse_pattern(regex_str) - # Not reduced, so that there are many states - regex_fsm, _ = make_deterministic_fsm(regex_pattern.to_fsm()) - - num_fsm_states = len(regex_fsm.states) - assert num_fsm_states == 220 - - tokenizer = AutoTokenizer.from_pretrained("gpt2") - tokenizer = TransformerTokenizer(tokenizer) - - # Pre-compile Numba functions - res, _ = create_fsm_index_tokenizer(regex_fsm, tokenizer) - assert len(res) > 1 - - profiler = LineProfiler(create_fsm_index_end_to_end) - - profiler.runctx( - "create_fsm_index_tokenizer(regex_fsm, tokenizer)", - globals(), - locals(), - ) - profiler.dump_stats("line-profiler-create_fsm_index.pkl") - profiler.print_stats(output_unit=1e-3, summarize=True, stripzeros=True) - - -@pytest.mark.skip(reason="Only for local profiling") -def test_json_index_performance(): - import json - from enum import Enum - - from line_profiler import LineProfiler # type: ignore [import] - from pydantic import BaseModel, constr - - import outlines - - class Weapon(str, Enum): - sword = "sword" - axe = "axe" - mace = "mace" - spear = "spear" - bow = "bow" - crossbow = "crossbow" - - class Armor(str, Enum): - leather = "leather" - chainmail = "chainmail" - plate = "plate" - - class Character(BaseModel): - name: constr(max_length=10) - # TODO: Add support for conint - age: int # conint(int, ge=18, le=100) - armor: Armor - weapon: Weapon - # TODO: Add support for conint - strength: int # conint(int, ge=0, le=100) - - model = outlines.models.transformers("gpt2", device="cuda") - json_schema = json.dumps(Character.model_json_schema()) - - def build_regex(): - regex_str = outlines.index.json_schema.build_regex_from_object(json_schema) - outlines.generate.regex(model, regex_str) - - profiler = LineProfiler(create_fsm_index_end_to_end) - profiler.add_function(create_fsm_index_tokenizer) - profiler.add_function(outlines.index.index.RegexFSM.__init__) - - profiler.runctx( - "build_regex()", - globals(), - locals(), - ) - profiler.dump_stats("line-profiler-build-json-regex.pkl") - profiler.print_stats(output_unit=1e-3, summarize=True, stripzeros=True) - - -def test_token_trans_keys_identical(): - """assert two tokens w/ identical behavior wrt FSM have same trans key seq""" - - class MockTokenizer: - vocabulary = {"a": 1, "b": 2, "z": 3, "eos": 4} - special_tokens = {"eos"} - eos_token_id = 4 - - def convert_token_to_string(self, token): - return token - - tokenizer = MockTokenizer() - - pattern = r"z[ab]z" - regex_pattern = interegular.parse_pattern(pattern) - interegular_fsm = regex_pattern.to_fsm().reduce() - regex_fsm, _ = make_deterministic_fsm(interegular_fsm) - vocabulary, _ = reduced_vocabulary(tokenizer) - token_trans_keys = get_vocabulary_transition_keys( - regex_fsm.fsm_info.alphabet_symbol_mapping, - regex_fsm.fsm_info.alphabet_anything_value, - vocabulary, - numba.typed.List.empty_list(numba.types.unicode_type), - ) - - token_str_to_tranition_keys = { - token_str: trans_key_seq - for (token_str, _), trans_key_seq in zip(vocabulary, token_trans_keys) - } - # `a` and `b` both are workable, but `z` has distinct transition rules - assert interegular_fsm.accepts("zaz") - assert interegular_fsm.accepts("zbz") - assert (token_str_to_tranition_keys["a"] == token_str_to_tranition_keys["b"]).all() - assert not ( - token_str_to_tranition_keys["a"] == token_str_to_tranition_keys["z"] - ).all() - - -def test_token_trans_keys_walk_fsm(): - """assert _walk_fsm works using transition keys""" - - class MockTokenizer: - vocabulary = {"ab": 1, "ac": 2, "az": 3, "eos": 4} - special_tokens = {"eos"} - eos_token_id = 4 - - def convert_token_to_string(self, token): - return token - - tokenizer = MockTokenizer() - - pattern = r"a[bc]z" - regex_pattern = interegular.parse_pattern(pattern) - interegular_fsm = regex_pattern.to_fsm().reduce() - regex_fsm, _ = make_deterministic_fsm(interegular_fsm) - vocabulary, _ = reduced_vocabulary(tokenizer) - token_trans_keys = get_vocabulary_transition_keys( - regex_fsm.fsm_info.alphabet_symbol_mapping, - regex_fsm.fsm_info.alphabet_anything_value, - vocabulary, - numba.typed.List.empty_list(numba.types.unicode_type), - ) - - token_str_trans_key_seq = { - token_str: trans_key_seq - for (token_str, _), trans_key_seq in zip(vocabulary, token_trans_keys) - } - - # verify initial state valid only for "ab" and "ac" using transition key seq - token_acceptance = {"ab": True, "ac": True, "az": False} - for token, should_accept in token_acceptance.items(): - token_trans_key_seq = token_str_trans_key_seq[token] - state_seq = _walk_fsm( - regex_fsm.fsm_info.transitions, - regex_fsm.fsm_info.initial, - regex_fsm.fsm_info.finals, - token_trans_key_seq, - regex_fsm.fsm_info.initial, - False, - ) - is_accepted = len(state_seq) >= len(token_trans_key_seq) - assert should_accept == is_accepted - - -def test_numba_leading_null_byte_UnicodeCharSeq_remains_broken(): - """Assert numba UnicodeCharSeq w/ leading \x00 is still broken""" - # EXPLANATION: - # https://github.com/dottxt-ai/outlines/pull/930#issuecomment-2143535968 - - # from https://github.com/numba/numba/issues/9542 - d = numba.typed.typeddict.Dict.empty(numba.types.UnicodeCharSeq(1), numba.int64) - d["一"] = 10 # \xe4\xb8\x80 - with pytest.raises(KeyError): - str(d) - - # most characters are fine, but "\x00" is converted to "" - l = np.fromiter(["\x99", "\x00"], dtype=np.dtype("U2")) - assert str(l[0]) == "\x99" # fine - assert str(l[1]) == "" # 1-byte null converted to 0-bytes - - -@pytest.mark.parametrize("input_key", ["一", "\x00"]) -def test_numba_leading_null_byte_unicode_type_sane(input_key): - """Assert numba unicode_type w/ leading \x00 is working""" - # EXPLANATION: - # https://github.com/dottxt-ai/outlines/pull/930#issuecomment-2143535968 - - # from https://github.com/numba/numba/issues/9542 - d = numba.typed.typeddict.Dict.empty(numba.types.unicode_type, numba.int64) - d["一"] = 10 # \xe4\xb8\x80 - str(d) # assert successfully interprets - - -@pytest.mark.parametrize( - "rare_token", - [ - "�", - "��", - "�.", - "�..", - "▁�", - "▁▁�", - "▁�.", - "▁�.", - "▁▁�..", - ], -) -def test_reduced_vocabulary_with_rare_tokens(rare_token): - """Assert reduced_vocabulary works with rare tokens. - - See [1] and [2] for context. - - [1]: https://github.com/dottxt-ai/outlines/pull/763 - [2]: https://github.com/dottxt-ai/outlines/pull/948 - [3]: https://github.com/dottxt-ai/outlines/pull/1153 - """ - tokenizer = AutoTokenizer.from_pretrained("openai-community/gpt2") - tokenizer = TransformerTokenizer(tokenizer=tokenizer) - tokenizer.vocabulary[rare_token] = max(tokenizer.vocabulary.values()) + 1 - reduced_vocabulary(tokenizer) - - -def test_reduced_vocabulary_with_byte_tokens(): - class MockTokenizer: - vocabulary = { - "string": 1, - b"\xa1": 2, # Qwen-Style - "eos": 3, - } - special_tokens = {"eos"} - eos_token_id = 3 - - def convert_token_to_string(self, token): - return b"\xef\xbf\xbd".decode() - - reduced_vocab = reduced_vocabulary(MockTokenizer()) - - # See fsm.regex.get_token_transition_keys() - # FSM transition keys represents bytes as - assert reduced_vocab[0][1][0] == "\x00A1" diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index cdd57e0c6..1d26a9ee4 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -5,11 +5,11 @@ import pytest import torch +from outlines_core.fsm.regex import reduced_vocabulary from pydantic import BaseModel, constr import outlines.generate as generate import outlines.models as models -from outlines.fsm.regex import reduced_vocabulary from outlines.models.transformers import Transformers, TransformerTokenizer from outlines.samplers import beam_search, greedy, multinomial From c5be3b84ad83b1e76e8c7510a02dc24323a5f4b3 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Tue, 1 Oct 2024 17:13:13 -0500 Subject: [PATCH 081/505] Pin outlines-core version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7229afa83..ab3ecd775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "typing_extensions", "pycountry", "airportsdata", - "outlines_core", + "outlines_core==0.1.0", ] dynamic = ["version"] From 4bf14705d62b87dd655159b26ba1dd9e124f8478 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 3 Oct 2024 11:04:55 -0700 Subject: [PATCH 082/505] add missing comma in llamacpp docs --- docs/reference/models/llamacpp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/models/llamacpp.md b/docs/reference/models/llamacpp.md index a84f0c187..0ab3dd83b 100644 --- a/docs/reference/models/llamacpp.md +++ b/docs/reference/models/llamacpp.md @@ -67,7 +67,7 @@ from outlines import models model = models.llamacpp( "TheBloke/phi-2-GGUF", - "phi-2.Q4_K_M.gguf" + "phi-2.Q4_K_M.gguf", n_gpu_layers=-1, # to use GPU acceleration ) ``` From 80b82f16ffd3a6eb15468c8891eef73e807bb098 Mon Sep 17 00:00:00 2001 From: isamu-isozaki Date: Fri, 28 Jun 2024 22:59:05 -0400 Subject: [PATCH 083/505] Exllamav2_filter Fix comment Fixed precommit issues Removed text Basic draft done Passed local test Fixed tests+precommit Revert change for pyairports Fixed precommit Wrap up Remove | for union Attempt changing to List Fixed for 3.8 Adding exllamav2 to optional dependency Fixed model Changed to fork Fix format Changed order Skip exllamav2 tests Attempt fixing coverage Attempt fix coverage Remove flash-attn requirement Fixed fixture tests Removed lora Passed coverage Added back transformers install Fixed per review Made coverage 100% --- outlines/generate/fsm.py | 13 +- outlines/generate/regex.py | 18 +- outlines/generate/text.py | 11 +- outlines/models/exllamav2.py | 410 ++++++++++++------- pyproject.toml | 1 + tests/generate/conftest.py | 8 +- tests/generate/test_generate.py | 10 + tests/generate/test_integration_exllamav2.py | 363 ++++++++++++++++ 8 files changed, 641 insertions(+), 193 deletions(-) create mode 100644 tests/generate/test_integration_exllamav2.py diff --git a/outlines/generate/fsm.py b/outlines/generate/fsm.py index a9338836a..1950812d2 100644 --- a/outlines/generate/fsm.py +++ b/outlines/generate/fsm.py @@ -4,11 +4,10 @@ from outlines.fsm.guide import RegexGuide from outlines.generate.api import ( - SequenceGenerator, SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import ExLlamaV2Model, TransformersVision +from outlines.models import TransformersVision from outlines.samplers import Sampler, multinomial @@ -30,13 +29,3 @@ def fsm_vision(model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() guide = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) logits_processor = GuideLogitsProcessor(tokenizer=model.tokenizer, guide=guide) return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) - - -@fsm.register(ExLlamaV2Model) -def fsm_exllamav2( - model, fsm: interegular.fsm.FSM, sampler: Sampler = multinomial() -) -> SequenceGenerator: - fsm = RegexGuide.from_interegular_fsm(fsm, model.tokenizer) - device = model.device - generator = SequenceGenerator(fsm, model, sampler, device) - return generator diff --git a/outlines/generate/regex.py b/outlines/generate/regex.py index 815a8b1b9..673880e49 100644 --- a/outlines/generate/regex.py +++ b/outlines/generate/regex.py @@ -1,12 +1,10 @@ from functools import singledispatch -from outlines.fsm.guide import RegexGuide from outlines.generate.api import ( - SequenceGenerator, SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import ExLlamaV2Model, OpenAI, TransformersVision +from outlines.models import OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial @@ -49,20 +47,6 @@ def regex_vision( return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) -@regex.register(ExLlamaV2Model) -def regex_exllamav2( - model, - regex_str: str, - sampler: Sampler = multinomial(), -) -> SequenceGenerator: - fsm = RegexGuide(regex_str, model.tokenizer) - - device = model.device - generator = SequenceGenerator(fsm, model, sampler, device) - - return generator - - @regex.register(OpenAI) def regex_openai( model: OpenAI, diff --git a/outlines/generate/text.py b/outlines/generate/text.py index 3fe3dc553..32530d0c4 100644 --- a/outlines/generate/text.py +++ b/outlines/generate/text.py @@ -1,12 +1,10 @@ from functools import singledispatch -from outlines.fsm.guide import StopAtEOSGuide from outlines.generate.api import ( - SequenceGenerator, SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import ExLlamaV2Model, OpenAI, TransformersVision +from outlines.models import OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial @@ -36,13 +34,6 @@ def text(model, sampler: Sampler = multinomial()) -> SequenceGeneratorAdapter: return SequenceGeneratorAdapter(model, None, sampler) -@text.register(ExLlamaV2Model) -def text_exllamav2(model, sampler: Sampler = multinomial()) -> SequenceGenerator: - fsm = StopAtEOSGuide(model.tokenizer) - device = model.device - return SequenceGenerator(fsm, model, sampler, device) - - @text.register(TransformersVision) def text_vision(model, sampler: Sampler = multinomial()): return VisionSequenceGeneratorAdapter(model, None, sampler) diff --git a/outlines/models/exllamav2.py b/outlines/models/exllamav2.py index 0ec6ef033..f06b7e46e 100644 --- a/outlines/models/exllamav2.py +++ b/outlines/models/exllamav2.py @@ -1,12 +1,21 @@ -import os -from typing import TYPE_CHECKING, Optional +import dataclasses +from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, TypedDict, Union + +from typing_extensions import Unpack + +from outlines.generate.api import GenerationParameters, SamplingParameters if TYPE_CHECKING: - from exllamav2 import ExLlamaV2, ExLlamaV2Cache, ExLlamaV2Lora - from transformers import PreTrainedTokenizer - import torch + from exllamav2 import ExLlamaV2Tokenizer + from exllamav2.generator import ExLlamaV2DynamicGenerator, ExLlamaV2Sampler + -from .transformers import TransformerTokenizer +class ExllamaV2Params(TypedDict, total=False): + max_tokens: int + stop_conditions: Optional[List[Union[int, str]]] + seed: Optional[int] + gen_settings: "ExLlamaV2Sampler.Settings" + max_new_tokens: List[int] class ExLlamaV2Model: @@ -14,108 +23,218 @@ class ExLlamaV2Model: def __init__( self, - model: "ExLlamaV2", - tokenizer: "PreTrainedTokenizer", - device, - cache: "ExLlamaV2Cache", - lora: Optional["ExLlamaV2Lora"] = None, + generator: "ExLlamaV2DynamicGenerator", + tokenizer: "ExLlamaV2Tokenizer", + max_seq_len: int, ): - self.device = device - self.model = model - self.tokenizer = TransformerTokenizer(tokenizer) - self.cache = cache - self.past_seq = None - self.lora = lora - - def forward(self, input_ids: "torch.LongTensor", *_): - """Compute a forward pass through the exl2 model.""" - import torch - - # Caching with past_seq - reset = True - seq_tensor = input_ids[0] - - if self.past_seq is not None: - min_length = min(self.past_seq.shape[0], seq_tensor.shape[0]) - indices = torch.nonzero( - ~torch.eq(self.past_seq[:min_length], seq_tensor[:min_length]) - ) - if len(indices) > 0: - longest_prefix = indices[0].item() - else: - longest_prefix = min_length - - if longest_prefix > 0: - reset = False - self.cache.current_seq_len = longest_prefix - if seq_tensor.shape[0] - longest_prefix > 1: - self.model.forward( - seq_tensor[longest_prefix:-1].view(1, -1), - self.cache, - preprocess_only=True, - loras=[self.lora], - ) - elif seq_tensor.shape[0] == longest_prefix: - self.cache.current_seq_len -= 1 - - if reset: - self.cache.current_seq_len = 0 - if seq_tensor.shape[0] > 1: - self.model.forward( - seq_tensor[:-1].view(1, -1), - self.cache, - preprocess_only=True, - loras=[self.lora], + self.generator = generator + self.tokenizer = tokenizer + self.max_seq_len = max_seq_len + + def prepare_generation_parameters( + self, + prompts: Union[str, List[str]], + generation_parameters: GenerationParameters, + sampling_parameters: SamplingParameters, + structure_logits_processor, + **exllamav2_params: Unpack[ExllamaV2Params], + ) -> Tuple[ExllamaV2Params, Union[str, List[str]]]: + """Prepare the generation parameters. + + `exllamav2` uses different default values + + """ + from exllamav2.generator import ExLlamaV2Sampler + + if isinstance(prompts, str): + prompts = [prompts] + max_tokens, stop_at, seed = dataclasses.astuple(generation_parameters) + + if max_tokens is None: + max_tokens = [] + for prompt in prompts: + ids = self.generator.tokenizer.encode( + prompt, encode_special_tokens=True ) + prompt_tokens = ids.shape[-1] + max_tokens.append(self.max_seq_len - prompt_tokens) + exllamav2_params["max_new_tokens"] = max_tokens + else: + exllamav2_params["max_new_tokens"] = [ + max_tokens for _ in range(len(prompts)) + ] - self.past_seq = seq_tensor + stop_conditions = [self.generator.tokenizer.eos_token_id] + if isinstance(generation_parameters.stop_at, str): + stop_conditions.append(generation_parameters.stop_at) + elif isinstance(generation_parameters.stop_at, list): + for stop_at in generation_parameters.stop_at: + stop_conditions.append(stop_at) + exllamav2_params["stop_conditions"] = stop_conditions + exllamav2_params["seed"] = seed - return self.model.forward( - seq_tensor[-1:].view(1, -1), self.cache, loras=[self.lora] - ) + gen_settings = ExLlamaV2Sampler.Settings() + if sampling_parameters.temperature is not None: + gen_settings.temperature = sampling_parameters.temperature + if sampling_parameters.top_p is not None: + gen_settings.top_p = sampling_parameters.top_p + if sampling_parameters.top_k is not None: + gen_settings.top_k = sampling_parameters.top_k + gen_settings.logits_processor = structure_logits_processor + exllamav2_params["gen_settings"] = gen_settings + if sampling_parameters.num_samples > 1: + prompts = prompts * sampling_parameters.num_samples + exllamav2_params["max_new_tokens"] = ( + exllamav2_params["max_new_tokens"] * sampling_parameters.num_samples + ) - def __call__(self, input_ids: "torch.LongTensor", *_) -> "torch.FloatTensor": - logits = self.forward(input_ids) - next_token_logits = logits[..., -1, :] + if len(prompts) == 1: + prompts = prompts[0] - return next_token_logits, None + return exllamav2_params, prompts - def update_lora(self, lora_path: Optional[str] = None): + def reformat_output( + self, output: Union[str, List[str]], sampling_parameters: SamplingParameters + ): """ - Update and apply the LoRA to the model. + The purpose of this function is to reformat the output from exllamav2's output format to outline's output format + For exllamav2, it mainly accepts only a list or a string(they also do cfg sampling with tuples but we will ignore this for now) + The exllamav2's logic is + 1. If the prompt is a string, return a string. This is the same as outlines + 2. If a prompt is a list, return a list. This is not the same as outlines output in that if the list is only one element, the string is expected to be outputted. + 3. There is no such thing as num_samples, so the prompts had to be duplicated by num_samples times. Then, we had the function output a list of lists + """ + if isinstance(output, str): + return output + if len(output) == 1: + return output[0] + if sampling_parameters.num_samples > 1: + if len(output) == sampling_parameters.num_samples: + return output + assert len(output) % sampling_parameters.num_samples == 0 + num_items_per_sample = len(output) // sampling_parameters.num_samples + new_output = [] + for i in range(sampling_parameters.num_samples): + curr_sample = [] + for j in range(num_items_per_sample): + curr_sample.append(output[i * num_items_per_sample + j]) + new_output.append(curr_sample) + return new_output + return output - Args: - lora_path (Optional[str]): The path to the LoRA directory. If None, the LoRA will be unloaded. + def generate( + self, + prompts: Union[str, List[str]], + generation_parameters: GenerationParameters, + structure_logits_processor, + sampling_parameters: SamplingParameters, + **exllamav2_params: Unpack[ExllamaV2Params], + ) -> Union[str, List[str]]: + exllamav2_params, prompts = self.prepare_generation_parameters( + prompts, + generation_parameters, + sampling_parameters, + structure_logits_processor, + ) """ - try: - from exllamav2 import ExLlamaV2Lora - except ImportError: - raise ImportError( - "The `exllamav2` library needs to be installed in order to use `exllamav2` models." + In exllamav2, it needs the max amount of new tokens generated. + The reason exllamav2_params["max_new_tokens"] is a list is because in prepare_generation_parameters + the max amount of tokens that can be generated by the model for each prompt(by encoding with tokenizer) is calculated. + The minimum is picked because otherwise it might be possible for one of the + prompts to exceed the max sequence length. + """ + output = self.generator.generate( + prompt=prompts, + gen_settings=exllamav2_params["gen_settings"], + max_new_tokens=min(exllamav2_params["max_new_tokens"]), + completion_only=True, + encode_special_tokens=True, + stop_conditions=exllamav2_params["stop_conditions"], + add_bos=False, + seed=exllamav2_params["seed"], + ) + + return self.reformat_output(output, sampling_parameters) + + def stream( + self, + prompts: Union[str, List[str]], + generation_parameters: GenerationParameters, + structure_logits_processor, + sampling_parameters: SamplingParameters, + **exllamav2_params: Unpack[ExllamaV2Params], + ) -> Iterator[Union[str, List[str]]]: + from exllamav2.generator import ExLlamaV2DynamicJob + + exllamav2_params, prompts = self.prepare_generation_parameters( + prompts, + generation_parameters, + sampling_parameters, + structure_logits_processor, + ) + + order = {} + if isinstance(prompts, str): + prompts = [prompts] + batch_size = len(prompts) + seed = exllamav2_params["seed"] + for idx, p in enumerate(prompts): + input_ids = self.generator.tokenizer.encode( + p, encode_special_tokens=True, add_bos=False ) - if lora_path is None: - if self.lora is not None: - print(" -- Unloading LoRA...") - self.lora = None - else: - self.lora = ExLlamaV2Lora.from_directory(self.model, lora_path) - print(" -- Loading LoRA...") + + job = ExLlamaV2DynamicJob( + input_ids=input_ids, + max_new_tokens=exllamav2_params["max_new_tokens"][idx], + min_new_tokens=0, + seed=seed, + stop_conditions=exllamav2_params["stop_conditions"], + gen_settings=exllamav2_params["gen_settings"], + token_healing=False, + decode_special_tokens=False, + ) + + if seed is not None: + seed += 1 + + serial = self.generator.enqueue(job) + order[serial] = idx + + # Collect outputs until all jobs finish + + next_text = [""] * batch_size + + def token_generator() -> Iterator[str]: + while self.generator.num_remaining_jobs(): + results = self.generator.iterate() + for r in results: + idx = order[r["serial"]] + if r["stage"] == "streaming": + text = r.get("text", "") + next_text[idx] = text + if r["eos"]: + next_text[idx] = "" + yield self.reformat_output(next_text, sampling_parameters) + return + + return token_generator() + + +# Taken from https://github.com/lapp0/exllamav2/pull/1/files#diff-26f303de07c10aad998e33d3df52581643673a598162cc4b35ef051f52d7c60b +def patch_tokenizer(tokenizer): + tokenizer.vocabulary = tokenizer.piece_to_id + tokenizer.special_tokens = set(tokenizer.extended_piece_to_id) + tokenizer.convert_token_to_string = lambda t: t + return tokenizer def exl2( model_path: str, - device: str, + draft_model_path: Optional[str] = None, max_seq_len: Optional[int] = None, - scale_pos_emb: Optional[float] = None, - scale_alpha_value: Optional[float] = None, - no_flash_attn: Optional[bool] = None, - num_experts_per_token: Optional[int] = None, - cache_8bit: bool = False, cache_q4: bool = False, - tokenizer_kwargs: dict = {}, - gpu_split: Optional[str] = None, - low_mem: Optional[bool] = None, - verbose: Optional[bool] = None, + paged: bool = True, + max_chunk_size: Optional[int] = None, ) -> ExLlamaV2Model: """ Load an ExLlamaV2 model. @@ -136,8 +255,6 @@ def exl2( Disable flash attention. Defaults to None. num_experts_per_token (Optional[int], optional) Number of experts per token. Defaults to None. - cache_8bit (bool, optional) - Use 8-bit cache. Defaults to False. cache_q4 (bool, optional) Use Q4 cache. Defaults to False. tokenizer_kwargs (dict, optional) @@ -162,71 +279,62 @@ def exl2( from exllamav2 import ( ExLlamaV2, ExLlamaV2Cache, - ExLlamaV2Cache_8bit, ExLlamaV2Cache_Q4, ExLlamaV2Config, + ExLlamaV2Tokenizer, ) - from transformers import AutoTokenizer + from exllamav2.generator import ExLlamaV2DynamicGenerator + except ImportError: raise ImportError( "The `exllamav2`, `transformers` and `torch` libraries needs to be installed in order to use `exllamav2` models." ) + config = ExLlamaV2Config(model_path) + if max_chunk_size is not None: + config.max_input_len = max_chunk_size + config.max_attention_size = max_chunk_size**2 - # Load tokenizer - if not verbose: - print(" -- Loading tokenizer...") - tokenizer_kwargs.setdefault("padding_side", "left") - tokenizer = AutoTokenizer.from_pretrained(model_path, **tokenizer_kwargs) - # tokenizer = TransformerTokenizer(model_path, **tokenizer_kwargs) - - # Check fasttensors for config - if os.name != "nt": - use_fasttensors = True - else: - use_fasttensors = False - - # Create config - config = ExLlamaV2Config() - config.model_dir = model_path - config.fasttensors = use_fasttensors - config.prepare() - - # Set config options - if max_seq_len is not None: - config.max_seq_len = max_seq_len - if scale_pos_emb is not None: - config.scale_pos_emb = scale_pos_emb - if scale_alpha_value is not None: - config.scale_alpha_value = scale_alpha_value - if no_flash_attn is not None: - config.no_flash_attn = no_flash_attn - if num_experts_per_token is not None: - config.num_experts_per_token = num_experts_per_token - if low_mem: - config.set_low_mem() - - # Prepare the model from the config + config.arch_compat_overrides() model = ExLlamaV2(config) - - # Create cache - if cache_8bit: - cache = ExLlamaV2Cache_8bit(model, lazy=not model.loaded) - elif cache_q4: - cache = ExLlamaV2Cache_Q4(model, lazy=not model.loaded) + if max_seq_len is None: + max_seq_len = -1 + if cache_q4: + cache = ExLlamaV2Cache_Q4(model, max_seq_len=max_seq_len, lazy=True) else: - cache = ExLlamaV2Cache(model, lazy=not model.loaded) - - # Load the model - split = None - if gpu_split and gpu_split != "auto": - split = [float(alloc) for alloc in gpu_split.split(",")] - if not verbose: - print(" -- Loading model...") - model.load(split) - - # Autoload if no GPU split was provided - if not model.loaded: - print(" -- Loading model...") - model.load_autosplit(cache) - - return ExLlamaV2Model(model, tokenizer, device, cache) + cache = ExLlamaV2Cache(model, max_seq_len=max_seq_len, lazy=True) + model.load_autosplit(cache, progress=True) + + print("Loading tokenizer...") + tokenizer = ExLlamaV2Tokenizer(config) + tokenizer = patch_tokenizer(tokenizer) + max_batch_size = 4 if paged else 1 + + draft_model = None + draft_cache = None + if draft_model_path is not None: + draft_config = ExLlamaV2Config(draft_model_path) + draft_model = ExLlamaV2(draft_config) + + if cache_q4: + draft_cache = ExLlamaV2Cache_Q4( + draft_model, max_seq_len=max_seq_len, lazy=True + ) + else: + draft_cache = ExLlamaV2Cache( + draft_model, max_seq_len=max_seq_len, lazy=True + ) + + # Initialize the generator with all default parameters + generator = ExLlamaV2DynamicGenerator( + model=model, + cache=cache, + draft_model=draft_model, + draft_cache=draft_cache, + tokenizer=tokenizer, + max_batch_size=max_batch_size, + use_ngram_draft=False, + max_chunk_size=max_chunk_size, + paged=paged, + ) + max_seq_len = cache.max_seq_len + return ExLlamaV2Model(generator, tokenizer, max_seq_len) diff --git a/pyproject.toml b/pyproject.toml index ab3ecd775..ac94ecf57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ test = [ "torch", "transformers", "pillow", + "exllamav2", ] serve = [ "vllm>=0.3.0", diff --git a/tests/generate/conftest.py b/tests/generate/conftest.py index ed8830119..abd9c72a4 100644 --- a/tests/generate/conftest.py +++ b/tests/generate/conftest.py @@ -27,9 +27,11 @@ def pytest_collection_modifyitems(config, items): for item in items: if "model_fixture" in item.fixturenames: model_param = item.callspec.params.get("model_fixture", None) - if model_param.startswith( - "model_transformers_vision" - ) or model_param.startswith("model_vllm"): + if ( + model_param.startswith("model_transformers_vision") + or model_param.startswith("model_vllm") + or model_param.startswith("model_exllamav2") + ): item.add_marker(skip_marker) if not is_metal_available(): diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index a96ce8673..b36baf9a4 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -20,6 +20,15 @@ def model_llamacpp(tmp_path_factory): ) +@pytest.fixture(scope="session") +def model_exllamav2(tmp_path_factory): + return models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + cache_q4=True, + paged=False, + ) + + @pytest.fixture(scope="session") def model_mlxlm(tmp_path_factory): return models.mlxlm("mlx-community/TinyLlama-1.1B-Chat-v1.0-4bit") @@ -98,6 +107,7 @@ def model_t5(tmp_path_factory): ALL_MODEL_FIXTURES = ( "model_llamacpp", + "model_exllamav2", "model_mlxlm", "model_mlxlm_phi3", "model_transformers_random", diff --git a/tests/generate/test_integration_exllamav2.py b/tests/generate/test_integration_exllamav2.py new file mode 100644 index 000000000..12c4143b3 --- /dev/null +++ b/tests/generate/test_integration_exllamav2.py @@ -0,0 +1,363 @@ +import importlib +from unittest.mock import patch + +import pytest + +import outlines.models as models +from outlines.generate.api import GenerationParameters, SamplingParameters +from outlines.models.exllamav2 import ExLlamaV2Model + + +@pytest.fixture(scope="session") +def model_exllamav2(tmp_path_factory): + return models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + cache_q4=True, + paged=False, + ) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_import_error(request, model_fixture): + with patch.dict("sys.modules", {"exllamav2": None}): + with pytest.raises(ImportError): + models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + cache_q4=True, + paged=False, + ) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_attributes(request, model_fixture): + model = request.getfixturevalue(model_fixture) + assert hasattr(model, "generator") + assert hasattr(model, "tokenizer") + assert model.tokenizer.convert_token_to_string(1) == 1 + assert hasattr(model, "max_seq_len") + assert isinstance(model.max_seq_len, int) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_generate_prompt_types(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at=None, seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + output = model.generate( + prompt, generation_params, structure_logits_processor, sampling_params + ) + assert isinstance(output, str) + prompt = ["test"] + output = model.generate( + prompt, generation_params, structure_logits_processor, sampling_params + ) + assert isinstance(output, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_generate_no_max_tokens(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=None, stop_at=None, seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + output = model.generate( + prompt, generation_params, structure_logits_processor, sampling_params + ) + assert isinstance(output, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_generate_test_stop_at(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at="stop", seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + output = model.generate( + prompt, generation_params, structure_logits_processor, sampling_params + ) + assert isinstance(output, str) + generation_params = GenerationParameters(max_tokens=10, stop_at=["stop"], seed=None) + output = model.generate( + prompt, generation_params, structure_logits_processor, sampling_params + ) + assert isinstance(output, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_generate_multisampling(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at="stop", seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 2, + ) + output = model.generate( + prompt, generation_params, structure_logits_processor, sampling_params + ) + assert isinstance(output, list) + assert isinstance(output[0], str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_prepare_generation_parameters(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at="stop", seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 2, + ) + exllamav2_params, prompts = model.prepare_generation_parameters( + prompt, generation_params, sampling_params, structure_logits_processor + ) + assert isinstance(exllamav2_params, dict) + assert isinstance(prompts, list) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_stream_prompt_types(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at=None, seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, str) + prompt = ["test"] + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_stream_no_max_tokens(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=None, stop_at=None, seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_stream_test_stop_at(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at="stop", seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, str) + generation_params = GenerationParameters(max_tokens=10, stop_at=["stop"], seed=None) + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_stream_multisampling(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, stop_at="stop", seed=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 2, + ) + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, list) + assert isinstance(token[0], str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_model_stream_seed(request, model_fixture): + model = request.getfixturevalue(model_fixture) + prompt = "test" + generation_params = GenerationParameters(max_tokens=10, seed=1, stop_at=None) + structure_logits_processor = None + sampling_params = SamplingParameters( + "multinomial", + 1, + 0.9, + 50, + 1.0, + ) + generator = model.stream( + prompt, generation_params, structure_logits_processor, sampling_params + ) + for token in generator: + assert isinstance(token, str) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_reformat_output(request, model_fixture): + model = request.getfixturevalue(model_fixture) + sampling_params = SamplingParameters( + "multinomial", + 1, + ) + output = "test" + reformatted_output = model.reformat_output(output, sampling_params) + assert reformatted_output == output + output = ["test"] + reformatted_output = model.reformat_output(output, sampling_params) + assert reformatted_output == output[0] + output = ["test", "test"] + sampling_params = SamplingParameters( + "multinomial", + 1, + ) + reformatted_output = model.reformat_output(output, sampling_params) + assert len(reformatted_output) == 2 + assert reformatted_output[0] == "test" + assert reformatted_output[1] == "test" + output = ["test", "test"] + sampling_params = SamplingParameters( + "multinomial", + 2, + ) + reformatted_output = model.reformat_output(output, sampling_params) + assert len(reformatted_output) == 2 + assert reformatted_output[0] == "test" + assert reformatted_output[1] == "test" + output = ["test", "test", "test", "test"] + sampling_params = SamplingParameters( + "multinomial", + 2, + ) + reformatted_output = model.reformat_output(output, sampling_params) + assert len(reformatted_output) == 2 + assert reformatted_output[0] == ["test", "test"] + assert reformatted_output[1] == ["test", "test"] + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_max_chunk_size(request, model_fixture): + model = models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + cache_q4=True, + paged=False, + max_chunk_size=128, + ) + assert isinstance(model, ExLlamaV2Model) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_cache_default(request, model_fixture): + model = models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + paged=False, + ) + assert isinstance(model, ExLlamaV2Model) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def is_flash_attn_available(): + try: + importlib.import_module("flash_attn") + except (ImportError, AssertionError): + return False + return True + + +@pytest.mark.skipif(not is_flash_attn_available(), reason="flash-attn is not installed") +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_paged(request, model_fixture): + model = models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + cache_q4=True, + paged=True, + ) + assert isinstance(model, ExLlamaV2Model) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_draft_model(request, model_fixture): + model = models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + draft_model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + cache_q4=True, + paged=False, + ) + assert isinstance(model, ExLlamaV2Model) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_draft_model_cache_default(request, model_fixture): + model = models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + draft_model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + paged=False, + ) + assert isinstance(model, ExLlamaV2Model) + + +@pytest.mark.parametrize("model_fixture", ["model_exllamav2"]) +def test_exl2_set_max_seq_len(request, model_fixture): + model = models.exl2( + model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + max_seq_len=2048, + paged=False, + cache_q4=True, + ) + assert isinstance(model, ExLlamaV2Model) From faa7c5c77fbe7a0d2f1117d9fc8c407766614294 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 4 Oct 2024 16:02:47 -0400 Subject: [PATCH 084/505] automatically download exl2 model in tests fix exl bug: sometimes piece_to_id not populated, but get_piece_to_id() still works fix exl bug: sometimes piece_to_id not populated, but get_piece_to_id() still works enable exl2 in generate.cfg cleate OutlinesExLlamaV2Tokenizer rather than monkey patching --- outlines/generate/cfg.py | 9 +------- outlines/models/exllamav2.py | 39 +++++++++++++++++++++++---------- tests/generate/test_generate.py | 10 ++++++++- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/outlines/generate/cfg.py b/outlines/generate/cfg.py index 034a65ae5..b677040d5 100644 --- a/outlines/generate/cfg.py +++ b/outlines/generate/cfg.py @@ -4,7 +4,7 @@ SequenceGeneratorAdapter, VisionSequenceGeneratorAdapter, ) -from outlines.models import ExLlamaV2Model, LlamaCpp, OpenAI, TransformersVision +from outlines.models import LlamaCpp, OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial @@ -41,13 +41,6 @@ def cfg_vision(model, cfg_str: str, sampler: Sampler = multinomial()): return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) -@cfg.register(ExLlamaV2Model) -def cfg_exllamav2(model, cfg_str: str, sampler: Sampler = multinomial()): - raise NotImplementedError( - "Not yet available, track progress in https://github.com/dottxt-ai/outlines/pull/1010" - ) - - @cfg.register(LlamaCpp) def cfg_llamacpp(model, cfg_str: str, sampler: Sampler = multinomial()): raise NotImplementedError("Not yet available due to bug in llama_cpp tokenizer") diff --git a/outlines/models/exllamav2.py b/outlines/models/exllamav2.py index f06b7e46e..821d4e591 100644 --- a/outlines/models/exllamav2.py +++ b/outlines/models/exllamav2.py @@ -1,12 +1,13 @@ import dataclasses from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, TypedDict, Union +import torch from typing_extensions import Unpack from outlines.generate.api import GenerationParameters, SamplingParameters if TYPE_CHECKING: - from exllamav2 import ExLlamaV2Tokenizer + import torch.LongTensor from exllamav2.generator import ExLlamaV2DynamicGenerator, ExLlamaV2Sampler @@ -18,13 +19,33 @@ class ExllamaV2Params(TypedDict, total=False): max_new_tokens: List[int] +class OutlinesExLlamaV2Tokenizer: + def __init__(self, tokenizer): + self.exl2_tokenizer = tokenizer + self.vocabulary = self.exl2_tokenizer.get_piece_to_id_dict() + self.special_tokens = set(self.exl2_tokenizer.extended_piece_to_id) + self.eos_token_id = self.exl2_tokenizer.eos_token_id + + def convert_token_to_string(self, token): + return token + + def decode(self, token_ids: "torch.LongTensor") -> List[str]: + decoded = self.exl2_tokenizer.decode( + torch.tensor(token_ids), + decode_special_tokens=False, + ) + if isinstance(decoded, str): + return [decoded] + return decoded + + class ExLlamaV2Model: """Represents a `exl2` model.""" def __init__( self, generator: "ExLlamaV2DynamicGenerator", - tokenizer: "ExLlamaV2Tokenizer", + tokenizer: "OutlinesExLlamaV2Tokenizer", max_seq_len: int, ): self.generator = generator @@ -220,14 +241,6 @@ def token_generator() -> Iterator[str]: return token_generator() -# Taken from https://github.com/lapp0/exllamav2/pull/1/files#diff-26f303de07c10aad998e33d3df52581643673a598162cc4b35ef051f52d7c60b -def patch_tokenizer(tokenizer): - tokenizer.vocabulary = tokenizer.piece_to_id - tokenizer.special_tokens = set(tokenizer.extended_piece_to_id) - tokenizer.convert_token_to_string = lambda t: t - return tokenizer - - def exl2( model_path: str, draft_model_path: Optional[str] = None, @@ -306,7 +319,6 @@ def exl2( print("Loading tokenizer...") tokenizer = ExLlamaV2Tokenizer(config) - tokenizer = patch_tokenizer(tokenizer) max_batch_size = 4 if paged else 1 draft_model = None @@ -337,4 +349,7 @@ def exl2( paged=paged, ) max_seq_len = cache.max_seq_len - return ExLlamaV2Model(generator, tokenizer, max_seq_len) + + outlines_tokenizer = OutlinesExLlamaV2Tokenizer(tokenizer) + outlines_exl2_model = ExLlamaV2Model(generator, outlines_tokenizer, max_seq_len) + return outlines_exl2_model diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index b36baf9a4..9c288c21e 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -22,8 +22,16 @@ def model_llamacpp(tmp_path_factory): @pytest.fixture(scope="session") def model_exllamav2(tmp_path_factory): + from huggingface_hub import snapshot_download + + tmp_dir = tmp_path_factory.mktemp("model_download") + model_path = snapshot_download( + repo_id="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4.6-exl2", + cache_dir=tmp_dir, + ) + return models.exl2( - model_path="blockblockblock/TinyLlama-1.1B-Chat-v1.0-bpw4-exl2", + model_path=model_path, cache_q4=True, paged=False, ) From 0da70390d63fba8d2b6f4c1a439ff091a6ddf23c Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 4 Oct 2024 22:24:29 -0400 Subject: [PATCH 085/505] document third party exllamav2 with logits processor --- docs/reference/models/exllamav2.md | 10 +++++++++- outlines/models/exllamav2.py | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/reference/models/exllamav2.md b/docs/reference/models/exllamav2.md index afe542112..a4e727840 100644 --- a/docs/reference/models/exllamav2.md +++ b/docs/reference/models/exllamav2.md @@ -1,7 +1,15 @@ # ExllamaV2 +The `outlines.models.exllamav2` model requires a Logits Processor component for compatibility with Outlines structured generation. While ExLlamaV2 doesn't natively support this feature, a third-party fork provides the necessary functionality. You can install it with the following command: + +```bash +pip install git+https://github.com/lapp0/exllamav2@sampler-logits-processor +``` + +Install other requirements: + ```bash -pip install exllamav2 transformers torch +pip install transformers torch ``` *Coming soon* diff --git a/outlines/models/exllamav2.py b/outlines/models/exllamav2.py index 821d4e591..78da796fb 100644 --- a/outlines/models/exllamav2.py +++ b/outlines/models/exllamav2.py @@ -300,7 +300,9 @@ def exl2( except ImportError: raise ImportError( - "The `exllamav2`, `transformers` and `torch` libraries needs to be installed in order to use `exllamav2` models." + "The `exllamav2`, `transformers` and `torch` libraries needs to be installed in order to use `exllamav2` models. " + "Please run `pip install transformers torch git+https://github.com/lapp0/exllamav2@sampler-logits-processor` " + "Documentation: https://dottxt-ai.github.io/outlines/reference/models/exllamav2/" ) config = ExLlamaV2Config(model_path) if max_chunk_size is not None: From ef4e819102cb3f94efb85dd8d510c09edabc36a5 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sun, 21 Jul 2024 14:41:40 -0500 Subject: [PATCH 086/505] Version the Documentation --- .github/workflows/publish_documentation.yml | 26 +++++++++++++++++++-- mkdocs.yml | 4 ++++ requirements-doc.txt | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_documentation.yml b/.github/workflows/publish_documentation.yml index d861424f3..d1235e085 100644 --- a/.github/workflows/publish_documentation.yml +++ b/.github/workflows/publish_documentation.yml @@ -5,6 +5,9 @@ on: push: branches: - main + release: + types: + - created permissions: contents: write @@ -14,6 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-python@v4 with: python-version: 3.x @@ -25,7 +30,24 @@ jobs: restore-keys: | mkdocs-material- - run: pip install -r requirements-doc.txt - - name: Build documentation + - run: mkdocs build + + - name: Set up Git + run: | + git config user.name ${{ github.actor }} + git config user.email ${{ github.actor }}@users.noreply.github.com + + - name: Publish Tag as latest + env: + GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }} + if: github.event_name == 'release' + run: | + mike deploy --push --update-aliases ${{ github.ref_name }} latest + mike set-default --push latest + + - name: Publish main as unstable env: GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }} - run: mkdocs gh-deploy --force + if: github.event_name == 'push' + run: | + mike deploy --push --update-aliases ${{ github.ref_name }} unstable diff --git a/mkdocs.yml b/mkdocs.yml index 4189df1c0..b200ee3d8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,10 @@ extra: analytics: provider: google property: !ENV GOOGLE_ANALYTICS_KEY + version: + provider: mike + default: latest + alias: true # Extensions markdown_extensions: diff --git a/requirements-doc.txt b/requirements-doc.txt index cf8c674a7..05f9f36ec 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -6,3 +6,4 @@ mkdocs-section-index mkdocstrings[python] mkdocs-git-committers-plugin-2 mkdocs-git-revision-date-localized-plugin +mike From 541a8b0f7a1c76eee085d9babc3673b0031a0f2c Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 5 Oct 2024 15:25:35 -0400 Subject: [PATCH 087/505] update logits in place for GuideLogitsProcessor --- outlines/processors/structured.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index 93d68727f..d9a912c89 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -102,12 +102,13 @@ def process_logits( sequence_states.append(self._guide_states[curr_state_key]) - mask = torch.full_like(logits, -math.inf) + mask = torch.ones_like(logits, dtype=torch.bool) for i, guide_state in enumerate(sequence_states): allowed_tokens = self.guide.get_next_instruction(guide_state).tokens - mask[i, allowed_tokens] = logits[i, allowed_tokens] + mask[i, allowed_tokens] = False + logits.masked_fill_(mask, float("-inf")) - return mask + return logits def copy(self) -> "GuideLogitsProcessor": """Return a copy of the logits processor.""" From d1b8728c6c65355f53a2c78432f8b07ad7518b7a Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Sat, 5 Oct 2024 16:29:21 -0400 Subject: [PATCH 088/505] construct logits mask in batch operation --- outlines/processors/base_logits_processor.py | 4 +-- outlines/processors/structured.py | 33 ++++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index feedf5253..9a52abecd 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -75,10 +75,10 @@ def __call__( # Guarantee passed as 2D Tensors, then covert back to original (1D or 2D) shape if len(torch_logits.shape) == 2: - processed_logits = self.process_logits(input_ids.tolist(), torch_logits) + processed_logits = self.process_logits(input_ids, torch_logits) elif len(torch_logits.shape) == 1: processed_logits = self.process_logits( - [input_ids.tolist()], torch_logits.unsqueeze(0) + input_ids.unsqueeze(0), torch_logits.unsqueeze(0) ).squeeze(0) # return logits as passed array type diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index d9a912c89..e3b9e60d3 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -70,7 +70,7 @@ def __init__(self, tokenizer: "Tokenizer", guide: Guide): self._seq_start_idx = None def process_logits( - self, input_ids: List[List[int]], logits: torch.Tensor + self, input_ids: torch.LongTensor, logits: torch.FloatTensor ) -> torch.Tensor: """Use the Guide to bias the logits before sampling the next token. @@ -93,19 +93,32 @@ def process_logits( for seq_ids in input_ids: gen_ids = seq_ids[self._seq_start_idx :] - curr_state_key = hash(tuple(gen_ids)) + curr_state_key = hash(tuple(gen_ids.tolist())) if curr_state_key not in self._guide_states: - prev_state = self._guide_states[hash(tuple(gen_ids[:-1]))] - curr_state = self.guide.get_next_state(prev_state, gen_ids[-1]) + prev_state = self._guide_states[hash(tuple(gen_ids[:-1].tolist()))] + curr_state = self.guide.get_next_state(prev_state, gen_ids[-1].item()) self._guide_states[curr_state_key] = curr_state sequence_states.append(self._guide_states[curr_state_key]) mask = torch.ones_like(logits, dtype=torch.bool) + + allowed_tokens_batch = [] + batch_indices = [] for i, guide_state in enumerate(sequence_states): - allowed_tokens = self.guide.get_next_instruction(guide_state).tokens - mask[i, allowed_tokens] = False + allowed_tokens = self.guide.get_next_instruction(guide_state).tokens.to( + mask.device, non_blocking=True + ) + allowed_tokens_batch.append(allowed_tokens) + batch_indices.append( + torch.full_like(allowed_tokens, i) + ) # Store batch index for each allowed token + + allowed_tokens_concat = torch.cat(allowed_tokens_batch) + batch_indices_concat = torch.cat(batch_indices) + + mask[batch_indices_concat, allowed_tokens_concat] = False logits.masked_fill_(mask, float("-inf")) return logits @@ -202,7 +215,7 @@ def __init__(self, cfg_str: str, tokenizer: "Tokenizer"): super().__init__(tokenizer=tokenizer, guide=cfg_guide) def process_logits( - self, input_ids: List[List[int]], logits: torch.Tensor + self, input_ids: torch.LongTensor, logits: torch.Tensor ) -> torch.Tensor: """Same behavior as GuideLogitsProcessor, but uses rejection sampling""" if self._seq_start_idx is None: @@ -212,11 +225,11 @@ def process_logits( for seq_ids in input_ids: gen_ids = seq_ids[self._seq_start_idx :] - curr_state_key = hash(tuple(gen_ids)) + curr_state_key = hash(tuple(gen_ids.tolist())) if curr_state_key not in self._guide_states: - prev_state = self._guide_states[hash(tuple(gen_ids[:-1]))] - curr_state = self.guide.get_next_state(prev_state, gen_ids[-1]) + prev_state = self._guide_states[hash(tuple(gen_ids[:-1].tolist()))] + curr_state = self.guide.get_next_state(prev_state, gen_ids[-1].item()) self._guide_states[curr_state_key] = curr_state sequence_states.append(self._guide_states[curr_state_key]) From 9e8bd6cc22a003bc805ff259c2722a3b8155e06c Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 7 Oct 2024 11:41:43 -0400 Subject: [PATCH 089/505] ensure valid link to docs with latest/ in url --- README.md | 10 +++++----- docs/cookbook/simtom.md | 4 ++-- outlines/fsm/guide.py | 2 +- outlines/models/exllamav2.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9b01c5662..832703ce6 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ Made with ❤👷️ by the team at [.txt](https://dottxt.co). pip install outlines ``` -First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/welcome) +First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/latest/welcome/) ## Features -- [x] 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/installation): OpenAI, transformers, llama.cpp, exllama2, mamba +- [x] 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/latest/installation): OpenAI, transformers, llama.cpp, exllama2, mamba - [x] 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/) - [x] 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping - [x] ⚡ Fast [regex-structured generation](#efficient-regex-structured-generation) @@ -35,7 +35,7 @@ First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/we - [x] 💾 Caching of generations - [x] 🗂️ Batch inference - [x] 🎲 Sample with the greedy, multinomial and beam search algorithms (and more to come!) -- [x] 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! +- [x] 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/latest/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! Outlines has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date! @@ -338,7 +338,7 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) ## Join us - 💡 **Have an idea?** Come chat with us on [Discord][discord] -- 🔨 **Want to contribute?** Consult our [contribution guide](https://dottxt-ai.github.io/outlines/community/contribute/). +- 🔨 **Want to contribute?** Consult our [contribution guide](https://dottxt-ai.github.io/outlines/latest/community/contribute/). - 🐞 **Found a bug?** Open an [issue](https://github.com/dottxt-ai/outlines/issues) @@ -353,7 +353,7 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) } ``` -[documentation]: https://dottxt-ai.github.io/outlines/welcome/ +[documentation]: https://dottxt-ai.github.io/outlines/latest/welcome/ [documentation-badge]: https://img.shields.io/readthedocs/outlines [contributors]: https://github.com/dottxt-ai/outlines/graphs/contributors [contributors-badge]: https://img.shields.io/github/contributors/dottxt-ai/outlines?style=flat-square&logo=github&logoColor=white&color=ECEFF4 diff --git a/docs/cookbook/simtom.md b/docs/cookbook/simtom.md index f730d029a..4ad78846b 100644 --- a/docs/cookbook/simtom.md +++ b/docs/cookbook/simtom.md @@ -17,9 +17,9 @@ SimToM calls an LLM with two consecutive prompts: To implement SimToM with Outlines, we will need to: -1. Write the prompts with [prompt functions](https://dottxt-ai.github.io/outlines/reference/prompting/). +1. Write the prompts with [prompt functions](https://dottxt-ai.github.io/outlines/latest/reference/prompting/). 2. Define the JSON object each prompt will return using Pydantic. -3. Generate responses with a Mistral model using the [transformers integration](https://dottxt-ai.github.io/outlines/reference/models/transformers/). +3. Generate responses with a Mistral model using the [transformers integration](https://dottxt-ai.github.io/outlines/latest/reference/models/transformers/). Let's dive into it! diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index a3a2c7369..697597234 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -107,7 +107,7 @@ def __init__(self, cfg_string: str, tokenizer): """ warnings.warn( "Outlines' public *community-contributed* CFG structured generation is experimental. " - "Please review https://dottxt-ai.github.io/outlines/reference/cfg#disclaimer" + "Please review https://dottxt-ai.github.io/outlines/latest/reference/generation/cfg#disclaimer" ) self.cfg_string = cfg_string diff --git a/outlines/models/exllamav2.py b/outlines/models/exllamav2.py index 78da796fb..d2aa84b0d 100644 --- a/outlines/models/exllamav2.py +++ b/outlines/models/exllamav2.py @@ -302,7 +302,7 @@ def exl2( raise ImportError( "The `exllamav2`, `transformers` and `torch` libraries needs to be installed in order to use `exllamav2` models. " "Please run `pip install transformers torch git+https://github.com/lapp0/exllamav2@sampler-logits-processor` " - "Documentation: https://dottxt-ai.github.io/outlines/reference/models/exllamav2/" + "Documentation: https://dottxt-ai.github.io/outlines/latest/reference/models/exllamav2/" ) config = ExLlamaV2Config(model_path) if max_chunk_size is not None: From a2fd35cfa58b9a9fb87e885a851af40019c10b62 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 7 Oct 2024 11:27:43 -0400 Subject: [PATCH 090/505] use not in mkdocs --- mkdocs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index b200ee3d8..c0494c6ea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,8 +6,8 @@ site_description: >- # Repository -repo_name: outlines-dev/outlines -repo_url: https://github.com/outlines-dev/outlines +repo_name: dottxt-ai/outlines +repo_url: https://github.com/dottxt-ai/outlines # Copyright copyright: Copyright © 2023- The Outlines Developers @@ -38,7 +38,7 @@ theme: extra: social: - icon: fontawesome/brands/github - link: https://github.com/outlines-dev + link: https://github.com/dottxt-ai - icon: fontawesome/brands/twitter link: https://twitter.com/remilouf generator: false @@ -93,7 +93,7 @@ plugins: cards_layout_options: color: #173a58 - git-committers: - repository: outlines-dev/outlines + repository: dottxt-ai/outlines branch: main - git-revision-date-localized: enable_creation_date: true From c1fae8aca517d1289e3f934750cb6ea52804ad6d Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 10 Oct 2024 14:34:17 -0700 Subject: [PATCH 091/505] Create lmstudio.md --- docs/reference/serve/lmstudio.md | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/reference/serve/lmstudio.md diff --git a/docs/reference/serve/lmstudio.md b/docs/reference/serve/lmstudio.md new file mode 100644 index 000000000..e8c336137 --- /dev/null +++ b/docs/reference/serve/lmstudio.md @@ -0,0 +1,89 @@ +# Serve with LM Studio + +!!! tip "Would rather not self-host?" + + If you want to get started quickly with JSON-structured generation you can call instead [.json](https://h1xbpbfsf0w.typeform.com/to/ZgBCvJHF), a [.txt](http://dottxt.co) API that guarantees valid JSON. + +[LM Studio](https://lmstudio.ai/) is an application that runs local LLMs. It flexibly mixes GPU and CPU compute in hardware-constrained environments. + +As of [LM Studio 0.3.4](https://lmstudio.ai/blog/lmstudio-v0.3.4), it natively supports Outlines for structured text generation, using an OpenAI-compatible endpoint. + +## Setup + +1. Install LM Studio by visiting their [downloads page](https://lmstudio.ai/download). +2. Enable the LM Studio [server functionality](https://lmstudio.ai/docs/basics/server). +3. Download [a model](https://lmstudio.ai/docs/basics#1-download-an-llm-to-your-computer). +4. Install Python dependencies. +```bash +pip install pydantic openai +``` + +## Calling the server + +By default, LM Studio will serve from `http://localhost:1234`. If you are serving on a different port or host, make sure to change the `base_url` argument in `OpenAI` to the relevant location. + +```python +class Testing(BaseModel): + """ + A class representing a testing schema. + """ + name: str + age: int + +openai_client = openai.OpenAI( + base_url="http://0.0.0.0:1234/v1", + api_key="dopeness" +) + +# Make a request to the local LM Studio server +response = openai_client.beta.chat.completions.parse( + model="hugging-quants/Llama-3.2-1B-Instruct-Q8_0-GGUF", + messages=[ + {"role": "system", "content": "You are like so good at whatever you do."}, + {"role": "user", "content": "My name is Cameron and I am 28 years old. What's my name and age?"} + ], + response_format=Testing +) +``` + +You should receive a `ParsedChatCompletion[Testing]` object back: + +```python +ParsedChatCompletion[Testing]( + id='chatcmpl-3hykyf0fxus7jc90k6gwlw', + choices=[ + ParsedChoice[Testing]( + finish_reason='stop', + index=0, + logprobs=None, + message=ParsedChatCompletionMessage[Testing]( + content='{ "age": 28, "name": "Cameron" }', + refusal=None, + role='assistant', + function_call=None, + tool_calls=[], + parsed=Testing(name='Cameron', age=28) + ) + ) + ], + created=1728595622, + model='lmstudio-community/Phi-3.1-mini-128k-instruct-GGUF/Phi-3.1-mini-128k-instruct-Q4_K_M.gguf', + object='chat.completion', + service_tier=None, + system_fingerprint='lmstudio-community/Phi-3.1-mini-128k-instruct-GGUF/Phi-3.1-mini-128k-instruct- +Q4_K_M.gguf', + usage=CompletionUsage( + completion_tokens=17, + prompt_tokens=47, + total_tokens=64, + completion_tokens_details=None, + prompt_tokens_details=None + ) +) +``` + +You can retrieve your `Testing` object with + +```python +response.choices[0].message.parsed +``` From 64fb30faae5201e4c9957770fbc19863c7d3750c Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Mon, 14 Oct 2024 18:43:42 -0700 Subject: [PATCH 092/505] use pre-commit --- docs/reference/serve/lmstudio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/serve/lmstudio.md b/docs/reference/serve/lmstudio.md index e8c336137..db383186a 100644 --- a/docs/reference/serve/lmstudio.md +++ b/docs/reference/serve/lmstudio.md @@ -82,7 +82,7 @@ Q4_K_M.gguf', ) ``` -You can retrieve your `Testing` object with +You can retrieve your `Testing` object with ```python response.choices[0].message.parsed From 866b9a3c25b88a1790228632063583701990e364 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Thu, 10 Oct 2024 15:03:04 -0400 Subject: [PATCH 093/505] recover fsm_union, get_sub_fsms_from_seq, walk_fsm. Add to fsm/parser.py --- outlines/fsm/parsing.py | 215 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 5 deletions(-) diff --git a/outlines/fsm/parsing.py b/outlines/fsm/parsing.py index 92d3cc166..e48fb69e4 100644 --- a/outlines/fsm/parsing.py +++ b/outlines/fsm/parsing.py @@ -1,10 +1,22 @@ from copy import copy, deepcopy from dataclasses import dataclass from functools import lru_cache -from typing import Any, Dict, FrozenSet, Iterator, Optional, Set, Tuple, Union +from typing import ( + Any, + Dict, + FrozenSet, + Generator, + Iterator, + List, + Optional, + Sequence, + Set, + Tuple, + Union, +) import interegular -from interegular.fsm import FSM +from interegular.fsm import FSM, Alphabet, OblivionError from interegular.patterns import Unsupported from lark import Lark, Token from lark.common import LexerConf, ParserConf @@ -35,11 +47,9 @@ from lark.parsers.lalr_interactive_parser import InteractiveParser from lark.parsers.lalr_parser import LALR_Parser, ParseConf, ParserState, _Parser from outlines_core.fsm.regex import ( - fsm_union, - get_sub_fsms_from_seq, + BetterFSM, get_token_transition_keys, make_deterministic_fsm, - walk_fsm, ) PartialParseState = Tuple[str, int] @@ -920,3 +930,198 @@ def terminals_to_fsms(lp: PartialLark) -> Dict[str, FSM]: symbol_names_and_fsms[terminal.name] = fsm return symbol_names_and_fsms + + +def fsm_union( + fsms: Sequence[FSM], +) -> Tuple[FSM, Dict[int, Tuple[Set[Tuple[int, int]], Set[int], Dict[int, Set[int]]]]]: + """Construct an FSM representing the union of the FSMs in `fsms`. + + This is an updated version of `interegular.fsm.FSM.union` made to return an + extra map of component FSMs to the sets of state transitions that + correspond to them in the new FSM. + + """ + + alphabet, new_to_old = Alphabet.union(*[fsm.alphabet for fsm in fsms]) + + indexed_fsms = tuple(enumerate(fsms)) + + initial = {i: fsm.initial for (i, fsm) in indexed_fsms} + + # Dedicated function accepting a "superset" and returning the next + # "superset" obtained by following this transition in the new FSM + def follow(current_state, new_transition: int): + next = {} + for i, f in indexed_fsms: + old_transition = new_to_old[i][new_transition] + if ( + i in current_state + and current_state[i] in f.map + and old_transition in f.map[current_state[i]] + ): + next[i] = f.map[current_state[i]][old_transition] + if not next: + raise OblivionError + return next + + states = [initial] + finals: Set[int] = set() + map: Dict[int, Dict[int, int]] = {} + + # Map component FSMs to their new state-to-state transitions, finals, and a + # map translating component FSM states to aggregate FSM states + fsms_to_trans_finals: Dict[ + int, Tuple[Set[Tuple[int, int]], Set[int], Dict[int, Set[int]]] + ] = {} + + i = 0 + while i < len(states): + state = states[i] + + # Add to the finals of the aggregate FSM whenever we hit a final in a + # component FSM + if any(state.get(j, -1) in fsm.finals for (j, fsm) in indexed_fsms): + finals.add(i) + + # Compute the map for this state + map[i] = {} + for transition in alphabet.by_transition: + try: + next = follow(state, transition) + except OblivionError: + # Reached an oblivion state; don't list it + continue + else: + try: + # TODO: Seems like this could--and should--be avoided + j = states.index(next) + except ValueError: + j = len(states) + states.append(next) + + map[i][transition] = j + + for fsm_id, fsm_state in next.items(): + ( + fsm_transitions, + fsm_finals, + fsm_old_to_new, + ) = fsms_to_trans_finals.setdefault(fsm_id, (set(), set(), {})) + old_from = state[fsm_id] + old_to = fsm_state + fsm_old_to_new.setdefault(old_from, set()).add(i) + fsm_old_to_new.setdefault(old_to, set()).add(j) + fsm_transitions.add((i, j)) + if fsm_state in fsms[fsm_id].finals: + fsm_finals.add(j) + + i += 1 + + fsm = FSM( + alphabet=alphabet, + states=range(len(states)), + initial=0, + finals=finals, + map=map, + __no_validation__=True, + ) + + fsm, old_to_new_states = make_deterministic_fsm(fsm) + _fsms_to_trans_finals = { + fsm_id: ( + {(old_to_new_states[s1], old_to_new_states[s2]) for s1, s2 in transitions}, + {old_to_new_states[s] for s in finals}, + { + old_state: {old_to_new_states[new_state] for new_state in new_states} + for old_state, new_states in old_to_new.items() + }, + ) + for fsm_id, (transitions, finals, old_to_new) in sorted( + fsms_to_trans_finals.items(), key=lambda x: x[0] + ) + } + + return ( + fsm, + _fsms_to_trans_finals, + ) + + +def get_sub_fsms_from_seq( + state_seq: Sequence[int], + fsms_to_trans_finals: Dict[ + int, Tuple[Set[Tuple[int, int]], Set[int], Dict[int, Set[int]]] + ], +) -> Generator[Tuple[int, bool, bool], None, None]: + """Get the indices of the sub-FSMs in `fsm` that could have matched the state sequence `state_seq`. + + Parameters + ---------- + state_seq + A state sequence. + fsms_to_trans_finals + A map from FSM indices to tuples containing sets of their state transitions + and sets of the final/accept states. + + Returns + ------- + A generator returning tuples containing each sub-FSM index (in the order + they were union-ed to construct `fsm`) and booleans indicating whether or + not there is another valid transition from the last state in the sequence + for the associated sub-FSM (i.e. if the FSM can continue + accepting/matching) and whether or not the sequence ends in a final state + of the sub-FSM. + """ + state_seq_transitions = set(zip(state_seq[:-1], state_seq[1:])) + last_fsm_state = state_seq[-1] + yield from ( + ( + # The sub-FMS index + fsm_idx, + # Is there another possible transition in this sub-FSM? + any(last_fsm_state == from_s for (from_s, to_s) in transitions), + # Is this sub-FSM in a final state? + state_seq[-1] in finals, + ) + for fsm_idx, (transitions, finals, _) in fsms_to_trans_finals.items() + if state_seq_transitions.issubset(transitions) + ) + + +def walk_fsm( + fsm: BetterFSM, + token_transition_keys: Sequence[int], + start_state: int, + full_match: bool = True, +) -> List[int]: + fsm_finals = fsm.finals + + state = start_state + accepted_states: List[int] = [] + last_final_idx: int = 0 + + fsm_transitions = fsm.flat_transition_map + + # Iterate over token transition key sequence. The transition key + # sequence represents the FSM traversal rules of the tokens symbols. + for i, trans_key in enumerate(token_transition_keys): + new_state = fsm_transitions.get((state, trans_key)) + + if new_state is None: + if not full_match and last_final_idx > 0: + return accepted_states[:last_final_idx] + + return [] + + state = new_state + + if state in fsm_finals: + last_final_idx = i + 1 + + accepted_states.append(state) + + if full_match and last_final_idx - 1 != i: + return [] + + return accepted_states From eabca69013298145acd09dcda2eb4fc2d58d9c2d Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Thu, 10 Oct 2024 15:03:16 -0400 Subject: [PATCH 094/505] update dependencies: torch is required, pin outlines-core==0.1.14 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ac94ecf57..fa7005afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,8 @@ dependencies = [ "typing_extensions", "pycountry", "airportsdata", - "outlines_core==0.1.0", + "torch", + "outlines_core==0.1.14", ] dynamic = ["version"] @@ -61,7 +62,6 @@ test = [ "huggingface_hub", "openai>=1.0.0", "vllm; sys_platform != 'darwin'", - "torch", "transformers", "pillow", "exllamav2", From 6f36b71e995cec4ff17d6d4d4fca28dcf5122cef Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Fri, 11 Oct 2024 10:13:40 -0400 Subject: [PATCH 095/505] update RegexGuide to conform with outlines-core --- benchmarks/bench_json_schema.py | 2 +- benchmarks/bench_regex_guide.py | 4 +-- outlines/fsm/guide.py | 26 +++++++++++-------- outlines/processors/structured.py | 2 +- tests/fsm/test_guide.py | 10 +++---- tests/generate/test_integration_llamacpp.py | 4 +-- .../generate/test_integration_transformers.py | 4 +-- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/benchmarks/bench_json_schema.py b/benchmarks/bench_json_schema.py index 8990b015c..62d9b3c1d 100644 --- a/benchmarks/bench_json_schema.py +++ b/benchmarks/bench_json_schema.py @@ -77,4 +77,4 @@ def time_json_schema_to_regex(self, schema_name): @cache_disabled() def time_json_schema_to_fsm(self, schema_name): regex = build_regex_from_schema(self.schema) - RegexGuide(regex, self.tokenizer) + RegexGuide.from_regex(regex, self.tokenizer) diff --git a/benchmarks/bench_regex_guide.py b/benchmarks/bench_regex_guide.py index 7aaef6bac..fa23a724f 100644 --- a/benchmarks/bench_regex_guide.py +++ b/benchmarks/bench_regex_guide.py @@ -25,7 +25,7 @@ def setup(self, pattern_name): @cache_disabled() def time_regex_to_guide(self, pattern_name): - RegexGuide(self.pattern, self.tokenizer) + RegexGuide.from_regex(self.pattern, self.tokenizer) class MemoryRegexGuideBenchmark: @@ -37,4 +37,4 @@ def setup(self, pattern_name): @cache_disabled() def peakmem_regex_to_guide(self, pattern_name): - RegexGuide(self.pattern, self.tokenizer) + RegexGuide.from_regex(self.pattern, self.tokenizer) diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index 697597234..d46228fe9 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -74,8 +74,8 @@ def copy(self): @cache() -def create_states_mapping(regex_string, tokenizer): - return uncached_create_states_mapping(regex_string, tokenizer) +def cached_create_states_mapping(regex_string, tokenizer, *args, **kwargs): + return uncached_create_states_mapping(regex_string, tokenizer, *args, **kwargs) class RegexGuide(CoreRegexGuide): @@ -84,15 +84,19 @@ class RegexGuide(CoreRegexGuide): CoreRegexGuide with outlines cache """ - def __init__(self, regex_string: str, tokenizer: "Tokenizer"): - ( - self.states_to_token_maps, - self.empty_token_ids, - fsm_finals, - ) = create_states_mapping(regex_string, tokenizer) - self.eos_token_id = tokenizer.eos_token_id - self.final_states = fsm_finals | {-1} - self._cache_state_to_token_tensor() + @classmethod + def from_regex( + cls, + regex_string: str, + tokenizer, + **kwargs, + ): + return super().from_regex( + regex_string, + tokenizer, + _create_states_mapping=cached_create_states_mapping, + **kwargs, + ) CFGState = collections.namedtuple("CFGState", ["parser_state", "prev_token"]) diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index e3b9e60d3..d2bc15f77 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -149,7 +149,7 @@ def __init__(self, regex_string: str, tokenizer: "Tokenizer"): tokenizer An Outlines tokenizer """ - guide = RegexGuide(regex_string, tokenizer) + guide = RegexGuide.from_regex(regex_string, tokenizer) super().__init__(tokenizer=tokenizer, guide=guide) diff --git a/tests/fsm/test_guide.py b/tests/fsm/test_guide.py index 67b4e0dd8..510faf4b0 100644 --- a/tests/fsm/test_guide.py +++ b/tests/fsm/test_guide.py @@ -43,7 +43,7 @@ def convert_token_to_string(self, token): regex_str = "[1-9]" with pytest.raises(ValueError, match="The vocabulary"): - RegexGuide(regex_str, MockTokenizer()) + RegexGuide.from_regex(regex_str, MockTokenizer()) def test_regex(): @@ -57,7 +57,7 @@ def convert_token_to_string(self, token): regex_str = "[1-9]" tokenizer = MockTokenizer() - fsm = RegexGuide(regex_str, tokenizer) + fsm = RegexGuide.from_regex(regex_str, tokenizer) assert fsm.states_to_token_maps == {0: {1: 1}} @@ -98,7 +98,7 @@ def convert_token_to_string(self, token): regex_str = "[😁-😎]" tokenizer = MockTokenizer() - fsm = RegexGuide(regex_str, tokenizer) + fsm = RegexGuide.from_regex(regex_str, tokenizer) assert fsm.states_to_token_maps == { 0: {5: 1, 4: 2}, @@ -145,7 +145,7 @@ def convert_token_to_string(self, token): regex_str = " [😁-😎]" tokenizer = MockTokenizer() - fsm = RegexGuide(regex_str, tokenizer) + fsm = RegexGuide.from_regex(regex_str, tokenizer) assert fsm.states_to_token_maps == { 0: {5: 1, 10: 2}, @@ -180,7 +180,7 @@ def convert_token_to_string(self, token): regex_str = r"`\n(\.\n)?`\n" tokenizer = MockTokenizer() - fsm = RegexGuide(regex_str, tokenizer) + fsm = RegexGuide.from_regex(regex_str, tokenizer) state = fsm.get_next_state(state=4, token_id=103) assert state == 5 diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py index 08521c672..8d4596d60 100644 --- a/tests/generate/test_integration_llamacpp.py +++ b/tests/generate/test_integration_llamacpp.py @@ -278,7 +278,7 @@ def test_RegexGuide_caching(model, temp_cache_dir): import llama_cpp import outlines.caching - from outlines.fsm.guide import create_states_mapping + from outlines.fsm.guide import cached_create_states_mapping assert outlines.caching._caching_enabled @@ -291,7 +291,7 @@ def test_RegexGuide_caching(model, temp_cache_dir): _ = cache.stats(enable=True) assert cache.statistics - assert create_states_mapping.__memory__ is cache + assert cached_create_states_mapping.__memory__ is cache generator = generate.regex(model, regex, sampler=samplers.greedy()) assert cache.stats() == (0, 1) diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index 1d26a9ee4..2462d9fcf 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -494,7 +494,7 @@ def test_transformers_use_existing_model_and_tokenizer(): def test_RegexGuide_caching(temp_cache_dir): import outlines.caching - from outlines.fsm.guide import create_states_mapping + from outlines.fsm.guide import cached_create_states_mapping assert outlines.caching._caching_enabled @@ -507,7 +507,7 @@ def test_RegexGuide_caching(temp_cache_dir): _ = cache.stats(enable=True) assert cache.statistics - assert create_states_mapping.__memory__ is cache + assert cached_create_states_mapping.__memory__ is cache model = models.transformers( "hf-internal-testing/tiny-random-XLMRobertaXLForCausalLM", device="cpu" From 969887ee3b626380921fa0cb8f6360cb11ff3ed9 Mon Sep 17 00:00:00 2001 From: Andrew Lapp Date: Mon, 14 Oct 2024 13:03:26 -0400 Subject: [PATCH 096/505] test fsm_union and walk_fsm --- tests/fsm/test_parsing.py | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/fsm/test_parsing.py b/tests/fsm/test_parsing.py index 3f4c1ba42..b7446fa0c 100644 --- a/tests/fsm/test_parsing.py +++ b/tests/fsm/test_parsing.py @@ -204,3 +204,104 @@ def test_sequential_parse_example(cleanup_lark_import): if i + 1 == len(input_tokens): assert all(tk in next_vocab for tk in ["\n", "\nde", " ", " + 1"]) + + +# TODO: Remove once fsm_union and walk_fsm are implemented in Outlines-Core +import interegular # noqa + +from outlines.fsm.parsing import fsm_union, walk_fsm # noqa + + +def test_outlines_interegular_union_consistency(): + fsm0 = interegular.parse_pattern(r"abc").to_fsm() + fsm1 = interegular.parse_pattern(r"WXYZ").to_fsm() + fsm2 = interegular.parse_pattern(r"12345").to_fsm() + + interegular_unioned_fsm = fsm0 | fsm1 | fsm2 + outlines_unioned_fsm, _ = fsm_union([fsm0, fsm1, fsm2]) + + assert list(outlines_unioned_fsm.strings()) == list( + interegular_unioned_fsm.strings() + ) + + +def _reconstruct_fsms(fsm, fsms_to_trans_finals): + """Reconstruct the original fsms for testing purposes""" + reconstructed_fsms = [] + for transitions, finals, state_map in fsms_to_trans_finals.values(): + inv_state_map = {new: orig for orig, news in state_map.items() for new in news} + states = set(inv_state_map.values()) + initial = inv_state_map.get(fsm.initial) or next( + (orig for orig, news in state_map.items() if fsm.initial in news), None + ) + finals = {inv_state_map[s] for s in finals} + + transition_map = {} + alphabet = {} + for trans_id, (from_state, to_state) in enumerate(transitions): + orig_from, orig_to = inv_state_map[from_state], inv_state_map[to_state] + # Collect symbols associated with the transition + symbols = { + symbol + for trans, dest in fsm.map.get(from_state, {}).items() + if dest == to_state + for symbol in fsm.alphabet.by_transition.get(trans, []) + } + if symbols: + # NOTE: THIS RECONSTRUCTOR DOESNT WORK FOR MORE THAN ONE TRANSITION PER SYMBOL + assert len(symbols) == 1 + symbol = list(symbols)[0] + alphabet[symbol] = trans_id + transition_map.setdefault(orig_from, {})[trans_id] = orig_to + + reconstructed_fsms.append( + interegular.fsm.FSM( + alphabet=interegular.fsm.Alphabet(alphabet), + states=frozenset(states), + initial=initial, + finals=frozenset(finals), + map=transition_map, + __no_validation__=True, + ) + ) + return reconstructed_fsms + + +def test_fsm_to_trans_finals_reconstruction(): + """Assert that _fsms_to_trans_finals is correct by reconstructing original fsms""" + fsm0 = interegular.parse_pattern(r"abc").to_fsm() + fsm1 = interegular.parse_pattern(r"XYZ").to_fsm() + fsm2 = interegular.parse_pattern(r"12345").to_fsm() + + fsm, _fsms_to_trans_finals = fsm_union([fsm0, fsm1, fsm2]) + + reconstructed = _reconstruct_fsms(fsm, _fsms_to_trans_finals) + + # assert reconstruction equivalent + assert list(fsm0.strings()) == list(reconstructed[0].strings()) + assert list(fsm1.strings()) == list(reconstructed[1].strings()) + assert list(fsm2.strings()) == list(reconstructed[2].strings()) + + +def test_walk_fsm(): + fsm = interegular.parse_pattern(r"abc*d").to_fsm() + # convert to BetterFSM + fsm = fsm_union([fsm])[0] + + # if match, produce equivalent number of states, assert state can terminate + transitions = [fsm.alphabet[letter] for letter in "abcccd"] + accepted_states = walk_fsm(fsm, transitions, fsm.initial, full_match=True) + assert len(accepted_states) == len(transitions) + assert accepted_states[-1] in fsm.finals + + # if no match, assert empty + accepted_states = walk_fsm( + fsm, [fsm.alphabet[letter] for letter in "b"], fsm.initial, full_match=True + ) + assert accepted_states == [] + + # if full_match, but last state not present, assert empty + accepted_states = walk_fsm( + fsm, [fsm.alphabet[letter] for letter in "abc"], fsm.initial, full_match=True + ) + assert accepted_states == [] From 6cff654ad7bb9b1fcb29b244c52987c1e29b782a Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Fri, 18 Oct 2024 03:43:54 -0700 Subject: [PATCH 097/505] Fix logo color and doc links (#1213) - Will's structured generation workflow cookbook example was not in the mkdocs index, so it was not being displayed. - Same with the LM Studio serving docs. - The brand color was also slightly off: ![image](https://github.com/user-attachments/assets/fd10fa4f-d140-4936-befa-4dcca09c0e51) It has been fixed to this: ![image](https://github.com/user-attachments/assets/b6c2d71b-6a7f-4b86-935a-bf5072f1d945) --- docs/stylesheets/extra.css | 2 +- mkdocs.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 7c5b5e808..33783c153 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -9,7 +9,7 @@ --md-code-fg-color: #FFFFFF; --md-text-font-family: "Inter"; --md-code-font: "Source Code Pro Custom"; - --md-typeset-a-color: #d53135; /*this is the brand color*/ + --md-typeset-a-color: #d16626; /*this is the brand color*/ /* don't inherit white fg color for mermaid diagrams from --md-code-fg-color */ --md-mermaid-label-fg-color: #000000; diff --git a/mkdocs.yml b/mkdocs.yml index c0494c6ea..83106c0df 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -116,6 +116,7 @@ nav: - Perspective-taking prompting: cookbook/simtom.md - Question-answering with citations: cookbook/qa-with-citations.md - Knowledge Graph Extraction: cookbook/knowledge_graph_extraction.md + - Structured Generation Workflow: cookbook/structured_generation_workflow.md - Chain of Thought (CoT): cookbook/chain_of_thought.md - ReAct Agent: cookbook/react_agent.md - Run on the cloud: @@ -139,6 +140,7 @@ nav: - Custom FSM operations: reference/generation/custom_fsm_ops.md - Utilities: - Serve with vLLM: reference/serve/vllm.md + - Serve with LM Studio: reference/serve/lmstudio.md - Custom types: reference/generation/types.md - Prompt templating: reference/prompting.md - Outlines functions: reference/functions.md From a2fa1e0f44e80dd287c215fffde342578e67b1d1 Mon Sep 17 00:00:00 2001 From: jphillips <120260158+fearnworks@users.noreply.github.com> Date: Mon, 21 Oct 2024 05:59:29 -0500 Subject: [PATCH 098/505] Add transformers vision cookbook with atomic caption flow (#1216) Request received in discord to add an example for the new transformers vision capability. # Vision-Language Models with Outlines This guide demonstrates how to use Outlines with vision-language models, leveraging the new transformers_vision module. Vision-language models can process both text and images, allowing for tasks like image captioning, visual question answering, and more. We will be using the Pixtral-12B model from Mistral to take advantage of some of its visual reasoning capabilities and a workflow to generate a multistage atomic caption. --------- Signed-off-by: jphillips --- docs/cookbook/atomic_caption.md | 189 ++++++++++++++++++++++++++++++++ docs/cookbook/index.md | 1 + mkdocs.yml | 1 + 3 files changed, 191 insertions(+) create mode 100644 docs/cookbook/atomic_caption.md diff --git a/docs/cookbook/atomic_caption.md b/docs/cookbook/atomic_caption.md new file mode 100644 index 000000000..a2b43e5e2 --- /dev/null +++ b/docs/cookbook/atomic_caption.md @@ -0,0 +1,189 @@ +# Vision-Language Models with Outlines +This guide demonstrates how to use Outlines with vision-language models, leveraging the new transformers_vision module. Vision-language models can process both text and images, allowing for tasks like image captioning, visual question answering, and more. + +We will be using the Pixtral-12B model from Mistral to take advantage of some of its visual reasoning capabilities and a workflow to generate a multistage atomic caption. + +## Setup +First, we need to install the necessary dependencies. In addition to Outlines, we'll need to install the transformers library and any specific requirements for the vision-language model we'll be using. + +```bash +pip install outlines transformers torch +``` + +### Initializing the Model +We'll use the transformers_vision function to initialize our vision-language model. This function is specifically designed to handle models that can process both text and image inputs. Today we'll be using the Pixtral model with the llama tokenizer. (Currently the mistral tokenizer is pending support). + +```python +import torch +from transformers import ( + LlavaForConditionalGeneration, +) +model_name="mistral-community/pixtral-12b" # original magnet model is able to be loaded without issue +model_class=LlavaForConditionalGeneration + +def get_vision_model(model_name: str, model_class: VisionModel): + model_kwargs = { + "torch_dtype": torch.bfloat16, + "attn_implementation": "flash_attention_2", + "device_map": "auto", + } + processor_kwargs = { + "device": "cuda", + } + + model = outlines.models.transformers_vision( + model.model_name, + model_class=model.model_class, + model_kwargs=model_kwargs, + processor_kwargs=processor_kwargs, + ) + return model +model = get_vision_model(model_name, model_class) +``` + +### Defining the Schema +Next, we'll define a schema for the output we expect from our vision-language model. This schema will help structure the model's responses. + + +```python +from pydantic import BaseModel, Field, confloat, constr +from pydantic.types import StringConstraints, PositiveFloat +from typing import List +from typing_extensions import Annotated + +from enum import StrEnum +class TagType(StrEnum): + ENTITY = "Entity" + RELATIONSHIP = "Relationship" + STYLE = "Style" + ATTRIBUTE = "Attribute" + COMPOSITION = "Composition" + CONTEXTUAL = "Contextual" + TECHNICAL = "Technical" + SEMANTIC = "Semantic" + +class ImageTag(BaseModel): + tag: Annotated[ + constr(min_length=1, max_length=30), + Field( + description=( + "Descriptive keyword or phrase representing the tag." + ) + ) + ] + category: TagType + confidence: Annotated[ + confloat(le=1.0), + Field( + description=( + "Confidence score for the tag, between 0 (exclusive) and 1 (inclusive)." + ) + ) + ] + +class ImageData(BaseModel): + tags_list: List[ImageTag] = Field(..., min_items=8, max_items=20) + short_caption: Annotated[str, StringConstraints(min_length=10, max_length=150)] + dense_caption: Annotated[str, StringConstraints(min_length=100, max_length=2048)] + +image_data_generator = outlines.generate.json(model, ImageData) +``` + +This schema defines the structure for image tags, including categories like Entity, Relationship, Style, etc., as well as short and dense captions. + +### Preparing the Prompt + +We'll create a prompt that instructs the model on how to analyze the image and generate the structured output: + + +```python +pixtral_instruction = """ +[INST] +You are a structured image analysis agent. Generate comprehensive tag list, caption, and dense caption for an image classification system. + +- Entity : The content of the image, including the objects, people, and other elements. +- Relationship : The relationships between the entities in the image. +- Style : The style of the image, including the color, lighting, and other stylistic elements. +- Attribute : The most important attributes of the entities and relationships in the image. +- Composition : The composition of the image, including the arrangement of elements. +- Contextual : The contextual elements of the image, including the background, foreground, and other elements. +- Technical : The technical elements of the image, including the camera angle, lighting, and other technical details. +- Semantic : The semantic elements of the image, including the meaning of the image, the symbols, and other semantic details. + +{ + "tags_list": [ + { + "tag": "subject 1", + "category": "Entity", + "confidence": 0.98 + }, + { + "tag": "subject 2", + "category": "Entity", + "confidence": 0.95 + }, + { + "tag": "subject 1 runs from subject 2", + "category": "Relationship", + "confidence": 0.90 + }, + } + + + +\n[IMG][/INST] +""".strip() +``` + +This prompt provides detailed instructions to the model on how to generate comprehensive tag lists, captions, and dense captions for image analysis. Because of the ordering of the instructions the original tag generation serves as a sort of visual grounding for the captioning task, reducing the amount of manual post processing required. + +### Generating Structured Output +Now we can use our model to generate structured output based on an input image: + +```python +def img_from_url(url): + img_byte_stream = BytesIO(urlopen(url).read()) + return Image.open(img_byte_stream).convert("RGB") + +image_url="https://upload.wikimedia.org/wikipedia/commons/9/98/Aldrin_Apollo_11_original.jpg" +image= img_from_url(image_url) +result = image_data_generator( + pixtral_instruction, + [image] +) +print(result) +``` + +This code loads an image from a URL, passes it to our vision-language model along with the instruction prompt, and generates a structured output based on the defined schema. We end up with an output like this, ready to be used for the next stage in your pipeline: + +```json +{'tags_list': [{'tag': 'astronaut', + 'category': , + 'confidence': 0.99}, + {'tag': 'moon', 'category': , 'confidence': 0.98}, + {'tag': 'space suit', + 'category': , + 'confidence': 0.97}, + {'tag': 'lunar module', + 'category': , + 'confidence': 0.95}, + {'tag': 'shadow of astronaut', + 'category': , + 'confidence': 0.95}, + {'tag': 'footprints in moon dust', + 'category': , + 'confidence': 0.93}, + {'tag': 'low angle shot', + 'category': , + 'confidence': 0.92}, + {'tag': 'human first steps on the moon', + 'category': , + 'confidence': 0.95}], + 'short_caption': 'First man on the Moon', + 'dense_caption': "The figure clad in a pristine white space suit, emblazoned with the American flag, stands powerfully on the moon's desolate and rocky surface. The lunar module, a workhorse of space engineering, looms in the background, its metallic legs sinking slightly into the dust where footprints and tracks from the mission's journey are clearly visible. The photograph captures the astronaut from a low angle, emphasizing his imposing presence against the desolate lunar backdrop. The stark contrast between the blacks and whiteslicks of lost light and shadow adds dramatic depth to this seminal moment in human achievement."} +``` + +## Conclusion +The transformers_vision module in Outlines provides a powerful way to work with vision-language models. It allows for structured generation of outputs that combine image analysis with natural language processing, opening up possibilities for complex tasks like detailed image captioning, visual question answering, and more. + +By leveraging the capabilities of models like Pixtral-12B and the structured output generation of Outlines, you can create sophisticated applications that understand and describe visual content in a highly structured and customizable manner. diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index a844ce240..b163feb62 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -12,3 +12,4 @@ This part of the documentation provides a few cookbooks that you can browse to g - [Knowledge Graph Generation](knowledge_graph_extraction.md): Generate a Knowledge Graph from unstructured text using JSON-structured generation. - [Chain Of Thought (CoT)](chain_of_thought.md): Generate a series of intermediate reasoning steps using regex-structured generation. - [ReAct Agent](react_agent.md): Build an agent with open weights models using regex-structured generation. +- [Vision-Language Models](atomic_caption.md): Use Outlines with vision-language models for tasks like image captioning and visual reasoning. diff --git a/mkdocs.yml b/mkdocs.yml index 83106c0df..146a766a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -119,6 +119,7 @@ nav: - Structured Generation Workflow: cookbook/structured_generation_workflow.md - Chain of Thought (CoT): cookbook/chain_of_thought.md - ReAct Agent: cookbook/react_agent.md + - Vision-Language Models: cookbook/atomic_caption.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From dc31b9b4027e71bd69f213bd3ab7d98b2f0a1529 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 27 Oct 2024 22:31:21 +0900 Subject: [PATCH 099/505] Update llamacpp.md (#1231) accross -> across --- docs/reference/models/llamacpp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/models/llamacpp.md b/docs/reference/models/llamacpp.md index 0ab3dd83b..24b0fdc97 100644 --- a/docs/reference/models/llamacpp.md +++ b/docs/reference/models/llamacpp.md @@ -47,7 +47,7 @@ model = models.llamacpp( | `n_gpu_layers`| `int` | Number of layers to offload to GPU. If -1, all layers are offloaded | `0` | | `split_mode` | `int` | How to split the model across GPUs. `1` for layer-wise split, `2` for row-wise split | `1` | | `main_gpu` | `int` | Main GPU | `0` | -| `tensor_split` | `Optional[List[float]]` | How split tensors should be distributed accross GPUs. If `None` the model is not split. | `None` | +| `tensor_split` | `Optional[List[float]]` | How split tensors should be distributed across GPUs. If `None` the model is not split. | `None` | | `n_ctx` | `int` | Text context. Inference from the model if set to `0` | `0` | | `n_threads` | `Optional[int]` | Number of threads to use for generation. All available threads if set to `None`.| `None` | | `verbose` | `bool` | Print verbose outputs to `stderr` | `False` | From 5f39ded4b872d60de563658f1d7a2642de0e8c4e Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 6 Nov 2024 00:54:59 -0800 Subject: [PATCH 100/505] Earnings report cookbook (#1235) This is a condensed version of the demo for [extracting earnings reports](https://github.com/dottxt-ai/demos/tree/main/earnings-reports) to CSV. Overview: - Shows how to use Outlines to structure CSV output - Provides simple tools for converting a table specification to regular expressions - Includes a tuned extraction prompt that performs reasonably well on income statements --- docs/cookbook/earnings-reports.md | 278 +++++++++++++++++++++++++ docs/cookbook/images/nvidia-income.png | Bin 0 -> 185563 bytes mkdocs.yml | 1 + 3 files changed, 279 insertions(+) create mode 100644 docs/cookbook/earnings-reports.md create mode 100644 docs/cookbook/images/nvidia-income.png diff --git a/docs/cookbook/earnings-reports.md b/docs/cookbook/earnings-reports.md new file mode 100644 index 000000000..26c2a9ccf --- /dev/null +++ b/docs/cookbook/earnings-reports.md @@ -0,0 +1,278 @@ +# Extracting financial data from earnings reports + +A common task in finance is to extract financial data from earnings reports. Earnings reports are infamously poorly formatted, as the SEC does not have requirements for producing machine-readable documents. + +Earnings reports are often provided as HTML documents, which can be difficult to parse. Investors often use complicated parsing systems or manual review to extract data. Entire companies are built around automating this task. + +This cookbook is a proof of concept about how we can use LLMs to extract financial data directly into CSV. Comma-separated values are well-structured and can be defined by a regular expression, which Outlines can use to guide the LLM's output. + +The example is a smaller subset of a full demo found [here](https://github.com/dottxt-ai/demos/tree/main/earnings-reports). The demo contains the full set of pre-processing steps needed to convert raw HTML into a structured CSV file, and tests the results across three company's 10k reports. + +## Setup + +Install outlines and required dependencies: + +```bash +# Later versions of torch can have difficulty with certain CUDA drivers. +# We recommend using 2.4.0 for now, but you may wish to experiment with +# other versions. +pip install outlines pandas transformers torch==2.4.0 accelerate +``` + +## Load the model + +Choose your language model. We'll use Phi-3 mini, which is small enough to run on reasonably small machines. + +```python +import outlines +import torch + +model_name = 'microsoft/Phi-3-mini-4k-instruct' +model = outlines.models.transformers( + model_name, + device='auto', + model_kwargs={ + # To reduce memory usage, we'll use bfloat16 + "torch_dtype": torch.bfloat16, + }, +) +``` + +## Set up the data + +For brevity, we've attached the markdown version of Nvidia's 10k report. The [full demonstration](https://github.com/dottxt-ai/demos/tree/main/earnings-reports) processes the raw HTML version of the report to these markdown tables. Pages are filtered by whether they seem to contain income statements, and then compacted into the string you see below. + +```python +income_statement = """ +Table of ContentsNVIDIA Corporation and SubsidiariesConsolidated Statements of Income(In millions, except per share data) + +| | | | | | | | | | | | | | | | | | | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| | | | Year Ended | | | | | | | | | | | | | | | +| | | | Jan 28, 2024 | | | | | | Jan 29, 2023 | | | | | | Jan 30, 2022 | | | +| Revenue | | | $ | 60,922 | | | | | $ | 26,974 | | | | | $ | 26,914 | | +| Cost of revenue | | | 16,621 | | | | | | 11,618 | | | | | | 9,439 | | | +| Gross profit | | | 44,301 | | | | | | 15,356 | | | | | | 17,475 | | | +| Operating expenses | | | | | | | | | | | | | | | | | | +| Research and development | | | 8,675 | | | | | | 7,339 | | | | | | 5,268 | | | +| Sales, general and administrative | | | 2,654 | | | | | | 2,440 | | | | | | 2,166 | | | +| Acquisition termination cost | | | — | | | | | | 1,353 | | | | | | — | | | +| Total operating expenses | | | 11,329 | | | | | | 11,132 | | | | | | 7,434 | | | +| Operating income | | | 32,972 | | | | | | 4,224 | | | | | | 10,041 | | | +| Interest income | | | 866 | | | | | | 267 | | | | | | 29 | | | +| Interest expense | | | (257) | | | | | | (262) | | | | | | (236) | | | +| Other, net | | | 237 | | | | | | (48) | | | | | | 107 | | | +| Other income (expense), net | | | 846 | | | | | | (43) | | | | | | (100) | | | +| Income before income tax | | | 33,818 | | | | | | 4,181 | | | | | | 9,941 | | | +| Income tax expense (benefit) | | | 4,058 | | | | | | (187) | | | | | | 189 | | | +| Net income | | | $ | 29,760 | | | | | $ | 4,368 | | | | | $ | 9,752 | | +| | | | | | | | | | | | | | | | | | | +| Net income per share: | | | | | | | | | | | | | | | | | | +| Basic | | | $ | 12\.05 | | | | | $ | 1\.76 | | | | | $ | 3\.91 | | +| Diluted | | | $ | 11\.93 | | | | | $ | 1\.74 | | | | | $ | 3\.85 | | +| | | | | | | | | | | | | | | | | | | +| Weighted average shares used in per share computation: | | | | | | | | | | | | | | | | | | +| Basic | | | 2,469 | | | | | | 2,487 | | | | | | 2,496 | | | +| Diluted | | | 2,494 | | | | | | 2,507 | | | | | | 2,535 | | | +""" +``` + +The markdown tables extracted from the earnings reports can vary widely in row names, column counts, data types, etc. The advantage of LLMs here is that we can define the data we want in terms of the data types, and the LLM will output the data in the desired format. + +For comparison, here is how the income statement looks in the original HTML: + +![Nvidia income statement](./images/nvidia-income.png) + +## Define the data we want + +Outlines is often used for JSON output, but it can also be used for CSV. We know the columns we want to extract, and we know the data types of the columns. Year for example is always a four-digit number, revenue is a number with commas, and so on. + +We can define a regex pattern for each column type: + +```python +# Define the column type regex patterns +column_types = { + # Year is always a four-digit number + "year": r"\d{4}", + + # Revenue, operating income, and net income are always numbers with commas. + # This regex permits integers that may begin with a minus sign, and may have + # commas separating the thousands, millions, etc. + "integer_comma": r"((-?\d+),?\d+|(-?\d+))", + # Number is currently not used, but it represents a number with up to two decimal places. + "number": r"(-?\d+(?:\.\d{1,2})?)", +} +``` + +Next, let's choose the columns we want to extract. We want + +- Year, always a four-digit number +- Revenue, a number with commas +- Operating income, a number with commas +- Net income, a number with commas + +```python +# Define the columns to extract, and their data types. +columns_to_extract = { + "year": "year", + "revenue": "integer_comma", + "operating_income": "integer_comma", + "net_income": "integer_comma", +} +``` + +You can modify `column_type_regex` to match the data types of the columns you want to extract. Adding a new financial metric to extract is as simple as adding a new key/value pair to `columns_to_extract`: + +```python +columns_to_extract["diluted_earnings_per_share"] = "number" +``` + +Additional columns are not well tested for accuracy, so use with caution. + +## Create the regex describing the data we want + + +```python +# Create the header line. This is the requested column names +# separated by commas, i.e. "year,revenue,..." +header = ",".join(columns_to_extract.keys()) + +# Create the data capture patterns. These are the regex patterns +# that will be used to capture the data in each column +data_patterns = [column_types[dtype] for dtype in columns_to_extract.values()] +data_line = ",".join(data_patterns) + +# Our final regex pattern. +max_rows = 3 # We expect 3 rows of data, firms usually report 3 years of income statements +csv_regex = f"{header}(\n{data_line}){{,{max_rows}}}\n\n" + +print(csv_regex) +``` + +which gives us + +``` +year,revenue,operating_income,net_income,basic_earnings_per_share( +\d{4},((-?\d+),?\d+|(-?\d+)),((-?\d+),?\d+|(-?\d+)),((-?\d+),?\d+|(-?\d+)),(-?\d+(?:\.\d{1,2})?)){,3} +``` + +Pretty hairy, right? Thankfully, we have a simple function to construct this regex for you. The regex defines a header line, followed by a data line that repeats for each row of data we want to extract. Passing the regex to `outlines.generate.regex` will produce a function that will __always__ produce a CSV string that is consistent with the regex. + +## Prompting the model + +Outlines does not add system or instruction tokens by default, so we need to use `transformers.AutoTokenizer` to add them for whatever model we're using. + +```python +from transformers import AutoTokenizer + +tokenizer = AutoTokenizer.from_pretrained(model_name) + +def add_instruction(prompt): + return tokenizer.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True) + +print(add_instruction("Howdy")) +``` +``` +<|user|> +Howdy<|end|> +<|assistant|> +``` + +Our prompt roughly describes the task we want the model to perform, and a few pieces of information it may need to know about income statements. + +```python +def extract_financial_data_prompt(columns_to_extract, income_statement): + user_prompt = f""" + Extract annual financial data from this set of pages. Pages + are from a 10k filing and were chosen because they may contain + a comprehensive income statement. Note that selected pages may + be incorrectly extracted, so you should verify that you are extracting + from the comprehensive income statement and not some other financial + statement. + + Create a row for each year available in the income statement with the + following columns: {', '.join(columns_to_extract.keys())}. Firms typically report the + most recent 3 years of data, but this can vary. + + Each column has types: {', '.join(columns_to_extract.values())}. + + # Relevant pages: + + {income_statement} + + # Key instructions: + + 1. Look ONLY at the "Consolidated Statements of Income" table + 2. For operating income, look for "Income from operations" or "Operating income" + 3. For net income, use the TOTAL net income figure, not amounts allocated to specific share classes + 4. Use NULL for missing values + 5. Operating income must be less than revenue + 6. Net income must be less than operating income + 7. Ignore segment breakdowns, quarterly data, or per-share amounts + + # Output format: + + - CSV format with headers: {','.join(columns_to_extract.keys())} + - Use NULL for missing values + - If no data are found, do not create a row. + - Enter two newline characters to terminate the CSV when no more data are found. + + # Definitions: + - Revenue: Total sales of goods and services. Usually this is at the top of the + income statement. + - Operating income: Revenue minus operating expenses for the entire company. This is revenue + minus costs. Operating income is also called operating profit, EBIT, or income from + operations. + - Net income: Operating income minus taxes. This is the bottom line of the + income statement. + """ + + return add_instruction(user_prompt) +``` + +## Running the model + +Now that we have our prompt and regular expression, we can run the model. + +Construct our regex extractor function. We'll use a greedy sampler, which samples the most likely next token at each step. It's a simple sampler that is more reproducible than multinomial sampling. + +```python +csv_extractor = outlines.generate.regex( + model, csv_regex, sampler=outlines.samplers.greedy() +) +``` + +Provide the prompt to the model and run it: + +```python +csv_data = csv_extractor( + extract_financial_data_prompt(columns_to_extract, income_statement), + max_tokens=1024, +) + +print(csv_data) +``` +``` +year,revenue,operating_income,net_income +2024,60922,32972,29760 +2023,26974,4224,4368 +2022,26914,10041,9752 +``` + +Voila! We've extracted the financial data from the income statement, and it's correct upon inspection. + +You can even load this into a `pandas` DataFrame for further analysis: + +```python +import pandas as pd +from io import StringIO + +df = pd.read_csv(StringIO(csv_data)) +print(df) +``` +``` + year revenue operating_income net_income +0 2024 60922 32972 29760 +1 2023 26974 4224 4368 +2 2022 26914 10041 9752 +``` diff --git a/docs/cookbook/images/nvidia-income.png b/docs/cookbook/images/nvidia-income.png new file mode 100644 index 0000000000000000000000000000000000000000..9727bc272fd7457b07f162c81dff8e987278bb48 GIT binary patch literal 185563 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 z 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 bT95Dcu5OxQcl5HtKjTMFwmds>{>J|W@-@wt literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 146a766a2..b97a2e41e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -120,6 +120,7 @@ nav: - Chain of Thought (CoT): cookbook/chain_of_thought.md - ReAct Agent: cookbook/react_agent.md - Vision-Language Models: cookbook/atomic_caption.md + - Earnings reports to CSV: cookbook/earnings-reports.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From 906e84e061e6f4027522f57efa1c825f36a53209 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Fri, 8 Nov 2024 09:15:26 -0800 Subject: [PATCH 101/505] Add PDF cookbook (#1256) Adds a cookbook on extracting structured output from PDFs. I included some extra bells and whistles here by showing how to do JSON, regex, and `choice`, which should help provide inspiration to people working with PDFs. --- docs/cookbook/index.md | 1 + docs/cookbook/read-pdfs.md | 376 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 378 insertions(+) create mode 100644 docs/cookbook/read-pdfs.md diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index b163feb62..97bf06687 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -13,3 +13,4 @@ This part of the documentation provides a few cookbooks that you can browse to g - [Chain Of Thought (CoT)](chain_of_thought.md): Generate a series of intermediate reasoning steps using regex-structured generation. - [ReAct Agent](react_agent.md): Build an agent with open weights models using regex-structured generation. - [Vision-Language Models](atomic_caption.md): Use Outlines with vision-language models for tasks like image captioning and visual reasoning. +- [Structured Generation from PDFs](read-pdfs.md): Use Outlines with vision-language models to read PDFs and produce structured output. diff --git a/docs/cookbook/read-pdfs.md b/docs/cookbook/read-pdfs.md new file mode 100644 index 000000000..dbe4ccb02 --- /dev/null +++ b/docs/cookbook/read-pdfs.md @@ -0,0 +1,376 @@ +# PDF to structured output with vision language models + +A common task with language models is to ask language models questions about a PDF file. + +Typically, the output is unstructured text, i.e. "talking" to your PDF. + +In some cases, you may wish to extract structured information from the PDF, like tables, lists, citations, etc. + +PDFs are difficult to machine read. However, you can simply convert the PDF to images, and then use a vision language model to extract structured information from the images. + +This cookbook demonstrates how to + +1. Convert a PDF to a list of images +2. Use a vision language model to extract structured information from the images + +## Dependencies + +You'll need to install these dependencies: + +```bash +pip install outlines pillow transformers torch==2.4.0 pdf2image + +# Optional, but makes the output look nicer +pip install rich +``` + +## Import the necessary libraries + +```python +from PIL import Image +import outlines +import torch +from transformers import AutoProcessor +from pydantic import BaseModel +from typing import List, Optional +from pdf2image import convert_from_path +import os +from rich import print +import requests +``` + +## Choose a model + +We've tested this example with [Pixtral 12b](https://huggingface.co/mistral-community/pixtral-12b) and [Qwen2-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2-VL-7B-Instruct). + +To use Pixtral: + +```python +from transformers import LlavaForConditionalGeneration +model_name="mistral-community/pixtral-12b" +model_class=LlavaForConditionalGeneration +``` + +To use Qwen-2-VL: + +```python +from transformers import Qwen2VLForConditionalGeneration +model_name = "Qwen/Qwen2-VL-7B-Instruct" +model_class = Qwen2VLForConditionalGeneration +``` + +You can load your model into memory with: + +```python +# This loads the model into memory. On your first run, +# it will have to download the model, which might take a while. +model = outlines.models.transformers_vision( + model_name, + model_class=model_class, + model_kwargs={ + "device_map": "auto", + "torch_dtype": torch.bfloat16, + }, + processor_kwargs={ + "device": "auto", + }, +) +``` + +## Convert the PDF to images + +We'll use the `pdf2image` library to convert each page of the PDF to an image. + +`convert_pdf_to_images` is a convenience function that converts each page of the PDF to an image, and optionally saves the images to disk when `output_dir` is provided. + +Note: the `dpi` argument is important. It controls the resolution of the images. High DPI images are higher quality and may yield better results, +but they are also larger, slower to process, and require more memory. + +```python +from pdf2image import convert_from_path +from PIL import Image +import os +from typing import List, Optional + +def convert_pdf_to_images( + pdf_path: str, + output_dir: Optional[str] = None, + dpi: int = 120, + fmt: str = 'PNG' +) -> List[Image.Image]: + """ + Convert a PDF file to a list of PIL Image objects. + + Args: + pdf_path: Path to the PDF file + output_dir: Optional directory to save the images + dpi: Resolution for the conversion. High DPI is high quality, but also slow and memory intensive. + fmt: Output format (PNG recommended for quality) + + Returns: + List of PIL Image objects + """ + # Convert PDF to list of images + images = convert_from_path( + pdf_path, + dpi=dpi, + fmt=fmt + ) + + # Optionally save images + if output_dir: + os.makedirs(output_dir, exist_ok=True) + for i, image in enumerate(images): + image.save(os.path.join(output_dir, f'page_{i+1}.{fmt.lower()}')) + + return images +``` + +We're going to use the [Louf & Willard paper](https://arxiv.org/pdf/2307.09702) that described the method that Outlines uses for structured generation. + +To download the PDF, run: + +```python +# Download the PDF file +pdf_url = "https://arxiv.org/pdf/2307.09702" +response = requests.get(pdf_url) + +# Save the PDF locally +with open("louf-willard.pdf", "wb") as f: + f.write(response.content) +``` + +Now, we can convert the PDF to a list of images: + +```python +# Load the pdf +images = convert_pdf_to_images( + "louf-willard.pdf", + dpi=120, + output_dir="output_images" +) +``` + +## Extract structured information from the images + +The structured output you can extract is exactly the same as everywhere else in Outlines -- you can use regular expressions, JSON schemas, selecting from a list of options, etc. + +### Extracting data into JSON + +Suppose you wished to go through each page of the PDF, and extract the page description, key takeaways, and page number. + +You can do this by defining a JSON schema, and then using `outlines.generate.json` to extract the data. + +First, define the structure you want to extract: + +```python +class PageSummary(BaseModel): + description: str + key_takeaways: List[str] + page_number: int +``` + +Second, we need to set up the prompt. Adding special tokens can be tricky, so we use the transformers `AutoProcessor` to apply the special tokens for us. To do so, we specify a list of messages, where each message is a dictionary with a `role` and `content` key. + +Images are denoted with `type: "image"`, and text is denoted with `type: "text"`. + +```python +messages = [ + { + "role": "user", + "content": [ + # The text you're passing to the model -- + # this is where you do your standard prompting. + {"type": "text", "text": f""" + Describe the page in a way that is easy for a PhD student to understand. + + Return the information in the following JSON schema: + {PageSummary.model_json_schema()} + + Here is the page: + """ + }, + + # Don't need to pass in an image, since we do this + # when we call the generator function down below. + {"type": "image", "image": ""}, + ], + } +] + +# Convert the messages to the final prompt +processor = AutoProcessor.from_pretrained(model_name) +instruction = processor.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True +) +``` + +Now we iterate through each image, and extract the structured information: + +```python +# Page summarizer function +page_summary_generator = outlines.generate.json(model, PageSummary) + +for image in images: + result = page_summary_generator(instruction, [image]) + print(result) +``` + +### Regular expressions to extract the arxiv paper identifier + +The [arXiv paper identifier](https://info.arxiv.org/help/arxiv_identifier.html) is a unique identifier for each paper. These identifiers have the format `arXiv:YYMM.NNNNN` (five end digits) or `arXiv:YYMM.NNNN` (four end digits). arXiv identifiers are typically watermarked on papers uploaded to arXiv. + +arXiv identifiers are optionally followed by a version number, i.e. `arXiv:YYMM.NNNNNvX`. + +We can use a regular expression to define this patter: + +```python +paper_regex = r'arXiv:\d{2}[01]\d\.\d{4,5}(v\d)?' +``` + +We can build an extractor function from the regex: + +```python +id_extractor = outlines.generate.regex(model, paper_regex) +``` + +Now, we can extract the arxiv paper identifier from the first image: + +```python +arxiv_instruction = processor.apply_chat_template( + [ + { + "role": "user", + "content": [ + {"type": "text", "text": f""" + Extract the arxiv paper identifier from the page. + + Here is the page: + """}, + {"type": "image", "image": ""}, + ], + } + ], + tokenize=False, + add_generation_prompt=True +) + +# Extract the arxiv paper identifier +paper_id = id_extractor(arxiv_instruction, [images[0]]) +``` + +As of the time of this writing, the arxiv paper identifier is + +``` +arXiv:2307.09702v4 +``` + +Your version number may be different, but the part before `vX` should match. + +### Categorize the paper into one of several categories + +`outlines.generate.choice` allows the model to select one of several options. Suppose we wanted to categorize the paper into being about "language models", "economics", "structured generation", or "other". + +Let's define a few categories we might be interested in: + +```python +categories = [ + "llms", + "cell biology", + "other" +] +``` + +Now we can construct the prompt: + +```python +categorization_instruction = processor.apply_chat_template( + [ + { + "role": "user", + "content": [ + {"type": "text", "text": f""" + Please choose one of the following categories + that best describes the paper. + + {categories} + + Here is the paper: + """}, + + {"type": "image", "image": ""}, + ], + } + ], + tokenize=False, + add_generation_prompt=True +) +``` + +Now we can show the model the first page and extract the category: + +```python +# Build the choice extractor +categorizer = outlines.generate.choice( + model, + categories +) + +# Categorize the paper +category = categorizer(categorization_instruction, [images[0]]) +print(category) +``` + +Which should return: + +``` +llms +``` + +## Additional notes + +You can provide multiple images to the model by + +1. Adding additional image messages +2. Providing a list of images to the `generate` function + +For example, to have two images, you can do: + +```python +two_image_prompt = processor.apply_chat_template( + [ + { + "role": "user", + "content": [ + {"type": "text", "text": "are both of these images of hot dogs?"}, + + # Tell the model there are two images + {"type": "image", "image": ""}, + {"type": "image", "image": ""}, + ], + } + ], + tokenize=False, + add_generation_prompt=True +) + +# Pass two images to the model +generator = outlines.generate.choice( + model, + ["hot dog", "not hot dog"] +) + +result = generator( + two_image_prompt, + + # Pass two images to the model + [images[0], images[1]] +) +print(result) +``` + +Using the first to pages of the paper (they are not images of hot dogs), we should get + +``` +not hot dog +``` diff --git a/mkdocs.yml b/mkdocs.yml index b97a2e41e..7272e4a63 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -120,6 +120,7 @@ nav: - Chain of Thought (CoT): cookbook/chain_of_thought.md - ReAct Agent: cookbook/react_agent.md - Vision-Language Models: cookbook/atomic_caption.md + - Structured Generation from PDFs: cookbook/read-pdfs.md - Earnings reports to CSV: cookbook/earnings-reports.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md From b93f5503a11fcef07ae8c62eb0d403358fd6ad8e Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Fri, 8 Nov 2024 09:16:06 -0800 Subject: [PATCH 102/505] Add earnings reports to cookbook index (#1255) Forgot to add the earnings report cookbook to the cookbook index (#1235), this fixes it. --- docs/cookbook/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 97bf06687..6d279bff5 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -12,5 +12,6 @@ This part of the documentation provides a few cookbooks that you can browse to g - [Knowledge Graph Generation](knowledge_graph_extraction.md): Generate a Knowledge Graph from unstructured text using JSON-structured generation. - [Chain Of Thought (CoT)](chain_of_thought.md): Generate a series of intermediate reasoning steps using regex-structured generation. - [ReAct Agent](react_agent.md): Build an agent with open weights models using regex-structured generation. +- [Earnings reports to CSV](earnings-reports.md): Extract data from earnings reports to CSV using regex-structured generation. - [Vision-Language Models](atomic_caption.md): Use Outlines with vision-language models for tasks like image captioning and visual reasoning. - [Structured Generation from PDFs](read-pdfs.md): Use Outlines with vision-language models to read PDFs and produce structured output. From b5d26c4f7cc532cb725fc96b529b61e1b8811a46 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Fri, 8 Nov 2024 12:37:17 -0800 Subject: [PATCH 103/505] Add a receipt processing cookbook (#1249) I added a receipt processing cookbook. - Uses Qwen or Pixtral - General purpose message templating, no messy model-specific token adding - Easy function for compressing images down for lower processing/memory requirements Should help illustrate a simple use case for vision models. --- docs/cookbook/images/trader-joes-receipt.jpg | Bin 0 -> 430638 bytes docs/cookbook/index.md | 1 + docs/cookbook/receipt-digitization.md | 296 +++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 298 insertions(+) create mode 100644 docs/cookbook/images/trader-joes-receipt.jpg create mode 100644 docs/cookbook/receipt-digitization.md diff --git a/docs/cookbook/images/trader-joes-receipt.jpg b/docs/cookbook/images/trader-joes-receipt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6742d39462449d7264cbc2724668505f6ceef7f9 GIT binary patch literal 430638 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]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%0D$lt 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%0AZ76KHv0Nns&AtlXCkIq%K3m%{6O6 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 UC!$H{&tzX(PZ4?|^k1X@*;Pv=jsO4v literal 0 HcmV?d00001 diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 6d279bff5..c36b98969 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -14,4 +14,5 @@ This part of the documentation provides a few cookbooks that you can browse to g - [ReAct Agent](react_agent.md): Build an agent with open weights models using regex-structured generation. - [Earnings reports to CSV](earnings-reports.md): Extract data from earnings reports to CSV using regex-structured generation. - [Vision-Language Models](atomic_caption.md): Use Outlines with vision-language models for tasks like image captioning and visual reasoning. +- [Receipt Digitization](receipt-digitization.md): Extract information from a picture of a receipt using structured generation. - [Structured Generation from PDFs](read-pdfs.md): Use Outlines with vision-language models to read PDFs and produce structured output. diff --git a/docs/cookbook/receipt-digitization.md b/docs/cookbook/receipt-digitization.md new file mode 100644 index 000000000..67830fa81 --- /dev/null +++ b/docs/cookbook/receipt-digitization.md @@ -0,0 +1,296 @@ +# Receipt Data Extraction with VLMs + +## Setup + +You'll need to install the dependencies: + +```bash +pip install outlines torch==2.4.0 transformers accelerate pillow rich +``` + +## Import libraries + +Load all the necessary libraries: + +```python +# LLM stuff +import outlines +import torch +from transformers import AutoProcessor +from pydantic import BaseModel, Field +from typing import Literal, Optional, List + +# Image stuff +from PIL import Image +import requests + +# Rich for pretty printing +from rich import print +``` + +## Choose a model + +This example has been tested with `mistral-community/pixtral-12b` ([HF link](https://huggingface.co/mistral-community/pixtral-12b)) and `Qwen/Qwen2-VL-7B-Instruct` ([HF link](https://huggingface.co/Qwen/Qwen2-VL-7B-Instruct)). + +We recommend Qwen-2-VL as we have found it to be more accurate than Pixtral. + +If you want to use Qwen-2-VL, you can do the following: + +```python +# To use Qwen-2-VL: +from transformers import Qwen2VLForConditionalGeneration +model_name = "Qwen/Qwen2-VL-7B-Instruct" +model_class = Qwen2VLForConditionalGeneration +``` + +If you want to use Pixtral, you can do the following: + +```python +# To use Pixtral: +from transformers import LlavaForConditionalGeneration +model_name="mistral-community/pixtral-12b" +model_class=LlavaForConditionalGeneration +``` + +## Load the model + +Load the model into memory: + +```python +model = outlines.models.transformers_vision( + model_name, + model_class=model_class, + model_kwargs={ + "device_map": "auto", + "torch_dtype": torch.bfloat16, + }, + processor_kwargs={ + "device": "cuda", # set to "cpu" if you don't have a GPU + }, +) +``` + +## Image processing + +Images can be quite large. In GPU-poor environments, you may need to resize the image to a smaller size. + +Here's a helper function to do that: + +```python +def load_and_resize_image(image_path, max_size=1024): + """ + Load and resize an image while maintaining aspect ratio + + Args: + image_path: Path to the image file + max_size: Maximum dimension (width or height) of the output image + + Returns: + PIL Image: Resized image + """ + image = Image.open(image_path) + + # Get current dimensions + width, height = image.size + + # Calculate scaling factor + scale = min(max_size / width, max_size / height) + + # Only resize if image is larger than max_size + if scale < 1: + new_width = int(width * scale) + new_height = int(height * scale) + image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) + + return image +``` + +You can change the resolution of the image by changing the `max_size` argument. Small max sizes will make the image more blurry, but processing will be faster and require less memory. + +## Load an image + +Load an image and resize it. We've provided a sample image of a Trader Joe's receipt, but you can use any image you'd like. + +Here's what the image looks like: + +![Trader Joe's receipt](./images/trader-joes-receipt.jpg) + +```python +# Path to the image +image_path = "https://dottxt-ai.github.io/outlines/main/cookbook/images/trader-joes-receipt.png" + +# Download the image +response = requests.get(image_path) +with open("receipt.png", "wb") as f: + f.write(response.content) + +# Load + resize the image +image = load_and_resize_image("receipt.png") +``` + +## Define the output structure + +We'll define a Pydantic model to describe the data we want to extract from the image. + +In our case, we want to extract the following information: + +- The store name +- The store address +- The store number +- A list of items, including the name, quantity, price per unit, and total price +- The tax +- The total +- The date +- The payment method + +Most fields are optional, as not all receipts contain all information. + +```python +class Item(BaseModel): + name: str + quantity: Optional[int] + price_per_unit: Optional[float] + total_price: Optional[float] + +class ReceiptSummary(BaseModel): + store_name: str + store_address: str + store_number: Optional[int] + items: List[Item] + tax: Optional[float] + total: Optional[float] + # Date is in the format YYYY-MM-DD. We can apply a regex pattern to ensure it's formatted correctly. + date: Optional[str] = Field(pattern=r'\d{4}-\d{2}-\d{2}', description="Date in the format YYYY-MM-DD") + payment_method: Literal["cash", "credit", "debit", "check", "other"] +``` + +## Prepare the prompt + +We'll use the `AutoProcessor` to convert the image and the text prompt into a format that the model can understand. Practically, +this is the code that adds user, system, assistant, and image tokens to the prompt. + +```python +# Set up the content you want to send to the model +messages = [ + { + "role": "user", + "content": [ + { + # The image is provided as a PIL Image object + "type": "image", + "image": image, + }, + { + "type": "text", + "text": f"""You are an expert at extracting information from receipts. + Please extract the information from the receipt. Be as detailed as possible -- + missing or misreporting information is a crime. + + Return the information in the following JSON schema: + {ReceiptSummary.model_json_schema()} + """}, + ], + } +] + +# Convert the messages to the final prompt +processor = AutoProcessor.from_pretrained(model_name) +prompt = processor.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True +) +``` + +If you are curious, the final prompt that is sent to the model looks (roughly) like this: + +``` +<|im_start|>system +You are a helpful assistant.<|im_end|> +<|im_start|>user +<|vision_start|><|image_pad|><|vision_end|> +You are an expert at extracting information from receipts. +Please extract the information from the receipt. Be as detailed as +possible -- missing or misreporting information is a crime. + +Return the information in the following JSON schema: + + +<|im_end|> +<|im_start|>assistant +``` + +## Run the model + +```python +# Prepare a function to process receipts +receipt_summary_generator = outlines.generate.json( + model, + ReceiptSummary, + + # Greedy sampling is a good idea for numeric + # data extraction -- no randomness. + sampler=outlines.samplers.greedy() +) + +# Generate the receipt summary +result = receipt_summary_generator(prompt, [image]) +print(result) +``` + +## Output + +The output should look like this: + +``` +ReceiptSummary( + store_name="Trader Joe's", + store_address='401 Bay Street, San Francisco, CA 94133', + store_number=0, + items=[ + Item(name='BANANA EACH', quantity=7, price_per_unit=0.23, total_price=1.61), + Item(name='BAREBELLS CHOCOLATE DOUG', quantity=1, price_per_unit=2.29, total_price=2.29), + Item(name='BAREBELLS CREAMY CRISP', quantity=1, price_per_unit=2.29, total_price=2.29), + Item(name='BAREBELLS CHOCOLATE DOUG', quantity=1, price_per_unit=2.29, total_price=2.29), + Item(name='BAREBELLS CARAMEL CASHEW', quantity=2, price_per_unit=2.29, total_price=4.58), + Item(name='BAREBELLS CREAMY CRISP', quantity=1, price_per_unit=2.29, total_price=2.29), + Item(name='SPINDRIFT ORANGE MANGO 8', quantity=1, price_per_unit=7.49, total_price=7.49), + Item(name='Bottle Deposit', quantity=8, price_per_unit=0.05, total_price=0.4), + Item(name='MILK ORGANIC GALLON WHOL', quantity=1, price_per_unit=6.79, total_price=6.79), + Item(name='CLASSIC GREEK SALAD', quantity=1, price_per_unit=3.49, total_price=3.49), + Item(name='COBB SALAD', quantity=1, price_per_unit=5.99, total_price=5.99), + Item(name='PEPPER BELL RED XL EACH', quantity=1, price_per_unit=1.29, total_price=1.29), + Item(name='BAG FEE.', quantity=1, price_per_unit=0.25, total_price=0.25), + Item(name='BAG FEE.', quantity=1, price_per_unit=0.25, total_price=0.25) + ], + tax=0.68, + total=41.98, + date='2023-11-04', + payment_method='debit', + +) +``` + +Voila! You've successfully extracted information from a receipt using an LLM. + +## Bonus: roasting the user for their receipt + +You can roast the user for their receipt by adding a `roast` field to the end of the `ReceiptSummary` model. + +```python +class ReceiptSummary(BaseModel): + ... + roast: str +``` + +which gives you a result like + +``` +ReceiptSummary( + ... + roast="You must be a fan of Trader Joe's because you bought enough + items to fill a small grocery bag and still had to pay for a bag fee. + Maybe you should start using reusable bags to save some money and the + environment." +) +``` + +Qwen is not particularly funny, but worth a shot. diff --git a/mkdocs.yml b/mkdocs.yml index 7272e4a63..7d48bf392 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -122,6 +122,7 @@ nav: - Vision-Language Models: cookbook/atomic_caption.md - Structured Generation from PDFs: cookbook/read-pdfs.md - Earnings reports to CSV: cookbook/earnings-reports.md + - Digitizing receipts with vision models: cookbook/receipt-digitization.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From d842522c704c0c0ee23bf493873116425eb76e5d Mon Sep 17 00:00:00 2001 From: Sebastien Campion Date: Sun, 10 Nov 2024 11:00:09 +0100 Subject: [PATCH 104/505] Update README.md (#1258) Fix that error NameError: name 'rng' is not defined --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 832703ce6..c7f071503 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ character = generator("Give me a character description", seed=seed) print(repr(character)) # Character(name='Anderson', age=28, armor=, weapon=, strength=8) -character = generator("Give me an interesting character description", rng=rng) +character = generator("Give me an interesting character description") print(repr(character)) # Character(name='Vivian Thr', age=44, armor=, weapon=, strength=125) From c406da8f9ecacb96b93982b8acaad2f5d12dab6e Mon Sep 17 00:00:00 2001 From: Michael Goin Date: Mon, 18 Nov 2024 17:03:48 -0500 Subject: [PATCH 105/505] Bump to outlines-core=0.1.17 for python 3.12-3.13 support (#1273) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa7005afd..1fd2897aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "pycountry", "airportsdata", "torch", - "outlines_core==0.1.14", + "outlines_core==0.1.17", ] dynamic = ["version"] From 2eab80c1ceabdf5ba340b921511f22c235e1c063 Mon Sep 17 00:00:00 2001 From: "Victoria Terenina (torymur)" Date: Thu, 21 Nov 2024 17:05:28 +0000 Subject: [PATCH 106/505] Turn off guide caching --- outlines/fsm/guide.py | 2 -- tests/generate/test_integration_llamacpp.py | 1 + tests/generate/test_integration_transformers.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/outlines/fsm/guide.py b/outlines/fsm/guide.py index d46228fe9..6b97d7729 100644 --- a/outlines/fsm/guide.py +++ b/outlines/fsm/guide.py @@ -15,7 +15,6 @@ ) from outlines import grammars -from outlines.caching import cache from outlines.fsm.parsing import PartialLark, PartialParserState if TYPE_CHECKING: @@ -73,7 +72,6 @@ def copy(self): return self -@cache() def cached_create_states_mapping(regex_string, tokenizer, *args, **kwargs): return uncached_create_states_mapping(regex_string, tokenizer, *args, **kwargs) diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py index 8d4596d60..fd5be2171 100644 --- a/tests/generate/test_integration_llamacpp.py +++ b/tests/generate/test_integration_llamacpp.py @@ -274,6 +274,7 @@ def test_llama_cpp_pre_tokenizer_remains_broken(): generate.choice(model, ["skirt", "dress", "pen", "jacket"]) +@pytest.mark.skip("Caching for guide was temporarily turned off") def test_RegexGuide_caching(model, temp_cache_dir): import llama_cpp diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index 2462d9fcf..8acb87500 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -492,6 +492,7 @@ def test_transformers_use_existing_model_and_tokenizer(): assert isinstance(sequence, str) +@pytest.mark.skip("Caching for guide was temporarily turned off") def test_RegexGuide_caching(temp_cache_dir): import outlines.caching from outlines.fsm.guide import cached_create_states_mapping From f099f9668986340f1167a82c5e75fc1066c79594 Mon Sep 17 00:00:00 2001 From: "Victoria Terenina (torymur)" Date: Thu, 21 Nov 2024 19:34:22 +0000 Subject: [PATCH 107/505] Fix index interface in tests --- tests/fsm/test_guide.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/fsm/test_guide.py b/tests/fsm/test_guide.py index 510faf4b0..bf25c43c4 100644 --- a/tests/fsm/test_guide.py +++ b/tests/fsm/test_guide.py @@ -59,7 +59,7 @@ def convert_token_to_string(self, token): tokenizer = MockTokenizer() fsm = RegexGuide.from_regex(regex_str, tokenizer) - assert fsm.states_to_token_maps == {0: {1: 1}} + assert fsm.states_to_token_maps.get_transitions() == {0: {1: 1}} instruction = fsm.get_next_instruction(0) assert isinstance(instruction, Generate) @@ -70,9 +70,6 @@ def convert_token_to_string(self, token): assert fsm.is_final_state(0) is False - for state in fsm.final_states: - assert fsm.is_final_state(state) is True - def test_regex_multi_byte_llama_like(): class MockTokenizer: @@ -100,7 +97,7 @@ def convert_token_to_string(self, token): tokenizer = MockTokenizer() fsm = RegexGuide.from_regex(regex_str, tokenizer) - assert fsm.states_to_token_maps == { + assert fsm.states_to_token_maps.get_transitions() == { 0: {5: 1, 4: 2}, 1: {6: 3}, 3: {7: 4}, @@ -116,9 +113,6 @@ def convert_token_to_string(self, token): assert fsm.is_final_state(0) is False - for state in fsm.final_states: - assert fsm.is_final_state(state) is True - def test_regex_multi_byte_gpt2_like(): class MockTokenizer: @@ -147,7 +141,7 @@ def convert_token_to_string(self, token): tokenizer = MockTokenizer() fsm = RegexGuide.from_regex(regex_str, tokenizer) - assert fsm.states_to_token_maps == { + assert fsm.states_to_token_maps.get_transitions() == { 0: {5: 1, 10: 2}, 1: {8: 5, 4: 3}, 2: {11: 3}, @@ -163,9 +157,6 @@ def convert_token_to_string(self, token): assert fsm.is_final_state(0) is False - for state in fsm.final_states: - assert fsm.is_final_state(state) is True - def test_regex_final_state(): """Make sure that the FSM stays in the final state as we keep generating""" From b55d31463cb6ed38fc0109e018f53ce0cdafbe19 Mon Sep 17 00:00:00 2001 From: Jeremy Zucker Date: Wed, 6 Nov 2024 16:25:29 -0800 Subject: [PATCH 108/505] Update generation.md `[Outlines model](../models)` does not return the link correctly. Tried switching to `[Outlines model](../models/models.md)` --- docs/reference/generation/generation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/generation/generation.md b/docs/reference/generation/generation.md index a14818514..930ad9d22 100644 --- a/docs/reference/generation/generation.md +++ b/docs/reference/generation/generation.md @@ -4,7 +4,7 @@ title: Generation # Generation -Once an [Outlines model](../models) is constructed you can use `outlines.generate` to generate text. Standard LLM generation is possible via `outlines.generate.text`, along with a variety of structured generation methods described below. (For a detailed technical explanation of how structured generation works, you may review the [Structured Generation Explanation](./structured_generation_explanation.md) page) +Once an [Outlines model](../models/models.md) is constructed you can use `outlines.generate` to generate text. Standard LLM generation is possible via `outlines.generate.text`, along with a variety of structured generation methods described below. (For a detailed technical explanation of how structured generation works, you may review the [Structured Generation Explanation](./structured_generation_explanation.md) page) Before generating text, you must construct an `outlines.model`. Example: From bf62d119748a33e558d9ef87385117dde040e40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Wed, 27 Nov 2024 13:25:13 +0100 Subject: [PATCH 109/505] Add link to YT channel and .txt blog --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c7f071503..f14c1e925 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,18 @@ Outlines Logo -[![.txt Twitter][dottxt-twitter-badge]][dottxt-twitter] + + 🗒️ *Make LLMs speak the language of every application.* 🗒️ + +Made with ❤👷️ by the team at [.txt](https://dottxt.co). [![Documentation][documentation-badge]][documentation] [![Contributors][contributors-badge]][contributors] [![Downloads][downloads-badge]][pypistats] [![Discord][discord-badge]][discord] +[Youtube channel][youtube-dottxt] | [.txt blog][blog-dottxt] | [Twitter][dottxt-twitter] -*Robust (structured) text generation.* - -Made with ❤👷️ by the team at [.txt](https://dottxt.co).

@@ -363,3 +364,5 @@ answer = outlines.generate.text(model)(prompt, max_tokens=100) [downloads-badge]: https://img.shields.io/pypi/dm/outlines?color=89AC6B&logo=python&logoColor=white&style=flat-square [pypistats]: https://pypistats.org/packages/outlines [dottxt-twitter-badge]: https://img.shields.io/twitter/follow/dottxtai?style=social +[youtube-dottxt]: https://www.youtube.com/@dottxt-ai +[blog-dottxt]: https://blog.dottxt.co/ From 7a9baad809eff63df17f2f0ae79acd5cd4187db4 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 27 Nov 2024 03:28:33 +0200 Subject: [PATCH 110/505] Update home page spacing --- docs/overrides/home.html | 111 ++++++++++++++++++++++++------------- docs/stylesheets/extra.css | 3 + 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 0c97d5ac6..6525ab062 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -7,27 +7,15 @@ {{ super() }}
@@ -89,26 +122,24 @@ Outlines Logo
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 33783c153..e71a6124f 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -112,6 +112,9 @@ h1.title { h2.subtitle { margin: 5px 0px 25px; + font-size: 1rem; + max-width: 540px; + margin: 0 auto; } .md-typeset { From 63b4feb2c4ea3c7767d7440bdf5d01fa887cbbcb Mon Sep 17 00:00:00 2001 From: Neil Mehta Date: Tue, 12 Nov 2024 12:28:39 -0500 Subject: [PATCH 111/505] Add compatibility for numpy 2 while preserving numpy 1 compatibility --- outlines/base.py | 23 +++++++++++++++++------ pyproject.toml | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/outlines/base.py b/outlines/base.py index 4de8ccf5a..29d42c54c 100644 --- a/outlines/base.py +++ b/outlines/base.py @@ -5,12 +5,23 @@ from typing import Callable, Optional import numpy as np -from numpy.lib.function_base import ( - _calculate_shapes, - _parse_gufunc_signature, - _parse_input_dimensions, - _update_dim_sizes, -) + +# Import required functions based on NumPy version +np_major_version = int(np.__version__.split(".")[0]) +if np_major_version >= 2: + from numpy.lib._function_base_impl import ( + _calculate_shapes, + _parse_gufunc_signature, + _parse_input_dimensions, + _update_dim_sizes, + ) +else: + from numpy.lib.function_base import ( + _calculate_shapes, + _parse_gufunc_signature, + _parse_input_dimensions, + _update_dim_sizes, + ) # Allow nested loops for running in notebook. We don't enable it globally as it # may interfere with other libraries that use asyncio. diff --git a/pyproject.toml b/pyproject.toml index 1fd2897aa..a8b77011a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "jinja2", "lark", "nest_asyncio", - "numpy<2.0.0", + "numpy", "cloudpickle", "diskcache", "pydantic>=2.0", From 7cdaeacc89df059be0e302a6708c19b910f1553b Mon Sep 17 00:00:00 2001 From: Chris McMaster Date: Mon, 11 Nov 2024 16:46:06 +1100 Subject: [PATCH 112/505] Update `mlx-lm` kvcache creation --- outlines/models/mlxlm.py | 7 +-- pyproject.toml | 2 +- tests/models/test_mlxlm.py | 100 +++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 tests/models/test_mlxlm.py diff --git a/outlines/models/mlxlm.py b/outlines/models/mlxlm.py index 6e63ef5b6..d8b7e032c 100644 --- a/outlines/models/mlxlm.py +++ b/outlines/models/mlxlm.py @@ -167,12 +167,7 @@ def sample(logits: "mx.array") -> Tuple["mx.array", float]: prob = softmax_logits[0, token] return token, prob - kv_heads = ( - [self.model.n_kv_heads] * len(self.model.layers) - if isinstance(self.model.n_kv_heads, int) - else self.model.n_kv_heads - ) - cache = [mlx_lm.models.base.KVCache(self.model.head_dim, n) for n in kv_heads] + cache = mlx_lm.models.cache.make_prompt_cache(self.model) # kv cache contains processed input IDs, we pass the unprocessed inputs and cache to model() unprocessed_input_ids = prompt diff --git a/pyproject.toml b/pyproject.toml index a8b77011a..4972f09ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ test = [ "beartype<0.16.0", "responses", "llama-cpp-python", - "mlx-lm; platform_machine == 'arm64' and sys_platform == 'darwin'", + "mlx-lm>=0.19.2; platform_machine == 'arm64' and sys_platform == 'darwin'", "huggingface_hub", "openai>=1.0.0", "vllm; sys_platform != 'darwin'", diff --git a/tests/models/test_mlxlm.py b/tests/models/test_mlxlm.py new file mode 100644 index 000000000..20e59da81 --- /dev/null +++ b/tests/models/test_mlxlm.py @@ -0,0 +1,100 @@ +import pytest + +from outlines.models.mlxlm import mlxlm +from outlines.models.transformers import TransformerTokenizer + +try: + import mlx.core as mx + + HAS_MLX = mx.metal.is_available() +except ImportError: + HAS_MLX = False + + +TEST_MODEL = "mlx-community/SmolLM-135M-Instruct-4bit" + + +@pytest.mark.skipif(not HAS_MLX, reason="MLX tests require Apple Silicon") +def test_mlxlm_model(): + model = mlxlm(TEST_MODEL) + assert hasattr(model, "model") + assert hasattr(model, "tokenizer") + assert isinstance(model.tokenizer, TransformerTokenizer) + + +@pytest.mark.skipif(not HAS_MLX, reason="MLX tests require Apple Silicon") +def test_mlxlm_tokenizer(): + model = mlxlm(TEST_MODEL) + + # Test single string encoding/decoding + test_text = "Hello, world!" + token_ids = mx.array(model.mlx_tokenizer.encode(test_text)) + assert isinstance(token_ids, mx.array) + + +@pytest.mark.skipif(not HAS_MLX, reason="MLX tests require Apple Silicon") +def test_mlxlm_generate(): + from outlines.generate.api import GenerationParameters, SamplingParameters + + model = mlxlm(TEST_MODEL) + prompt = "Write a haiku about programming:" + + # Test with basic generation parameters + gen_params = GenerationParameters(max_tokens=50, stop_at=None, seed=None) + + # Test with different sampling parameters + sampling_params = SamplingParameters( + sampler="multinomial", num_samples=1, top_p=0.9, top_k=None, temperature=0.7 + ) + + # Test generation + output = model.generate(prompt, gen_params, None, sampling_params) + assert isinstance(output, str) + assert len(output) > 0 + + +@pytest.mark.skipif(not HAS_MLX, reason="MLX tests require Apple Silicon") +def test_mlxlm_stream(): + from outlines.generate.api import GenerationParameters, SamplingParameters + + model = mlxlm(TEST_MODEL) + prompt = "Count from 1 to 5:" + + gen_params = GenerationParameters(max_tokens=20, stop_at=None, seed=None) + + sampling_params = SamplingParameters( + sampler="greedy", # Use greedy sampling for deterministic output + num_samples=1, + top_p=None, + top_k=None, + temperature=0.0, + ) + + # Test streaming + stream = model.stream(prompt, gen_params, None, sampling_params) + tokens = list(stream) + assert len(tokens) > 0 + assert all(isinstance(token, str) for token in tokens) + + # Test that concatenated streaming output matches generate output + streamed_text = "".join(tokens) + generated_text = model.generate(prompt, gen_params, None, sampling_params) + assert streamed_text == generated_text + + +@pytest.mark.skipif(not HAS_MLX, reason="MLX tests require Apple Silicon") +def test_mlxlm_errors(): + model = mlxlm(TEST_MODEL) + + # Test batch inference (should raise NotImplementedError) + with pytest.raises(NotImplementedError): + from outlines.generate.api import GenerationParameters, SamplingParameters + + gen_params = GenerationParameters(max_tokens=10, stop_at=None, seed=None) + sampling_params = SamplingParameters("multinomial", 1, None, None, 1.0) + model.generate(["prompt1", "prompt2"], gen_params, None, sampling_params) + + # Test beam search (should raise NotImplementedError) + with pytest.raises(NotImplementedError): + sampling_params = SamplingParameters("beam_search", 1, None, None, 1.0) + model.generate("test prompt", gen_params, None, sampling_params) From 2db34c5ce7645ee7b7c67c1734e94e51d301f790 Mon Sep 17 00:00:00 2001 From: beta9 Date: Thu, 24 Oct 2024 17:00:50 -0400 Subject: [PATCH 113/505] Add `beam` deployment example --- examples/beam-cloud/README.md | 5 +++++ examples/beam-cloud/app.py | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 examples/beam-cloud/README.md create mode 100644 examples/beam-cloud/app.py diff --git a/examples/beam-cloud/README.md b/examples/beam-cloud/README.md new file mode 100644 index 000000000..5f190d76f --- /dev/null +++ b/examples/beam-cloud/README.md @@ -0,0 +1,5 @@ +## Deploy Outlines on Beam + +1. Create an account [here](https://beam.cloud) and install the Beam SDK +2. Download the `app.py` file to your computer +3. Deploy it as a serverless API by running: `beam deploy app.py:predict` diff --git a/examples/beam-cloud/app.py b/examples/beam-cloud/app.py new file mode 100644 index 000000000..fb6c2cb2b --- /dev/null +++ b/examples/beam-cloud/app.py @@ -0,0 +1,39 @@ +from beam import Image, endpoint, env + +if env.is_remote(): + import outlines + + +# Pre-load models when the container first starts +def load_models(): + import outlines + + model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") + return model + + +@endpoint( + name="outlines-serverless", + gpu="A10G", + cpu=1, + memory="16Gi", + on_start=load_models, + image=Image().add_python_packages( + ["outlines", "torch", "transformers", "accelerate"] + ), +) +def predict(context, **inputs): + default_prompt = """You are a sentiment-labelling assistant. + Is the following review positive or negative? + + Review: This restaurant is just awesome! + """ + + prompt = inputs.get("prompt", default_prompt) + + # Unpack cached model from context + model = context.on_start_value + # Inference + generator = outlines.generate.choice(model, ["Positive", "Negative"]) + answer = generator(prompt) + return {"answer": answer} From 66f662781c43154a4411f98c11ea0290ef727c39 Mon Sep 17 00:00:00 2001 From: Ulf Aslak Date: Sat, 28 Sep 2024 22:21:49 +0200 Subject: [PATCH 114/505] Add OpenAI to LogitsGenerator --- outlines/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outlines/models/__init__.py b/outlines/models/__init__.py index d28fcb2d7..fe6f861ac 100644 --- a/outlines/models/__init__.py +++ b/outlines/models/__init__.py @@ -16,4 +16,4 @@ from .transformers_vision import TransformersVision, transformers_vision from .vllm import VLLM, vllm -LogitsGenerator = Union[Transformers, LlamaCpp, ExLlamaV2Model, MLXLM, VLLM] +LogitsGenerator = Union[Transformers, LlamaCpp, OpenAI, ExLlamaV2Model, MLXLM, VLLM] From 568f252da3835329e65b74065166e21052c6a9b5 Mon Sep 17 00:00:00 2001 From: Sebastien Campion Date: Wed, 27 Nov 2024 21:11:27 +0100 Subject: [PATCH 115/505] Add cookbook recipe to extract event details from text (#1269) --- docs/cookbook/extract_event_details.md | 34 +++++++++++++++++++ docs/cookbook/extract_event_details.py | 46 ++++++++++++++++++++++++++ mkdocs.yml | 2 ++ 3 files changed, 82 insertions(+) create mode 100644 docs/cookbook/extract_event_details.md create mode 100644 docs/cookbook/extract_event_details.py diff --git a/docs/cookbook/extract_event_details.md b/docs/cookbook/extract_event_details.md new file mode 100644 index 000000000..0f87d9586 --- /dev/null +++ b/docs/cookbook/extract_event_details.md @@ -0,0 +1,34 @@ +This recipe demonstrates how to use the `outlines` library to extract structured event details from a text message. +We will extract the title, location, and start date and time from messages like the following: + +```plaintext +Hello Kitty, my grandmother will be here, I think it's better to postpone +our appointment to review math lessons to next Monday at 2pm at the same +place, 3 avenue des tanneurs, one hour will be enough see you 😘 +``` + +Let see how to extract the event details from the message with the MLX +library dedicated to Apple Silicon processor (M series). + +```python +--8<-- "docs/cookbook/extract_event_details.py" +``` + +The output will be: + +```plaintext +Today: Saturday 16 November 2024 and it's 10:55 +``` + +and the extracted event information will be: + +```json +{ + "title":"Math Review", + "location":"3 avenue des tanneurs", + "start":"2024-11-22T14:00:00Z" +} +``` + + +To find out more about this use case, we recommend the project developped by [Joseph Rudoler](https://x.com/JRudoler) the [ICS Generator](https://github.com/jrudoler/ics-generator) diff --git a/docs/cookbook/extract_event_details.py b/docs/cookbook/extract_event_details.py new file mode 100644 index 000000000..b51f8d921 --- /dev/null +++ b/docs/cookbook/extract_event_details.py @@ -0,0 +1,46 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + +from outlines import generate, models + +# Load the model +model = models.mlxlm("mlx-community/Hermes-3-Llama-3.1-8B-8bit") + + +# Define the event schema using Pydantic +class Event(BaseModel): + title: str = Field(description="title of the event") + location: str + start: datetime = Field( + default=None, description="date of the event if available in iso format" + ) + + +# Get the current date and time +now = datetime.now().strftime("%A %d %B %Y and it's %H:%M") + +# Define the prompt +prompt = f""" +Today's date and time are {now} +Given a user message, extract information of the event like date and time in iso format, location and title. +If the given date is relative, think step by step to find the right date. +Here is the message: +""" + +# Sample message +message = """Hello Kitty, my grandmother will be here , I think it's better to postpone our +appointment to review math lessons to next Friday at 2pm at the same place, 3 avenue des tanneurs, I think that one hour will be enough +see you 😘 """ + +# Create the generator +generator = generate.json(model, Event) + +# Extract the event information +event = generator(prompt + message) + +# Print the current date and time +print(f"Today: {now}") + +# Print the extracted event information in JSON format +print(event.json()) diff --git a/mkdocs.yml b/mkdocs.yml index 7d48bf392..4888309b9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,7 @@ markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.snippets: extra_css: @@ -123,6 +124,7 @@ nav: - Structured Generation from PDFs: cookbook/read-pdfs.md - Earnings reports to CSV: cookbook/earnings-reports.md - Digitizing receipts with vision models: cookbook/receipt-digitization.md + - Extract events details from text: cookbook/extract_event_details.md - Run on the cloud: - BentoML: cookbook/deploy-using-bentoml.md - Cerebrium: cookbook/deploy-using-cerebrium.md From 5608dd809e87b36eb395fdbeec8dd48d0f513a73 Mon Sep 17 00:00:00 2001 From: Aakash Thatte <84656834+sky-2002@users.noreply.github.com> Date: Thu, 28 Nov 2024 01:42:25 +0530 Subject: [PATCH 116/505] Add jax compatible api (#1207) This PR adds a JAX compatible API, refer issue #1027 --- benchmarks/bench_processors.py | 23 ++++++ outlines/processors/base_logits_processor.py | 21 ++++++ pyproject.toml | 3 + tests/processors/test_base_processor.py | 74 ++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 tests/processors/test_base_processor.py diff --git a/benchmarks/bench_processors.py b/benchmarks/bench_processors.py index 5b4901540..db1e4a8f1 100644 --- a/benchmarks/bench_processors.py +++ b/benchmarks/bench_processors.py @@ -9,6 +9,12 @@ except ImportError: pass +try: + import jax + import jax.numpy as jnp +except ImportError: + pass + def is_mlx_lm_allowed(): try: @@ -18,6 +24,14 @@ def is_mlx_lm_allowed(): return mx.metal.is_available() +def is_jax_allowed(): + try: + import jax # noqa: F401 + except ImportError: + return False + return True + + def get_mock_processor_inputs(array_library, num_tokens=30000): """ logits: (4, 30,000 ) dtype=float @@ -43,6 +57,13 @@ def get_mock_processor_inputs(array_library, num_tokens=30000): input_ids = mx.random.randint( low=0, high=num_tokens, shape=(4, 2048), dtype=mx.int32 ) + elif array_library == "jax": + logits = jnp.random.uniform( + key=jax.random.PRNGKey(0), shape=(4, num_tokens), dtype=jnp.float32 + ) + input_ids = jnp.random.randint( + key=jax.random.PRNGKey(0), low=0, high=num_tokens, shape=(4, 2048) + ) else: raise ValueError @@ -67,6 +88,8 @@ class LogitsProcessorPassthroughBenchmark: params += ["mlx"] if torch.cuda.is_available(): params += ["torch_cuda"] + if is_jax_allowed(): + params += ["jax"] def setup(self, array_library): self.logits_processor = HalvingLogitsProcessor() diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index 9a52abecd..eec7de121 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -20,6 +20,16 @@ def is_mlx_array_type(array_type): return issubclass(array_type, mx.array) +def is_jax_array_type(array_type): + try: + import jaxlib + except ImportError: + return False + return issubclass(array_type, jaxlib.xla_extension.ArrayImpl) or isinstance( + array_type, jaxlib.xla_extension.ArrayImpl + ) + + class OutlinesLogitsProcessor(Protocol): """ Base class for logits processors which normalizes types of logits: @@ -101,6 +111,12 @@ def _to_torch(tensor_like: Array) -> torch.Tensor: # https://ml-explore.github.io/mlx/build/html/usage/numpy.html return torch.from_dlpack(tensor_like) + elif is_jax_array_type(type(tensor_like)): + import jax + + torch_tensor = torch.from_dlpack(jax.dlpack.to_dlpack(tensor_like)) + return torch_tensor + else: raise TypeError( "LogitsProcessor must be called with either np.NDArray, " @@ -129,6 +145,11 @@ def _from_torch(tensor: torch.Tensor, target_type: Type) -> Array: # numpy doesn't support bfloat16, mlx doesn't support direct conversion from torch return mx.array(tensor.float().numpy()) + elif is_jax_array_type(target_type): + import jax + + return jax.dlpack.from_dlpack(tensor) + else: raise TypeError( f"Failed to convert torch tensors to target_type `{target_type}`" diff --git a/pyproject.toml b/pyproject.toml index 4972f09ef..294fbe4b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,9 @@ enable_incomplete_feature = ["Unpack"] [[tool.mypy.overrides]] module = [ "exllamav2.*", + "jax", + "jaxlib", + "jax.numpy", "jinja2", "jsonschema.*", "openai.*", diff --git a/tests/processors/test_base_processor.py b/tests/processors/test_base_processor.py new file mode 100644 index 000000000..cd9f48278 --- /dev/null +++ b/tests/processors/test_base_processor.py @@ -0,0 +1,74 @@ +from typing import List + +import jax.numpy as jnp +import numpy as np +import pytest +import torch + +from outlines.processors.base_logits_processor import OutlinesLogitsProcessor + +arrays = { + "list": [[1.0, 2.0], [3.0, 4.0]], + "np": np.array([[1, 2], [3, 4]], dtype=np.float32), + "jax": jnp.array([[1, 2], [3, 4]], dtype=jnp.float32), + "torch": torch.tensor([[1, 2], [3, 4]], dtype=torch.float32), +} + +try: + import mlx.core as mx + + arrays["mlx"] = mx.array([[1, 2], [3, 4]], dtype=mx.float32) +except ImportError: + pass + +try: + import jax.numpy as jnp + + arrays["jax"] = jnp.array([[1, 2], [3, 4]], dtype=jnp.float32) +except ImportError: + pass + + +# Mock implementation of the abstract class for testing +class MockLogitsProcessor(OutlinesLogitsProcessor): + def process_logits( + self, input_ids: List[List[int]], logits: torch.Tensor + ) -> torch.Tensor: + # For testing purposes, let's just return logits multiplied by 2 + return logits * 2 + + +@pytest.fixture +def processor(): + """Fixture for creating an instance of the MockLogitsProcessor.""" + return MockLogitsProcessor() + + +@pytest.mark.parametrize("array_type", arrays.keys()) +def test_to_torch(array_type, processor): + data = arrays[array_type] + torch_tensor = processor._to_torch(data) + assert isinstance(torch_tensor, torch.Tensor) + assert torch.allclose( + torch_tensor.cpu(), torch.tensor([[1, 2], [3, 4]], dtype=torch.float32) + ) + + +@pytest.mark.parametrize("array_type", arrays.keys()) +def test_from_torch(array_type, processor): + torch_tensor = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32) + data = processor._from_torch(torch_tensor, type(arrays[array_type])) + assert isinstance(data, type(arrays[array_type])) + assert np.allclose(data, arrays[array_type]) + + +@pytest.mark.parametrize("array_type", arrays.keys()) +def test_call(array_type, processor): + input_ids = arrays[array_type] + logits = arrays[array_type] + processed_logits = processor(input_ids, logits) + + assert isinstance(processed_logits, type(arrays[array_type])) + assert np.allclose( + np.array(processed_logits), np.array([[2.0, 4.0], [6.0, 8.0]], dtype=np.float32) + ) From e9485cf2126d9c14bd749d55f8aa6729d96808d0 Mon Sep 17 00:00:00 2001 From: g-prz <158364710+g-prz@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:28:25 +0000 Subject: [PATCH 117/505] Add json call with multi-function enums (#1277) This PR aims at solving #1217 --- README.md | 27 +++++++++ outlines/fsm/json_schema.py | 18 +++++- outlines/generate/json.py | 12 +++- tests/fsm/test_json_schema.py | 59 ++++++++++++++++++- .../generate/test_integration_transformers.py | 24 ++++++++ 5 files changed, 136 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f14c1e925..e9285e863 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,33 @@ print(add(**result)) A great advantage of passing functions directly to specify the structure is that the structure of the LLM will change with the function's definition. No need to change the code at several places! +You can also embed various functions into an enum to generate params: + +```python +from enum import Enum +from functools import partial + +import outlines + + +def add(a: int, b: int) -> int: + return a + b + +def mul(c: float, d: float) -> float: + return c * d + +class Operation(Enum): + add = partial(add) + mul = partial(mul) + +model = outlines.models.transformers("WizardLM/WizardMath-7B-V1.1") +generator = outlines.generate.json(model, add) +result = generator("Return json with two float named c and d respectively. c is negative and d greater than 1.0.") + +print(result) +# {'c': -3.14, 'd': 1.5} +``` + ## Prompting Building prompts can get messy. **Outlines** makes it easier to write and manage diff --git a/outlines/fsm/json_schema.py b/outlines/fsm/json_schema.py index 98d2de59c..0bab57923 100644 --- a/outlines/fsm/json_schema.py +++ b/outlines/fsm/json_schema.py @@ -2,6 +2,7 @@ import json import re import warnings +from enum import Enum from typing import Callable, Optional, Tuple, Type, Union from jsonschema.protocols import Validator @@ -306,6 +307,8 @@ def to_regex( for choice in instance["enum"]: if type(choice) in [int, float, bool, type(None), str]: choices.append(re.escape(json.dumps(choice))) + elif isinstance(choice, dict): + choices.append(to_regex(resolver, choice, whitespace_pattern)) else: raise TypeError(f"Unsupported data type in enum: {type(choice)}") return f"({'|'.join(choices)})" @@ -524,7 +527,7 @@ def to_regex( ) -def get_schema_from_signature(fn: Callable) -> str: +def get_schema_from_signature(fn: Callable) -> dict: """Turn a function signature into a JSON schema. Every JSON object valid to the output JSON Schema can be passed @@ -550,3 +553,16 @@ def get_schema_from_signature(fn: Callable) -> str: model = create_model(fn_name, **arguments) return model.model_json_schema() + + +def get_schema_from_enum(myenum: type[Enum]) -> dict: + if len(myenum) == 0: + raise ValueError( + f"Your enum class {myenum.__name__} has 0 members. If you are working with an enum of functions, do not forget to register them as callable (using `partial` for instance)" + ) + choices = [ + get_schema_from_signature(elt.value.func) if callable(elt.value) else elt.value + for elt in myenum + ] + schema = {"title": myenum.__name__, "enum": choices} + return schema diff --git a/outlines/generate/json.py b/outlines/generate/json.py index f75878d29..703447958 100644 --- a/outlines/generate/json.py +++ b/outlines/generate/json.py @@ -1,10 +1,15 @@ import json as pyjson +from enum import Enum from functools import singledispatch from typing import Callable, Optional, Union from pydantic import BaseModel -from outlines.fsm.json_schema import build_regex_from_schema, get_schema_from_signature +from outlines.fsm.json_schema import ( + build_regex_from_schema, + get_schema_from_enum, + get_schema_from_signature, +) from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial @@ -48,6 +53,11 @@ def json( regex_str = build_regex_from_schema(schema, whitespace_pattern) generator = regex(model, regex_str, sampler) generator.format_sequence = lambda x: schema_object.parse_raw(x) + elif isinstance(schema_object, type(Enum)): + schema = pyjson.dumps(get_schema_from_enum(schema_object)) + regex_str = build_regex_from_schema(schema, whitespace_pattern) + generator = regex(model, regex_str, sampler) + generator.format_sequence = lambda x: pyjson.loads(x) elif callable(schema_object): schema = pyjson.dumps(get_schema_from_signature(schema_object)) regex_str = build_regex_from_schema(schema, whitespace_pattern) diff --git a/tests/fsm/test_json_schema.py b/tests/fsm/test_json_schema.py index 7565ff642..6f0b59c50 100644 --- a/tests/fsm/test_json_schema.py +++ b/tests/fsm/test_json_schema.py @@ -1,5 +1,8 @@ import json import re +from contextlib import nullcontext +from enum import Enum +from functools import partial from typing import List, Literal, Union import interegular @@ -19,6 +22,7 @@ UUID, WHITESPACE, build_regex_from_schema, + get_schema_from_enum, get_schema_from_signature, to_regex, ) @@ -237,8 +241,26 @@ def test_match_number(pattern, does_match): ), # Enum mix of types ( - {"title": "Foo", "enum": [6, 5.3, "potato", True, None]}, - r'(6|5\.3|"potato"|true|null)', + { + "title": "Foo", + "enum": [ + 6, + 5.3, + "potato", + True, + None, + { + "properties": { + "a": {"title": "A", "type": "number"}, + "b": {"title": "B", "type": "number"}, + }, + "required": ["a", "b"], + "title": "add", + "type": "object", + }, + ], + }, + r'(6|5\.3|"potato"|true|null|\{[ ]?"a"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\.[0-9]+)?([eE][+-][0-9]+)?[ ]?,[ ]?"b"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\.[0-9]+)?([eE][+-][0-9]+)?[ ]?\})', [ ("6", True), ("5.3", True), @@ -248,6 +270,8 @@ def test_match_number(pattern, does_match): ("523", False), ("True", False), ("None", False), + ('{"a": -1.0, "b": 1.1}', True), + ('{"a": "a", "b": 1.1}', False), ], ), # integer @@ -1039,3 +1063,34 @@ class Model(BaseModel): # check if the pattern uses lookarounds incompatible with interegular.Pattern.to_fsm() interegular.parse_pattern(pattern).to_fsm() + + +def add(a: float, b: float) -> float: + return a + b + + +class MyEnum(Enum): + add = partial(add) + a = "a" + b = 2 + + +# if you don't register your function as callable, you will get an empty enum +class EmptyEnum(Enum): + add = add + + +@pytest.mark.parametrize( + "enum,expectation", + [ + (MyEnum, nullcontext()), + (EmptyEnum, pytest.raises(ValueError)), + ], +) +def test_enum_schema(enum, expectation): + with expectation: + result = get_schema_from_enum(enum) + assert result["title"] == enum.__name__ + assert len(result["enum"]) == len(enum) + for elt in result["enum"]: + assert type(elt) in [int, float, bool, type(None), str, dict] diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index 8acb87500..92c5d789c 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -1,6 +1,7 @@ import datetime import re from enum import Enum +from functools import partial from typing import List, Union import pytest @@ -354,6 +355,29 @@ class User(BaseModel): assert result.user_id in [1, 2] +def add(a: int, b: int) -> int: + return a + b + + +def mul(c: float, d: float) -> float: + return c * d + + +def test_transformers_json_function_enum(model): + prompt = "Output some JSON " + + class Operation(Enum): + add = partial(add) + mul = partial(mul) + + result = generate.json(model, Operation)(prompt, seed=0) + assert isinstance(result, dict) + assert len(result) == 2 + for k, v in result.items(): + assert k in ["a", "b", "c", "d"] + assert isinstance(v, (int, float)) + + def test_transformers_json_array(model): prompt = "Output some JSON " From 36f1bf27a80f5877b88a915749341664cef4a1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Thu, 28 Nov 2024 23:15:15 +0100 Subject: [PATCH 118/505] Fix library top-level imports (#1296) Users are currently running into install issues. After a clean install of `outlines` they get an error message that asks for `transformers` to be installed. This should not be the case, as the library is not required for every integration. In this PR we remove `transformers` and `datasets` top-level imports, and add per-integration optional dependencies. ## TODO - [x] Test `import outlines` from clean install - [x] Test installing outlines with vLLM optional dependencies - [x] Test installing outlines with MLX optional dependencies - [x] Test installing outlines with transformers optional dependencies - [x] Test installing outlines with llama-cpp optional dependencies - [x] Test installing outlines with exllamav2 optional dependencies - [x] Test installing outlines with openai optional dependencies - [x] Update the documentation Supersedes #1295. Fixes #1263. --- docs/reference/models/llamacpp.md | 6 +++++- docs/reference/models/mlxlm.md | 6 +++++- docs/reference/models/openai.md | 6 +++++- docs/reference/models/transformers.md | 4 ++-- docs/reference/models/vllm.md | 6 +++++- outlines/models/transformers.py | 4 ++-- outlines/models/vllm.py | 7 ++++--- pyproject.toml | 9 ++++++++- tests/generate/test_integration_transformers_vision.py | 2 +- 9 files changed, 37 insertions(+), 13 deletions(-) diff --git a/docs/reference/models/llamacpp.md b/docs/reference/models/llamacpp.md index 24b0fdc97..51b62eca8 100644 --- a/docs/reference/models/llamacpp.md +++ b/docs/reference/models/llamacpp.md @@ -4,7 +4,11 @@ Outlines provides an integration with [Llama.cpp](https://github.com/ggerganov/l !!! Note "Installation" - You need to install the `llama-cpp-python` library to use the llama.cpp integration. See the [installation section](#installation) for instructions to install `llama-cpp-python` with CUDA, Metal, ROCm and other backends. + You need to install the `llama-cpp-python` library to use the llama.cpp integration. See the [installation section](#installation) for instructions to install `llama-cpp-python` with CUDA, Metal, ROCm and other backends. To get started quickly you can also run: + + ```bash + pip install "outlines[llamacpp]" + ``` ## Load the model diff --git a/docs/reference/models/mlxlm.md b/docs/reference/models/mlxlm.md index cf7bb7443..d435b9c1f 100644 --- a/docs/reference/models/mlxlm.md +++ b/docs/reference/models/mlxlm.md @@ -4,7 +4,11 @@ Outlines provides an integration with [mlx-lm](https://github.com/ml-explore/mlx !!! Note "Installation" - You need to install the `mlx` and `mlx-lm` libraries on a device which [supports Metal](https://support.apple.com/en-us/102894) to use the mlx-lm integration. + You need to install the `mlx` and `mlx-lm` libraries on a device which [supports Metal](https://support.apple.com/en-us/102894) to use the mlx-lm integration. To get started quickly you can also run: + + ```bash + pip install "outlines[mlxlm]" + ``` ## Load the model diff --git a/docs/reference/models/openai.md b/docs/reference/models/openai.md index 5c737c916..638107568 100644 --- a/docs/reference/models/openai.md +++ b/docs/reference/models/openai.md @@ -2,7 +2,11 @@ !!! Installation - You need to install the `openai` library to be able to use the OpenAI API in Outlines. + You need to install the `openai` library to be able to use the OpenAI API in Outlines. Or alternatively: + + ```bash + pip install "outlines[openai]" + ``` ## OpenAI models diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index 2a13e28ec..f4c319540 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -3,10 +3,10 @@ !!! Installation - You need to install the `transformer`, `datasets` and `torch` libraries to be able to use these models in Outlines: + You need to install the `transformer`, `datasets` and `torch` libraries to be able to use these models in Outlines, or alternatively: ```bash - pip install torch transformers datasets + pip install "outlines[transformers]" ``` diff --git a/docs/reference/models/vllm.md b/docs/reference/models/vllm.md index fb1c830fa..8789b588e 100644 --- a/docs/reference/models/vllm.md +++ b/docs/reference/models/vllm.md @@ -3,7 +3,11 @@ !!! Note "Installation" - You need to install the `vllm` library to use the vLLM integration. See the [installation section](#installation) for instructions to install vLLM for CPU or ROCm. + You need to install the `vllm` library to use the vLLM integration. See the [installation section](#installation) for instructions to install vLLM for CPU or ROCm. To get started you can also run: + + ```bash + pip install "outlines[vllm]" + ``` ## Load the model diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index 7ecc9013f..444492500 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -2,8 +2,6 @@ import inspect from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union -from datasets.fingerprint import Hasher - from outlines.generate.api import GenerationParameters, SamplingParameters from outlines.models.tokenizer import Tokenizer @@ -116,6 +114,8 @@ def __eq__(self, other): return NotImplemented def __hash__(self): + from datasets.fingerprint import Hasher + return hash(Hasher.hash(self.tokenizer)) def __getstate__(self): diff --git a/outlines/models/vllm.py b/outlines/models/vllm.py index d1f97bde2..778c27c6f 100644 --- a/outlines/models/vllm.py +++ b/outlines/models/vllm.py @@ -1,11 +1,10 @@ import dataclasses from typing import TYPE_CHECKING, List, Optional, Union -from transformers import SPIECE_UNDERLINE, PreTrainedTokenizerBase - from outlines.generate.api import GenerationParameters, SamplingParameters if TYPE_CHECKING: + from transformers import PreTrainedTokenizerBase from vllm import LLM from vllm.sampling_params import SamplingParams @@ -188,7 +187,7 @@ def vllm(model_name: str, **vllm_model_params): return VLLM(model) -def adapt_tokenizer(tokenizer: PreTrainedTokenizerBase) -> PreTrainedTokenizerBase: +def adapt_tokenizer(tokenizer: "PreTrainedTokenizerBase") -> "PreTrainedTokenizerBase": """Adapt a tokenizer to use to compile the FSM. The API of Outlines tokenizers is slightly different to that of `transformers`. In @@ -205,6 +204,8 @@ def adapt_tokenizer(tokenizer: PreTrainedTokenizerBase) -> PreTrainedTokenizerBa PreTrainedTokenizerBase The adapted tokenizer. """ + from transformers import SPIECE_UNDERLINE + tokenizer.vocabulary = tokenizer.get_vocab() tokenizer.special_tokens = set(tokenizer.all_special_tokens) diff --git a/pyproject.toml b/pyproject.toml index 294fbe4b8..5b005cfbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ dependencies = [ "jsonschema", "requests", "tqdm", - "datasets", "typing_extensions", "pycountry", "airportsdata", @@ -46,6 +45,12 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] +vllm = ["vllm", "transformers", "numpy2"] +transformers = ["transformers", "accelerate", "datasets", "numpy<2"] +mlxlm = ["mlx-lm", "datasets"] +openai = ["openai"] +llamacpp = ["llama-cpp-python", "transformers", "datasets", "numpy<2"] +exllamav2 = ["exllamav2"] test = [ "pre-commit", "pytest", @@ -61,10 +66,12 @@ test = [ "mlx-lm>=0.19.2; platform_machine == 'arm64' and sys_platform == 'darwin'", "huggingface_hub", "openai>=1.0.0", + "datasets", "vllm; sys_platform != 'darwin'", "transformers", "pillow", "exllamav2", + "jax" ] serve = [ "vllm>=0.3.0", diff --git a/tests/generate/test_integration_transformers_vision.py b/tests/generate/test_integration_transformers_vision.py index 28b516c57..ee4f84c06 100644 --- a/tests/generate/test_integration_transformers_vision.py +++ b/tests/generate/test_integration_transformers_vision.py @@ -23,7 +23,7 @@ def img_from_url(url): @pytest.fixture(scope="session") def model(tmp_path_factory): return transformers_vision( - "trl-internal-testing/tiny-random-LlavaForConditionalGeneration", + "trl-internal-testing/tiny-LlavaForConditionalGeneration", model_class=LlavaForConditionalGeneration, device="cpu", ) From c8e69c6b9b81fc9f64328d9d3dac9e032f94efbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Fri, 6 Dec 2024 15:08:24 +0100 Subject: [PATCH 119/505] Bump the outlines core version to 0.1.20 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5b005cfbd..f738c965f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycountry", "airportsdata", "torch", - "outlines_core==0.1.17", + "outlines_core==0.1.20", ] dynamic = ["version"] From dccaaced6c9972f1c75dab0de83afbd68cfde933 Mon Sep 17 00:00:00 2001 From: Hicham Randrianarivo Date: Tue, 3 Dec 2024 10:49:34 +0100 Subject: [PATCH 120/505] Fix typo in vllm extra requirements --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f738c965f..2001f4c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -vllm = ["vllm", "transformers", "numpy2"] +vllm = ["vllm", "transformers", "numpy<2"] transformers = ["transformers", "accelerate", "datasets", "numpy<2"] mlxlm = ["mlx-lm", "datasets"] openai = ["openai"] From e4f96fbce45593222e40805ca614ace251728ef2 Mon Sep 17 00:00:00 2001 From: g-prz <158364710+g-prz@users.noreply.github.com> Date: Sun, 8 Dec 2024 15:40:24 +0000 Subject: [PATCH 121/505] Handle enum in choice (#1279) This PR aims at solving #1218 (requires #1277 to be merged) Quick fix to solve #1275 --- .gitignore | 1 + README.md | 23 +++++++++++++++++++++++ outlines/generate/choice.py | 18 ++++++++++++++---- tests/generate/test_generate.py | 31 ++++++++++++++++++++++++++++--- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 4984b18cb..08390ae3d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ docs/build *.gguf .venv benchmarks/results +.python-version # Remove doc build folders .cache/ diff --git a/README.md b/README.md index e9285e863..d34b0984d 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,29 @@ generator = outlines.generate.choice(model, ["Positive", "Negative"]) answer = generator(prompt) ``` +You can also pass these choices through en enum: + +````python +from enum import Enum + +import outlines + +class Sentiment(str, Enum): + positive = "Positive" + negative = "Negative" + +model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") + +prompt = """You are a sentiment-labelling assistant. +Is the following review positive or negative? + +Review: This restaurant is just awesome! +""" + +generator = outlines.generate.choice(model, Sentiment) +answer = generator(prompt) +```` + ### Type constraint You can instruct the model to only return integers or floats: diff --git a/outlines/generate/choice.py b/outlines/generate/choice.py index 595513d52..75fc71271 100644 --- a/outlines/generate/choice.py +++ b/outlines/generate/choice.py @@ -1,7 +1,10 @@ import json as pyjson +import re +from enum import Enum from functools import singledispatch -from typing import Callable, List +from typing import Callable, List, Union +from outlines.fsm.json_schema import build_regex_from_schema, get_schema_from_enum from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial @@ -12,12 +15,19 @@ @singledispatch def choice( - model, choices: List[str], sampler: Sampler = multinomial() + model, choices: Union[List[str], type[Enum]], sampler: Sampler = multinomial() ) -> SequenceGeneratorAdapter: - regex_str = r"(" + r"|".join(choices) + r")" + if isinstance(choices, type(Enum)): + regex_str = build_regex_from_schema(pyjson.dumps(get_schema_from_enum(choices))) + else: + choices = [re.escape(choice) for choice in choices] # type: ignore + regex_str = r"(" + r"|".join(choices) + r")" generator = regex(model, regex_str, sampler) - generator.format_sequence = lambda x: x + if isinstance(choices, type(Enum)): + generator.format_sequence = lambda x: pyjson.loads(x) + else: + generator.format_sequence = lambda x: x return generator diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index 9c288c21e..f91bc8653 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -1,5 +1,6 @@ import contextlib import re +from enum import Enum import pytest @@ -127,6 +128,18 @@ def model_t5(tmp_path_factory): ) +class MyEnum(Enum): + foo = "foo" + bar = "bar" + baz = "baz" + + +ALL_SAMPLE_CHOICES_FIXTURES = ( + ["foo", "bar", "baz"], + MyEnum, +) + + ########################################## # Stuctured Generation Inputs ########################################## @@ -264,21 +277,33 @@ def test_generate_json(request, model_fixture, sample_schema): @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +@pytest.mark.parametrize("sample_choices", ALL_SAMPLE_CHOICES_FIXTURES) def test_generate_choice(request, model_fixture, sample_choices): model = request.getfixturevalue(model_fixture) generator = generate.choice(model, sample_choices) res = generator(**get_inputs(model_fixture)) - assert res in sample_choices + if isinstance(sample_choices, type(Enum)): + assert res in [elt.value for elt in sample_choices] + else: + assert res in sample_choices @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) +@pytest.mark.parametrize("sample_choices", ALL_SAMPLE_CHOICES_FIXTURES) def test_generate_choice_twice(request, model_fixture, sample_choices): model = request.getfixturevalue(model_fixture) generator = generate.choice(model, sample_choices) res = generator(**get_inputs(model_fixture)) - assert res in sample_choices + if isinstance(sample_choices, type(Enum)): + assert res in [elt.value for elt in sample_choices] + else: + assert res in sample_choices + res = generator(**get_inputs(model_fixture)) - assert res in sample_choices + if isinstance(sample_choices, type(Enum)): + assert res in [elt.value for elt in sample_choices] + else: + assert res in sample_choices @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) From 1b7ec18a91b6d39e0279dd91cfe9bab415dc80c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 2 Dec 2024 15:50:52 +0100 Subject: [PATCH 122/505] Use `outlines-core` to translate JSON Schemas into regexes --- benchmarks/bench_json_schema.py | 7 +- outlines/fsm/json_schema.py | 489 +------- outlines/generate/choice.py | 4 +- outlines/generate/json.py | 7 +- outlines/processors/structured.py | 3 +- tests/fsm/test_json_schema.py | 1032 +---------------- .../generate/test_integration_transformers.py | 1 + 7 files changed, 17 insertions(+), 1526 deletions(-) diff --git a/benchmarks/bench_json_schema.py b/benchmarks/bench_json_schema.py index 62d9b3c1d..3a1f72cb6 100644 --- a/benchmarks/bench_json_schema.py +++ b/benchmarks/bench_json_schema.py @@ -1,6 +1,7 @@ +from outlines_core.fsm.json_schema import build_regex_from_schema + from outlines.caching import cache_disabled from outlines.fsm.guide import RegexGuide -from outlines.fsm.json_schema import build_regex_from_schema from .common import setup_tokenizer # noqa: E402 @@ -70,10 +71,6 @@ def setup(self, schema_name): self.tokenizer = setup_tokenizer() self.schema = schemas[schema_name] - @cache_disabled() - def time_json_schema_to_regex(self, schema_name): - build_regex_from_schema(self.schema) - @cache_disabled() def time_json_schema_to_fsm(self, schema_name): regex = build_regex_from_schema(self.schema) diff --git a/outlines/fsm/json_schema.py b/outlines/fsm/json_schema.py index 0bab57923..bae0ad17a 100644 --- a/outlines/fsm/json_schema.py +++ b/outlines/fsm/json_schema.py @@ -1,90 +1,10 @@ import inspect import json -import re import warnings from enum import Enum -from typing import Callable, Optional, Tuple, Type, Union +from typing import Callable, Type, Union -from jsonschema.protocols import Validator from pydantic import BaseModel, create_model -from referencing import Registry, Resource -from referencing._core import Resolver -from referencing.jsonschema import DRAFT202012 - -# allow `\"`, `\\`, or any character which isn't a control sequence -STRING_INNER = r'([^"\\\x00-\x1F\x7F-\x9F]|\\["\\])' -STRING = f'"{STRING_INNER}*"' - -INTEGER = r"(-)?(0|[1-9][0-9]*)" -NUMBER = rf"({INTEGER})(\.[0-9]+)?([eE][+-][0-9]+)?" -BOOLEAN = r"(true|false)" -NULL = r"null" -WHITESPACE = r"[ ]?" - -type_to_regex = { - "string": STRING, - "integer": INTEGER, - "number": NUMBER, - "boolean": BOOLEAN, - "null": NULL, -} - -DATE_TIME = r'"(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]{3})?(Z)?"' -DATE = r'"(?:\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])"' -TIME = r'"(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z)?"' -UUID = r'"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"' - -format_to_regex = { - "uuid": UUID, - "date-time": DATE_TIME, - "date": DATE, - "time": TIME, -} - - -def build_regex_from_schema(schema: str, whitespace_pattern: Optional[str] = None): - """Turn a JSON schema into a regex that matches any JSON object that follows - this schema. - - JSON Schema is a declarative language that allows to annotate JSON documents - with types and descriptions. These schemas can be generated from any Python - datastructure that has type annotation: namedtuples, dataclasses, Pydantic - models. And by ensuring that the generation respects the schema we ensure - that the output can be parsed into these objects. - This function parses the provided schema and builds a generation schedule which - mixes deterministic generation (fixed strings), and sampling with constraints. - - Parameters - ---------- - schema - A string that represents a JSON Schema. - whitespace_pattern - Pattern to use for JSON syntactic whitespace (doesn't impact string literals) - Example: allow only a single space or newline with `whitespace_pattern=r"[\n ]?"` - - Returns - ------- - A generation schedule. A list of strings that represent the JSON - schema's structure and regular expression that define the structure of - the fields. - - References - ---------- - .. [0] JSON Schema. https://json-schema.org/ - - """ - - schema = json.loads(schema) - Validator.check_schema(schema) - - # Build reference resolver - schema = Resource(contents=schema, specification=DRAFT202012) - uri = schema.id() if schema.id() is not None else "" - registry = Registry().with_resource(uri=uri, resource=schema) - resolver = registry.resolver() - - content = schema.contents - return to_regex(resolver, content, whitespace_pattern) def convert_json_schema_to_str(json_schema: Union[dict, str, Type[BaseModel]]) -> str: @@ -120,413 +40,6 @@ def convert_json_schema_to_str(json_schema: Union[dict, str, Type[BaseModel]]) - return schema_str -def _get_num_items_pattern(min_items, max_items, whitespace_pattern): - # Helper function for arrays and objects - min_items = int(min_items or 0) - if max_items is None: - return rf"{{{max(min_items - 1, 0)},}}" - else: - max_items = int(max_items) - if max_items < 1: - return None - return rf"{{{max(min_items - 1, 0)},{max_items - 1}}}" - - -def validate_quantifiers( - min_bound: Optional[str], max_bound: Optional[str], start_offset: int = 0 -) -> Tuple[str, str]: - """ - Ensures that the bounds of a number are valid. Bounds are used as quantifiers in the regex. - - Parameters - ---------- - min_bound - The minimum value that the number can take. - max_bound - The maximum value that the number can take. - start_offset - Number of elements that are already present in the regex but still need to be counted. - ex: if the regex is already "(-)?(0|[1-9][0-9])", we will always have at least 1 digit, so the start_offset is 1. - - Returns - ------- - min_bound - The minimum value that the number can take. - max_bound - The maximum value that the number can take. - - Raises - ------ - ValueError - If the minimum bound is greater than the maximum bound. - - TypeError or ValueError - If the minimum bound is not an integer or None. - or - If the maximum bound is not an integer or None. - """ - min_bound = "" if min_bound is None else str(int(min_bound) - start_offset) - max_bound = "" if max_bound is None else str(int(max_bound) - start_offset) - if min_bound and max_bound: - if int(max_bound) < int(min_bound): - raise ValueError("max bound must be greater than or equal to min bound") - return min_bound, max_bound - - -def to_regex( - resolver: Resolver, instance: dict, whitespace_pattern: Optional[str] = None -): - """Translate a JSON Schema instance into a regex that validates the schema. - - Note - ---- - Many features of JSON schema are missing: - - Handle `additionalProperties` keyword - - Handle types defined as a list - - Handle constraints on numbers - - Handle special patterns: `date`, `uri`, etc. - - This does not support recursive definitions. - - Parameters - ---------- - resolver - An object that resolves references to other instances within a schema - instance - The instance to translate - whitespace_pattern - Pattern to use for JSON syntactic whitespace (doesn't impact string literals) - Example: allow only a single space or newline with `whitespace_pattern=r"[\n ]?"` - """ - - # set whitespace pattern - if whitespace_pattern is None: - whitespace_pattern = WHITESPACE - - if instance == {}: - # JSON Schema Spec: Empty object means unconstrained, any json type is legal - types = [ - {"type": "boolean"}, - {"type": "null"}, - {"type": "number"}, - {"type": "integer"}, - {"type": "string"}, - {"type": "array"}, - {"type": "object"}, - ] - regexes = [to_regex(resolver, t, whitespace_pattern) for t in types] - regexes = [rf"({r})" for r in regexes] - return rf"{'|'.join(regexes)}" - - elif "properties" in instance: - regex = "" - regex += r"\{" - properties = instance["properties"] - required_properties = instance.get("required", []) - is_required = [item in required_properties for item in properties] - # If at least one property is required, we include the one in the lastest position - # without any comma. - # For each property before it (optional or required), we add with a comma after the property. - # For each property after it (optional), we add with a comma before the property. - if any(is_required): - last_required_pos = max([i for i, value in enumerate(is_required) if value]) - for i, (name, value) in enumerate(properties.items()): - subregex = f'{whitespace_pattern}"{re.escape(name)}"{whitespace_pattern}:{whitespace_pattern}' - subregex += to_regex(resolver, value, whitespace_pattern) - if i < last_required_pos: - subregex = f"{subregex}{whitespace_pattern}," - elif i > last_required_pos: - subregex = f"{whitespace_pattern},{subregex}" - regex += subregex if is_required[i] else f"({subregex})?" - # If no property is required, we have to create a possible pattern for each property in which - # it's the last one necessarilly present. Then, we add the others as optional before and after - # following the same strategy as described above. - # The whole block is made optional to allow the case in which no property is returned. - else: - property_subregexes = [] - for i, (name, value) in enumerate(properties.items()): - subregex = f'{whitespace_pattern}"{name}"{whitespace_pattern}:{whitespace_pattern}' - subregex += to_regex(resolver, value, whitespace_pattern) - property_subregexes.append(subregex) - possible_patterns = [] - for i in range(len(property_subregexes)): - pattern = "" - for subregex in property_subregexes[:i]: - pattern += f"({subregex}{whitespace_pattern},)?" - pattern += property_subregexes[i] - for subregex in property_subregexes[i + 1 :]: - pattern += f"({whitespace_pattern},{subregex})?" - possible_patterns.append(pattern) - regex += f"({'|'.join(possible_patterns)})?" - - regex += f"{whitespace_pattern}" + r"\}" - - return regex - - # To validate against allOf, the given data must be valid against all of the - # given subschemas. - elif "allOf" in instance: - subregexes = [ - to_regex(resolver, t, whitespace_pattern) for t in instance["allOf"] - ] - subregexes_str = [f"{subregex}" for subregex in subregexes] - return rf"({''.join(subregexes_str)})" - - # To validate against `anyOf`, the given data must be valid against - # any (one or more) of the given subschemas. - elif "anyOf" in instance: - subregexes = [ - to_regex(resolver, t, whitespace_pattern) for t in instance["anyOf"] - ] - return rf"({'|'.join(subregexes)})" - - # To validate against oneOf, the given data must be valid against exactly - # one of the given subschemas. - elif "oneOf" in instance: - subregexes = [ - to_regex(resolver, t, whitespace_pattern) for t in instance["oneOf"] - ] - - xor_patterns = [f"(?:{subregex})" for subregex in subregexes] - - return rf"({'|'.join(xor_patterns)})" - - # Create pattern for Tuples, per JSON Schema spec, `prefixItems` determines types at each idx - elif "prefixItems" in instance: - element_patterns = [ - to_regex(resolver, t, whitespace_pattern) for t in instance["prefixItems"] - ] - comma_split_pattern = rf"{whitespace_pattern},{whitespace_pattern}" - tuple_inner = comma_split_pattern.join(element_patterns) - return rf"\[{whitespace_pattern}{tuple_inner}{whitespace_pattern}\]" - - # The enum keyword is used to restrict a value to a fixed set of values. It - # must be an array with at least one element, where each element is unique. - elif "enum" in instance: - choices = [] - for choice in instance["enum"]: - if type(choice) in [int, float, bool, type(None), str]: - choices.append(re.escape(json.dumps(choice))) - elif isinstance(choice, dict): - choices.append(to_regex(resolver, choice, whitespace_pattern)) - else: - raise TypeError(f"Unsupported data type in enum: {type(choice)}") - return f"({'|'.join(choices)})" - - elif "const" in instance: - const = instance["const"] - if type(const) in [int, float, bool, type(None), str]: - const = re.escape(json.dumps(const)) - else: - raise TypeError(f"Unsupported data type in const: {type(const)}") - return const - - elif "$ref" in instance: - path = f"{instance['$ref']}" - instance = resolver.lookup(path).contents - return to_regex(resolver, instance, whitespace_pattern) - - # The type keyword may either be a string or an array: - # - If it's a string, it is the name of one of the basic types. - # - If it is an array, it must be an array of strings, where each string is - # the name of one of the basic types, and each element is unique. In this - # case, the JSON snippet is valid if it matches any of the given types. - elif "type" in instance: - instance_type = instance["type"] - if instance_type == "string": - if "maxLength" in instance or "minLength" in instance: - max_items = instance.get("maxLength", "") - min_items = instance.get("minLength", "") - try: - if int(max_items) < int(min_items): - raise ValueError( - "maxLength must be greater than or equal to minLength" - ) # FIXME this raises an error but is caught right away by the except (meant for int("") I assume) - except ValueError: - pass - return f'"{STRING_INNER}{{{min_items},{max_items}}}"' - elif "pattern" in instance: - pattern = instance["pattern"] - if pattern[0] == "^" and pattern[-1] == "$": - return rf'("{pattern[1:-1]}")' - else: - return rf'("{pattern}")' - elif "format" in instance: - format = instance["format"] - if format == "date-time": - return format_to_regex["date-time"] - elif format == "uuid": - return format_to_regex["uuid"] - elif format == "date": - return format_to_regex["date"] - elif format == "time": - return format_to_regex["time"] - else: - raise NotImplementedError( - f"Format {format} is not supported by Outlines" - ) - else: - return type_to_regex["string"] - - elif instance_type == "number": - bounds = { - "minDigitsInteger", - "maxDigitsInteger", - "minDigitsFraction", - "maxDigitsFraction", - "minDigitsExponent", - "maxDigitsExponent", - } - if bounds.intersection(set(instance.keys())): - min_digits_integer, max_digits_integer = validate_quantifiers( - instance.get("minDigitsInteger"), - instance.get("maxDigitsInteger"), - start_offset=1, - ) - min_digits_fraction, max_digits_fraction = validate_quantifiers( - instance.get("minDigitsFraction"), instance.get("maxDigitsFraction") - ) - min_digits_exponent, max_digits_exponent = validate_quantifiers( - instance.get("minDigitsExponent"), instance.get("maxDigitsExponent") - ) - integers_quantifier = ( - f"{{{min_digits_integer},{max_digits_integer}}}" - if min_digits_integer or max_digits_integer - else "*" - ) - fraction_quantifier = ( - f"{{{min_digits_fraction},{max_digits_fraction}}}" - if min_digits_fraction or max_digits_fraction - else "+" - ) - exponent_quantifier = ( - f"{{{min_digits_exponent},{max_digits_exponent}}}" - if min_digits_exponent or max_digits_exponent - else "+" - ) - return rf"((-)?(0|[1-9][0-9]{integers_quantifier}))(\.[0-9]{fraction_quantifier})?([eE][+-][0-9]{exponent_quantifier})?" - return type_to_regex["number"] - - elif instance_type == "integer": - if "minDigits" in instance or "maxDigits" in instance: - min_digits, max_digits = validate_quantifiers( - instance.get("minDigits"), instance.get("maxDigits"), start_offset=1 - ) - return rf"(-)?(0|[1-9][0-9]{{{min_digits},{max_digits}}})" - return type_to_regex["integer"] - - elif instance_type == "array": - num_repeats = _get_num_items_pattern( - instance.get("minItems"), instance.get("maxItems"), whitespace_pattern - ) - if num_repeats is None: - return rf"\[{whitespace_pattern}\]" - - allow_empty = "?" if int(instance.get("minItems", 0)) == 0 else "" - - if "items" in instance: - items_regex = to_regex(resolver, instance["items"], whitespace_pattern) - return rf"\[{whitespace_pattern}(({items_regex})(,{whitespace_pattern}({items_regex})){num_repeats}){allow_empty}{whitespace_pattern}\]" - else: - # Here we need to make the choice to exclude generating list of objects - # if the specification of the object is not given, even though a JSON - # object that contains an object here would be valid under the specification. - legal_types = [ - {"type": "boolean"}, - {"type": "null"}, - {"type": "number"}, - {"type": "integer"}, - {"type": "string"}, - ] - depth = instance.get("depth", 2) - if depth > 0: - legal_types.append({"type": "object", "depth": depth - 1}) - legal_types.append({"type": "array", "depth": depth - 1}) - - regexes = [ - to_regex(resolver, t, whitespace_pattern) for t in legal_types - ] - return rf"\[{whitespace_pattern}({'|'.join(regexes)})(,{whitespace_pattern}({'|'.join(regexes)})){num_repeats}{allow_empty}{whitespace_pattern}\]" - - elif instance_type == "object": - # pattern for json object with values defined by instance["additionalProperties"] - # enforces value type constraints recursively, "minProperties", and "maxProperties" - # doesn't enforce "required", "dependencies", "propertyNames" "any/all/on Of" - num_repeats = _get_num_items_pattern( - instance.get("minProperties"), - instance.get("maxProperties"), - whitespace_pattern, - ) - if num_repeats is None: - return rf"\{{{whitespace_pattern}\}}" - - allow_empty = "?" if int(instance.get("minProperties", 0)) == 0 else "" - - additional_properties = instance.get("additionalProperties") - - if additional_properties is None or additional_properties is True: - # JSON Schema behavior: If the additionalProperties of an object is - # unset or True, it is unconstrained object. - # We handle this by setting additionalProperties to anyOf: {all types} - - legal_types = [ - {"type": "string"}, - {"type": "number"}, - {"type": "boolean"}, - {"type": "null"}, - ] - - # We set the object depth to 2 to keep the expression finite, but the "depth" - # key is not a true component of the JSON Schema specification. - depth = instance.get("depth", 2) - if depth > 0: - legal_types.append({"type": "object", "depth": depth - 1}) - legal_types.append({"type": "array", "depth": depth - 1}) - additional_properties = {"anyOf": legal_types} - - value_pattern = to_regex( - resolver, additional_properties, whitespace_pattern - ) - key_value_pattern = ( - f"{STRING}{whitespace_pattern}:{whitespace_pattern}{value_pattern}" - ) - key_value_successor_pattern = ( - f"{whitespace_pattern},{whitespace_pattern}{key_value_pattern}" - ) - multiple_key_value_pattern = f"({key_value_pattern}({key_value_successor_pattern}){num_repeats}){allow_empty}" - - return ( - r"\{" - + whitespace_pattern - + multiple_key_value_pattern - + whitespace_pattern - + r"\}" - ) - - elif instance_type == "boolean": - return type_to_regex["boolean"] - - elif instance_type == "null": - return type_to_regex["null"] - - elif isinstance(instance_type, list): - # Here we need to make the choice to exclude generating an object - # if the specification of the object is not give, even though a JSON - # object that contains an object here would be valid under the specification. - regexes = [ - to_regex(resolver, {"type": t}, whitespace_pattern) - for t in instance_type - if t != "object" - ] - return rf"({'|'.join(regexes)})" - - raise NotImplementedError( - f"""Could not translate the instance {instance} to a - regular expression. Make sure it is valid to the JSON Schema specification. If - it is, please open an issue on the Outlines repository""" - ) - - def get_schema_from_signature(fn: Callable) -> dict: """Turn a function signature into a JSON schema. diff --git a/outlines/generate/choice.py b/outlines/generate/choice.py index 75fc71271..afb998f52 100644 --- a/outlines/generate/choice.py +++ b/outlines/generate/choice.py @@ -4,7 +4,9 @@ from functools import singledispatch from typing import Callable, List, Union -from outlines.fsm.json_schema import build_regex_from_schema, get_schema_from_enum +from outlines_core.fsm.json_schema import build_regex_from_schema + +from outlines.fsm.json_schema import get_schema_from_enum from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial diff --git a/outlines/generate/json.py b/outlines/generate/json.py index 703447958..d098d920d 100644 --- a/outlines/generate/json.py +++ b/outlines/generate/json.py @@ -3,13 +3,10 @@ from functools import singledispatch from typing import Callable, Optional, Union +from outlines_core.fsm.json_schema import build_regex_from_schema from pydantic import BaseModel -from outlines.fsm.json_schema import ( - build_regex_from_schema, - get_schema_from_enum, - get_schema_from_signature, -) +from outlines.fsm.json_schema import get_schema_from_enum, get_schema_from_signature from outlines.generate.api import SequenceGeneratorAdapter from outlines.models import OpenAI from outlines.samplers import Sampler, multinomial diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index d2bc15f77..64892b73f 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -27,10 +27,11 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union import torch +from outlines_core.fsm.json_schema import build_regex_from_schema from pydantic import BaseModel from outlines.fsm.guide import CFGGuide, Guide, RegexGuide -from outlines.fsm.json_schema import build_regex_from_schema, convert_json_schema_to_str +from outlines.fsm.json_schema import convert_json_schema_to_str from .base_logits_processor import OutlinesLogitsProcessor diff --git a/tests/fsm/test_json_schema.py b/tests/fsm/test_json_schema.py index 6f0b59c50..825433397 100644 --- a/tests/fsm/test_json_schema.py +++ b/tests/fsm/test_json_schema.py @@ -1,31 +1,14 @@ import json -import re from contextlib import nullcontext from enum import Enum from functools import partial -from typing import List, Literal, Union +from typing import List -import interegular import pytest -from pydantic import BaseModel, Field, constr +from outlines_core.fsm.json_schema import build_regex_from_schema +from pydantic import BaseModel, constr -from outlines.fsm.json_schema import ( - BOOLEAN, - DATE, - DATE_TIME, - INTEGER, - NULL, - NUMBER, - STRING, - STRING_INNER, - TIME, - UUID, - WHITESPACE, - build_regex_from_schema, - get_schema_from_enum, - get_schema_from_signature, - to_regex, -) +from outlines.fsm.json_schema import get_schema_from_enum, get_schema_from_signature def test_function_basic(): @@ -58,1011 +41,8 @@ class User(BaseModel): is_true: bool schema = json.dumps(User.model_json_schema()) - schedule = build_regex_from_schema(schema) - assert isinstance(schedule, str) - - -@pytest.mark.parametrize( - "pattern,does_match", - [ - ({"integer": "0"}, True), - ({"integer": "1"}, True), - ({"integer": "-1"}, True), - ({"integer": "01"}, False), - ({"integer": "1.3"}, False), - ({"integer": "t"}, False), - ], -) -def test_match_integer(pattern, does_match): - step = {"title": "Foo", "type": "integer"} - regex = to_regex(None, step) - assert regex == INTEGER - - value = pattern["integer"] - match = re.fullmatch(regex, value) - if does_match: - assert match[0] == value - assert match.span() == (0, len(value)) - else: - assert match is None - - -@pytest.mark.parametrize( - "pattern,does_match", - [ - ({"number": "1"}, True), - ({"number": "0"}, True), - ({"number": "01"}, False), - ({"number": ".3"}, False), - ({"number": "1.3"}, True), - ({"number": "-1.3"}, True), - ({"number": "1.3e9"}, False), - ({"number": "1.3e+9"}, True), - ], -) -def test_match_number(pattern, does_match): - step = {"title": "Foo", "type": "number"} - regex = to_regex(None, step) - assert regex == NUMBER - - value = pattern["number"] - match = re.fullmatch(regex, value) - if does_match: - assert match[0] == value - assert match.span() == (0, len(value)) - else: - assert match is None - - -@pytest.mark.parametrize( - "schema,regex,examples", - [ - # String - ( - {"title": "Foo", "type": "string"}, - STRING, - [ - ("unquotedstring", False), - ('"(parenthesized_string)"', True), - ('"malformed) parenthesis (((() string"', True), - ('"quoted_string"', True), - (r'"escape_\character"', False), - (r'"double_\\escape"', True), - (r'"\n"', False), - (r'"\\n"', True), - (r'"unescaped " quote"', False), - (r'"escaped \" quote"', True), - ], - ), - # String with maximum length - ( - {"title": "Foo", "type": "string", "maxLength": 3}, - f'"{STRING_INNER}{{,3}}"', - [('"ab"', True), ('"a""', False), ('"abcd"', False)], - ), - # String with minimum length - ( - {"title": "Foo", "type": "string", "minLength": 3}, - f'"{STRING_INNER}{{3,}}"', - [('"ab"', False), ('"abcd"', True), ('"abc""', False)], - ), - # String with both minimum and maximum length - ( - {"title": "Foo", "type": "string", "minLength": 3, "maxLength": 5}, - f'"{STRING_INNER}{{3,5}}"', - [('"ab"', False), ('"abcd"', True), ('"abcdef""', False)], - ), - # String defined by a regular expression - ( - {"title": "Foo", "type": "string", "pattern": r"^[a-z]$"}, - r'("[a-z]")', - [('"a"', True), ('"1"', False)], - ), - # Boolean - ( - {"title": "Foo", "type": "boolean"}, - BOOLEAN, - [ - ("true", True), - ("false", True), - ("null", False), - ("0", False), - ], - ), - # Null - ( - {"title": "Foo", "type": "null"}, - NULL, - [ - ("null", True), - ("true", False), - ("0", False), - ], - ), - # Const string - ( - {"title": "Foo", "const": "Marc", "type": "string"}, - '"Marc"', - [('"Marc"', True), ('"Jean"', False), ('"John"', False)], - ), - # Make sure strings are escaped with regex escaping - ( - {"title": "Foo", "const": ".*", "type": "string"}, - r'"\.\*"', - [('".*"', True), (r'"\s*"', False), (r'"\.\*"', False)], - ), - # Make sure strings are escaped with JSON escaping - ( - {"title": "Foo", "const": '"', "type": "string"}, - r'"\\""', - [('"\\""', True), ('"""', False)], - ), - # Const integer - ( - {"title": "Foo", "const": 0, "type": "integer"}, - "0", - [("0", True), ("1", False), ("a", False)], - ), - # Const float - ( - {"title": "Foo", "const": 0.2, "type": "float"}, - r"0\.2", - [("0.2", True), ("032", False)], - ), - # Const boolean - ( - {"title": "Foo", "const": True, "type": "boolean"}, - "true", - [("true", True), ("True", False)], - ), - # Const null - ( - {"title": "Foo", "const": None, "type": "null"}, - "null", - [("null", True), ("None", False), ("", False)], - ), - # Enum string - ( - {"title": "Foo", "enum": ["Marc", "Jean"], "type": "string"}, - '("Marc"|"Jean")', - [('"Marc"', True), ('"Jean"', True), ('"John"', False)], - ), - # Make sure strings are escaped with regex and JSON escaping - ( - {"title": "Foo", "enum": [".*", r"\s*"], "type": "string"}, - r'("\.\*"|"\\\\s\*")', - [('".*"', True), (r'"\\s*"', True), (r'"\.\*"', False)], - ), - # Enum integer - ( - {"title": "Foo", "enum": [0, 1], "type": "integer"}, - "(0|1)", - [("0", True), ("1", True), ("a", False)], - ), - # Enum mix of types - ( - { - "title": "Foo", - "enum": [ - 6, - 5.3, - "potato", - True, - None, - { - "properties": { - "a": {"title": "A", "type": "number"}, - "b": {"title": "B", "type": "number"}, - }, - "required": ["a", "b"], - "title": "add", - "type": "object", - }, - ], - }, - r'(6|5\.3|"potato"|true|null|\{[ ]?"a"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\.[0-9]+)?([eE][+-][0-9]+)?[ ]?,[ ]?"b"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\.[0-9]+)?([eE][+-][0-9]+)?[ ]?\})', - [ - ("6", True), - ("5.3", True), - ('"potato"', True), - ("true", True), - ("null", True), - ("523", False), - ("True", False), - ("None", False), - ('{"a": -1.0, "b": 1.1}', True), - ('{"a": "a", "b": 1.1}', False), - ], - ), - # integer - ( - { - "title": "Foo", - "type": "object", - "properties": {"count": {"title": "Count", "type": "integer"}}, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?(-)?(0|[1-9][0-9]*)[ ]?\\}', - [('{ "count": 100 }', True)], - ), - # integer with minimum digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": {"title": "Count", "type": "integer", "minDigits": 3} - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?(-)?(0|[1-9][0-9]{2,})[ ]?\\}', - [('{ "count": 10 }', False), ('{ "count": 100 }', True)], - ), - # integer with maximum digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": {"title": "Count", "type": "integer", "maxDigits": 3} - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?(-)?(0|[1-9][0-9]{,2})[ ]?\\}', - [('{ "count": 100 }', True), ('{ "count": 1000 }', False)], - ), - # integer with minimum and maximum digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": { - "title": "Count", - "type": "integer", - "minDigits": 3, - "maxDigits": 5, - } - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?(-)?(0|[1-9][0-9]{2,4})[ ]?\\}', - [ - ('{ "count": 10 }', False), - ('{ "count": 100 }', True), - ('{ "count": 10000 }', True), - ('{ "count": 100000 }', False), - ], - ), - # number - ( - { - "title": "Foo", - "type": "object", - "properties": {"count": {"title": "Count", "type": "number"}}, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\\.[0-9]+)?([eE][+-][0-9]+)?[ ]?\\}', - [('{ "count": 100 }', True), ('{ "count": 100.5 }', True)], - ), - # number with min and max integer digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": { - "title": "Count", - "type": "number", - "minDigitsInteger": 3, - "maxDigitsInteger": 5, - } - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?((-)?(0|[1-9][0-9]{2,4}))(\\.[0-9]+)?([eE][+-][0-9]+)?[ ]?\\}', - [ - ('{ "count": 10.005 }', False), - ('{ "count": 100.005 }', True), - ('{ "count": 10000.005 }', True), - ('{ "count": 100000.005 }', False), - ], - ), - # number with min and max fraction digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": { - "title": "Count", - "type": "number", - "minDigitsFraction": 3, - "maxDigitsFraction": 5, - } - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\\.[0-9]{3,5})?([eE][+-][0-9]+)?[ ]?\\}', - [ - ('{ "count": 1.05 }', False), - ('{ "count": 1.005 }', True), - ('{ "count": 1.00005 }', True), - ('{ "count": 1.000005 }', False), - ], - ), - # number with min and max exponent digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": { - "title": "Count", - "type": "number", - "minDigitsExponent": 3, - "maxDigitsExponent": 5, - } - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?((-)?(0|[1-9][0-9]*))(\\.[0-9]+)?([eE][+-][0-9]{3,5})?[ ]?\\}', - [ - ('{ "count": 1.05e1 }', False), - ('{ "count": 1.05e+001 }', True), - ('{ "count": 1.05e-00001 }', True), - ('{ "count": 1.05e0000001 }', False), - ], - ), - # number with min and max integer, fraction and exponent digits - ( - { - "title": "Foo", - "type": "object", - "properties": { - "count": { - "title": "Count", - "type": "number", - "minDigitsInteger": 3, - "maxDigitsInteger": 5, - "minDigitsFraction": 3, - "maxDigitsFraction": 5, - "minDigitsExponent": 3, - "maxDigitsExponent": 5, - } - }, - "required": ["count"], - }, - '\\{[ ]?"count"[ ]?:[ ]?((-)?(0|[1-9][0-9]{2,4}))(\\.[0-9]{3,5})?([eE][+-][0-9]{3,5})?[ ]?\\}', - [ - ('{ "count": 1.05e1 }', False), - ('{ "count": 100.005e+001 }', True), - ('{ "count": 10000.00005e-00001 }', True), - ('{ "count": 100000.000005e0000001 }', False), - ], - ), - # array - ( - {"title": "Foo", "type": "array", "items": {"type": "number"}}, - rf"\[{WHITESPACE}(({NUMBER})(,{WHITESPACE}({NUMBER})){{0,}})?{WHITESPACE}\]", - [("[1e+9,1.3]", True), ("[]", True), ("[1", False)], - ), - # array with a set length of 1 - ( - { - "title": "Foo", - "type": "array", - "items": {"type": "integer"}, - "minItems": 1, - "maxItems": 1, - }, - rf"\[{WHITESPACE}(({INTEGER})(,{WHITESPACE}({INTEGER})){{0,0}}){WHITESPACE}\]", - [("[1]", True), ("[1,2]", False), ('["a"]', False), ("[]", False)], - ), - # array with a set length greather than 1 - ( - { - "title": "Foo", - "type": "array", - "items": {"type": "integer"}, - "minItems": 3, - "maxItems": 3, - }, - rf"\[{WHITESPACE}(({INTEGER})(,{WHITESPACE}({INTEGER})){{2,2}}){WHITESPACE}\]", - [("[1]", False), ("[]", False), ("[1,2,3]", True), ("[1,2,3,4]", False)], - ), - # array with length 0 - ( - { - "title": "Foo", - "type": "array", - "items": {"type": "integer"}, - "minItems": 0, - "maxItems": 0, - }, - rf"\[{WHITESPACE}\]", - [("[1]", False), ("[]", True), ("[1,2,3]", False), ("[1,2,3,4]", False)], - ), - # object - ( - { - "title": "TestSchema", - "type": "object", - "properties": { - "test_dict": { - "title": "Test Dict", - "additionalProperties": {"type": "string"}, - "type": "object", - } - }, - "required": ["test_dict"], - }, - rf"""\{{{WHITESPACE}"test_dict"{WHITESPACE}:{WHITESPACE}\{{{WHITESPACE}({STRING}{WHITESPACE}:{WHITESPACE}{STRING}({WHITESPACE},{WHITESPACE}{STRING}{WHITESPACE}:{WHITESPACE}{STRING}){{0,}})?{WHITESPACE}\}}{WHITESPACE}\}}""", - [ - ("""{ "test_dict":{"foo":"bar","baz": "bif"}}""", True), - ("""{ "test_dict":{"foo":"bar" }}""", True), - ("""{ "test_dict":{}}""", True), - ("""{ "WRONG_KEY":{}}""", False), - ("""{ "test_dict":{"wrong_type" 1}}""", False), - ], - ), - # object containing object - ( - { - "title": "TestSchema", - "type": "object", - "properties": { - "test_dict": { - "title": "Test Dict", - "additionalProperties": { - "additionalProperties": {"type": "integer"}, - "type": "object", - }, - "type": "object", - } - }, - "required": ["test_dict"], - }, - rf"""\{{{WHITESPACE}"test_dict"{WHITESPACE}:{WHITESPACE}\{{{WHITESPACE}({STRING}{WHITESPACE}:{WHITESPACE}\{{{WHITESPACE}({STRING}{WHITESPACE}:{WHITESPACE}{INTEGER}({WHITESPACE},{WHITESPACE}{STRING}{WHITESPACE}:{WHITESPACE}{INTEGER}){{0,}})?{WHITESPACE}\}}({WHITESPACE},{WHITESPACE}{STRING}{WHITESPACE}:{WHITESPACE}\{{{WHITESPACE}({STRING}{WHITESPACE}:{WHITESPACE}{INTEGER}({WHITESPACE},{WHITESPACE}{STRING}{WHITESPACE}:{WHITESPACE}{INTEGER}){{0,}})?{WHITESPACE}\}}){{0,}})?{WHITESPACE}\}}{WHITESPACE}\}}""", - [ - ( - """{"test_dict": {"foo": {"bar": 123, "apple": 99}, "baz": {"bif": 456}}}""", - True, - ), - ( - """{"test_dict": {"anykey": {"anykey": 123}, "anykey2": {"bif": 456}}}""", - True, - ), - ("""{"test_dict": {}}""", True), - ("""{"test_dict": {"dict of empty dicts are ok": {} }}""", True), - ( - """{"test_dict": {"anykey": {"ONLY Dict[Dict]": 123}, "No Dict[int]" 1: }}""", - False, - ), - ], - ), - # oneOf - ( - { - "title": "Foo", - "oneOf": [{"type": "string"}, {"type": "number"}, {"type": "boolean"}], - }, - rf'((?:"{STRING_INNER}*")|(?:{NUMBER})|(?:{BOOLEAN}))', - [ - ("12.3", True), - ("true", True), - ('"a"', True), - ("null", False), - ("", False), - ("12true", False), - ('1.3"a"', False), - ('12.3true"a"', False), - ], - ), - # anyOf - ( - { - "title": "Foo", - "anyOf": [{"type": "string"}, {"type": "integer"}], - }, - rf"({STRING}|{INTEGER})", - [("12", True), ('"a"', True), ('1"a"', False)], - ), - # allOf - ( - { - "title": "Foo", - "allOf": [{"type": "string"}, {"type": "integer"}], - }, - rf"({STRING}{INTEGER})", - [('"a"1', True), ('"a"', False), ('"1"', False)], - ), - # Tuple / prefixItems - ( - { - "title": "Foo", - "prefixItems": [{"type": "string"}, {"type": "integer"}], - }, - rf"\[{WHITESPACE}{STRING}{WHITESPACE},{WHITESPACE}{INTEGER}{WHITESPACE}\]", - [('["a", 1]', True), ('["a", 1, 1]', False), ("[]", False)], - ), - # Nested schema - ( - { - "title": "Bar", - "type": "object", - "properties": { - "fuzz": { - "title": "Foo", - "type": "object", - "properties": {"spam": {"title": "Spam", "type": "integer"}}, - "required": ["spam"], - } - }, - "required": ["fuzz"], - }, - f'\\{{[ ]?"fuzz"[ ]?:[ ]?\\{{[ ]?"spam"[ ]?:[ ]?{INTEGER}[ ]?\\}}[ ]?\\}}', - [('{ "fuzz": { "spam": 100 }}', True)], - ), - # Schema with a reference - ( - { - "title": "User", - "type": "object", - "properties": { - "user_id": {"title": "User Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "a": {"$ref": "#/properties/name"}, - }, - "required": ["user_id", "name", "a"], - }, - f'\\{{[ ]?"user_id"[ ]?:[ ]?{INTEGER}[ ]?,[ ]?"name"[ ]?:[ ]?{STRING}[ ]?,[ ]?"a"[ ]?:[ ]?{STRING}[ ]?\\}}', - [('{"user_id": 100, "name": "John", "a": "Marc"}', True)], - ), - ( - { - "title": "User", - "type": "object", - "$defs": {"name": {"title": "Name2", "type": "string"}}, - "properties": { - "user_id": {"title": "User Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "name2": {"$ref": "#/$defs/name"}, - }, - "required": ["user_id", "name", "name2"], - }, - f'\\{{[ ]?"user_id"[ ]?:[ ]?{INTEGER}[ ]?,[ ]?"name"[ ]?:[ ]?{STRING}[ ]?,[ ]?"name2"[ ]?:[ ]?{STRING}[ ]?\\}}', - [('{"user_id": 100, "name": "John", "name2": "Marc"}', True)], - ), - ( - { - "$id": "customer", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Customer", - "type": "object", - "properties": { - "name": {"type": "string"}, - "last_name": {"type": "string"}, - "address": {"$ref": "customer#/$defs/address"}, - }, - "required": [ - "name", - "first_name", - "last_name", - "address", - "shipping_address", - "billing_address", - ], - "$defs": { - "address": { - "title": "Address", - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "city": {"type": "string"}, - }, - "required": ["street_address", "city", "state"], - "definitions": { - "state": { - "type": "object", - "title": "State", - "properties": {"name": {"type": "string"}}, - "required": ["name"], - } - }, - } - }, - }, - f'\\{{[ ]?"name"[ ]?:[ ]?{STRING}[ ]?,[ ]?"last_name"[ ]?:[ ]?{STRING}[ ]?,[ ]?"address"[ ]?:[ ]?\\{{[ ]?"city"[ ]?:[ ]?{STRING}[ ]?\\}}[ ]?\\}}', - [ - ( - '{"name": "John", "last_name": "Doe", "address": {"city": "Paris"}}', - True, - ) - ], - ), - # Optional properties - # Last required property in first position - ( - { - "properties": { - "name": {"type": "string"}, - "age": {"anyOf": [{"type": "integer"}, {"type": "null"}]}, - "weapon": {"anyOf": [{"type": "string"}, {"type": "null"}]}, - }, - "required": ["name"], - "title": "Character", - "type": "object", - }, - f'\\{{[ ]?"name"[ ]?:[ ]?{STRING}([ ]?,[ ]?"age"[ ]?:[ ]?({INTEGER}|null))?([ ]?,[ ]?"weapon"[ ]?:[ ]?({STRING}|null))?[ ]?\\}}', - [ - ('{ "name" : "Player" }', True), - ('{ "name" : "Player", "weapon" : "sword" }', True), - ('{ "age" : 10, "weapon" : "sword" }', False), - ], - ), - # Last required property in middle position - ( - { - "properties": { - "name": {"type": "string"}, - "age": {"anyOf": [{"type": "integer"}, {"type": "null"}]}, - "weapon": {"type": "string"}, - "strength": {"anyOf": [{"type": "integer"}, {"type": "null"}]}, - }, - "required": ["name", "weapon"], - "title": "Character", - "type": "object", - }, - f'\\{{[ ]?"name"[ ]?:[ ]?{STRING}[ ]?,([ ]?"age"[ ]?:[ ]?({INTEGER}|null)[ ]?,)?[ ]?"weapon"[ ]?:[ ]?{STRING}([ ]?,[ ]?"strength"[ ]?:[ ]?({INTEGER}|null))?[ ]?\\}}', - [ - ('{ "name" : "Player" , "weapon" : "sword" }', True), - ( - '{ "name" : "Player", "age" : 10, "weapon" : "sword" , "strength" : 10 }', - True, - ), - ('{ "weapon" : "sword" }', False), - ], - ), - # Last required property in last position - ( - { - "properties": { - "name": {"anyOf": [{"type": "string"}, {"type": "null"}]}, - "age": {"type": "integer"}, - "armor": {"type": "string"}, - "strength": {"anyOf": [{"type": "integer"}, {"type": "null"}]}, - "weapon": {"title": "Weapon", "type": "string"}, - }, - "required": ["age", "armor", "weapon"], - "title": "Character", - "type": "object", - }, - f'\\{{([ ]?"name"[ ]?:[ ]?({STRING}|null)[ ]?,)?[ ]?"age"[ ]?:[ ]?{INTEGER}[ ]?,[ ]?"armor"[ ]?:[ ]?{STRING}[ ]?,([ ]?"strength"[ ]?:[ ]?({INTEGER}|null)[ ]?,)?[ ]?"weapon"[ ]?:[ ]?{STRING}[ ]?\\}}', - [ - ( - '{ "name" : "Player", "age" : 10, "armor" : "plate", "strength" : 11, "weapon" : "sword" }', - True, - ), - ('{ "age" : 10, "armor" : "plate", "weapon" : "sword" }', True), - ( - '{ "name" : "Kahlhanbeh", "armor" : "plate", "weapon" : "sword" }', - False, - ), - ], - ), - # All properties are optional - ( - { - "properties": { - "name": {"anyOf": [{"type": "string"}, {"type": "null"}]}, - "age": {"anyOf": [{"type": "integer"}, {"type": "null"}]}, - "strength": {"anyOf": [{"type": "integer"}, {"type": "null"}]}, - }, - "title": "Character", - "type": "object", - }, - f'\\{{([ ]?"name"[ ]?:[ ]?({STRING}|null)([ ]?,[ ]?"age"[ ]?:[ ]?({INTEGER}|null))?([ ]?,[ ]?"strength"[ ]?:[ ]?({INTEGER}|null))?|([ ]?"name"[ ]?:[ ]?({STRING}|null)[ ]?,)?[ ]?"age"[ ]?:[ ]?({INTEGER}|null)([ ]?,[ ]?"strength"[ ]?:[ ]?({INTEGER}|null))?|([ ]?"name"[ ]?:[ ]?({STRING}|null)[ ]?,)?([ ]?"age"[ ]?:[ ]?({INTEGER}|null)[ ]?,)?[ ]?"strength"[ ]?:[ ]?({INTEGER}|null))?[ ]?\\}}', - [ - ('{ "name" : "Player" }', True), - ('{ "name" : "Player", "age" : 10, "strength" : 10 }', True), - ('{ "age" : 10, "strength" : 10 }', True), - ("{ }", True), - ], - ), - ], -) -def test_match(schema, regex, examples): - interegular.parse_pattern(regex) - schema = json.dumps(schema) - test_regex = build_regex_from_schema(schema) - assert test_regex == regex - - for string, does_match in examples: - match = re.fullmatch(test_regex, string) - if does_match: - if match is None: - raise ValueError(f"Expected match for '{string}'") - assert match[0] == string - assert match.span() == (0, len(string)) - else: - assert match is None - - -@pytest.mark.parametrize( - "schema,regex,examples", - [ - # UUID - ( - {"title": "Foo", "type": "string", "format": "uuid"}, - UUID, - [ - ("123e4567-e89b-12d3-a456-426614174000", False), - ('"123e4567-e89b-12d3-a456-426614174000"', True), - ('"123e4567-e89b-12d3-a456-42661417400"', False), - ('"123e4567-e89b-12d3-a456-42661417400g"', False), - ('"123e4567-e89b-12d3-a456-42661417400-"', False), - ('""', False), - ], - ), - # DATE-TIME - ( - {"title": "Foo", "type": "string", "format": "date-time"}, - DATE_TIME, - [ - ("2018-11-13T20:20:39Z", False), - ('"2018-11-13T20:20:39Z"', True), - ('"2016-09-18T17:34:02.666Z"', True), - ('"2008-05-11T15:30:00Z"', True), - ('"2021-01-01T00:00:00"', True), - ('"2022-01-10 07:19:30"', False), # missing T - ('"2022-12-10T10-04-29"', False), # incorrect separator - ('"2023-01-01"', False), - ], - ), - # DATE - ( - {"title": "Foo", "type": "string", "format": "date"}, - DATE, - [ - ("2018-11-13", False), - ('"2018-11-13"', True), - ('"2016-09-18"', True), - ('"2008-05-11"', True), - ('"2015-13-01"', False), # incorrect month - ('"2022-01"', False), # missing day - ('"2022/12/01"', False), # incorrect separator" - ], - ), - # TIME - ( - {"title": "Foo", "type": "string", "format": "time"}, - TIME, - [ - ("20:20:39Z", False), - ('"20:20:39Z"', True), - ('"15:30:00Z"', True), - ('"25:30:00"', False), # incorrect hour - ('"15:30"', False), # missing seconds - ('"15:30:00.000"', False), # missing Z - ('"15-30-00"', False), # incorrect separator - ('"15:30:00+01:00"', False), # incorrect separator - ], - ), - ], -) -def test_format(schema, regex, examples): - interegular.parse_pattern(regex) - schema = json.dumps(schema) - test_regex = build_regex_from_schema(schema) - assert test_regex == regex - - for string, does_match in examples: - match = re.fullmatch(test_regex, string) - if does_match: - assert match[0] == string - assert match.span() == (0, len(string)) - else: - assert match is None - - -@pytest.mark.parametrize( - "schema,examples", - [ - # NESTED UUID - ( - { - "title": "Foo", - "type": "object", - "properties": {"uuid": {"type": "string", "format": "uuid"}}, - }, - [ - ('{"uuid": "123e4567-e89b-12d3-a456-426614174000"}', True), - ('{"uuid":"123e4567-e89b-12d3-a456-42661417400"}', False), - ('{"uuid":"123e4567-e89b-12d3-a456-42661417400g"}', False), - ('{"uuid":"123e4567-e89b-12d3-a456-42661417400-"}', False), - ( - '{"uuid":123e4567-e89b-12d3-a456-426614174000}', - False, - ), # missing quotes for value - ('{"uuid":""}', False), - ], - ), - # NESTED DATE-TIME - ( - { - "title": "Foo", - "type": "object", - "properties": {"dateTime": {"type": "string", "format": "date-time"}}, - }, - [ - ('{"dateTime": "2018-11-13T20:20:39Z"}', True), - ('{"dateTime":"2016-09-18T17:34:02.666Z"}', True), - ('{"dateTime":"2008-05-11T15:30:00Z"}', True), - ('{"dateTime":"2021-01-01T00:00:00"}', True), - ('{"dateTime":"2022-01-10 07:19:30"}', False), # missing T - ('{"dateTime":"2022-12-10T10-04-29"}', False), # incorrect separator - ( - '{"dateTime":2018-11-13T20:20:39Z}', - False, - ), # missing quotes for value - ('{"dateTime":"2023-01-01"}', False), - ], - ), - # NESTED DATE - ( - { - "title": "Foo", - "type": "object", - "properties": {"date": {"type": "string", "format": "date"}}, - }, - [ - ('{"date": "2018-11-13"}', True), - ('{"date":"2016-09-18"}', True), - ('{"date":"2008-05-11"}', True), - ('{"date":"2015-13-01"}', False), # incorrect month - ('{"date":"2022-01"}', False), # missing day - ('{"date":"2022/12/01"}', False), # incorrect separator" - ('{"date":2018-11-13}', False), # missing quotes for value - ], - ), - # NESTED TIME - ( - { - "title": "Foo", - "type": "object", - "properties": {"time": {"type": "string", "format": "time"}}, - }, - [ - ('{"time": "20:20:39Z"}', True), - ('{"time":"15:30:00Z"}', True), - ('{"time":"25:30:00"}', False), # incorrect hour - ('{"time":"15:30"}', False), # missing seconds - ('{"time":"15:30:00.000"}', False), # missing Z - ('{"time":"15-30-00"}', False), # incorrect separator - ('{"time":"15:30:00+01:00"}', False), # incorrect separator - ('{"time":20:20:39Z}', False), # missing quotes for value - ], - ), - # Unconstrained Object - ( - { - "title": "Foo", - "type": "object", - }, - [ - ("{}", True), - ('{"a": 1, "b": null}', True), - ('{"a": {"z": {"g": 4}}, "b": null}', True), - ("1234", False), # not an object - ('["a", "a"]', False), # not an array - ], - ), - # Unconstrained Array - ( - { - "type": "array", - }, - [ - ("[1, {}, false]", True), - ("[{}]", True), - ('[{"a": {"z": "q"}, "b": null}]', True), - ('[{"a": [1, 2, true], "b": null}]', True), - ('[{"a": [1, 2, true], "b": {"a": "b"}}, 1, true, [1, [2]]]', True), - # too deep, default unconstrained depth limit = 2 - ( - '[{"a": [1, 2, true], "b": {"a": "b"}}, 1, true, [1, [2, [3]]]]', - False, - ), - ('[{"a": {"z": {"g": 4}}, "b": null}]', False), - ("[[[[1]]]]", False), - # not an array - ("{}", False), - ('{"a": 1, "b": null}', False), - ('{"a": {"z": {"g": 4}}, "b": null}', False), - ("1234", False), # not an array - ('{"a": "a"}', False), # not an array - ], - ), - # No schema / unconstrained value - ( - {}, - [ - ('"aaabbuecuh"', True), # string - ("5.554", True), # number - ("true", True), # boolean - ("null", True), # null - ("5999", True), # integer - ('["a", "b"]', True), # array - ('{"key": {"k2": "value"}}', True), # nested object - ("this isnt valid json", False), - ], - ), - ], -) -def test_format_without_regex(schema, examples): - schema = json.dumps(schema) - test_regex = build_regex_from_schema(schema) - for string, does_match in examples: - match = re.fullmatch(test_regex, string) - if does_match: - assert match[0] == string - assert match.span() == (0, len(string)) - else: - assert match is None - - -@pytest.mark.parametrize("whitespace_pattern", [None, r"[\n ]*", "abc"]) -def test_json_schema_custom_whitespace_pattern(whitespace_pattern): - """assert whitespace_pattern setting respected""" - - class MockModel(BaseModel): - foo: int - bar: str - - schema = json.dumps(MockModel.model_json_schema()) - - # assert any ws pattern can be used - if whitespace_pattern == "abc": - build_regex_from_schema(schema, whitespace_pattern) - return - - pattern = build_regex_from_schema(schema, whitespace_pattern) - - mock_result_mult_ws = ( - """{ "foo" : 4, \n\n\n "bar": "baz baz baz bar"\n\n}""" - ) - mock_result_maybe_ws = """{"foo" : 4 ,"bar":"baz baz baz bar"}""" - - match_default_ws = re.fullmatch(pattern, mock_result_maybe_ws) - if whitespace_pattern is None: - assert match_default_ws - else: - assert re.fullmatch(pattern, mock_result_mult_ws) - - -def test_one_of_doesnt_produce_illegal_lookaround(): - """Reproduces failure in https://github.com/dottxt-ai/outlines/issues/823""" - - class Cat(BaseModel): - pet_type: Literal["cat"] - meows: int - - class Dog(BaseModel): - pet_type: Literal["dog"] - barks: float - - class Model(BaseModel): - pet: Union[Cat, Dog] = Field(..., discriminator="pet_type") - n: int - - json_schema = Model.schema_json() - - json_schema = Model.schema_json() - pattern = build_regex_from_schema(json_schema, whitespace_pattern=None) - - # check if the pattern uses lookarounds incompatible with interegular.Pattern.to_fsm() - interegular.parse_pattern(pattern).to_fsm() + regex_str = build_regex_from_schema(schema) + assert isinstance(regex_str, str) def add(a: float, b: float) -> float: diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index 92c5d789c..7b7973d23 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -363,6 +363,7 @@ def mul(c: float, d: float) -> float: return c * d +@pytest.mark.xfail(reason="Enum of objects are not supported in outlines-core") def test_transformers_json_function_enum(model): prompt = "Output some JSON " From 5113f9415f999f8ba64a4332b25078d90c89a1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 9 Dec 2024 13:20:31 +0100 Subject: [PATCH 123/505] Convert `enum` of objects to `oneOf` with consts The `enum` keyword is reserved for arrays of constants. When we have callables as members of a Python `Enum`, we get an object whose properties are the arguments of the callable. The `Enum` thus needs to be converted into a `oneOf` field. --- outlines/fsm/json_schema.py | 6 ++++-- tests/fsm/test_json_schema.py | 10 ++++++---- tests/generate/test_integration_transformers.py | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/outlines/fsm/json_schema.py b/outlines/fsm/json_schema.py index bae0ad17a..578ee7626 100644 --- a/outlines/fsm/json_schema.py +++ b/outlines/fsm/json_schema.py @@ -74,8 +74,10 @@ def get_schema_from_enum(myenum: type[Enum]) -> dict: f"Your enum class {myenum.__name__} has 0 members. If you are working with an enum of functions, do not forget to register them as callable (using `partial` for instance)" ) choices = [ - get_schema_from_signature(elt.value.func) if callable(elt.value) else elt.value + get_schema_from_signature(elt.value.func) + if callable(elt.value) + else {"const": elt.value} for elt in myenum ] - schema = {"title": myenum.__name__, "enum": choices} + schema = {"title": myenum.__name__, "oneOf": choices} return schema diff --git a/tests/fsm/test_json_schema.py b/tests/fsm/test_json_schema.py index 825433397..23864e029 100644 --- a/tests/fsm/test_json_schema.py +++ b/tests/fsm/test_json_schema.py @@ -69,8 +69,10 @@ class EmptyEnum(Enum): ) def test_enum_schema(enum, expectation): with expectation: - result = get_schema_from_enum(enum) - assert result["title"] == enum.__name__ - assert len(result["enum"]) == len(enum) - for elt in result["enum"]: + schema = get_schema_from_enum(enum) + regex_str = build_regex_from_schema(json.dumps(schema)) + assert isinstance(regex_str, str) + assert schema["title"] == enum.__name__ + assert len(schema["oneOf"]) == len(enum) + for elt in schema["oneOf"]: assert type(elt) in [int, float, bool, type(None), str, dict] diff --git a/tests/generate/test_integration_transformers.py b/tests/generate/test_integration_transformers.py index 7b7973d23..92c5d789c 100644 --- a/tests/generate/test_integration_transformers.py +++ b/tests/generate/test_integration_transformers.py @@ -363,7 +363,6 @@ def mul(c: float, d: float) -> float: return c * d -@pytest.mark.xfail(reason="Enum of objects are not supported in outlines-core") def test_transformers_json_function_enum(model): prompt = "Output some JSON " From 147b03e8d4d7279177fdaac82be4646d10d8c8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 9 Dec 2024 13:43:33 +0100 Subject: [PATCH 124/505] Fail as soon as one test fails --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36e6f8526..f54a888fa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: echo "::set-output name=id::$MATRIX_ID" - name: Run tests run: | - pytest --cov=outlines + pytest -x --cov=outlines env: COVERAGE_FILE: .coverage.${{ steps.matrix-id.outputs.id }} - name: Upload coverage data From fb43f4fb7f711cc765e78671e991e1eecd51d38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 9 Dec 2024 14:55:12 +0100 Subject: [PATCH 125/505] Fix conversion from MLX to torch tensor --- outlines/processors/base_logits_processor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index eec7de121..44b55af2e 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -107,9 +107,12 @@ def _to_torch(tensor_like: Array) -> torch.Tensor: return torch.tensor(tensor_like) elif is_mlx_array_type(type(tensor_like)): - # mlx -> torch -> mlx conversion docs: - # https://ml-explore.github.io/mlx/build/html/usage/numpy.html - return torch.from_dlpack(tensor_like) + import mlx.core as mx + + # https://ml-explore.github.io/mlx/build/html/usage/numpy.html#pytorch + return torch.from_dlpack( + np.array(tensor_like.astype(mx.float32), copy=False) + ) elif is_jax_array_type(type(tensor_like)): import jax From 9df3b1b2d6c9d4b78551b759758ce5602995ce97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 9 Dec 2024 22:05:26 +0100 Subject: [PATCH 126/505] Bump outlines-core to 0.1.24 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2001f4c40..896e8aadd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycountry", "airportsdata", "torch", - "outlines_core==0.1.20", + "outlines_core==0.1.24", ] dynamic = ["version"] From 05bcb4b0370e6a7b2136092ac1d1b599a5660cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Wed, 11 Dec 2024 14:44:34 +0100 Subject: [PATCH 127/505] Bump `outlines-core` version to 0.1.25 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 896e8aadd..b83275f89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycountry", "airportsdata", "torch", - "outlines_core==0.1.24", + "outlines_core==0.1.25", ] dynamic = ["version"] From 5c41fb3599cbd8771d5aae072bda837717d0742d Mon Sep 17 00:00:00 2001 From: Sidharth Rajaram Date: Thu, 12 Dec 2024 16:24:26 -0800 Subject: [PATCH 128/505] Bump outlines-core version to 0.1.26 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b83275f89..1a6d08e54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycountry", "airportsdata", "torch", - "outlines_core==0.1.25", + "outlines_core==0.1.26", ] dynamic = ["version"] From 365b566e9f26d7db2aef399c7e987f3b9358b6d5 Mon Sep 17 00:00:00 2001 From: Avishkar Gupta Date: Mon, 16 Dec 2024 15:14:36 +0530 Subject: [PATCH 129/505] Fix image link in receipt-digitization example The current link points to a HTML instead of a PNG, so after it's downloaded via requests.get, PIL isn't able to recognize the format and rightly so. --- docs/cookbook/receipt-digitization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/receipt-digitization.md b/docs/cookbook/receipt-digitization.md index 67830fa81..0fa256db7 100644 --- a/docs/cookbook/receipt-digitization.md +++ b/docs/cookbook/receipt-digitization.md @@ -117,7 +117,7 @@ Here's what the image looks like: ```python # Path to the image -image_path = "https://dottxt-ai.github.io/outlines/main/cookbook/images/trader-joes-receipt.png" +image_path = "https://raw.githubusercontent.com/dottxt-ai/outlines/refs/heads/main/docs/cookbook/images/trader-joes-receipt.jpg" # Download the image response = requests.get(image_path) From 9b586dbda46bce06363bfb26995e35255b58f55e Mon Sep 17 00:00:00 2001 From: Aakash Thatte <84656834+sky-2002@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:37:50 +0530 Subject: [PATCH 130/505] Move generation of sampling params (#1340) This PR addresses #1335 I have kept the `SamplingParameters` in `outlines/generate/api.py` cause its imported at many places and may break examples ig. Would you want to change that @dgerlanc ?? Otherwise if this looks good, I can make docs changes if needd, lmk. --- outlines/generate/api.py | 28 +++++++--------------------- outlines/samplers.py | 30 ++++++++++++++++++++++++++++++ tests/test_samplers.py | 23 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/outlines/generate/api.py b/outlines/generate/api.py index 4919f2090..5d3c52a8a 100644 --- a/outlines/generate/api.py +++ b/outlines/generate/api.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union from outlines.generate.generator import sequence_generator -from outlines.samplers import BeamSearchSampler, GreedySampler, MultinomialSampler if TYPE_CHECKING: import torch @@ -353,11 +352,13 @@ def token_generator() -> Iterator[Union[List[str], str, List[List[str]]]]: ] generated_sequences = [ - self.format_sequence( - self.strip_stop_sequences(sequence, stop_sequences) + ( + self.format_sequence( + self.strip_stop_sequences(sequence, stop_sequences) + ) + if stop + else sequence ) - if stop - else sequence for sequence, stop in zip( generated_sequences, is_stop_at_reached ) @@ -428,22 +429,7 @@ def __init__(self, model, logits_processor, sampler): self.model = model self.logits_processor = logits_processor - if isinstance(sampler, MultinomialSampler): - self.sampling_params = SamplingParameters( - "multinomial", - sampler.samples, - sampler.top_p, - sampler.top_k, - sampler.temperature, - ) - elif isinstance(sampler, GreedySampler): - self.sampling_params = SamplingParameters( - "greedy", sampler.samples, None, None, 0.0 - ) - elif isinstance(sampler, BeamSearchSampler): - self.sampling_params = SamplingParameters( - "beam_search", sampler.samples, None, None, 1.0 - ) + self.sampling_params = sampler.sampling_params def prepare_generation_parameters( self, diff --git a/outlines/samplers.py b/outlines/samplers.py index b1421971f..3ab1728fc 100644 --- a/outlines/samplers.py +++ b/outlines/samplers.py @@ -1,4 +1,5 @@ import math +from dataclasses import dataclass from typing import TYPE_CHECKING, Callable, Optional, Protocol, Tuple if TYPE_CHECKING: @@ -17,6 +18,17 @@ def __call__( ... +@dataclass(frozen=True) +class SamplingParameters: + """Sampling parameters available in Outlines.""" + + sampler: str + num_samples: int = 1 + top_p: Optional[float] = None + top_k: Optional[int] = None + temperature: Optional[float] = None + + class GreedySampler: """Greedy Sampling algorithm. @@ -76,6 +88,10 @@ def __call__( return next_token_ids, ancestors, weights + @property + def sampling_params(self): + return SamplingParameters("greedy", self.samples, None, None, 0.0) + greedy = GreedySampler @@ -161,6 +177,16 @@ def __call__( return next_token_ids, ancestors, weights + @property + def sampling_params(self): + return SamplingParameters( + "multinomial", + self.samples, + self.top_p, + self.top_k, + self.temperature, + ) + multinomial = MultinomialSampler @@ -320,5 +346,9 @@ def __call__( return next_token_ids, ancestors, weights + @property + def sampling_params(self): + return SamplingParameters("beam_search", self.samples, None, None, 1.0) + beam_search = BeamSearchSampler diff --git a/tests/test_samplers.py b/tests/test_samplers.py index 88cdb0fbc..10a7be26f 100644 --- a/tests/test_samplers.py +++ b/tests/test_samplers.py @@ -47,6 +47,13 @@ def test_greedy(): assert ancestors.equal(torch.tensor([0, 1])) assert weights.equal(torch.tensor([logprobs[0, 0], logprobs[1, 2]])) + params = sampler.sampling_params + assert params.sampler == "greedy" + assert params.num_samples == 1 + assert params.top_p is None + assert params.top_k is None + assert params.temperature == 0.0 + def test_multinomial(): rng = torch.Generator() @@ -72,6 +79,14 @@ def test_multinomial(): assert ancestors.equal(torch.tensor([0, 1])) assert weights.equal(torch.tensor([logprobs[0, 0], logprobs[1, 2]])) + sampler = MultinomialSampler(samples=5, top_k=10, top_p=0.9, temperature=0.8) + params = sampler.sampling_params + assert params.sampler == "multinomial" + assert params.num_samples == 5 + assert params.top_p == 0.9 + assert params.top_k == 10 + assert params.temperature == 0.8 + def test_multinomial_init(): sampler = MultinomialSampler() @@ -252,3 +267,11 @@ def test_beam_search(): ] ) ) + + sampler = BeamSearchSampler(beams=3) + params = sampler.sampling_params + assert params.sampler == "beam_search" + assert params.num_samples == 3 + assert params.top_p is None + assert params.top_k is None + assert params.temperature == 1.0 From 061f2ba1966222e9b59a19b6fe4b8b9cefd61686 Mon Sep 17 00:00:00 2001 From: Tim Zaman Date: Thu, 19 Dec 2024 15:46:25 -0800 Subject: [PATCH 131/505] Fix unresolved imports in transformers doc --- docs/reference/models/transformers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index f4c319540..0b0add226 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -133,7 +133,7 @@ T5 Example: import outlines from transformers import AutoModelForSeq2SeqLM -model_pile_t5 = models.transformers( +model_pile_t5 = outlines.models.transformers( model_name="EleutherAI/pile-t5-large", model_class=AutoModelForSeq2SeqLM, ) @@ -141,7 +141,7 @@ model_pile_t5 = models.transformers( Bart Example: ```python -model_bart = models.transformers( +model_bart = outlines.models.transformers( model_name="facebook/bart-large", model_class=AutoModelForSeq2SeqLM, ) From 9744f00aa7a803c6637175e2f640f01f144aa8c7 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:19:12 +0100 Subject: [PATCH 132/505] Set docs `docstring_style` (#1343) Hi, Thank you for this great library! It seems that the docstrings are not rendered correctly in the docs. I think we should explicitly set the `docstring_style` because [it defaults to `"google"`](https://mkdocstrings.github.io/python/usage/configuration/docstrings/#docstring_style) but outlines is using numpy. Before: ![Screenshot 2024-12-16 at 23 00 26](https://github.com/user-attachments/assets/c752ee3d-519e-4098-b943-3aab43c8af25) After: ![Screenshot 2024-12-16 at 23 00 41](https://github.com/user-attachments/assets/5b5f524b-6921-4dbe-994d-72c079e677bc) There seem to be other issues in the docstrings: - for example [`Properties`](https://github.com/dottxt-ai/outlines/blob/main/outlines/models/openai.py#L23) should be [`Attributes`](https://numpydoc.readthedocs.io/en/latest/format.html#parameters) - only openai and transformers models are present in the [api reference](https://github.com/dottxt-ai/outlines/blob/main/docs/api/models.md) I'm happy to make followup PRs for those. Please let me know if I missed something, I couldn't find related issues/PRs. --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 4888309b9..cca3e3af1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -87,6 +87,7 @@ plugins: handlers: python: options: + docstring_style: numpy show_submodules: true - search - section-index From 6205c0b367a8c226f219a2b400c9695d63a6122d Mon Sep 17 00:00:00 2001 From: Marco De Nadai Date: Sat, 28 Dec 2024 11:54:14 +0100 Subject: [PATCH 133/505] Fix JSON structured import --- examples/vllm_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vllm_integration.py b/examples/vllm_integration.py index 5f833d19f..c9ce057bc 100644 --- a/examples/vllm_integration.py +++ b/examples/vllm_integration.py @@ -3,7 +3,7 @@ import vllm from pydantic import BaseModel -from outlines.integrations.vllm import JSONLogitsProcessor +from outlines.processors.structured import JSONLogitsProcessor class Person(BaseModel): From fef70c0febf26521e81f1b072900f7b529a2a49f Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 31 Dec 2024 02:05:11 +0800 Subject: [PATCH 134/505] Update mkdocs.yml: jump to mamba section --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index cca3e3af1..7a3ca1e98 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -161,7 +161,7 @@ nav: - TGI: reference/models/tgi.md - ExllamaV2: reference/models/exllamav2.md - MLX: reference/models/mlxlm.md - - Mamba: reference/models/transformers.md + - Mamba: reference/models/transformers/#mamba - API: - OpenAI: reference/models/openai.md - API Reference: From 59208706d3d502e2a50b82fc13d4975b68ad457f Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 31 Dec 2024 02:19:13 +0800 Subject: [PATCH 135/505] Update transformers.md: fix headings level for mkdocs toc --- docs/reference/models/transformers.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/models/transformers.md b/docs/reference/models/transformers.md index 0b0add226..3649b9b13 100644 --- a/docs/reference/models/transformers.md +++ b/docs/reference/models/transformers.md @@ -30,7 +30,7 @@ tokenizer = AutoTokenizer.from_pretrained("gpt2") model = models.Transformers(llm, tokenizer) ``` -# Using Logits Processors +## Using Logits Processors There are two ways to use Outlines Structured Generation with HuggingFace Transformers: @@ -39,7 +39,7 @@ There are two ways to use Outlines Structured Generation with HuggingFace Transf Outlines supports a myriad of logits processors for structured generation. In these example, we will use the `RegexLogitsProcessor` which guarantees generated text matches the specified pattern. -## Using `outlines.models.transformers` +### Using `outlines.models.transformers` ```python import outlines @@ -54,7 +54,7 @@ print(output) # 2:30 pm ``` -## Using models initialized via the `transformers` library +### Using models initialized via the `transformers` library ```python import outlines @@ -85,7 +85,7 @@ print(output) [transformers]: https://github.com/huggingface/transformers -# Alternative Model Classes +## Alternative Model Classes `outlines.models.transformers` defaults to `transformers.AutoModelForCausalLM`, which is the appropriate class for most standard large language models, including Llama 3, Mistral, Phi-3, etc. From fddfc8f92da4021e58a57af545934b92d79157c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20H=C3=B6sler?= Date: Thu, 28 Nov 2024 13:16:25 +0100 Subject: [PATCH 136/505] remove non_blocking=True --- outlines/processors/structured.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index 64892b73f..ede9f5f8d 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -109,7 +109,7 @@ def process_logits( batch_indices = [] for i, guide_state in enumerate(sequence_states): allowed_tokens = self.guide.get_next_instruction(guide_state).tokens.to( - mask.device, non_blocking=True + mask.device ) allowed_tokens_batch.append(allowed_tokens) batch_indices.append( From 2f0740e3ef1df21be19bf01031ac86bd42fc326e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20H=C3=B6sler?= Date: Thu, 5 Dec 2024 15:24:00 +0100 Subject: [PATCH 137/505] optimize tensor creation --- outlines/processors/structured.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index ede9f5f8d..583fcc98f 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -103,22 +103,19 @@ def process_logits( sequence_states.append(self._guide_states[curr_state_key]) - mask = torch.ones_like(logits, dtype=torch.bool) - allowed_tokens_batch = [] batch_indices = [] for i, guide_state in enumerate(sequence_states): - allowed_tokens = self.guide.get_next_instruction(guide_state).tokens.to( - mask.device - ) + allowed_tokens = self.guide.get_next_instruction(guide_state).tokens allowed_tokens_batch.append(allowed_tokens) batch_indices.append( torch.full_like(allowed_tokens, i) ) # Store batch index for each allowed token - allowed_tokens_concat = torch.cat(allowed_tokens_batch) - batch_indices_concat = torch.cat(batch_indices) + allowed_tokens_concat = torch.cat(allowed_tokens_batch).to(logits.device) + batch_indices_concat = torch.cat(batch_indices).to(logits.device) + mask = torch.ones_like(logits, dtype=torch.bool) mask[batch_indices_concat, allowed_tokens_concat] = False logits.masked_fill_(mask, float("-inf")) From 6a8612bd10525e95a62d9f9c727d3e54d0723f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20H=C3=B6sler?= Date: Thu, 5 Dec 2024 21:18:32 +0100 Subject: [PATCH 138/505] add mps to processor benchmark --- benchmarks/bench_processors.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/benchmarks/bench_processors.py b/benchmarks/bench_processors.py index db1e4a8f1..02ea52b79 100644 --- a/benchmarks/bench_processors.py +++ b/benchmarks/bench_processors.py @@ -37,15 +37,12 @@ def get_mock_processor_inputs(array_library, num_tokens=30000): logits: (4, 30,000 ) dtype=float input_ids shape: (4, 2048) dtype=int """ - if array_library == "torch": - logits = torch.rand((4, num_tokens), dtype=torch.float) - input_ids = torch.randint( - low=0, high=num_tokens, size=(4, 2048), dtype=torch.int - ) - elif array_library == "torch_cuda": - logits = torch.rand((4, num_tokens), dtype=torch.float, device="cuda") + if array_library.startswith("torch"): + device = array_library.split("_")[1] if "_" in array_library else "cpu" + + logits = torch.rand((4, num_tokens), dtype=torch.float, device=device) input_ids = torch.randint( - low=0, high=num_tokens, size=(4, 2048), dtype=torch.int, device="cuda" + low=0, high=num_tokens, size=(4, 2048), dtype=torch.int, device=device ) elif array_library == "numpy": logits = np.random.rand(4, num_tokens).astype(np.float32) @@ -88,6 +85,8 @@ class LogitsProcessorPassthroughBenchmark: params += ["mlx"] if torch.cuda.is_available(): params += ["torch_cuda"] + if torch.mps.is_available(): + params += ["torch_mps"] if is_jax_allowed(): params += ["jax"] @@ -108,9 +107,10 @@ class LogitsProcessorStructuredBenchmark: array_libraries = ["torch", "numpy"] if is_mlx_lm_allowed(): array_libraries += ["mlx"] - # PR TODO if torch.cuda.is_available(): array_libraries += ["torch_cuda"] + if torch.mps.is_available(): + array_libraries += ["torch_mps"] # accept very many or very few tokens, respectively patterns = [r"[^Z]*", "Z*"] From d32dfde3ae7eac71f57af96d5141975ef9322b12 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Thu, 2 Jan 2025 18:52:10 +0100 Subject: [PATCH 139/505] Skip `vllm`-related tests on non-linux platforms Fix #1356 --- pyproject.toml | 2 +- tests/conftest.py | 14 ++++++++++++++ tests/generate/test_integration_vllm.py | 6 +++++- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 1a6d08e54..28060f797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ test = [ "huggingface_hub", "openai>=1.0.0", "datasets", - "vllm; sys_platform != 'darwin'", + "vllm; sys_platform == 'linux'", "transformers", "pillow", "exllamav2", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..bbc56afdb --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import sys + +import pytest + + +def pytest_collection_modifyitems(config, items): + if sys.platform != "linux": + skip_vllm = pytest.mark.skip(reason="vLLM models can only be run on Linux.") + for item in items: + if "test_integration_vllm" in item.nodeid: + item.add_marker(skip_vllm) + print( + f"WARNING: {item.nodeid} is skipped because vLLM only supports Linux platform (including WSL)." + ) diff --git a/tests/generate/test_integration_vllm.py b/tests/generate/test_integration_vllm.py index 4634bc839..d812d3cf2 100644 --- a/tests/generate/test_integration_vllm.py +++ b/tests/generate/test_integration_vllm.py @@ -4,7 +4,11 @@ import pytest import torch from pydantic import BaseModel, constr -from vllm.sampling_params import SamplingParams + +try: + from vllm.sampling_params import SamplingParams +except ImportError: + pass import outlines.generate as generate import outlines.grammars as grammars From 3cc399d3d6d2a1b0b2242d9434f4d6ab15ccc30e Mon Sep 17 00:00:00 2001 From: Tyler Thomas <36181311+tylerjthomas9@users.noreply.github.com> Date: Fri, 3 Jan 2025 22:43:54 -0700 Subject: [PATCH 140/505] Fix `json_schema` imports in cot example --- docs/cookbook/chain_of_thought.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cookbook/chain_of_thought.md b/docs/cookbook/chain_of_thought.md index b814ae048..5ac18af25 100644 --- a/docs/cookbook/chain_of_thought.md +++ b/docs/cookbook/chain_of_thought.md @@ -80,8 +80,8 @@ json_schema = Reasoning.model_json_schema() We could generate a response using the json schema but for a change we will use the regex: ```python -from outlines.integrations.utils import convert_json_schema_to_str -from outlines.fsm.json_schema import build_regex_from_schema +from outlines.fsm.json_schema import convert_json_schema_to_str +from outlines_core.fsm.json_schema import build_regex_from_schema schema_str = convert_json_schema_to_str(json_schema=json_schema) regex_str = build_regex_from_schema(schema_str) From 4456f3ccba277aae18f013b429a6cc74e6655b6c Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Thu, 9 Jan 2025 17:11:11 +0100 Subject: [PATCH 141/505] Add `from_file` class method to the `Prompt` object --- outlines/__init__.py | 4 +- outlines/prompts.py | 143 ++++++++++++++++++++++++++++-------------- tests/test_prompts.py | 96 ++++++++++++++++++++++++++-- 3 files changed, 189 insertions(+), 54 deletions(-) diff --git a/outlines/__init__.py b/outlines/__init__.py index 307d2ba6f..eeba78dc7 100644 --- a/outlines/__init__.py +++ b/outlines/__init__.py @@ -1,4 +1,5 @@ """Outlines is a Generative Model Programming Framework.""" + import outlines.generate import outlines.grammars import outlines.models @@ -7,7 +8,7 @@ from outlines.base import vectorize from outlines.caching import clear_cache, disable_cache, get_cache from outlines.function import Function -from outlines.prompts import prompt +from outlines.prompts import Prompt, prompt __all__ = [ "clear_cache", @@ -15,6 +16,7 @@ "get_cache", "Function", "prompt", + "Prompt", "vectorize", "grammars", ] diff --git a/outlines/prompts.py b/outlines/prompts.py index a7824451a..2110630c9 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -1,12 +1,14 @@ import functools import inspect import json +import os import re import textwrap from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional, Type, cast +from pathlib import Path +from typing import Any, Callable, Dict, Optional, Type, cast -from jinja2 import Environment, StrictUndefined +import jinja2 from pydantic import BaseModel @@ -19,12 +21,8 @@ class Prompt: """ - template: str - signature: inspect.Signature - - def __post_init__(self): - self.parameters: List[str] = list(self.signature.parameters.keys()) - self.jinja_environment = create_jinja_template(self.template) + template: jinja2.Template + signature: Optional[inspect.Signature] def __call__(self, *args, **kwargs) -> str: """Render and return the template. @@ -34,12 +32,93 @@ def __call__(self, *args, **kwargs) -> str: The rendered template as a Python ``str``. """ - bound_arguments = self.signature.bind(*args, **kwargs) - bound_arguments.apply_defaults() - return self.jinja_environment.render(**bound_arguments.arguments) + if self.signature is not None: + bound_arguments = self.signature.bind(*args, **kwargs) + bound_arguments.apply_defaults() + return self.template.render(**bound_arguments.arguments) + else: + return self.template.render(**kwargs) - def __str__(self): - return self.template + @classmethod + def from_str(cls, content: str): + """ + Create an instance of the class from a string. + + Parameters + ---------- + content : str + The string content to be converted into a template. + + Returns + ------- + An instance of the class with the provided content as a template. + """ + return cls(cls._template_from_str(content), None) + + @classmethod + def from_file(cls, path: Path): + """ + Create a Prompt instance from a file containing a Jinja template. + + Note: This method does not allow to include and inheritance to reference files + that are outside the folder or subfolders of the file given to `from_file`. + + Parameters + ---------- + path : Path + The path to the file containing the Jinja template. + + Returns + ------- + Prompt + An instance of the Prompt class with the template loaded from the file. + """ + # We don't use a `Signature` here because it seems not feasible to infer one from a Jinja2 environment that is + # split across multiple files (since e.g. we support features like Jinja2 includes and template inheritance) + return cls(cls._template_from_file(path), None) + + @classmethod + def _template_from_str(_, content: str) -> jinja2.Template: + # Dedent, and remove extra linebreak + cleaned_template = inspect.cleandoc(content) + + # Add linebreak if there were any extra linebreaks that + # `cleandoc` would have removed + ends_with_linebreak = content.replace(" ", "").endswith("\n\n") + if ends_with_linebreak: + cleaned_template += "\n" + + # Remove extra whitespaces, except those that immediately follow a newline symbol. + # This is necessary to avoid introducing whitespaces after backslash `\` characters + # used to continue to the next line without linebreak. + cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template) + + env = jinja2.Environment( + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + undefined=jinja2.StrictUndefined, + ) + env.filters["name"] = get_fn_name + env.filters["description"] = get_fn_description + env.filters["source"] = get_fn_source + env.filters["signature"] = get_fn_signature + env.filters["schema"] = get_schema + env.filters["args"] = get_fn_args + + return env.from_string(cleaned_template) + + @classmethod + def _template_from_file(_, path: Path) -> jinja2.Template: + file_directory = os.path.dirname(os.path.abspath(path)) + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(file_directory), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + undefined=jinja2.StrictUndefined, + ) + return env.get_template(os.path.basename(path)) def prompt(fn: Callable) -> Prompt: @@ -92,7 +171,7 @@ def prompt(fn: Callable) -> Prompt: return Prompt(template, signature) -def render(template: str, **values: Optional[Dict[str, Any]]) -> str: +def render(content: str, **values: Optional[Dict[str, Any]]) -> str: r"""Parse a Jinaj2 template and translate it into an Outlines graph. This function removes extra whitespaces and linebreaks from templates to @@ -183,40 +262,8 @@ def render(template: str, **values: Optional[Dict[str, Any]]) -> str: A string that contains the rendered template. """ - jinja_template = create_jinja_template(template) - return jinja_template.render(**values) - - -def create_jinja_template(template: str): - # Dedent, and remove extra linebreak - cleaned_template = inspect.cleandoc(template) - - # Add linebreak if there were any extra linebreaks that - # `cleandoc` would have removed - ends_with_linebreak = template.replace(" ", "").endswith("\n\n") - if ends_with_linebreak: - cleaned_template += "\n" - - # Remove extra whitespaces, except those that immediately follow a newline symbol. - # This is necessary to avoid introducing whitespaces after backslash `\` characters - # used to continue to the next line without linebreak. - cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template) - - env = Environment( - trim_blocks=True, - lstrip_blocks=True, - keep_trailing_newline=True, - undefined=StrictUndefined, - ) - env.filters["name"] = get_fn_name - env.filters["description"] = get_fn_description - env.filters["source"] = get_fn_source - env.filters["signature"] = get_fn_signature - env.filters["schema"] = get_schema - env.filters["args"] = get_fn_args - - jinja_template = env.from_string(cleaned_template) - return jinja_template + template = Prompt._template_from_str(content) + return template.render(**values) def get_fn_name(fn: Callable): diff --git a/tests/test_prompts.py b/tests/test_prompts.py index a0433c0e5..de35a547f 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -1,10 +1,12 @@ +import os +import tempfile from typing import Dict, List import pytest from pydantic import BaseModel, Field import outlines -from outlines.prompts import render +from outlines.prompts import Prompt, render def test_render(): @@ -110,8 +112,7 @@ def test_prompt_basic(): def test_tpl(variable): """{{variable}} test""" - assert test_tpl.template == "{{variable}} test" - assert test_tpl.parameters == ["variable"] + assert list(test_tpl.signature.parameters) == ["variable"] with pytest.raises(TypeError): test_tpl(v="test") @@ -126,6 +127,8 @@ def test_tpl(variable): def test_single_quote_tpl(variable): "${variable} test" + assert list(test_single_quote_tpl.signature.parameters) == ["variable"] + p = test_tpl("test") assert p == "test test" @@ -135,8 +138,7 @@ def test_prompt_kwargs(): def test_kwarg_tpl(var, other_var="other"): """{{var}} and {{other_var}}""" - assert test_kwarg_tpl.template == "{{var}} and {{other_var}}" - assert test_kwarg_tpl.parameters == ["var", "other_var"] + assert list(test_kwarg_tpl.signature.parameters) == ["var", "other_var"] p = test_kwarg_tpl("test") assert p == "test and other" @@ -312,3 +314,87 @@ def args_prompt(fn): args_prompt(with_all) == "args: x1, y1, z1, x2: bool, y2: str, z2: Dict[int, List[str]], x3=True, y3='Hi', z3={4: ['I', 'love', 'outlines']}, x4: bool = True, y4: str = 'Hi', z4: Dict[int, List[str]] = {4: ['I', 'love', 'outlines']}" ) + + +@pytest.fixture +def temp_prompt_file(): + test_dir = tempfile.mkdtemp() + + base_template_path = os.path.join(test_dir, "base_template.txt") + with open(base_template_path, "w") as f: + f.write( + """{% block content %}{% endblock %} +""" + ) + + include_file_path = os.path.join(test_dir, "include.txt") + with open(include_file_path, "w") as f: + f.write( + """{% for example in examples %} +- Q: {{ example.question }} +- A: {{ example.answer }} +{% endfor %} +""" + ) + + prompt_file_path = os.path.join(test_dir, "prompt.txt") + with open(prompt_file_path, "w") as f: + f.write( + """{% extends "base_template.txt" %} + +{% block content %} +Here is a prompt with examples: + +{% include "include.txt" %} + +Now please answer the following question: + +Q: {{ question }} +A: +{% endblock %} +""" + ) + yield prompt_file_path + + +def test_prompt_from_file(temp_prompt_file): + prompt = Prompt.from_file(temp_prompt_file) + assert prompt.signature is None + examples = [ + {"question": "What is the capital of France?", "answer": "Paris"}, + {"question": "What is 2 + 2?", "answer": "4"}, + ] + question = "What is the Earth's diameter?" + rendered = prompt(examples=examples, question=question) + expected = """Here is a prompt with examples: + +- Q: What is the capital of France? +- A: Paris +- Q: What is 2 + 2? +- A: 4 + +Now please answer the following question: + +Q: What is the Earth's diameter? +A: +""" + assert rendered.strip() == expected.strip() + + +def test_prompt_from_str(): + content = """ + Hello, {{ name }}! + """ + prompt = Prompt.from_str(content) + assert prompt.signature is None + assert prompt(name="World") == "Hello, World!" + + +def test_template_from_str_with_extra_linebreaks(): + content = """ + Hello, {{ name }}! + + + """ + template = Prompt._template_from_str(content) + assert template.render(name="World") == "Hello, World!\n" From bf27c77b01319de77e41986dc7cb39396a7a3e15 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Thu, 9 Jan 2025 17:11:51 +0100 Subject: [PATCH 142/505] Remove `prompt.render` method --- outlines/prompts.py | 97 +------------------------------------------ tests/test_prompts.py | 7 +++- 2 files changed, 7 insertions(+), 97 deletions(-) diff --git a/outlines/prompts.py b/outlines/prompts.py index 2110630c9..66e723205 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -166,106 +166,11 @@ def prompt(fn: Callable) -> Prompt: if docstring is None: raise TypeError("Could not find a template in the function's docstring.") - template = cast(str, docstring) + template = Prompt._template_from_str(cast(str, docstring)) return Prompt(template, signature) -def render(content: str, **values: Optional[Dict[str, Any]]) -> str: - r"""Parse a Jinaj2 template and translate it into an Outlines graph. - - This function removes extra whitespaces and linebreaks from templates to - allow users to enter prompts more naturally than if they used Python's - constructs directly. See the examples for a detailed explanation. - - Examples - -------- - - Outlines follow Jinja2's syntax - - >>> import outlines - >>> outline = outlines.render("I like {{food}} and {{sport}}", food="tomatoes", sport="tennis") - I like tomatoes and tennis - - If the first line of the template is empty, `render` removes it - - >>> from outlines import render - >>> - >>> tpl = ''' - ... A new string''' - >>> tpl - ... '\nA new string' - >>> render(tpl) - ... 'a new string' - - Similarly, `render` ignores linebreaks introduced by placing the closing quotes - underneath the text: - - >>> tpl = ''' - ... A new string - ... ''' - >>> tpl - ... '\nA new string\n' - >>> render(tpl) - ... 'A new string' - - If you want to insert a linebreak at the end of the rendered template, you will - need to leave an empty line at the end of the template: - - >>> tpl = ''' - ... A new string - ... - ... ''' - >>> tpl - ... '\nA new string\n\n' - >>> render(tpl) - ... 'A new string\n' - - `render` removes the identation in docstrings. This is particularly important - when using prompt functions - - >>> tpl = ''' - ... a string - ... and another string''' - >>> tpl - ... '\n a string\n and another string' - >>> render(tpl) - ... 'a string\nand another string' - - The indentation of the first line is assumed to be the same as the second line's - - >>> tpl = '''a string - ... and another''' - >>> tpl - ... 'a string\n and another' - >>> render(tpl) - ... 'a string\nand another' - - To get a different indentation for the first and the second line, we can start the - prompt on the string's second line: - - >>> tpl = ''' - ... First line - ... Second line''' - >>> render(tpl) - ... 'First Line\n Second Line' - - Parameters - ---------- - template - A string that contains a template written with the Jinja2 syntax. - **values - Map from the variables in the template to their value. - - Returns - ------- - A string that contains the rendered template. - - """ - template = Prompt._template_from_str(content) - return template.render(**values) - - def get_fn_name(fn: Callable): """Returns the name of a callable.""" if not callable(fn): diff --git a/tests/test_prompts.py b/tests/test_prompts.py index de35a547f..4cc4d8ff1 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -6,7 +6,12 @@ from pydantic import BaseModel, Field import outlines -from outlines.prompts import Prompt, render +from outlines.prompts import Prompt + + +def render(content: str, **kwargs): + template = Prompt._template_from_str(content) + return template.render(kwargs) def test_render(): From 7149a5e613b9c2bb3c2ed3ee5b7c9228772504b1 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Thu, 9 Jan 2025 17:12:43 +0100 Subject: [PATCH 143/505] Apply a small fix suggested by linter: Ruff E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks --- outlines/prompts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/outlines/prompts.py b/outlines/prompts.py index 66e723205..86519adaf 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -9,7 +9,7 @@ from typing import Any, Callable, Dict, Optional, Type, cast import jinja2 -from pydantic import BaseModel +import pydantic @dataclass @@ -253,10 +253,10 @@ def get_schema_dict(model: Dict): return json.dumps(model, indent=2) -@get_schema.register(type(BaseModel)) -def get_schema_pydantic(model: Type[BaseModel]): +@get_schema.register(type(pydantic.BaseModel)) +def get_schema_pydantic(model: Type[pydantic.BaseModel]): """Return the schema of a Pydantic model.""" - if not type(model) == type(BaseModel): + if not isinstance(model, type(pydantic.BaseModel)): raise TypeError("The `schema` filter only applies to Pydantic models.") if hasattr(model, "model_json_schema"): From e7be56284a0ad578ca1b0b20307504be8c9e7fbd Mon Sep 17 00:00:00 2001 From: Per von Rosen <46034369+pervrosen@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:23:18 +0100 Subject: [PATCH 144/505] Update react_agent.md with current import structure the old library structure has not been updated to reflect the present one. --- docs/cookbook/react_agent.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cookbook/react_agent.md b/docs/cookbook/react_agent.md index 0597eab07..15b6ed369 100644 --- a/docs/cookbook/react_agent.md +++ b/docs/cookbook/react_agent.md @@ -120,8 +120,8 @@ class Decision(BaseModel): We could generate a response using the json schema but we will use the regex and check that everything is working as expected: ```python -from outlines.integrations.utils import convert_json_schema_to_str -from outlines.fsm.json_schema import build_regex_from_schema +from outlines.fsm.json_schema import convert_json_schema_to_str +from outlines_core.fsm.json_schema import build_regex_from_schema json_schema = Decision.model_json_schema() schema_str = convert_json_schema_to_str(json_schema=json_schema) From 03d387893b95f151c74ff6b7d7864e15cb484eee Mon Sep 17 00:00:00 2001 From: Joao Gante Date: Thu, 9 Jan 2025 18:45:17 +0000 Subject: [PATCH 145/505] Update transformers_vision.md --- docs/reference/models/transformers_vision.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/models/transformers_vision.md b/docs/reference/models/transformers_vision.md index b488bca1a..451e54a8f 100644 --- a/docs/reference/models/transformers_vision.md +++ b/docs/reference/models/transformers_vision.md @@ -2,7 +2,7 @@ Outlines allows seamless use of [vision models](https://huggingface.co/learn/computer-vision-course/en/unit4/multimodal-models/tasks-models-part1). -`outlines.models.transformers_vision` has shares interfaces with, and is based on [outlines.models.transformers](./transformers.md). +`outlines.models.transformers_vision` shares interfaces with, and is based on [outlines.models.transformers](./transformers.md). Tasks supported include @@ -62,7 +62,7 @@ image_urls = [ ] description_generator = outlines.generate.text(model) description_generator( - "What shapes are present?", + "What shapes are present?", list(map(img_from_url, image_urls)), ) ``` @@ -110,6 +110,6 @@ image_data_generator( ## Resources -### Chosing a model +### Choosing a model - https://mmbench.opencompass.org.cn/leaderboard - https://huggingface.co/spaces/WildVision/vision-arena From 3af1e5ac56a912433a948a070e8e7e41a3d157cf Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Mon, 6 Jan 2025 18:33:33 +0100 Subject: [PATCH 146/505] Fix `vllm`-related pytest warning (that was spaming user) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, when you ran pytest -k specific_test, it spawned dozens of the same skipped warnings message on stdout... IMHO, that was not ideal ^^ Bug introduced in d32dfde3ae7eac71f57af96d5141975ef9322b12 --- tests/conftest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bbc56afdb..ea99b69ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,10 +5,13 @@ def pytest_collection_modifyitems(config, items): if sys.platform != "linux": + if not config.option.keyword or ( + config.option.keyword and "test_integration_vllm" in config.option.keyword + ): + print( + "WARNING: test_integration_vllm tests are skipped because vLLM only supports Linux platform (including WSL)." + ) skip_vllm = pytest.mark.skip(reason="vLLM models can only be run on Linux.") for item in items: if "test_integration_vllm" in item.nodeid: item.add_marker(skip_vllm) - print( - f"WARNING: {item.nodeid} is skipped because vLLM only supports Linux platform (including WSL)." - ) From 79100b293f861ee8bd74390527ec25cb6fe3b670 Mon Sep 17 00:00:00 2001 From: Frederik Fix Date: Tue, 14 Jan 2025 11:32:30 +0100 Subject: [PATCH 147/505] Add filters to prompt function (#1371) Allow giving custom filters to the prompt decorator ``` def reverses: str) -> str: return s[::-1] @prompt(filters={ 'reverse': reverse }) def reverse_prompt(text): '''{{ text | reverse }}''' prompt = reverse_prompt("Hello") print(prompt) >>> "olleH" ``` --- outlines/prompts.py | 71 ++++++++++++++++++++++++++++++------------- tests/test_prompts.py | 17 +++++++++++ 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/outlines/prompts.py b/outlines/prompts.py index 86519adaf..1cc264226 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -40,7 +40,7 @@ def __call__(self, *args, **kwargs) -> str: return self.template.render(**kwargs) @classmethod - def from_str(cls, content: str): + def from_str(cls, content: str, filters: Dict[str, Callable] = {}): """ Create an instance of the class from a string. @@ -53,10 +53,10 @@ def from_str(cls, content: str): ------- An instance of the class with the provided content as a template. """ - return cls(cls._template_from_str(content), None) + return cls(cls._template_from_str(content, filters), None) @classmethod - def from_file(cls, path: Path): + def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): """ Create a Prompt instance from a file containing a Jinja template. @@ -75,10 +75,12 @@ def from_file(cls, path: Path): """ # We don't use a `Signature` here because it seems not feasible to infer one from a Jinja2 environment that is # split across multiple files (since e.g. we support features like Jinja2 includes and template inheritance) - return cls(cls._template_from_file(path), None) + return cls(cls._template_from_file(path, filters), None) @classmethod - def _template_from_str(_, content: str) -> jinja2.Template: + def _template_from_str( + _, content: str, filters: Dict[str, Callable] = {} + ) -> jinja2.Template: # Dedent, and remove extra linebreak cleaned_template = inspect.cleandoc(content) @@ -93,12 +95,7 @@ def _template_from_str(_, content: str) -> jinja2.Template: # used to continue to the next line without linebreak. cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template) - env = jinja2.Environment( - trim_blocks=True, - lstrip_blocks=True, - keep_trailing_newline=True, - undefined=jinja2.StrictUndefined, - ) + env = create_jinja_env(None, filters) env.filters["name"] = get_fn_name env.filters["description"] = get_fn_description env.filters["source"] = get_fn_source @@ -109,19 +106,19 @@ def _template_from_str(_, content: str) -> jinja2.Template: return env.from_string(cleaned_template) @classmethod - def _template_from_file(_, path: Path) -> jinja2.Template: + def _template_from_file( + _, path: Path, filters: Dict[str, Callable] = {} + ) -> jinja2.Template: file_directory = os.path.dirname(os.path.abspath(path)) - env = jinja2.Environment( - loader=jinja2.FileSystemLoader(file_directory), - trim_blocks=True, - lstrip_blocks=True, - keep_trailing_newline=True, - undefined=jinja2.StrictUndefined, - ) + env = create_jinja_env(jinja2.FileSystemLoader(file_directory), filters) + return env.get_template(os.path.basename(path)) -def prompt(fn: Callable) -> Prompt: +def prompt( + fn: Optional[Callable] = None, + filters: Dict[str, Callable] = {}, +) -> Callable: """Decorate a function that contains a prompt template. This allows to define prompts in the docstring of a function and simplify their @@ -152,11 +149,26 @@ def prompt(fn: Callable) -> Prompt: ... >>> hal = ft.partial(solve_task, "HAL", "Travel to Jupiter") + Additional Jinja2 filters can be provided as keyword arguments to the decorator. + + >>> def reverse(s: str) -> str: + ... return s[::-1] + ... + >>> @outlines.prompt(filters={ 'reverse': reverse }) + ... def reverse_prompt(text): + ... '''{{ text | reverse }}''' + ... + >>> prompt = reverse_prompt("Hello") + >>> print(prompt) + ... "olleH" + Returns ------- A `Prompt` callable class which will render the template when called. """ + if fn is None: + return lambda fn: prompt(fn, cast(Dict[str, Callable], filters)) signature = inspect.signature(fn) @@ -166,11 +178,28 @@ def prompt(fn: Callable) -> Prompt: if docstring is None: raise TypeError("Could not find a template in the function's docstring.") - template = Prompt._template_from_str(cast(str, docstring)) + template = Prompt._template_from_str(cast(str, docstring), filters) return Prompt(template, signature) +def create_jinja_env( + loader: Optional[jinja2.BaseLoader], filters: Dict[str, Callable] +) -> jinja2.Environment: + env = jinja2.Environment( + loader=loader, + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + undefined=jinja2.StrictUndefined, + ) + + for name, filter_fn in filters.items(): + env.filters[name] = filter_fn + + return env + + def get_fn_name(fn: Callable): """Returns the name of a callable.""" if not callable(fn): diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 4cc4d8ff1..f59c04ac0 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -321,6 +321,23 @@ def args_prompt(fn): ) +def test_prompt_with_additional_filters(): + def reverse(s: str) -> str: + return s[::-1] + + @outlines.prompt(filters=dict(reverse=reverse)) + def test_tpl(variable): + """{{ variable | reverse }} test""" + + assert list(test_tpl.signature.parameters) == ["variable"] + + p = test_tpl("test") + assert p == "tset test" + + p = test_tpl(variable="example") + assert p == "elpmaxe test" + + @pytest.fixture def temp_prompt_file(): test_dir = tempfile.mkdtemp() From a03c25451f8b5be95f76de95cf759d52b99bd2cf Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Tue, 14 Jan 2025 10:43:25 -0800 Subject: [PATCH 148/505] Remove incorrect text from feature matrix docs There's an extra `outlines.generate` row in the feature matrix docs. This removes it. I also modified the markdown syntax for one header to use ** rather than __, consistent with the rest of the table. --- docs/reference/models/models.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/reference/models/models.md b/docs/reference/models/models.md index 4d2dda8c9..eab70f2a1 100644 --- a/docs/reference/models/models.md +++ b/docs/reference/models/models.md @@ -40,9 +40,8 @@ model = outlines.models.openai( | **Generation** | | | | | | | | | Batch | ✅ | ✅ | ✅ | ❌ | ? | ❌ | ❌ | | Stream | ✅ | ❌ | ❌ | ✅ | ? | ✅ | ❌ | -| **`outlines.generate`** | | | | | | | | | Text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| __Structured__ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Structured** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | JSON Schema | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Choice | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Regex | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | From 7b9012b49de25a6957039a3b5e53452c56b26e8c Mon Sep 17 00:00:00 2001 From: Marco De Nadai Date: Wed, 15 Jan 2025 13:25:12 +0100 Subject: [PATCH 149/505] Update vllm_integration.py to the latest version of outlines (#1352) --- examples/vllm_integration.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/vllm_integration.py b/examples/vllm_integration.py index c9ce057bc..ee1b24a33 100644 --- a/examples/vllm_integration.py +++ b/examples/vllm_integration.py @@ -2,8 +2,10 @@ import vllm from pydantic import BaseModel +from transformers import AutoTokenizer -from outlines.processors.structured import JSONLogitsProcessor +from outlines.models.vllm import adapt_tokenizer +from outlines.processors import JSONLogitsProcessor class Person(BaseModel): @@ -11,8 +13,12 @@ class Person(BaseModel): surname: str -llm = vllm.LLM(model="mistralai/Mistral-7B-v0.1", max_model_len=512) -logits_processor = JSONLogitsProcessor(schema=Person, llm=llm, whitespace_pattern=r" ?") +MODEL_ID = "mistralai/Mistral-7B-v0.1" +llm = vllm.LLM(model=MODEL_ID, max_model_len=512) +tokenizer = adapt_tokenizer(AutoTokenizer.from_pretrained(MODEL_ID)) +logits_processor = JSONLogitsProcessor( + schema=Person, tokenizer=tokenizer, whitespace_pattern=r" ?" +) result = llm.generate( ["He is Tom Jones", "She saw Linda Smith"], sampling_params=vllm.SamplingParams( From 088f4399296a038123712a2ac7e263c1b1b95714 Mon Sep 17 00:00:00 2001 From: Neil Mehta Date: Wed, 15 Jan 2025 07:26:47 -0500 Subject: [PATCH 150/505] [Preserve dtype of array when converting to torch (#1349) We have noticing the following error with a recent version of outlines when used with MLX: ``` TypeError: argument 'token_id': 'float' object cannot be interpreted as an integer At: /.../outlines_core/fsm/guide.py(294): get_next_state /.../outlines/processors/structured.py(101): process_logits /.../outlines/processors/base_logits_processor.py(90): __call__ ``` The issue is that the MLX array of tokens, which are integers, are being force-converted to floats, even though outlines expects an integer array. This is because all MLX arrays are being converted to `float32`, even when it's not necessarily appropriate, like in this case. Looking at the [commented link](https://ml-explore.github.io/mlx/build/html/usage/numpy.html#pytorch), the advice was to convert to `float32` only for `bfloat16`, because numpy does not support `bfloat16`. Now the MLX `_to_torch` implementation matches the other array libraries, none of the other libraries are being force-casted to float --- outlines/processors/base_logits_processor.py | 6 +++--- tests/processors/test_base_processor.py | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index 44b55af2e..800e69f79 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -110,9 +110,9 @@ def _to_torch(tensor_like: Array) -> torch.Tensor: import mlx.core as mx # https://ml-explore.github.io/mlx/build/html/usage/numpy.html#pytorch - return torch.from_dlpack( - np.array(tensor_like.astype(mx.float32), copy=False) - ) + if tensor_like.dtype == mx.bfloat16: + tensor_like = tensor_like.astype(mx.float32) + return torch.from_dlpack(np.array(tensor_like, copy=False)) elif is_jax_array_type(type(tensor_like)): import jax diff --git a/tests/processors/test_base_processor.py b/tests/processors/test_base_processor.py index cd9f48278..d2a1e1af2 100644 --- a/tests/processors/test_base_processor.py +++ b/tests/processors/test_base_processor.py @@ -18,6 +18,7 @@ import mlx.core as mx arrays["mlx"] = mx.array([[1, 2], [3, 4]], dtype=mx.float32) + arrays["mlx_bfloat16"] = mx.array([[1, 2], [3, 4]], dtype=mx.bfloat16) except ImportError: pass @@ -59,7 +60,12 @@ def test_from_torch(array_type, processor): torch_tensor = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32) data = processor._from_torch(torch_tensor, type(arrays[array_type])) assert isinstance(data, type(arrays[array_type])) - assert np.allclose(data, arrays[array_type]) + if array_type == "mlx_bfloat16": + # For bfloat16, we expect the output to be float32 due to the conversion + assert data.dtype == mx.float32 + assert np.allclose(np.array(data), np.array([[1, 2], [3, 4]], dtype=np.float32)) + else: + assert np.allclose(data, arrays[array_type]) @pytest.mark.parametrize("array_type", arrays.keys()) From 8b173a315d23d872f1347e6539af4ad2e22b18a7 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 16 Jan 2025 10:34:21 +0200 Subject: [PATCH 151/505] Fix typo in docs/community/examples --- docs/community/examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community/examples.md b/docs/community/examples.md index 2ebaf2765..813bd4834 100644 --- a/docs/community/examples.md +++ b/docs/community/examples.md @@ -1,6 +1,6 @@ # Community projects and articles -Publishing examples and articles about Outlines are a meaningful way to contrinute to the community. Here is a list of projects we are aware of. Drop us a line if we forgot yours! +Publishing examples and articles about Outlines are a meaningful way to contribute to the community. Here is a list of projects we are aware of. Drop us a line if we forgot yours! [MMSG](https://github.com/leloykun/mmsg) is a Python library for generating interleaved text and image content in a structured format you can directly pass to downstream APIs. From 063291d816015cbdb04b66a8db6c6020b619252a Mon Sep 17 00:00:00 2001 From: Mohammad Nasirifar Date: Fri, 17 Jan 2025 19:50:38 +0000 Subject: [PATCH 152/505] Fix readme to reflect routing through enum --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d34b0984d..e3e6dc24d 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ class Operation(Enum): mul = partial(mul) model = outlines.models.transformers("WizardLM/WizardMath-7B-V1.1") -generator = outlines.generate.json(model, add) +generator = outlines.generate.json(model, Operation) result = generator("Return json with two float named c and d respectively. c is negative and d greater than 1.0.") print(result) From 51ef66388799dd285afa7393db15f773193d4e0e Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Wed, 22 Jan 2025 11:19:05 +0100 Subject: [PATCH 153/505] Sort, clean and update .gitignore --- .gitignore | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 08390ae3d..eefa0cb13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,19 @@ -*.egg-info __pycache__ -*_version.py -docs/build +.benchmarks +.cache .coverage -.idea/ -*.gguf +.direnv +.env +.idea +.pytest_cache +.python-version .venv +*_version.py +*.egg-info +*.gguf benchmarks/results -.python-version - -# Remove doc build folders -.cache/ -build/ +build +docs/build +logs +logs/ +__pycache__/ From ea4904ac5a2d1064ff4c00a19a66a21dc27c9b43 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 23 Jan 2025 09:18:13 -0800 Subject: [PATCH 154/505] Update README to include chat templating (#1372) The existing README has underwhelming or incorrect results (Example is underwhelming #1347) due to lack of templating for instruct models. This adds special tokens for each instruct model call, as well as provide comments on how to obtain/produce special tokens. --------- Co-authored-by: Victoria Terenina --- README.md | 88 ++++++++++++++++++---------- docs/reference/chat_templating.md | 38 ++++++++++++ mkdocs.yml | 97 +++++++++++++++---------------- 3 files changed, 143 insertions(+), 80 deletions(-) create mode 100644 docs/reference/chat_templating.md diff --git a/README.md b/README.md index e3e6dc24d..b3cde1dd1 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,21 @@ is to ensure that there is a well-defined interface between their output and user-defined code. **Outlines** provides ways to control the generation of language models to make their output more predictable. +The following methods of structured generation are supported: + +- [Multiple choices](#multiple-choices) +- [Type constraints](#type-constraint) +- [Efficient regex-structured generation](#efficient-regex-structured-generation) +- [Efficient JSON generation following a Pydantic model](#efficient-json-generation-following-a-pydantic-model) +- [Using context-free grammars to guide generation](#using-context-free-grammars-to-guide-generation) +- [Open functions](#open-functions) + +### Chat template tokens + +Outlines does not manage chat templating tokens when using instruct models. You must apply the chat template tokens to the prompt yourself. Chat template tokens are not needed for base models. + +Please see [the documentation](https://dottxt-ai.github.io/outlines/latest/reference/chat_templating) on chat templating for more. + ### Multiple choices You can reduce the completion to a choice between multiple possibilities: @@ -72,42 +87,47 @@ You can reduce the completion to a choice between multiple possibilities: ``` python import outlines -model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") +model_name = "HuggingFaceTB/SmolLM2-360M-Instruct" +model = outlines.models.transformers(model_name) + +# You must apply the chat template tokens to the prompt! +# See below for an example. +prompt = """ +<|im_start|>system +You extract information from text. +<|im_end|> -prompt = """You are a sentiment-labelling assistant. -Is the following review positive or negative? +<|im_start|>user +What food does the following text describe? -Review: This restaurant is just awesome! +Text: I really really really want pizza. +<|im_end|> +<|im_start|>assistant """ -generator = outlines.generate.choice(model, ["Positive", "Negative"]) +generator = outlines.generate.choice(model, ["Pizza", "Pasta", "Salad", "Dessert"]) answer = generator(prompt) + +# Likely answer: Pizza ``` -You can also pass these choices through en enum: +You can also pass in choices with an `Enum`: ````python from enum import Enum -import outlines - -class Sentiment(str, Enum): - positive = "Positive" - negative = "Negative" +class Food(str, Enum): + pizza = "Pizza" + pasta = "Pasta" + salad = "Salad" + dessert = "Dessert" -model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") - -prompt = """You are a sentiment-labelling assistant. -Is the following review positive or negative? - -Review: This restaurant is just awesome! -""" - -generator = outlines.generate.choice(model, Sentiment) +generator = outlines.generate.choice(model, Food) answer = generator(prompt) +# Likely answer: Pizza ```` -### Type constraint +### Type constraints You can instruct the model to only return integers or floats: @@ -140,7 +160,17 @@ import outlines model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") -prompt = "What is the IP address of the Google DNS servers? " +prompt = """ +<|im_start|>system You are a helpful assistant. +<|im_end|> + +<|im_start|>user +What is an IP address of the Google DNS servers? +<|im_end|> +<|im_start|>assistant +The IP address of a Google DNS server is + +""" generator = outlines.generate.text(model) unstructured = generator(prompt, max_tokens=30) @@ -148,19 +178,17 @@ unstructured = generator(prompt, max_tokens=30) generator = outlines.generate.regex( model, r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)", + sampler=outlines.samplers.greedy(), ) structured = generator(prompt, max_tokens=30) print(unstructured) -# What is the IP address of the Google DNS servers? +# 8.8.8.8 # -# Passive DNS servers are at DNS servers that are private. -# In other words, both IP servers are private. The database -# does not contain Chelsea Manning +# <|im_end|> print(structured) -# What is the IP address of the Google DNS servers? -# 2.2.6.1 +# 8.8.8.8 ``` Unlike other libraries, regex-structured generation in Outlines is almost as fast @@ -168,15 +196,13 @@ as non-structured generation. ### Efficient JSON generation following a Pydantic model -Outlines allows to guide the generation process so the output is *guaranteed* to follow a [JSON schema](https://json-schema.org/) or [Pydantic model](https://docs.pydantic.dev/latest/): +Outlines users can guide the generation process so the output is *guaranteed* to follow a [JSON schema](https://json-schema.org/) or [Pydantic model](https://docs.pydantic.dev/latest/): ```python from enum import Enum from pydantic import BaseModel, constr import outlines -import torch - class Weapon(str, Enum): sword = "sword" diff --git a/docs/reference/chat_templating.md b/docs/reference/chat_templating.md new file mode 100644 index 000000000..e3f33fbfd --- /dev/null +++ b/docs/reference/chat_templating.md @@ -0,0 +1,38 @@ +# Chat templating + +Instruction-tuned language models use "special tokens" to indicate different parts of text, such as the system prompt, the user prompt, any images, and the assistant's response. A [chat template](https://huggingface.co/docs/transformers/main/en/chat_templating) is how different types of input are composited together into a single, machine-readable string. + +Outlines does not manage chat templating tokens when using instruct models. You must apply the chat template tokens to the prompt yourself -- if you do not apply chat templating on instruction-tuned models, you will often get nonsensical output from the model. + +Chat template tokens are not needed for base models. + +You can find the chat template tokens in the model's HuggingFace repo or documentation. As an example, the `SmolLM2-360M-Instruct` special tokens can be found [here](https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct/blob/main/special_tokens_map.json). + +However, it can be slow to manually look up a model's special tokens, and special tokens vary by models. If you change the model, your prompts may break if you have hard-coded special tokens. + +If you need a convenient tool to apply chat templating for you, you should use the `tokenizer` from the `transformers` library: + +```python +from transformers import AutoTokenizer + +tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM2-360M-Instruct") +prompt = tokenizer.apply_chat_template( + [ + {"role": "system", "content": "You extract information from text."}, + {"role": "user", "content": "What food does the following text describe?"}, + ], + tokenize=False, + add_bos=True, + add_generation_prompt=True, +) +``` + +yields + +``` +<|im_start|>system +You extract information from text.<|im_end|> +<|im_start|>user +What food does the following text describe?<|im_end|> +<|im_start|>assistant +``` diff --git a/mkdocs.yml b/mkdocs.yml index 7a3ca1e98..8d051d1af 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,6 @@ site_author: The Outlines developers site_description: >- Structured text generation with LLMs - # Repository repo_name: dottxt-ai/outlines repo_url: https://github.com/dottxt-ai/outlines @@ -76,7 +75,6 @@ markdown_extensions: emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.snippets: - extra_css: - stylesheets/extra.css @@ -131,53 +129,54 @@ nav: - Cerebrium: cookbook/deploy-using-cerebrium.md - Modal: cookbook/deploy-using-modal.md - Docs: - - reference/index.md - - Generation: - - Overview: reference/generation/generation.md - - Text: reference/text.md - - Samplers: reference/samplers.md - - Structured generation: - - How does it work?: reference/generation/structured_generation_explanation.md - - Classification: reference/generation/choices.md - - Regex: reference/generation/regex.md - - Type constraints: reference/generation/format.md - - JSON (function calling): reference/generation/json.md - - Grammar: reference/generation/cfg.md - - Creating Grammars: reference/generation/creating_grammars.md - - Custom FSM operations: reference/generation/custom_fsm_ops.md - - Utilities: - - Serve with vLLM: reference/serve/vllm.md - - Serve with LM Studio: reference/serve/lmstudio.md - - Custom types: reference/generation/types.md - - Prompt templating: reference/prompting.md - - Outlines functions: reference/functions.md - - Models: - - Overview: reference/models/models.md - - Open source: - - Transformers: reference/models/transformers.md - - Transformers Vision: reference/models/transformers_vision.md - - Llama.cpp: reference/models/llamacpp.md - - vLLM: reference/models/vllm.md - - TGI: reference/models/tgi.md - - ExllamaV2: reference/models/exllamav2.md - - MLX: reference/models/mlxlm.md - - Mamba: reference/models/transformers/#mamba - - API: - - OpenAI: reference/models/openai.md + - reference/index.md + - Generation: + - Overview: reference/generation/generation.md + - Chat templating: reference/chat_templating.md + - Text: reference/text.md + - Samplers: reference/samplers.md + - Structured generation: + - How does it work?: reference/generation/structured_generation_explanation.md + - Classification: reference/generation/choices.md + - Regex: reference/generation/regex.md + - Type constraints: reference/generation/format.md + - JSON (function calling): reference/generation/json.md + - Grammar: reference/generation/cfg.md + - Creating Grammars: reference/generation/creating_grammars.md + - Custom FSM operations: reference/generation/custom_fsm_ops.md + - Utilities: + - Serve with vLLM: reference/serve/vllm.md + - Serve with LM Studio: reference/serve/lmstudio.md + - Custom types: reference/generation/types.md + - Prompt templating: reference/prompting.md + - Outlines functions: reference/functions.md + - Models: + - Overview: reference/models/models.md + - Open source: + - Transformers: reference/models/transformers.md + - Transformers Vision: reference/models/transformers_vision.md + - Llama.cpp: reference/models/llamacpp.md + - vLLM: reference/models/vllm.md + - TGI: reference/models/tgi.md + - ExllamaV2: reference/models/exllamav2.md + - MLX: reference/models/mlxlm.md + - Mamba: reference/models/transformers/#mamba + - API: + - OpenAI: reference/models/openai.md - API Reference: - - api/index.md - - api/models.md - - api/prompts.md - - api/json_schema.md - - api/guide.md - - api/parsing.md - - api/regex.md - - api/samplers.md + - api/index.md + - api/models.md + - api/prompts.md + - api/json_schema.md + - api/guide.md + - api/parsing.md + - api/regex.md + - api/samplers.md - Community: - - community/index.md - - Feedback 🫶: community/feedback.md - - Chat with us ☕: https://discord.com/invite/R9DSu34mGd - - How to contribute 🏗️: community/contribute.md - - Your projects 👏: community/examples.md - - Versioning Guide 📌: community/versioning.md + - community/index.md + - Feedback 🫶: community/feedback.md + - Chat with us ☕: https://discord.com/invite/R9DSu34mGd + - How to contribute 🏗️: community/contribute.md + - Your projects 👏: community/examples.md + - Versioning Guide 📌: community/versioning.md - Blog: blog/index.md From 437ffe4f73f8275ec3ca6e5b19c692913f5fcf98 Mon Sep 17 00:00:00 2001 From: g-prz <158364710+g-prz@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:22:08 +0100 Subject: [PATCH 155/505] Add `genson` integration for json generation (#1390) This PR aims at integrating support of the `genson` package (in `generate.json`) to be able to use dynamic json schema generation as proposed in #1383. --- docs/reference/generation/json.md | 73 +++++++++++++++++++++++++++++++ outlines/generate/json.py | 6 +++ pyproject.toml | 4 +- tests/generate/test_generate.py | 16 +++++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/docs/reference/generation/json.md b/docs/reference/generation/json.md index da9f14729..05dc69654 100644 --- a/docs/reference/generation/json.md +++ b/docs/reference/generation/json.md @@ -103,3 +103,76 @@ print(add(**result)) ``` A great advantage of passing functions directly to specify the structure is that the structure of the LLM will change with the function's definition. No need to change the code at several places! + + +## From a dynamic JSON schema builder - GenSON + +Outlines integrated [GenSON](https://github.com/wolverdude/GenSON) builders to be able to dynamicly declare JSON schemas. It can be used as follow: + +```python +from genson import SchemaBuilder + +from outlines import models +from outlines import generate + +builder = SchemaBuilder() +builder.add_schema({"type": "object", "properties": {}}) +builder.add_object({"name": "Toto", "age": 5}) + +model = models.transformers( + "HuggingFaceTB/SmolLM2-135M", + device="auto", +) +generator = generate.json(model, builder) + +res = generator("Return a json of a young boy") +print(res) +# {"name": "Ben", "age": 10} +``` + +Anytime you are updating the schema through the builder, you need to redifine the outline generator to include these changes. From the the previous example: + +```python +from genson import SchemaBuilder + +from outlines import models +from outlines import generate + +builder = SchemaBuilder() +builder.add_schema({"type": "object", "properties": {}}) +builder.add_object({"name": "Toto", "age": 5}) + +model = models.transformers( + "HuggingFaceTB/SmolLM2-135M", + device="auto", +) +generator = generate.json(model, builder) + +res = generator("Return a json of a young boy") +print(res) +# {"name": "Ben", "age": 10} + +builder.add_object({"hobby": "sports"}) +generator = generate.json(model, builder) + +res = generator("Return a json of a youg boy whose hobby is coding") +print(res) +# {"name": "Ben", "age": 10, "hobby": "coding"} +``` + +!!! Note + + Beware of [GenSON](https://github.com/wolverdude/GenSON)'s behavior regarding dynamic amending of schemas through their builder. Here is an example of how you could lose `required` informations and generate json with missing fields: + + ```python + builder = SchemaBuilder() + builder.add_schema({"type": "object", "properties": {}}) + builder.add_object({"name": "Toto", "age": 5}) + + print(builder.to_schema()) + # {'$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': {'name': {'type': 'string'}, 'age': {'type': 'integer'}}, 'required': ['age', 'name']} + + builder.add_object({"hobby": "sport"}) + print(builder.to_schema()) + # {'name': {'type': 'string'}, 'age': {'type': 'integer'}, 'hobby': {'type': 'string'}}} + ``` diff --git a/outlines/generate/json.py b/outlines/generate/json.py index d098d920d..008662206 100644 --- a/outlines/generate/json.py +++ b/outlines/generate/json.py @@ -3,6 +3,7 @@ from functools import singledispatch from typing import Callable, Optional, Union +from genson import SchemaBuilder from outlines_core.fsm.json_schema import build_regex_from_schema from pydantic import BaseModel @@ -55,6 +56,11 @@ def json( regex_str = build_regex_from_schema(schema, whitespace_pattern) generator = regex(model, regex_str, sampler) generator.format_sequence = lambda x: pyjson.loads(x) + elif isinstance(schema_object, SchemaBuilder): + schema = schema_object.to_json() + regex_str = build_regex_from_schema(schema, whitespace_pattern) + generator = regex(model, regex_str, sampler) + generator.format_sequence = lambda x: pyjson.loads(x) elif callable(schema_object): schema = pyjson.dumps(get_schema_from_signature(schema_object)) regex_str = build_regex_from_schema(schema, whitespace_pattern) diff --git a/pyproject.toml b/pyproject.toml index 28060f797..0f1683d41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "airportsdata", "torch", "outlines_core==0.1.26", + "genson", ] dynamic = ["version"] @@ -71,7 +72,7 @@ test = [ "transformers", "pillow", "exllamav2", - "jax" + "jax", ] serve = [ "vllm>=0.3.0", @@ -147,6 +148,7 @@ module = [ "pycountry.*", "airportsdata.*", "outlines_core.*", + "genson", ] ignore_missing_imports = true diff --git a/tests/generate/test_generate.py b/tests/generate/test_generate.py index f91bc8653..3d36faa5d 100644 --- a/tests/generate/test_generate.py +++ b/tests/generate/test_generate.py @@ -276,6 +276,22 @@ def test_generate_json(request, model_fixture, sample_schema): generator(**get_inputs(model_fixture), max_tokens=100) +def test_integrate_genson_generate_json(request): + from genson import SchemaBuilder + + builder = SchemaBuilder() + builder.add_schema({"type": "object", "properties": {}}) + builder.add_object({"name": "Toto", "age": 5}) + + model = request.getfixturevalue("model_transformers_opt125m") + + generator = generate.json(model, builder) + res = generator("Return a json of a young boy") + + assert "name" in res + assert "age" in res + + @pytest.mark.parametrize("model_fixture", ALL_MODEL_FIXTURES) @pytest.mark.parametrize("sample_choices", ALL_SAMPLE_CHOICES_FIXTURES) def test_generate_choice(request, model_fixture, sample_choices): From 1842fe784085deb1bde0c197685b9702b1920429 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Wed, 5 Feb 2025 12:51:28 +0100 Subject: [PATCH 156/505] Add a few dotfiles to improve newcomers' experience (#1368) --- .editorconfig | 15 +++++++++++ .gitignore | 2 -- .pre-commit-config.yaml | 27 +++++--------------- .pydocstyle | 2 ++ .vscode/settings.json | 7 +++++ outlines/fsm/types.py | 6 ++--- outlines/processors/base_logits_processor.py | 4 +-- pyproject.toml | 7 +++++ 8 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 .editorconfig create mode 100644 .pydocstyle create mode 100644 .vscode/settings.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e795376f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yaml] +indent_size = 2 diff --git a/.gitignore b/.gitignore index eefa0cb13..4b1f5e921 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,3 @@ benchmarks/results build docs/build logs -logs/ -__pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9039b605..3c05ecb80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,33 +1,20 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: [--profile, black] -- repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [--py37-plus] -- repo: https://github.com/pycqa/flake8 - rev: 6.0.0 - hooks: - - id: flake8 -- repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 + rev: v1.14.1 hooks: - id: mypy args: [--allow-redefinition] exclude: ^examples/ additional_dependencies: [types-tqdm, types-Pillow] +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.1 + hooks: + - id: ruff + args: ["--config=pyproject.toml"] diff --git a/.pydocstyle b/.pydocstyle new file mode 100644 index 000000000..6663458d8 --- /dev/null +++ b/.pydocstyle @@ -0,0 +1,2 @@ +[pydocstyle] +convention = numpy diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a3a183836 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/outlines/fsm/types.py b/outlines/fsm/types.py index 5695dee07..cdb1d2115 100644 --- a/outlines/fsm/types.py +++ b/outlines/fsm/types.py @@ -39,19 +39,19 @@ def enum_format_fn(sequence: str) -> str: return enum_regex_str, enum_format_fn - if python_type == float: + if python_type is float: def float_format_fn(sequence: str) -> float: return float(sequence) return FLOAT, float_format_fn - elif python_type == int: + elif python_type is int: def int_format_fn(sequence: str) -> int: return int(sequence) return INTEGER, int_format_fn - elif python_type == bool: + elif python_type is bool: def bool_format_fn(sequence: str) -> bool: return bool(sequence) diff --git a/outlines/processors/base_logits_processor.py b/outlines/processors/base_logits_processor.py index 800e69f79..d6fe346e0 100644 --- a/outlines/processors/base_logits_processor.py +++ b/outlines/processors/base_logits_processor.py @@ -136,10 +136,10 @@ def _from_torch(tensor: torch.Tensor, target_type: Type) -> Array: elif target_type == np.ndarray: return tensor.detach().numpy() - elif target_type == list: + elif target_type is list: return tensor.detach().tolist() - elif target_type == tuple: + elif target_type is tuple: return tuple(tensor.detach().tolist()) elif is_mlx_array_type(target_type): diff --git a/pyproject.toml b/pyproject.toml index 0f1683d41..5c45b0e9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,3 +174,10 @@ show_missing = true [tool.diff_cover] compare_branch = "origin/main" diff_range_notation = ".." + +[tool.docformatter] +style = "numpy" +in-place = true + +[tool.ruff.lint] +ignore = [ "E731", "F401" ] From 5d64b23e7f75357050f4ef30b9e2fcaa4d9b741d Mon Sep 17 00:00:00 2001 From: multi-stager <133181681+multi-stager@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:12:40 +0800 Subject: [PATCH 157/505] Update Dockerfile to Multi-Stage --- Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 117e39e88..7e6f2e68a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.10 +### Build stage +FROM python:3.10 AS builder WORKDIR /outlines @@ -13,5 +14,10 @@ COPY outlines ./outlines RUN --mount=source=.git,target=.git,type=bind \ pip install --no-cache-dir .[serve] +### Runtime stage +FROM python:3.10 +WORKDIR /outlines +COPY --from=builder /outlines /outlines + # https://dottxt-ai.github.io/outlines/reference/vllm/ ENTRYPOINT ["python3", "-m", "outlines.serve.serve"] From 69418daf0c176bfbb2fe4c271c12359b7cb414f3 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Wed, 22 Jan 2025 11:20:08 +0100 Subject: [PATCH 158/505] add DevContainer configuration file --- .devcontainer/devcontainer.json | 19 +++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..ae3e00d45 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +{ + "name": "dottxt-ai", + "image": "mcr.microsoft.com/devcontainers/python:3.12", + "runArgs": [ + "--device=nvidia.com/gpu=all" + ], + "hostRequirements": { + "gpu": "optional" + }, + "features": { + "ghcr.io/devcontainers/features/conda:1": {}, + "ghcr.io/devcontainers/features/nvidia-cuda:1": { + "installCudnn": true, + "installToolkit": true, + "cudaVersion": "12.4" + }, + "ghcr.io/devcontainers/features/rust:1": {} + } +} diff --git a/pyproject.toml b/pyproject.toml index 5c45b0e9e..7c8b9fec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "outlines" authors= [{name = "Outlines Developers"}] description = "Probabilistic Generative Model Programming" -requires-python = ">=3.9" +requires-python = ">=3.9,<3.13" license = {text = "Apache-2.0"} keywords=[ "machine learning", From 6c598c3cb94d6a1419a35700c0be0b275f70b22f Mon Sep 17 00:00:00 2001 From: aman-17 Date: Tue, 11 Feb 2025 11:18:39 -0800 Subject: [PATCH 159/505] replaced pycountry with iso3166 --- outlines/types/countries.py | 23 +++++++++++++++-------- pyproject.toml | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/outlines/types/countries.py b/outlines/types/countries.py index bbfc0dde7..56e68d0c2 100644 --- a/outlines/types/countries.py +++ b/outlines/types/countries.py @@ -1,19 +1,26 @@ """Generate valid country codes and names.""" from enum import Enum -import pycountry +from iso3166 import countries -ALPHA_2_CODE = [(country.alpha_2, country.alpha_2) for country in pycountry.countries] + +def get_country_flags(): + """Generate Unicode flags for all ISO 3166-1 alpha-2 country codes in Alpha2 Enum.""" + base = ord('🇦') + return {code.name: chr(base + ord(code.name[0]) - ord('A')) + chr(base + ord(code.name[1]) - ord('A')) for code in Alpha2} + +ALPHA_2_CODE = [(country.alpha2, country.alpha2) for country in countries] Alpha2 = Enum("Alpha_2", ALPHA_2_CODE) # type:ignore -ALPHA_3_CODE = [(country.alpha_3, country.alpha_3) for country in pycountry.countries] -Alpha3 = Enum("Alpha_2", ALPHA_3_CODE) # type:ignore +ALPHA_3_CODE = [(country.alpha3, country.alpha3) for country in countries] +Alpha3 = Enum("Alpha_3", ALPHA_3_CODE) # type:ignore -NUMERIC_CODE = [(country.numeric, country.numeric) for country in pycountry.countries] +NUMERIC_CODE = [(str(country.numeric), str(country.numeric)) for country in countries] Numeric = Enum("Numeric_code", NUMERIC_CODE) # type:ignore -NAME = [(country.name, country.name) for country in pycountry.countries] +NAME = [(country.name, country.name) for country in countries] Name = Enum("Name", NAME) # type:ignore -FLAG = [(country.flag, country.flag) for country in pycountry.countries] -Flag = Enum("Flag", FLAG) # type:ignore +flag_mapping = get_country_flags() +FLAG = [(flag, flag) for code, flag in flag_mapping.items()] +Flag = Enum("Flag", FLAG) # type:ignore \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7c8b9fec3..e76f4f7b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "requests", "tqdm", "typing_extensions", - "pycountry", + "iso3166", "airportsdata", "torch", "outlines_core==0.1.26", @@ -145,7 +145,7 @@ module = [ "vllm.*", "uvicorn.*", "fastapi.*", - "pycountry.*", + "iso3166.*", "airportsdata.*", "outlines_core.*", "genson", From 774fe567e460ce1a1a3b03b7e6e501cb8dca5653 Mon Sep 17 00:00:00 2001 From: aman-17 Date: Tue, 11 Feb 2025 12:26:55 -0800 Subject: [PATCH 160/505] fixed code style --- outlines/types/countries.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/outlines/types/countries.py b/outlines/types/countries.py index 56e68d0c2..c612640b4 100644 --- a/outlines/types/countries.py +++ b/outlines/types/countries.py @@ -6,8 +6,13 @@ def get_country_flags(): """Generate Unicode flags for all ISO 3166-1 alpha-2 country codes in Alpha2 Enum.""" - base = ord('🇦') - return {code.name: chr(base + ord(code.name[0]) - ord('A')) + chr(base + ord(code.name[1]) - ord('A')) for code in Alpha2} + base = ord("🇦") + return { + code.name: chr(base + ord(code.name[0]) - ord("A")) + + chr(base + ord(code.name[1]) - ord("A")) + for code in Alpha2 + } + ALPHA_2_CODE = [(country.alpha2, country.alpha2) for country in countries] Alpha2 = Enum("Alpha_2", ALPHA_2_CODE) # type:ignore @@ -23,4 +28,4 @@ def get_country_flags(): flag_mapping = get_country_flags() FLAG = [(flag, flag) for code, flag in flag_mapping.items()] -Flag = Enum("Flag", FLAG) # type:ignore \ No newline at end of file +Flag = Enum("Flag", FLAG) # type:ignore From ad7a7403c1557f4e05293c665827ac55fd8df34d Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Fri, 14 Feb 2025 14:31:17 +0100 Subject: [PATCH 161/505] Update "Contribute" guide with `uv` and DevContainer intructions (#1405) Also add instructions about different outlines "flavors"! Co-authored-by: Cameron Pfiffer --- docs/community/contribute.md | 76 ++++++++++++++++++++++++++++-------- docs/installation.md | 29 +++++++++----- 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/docs/community/contribute.md b/docs/community/contribute.md index d29576b75..6867263b4 100644 --- a/docs/community/contribute.md +++ b/docs/community/contribute.md @@ -23,65 +23,108 @@ git clone git@github.com/YourUserName/outlines.git cd outlines ``` -Create a new virtual environment. *If you are using conda*: +Create a new virtual environment: + +*If you are using `uv`*: ```bash -conda env create -f environment.yml +uv venv +source .venv/bin/activate +alias pip="uv pip" # ... or just remember to prepend any pip command with uv in the rest of this guide ``` -*If you are using venv*: +*If you are using `venv`*: -```python +```bash python -m venv .venv source .venv/bin/activate ``` -Then install the dependencies in editable mode, and install the pre-commit hooks: +*If you are using `conda`*: + +```bash +conda env create -f environment.yml +``` + +Then install the dependencies in editable mode, and install the `pre-commit` hooks: -```python +```bash pip install -e ".[test]" pre-commit install ``` +Outlines provides optional dependencies for different supported backends, which you can install with + +```bash +pip install ".[vllm]" +``` + +A list of supported optional dependencies can be found in the [installation guide](/installation). + +### Using VSCode DevContainer / GitHub Codespaces + +If you want a fully pre-configured development environment, you can use VSCode DevContainers or GitHub Codespaces. + +#### VSCode DevContainer + +1. Ensure that the [Docker](https://www.docker.com/get-started/) daemon is running on your machine. +2. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension in VSCode. +3. Open the Outlines repository in VSCode. When prompted, **Reopen in Container** (or press `F1` and select "Remote-Containers: Reopen in Container"). +4. Run the normal setup steps. Your environment will not complain about missing system dependencies! + +#### GitHub Codespaces + +1. Navigate to the Outlines repository on GitHub. +2. Click on the **Code** button and select the **Codespaces** tab. +3. Click **Create codespace on main** (or another branch you are working on). +4. GitHub will launch a pre-configured cloud development environment. + +You will not have access to a GPU, but you'll be able to make basic contributions to the project on the go while using a fully featured web-based IDE. + ### Before pushing your code Run the tests: -```python +```bash pytest ``` And run the code style checks: -```python +```bash pre-commit run --all-files ``` ### Benchmarking -Outlines uses [asv](https://asv.readthedocs.io) for automated benchmark testing. Benchmarks are run automatically before pull requests are merged to prevent performance degredation. +Outlines uses [asv](https://asv.readthedocs.io) for automated benchmark testing. Benchmarks are run automatically before pull requests are merged to prevent performance degradation. You can run the benchmark test suite locally with the following command: -``` + +```bash asv run --config benchmarks/asv.conf.json ``` Caveats: + - If you're on a device with CUDA, you must add the argument `--launch-method spawn` - Uncommitted code will not be benchmarked, you must first commit your changes. #### Run a specific test: -``` + +```bash asv run --config benchmarks/asv.conf.json -b bench_json_schema.JsonSchemaBenchmark.time_json_schema_to_fsm ``` #### Profile a specific test: -``` + +```bash asv run --config benchmarks/asv.conf.json --profile -b bench_json_schema.JsonSchemaBenchmark.time_json_schema_to_fsm ``` #### Compare to `origin/main` -``` + +```bash get fetch origin asv continuous origin/main HEAD --config benchmarks/asv.conf.json ``` @@ -91,20 +134,19 @@ asv continuous origin/main HEAD --config benchmarks/asv.conf.json - **View ASV Benchmark Results:** Open the workflow, view `BENCHMARK RESULTS` section. - Merging is blocked unless benchmarks are run for the latest commit. - Benchmarks fail if performance degrades by more than 10% for any individual benchmark. -- The "Benchmark PR" workflow runs when its manually dispatched, or if the `run_benchmarks` label is added to the PR they run for every commit. - +- The "Benchmark PR" workflow runs when it is manually dispatched, or if the `run_benchmarks` label is added to the PR they run for every commit. ### Contribute to the documentation To work on the *documentation* you will need to install the related dependencies: -```python +```bash pip install -r requirements-doc.txt ``` To build the documentation and serve it locally, run the following command in the repository's root folder: -```python +```bash mkdocs serve ``` diff --git a/docs/installation.md b/docs/installation.md index bf2da86f9..5f8bb2d13 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,13 +6,13 @@ title: installation You can install Outlines with `pip`: -```python +```sh pip install outlines ``` -Outlines supports OpenAI, transformers, Mamba, llama.cpp and exllama2 but **you will need to install them manually**: +Outlines supports OpenAI, Transformers, Mamba, llama.cpp, and ExLlamaV2, but **you will need to install them manually**: -```python +```sh pip install openai pip install transformers datasets accelerate torch pip install llama-cpp-python @@ -21,18 +21,29 @@ pip install mamba_ssm transformers torch pip install vllm ``` -If you encounter any problem using Outlines with these libraries, take a look at their installation instructions. The installation of `openai` and `transformers` should be straightforward, but other libraries have specific hardware requirements. +If you encounter any problems using Outlines with these libraries, take a look at their installation instructions. The installation of `openai` and `transformers` should be straightforward, but other libraries have specific hardware requirements. -## Bleeding edge +## Optional Dependencies -You can install the latest version of Outlines on the repository's `main` branch: +Outlines provides multiple optional dependency sets to support different backends and use cases. You can install them as needed using: -```python +- `pip install "outlines[vllm]"` for [vLLM](https://github.com/vllm-project/vllm), optimized for high-throughput inference. +- `pip install "outlines[transformers]"` for [Hugging Face Transformers](https://huggingface.co/docs/transformers/index). +- `pip install "outlines[mlx]"` for [MLX-LM](https://github.com/ml-explore/mlx-lm), optimized for Apple silicon. +- `pip install "outlines[openai]"` to use OpenAI’s API. +- `pip install "outlines[llamacpp]"` for [llama.cpp](https://github.com/ggerganov/llama.cpp), a lightweight LLM inference engine. +- `pip install "outlines[exllamav2]"` for [ExLlamaV2](https://github.com/turboderp/exllamav2), optimized for NVIDIA GPUs. + +## Bleeding Edge + +You can install the latest version of Outlines from the repository's `main` branch: + +```sh pip install git+https://github.com/dottxt-ai/outlines.git@main ``` This can be useful, for instance, when a fix has been merged but not yet released. -## Installing for development +## Installing for Development -See the [contributing documentation](community/contribute.md) for instructions on how to install Outlines for development. +See the [contributing documentation](community/contribute.md) for instructions on how to install Outlines for development, including an example using the `dot-install` method for one of the backends. From ac49f578f046a1d6106ddcdf5b2276c45754286c Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Wed, 22 Jan 2025 11:16:39 +0100 Subject: [PATCH 162/505] Add nix configs and uv lock file --- flake.lock | 59 + flake.nix | 11 + pyproject.toml | 1 + shell.nix | 73 + uv.lock | 3979 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 4123 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 shell.nix create mode 100644 uv.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..16d4c44a4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,59 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1738136902, + "narHash": "sha256-pUvLijVGARw4u793APze3j6mU1Zwdtz7hGkGGkD87qw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9a5db3142ce450045840cc8d832b13b8a2018e0c", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..c82584b49 --- /dev/null +++ b/flake.nix @@ -0,0 +1,11 @@ +{ + inputs.flake-utils.url = "github:numtide/flake-utils"; + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + in { devShells.default = import ./shell.nix { inherit pkgs; }; }); +} diff --git a/pyproject.toml b/pyproject.toml index e76f4f7b9..f1ff57323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ "torch", "outlines_core==0.1.26", "genson", + "pre-commit>=4.0.1", ] dynamic = ["version"] diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..b67ed5b89 --- /dev/null +++ b/shell.nix @@ -0,0 +1,73 @@ +{ pkgs ? import { config = { allowUnfree = true; }; } }: + +(pkgs.buildFHSEnv { + name = "dottxt-ai"; + targetPkgs = pkgs: + with pkgs; [ + autoconf + binutils + cmake + cudatoolkit + curl + freeglut + gcc13 + git + gitRepo + gnumake + gnupg + gperf + libGL + libGLU + linuxPackages.nvidia_x11 + m4 + ncurses5 + procps + python311 + stdenv.cc + unzip + util-linux + uv + xorg.libX11 + xorg.libXext + xorg.libXi + xorg.libXmu + xorg.libXrandr + xorg.libXv + zlib + ]; + + multiPkgs = pkgs: with pkgs; [ zlib ]; + + runScript = "bash"; + + profile = '' + # CUDA paths + export CUDA_HOME=${pkgs.cudatoolkit} + export CUDA_PATH=${pkgs.cudatoolkit} + + # Ensure proper binary paths are included + export PATH=${pkgs.gcc13}/bin:${pkgs.cudatoolkit}/bin:$PATH + + # Set library paths, including additional directories for CUPTI + export LD_LIBRARY_PATH=${pkgs.cudatoolkit}/lib64:${pkgs.cudatoolkit}/extras/CUPTI/lib64:${pkgs.linuxPackages.nvidia_x11}/lib:$LD_LIBRARY_PATH + + # Add static library paths to EXTRA_LDFLAGS for the linker + export EXTRA_LDFLAGS="-L${pkgs.cudatoolkit}/lib64 -L${pkgs.cudatoolkit}/extras/CUPTI/lib64 -L${pkgs.linuxPackages.nvidia_x11}/lib -L${pkgs.cudatoolkit}/libdevice $EXTRA_LDFLAGS" + export EXTRA_CCFLAGS="-I${pkgs.cudatoolkit}/include $EXTRA_CCFLAGS" + + # Set CMake paths + export CMAKE_PREFIX_PATH=${pkgs.cudatoolkit}:${pkgs.linuxPackages.nvidia_x11}:$CMAKE_PREFIX_PATH + + # C++ and CC flags + export CXXFLAGS="--std=c++17 $EXTRA_CCFLAGS" + export CC=${pkgs.gcc13}/bin/gcc + export CXX=${pkgs.gcc13}/bin/g++ + + # NVCC flags to use the right compiler + export NVCC_FLAGS="-ccbin ${pkgs.gcc13}/bin/gcc" + ''; + + structuredAttrs__ = { + stdenv = pkgs.stdenv.overrideCC pkgs.stdenv.cc pkgs.gcc13; + }; +}).env diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..f1deefbe6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3979 @@ +version = 1 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version >= '3.9.17' and python_full_version < '3.10'", + "python_full_version < '3.9.17'", +] + +[[package]] +name = "accelerate" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/15/0fab0260ab4069e5224e637d2e400538bb27b0dfc36f17daf68db9770d78/accelerate-1.3.0.tar.gz", hash = "sha256:518631c0adb80bd3d42fb29e7e2dc2256bcd7c786b0ba9119bbaa08611b36d9c", size = 342758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/de/64508cb91af013aaba214752309c0967568a4219d50a4ea30e822af3c976/accelerate-1.3.0-py3-none-any.whl", hash = "sha256:5788d9e6a7a9f80fed665cf09681c4dddd9dc056bea656db4140ffc285ce423e", size = 336647 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/7d/ff2e314b8f9e0b1df833e2d4778eaf23eae6b8cc8f922495d110ddcbf9e1/aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8", size = 708550 }, + { url = "https://files.pythonhosted.org/packages/09/b8/aeb4975d5bba233d6f246941f5957a5ad4e3def8b0855a72742e391925f2/aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5", size = 468430 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/5b620279b3df46e597008b09fa1e10027a39467387c2332657288e25811a/aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2", size = 455593 }, + { url = "https://files.pythonhosted.org/packages/d8/75/0cdf014b816867d86c0bc26f3d3e3f194198dbf33037890beed629cd4f8f/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43", size = 1584635 }, + { url = "https://files.pythonhosted.org/packages/df/2f/95b8f4e4dfeb57c1d9ad9fa911ede35a0249d75aa339edd2c2270dc539da/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f", size = 1632363 }, + { url = "https://files.pythonhosted.org/packages/39/cb/70cf69ea7c50f5b0021a84f4c59c3622b2b3b81695f48a2f0e42ef7eba6e/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d", size = 1668315 }, + { url = "https://files.pythonhosted.org/packages/2f/cc/3a3fc7a290eabc59839a7e15289cd48f33dd9337d06e301064e1e7fb26c5/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef", size = 1589546 }, + { url = "https://files.pythonhosted.org/packages/15/b4/0f7b0ed41ac6000e283e7332f0f608d734b675a8509763ca78e93714cfb0/aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438", size = 1544581 }, + { url = "https://files.pythonhosted.org/packages/58/b9/4d06470fd85c687b6b0e31935ef73dde6e31767c9576d617309a2206556f/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3", size = 1529256 }, + { url = "https://files.pythonhosted.org/packages/61/a2/6958b1b880fc017fd35f5dfb2c26a9a50c755b75fd9ae001dc2236a4fb79/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55", size = 1536592 }, + { url = "https://files.pythonhosted.org/packages/0f/dd/b974012a9551fd654f5bb95a6dd3f03d6e6472a17e1a8216dd42e9638d6c/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e", size = 1607446 }, + { url = "https://files.pythonhosted.org/packages/e0/d3/6c98fd87e638e51f074a3f2061e81fcb92123bcaf1439ac1b4a896446e40/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33", size = 1628809 }, + { url = "https://files.pythonhosted.org/packages/a8/2e/86e6f85cbca02be042c268c3d93e7f35977a0e127de56e319bdd1569eaa8/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c", size = 1564291 }, + { url = "https://files.pythonhosted.org/packages/0b/8d/1f4ef3503b767717f65e1f5178b0173ab03cba1a19997ebf7b052161189f/aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745", size = 416601 }, + { url = "https://files.pythonhosted.org/packages/ad/86/81cb83691b5ace3d9aa148dc42bacc3450d749fc88c5ec1973573c1c1779/aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9", size = 442007 }, + { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 }, + { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 }, + { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 }, + { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 }, + { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 }, + { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 }, + { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 }, + { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 }, + { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 }, + { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 }, + { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 }, + { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 }, + { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 }, + { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 }, + { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 }, + { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 }, + { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 }, + { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 }, + { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 }, + { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 }, + { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 }, + { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 }, + { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 }, + { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 }, + { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 }, + { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 }, + { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 }, + { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 }, + { url = "https://files.pythonhosted.org/packages/49/d1/d8af164f400bad432b63e1ac857d74a09311a8334b0481f2f64b158b50eb/aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", size = 697982 }, + { url = "https://files.pythonhosted.org/packages/92/d1/faad3bf9fa4bfd26b95c69fc2e98937d52b1ff44f7e28131855a98d23a17/aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", size = 460662 }, + { url = "https://files.pythonhosted.org/packages/db/61/0d71cc66d63909dabc4590f74eba71f91873a77ea52424401c2498d47536/aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", size = 452950 }, + { url = "https://files.pythonhosted.org/packages/07/db/6d04bc7fd92784900704e16b745484ef45b77bd04e25f58f6febaadf7983/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", size = 1665178 }, + { url = "https://files.pythonhosted.org/packages/54/5c/e95ade9ae29f375411884d9fd98e50535bf9fe316c9feb0f30cd2ac8f508/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", size = 1717939 }, + { url = "https://files.pythonhosted.org/packages/6f/1c/1e7d5c5daea9e409ed70f7986001b8c9e3a49a50b28404498d30860edab6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", size = 1775125 }, + { url = "https://files.pythonhosted.org/packages/5d/66/890987e44f7d2f33a130e37e01a164168e6aff06fce15217b6eaf14df4f6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", size = 1677176 }, + { url = "https://files.pythonhosted.org/packages/8f/dc/e2ba57d7a52df6cdf1072fd5fa9c6301a68e1cd67415f189805d3eeb031d/aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", size = 1603192 }, + { url = "https://files.pythonhosted.org/packages/6c/9e/8d08a57de79ca3a358da449405555e668f2c8871a7777ecd2f0e3912c272/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", size = 1618296 }, + { url = "https://files.pythonhosted.org/packages/56/51/89822e3ec72db352c32e7fc1c690370e24e231837d9abd056490f3a49886/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", size = 1616524 }, + { url = "https://files.pythonhosted.org/packages/2c/fa/e2e6d9398f462ffaa095e84717c1732916a57f1814502929ed67dd7568ef/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", size = 1685471 }, + { url = "https://files.pythonhosted.org/packages/ae/5f/6bb976e619ca28a052e2c0ca7b0251ccd893f93d7c24a96abea38e332bf6/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", size = 1715312 }, + { url = "https://files.pythonhosted.org/packages/79/c1/756a7e65aa087c7fac724d6c4c038f2faaa2a42fe56dbc1dd62a33ca7213/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", size = 1672783 }, + { url = "https://files.pythonhosted.org/packages/73/ba/a6190ebb02176c7f75e6308da31f5d49f6477b651a3dcfaaaca865a298e2/aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", size = 410229 }, + { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081 }, + { url = "https://files.pythonhosted.org/packages/9f/37/326ee86b7640be6ca4493c8121cb9a4386e07cf1e5757ce6b7fa854d0a5f/aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e", size = 709424 }, + { url = "https://files.pythonhosted.org/packages/9c/c5/a88ec2160b06c22e57e483a1f78f99f005fcd4e7d6855a2d3d6510881b65/aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add", size = 468907 }, + { url = "https://files.pythonhosted.org/packages/b2/f0/02f03f818e91996161cce200241b631bb2b4a87e61acddb5b974e254a288/aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a", size = 455981 }, + { url = "https://files.pythonhosted.org/packages/0e/17/c8be12436ec19915f67b1ab8240d4105aba0f7e0894a1f0d8939c3e79c70/aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350", size = 1587395 }, + { url = "https://files.pythonhosted.org/packages/43/c0/f4db1ac30ebe855b2fefd6fa98767862d88ac54ab08a6ad07d619146270c/aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6", size = 1636243 }, + { url = "https://files.pythonhosted.org/packages/ea/a7/9acf20e9a09b0d38b5b55691410500d051a9f4194692cac22b0d0fc92ad9/aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1", size = 1672323 }, + { url = "https://files.pythonhosted.org/packages/f7/5b/a27e8fe1a3b0e245ca80863eefd83fc00136752d27d2cf1afa0130a76f34/aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e", size = 1589521 }, + { url = "https://files.pythonhosted.org/packages/25/50/8bccd08004e15906791b46f0a908a8e7f5e0c5882b17da96d1933bd34ac0/aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd", size = 1544059 }, + { url = "https://files.pythonhosted.org/packages/84/5a/42250b37b06ee0cb7a03dd1630243b1d739ca3edb5abd8b18f479a539900/aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1", size = 1530217 }, + { url = "https://files.pythonhosted.org/packages/18/08/eb334da86cd2cdbd0621bb7039255b19ca74ce8b05e8fb61850e2589938c/aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c", size = 1536081 }, + { url = "https://files.pythonhosted.org/packages/1a/a9/9d59958084d5bad7e77a44841013bd59768cda94f9f744769461b66038fc/aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e", size = 1606918 }, + { url = "https://files.pythonhosted.org/packages/4f/e7/27feb1cff17dcddb7a5b703199106196718d622a3aa70f80a386d15361d7/aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28", size = 1629101 }, + { url = "https://files.pythonhosted.org/packages/e8/29/49debcd858b997c655fca274c5247fcfe29bf31a4ddb1ce3f088539b14e4/aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226", size = 1567338 }, + { url = "https://files.pythonhosted.org/packages/3b/34/33af1e97aba1862e1812e2e2b96a1e050c5a6e9cecd5a5370591122fb07b/aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3", size = 416914 }, + { url = "https://files.pythonhosted.org/packages/2d/47/28b3fbd97026963af2774423c64341e0d4ec180ea3b79a2762a3c18d5d94/aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1", size = 442225 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + +[[package]] +name = "airportsdata" +version = "20241001" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/75/0af9b73babfec14df1395ca60e2a174e887c5d515e823c39a60f27bbbb30/airportsdata-20241001.tar.gz", hash = "sha256:fa0bd143b4f4be3557cb892fa0612ef210fd91a92bd720b4d8221de576a4fa00", size = 903075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/57/57635b0af4a23723b2b43109aa4eae7395d346d1eed8a00691cf3acfc5bd/airportsdata-20241001-py3-none-any.whl", hash = "sha256:67d71cf2c5378cc17ff66b62b1e11aa2444043949c894543ac8fd8dafce192fd", size = 912677 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "attrs" +version = "24.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, +] + +[[package]] +name = "beartype" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/6c/fecfe9c7d7b903a52b732b8f33968a067cad8209848ae8037c4d0ed53055/beartype-0.15.0.tar.gz", hash = "sha256:2af6a8d8a7267ccf7d271e1a3bd908afbc025d2a09aa51123567d7d7b37438df", size = 1002709 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/e7/9220de010e015fdfb7ce22e7d9a846bfcd6e5d686c4fc555fa76a22846ba/beartype-0.15.0-py3-none-any.whl", hash = "sha256:52cd2edea72fdd84e4e7f8011a9e3007bf0125c3d6d7219e937b9d8868169177", size = 777565 }, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992 }, +] + +[[package]] +name = "cmake" +version = "3.31.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/cb/3a327fa784a5dbaf838b135cb1729f43535c52d83bbf02191fb8a0cb118e/cmake-3.31.4.tar.gz", hash = "sha256:a6ac2242e0b16ad7d94c9f8572d6f232e6169747be50e5cdf497f206c4819ce1", size = 34278 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/db/50efa1d3e29cb2a6e8e143e522e52698b3fc08f4b56100fb35f97a70af79/cmake-3.31.4-py3-none-macosx_10_10_universal2.whl", hash = "sha256:fc048b4b70facd16699a43c737f6782b4eff56e8e6093090db5979532d9db0f6", size = 47198138 }, + { url = "https://files.pythonhosted.org/packages/c7/76/ccb8764761c739ef16bd8957a16ecbda01b03c2d7d241c376bfca6bf2822/cmake-3.31.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a37be93534df04513f0845492d71bc80899c3f87b77e3b01c95aff1a7fc9bde", size = 27556485 }, + { url = "https://files.pythonhosted.org/packages/ad/8e/888e2944655d7fa1ea5af46b60883a0e7847bbf9fb7ecc321c8e5f0a1394/cmake-3.31.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9f5f8289c5e7bd2ed654cbac164021fa7723064fee0443a2f0068bc08413d81", size = 26808834 }, + { url = "https://files.pythonhosted.org/packages/59/f4/0b2b1430a441c3c09ee102bf8c5d9ec1dc11d002ff4affef15c656f37ce9/cmake-3.31.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926d91cae2ba7d2f3df857d0fc066bdac4f3904bf5c95e99b60435e85aabedb4", size = 27140820 }, + { url = "https://files.pythonhosted.org/packages/d1/f9/a274b4e36e457d8e99db1038cc31a6c391bf3bc26230c2dc9caf37499753/cmake-3.31.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:929a8d8d289d69e43784661748ddd08933ce1ec5db8f9bcfce6ee817a48f8787", size = 28868269 }, + { url = "https://files.pythonhosted.org/packages/9b/35/8da1ffa00a3f3853881aa5025cdf11c744303013df70c8716155b83825d3/cmake-3.31.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b463efdf5b92f3b290235aa9f8da092b3dac19b7636c563fd156022dab580649", size = 30732267 }, + { url = "https://files.pythonhosted.org/packages/79/48/bb8485687f5a64d52ac68cfcb02e9b8e46a9e107f380c54d484b6632c87e/cmake-3.31.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:225d9a643b0b60ffce0399ff0cabd7a4820e0dbcb794e97d3aacfcf7c0589ae6", size = 26908885 }, + { url = "https://files.pythonhosted.org/packages/e5/9e/2594d7fa8b263296497bf044469b4ab4797c51675ea629f9672011cdfe09/cmake-3.31.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89143a5e2a5916061f2cfc5012e9fe6281aaf7c0dae7930bdc68d105d22ddc39", size = 27784555 }, + { url = "https://files.pythonhosted.org/packages/95/16/5b1989f1d2287b05cd68792c0a48b721c060f728506d719fcf0e3b80ceb2/cmake-3.31.4-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:f96127bf663168accd29d5a50ee68ea80f26bcd37f96c7a14ef2378781f19936", size = 24965366 }, + { url = "https://files.pythonhosted.org/packages/5a/4c/289fb0986c6ff63583383eca0c9479147f362330938856a9b5201c84cee8/cmake-3.31.4-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:25c5094394f0cee21130b5678e5b4552f72470e266df6d6fb1d5c505100f0eaa", size = 27824887 }, + { url = "https://files.pythonhosted.org/packages/3c/f3/d45ba2b5bb54f4ef615a6a24cf6258600eec790a9d5017c9584107b445b9/cmake-3.31.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:466c9295af440bb4a47cc5e1af10576cf2227620528afd0fd0b3effa1d513b49", size = 31368421 }, + { url = "https://files.pythonhosted.org/packages/34/3d/f6b712241ede5fb8e32c13e119c06e142f3f12ead1656721b1f67756106b/cmake-3.31.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f6af3b83a1b1fc1d990d18b6a566ee9c95c0393f986c6df15f2505dda8ad1bcc", size = 32074545 }, + { url = "https://files.pythonhosted.org/packages/f0/23/48cd0404d7238d703a4cd4d7434eeaf12e8fbe68160d52f1489f55f582df/cmake-3.31.4-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:23781e17563693a68b0cef85749746894b8a61488e56e96fc6649b73652e8236", size = 27946950 }, + { url = "https://files.pythonhosted.org/packages/21/03/014d9710bccf5a7e04c6f6ee27bfaba1220e79ee145d7b95f84e7843729b/cmake-3.31.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:838a388b559137f3654d8cf30f62bbdec10f8d1c3624f0d289614d33cdf4fba1", size = 29473412 }, + { url = "https://files.pythonhosted.org/packages/23/de/5a8142732f0a52dedac2887e0c105c9bbb449e517ade500e56bf2af520d1/cmake-3.31.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6a3b0b9557f41c955a6b25c94205f2ca9c3a46edca809ad87507c5ef6bc4274", size = 32971081 }, + { url = "https://files.pythonhosted.org/packages/a5/a1/50c11f0b110986c753592f025970094030b25748df126abe8e38265be722/cmake-3.31.4-py3-none-win32.whl", hash = "sha256:d378c9e58eac906bddafd673c7571262dcd5a9946bb1e8f9e3902572a8fa95ca", size = 33351393 }, + { url = "https://files.pythonhosted.org/packages/0c/7f/331d181b6b1b8942ec5fad23e98fff85218485f29f62f6bc60663d424df8/cmake-3.31.4-py3-none-win_amd64.whl", hash = "sha256:20be7cdb41903edf85e8a498c4beff8d6854acbb087abfb07c362c738bdf0018", size = 36496715 }, + { url = "https://files.pythonhosted.org/packages/65/26/11a78723364716004928b7bea7d96cf2c72dc3abfaa7c163159110fcb649/cmake-3.31.4-py3-none-win_arm64.whl", hash = "sha256:9479a9255197c49e135df039d8484c69aa63158a06ae9c2d0eb939da2f0f7dff", size = 35559239 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 }, + { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 }, + { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 }, + { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 }, + { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 }, + { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 }, + { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 }, + { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 }, + { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 }, + { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 }, + { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, + { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, + { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, + { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, + { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, + { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, + { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, + { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, + { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, + { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, + { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, + { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, + { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, + { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, + { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, + { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, + { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, + { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, + { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, + { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, + { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, + { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, + { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, + { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, + { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, + { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, + { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, + { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, + { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, + { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, + { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, + { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, + { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, + { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, + { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, + { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, + { url = "https://files.pythonhosted.org/packages/40/41/473617aadf9a1c15bc2d56be65d90d7c29bfa50a957a67ef96462f7ebf8e/coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", size = 207978 }, + { url = "https://files.pythonhosted.org/packages/10/f6/480586607768b39a30e6910a3c4522139094ac0f1677028e1f4823688957/coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", size = 208415 }, + { url = "https://files.pythonhosted.org/packages/f1/af/439bb760f817deff6f4d38fe7da08d9dd7874a560241f1945bc3b4446550/coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", size = 236452 }, + { url = "https://files.pythonhosted.org/packages/d0/13/481f4ceffcabe29ee2332e60efb52e4694f54a402f3ada2bcec10bb32e43/coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", size = 234374 }, + { url = "https://files.pythonhosted.org/packages/c5/59/4607ea9d6b1b73e905c7656da08d0b00cdf6e59f2293ec259e8914160025/coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", size = 235505 }, + { url = "https://files.pythonhosted.org/packages/85/60/d66365723b9b7f29464b11d024248ed3523ce5aab958e4ad8c43f3f4148b/coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", size = 234616 }, + { url = "https://files.pythonhosted.org/packages/74/f8/2cf7a38e7d81b266f47dfcf137fecd8fa66c7bdbd4228d611628d8ca3437/coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", size = 233099 }, + { url = "https://files.pythonhosted.org/packages/50/2b/bff6c1c6b63c4396ea7ecdbf8db1788b46046c681b8fcc6ec77db9f4ea49/coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", size = 234089 }, + { url = "https://files.pythonhosted.org/packages/bf/b5/baace1c754d546a67779358341aa8d2f7118baf58cac235db457e1001d1b/coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", size = 210701 }, + { url = "https://files.pythonhosted.org/packages/b1/bf/9e1e95b8b20817398ecc5a1e8d3e05ff404e1b9fb2185cd71561698fe2a2/coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", size = 211482 }, + { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cramjam" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/68/09b6b5603d21a0c7d4362d513217a5079c47b1b7a88967c52dbef13db183/cramjam-2.9.1.tar.gz", hash = "sha256:336cc591d86cbd225d256813779f46624f857bc9c779db126271eff9ddc524ae", size = 47892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/5d/0b03115fa6a95a6dd9be344cd186879b763f1a6fab57ae55ffe2777aa0a7/cramjam-2.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8e82464d1e00fbbb12958999b8471ba5e9f3d9711954505a0a7b378762332e6f", size = 2136622 }, + { url = "https://files.pythonhosted.org/packages/6f/ac/a17644e182ede7e8e24fb3af038bc2c1cf3dd0447c935cb10409f21d099b/cramjam-2.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d2df8a6511cc08ef1fccd2e0c65e2ebc9f57574ec8376052a76851af5398810", size = 1927947 }, + { url = "https://files.pythonhosted.org/packages/9e/1e/e6c4f9695e4ba7b9c63160dcbfa76428bd3221930eedeb8f16364ab6f642/cramjam-2.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:21ea784e6c3f1843d3523ae0f03651dd06058b39eeb64beb82ee3b100fa83662", size = 2268766 }, + { url = "https://files.pythonhosted.org/packages/ab/37/4c81e5d039bdfc75a695abd426e6cdd9ab18a87f65d57837d78936cfa226/cramjam-2.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0c5d98a4e791f0bbd0ffcb7dae879baeb2dcc357348a8dc2be0a8c10403a2a", size = 2108762 }, + { url = "https://files.pythonhosted.org/packages/b9/bb/3bf3a8877b9a4105b625d710410bd2bc83ef38d4a7fe4eaeb3895d997b2d/cramjam-2.9.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e076fd87089197cb61117c63dbe7712ad5eccb93968860eb3bae09b767bac813", size = 2086694 }, + { url = "https://files.pythonhosted.org/packages/c3/78/317b7ab6a9b0f24c45d56305a8288cdb6408f855034dc80530ed16a5cc6c/cramjam-2.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d86b44933aea0151e4a2e1e6935448499849045c38167d288ca4c59d5b8cd4e", size = 2441698 }, + { url = "https://files.pythonhosted.org/packages/c5/2d/bc98992c29eb8647196b3bda814fd7ecfba6aff85177d44180be2aa320e8/cramjam-2.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb032549dec897b942ddcf80c1cdccbcb40629f15fc902731dbe6362da49326", size = 2759280 }, + { url = "https://files.pythonhosted.org/packages/dd/64/a4e54d74110c22477e467586935167d61fc7bae5284d393e76779b214a3e/cramjam-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf29b4def86ec503e329fe138842a9b79a997e3beb6c7809b05665a0d291edff", size = 2385128 }, + { url = "https://files.pythonhosted.org/packages/b0/1a/6ee093bf8a41cf31980175310abbbcdd1a39dadadbe96843112f42cef0fe/cramjam-2.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a36adf7d13b7accfa206e1c917f08924eb905b45aa8e62176509afa7b14db71e", size = 2373494 }, + { url = "https://files.pythonhosted.org/packages/9d/a6/1ae1f1a8ef559c2fab9d6d7f09b19995684e6727e617bf1b73967ee1c6be/cramjam-2.9.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:cf4ea758d98b6fad1b4b2d808d0de690d3162ac56c26968aea0af6524e3eb736", size = 2386900 }, + { url = "https://files.pythonhosted.org/packages/d9/e6/cf18deeaa0a96e7fc87f0eacde3c97e2893b573ac148ec746655570c18fc/cramjam-2.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4826d6d81ea490fa7a3ae7a4b9729866a945ffac1f77fe57b71e49d6e1b21efd", size = 2400609 }, + { url = "https://files.pythonhosted.org/packages/90/97/98a8fa24249dc72a936a9a51a81407a399070ba4ceb528d0af291c760eff/cramjam-2.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:335103317475bf992953c58838152a4761fc3c87354000edbfc4d7e57cf05909", size = 2553159 }, + { url = "https://files.pythonhosted.org/packages/ae/6b/4f71f72bc3405f221ec8bd2ba869e324d5f87ddd58c14bf59f7937ea37ab/cramjam-2.9.1-cp310-cp310-win32.whl", hash = "sha256:258120cb1e3afc3443f756f9de161ed63eed56a2c31f6093e81c571c0f2dc9f6", size = 1817873 }, + { url = "https://files.pythonhosted.org/packages/8e/f4/32639916897d59e94d286b5b22263ce8c2903ecc93a868ebe9443ece8f12/cramjam-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c60e5996aa02547d12bc2740d44e90e006b0f93100f53206f7abe6732ad56e69", size = 2092168 }, + { url = "https://files.pythonhosted.org/packages/6c/28/dd2b62be30ffe1fa8df10c99ba7b46abfbfb2fc6ace6acbbf9264a1a6b48/cramjam-2.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b9db1debe48060e41a5b91af9193c524e473c57f6105462c5524a41f5aabdb88", size = 2136699 }, + { url = "https://files.pythonhosted.org/packages/03/c9/fcebeb6f06879af4226337715fbc42ffe543158bcba8c244bba144767897/cramjam-2.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f6f18f0242212d3409d26ce3874937b5b979cebd61f08b633a6ea893c32fc7b6", size = 1927934 }, + { url = "https://files.pythonhosted.org/packages/e8/f3/77032e4f5db4dfcc2b0365f92655b7d6f3fc1527ea5b637f9fb9f8156a65/cramjam-2.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b5b1cd7d39242b2b903cf09cd4696b3a6e04dc537ffa9f3ac8668edae76eecb6", size = 2268584 }, + { url = "https://files.pythonhosted.org/packages/38/16/52175e94390f57196382783a3386c122ace7656b57339abaacdc9433b609/cramjam-2.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47de0a68f5f4d9951250ef5af31f2a7228132caa9ed60994234f7eb98090d33", size = 2108599 }, + { url = "https://files.pythonhosted.org/packages/99/25/5f7476d127a8d18cd19a2f3fd25c0fe09ef7848069d23aac70bc96385eb6/cramjam-2.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e13c9a697881e5e38148958612dc6856967f5ff8cd7bba5ff751f2d6ac020aa4", size = 2086632 }, + { url = "https://files.pythonhosted.org/packages/7b/97/76ff3e1209add6acb7e2aa7997be48dc1f92ad66ee3e8fa1179eb2bb9b44/cramjam-2.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba560244bc1335b420b74e91e35f9d4e7f307a3be3a4603ce0f0d7e15a0acdf0", size = 2441757 }, + { url = "https://files.pythonhosted.org/packages/69/c4/228e74c30576556d11e54d86f356955cd86ff5e11bbfec74b66ed0dd237d/cramjam-2.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d47fd41ce260cf4f0ff0e788de961fab9e9c6844a05ce55d06ce31e06107bdc", size = 2758144 }, + { url = "https://files.pythonhosted.org/packages/4b/e7/0fd22e12c6a2879abc501979779d4b8cfe8fe692c708c2c0d1664e88fd79/cramjam-2.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84d154fbadece82935396eb6bcb502085d944d2fd13b07a94348364344370c2c", size = 2385062 }, + { url = "https://files.pythonhosted.org/packages/dd/9c/845592ddf9eb7130ae8bc5958a01d469304a43f8071effe164e2d239e3fa/cramjam-2.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:038df668ffb94d64d67b6ecc59cbd206745a425ffc0402897dde12d89fa6a870", size = 2373473 }, + { url = "https://files.pythonhosted.org/packages/10/c2/287cc94b7f8e87e3b0c21819d3a5deead99ebfdcb2b2d85cd04011b37292/cramjam-2.9.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:4125d8cd86fa08495d310e80926c2f0563f157b76862e7479f9b2cf94823ea0c", size = 2386816 }, + { url = "https://files.pythonhosted.org/packages/7c/22/869a1eeea53db4d9fbde6693a2465909762bffeab1a671e193c95b26f99f/cramjam-2.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4206ebdd1d1ef0f3f86c8c2f7c426aa4af6094f4f41e274601fd4c4569f37454", size = 2400713 }, + { url = "https://files.pythonhosted.org/packages/3f/89/ff988bd6427f01041ccb1a9104c05b6373ae476682d317b6844f4b40af92/cramjam-2.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab687bef5c493732b9a4ab870542ee43f5eae0025f9c684c7cb399c3a85cb380", size = 2553081 }, + { url = "https://files.pythonhosted.org/packages/2e/68/13fa8561335de609f3cd40b132c1a3abbaf26d3c277e8b8a7446de34ef2c/cramjam-2.9.1-cp311-cp311-win32.whl", hash = "sha256:dda7698b6d7caeae1047adafebc4b43b2a82478234f6c2b45bc3edad854e0600", size = 1817782 }, + { url = "https://files.pythonhosted.org/packages/94/75/f3506ee802460e3b86a91e53bba1f67cf457fa04e4316fe7d5823ba5d28b/cramjam-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:872b00ff83e84bcbdc7e951af291ebe65eed20b09c47e7c4af21c312f90b796f", size = 2092227 }, + { url = "https://files.pythonhosted.org/packages/56/66/69a1c17331e38b02c78c923262fc315272de7c2618ef7eac8b3358969d90/cramjam-2.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:79417957972553502b217a0093532e48893c8b4ca30ccc941cefe9c72379df7c", size = 2132273 }, + { url = "https://files.pythonhosted.org/packages/3d/17/23d0b1d3301480e924545cdd27f2b949c50438949f64c74e800a09c12c37/cramjam-2.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce2b94117f373defc876f88e74e44049a9969223dbca3240415b71752d0422fb", size = 1926919 }, + { url = "https://files.pythonhosted.org/packages/8e/da/e9565f4abbbaa14645ccd7ce83f9631e90955454b87dc3ef9208aebc72e6/cramjam-2.9.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:67040e0fd84404885ec716a806bee6110f9960c3647e0ef1670aab3b7375a70a", size = 2271776 }, + { url = "https://files.pythonhosted.org/packages/88/ac/e6e0794ac01deb52e7a6a3e59720699abdee08d9b9c63a8d8874201d8155/cramjam-2.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bedb84e068b53c944bd08dcb501fd00d67daa8a917922356dd559b484ce7eab", size = 2109248 }, + { url = "https://files.pythonhosted.org/packages/22/0f/c3724b2dcdfbe7e07917803cf7a6db4a874818a6f8d2b95ca1ceaf177170/cramjam-2.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06e3f97a379386d97debf08638a78b3d3850fdf6124755eb270b54905a169930", size = 2088611 }, + { url = "https://files.pythonhosted.org/packages/ce/16/929a5ae899ad6298f58e66622dc223476fe8e1d4e8dae608f4e1a34bfd09/cramjam-2.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11118675e9c7952ececabc62f023290ee4f8ecf0bee0d2c7eb8d1c402ee9769d", size = 2438373 }, + { url = "https://files.pythonhosted.org/packages/2a/2a/ad473f1ca65d3285e8c1d99fc0289f5856224c0d452dabcf856fd4dcdd77/cramjam-2.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b7de6b61b11545570e4d6033713f3599525efc615ee353a822be8f6b0c65b77", size = 2836669 }, + { url = "https://files.pythonhosted.org/packages/9b/5a/e9b4868ee27099a2a21646cf5ea5cf08c660eae90b55a395ada974dcf3fb/cramjam-2.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57ca8f3775324a9de3ee6f05ca172687ba258c0dea79f7e3a6b4112834982f2a", size = 2343995 }, + { url = "https://files.pythonhosted.org/packages/5f/c4/870a9b4524107bf85a207b82a42613318881238b20f2d237e62815af646a/cramjam-2.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9847dd6f288f1c56359f52acb48ff2df848ff3e3bff34d23855bbcf7016427cc", size = 2374270 }, + { url = "https://files.pythonhosted.org/packages/70/4b/b69e8e3951b7cec5e7da2539b7573bb396bed66af07d760b1878b00fd120/cramjam-2.9.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d1248dfa7f151e893ce819670f00879e4b7650b8d4c01279ce4f12140d68dd2", size = 2388789 }, + { url = "https://files.pythonhosted.org/packages/05/1a/af02f6192060413314735c0db61259d7279b0d8d99eee29eff2af09c5892/cramjam-2.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9da6d970281083bae91b914362de325414aa03c01fc806f6bb2cc006322ec834", size = 2402459 }, + { url = "https://files.pythonhosted.org/packages/20/9a/a4ab3e90d72eb4f2c1b983fa32b4050ba676f533ba15bd78158f0632295a/cramjam-2.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1c33bc095db5733c841a102b8693062be5db8cdac17b9782ebc00577c6a94480", size = 2518440 }, + { url = "https://files.pythonhosted.org/packages/35/3b/e632dd7e2c5c8a2af2d83144b00d6840f1afcf9c6959ed59ec5b0f925288/cramjam-2.9.1-cp312-cp312-win32.whl", hash = "sha256:9e9193cd4bb57e7acd3af24891526299244bfed88168945efdaa09af4e50720f", size = 1822630 }, + { url = "https://files.pythonhosted.org/packages/0e/a2/d1c46618b81b83578d58a62f3709046c4f3b4ddba10df4b9797cfe096b98/cramjam-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:15955dd75e80f66c1ea271167a5347661d9bdc365f894a57698c383c9b7d465c", size = 2094684 }, + { url = "https://files.pythonhosted.org/packages/85/45/f1d1e6ffdceb3b0c18511df2f8e779e03972459fb71d7c1ab0f6a5c063a3/cramjam-2.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5a7797a2fff994fc5e323f7a967a35a3e37e3006ed21d64dcded086502f482af", size = 2131814 }, + { url = "https://files.pythonhosted.org/packages/3a/96/36bbd431fbf0fa2ff51fd2db4c3bead66e9e373693a8455d411d45125a68/cramjam-2.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d51b9b140b1df39a44bff7896d98a10da345b7d5f5ce92368d328c1c2c829167", size = 1926380 }, + { url = "https://files.pythonhosted.org/packages/67/c4/99b6507ec697d5f56d32c9c04614775004b05b7fa870725a492dc6b639eb/cramjam-2.9.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:07ac76b7f992556e7aa910244be11ece578cdf84f4d5d5297461f9a895e18312", size = 2271581 }, + { url = "https://files.pythonhosted.org/packages/cb/1b/6d55dff244fb22c0b686dd5a96a754c0638f8a94056beb27c457c6035cc5/cramjam-2.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d90a72608c7550cd7eba914668f6277bfb0b24f074d1f1bd9d061fcb6f2adbd6", size = 2109255 }, + { url = "https://files.pythonhosted.org/packages/ca/fb/b9fcf492a21a8d978c6f999025fce2c6656399448c017ed2fc859425f37f/cramjam-2.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56495975401b1821dbe1f29cf222e23556232209a2fdb809fe8156d120ca9c7f", size = 2088323 }, + { url = "https://files.pythonhosted.org/packages/88/1f/69b523395aeaa201dbd53d203453288205a0c651e7c910161892d694eb4d/cramjam-2.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b695259e71fde6d5be66b77a4474523ced9ffe9fe8a34cb9b520ec1241a14d3", size = 2437930 }, + { url = "https://files.pythonhosted.org/packages/b0/2c/d07e802f1786c4082e8286db1087563e4fab31cd6534ed31523f1f9584d1/cramjam-2.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab1e69dc4831bbb79b6d547077aae89074c83e8ad94eba1a3d80e94d2424fd02", size = 2836655 }, + { url = "https://files.pythonhosted.org/packages/1f/f5/6b425e82395c078bc95a7437b685e6bdba39d28c2b2986d79374fc1681aa/cramjam-2.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440b489902bfb7a26d3fec1ca888007615336ff763d2a32a2fc40586548a0dbf", size = 2387107 }, + { url = "https://files.pythonhosted.org/packages/33/65/7bf97d89ba7607aaea5464af6f249e3d94c291acf73d72768367a3e361c0/cramjam-2.9.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:217fe22b41f8c3dce03852f828b059abfad11d1344a1df2f43d3eb8634b18d75", size = 2374006 }, + { url = "https://files.pythonhosted.org/packages/29/11/8b6c82eda6d0affbc15d7ab4dc758856eb4308e8ddae73300c1648f5aa0f/cramjam-2.9.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:95f3646ddc98af25af25d5692ae65966488a283813336ea9cf41b22e542e7c0d", size = 2388731 }, + { url = "https://files.pythonhosted.org/packages/48/25/6cdd57c0b1a83c98aec9029310d09a6c1a31e9e9fb8efd9001bd0cbea992/cramjam-2.9.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:6b19fc60ead1cae9795a5b359599da3a1c95d38f869bdfb51c441fd76b04e926", size = 2402131 }, + { url = "https://files.pythonhosted.org/packages/b4/e7/cbf80c9647fa582432aa833c4bdd20cf437917c8066ce653e3b78deff658/cramjam-2.9.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8dc5207567459d049696f62a1fdfb220f3fe6aa0d722285d44753e12504dac6c", size = 2555296 }, + { url = "https://files.pythonhosted.org/packages/18/a6/fabe1959a980f5d2783a6c138311509dd168bd76e62018624a91cd1cbb41/cramjam-2.9.1-cp313-cp313-win32.whl", hash = "sha256:fbfe35929a61b914de9e5dbacde0cfbba86cbf5122f9285a24c14ed0b645490b", size = 1822484 }, + { url = "https://files.pythonhosted.org/packages/55/d5/24e4562771711711c466768c92097640ed97b0283abe9043ffb6c6d4cf04/cramjam-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:06068bd191a82ad4fc1ac23d6f8627fb5e37ec4be0431711b9a2dbacaccfeddb", size = 2094445 }, + { url = "https://files.pythonhosted.org/packages/c7/5a/50523fd478390acb6ca8e57239f7cf79f7260dc0d16be89137d47823e50a/cramjam-2.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:af39006faddfc6253beb93ca821d544931cfee7f0177b99ff106dfd8fd6a2cd8", size = 2137158 }, + { url = "https://files.pythonhosted.org/packages/df/83/54eca302e431d51149074d8aad6ec588870c5797060e2142dfe6ca3599a8/cramjam-2.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b3291be0d3f73d5774d69013be4ab33978c777363b5312d14f62f77817c2f75a", size = 1927910 }, + { url = "https://files.pythonhosted.org/packages/6d/e9/5d38ffa5376c5bffcbd16545707d9dac6beffccd00410f0cc19d83d85ef7/cramjam-2.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1539fd758f0e57fad7913cebff8baaee871bb561ddf6fa710a427b74da6b6778", size = 2269458 }, + { url = "https://files.pythonhosted.org/packages/15/f3/99fedc4210db1967256e602fdcb60947585421fd659f8baeeeb4ea16e4c7/cramjam-2.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff362f68bd68ac0eccb445209238d589bba728fb6d7f2e9dc199e0ec3a61d6e0", size = 2109406 }, + { url = "https://files.pythonhosted.org/packages/f2/e9/f380e0c1bd03046c522da4fd6d43ea897ba0b832c78fc4ea5708d8c35c21/cramjam-2.9.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23b9786d1d17686fb8d600ade2a19374c7188d4b8867efa9af0d8274a220aec7", size = 2086677 }, + { url = "https://files.pythonhosted.org/packages/13/a7/3ae887753f6d41f6e4af8e25654d103c56e13dda2f4b4d13acac570c65c1/cramjam-2.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bc9c2c748aaf91863d89c4583f529c1c709485c94f8dfeb3ee48662d88e3258", size = 2442136 }, + { url = "https://files.pythonhosted.org/packages/de/a2/763fd98340936057e44ea0b870c9cdb87ad5f90d49e492e8a11cf74e7b29/cramjam-2.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd0fa9a0e7f18224b6d2d1d69dbdc3aecec80ef1393c59244159b131604a4395", size = 2754985 }, + { url = "https://files.pythonhosted.org/packages/33/31/7c8cdf6b16fcd46bad4a307c8203a58b7a2fddf6cb3aad9dc441c050f62f/cramjam-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ceef6e09ee22457997370882aa3c69de01e6dd0aaa2f953e1e87ad11641d042", size = 2385597 }, + { url = "https://files.pythonhosted.org/packages/dd/ba/ec0f3b5a3a90721bdb42f4f4989b60adf823d137f40365e83df0cd299378/cramjam-2.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1376f6fdbf0b30712413a0b4e51663a4938ae2f6b449f8e4635dbb3694db83cf", size = 2374339 }, + { url = "https://files.pythonhosted.org/packages/ff/0a/f5bccdc8d12821aed4473a427e9eb8282a38c9337a30e02ed102b18941bf/cramjam-2.9.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:342fb946f8d3e9e35b837288b03ab23cfbe0bb5a30e582ed805ef79706823a96", size = 2386933 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/ce3ffad2b3b8cb73156a19345e27a2e27fb5be79b64f2c81b0c6d6e16c57/cramjam-2.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a237064a6e2c2256c9a1cf2beb7c971382190c0f1eb2e810e02e971881756132", size = 2400860 }, + { url = "https://files.pythonhosted.org/packages/32/a9/e4509e5dfc8f41d9e7f9fdddbf567967937303621d410197c86b11d6a3e4/cramjam-2.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53145fc9f2319c1245d4329e1da8cfacd6e35e27090c07c0b9d453ae2bbdac3e", size = 2553681 }, + { url = "https://files.pythonhosted.org/packages/0a/83/52401c5c654ddff2850d890b0f1cfc355ff6887c6def420d0c8d8178ff97/cramjam-2.9.1-cp39-cp39-win32.whl", hash = "sha256:8a9f52c27292c21457f43c4ce124939302a9acfb62295e7cda8667310563a5a3", size = 1818130 }, + { url = "https://files.pythonhosted.org/packages/93/b3/1645986d8b915fd0426a7224cd00c2c17c32b4d69bc5faad3fb3f5fd5081/cramjam-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:8097ee39b61c86848a443c0b25b2df1de6b331fd512b20836a4f5cfde51ab255", size = 2092440 }, + { url = "https://files.pythonhosted.org/packages/bc/91/3f7884172573072a4280bc8bc19b7562b2cd66d2a65576b11e72115cd5fe/cramjam-2.9.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:86824c695688fcd06c5ac9bbd3fea9bdfb4cca194b1e706fbf11a629df48d2b4", size = 2159537 }, + { url = "https://files.pythonhosted.org/packages/ef/49/a0a89e9c45413e89a1e408d4ab416c0f88f19f6db7571fd5c517e429e276/cramjam-2.9.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:27571bfa5a5d618604696747d0dc1d2a99b5906c967c8dee53c13a7107edfde6", size = 1936244 }, + { url = "https://files.pythonhosted.org/packages/26/f7/6422b9e4d148f1a351c0358a95d59023f25cab76609b180804f6a3ed17e9/cramjam-2.9.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb01f6e38719818778144d3165a89ea1ad9dc58c6342b7f20aa194c70f34cbd1", size = 2119487 }, + { url = "https://files.pythonhosted.org/packages/b5/59/6fc930217f7ae085eca6d22d3477cd0145a105cdc39e63b834cb0c1b25e3/cramjam-2.9.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b5cef5cf40725fe64592af9ec163e7389855077700678a1d94bec549403a74d", size = 2400910 }, + { url = "https://files.pythonhosted.org/packages/2d/36/7e53cf5aaed4b446490e298f7571e69ce15d0dfb148feabe8bf02e58827f/cramjam-2.9.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ac48b978aa0675f62b642750e798c394a64d25ce852e4e541f69bef9a564c2f0", size = 2100860 }, +] + +[[package]] +name = "datasets" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/48/744286c044e2b942d4fa67f92816126522ad1f0675def0ea3264e6242005/datasets-3.2.0.tar.gz", hash = "sha256:9a6e1a356052866b5dbdd9c9eedb000bf3fc43d986e3584d9b028f4976937229", size = 558366 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/84/0df6c5981f5fc722381662ff8cfbdf8aad64bec875f75d80b55bfef394ce/datasets-3.2.0-py3-none-any.whl", hash = "sha256:f3d2ba2698b7284a4518019658596a6a8bc79f31e51516524249d6c59cf0fe2a", size = 480647 }, +] + +[[package]] +name = "diff-cover" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9.17'", +] +dependencies = [ + { name = "chardet", marker = "python_full_version < '3.9.17'" }, + { name = "jinja2", marker = "python_full_version < '3.9.17'" }, + { name = "pluggy", marker = "python_full_version < '3.9.17'" }, + { name = "pygments", marker = "python_full_version < '3.9.17'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3a/e49ccba052a4dda264fbad4f467739ecc63498f7223bfc03d4bfac23ea95/diff_cover-9.2.0.tar.gz", hash = "sha256:85a0b353ebbb678f9e87ea303f75b545bd0baca38f563219bb72f2ae862bba36", size = 94857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/8a/bddb8e4aea550066559144e72d3566e9ae2f757b8ac154e769c563f48177/diff_cover-9.2.0-py3-none-any.whl", hash = "sha256:1e24edc51c39e810c47dd9986e76c333ed95859655c091f572e590c39cabbdbe", size = 52561 }, +] + +[[package]] +name = "diff-cover" +version = "9.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version >= '3.9.17' and python_full_version < '3.10'", +] +dependencies = [ + { name = "chardet", marker = "python_full_version >= '3.9.17'" }, + { name = "jinja2", marker = "python_full_version >= '3.9.17'" }, + { name = "pluggy", marker = "python_full_version >= '3.9.17'" }, + { name = "pygments", marker = "python_full_version >= '3.9.17'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/b0/f3ccf97926f6e5cc76d5ece42f3c685d75673d1886fcec62886b8b00c51a/diff_cover-9.2.1.tar.gz", hash = "sha256:5fa5b2d71ccf5d16cd222a71c2ca069d9bf5fa3d657f6fac9b4d9c23379323bf", size = 94964 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/67/5785b653e392a17bec8a340fc5f4f9278ff3588098cc96b1b3b8f9c3f167/diff_cover-9.2.1-py3-none-any.whl", hash = "sha256:97393d4acaa7545eae86f66e287ad96ffbeef70c188cca5a7f433983363e3849", size = 52560 }, +] + +[[package]] +name = "dill" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252 }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "exllamav2" +version = "0.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastparquet" }, + { name = "ninja" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pygments" }, + { name = "regex" }, + { name = "rich" }, + { name = "safetensors" }, + { name = "sentencepiece" }, + { name = "torch" }, + { name = "websockets" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/04/300012908311c9a904d44c74a037821e7cde530c648afff4ad6bed4e22c3/exllamav2-0.2.7-py3-none-any.whl", hash = "sha256:410d1a69fed329f6f1c10de6988a3b6b33e1fa715aa4fe32c545edd1abd1254b", size = 1512625 }, +] + +[[package]] +name = "fastapi" +version = "0.115.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/72/d83b98cd106541e8f5e5bfab8ef2974ab45a62e8a6c5b5e6940f26d2ed4b/fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", size = 301336 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 }, +] + +[[package]] +name = "fastparquet" +version = "2024.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "fsspec" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/66/862da14f5fde4eff2cedc0f51a8dc34ba145088e5041b45b2d57ac54f922/fastparquet-2024.11.0.tar.gz", hash = "sha256:e3b1fc73fd3e1b70b0de254bae7feb890436cb67e99458b88cb9bd3cc44db419", size = 467192 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/56/476f5b83476a256489879b78513bee737691a80905e246a2daa30ebcc362/fastparquet-2024.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:60ccf587410f0979105e17036df61bb60e1c2b81880dc91895cdb4ee65b71e7f", size = 910272 }, + { url = "https://files.pythonhosted.org/packages/3b/ad/4ce73440df874479f7205fe5445090f71ed4e9bd77fdb3b740253ce82703/fastparquet-2024.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5ad5fc14b0567e700bea3cd528a0bd45a6f9371370b49de8889fb3d10a6574a", size = 684095 }, + { url = "https://files.pythonhosted.org/packages/20/37/c3164261d6183d529a59afef2749821b262c8581d837faa91043837c6f76/fastparquet-2024.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b74333914f454344458dab9d1432fda9b70d62e28dc7acb1512d937ef1424ee", size = 1700355 }, + { url = "https://files.pythonhosted.org/packages/e6/95/cf4b175c22160ec21e4664830763bfaa80b2cf05133ef854c3f436d01c16/fastparquet-2024.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41d1610130b5cb1ce36467766191c5418cba8631e2bfe3affffaf13f9be4e7a8", size = 1714663 }, + { url = "https://files.pythonhosted.org/packages/2c/31/b6c8cdb6d5df964a192e4e8c8ecd979718afb9ca7e2dc9243a4368b370e9/fastparquet-2024.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d281edd625c33628ba028d3221180283d6161bc5ceb55eae1f0ca1678f864f26", size = 1666729 }, + { url = "https://files.pythonhosted.org/packages/31/e5/8a0575c46a7973849f8f2a88af16618b9c7efe98f249f03e3e3de69c2b86/fastparquet-2024.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fa56b19a29008c34cfe8831e810f770080debcbffc69aabd1df4d47572181f9c", size = 1741669 }, + { url = "https://files.pythonhosted.org/packages/bb/6a/669f8c9cf2fc6e30c9353832f870e5a2e170b458d12c5080837f742d963d/fastparquet-2024.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5914ecfa766b7763201b9f49d832a5e89c2dccad470ca4f9c9b228d9a8349756", size = 1782359 }, + { url = "https://files.pythonhosted.org/packages/70/c0/1374cb43924739f4542e39d972481c1f4c7dd96808a1947450808e4e7df7/fastparquet-2024.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:561202e8f0e859ccc1aa77c4aaad1d7901b2d50fd6f624ca018bae4c3c7a62ce", size = 670700 }, + { url = "https://files.pythonhosted.org/packages/7c/51/e0d6e702523ac923ede6c05e240f4a02533ccf2cea9fec7a43491078e920/fastparquet-2024.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:374cdfa745aa7d5188430528d5841cf823eb9ad16df72ad6dadd898ccccce3be", size = 909934 }, + { url = "https://files.pythonhosted.org/packages/0a/c8/5c0fb644c19a8d80b2ae4d8aa7d90c2d85d0bd4a948c5c700bea5c2802ea/fastparquet-2024.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c8401bfd86cccaf0ab7c0ade58c91ae19317ff6092e1d4ad96c2178197d8124", size = 683844 }, + { url = "https://files.pythonhosted.org/packages/33/4a/1e532fd1a0d4d8af7ffc7e3a8106c0bcd13ed914a93a61e299b3832dd3d2/fastparquet-2024.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9cca4c6b5969df5561c13786f9d116300db1ec22c7941e237cfca4ce602f59b", size = 1791698 }, + { url = "https://files.pythonhosted.org/packages/8d/e8/e1ede861bea68394a755d8be1aa2e2d60a3b9f6b551bfd56aeca74987e2e/fastparquet-2024.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a9387e77ac608d8978774caaf1e19de67eaa1386806e514dcb19f741b19cfe5", size = 1804289 }, + { url = "https://files.pythonhosted.org/packages/4f/1e/957090cccaede805583ca3f3e46e2762d0f9bf8860ecbce65197e47d84c1/fastparquet-2024.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6595d3771b3d587a31137e985f751b4d599d5c8e9af9c4858e373fdf5c3f8720", size = 1753638 }, + { url = "https://files.pythonhosted.org/packages/85/72/344787c685fd1531f07ae712a855a7c34d13deaa26c3fd4a9231bea7dbab/fastparquet-2024.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:053695c2f730b78a2d3925df7cd5c6444d6c1560076af907993361cc7accf3e2", size = 1814407 }, + { url = "https://files.pythonhosted.org/packages/6c/ec/ab9d5685f776a1965797eb68c4364c72edf57cd35beed2df49b34425d1df/fastparquet-2024.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a52eecc6270ae15f0d51347c3f762703dd667ca486f127dc0a21e7e59856ae5", size = 1874462 }, + { url = "https://files.pythonhosted.org/packages/90/4f/7a4ea9a7ddf0a3409873f0787f355806f9e0b73f42f2acecacdd9a8eff0a/fastparquet-2024.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:e29ff7a367fafa57c6896fb6abc84126e2466811aefd3e4ad4070b9e18820e54", size = 671023 }, + { url = "https://files.pythonhosted.org/packages/08/76/068ac7ec9b4fc783be21a75a6a90b8c0654da4d46934d969e524ce287787/fastparquet-2024.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dbad4b014782bd38b58b8e9f514fe958cfa7a6c4e187859232d29fd5c5ddd849", size = 915968 }, + { url = "https://files.pythonhosted.org/packages/c7/9e/6d3b4188ad64ed51173263c07109a5f18f9c84a44fa39ab524fca7420cda/fastparquet-2024.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:403d31109d398b6be7ce84fa3483fc277c6a23f0b321348c0a505eb098a041cb", size = 685399 }, + { url = "https://files.pythonhosted.org/packages/8f/6c/809220bc9fbe83d107df2d664c3fb62fb81867be8f5218ac66c2e6b6a358/fastparquet-2024.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbbb9057a26acf0abad7adf58781ee357258b7708ee44a289e3bee97e2f55d42", size = 1758557 }, + { url = "https://files.pythonhosted.org/packages/e0/2c/b3b3e6ca2e531484289024138cd4709c22512b3fe68066d7f9849da4a76c/fastparquet-2024.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63e0e416e25c15daa174aad8ba991c2e9e5b0dc347e5aed5562124261400f87b", size = 1781052 }, + { url = "https://files.pythonhosted.org/packages/21/fe/97ed45092d0311c013996dae633122b7a51c5d9fe8dcbc2c840dc491201e/fastparquet-2024.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2d7f02f57231e6c86d26e9ea71953737202f20e948790e5d4db6d6a1a150dc", size = 1715797 }, + { url = "https://files.pythonhosted.org/packages/24/df/02fa6aee6c0d53d1563b5bc22097076c609c4c5baa47056b0b4bed456fcf/fastparquet-2024.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fbe4468146b633d8f09d7b196fea0547f213cb5ce5f76e9d1beb29eaa9593a93", size = 1795682 }, + { url = "https://files.pythonhosted.org/packages/b0/25/f4f87557589e1923ee0e3bebbc84f08b7c56962bf90f51b116ddc54f2c9f/fastparquet-2024.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29d5c718817bcd765fc519b17f759cad4945974421ecc1931d3bdc3e05e57fa9", size = 1857842 }, + { url = "https://files.pythonhosted.org/packages/b1/f9/98cd0c39115879be1044d59c9b76e8292776e99bb93565bf990078fd11c4/fastparquet-2024.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:74a0b3c40ab373442c0fda96b75a36e88745d8b138fcc3a6143e04682cbbb8ca", size = 673269 }, + { url = "https://files.pythonhosted.org/packages/47/e3/e7db38704be5db787270d43dde895eaa1a825ab25dc245e71df70860ec12/fastparquet-2024.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:59e5c5b51083d5b82572cdb7aed0346e3181e3ac9d2e45759da2e804bdafa7ee", size = 912523 }, + { url = "https://files.pythonhosted.org/packages/d3/66/e3387c99293dae441634e7724acaa425b27de19a00ee3d546775dace54a9/fastparquet-2024.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdadf7b6bad789125b823bfc5b0a719ba5c4a2ef965f973702d3ea89cff057f6", size = 683779 }, + { url = "https://files.pythonhosted.org/packages/0a/21/d112d0573d086b578bf04302a502e9a7605ea8f1244a7b8577cd945eec78/fastparquet-2024.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46b2db02fc2a1507939d35441c8ab211d53afd75d82eec9767d1c3656402859b", size = 1751113 }, + { url = "https://files.pythonhosted.org/packages/6b/a7/040507cee3a7798954e8fdbca21d2dbc532774b02b882d902b8a4a6849ef/fastparquet-2024.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3afdef2895c9f459135a00a7ed3ceafebfbce918a9e7b5d550e4fae39c1b64d", size = 1780496 }, + { url = "https://files.pythonhosted.org/packages/bc/75/d0d9f7533d780ec167eede16ad88073ee71696150511126c31940e7f73aa/fastparquet-2024.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36b5c9bd2ffaaa26ff45d59a6cefe58503dd748e0c7fad80dd905749da0f2b9e", size = 1713608 }, + { url = "https://files.pythonhosted.org/packages/30/fa/1d95bc86e45e80669c4f374b2ca26a9e5895a1011bb05d6341b4a7414693/fastparquet-2024.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b7df5d3b61a19d76e209fe8d3133759af1c139e04ebc6d43f3cc2d8045ef338", size = 1792779 }, + { url = "https://files.pythonhosted.org/packages/13/3d/c076beeb926c79593374c04662a9422a76650eef17cd1c8e10951340764a/fastparquet-2024.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b35823ac7a194134e5f82fa4a9659e42e8f9ad1f2d22a55fbb7b9e4053aabbb", size = 1851322 }, + { url = "https://files.pythonhosted.org/packages/09/5a/1d0d47e64816002824d4a876644e8c65540fa23f91b701f0daa726931545/fastparquet-2024.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d20632964e65530374ff7cddd42cc06aa0a1388934903693d6d22592a5ba827b", size = 673266 }, + { url = "https://files.pythonhosted.org/packages/a2/3a/607301b22fd50065503a8be7ea0068ae61d9b7f63be44a4bad68a286f712/fastparquet-2024.11.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ae953c0e3832ae3936b6d92fde493ac7d8b775d7d59d02f7f46f67e1c21ed24", size = 911743 }, + { url = "https://files.pythonhosted.org/packages/e7/a7/a9ff37efc9cf5164e372a813630488c0b3600e35c6bc1a7add9b9d17296f/fastparquet-2024.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:869e167a4067116b4a27eb7adbe597130b2e2e9cfc0f3e84f60e2e182a933f23", size = 684662 }, + { url = "https://files.pythonhosted.org/packages/2c/76/dd9456425c312e8629e969c2099c4bc538fbf433542752395acb22c65559/fastparquet-2024.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb3356862fba2f9b2ea8e679d66901f466c92be8e023439fe854bc392fbf40a6", size = 1701403 }, + { url = "https://files.pythonhosted.org/packages/b3/9d/17a296e458b62e7b9af301c2e7e77a21af37bcf29030a20f56d5d6757ae3/fastparquet-2024.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc475993232c6a64f350aeb928013a807eb93f78675810fd019cbcff39f6baf3", size = 1715213 }, + { url = "https://files.pythonhosted.org/packages/d2/05/2b5722eb6e24249706886423fbab2c42a5d60b0fc0f1a27e955ff2b829e8/fastparquet-2024.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d24c923a2d9d22a5e7564245f856e6462d524d57982ac8f7479cde991ff73362", size = 1668310 }, + { url = "https://files.pythonhosted.org/packages/4c/9a/8a53dfa0c564f0683b693aa84878074d82ce2516e0f599d2a19ff3a0b073/fastparquet-2024.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6b936dcf40ca5fff9e70383d48811b1482b871ff74af857cb4db5f4d072f01ab", size = 1743186 }, + { url = "https://files.pythonhosted.org/packages/cd/f6/61986beb1a894b40e7b9e9f5d0228ca068e005e66576af0b4fa94481ffbc/fastparquet-2024.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4abd3426607335e5ad09be29ef4eeccdf097710e44420deac16893cee64ea0d8", size = 1784121 }, + { url = "https://files.pythonhosted.org/packages/91/71/2cf345d0ccf1239108190bcd838a19b289fa1bac5676e8ae701ae24b602f/fastparquet-2024.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:6ec7b398a86432993441d0a08dfae59e29649c803ed64ec4b1d7c3e0855b14cb", size = 671248 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, + { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, + { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, + { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, + { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, + { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, + { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, + { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, + { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, + { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, + { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, + { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, + { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, + { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, + { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/da/4d/d94ff0fb0f5313902c132817c62d19cdc5bdcd0c195d392006ef4b779fc6/frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", size = 95319 }, + { url = "https://files.pythonhosted.org/packages/8c/1b/d90e554ca2b483d31cb2296e393f72c25bdc38d64526579e95576bfda587/frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", size = 54749 }, + { url = "https://files.pythonhosted.org/packages/f8/66/7fdecc9ef49f8db2aa4d9da916e4ecf357d867d87aea292efc11e1b2e932/frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", size = 52718 }, + { url = "https://files.pythonhosted.org/packages/08/04/e2fddc92135276e07addbc1cf413acffa0c2d848b3e54cacf684e146df49/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", size = 241756 }, + { url = "https://files.pythonhosted.org/packages/c6/52/be5ff200815d8a341aee5b16b6b707355e0ca3652953852238eb92b120c2/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", size = 267718 }, + { url = "https://files.pythonhosted.org/packages/88/be/4bd93a58be57a3722fc544c36debdf9dcc6758f761092e894d78f18b8f20/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", size = 263494 }, + { url = "https://files.pythonhosted.org/packages/32/ba/58348b90193caa096ce9e9befea6ae67f38dabfd3aacb47e46137a6250a8/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", size = 232838 }, + { url = "https://files.pythonhosted.org/packages/f6/33/9f152105227630246135188901373c4f322cc026565ca6215b063f4c82f4/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", size = 242912 }, + { url = "https://files.pythonhosted.org/packages/a0/10/3db38fb3ccbafadd80a1b0d6800c987b0e3fe3ef2d117c6ced0246eea17a/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", size = 244763 }, + { url = "https://files.pythonhosted.org/packages/e2/cd/1df468fdce2f66a4608dffe44c40cdc35eeaa67ef7fd1d813f99a9a37842/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", size = 242841 }, + { url = "https://files.pythonhosted.org/packages/ee/5f/16097a5ca0bb6b6779c02cc9379c72fe98d56115d4c54d059fb233168fb6/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", size = 263407 }, + { url = "https://files.pythonhosted.org/packages/0f/f7/58cd220ee1c2248ee65a32f5b4b93689e3fe1764d85537eee9fc392543bc/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", size = 265083 }, + { url = "https://files.pythonhosted.org/packages/62/b8/49768980caabf81ac4a2d156008f7cbd0107e6b36d08a313bb31035d9201/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", size = 251564 }, + { url = "https://files.pythonhosted.org/packages/cb/83/619327da3b86ef957ee7a0cbf3c166a09ed1e87a3f7f1ff487d7d0284683/frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", size = 45691 }, + { url = "https://files.pythonhosted.org/packages/8b/28/407bc34a745151ed2322c690b6e7d83d7101472e81ed76e1ebdac0b70a78/frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", size = 51767 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "fsspec" +version = "2024.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/7c/12b0943011daaaa9c35c2a2e22e5eb929ac90002f08f1259d69aedad84de/fsspec-2024.9.0.tar.gz", hash = "sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8", size = 286206 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/a0/6aaea0c2fbea2f89bfd5db25fb1e3481896a423002ebe4e55288907a97a3/fsspec-2024.9.0-py3-none-any.whl", hash = "sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b", size = 179253 }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "genson" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 }, + { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 }, + { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 }, + { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 }, + { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 }, + { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 }, + { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 }, + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, + { url = "https://files.pythonhosted.org/packages/51/b1/4fc6f52afdf93b7c4304e21f6add9e981e4f857c2fa622a55dfe21b6059e/httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003", size = 201123 }, + { url = "https://files.pythonhosted.org/packages/c2/01/e6ecb40ac8fdfb76607c7d3b74a41b464458d5c8710534d8f163b0c15f29/httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab", size = 104507 }, + { url = "https://files.pythonhosted.org/packages/dc/24/c70c34119d209bf08199d938dc9c69164f585ed3029237b4bdb90f673cb9/httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547", size = 449615 }, + { url = "https://files.pythonhosted.org/packages/2b/62/e7f317fed3703bd81053840cacba4e40bcf424b870e4197f94bd1cf9fe7a/httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9", size = 448819 }, + { url = "https://files.pythonhosted.org/packages/2a/13/68337d3be6b023260139434c49d7aa466aaa98f9aee7ed29270ac7dde6a2/httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076", size = 422093 }, + { url = "https://files.pythonhosted.org/packages/fc/b3/3a1bc45be03dda7a60c7858e55b6cd0489a81613c1908fb81cf21d34ae50/httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd", size = 423898 }, + { url = "https://files.pythonhosted.org/packages/05/72/2ddc2ae5f7ace986f7e68a326215b2e7c32e32fd40e6428fa8f1d8065c7e/httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6", size = 89552 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/d2/d6976de7542792fc077b498d64af64882b6d8bb40679284ec0bff77d5929/huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b", size = 379407 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/3f/50f6b25fafdcfb1c089187a328c95081abf882309afd86f4053951507cd1/huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec", size = 450658 }, +] + +[[package]] +name = "identify" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "intel-openmp" +version = "2021.4.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/18/527f247d673ff84c38e0b353b6901539b99e83066cd505be42ad341ab16d/intel_openmp-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:6e863d8fd3d7e8ef389d52cf97a50fe2afe1a19247e8c0d168ce021546f96fc9", size = 1860605 }, + { url = "https://files.pythonhosted.org/packages/6f/21/b590c0cc3888b24f2ac9898c41d852d7454a1695fbad34bee85dba6dc408/intel_openmp-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:eef4c8bcc8acefd7f5cd3b9384dbf73d59e2c99fc56545712ded913f43c4a94f", size = 3516906 }, +] + +[[package]] +name = "interegular" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9d/8b6dde58a028a3962ce17e84d5fe73758df61378e00ef8ac3d85da34b0ff/interegular-0.3.3.tar.gz", hash = "sha256:d9b697b21b34884711399ba0f0376914b81899ce670032486d0d048344a76600", size = 24705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/01/72d6472f80651673716d1deda2a5bbb633e563ecf94f4479da5519d69d25/interegular-0.3.3-py37-none-any.whl", hash = "sha256:b0c07007d48c89d6d19f7204972d369b2a77222722e126b6aa63aa721dc3b19c", size = 23635 }, +] + +[[package]] +name = "jax" +version = "0.4.30" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9.17' and python_full_version < '3.10'", + "python_full_version < '3.9.17'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jaxlib", version = "0.4.30", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "opt-einsum", marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/41/d6dbafc31d6bd93eeec2e1c709adfa454266e83714ebeeed9de52a6ad881/jax-0.4.30.tar.gz", hash = "sha256:94d74b5b2db0d80672b61d83f1f63ebf99d2ab7398ec12b2ca0c9d1e97afe577", size = 1715462 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/f2/9dbb75de3058acfd1600cf0839bcce7ea391148c9d2b4fa5f5666e66f09e/jax-0.4.30-py3-none-any.whl", hash = "sha256:289b30ae03b52f7f4baf6ef082a9f4e3e29c1080e22d13512c5ecf02d5f1a55b", size = 2009197 }, +] + +[[package]] +name = "jax" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "jaxlib", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' or (python_full_version >= '3.10' and python_full_version < '3.12')" }, + { name = "ml-dtypes", version = "0.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "opt-einsum", marker = "python_full_version >= '3.10'" }, + { name = "scipy", version = "1.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/cb/22d62b26284f08e62d6eb64603d3b010004cfdb7a97ce6cca5c6cf86edab/jax-0.5.0.tar.gz", hash = "sha256:49df70bf293a345a7fb519f71193506d37a024c4f850b358042eb32d502c81c8", size = 1959707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/58/cc0721a1030fcbab0984beea0bf3c4610ec103f738423cdfa9c4ceb40598/jax-0.5.0-py3-none-any.whl", hash = "sha256:b3907aa87ae2c340b39cdbf80c07a74550369cafcaf7398fb60ba58d167345ab", size = 2270365 }, +] + +[[package]] +name = "jaxlib" +version = "0.4.30" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9.17' and python_full_version < '3.10'", + "python_full_version < '3.9.17'", +] +dependencies = [ + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/18/ff7f2f6d6195853ed55c5b5d835f5c8c3c8b190c7221cb04a0cb81f5db10/jaxlib-0.4.30-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:c40856e28f300938c6824ab1a615166193d6997dec946578823f6d402ad454e5", size = 83542097 }, + { url = "https://files.pythonhosted.org/packages/d4/c0/ff65503ecfed3aee11e4abe4c4e9e8a3513f072e0b595f8247b9989d1510/jaxlib-0.4.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bdfda6a3c7a2b0cc0a7131009eb279e98ca4a6f25679fabb5302dd135a5e349", size = 66694495 }, + { url = "https://files.pythonhosted.org/packages/b9/d7/82df748a31a1cfbd531a12979ea846d6b676d4adfa1e91114b848665b2aa/jaxlib-0.4.30-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:28e032c9b394ab7624d89b0d9d3bbcf4d1d71694fe8b3e09d3fe64122eda7b0c", size = 67781242 }, + { url = "https://files.pythonhosted.org/packages/4a/ca/561aabed63007bb2621a62f0d816aa2f68cfe947859c8b4e61519940344b/jaxlib-0.4.30-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:d83f36ef42a403bbf7c7f2da526b34ba286988e170f4df5e58b3bb735417868c", size = 79640266 }, + { url = "https://files.pythonhosted.org/packages/b0/90/8e5347eda95d3cb695cd5ebb82f850fa7866078a6a7a0568549e34125a82/jaxlib-0.4.30-cp310-cp310-win_amd64.whl", hash = "sha256:a56678b28f96b524ded6da8ef4b38e72a532356d139cfd434da804abf4234e14", size = 51945307 }, + { url = "https://files.pythonhosted.org/packages/33/2d/b6078f5d173d3087d32b1b49e5f65d406985fb3894ff1d21905972b9c89d/jaxlib-0.4.30-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:bfb5d85b69c29c3c6e8051a0ea715ac1e532d6e54494c8d9c3813dcc00deac30", size = 83539315 }, + { url = "https://files.pythonhosted.org/packages/12/95/399da9204c3b13696baefb93468402f3389416b0caecfd9126aa94742bf2/jaxlib-0.4.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:974998cd8a78550402e6c09935c1f8d850cad9cc19ccd7488bde45b6f7f99c12", size = 66690971 }, + { url = "https://files.pythonhosted.org/packages/a4/f8/b85a46cb0cc4bc228cea4366b0d15caf42656c6d43cf8c91d90f7399aa4d/jaxlib-0.4.30-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:e93eb0646b41ba213252b51b0b69096b9cd1d81a35ea85c9d06663b5d11efe45", size = 67780747 }, + { url = "https://files.pythonhosted.org/packages/a6/a3/951da3d1487b2f8995a2a14cc7e9496c9a7c93aa1f1d0b33e833e24dee92/jaxlib-0.4.30-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:16b2ab18ea90d2e15941bcf45de37afc2f289a029129c88c8d7aba0404dd0043", size = 79640352 }, + { url = "https://files.pythonhosted.org/packages/bb/1a/8f45ea28a5ca67e4d23ebd70fc78ea94be6fa20323f983c7607c32c6f9a5/jaxlib-0.4.30-cp311-cp311-win_amd64.whl", hash = "sha256:3a2e2c11c179f8851a72249ba1ae40ae817dfaee9877d23b3b8f7c6b7a012f76", size = 51943960 }, + { url = "https://files.pythonhosted.org/packages/19/40/ae943d3c1fc8b50947aebbaa3bad2842759e43bc9fc91e1758c1c20a81ab/jaxlib-0.4.30-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7704db5962b32a2be3cc07185433cbbcc94ed90ee50c84021a3f8a1ecfd66ee3", size = 83587124 }, + { url = "https://files.pythonhosted.org/packages/c6/e3/97f8edff6f64245a500415be021869522b235e8b38cd930d358b91243583/jaxlib-0.4.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57090d33477fd0f0c99dc686274882ea75c44c7d712ae42dd2460b10f896131d", size = 66724768 }, + { url = "https://files.pythonhosted.org/packages/4c/c7/ee1f48f8daa409d0ed039e0d8b5ae1a447e53db3acb2ff06239828ad96d5/jaxlib-0.4.30-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:0a3850e76278038e21685975a62b622bcf3708485f13125757a0561ee4512940", size = 67800348 }, + { url = "https://files.pythonhosted.org/packages/f2/fa/a2dddea0d6965b8e433bb99aeedbe5c8a9b47110c1c4f197a7b6239daf44/jaxlib-0.4.30-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:c58a8071c4e00898282118169f6a5a97eb15a79c2897858f3a732b17891c99ab", size = 79674030 }, + { url = "https://files.pythonhosted.org/packages/db/31/3500633d61b20b882a0fbcf8100013195c31b51f71249b0b38737851fc9a/jaxlib-0.4.30-cp312-cp312-win_amd64.whl", hash = "sha256:b7079a5b1ab6864a7d4f2afaa963841451186d22c90f39719a3ff85735ce3915", size = 51965689 }, + { url = "https://files.pythonhosted.org/packages/46/12/9de601dbae3c66666eeaaf5a28683d947909c046880baef390b7cd1d4b1d/jaxlib-0.4.30-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ea3a00005faafbe3c18b178d3b534208b3b4027b2be6230227e7b87ce399fc29", size = 83544602 }, + { url = "https://files.pythonhosted.org/packages/f3/1d/2d417a1445d5e696bb44d564c7519d4a6761db4d3e31712620c510ed0127/jaxlib-0.4.30-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d31e01191ce8052bd611aaf16ff967d8d0ec0b63f1ea4b199020cecb248d667", size = 66695975 }, + { url = "https://files.pythonhosted.org/packages/e4/f9/e29370046f4648bd464df7eceaebbbaefd091cc88c77da4a6e3a5f1a00d7/jaxlib-0.4.30-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:11602d5556e8baa2f16314c36518e9be4dfae0c2c256a361403fb29dc9dc79a4", size = 67784388 }, + { url = "https://files.pythonhosted.org/packages/07/3b/a596036325666624ca084df554636fb3777e78e9386b52476d96fa14394e/jaxlib-0.4.30-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f74a6b0e09df4b5e2ee399ebb9f0e01190e26e84ccb0a758fadb516415c07f18", size = 79643370 }, + { url = "https://files.pythonhosted.org/packages/8a/a3/7342ceb02e49803af9a42ab3ad9b6c272cf7b2a83163e3a06859360012d5/jaxlib-0.4.30-cp39-cp39-win_amd64.whl", hash = "sha256:54987e97a22db70f3829b437b9329e4799d653634bacc8b398554d3b90c76b2a", size = 51946140 }, +] + +[[package]] +name = "jaxlib" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' or (python_full_version >= '3.10' and python_full_version < '3.12')" }, + { name = "ml-dtypes", version = "0.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "scipy", version = "1.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/41/3e4ac64df72c4da126df3fd66a2214025a46b6263f7be266728e7b8e473e/jaxlib-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b8a6c4345f137f387650de2dbc488c20251b7412b55dd648e1a4f13bcf507fb", size = 79248968 }, + { url = "https://files.pythonhosted.org/packages/1e/5f/2a16e61f1d54ae5f55fbf3cb3e22ef5bb01bf9d7d6474e0d34fedba19c4d/jaxlib-0.5.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5b2efe3dfebf18a84c451d3803ac884ee242021c1113b279c13f4bbc378c3dc0", size = 93181077 }, + { url = "https://files.pythonhosted.org/packages/08/c3/573e2f01b99f1247e8fbe1aa46b95a0faa68ef208f9a8e8ef775d607b3e6/jaxlib-0.5.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:74440b632107336400d4f97a16481d767f13ea914c53ba14e544c6fda54819b3", size = 101969119 }, + { url = "https://files.pythonhosted.org/packages/6e/38/512f61ea13da41ca47f2411d7c05af0cf74a37f225e16725ed0e6fb58893/jaxlib-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:53478a28eee6c2ef01759b05a9491702daef9268c3ed013d6f8e2e5f5cae0887", size = 63883394 }, + { url = "https://files.pythonhosted.org/packages/92/4b/8875870ff52ad3fbea876c905228f691f05c8dc8556b226cbfaf0fba7f62/jaxlib-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6cd762ed1623132499fa701c4203446102e0a9c82ca23194b87288f746d12a29", size = 79242870 }, + { url = "https://files.pythonhosted.org/packages/a0/0f/00cdfa411d7218e4696c10c5867f7d3c396219adbcaeb02e95108ca802de/jaxlib-0.5.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:63088dbfaa85bb56cd521a925a3472fd7328b18ec93c2d8ffa85af331095c995", size = 93181807 }, + { url = "https://files.pythonhosted.org/packages/58/8e/a5c29db03d5a93b0326e297b556d0e0a9805e9c9c1ae5f82f69557273faa/jaxlib-0.5.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:09113ef1582ba34d7cbc440fedb318f4855b59b776711a8aba2473c9727d3025", size = 101969212 }, + { url = "https://files.pythonhosted.org/packages/70/86/ceae20e4f37fa07f1cc95551cc0f49170d0db46d2e82fdf511d26bffd801/jaxlib-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:78289fc3ddc1e4e9510de2536a6375df9fe1c50de0ac60826c286b7a5c5090fe", size = 63881994 }, + { url = "https://files.pythonhosted.org/packages/57/d6/d971b40cb156e0637aa3c1522a1e803b641142e9a8f3ade6a574711bb073/jaxlib-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73e335715760c56e635109d61426435a5d7f46f3363a115daea09427d5cd0efd", size = 79246087 }, + { url = "https://files.pythonhosted.org/packages/41/2e/ba9770330077c3e4082cd0353e6a61419f79bff3e2197f904ce70167b9ad/jaxlib-0.5.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:4b4b01afb0ddec96c08356bff2bb685ddbe97fdffe4ed6e2d834b30aba972f22", size = 93179593 }, + { url = "https://files.pythonhosted.org/packages/66/e9/211ba3e46ec22c722c4d61a739cfccf79b0618006d6f5fa53eb4eb93ed6d/jaxlib-0.5.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f980c733e98c998a8da87c9a8cc61b6726d0be667a58bd664c1d717b4b4eae75", size = 101984785 }, + { url = "https://files.pythonhosted.org/packages/2d/cb/11bb92324afb6ba678f388e10b78d6b02196bc8887eb5aa0d85ce398edf9/jaxlib-0.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:5baedbeeb60fa493c7528783254f04c6e986a2826266b198ed37e9336af2ef8c", size = 63899871 }, + { url = "https://files.pythonhosted.org/packages/22/ac/e400473e6a2f405fd6e4dc40a713bb9a3868a3f76a8ffc5eb66f6e686002/jaxlib-0.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed18ea7161d03aa8fd4d1b55494882f21420efdfea68e5f298c4aebcf2ac3f34", size = 79245359 }, + { url = "https://files.pythonhosted.org/packages/44/2d/c210abf4a9b2ce2e0858fcd3567c8773a739114e37d751af6c228901af57/jaxlib-0.5.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:7d9b17a7ea19355d45ecdb2ff0db5d707a86f0c5a862d94b89b4568d6c45311a", size = 93180025 }, + { url = "https://files.pythonhosted.org/packages/30/f8/316f7b4797c5eb50c6d70e461724a7cbe08b4505ca4da1bfd260c135895a/jaxlib-0.5.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:11eef01d37c0f1c5306265b76f207f1002d13480ded2e31fd63ec76912c93ca2", size = 101982281 }, + { url = "https://files.pythonhosted.org/packages/4c/f2/cfa012a0417c9b13b44c8e1d3ebf5fd04e8bb738b5c93e20c9fc97919880/jaxlib-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:61b4d26cd6a0c49ba0b1e4340c7d29198913ee2dc70b65ee90752717d22305bb", size = 63900219 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "jiter" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/70/90bc7bd3932e651486861df5c8ffea4ca7c77d28e8532ddefe2abc561a53/jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d", size = 163007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/f3/8c11e0e87bd5934c414f9b1cfae3cbfd4a938d4669d57cb427e1c4d11a7f/jiter-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b", size = 303381 }, + { url = "https://files.pythonhosted.org/packages/ea/28/4cd3f0bcbf40e946bc6a62a82c951afc386a25673d3d8d5ee461f1559bbe/jiter-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393", size = 311718 }, + { url = "https://files.pythonhosted.org/packages/0d/17/57acab00507e60bd954eaec0837d9d7b119b4117ff49b8a62f2b646f32ed/jiter-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c826a221851a8dc028eb6d7d6429ba03184fa3c7e83ae01cd6d3bd1d4bd17d", size = 335465 }, + { url = "https://files.pythonhosted.org/packages/74/b9/1a3ddd2bc95ae17c815b021521020f40c60b32137730126bada962ef32b4/jiter-0.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d35c864c2dff13dfd79fb070fc4fc6235d7b9b359efe340e1261deb21b9fcb66", size = 355570 }, + { url = "https://files.pythonhosted.org/packages/78/69/6d29e2296a934199a7d0dde673ecccf98c9c8db44caf0248b3f2b65483cb/jiter-0.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f557c55bc2b7676e74d39d19bcb8775ca295c7a028246175d6a8b431e70835e5", size = 381383 }, + { url = "https://files.pythonhosted.org/packages/22/d7/fbc4c3fb1bf65f9be22a32759b539f88e897aeb13fe84ab0266e4423487a/jiter-0.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:580ccf358539153db147e40751a0b41688a5ceb275e6f3e93d91c9467f42b2e3", size = 390454 }, + { url = "https://files.pythonhosted.org/packages/4d/a0/3993cda2e267fe679b45d0bcc2cef0b4504b0aa810659cdae9737d6bace9/jiter-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af102d3372e917cffce49b521e4c32c497515119dc7bd8a75665e90a718bbf08", size = 345039 }, + { url = "https://files.pythonhosted.org/packages/b9/ef/69c18562b4c09ce88fab5df1dcaf643f6b1a8b970b65216e7221169b81c4/jiter-0.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cadcc978f82397d515bb2683fc0d50103acff2a180552654bb92d6045dec2c49", size = 376200 }, + { url = "https://files.pythonhosted.org/packages/4d/17/0b5a8de46a6ab4d836f70934036278b49b8530c292b29dde3483326d4555/jiter-0.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba5bdf56969cad2019d4e8ffd3f879b5fdc792624129741d3d83fc832fef8c7d", size = 511158 }, + { url = "https://files.pythonhosted.org/packages/6c/b2/c401a0a2554b36c9e6d6e4876b43790d75139cf3936f0222e675cbc23451/jiter-0.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b94a33a241bee9e34b8481cdcaa3d5c2116f575e0226e421bed3f7a6ea71cff", size = 503956 }, + { url = "https://files.pythonhosted.org/packages/d4/02/a0291ed7d72c0ac130f172354ee3cf0b2556b69584de391463a8ee534f40/jiter-0.8.2-cp310-cp310-win32.whl", hash = "sha256:6e5337bf454abddd91bd048ce0dca5134056fc99ca0205258766db35d0a2ea43", size = 202846 }, + { url = "https://files.pythonhosted.org/packages/ad/20/8c988831ae4bf437e29f1671e198fc99ba8fe49f2895f23789acad1d1811/jiter-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:4a9220497ca0cb1fe94e3f334f65b9b5102a0b8147646118f020d8ce1de70105", size = 204414 }, + { url = "https://files.pythonhosted.org/packages/cb/b0/c1a7caa7f9dc5f1f6cfa08722867790fe2d3645d6e7170ca280e6e52d163/jiter-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b", size = 303666 }, + { url = "https://files.pythonhosted.org/packages/f5/97/0468bc9eeae43079aaa5feb9267964e496bf13133d469cfdc135498f8dd0/jiter-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15", size = 311934 }, + { url = "https://files.pythonhosted.org/packages/e5/69/64058e18263d9a5f1e10f90c436853616d5f047d997c37c7b2df11b085ec/jiter-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0", size = 335506 }, + { url = "https://files.pythonhosted.org/packages/9d/14/b747f9a77b8c0542141d77ca1e2a7523e854754af2c339ac89a8b66527d6/jiter-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f", size = 355849 }, + { url = "https://files.pythonhosted.org/packages/53/e2/98a08161db7cc9d0e39bc385415890928ff09709034982f48eccfca40733/jiter-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099", size = 381700 }, + { url = "https://files.pythonhosted.org/packages/7a/38/1674672954d35bce3b1c9af99d5849f9256ac8f5b672e020ac7821581206/jiter-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74", size = 389710 }, + { url = "https://files.pythonhosted.org/packages/f8/9b/92f9da9a9e107d019bcf883cd9125fa1690079f323f5a9d5c6986eeec3c0/jiter-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586", size = 345553 }, + { url = "https://files.pythonhosted.org/packages/44/a6/6d030003394e9659cd0d7136bbeabd82e869849ceccddc34d40abbbbb269/jiter-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc", size = 376388 }, + { url = "https://files.pythonhosted.org/packages/ad/8d/87b09e648e4aca5f9af89e3ab3cfb93db2d1e633b2f2931ede8dabd9b19a/jiter-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88", size = 511226 }, + { url = "https://files.pythonhosted.org/packages/77/95/8008ebe4cdc82eac1c97864a8042ca7e383ed67e0ec17bfd03797045c727/jiter-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6", size = 504134 }, + { url = "https://files.pythonhosted.org/packages/26/0d/3056a74de13e8b2562e4d526de6dac2f65d91ace63a8234deb9284a1d24d/jiter-0.8.2-cp311-cp311-win32.whl", hash = "sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44", size = 203103 }, + { url = "https://files.pythonhosted.org/packages/4e/1e/7f96b798f356e531ffc0f53dd2f37185fac60fae4d6c612bbbd4639b90aa/jiter-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855", size = 206717 }, + { url = "https://files.pythonhosted.org/packages/a1/17/c8747af8ea4e045f57d6cfd6fc180752cab9bc3de0e8a0c9ca4e8af333b1/jiter-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f", size = 302027 }, + { url = "https://files.pythonhosted.org/packages/3c/c1/6da849640cd35a41e91085723b76acc818d4b7d92b0b6e5111736ce1dd10/jiter-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44", size = 310326 }, + { url = "https://files.pythonhosted.org/packages/06/99/a2bf660d8ccffee9ad7ed46b4f860d2108a148d0ea36043fd16f4dc37e94/jiter-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f", size = 334242 }, + { url = "https://files.pythonhosted.org/packages/a7/5f/cea1c17864828731f11427b9d1ab7f24764dbd9aaf4648a7f851164d2718/jiter-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60", size = 356654 }, + { url = "https://files.pythonhosted.org/packages/e9/13/62774b7e5e7f5d5043efe1d0f94ead66e6d0f894ae010adb56b3f788de71/jiter-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57", size = 379967 }, + { url = "https://files.pythonhosted.org/packages/ec/fb/096b34c553bb0bd3f2289d5013dcad6074948b8d55212aa13a10d44c5326/jiter-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e", size = 389252 }, + { url = "https://files.pythonhosted.org/packages/17/61/beea645c0bf398ced8b199e377b61eb999d8e46e053bb285c91c3d3eaab0/jiter-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887", size = 345490 }, + { url = "https://files.pythonhosted.org/packages/d5/df/834aa17ad5dcc3cf0118821da0a0cf1589ea7db9832589278553640366bc/jiter-0.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d", size = 376991 }, + { url = "https://files.pythonhosted.org/packages/67/80/87d140399d382fb4ea5b3d56e7ecaa4efdca17cd7411ff904c1517855314/jiter-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152", size = 510822 }, + { url = "https://files.pythonhosted.org/packages/5c/37/3394bb47bac1ad2cb0465601f86828a0518d07828a650722e55268cdb7e6/jiter-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29", size = 503730 }, + { url = "https://files.pythonhosted.org/packages/f9/e2/253fc1fa59103bb4e3aa0665d6ceb1818df1cd7bf3eb492c4dad229b1cd4/jiter-0.8.2-cp312-cp312-win32.whl", hash = "sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e", size = 203375 }, + { url = "https://files.pythonhosted.org/packages/41/69/6d4bbe66b3b3b4507e47aa1dd5d075919ad242b4b1115b3f80eecd443687/jiter-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c", size = 204740 }, + { url = "https://files.pythonhosted.org/packages/6c/b0/bfa1f6f2c956b948802ef5a021281978bf53b7a6ca54bb126fd88a5d014e/jiter-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84", size = 301190 }, + { url = "https://files.pythonhosted.org/packages/a4/8f/396ddb4e292b5ea57e45ade5dc48229556b9044bad29a3b4b2dddeaedd52/jiter-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4", size = 309334 }, + { url = "https://files.pythonhosted.org/packages/7f/68/805978f2f446fa6362ba0cc2e4489b945695940656edd844e110a61c98f8/jiter-0.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587", size = 333918 }, + { url = "https://files.pythonhosted.org/packages/b3/99/0f71f7be667c33403fa9706e5b50583ae5106d96fab997fa7e2f38ee8347/jiter-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c", size = 356057 }, + { url = "https://files.pythonhosted.org/packages/8d/50/a82796e421a22b699ee4d2ce527e5bcb29471a2351cbdc931819d941a167/jiter-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18", size = 379790 }, + { url = "https://files.pythonhosted.org/packages/3c/31/10fb012b00f6d83342ca9e2c9618869ab449f1aa78c8f1b2193a6b49647c/jiter-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6", size = 388285 }, + { url = "https://files.pythonhosted.org/packages/c8/81/f15ebf7de57be488aa22944bf4274962aca8092e4f7817f92ffa50d3ee46/jiter-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef", size = 344764 }, + { url = "https://files.pythonhosted.org/packages/b3/e8/0cae550d72b48829ba653eb348cdc25f3f06f8a62363723702ec18e7be9c/jiter-0.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1", size = 376620 }, + { url = "https://files.pythonhosted.org/packages/b8/50/e5478ff9d82534a944c03b63bc217c5f37019d4a34d288db0f079b13c10b/jiter-0.8.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9", size = 510402 }, + { url = "https://files.pythonhosted.org/packages/8e/1e/3de48bbebbc8f7025bd454cedc8c62378c0e32dd483dece5f4a814a5cb55/jiter-0.8.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05", size = 503018 }, + { url = "https://files.pythonhosted.org/packages/d5/cd/d5a5501d72a11fe3e5fd65c78c884e5164eefe80077680533919be22d3a3/jiter-0.8.2-cp313-cp313-win32.whl", hash = "sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a", size = 203190 }, + { url = "https://files.pythonhosted.org/packages/51/bf/e5ca301245ba951447e3ad677a02a64a8845b185de2603dabd83e1e4b9c6/jiter-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865", size = 203551 }, + { url = "https://files.pythonhosted.org/packages/2f/3c/71a491952c37b87d127790dd7a0b1ebea0514c6b6ad30085b16bbe00aee6/jiter-0.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca", size = 308347 }, + { url = "https://files.pythonhosted.org/packages/a0/4c/c02408042e6a7605ec063daed138e07b982fdb98467deaaf1c90950cf2c6/jiter-0.8.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0", size = 342875 }, + { url = "https://files.pythonhosted.org/packages/91/61/c80ef80ed8a0a21158e289ef70dac01e351d929a1c30cb0f49be60772547/jiter-0.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566", size = 202374 }, + { url = "https://files.pythonhosted.org/packages/c9/b2/ed7fbabd21c3cf556d6ea849cee35c74f13a509e668baad8323091e2867e/jiter-0.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e41e75344acef3fc59ba4765df29f107f309ca9e8eace5baacabd9217e52a5ee", size = 304502 }, + { url = "https://files.pythonhosted.org/packages/75/6e/1386857ac9165c1e9c71031566e7884d8a4f63724ce29ad1ace5bfe1351c/jiter-0.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f22b16b35d5c1df9dfd58843ab2cd25e6bf15191f5a236bed177afade507bfc", size = 300982 }, + { url = "https://files.pythonhosted.org/packages/56/4c/b413977c20bbb359b4d6c91d04f7f36fc525af0b7778119815477fc97242/jiter-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7200b8f7619d36aa51c803fd52020a2dfbea36ffec1b5e22cab11fd34d95a6d", size = 335344 }, + { url = "https://files.pythonhosted.org/packages/b0/59/51b080519938192edd33b4e8d48adb7e9bf9e0d699ec8b91119b9269fc75/jiter-0.8.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70bf4c43652cc294040dbb62256c83c8718370c8b93dd93d934b9a7bf6c4f53c", size = 356298 }, + { url = "https://files.pythonhosted.org/packages/72/bb/828db5ea406916d7b2232be31393f782b0f71bcb0b128750c4a028157565/jiter-0.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9d471356dc16f84ed48768b8ee79f29514295c7295cb41e1133ec0b2b8d637d", size = 381703 }, + { url = "https://files.pythonhosted.org/packages/c0/88/45d33a8728733e161e9783c54d8ecca0fc4c1aa74b1cebea1d97917eddc3/jiter-0.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:859e8eb3507894093d01929e12e267f83b1d5f6221099d3ec976f0c995cb6bd9", size = 391281 }, + { url = "https://files.pythonhosted.org/packages/45/3e/142712e0f45c28ad8a678dc8732a78294ce5a36fc694141f772bb827a8f2/jiter-0.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa58399c01db555346647a907b4ef6d4f584b123943be6ed5588c3f2359c9f4", size = 345553 }, + { url = "https://files.pythonhosted.org/packages/36/42/9b463b59fd22687b6da1afcad6c9adc870464a808208651de73f1dbeda09/jiter-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f2d5ed877f089862f4c7aacf3a542627c1496f972a34d0474ce85ee7d939c27", size = 377063 }, + { url = "https://files.pythonhosted.org/packages/83/b3/44b1f5cd2e4eb15757eec341b25399da4c90515bb881ef6636b50a8c08a5/jiter-0.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:03c9df035d4f8d647f8c210ddc2ae0728387275340668fb30d2421e17d9a0841", size = 512543 }, + { url = "https://files.pythonhosted.org/packages/46/4e/c695c803aa2b668c057b2dea1cdd7a884d1a819ce610cec0be9666210bfd/jiter-0.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8bd2a824d08d8977bb2794ea2682f898ad3d8837932e3a74937e93d62ecbb637", size = 505141 }, + { url = "https://files.pythonhosted.org/packages/8e/51/e805b837db056f872db0b7a7a3610b7d764392be696dbe47afa0bea05bf2/jiter-0.8.2-cp39-cp39-win32.whl", hash = "sha256:ca29b6371ebc40e496995c94b988a101b9fbbed48a51190a4461fcb0a68b4a36", size = 203529 }, + { url = "https://files.pythonhosted.org/packages/32/b7/a3cde72c644fd1caf9da07fb38cf2c130f43484d8f91011940b7c4f42c8f/jiter-0.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1c0dfbd1be3cbefc7510102370d86e35d1d53e5a93d48519688b1bf0f761160a", size = 207527 }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036 }, +] + +[[package]] +name = "llama-cpp-python" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "diskcache" }, + { name = "jinja2" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/cd/efae0a18621ba66baf19d19507bb17f1aa2dd5e7f0d5dd4aa008c9bc7091/llama_cpp_python-0.3.6.tar.gz", hash = "sha256:86e35a8888274466958e24201b856cd71c8def0ea72e14312be13da96c15c7a4", size = 66893177 } + +[[package]] +name = "lm-format-enforcer" +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interegular" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ac/11fb977637125ed92adb1de4138b0e365918465e3e4e39a437fdb8156746/lm_format_enforcer-0.10.1.tar.gz", hash = "sha256:23e65a4199714fca348063e8c906838622619f905a673c4d6d428eee7e7d2095", size = 38781 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/fd/e21d1f0608da7696746c5d4cdf2f944d714daf63a97def416e0f3c154f98/lm_format_enforcer-0.10.1-py3-none-any.whl", hash = "sha256:5520004af248d787930327ead052aeff75e21fad595f388e5eade9f062ffddda", size = 42929 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mkl" +version = "2021.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "intel-openmp" }, + { name = "tbb" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/c6/892fe3bc91e811b78e4f85653864f2d92541d5e5c306b0cb3c2311e9ca64/mkl-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:439c640b269a5668134e3dcbcea4350459c4a8bc46469669b2d67e07e3d330e8", size = 129048357 }, + { url = "https://files.pythonhosted.org/packages/fe/1c/5f6dbf18e8b73e0a5472466f0ea8d48ce9efae39bd2ff38cebf8dce61259/mkl-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:ceef3cafce4c009dd25f65d7ad0d833a0fbadc3d8903991ec92351fe5de1e718", size = 228499609 }, +] + +[[package]] +name = "ml-dtypes" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version >= '3.9.17' and python_full_version < '3.10'", + "python_full_version < '3.9.17'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version != '3.12.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9e/76b84f77c7afee3b116dc8407903a2d5004ba3059a8f3dcdcfa6ebf33fff/ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5", size = 397975 }, + { url = "https://files.pythonhosted.org/packages/03/7b/32650e1b2a2713a5923a0af2a8503d0d4a8fc99d1e1e0a1c40e996634460/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24", size = 2182570 }, + { url = "https://files.pythonhosted.org/packages/16/86/a9f7569e7e4f5395f927de38a13b92efa73f809285d04f2923b291783dd2/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5e8f75fa371020dd30f9196e7d73babae2abd51cf59bdd56cb4f8de7e13354", size = 2160365 }, + { url = "https://files.pythonhosted.org/packages/04/1b/9a3afb437702503514f3934ec8d7904270edf013d28074f3e700e5dfbb0f/ml_dtypes-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:15fdd922fea57e493844e5abb930b9c0bd0af217d9edd3724479fc3d7ce70e3f", size = 126633 }, + { url = "https://files.pythonhosted.org/packages/d1/76/9835c8609c29f2214359e88f29255fc4aad4ea0f613fb48aa8815ceda1b6/ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975", size = 397973 }, + { url = "https://files.pythonhosted.org/packages/7e/99/e68c56fac5de973007a10254b6e17a0362393724f40f66d5e4033f4962c2/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9", size = 2185134 }, + { url = "https://files.pythonhosted.org/packages/28/bc/6a2344338ea7b61cd7b46fb24ec459360a5a0903b57c55b156c1e46c644a/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752", size = 2163661 }, + { url = "https://files.pythonhosted.org/packages/e8/d3/ddfd9878b223b3aa9a930c6100a99afca5cfab7ea703662e00323acb7568/ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6", size = 126727 }, + { url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077 }, + { url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554 }, + { url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488 }, + { url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462 }, + { url = "https://files.pythonhosted.org/packages/8f/d7/6e1372052fe95c0cacfdb9718dba04726203885ffddb0cfddd8f8aa89a3b/ml_dtypes-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e35e486e97aee577d0890bc3bd9e9f9eece50c08c163304008587ec8cfe7575b", size = 396578 }, + { url = "https://files.pythonhosted.org/packages/1a/f6/ad0bd2735b9570ebf9c113f024b4f2b34f2331f16197c60babdc168b22d5/ml_dtypes-0.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:560be16dc1e3bdf7c087eb727e2cf9c0e6a3d87e9f415079d2491cc419b3ebf5", size = 2181057 }, + { url = "https://files.pythonhosted.org/packages/3e/55/b9711de47135d4d8766ff7907fe54c8bffff545fd646817c352de37b0ad5/ml_dtypes-0.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad0b757d445a20df39035c4cdeed457ec8b60d236020d2560dbc25887533cf50", size = 2156131 }, + { url = "https://files.pythonhosted.org/packages/4b/f3/e5ff8dd27f66c8b80f97f0f89bb0b74e4a7005e5ff5f8f4237126c827911/ml_dtypes-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef0d7e3fece227b49b544fa69e50e607ac20948f0043e9f76b44f35f229ea450", size = 126727 }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version == '3.12.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/49/6e67c334872d2c114df3020e579f3718c333198f8312290e09ec0216703a/ml_dtypes-0.5.1.tar.gz", hash = "sha256:ac5b58559bb84a95848ed6984eb8013249f90b6bab62aa5acbad876e256002c9", size = 698772 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/88/11ebdbc75445eeb5b6869b708a0d787d1ed812ff86c2170bbfb95febdce1/ml_dtypes-0.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd73f51957949069573ff783563486339a9285d72e2f36c18e0c1aa9ca7eb190", size = 671450 }, + { url = "https://files.pythonhosted.org/packages/a4/a4/9321cae435d6140f9b0e7af8334456a854b60e3a9c6101280a16e3594965/ml_dtypes-0.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:810512e2eccdfc3b41eefa3a27402371a3411453a1efc7e9c000318196140fed", size = 4621075 }, + { url = "https://files.pythonhosted.org/packages/16/d8/4502e12c6a10d42e13a552e8d97f20198e3cf82a0d1411ad50be56a5077c/ml_dtypes-0.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141b2ea2f20bb10802ddca55d91fe21231ef49715cfc971998e8f2a9838f3dbe", size = 4738414 }, + { url = "https://files.pythonhosted.org/packages/6b/7e/bc54ae885e4d702e60a4bf50aa9066ff35e9c66b5213d11091f6bffb3036/ml_dtypes-0.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:26ebcc69d7b779c8f129393e99732961b5cc33fcff84090451f448c89b0e01b4", size = 209718 }, + { url = "https://files.pythonhosted.org/packages/c9/fd/691335926126bb9beeb030b61a28f462773dcf16b8e8a2253b599013a303/ml_dtypes-0.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:023ce2f502efd4d6c1e0472cc58ce3640d051d40e71e27386bed33901e201327", size = 671448 }, + { url = "https://files.pythonhosted.org/packages/ff/a6/63832d91f2feb250d865d069ba1a5d0c686b1f308d1c74ce9764472c5e22/ml_dtypes-0.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7000b6e4d8ef07542c05044ec5d8bbae1df083b3f56822c3da63993a113e716f", size = 4625792 }, + { url = "https://files.pythonhosted.org/packages/cc/2a/5421fd3dbe6eef9b844cc9d05f568b9fb568503a2e51cb1eb4443d9fc56b/ml_dtypes-0.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c09526488c3a9e8b7a23a388d4974b670a9a3dd40c5c8a61db5593ce9b725bab", size = 4743893 }, + { url = "https://files.pythonhosted.org/packages/60/30/d3f0fc9499a22801219679a7f3f8d59f1429943c6261f445fb4bfce20718/ml_dtypes-0.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:15ad0f3b0323ce96c24637a88a6f44f6713c64032f27277b069f285c3cf66478", size = 209712 }, + { url = "https://files.pythonhosted.org/packages/47/56/1bb21218e1e692506c220ffabd456af9733fba7aa1b14f73899979f4cc20/ml_dtypes-0.5.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6f462f5eca22fb66d7ff9c4744a3db4463af06c49816c4b6ac89b16bfcdc592e", size = 670372 }, + { url = "https://files.pythonhosted.org/packages/20/95/d8bd96a3b60e00bf31bd78ca4bdd2d6bbaf5acb09b42844432d719d34061/ml_dtypes-0.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f76232163b5b9c34291b54621ee60417601e2e4802a188a0ea7157cd9b323f4", size = 4635946 }, + { url = "https://files.pythonhosted.org/packages/08/57/5d58fad4124192b1be42f68bd0c0ddaa26e44a730ff8c9337adade2f5632/ml_dtypes-0.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4953c5eb9c25a56d11a913c2011d7e580a435ef5145f804d98efa14477d390", size = 4694804 }, + { url = "https://files.pythonhosted.org/packages/38/bc/c4260e4a6c6bf684d0313308de1c860467275221d5e7daf69b3fcddfdd0b/ml_dtypes-0.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:9626d0bca1fb387d5791ca36bacbba298c5ef554747b7ebeafefb4564fc83566", size = 210853 }, + { url = "https://files.pythonhosted.org/packages/0f/92/bb6a3d18e16fddd18ce6d5f480e1919b33338c70e18cba831c6ae59812ee/ml_dtypes-0.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:12651420130ee7cc13059fc56dac6ad300c3af3848b802d475148c9defd27c23", size = 667696 }, + { url = "https://files.pythonhosted.org/packages/6d/29/cfc89d842767e9a51146043b0fa18332c2b38f8831447e6cb1160e3c6102/ml_dtypes-0.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9945669d3dadf8acb40ec2e57d38c985d8c285ea73af57fc5b09872c516106d", size = 4638365 }, + { url = "https://files.pythonhosted.org/packages/be/26/adc36e3ea09603d9f6d114894e1c1b7b8e8a9ef6d0b031cc270c6624a37c/ml_dtypes-0.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf9975bda82a99dc935f2ae4c83846d86df8fd6ba179614acac8e686910851da", size = 4702722 }, + { url = "https://files.pythonhosted.org/packages/da/8a/a2b9375c94077e5a488a624a195621407846f504068ce22ccf805c674156/ml_dtypes-0.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fd918d4e6a4e0c110e2e05be7a7814d10dc1b95872accbf6512b80a109b71ae1", size = 210850 }, + { url = "https://files.pythonhosted.org/packages/52/38/703169100fdde27957f061d4d0ea3e00525775a09acaccf7e655d9609d55/ml_dtypes-0.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:05f23447a1c20ddf4dc7c2c661aa9ed93fcb2658f1017c204d1e758714dc28a8", size = 693043 }, + { url = "https://files.pythonhosted.org/packages/28/ff/4e234c9c23e0d456f5da5a326c103bf890c746d93351524d987e41f438b3/ml_dtypes-0.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b7fbe5571fdf28fd3aaab3ef4aafc847de9ebf263be959958c1ca58ec8eadf5", size = 4903946 }, + { url = "https://files.pythonhosted.org/packages/b7/45/c1a1ccfdd02bc4173ca0f4a2d327683a27df85797b885eb1da1ca325b85c/ml_dtypes-0.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d13755f8e8445b3870114e5b6240facaa7cb0c3361e54beba3e07fa912a6e12b", size = 5052731 }, + { url = "https://files.pythonhosted.org/packages/4c/17/b8f22639eb0cf52aee86c38c6e4cf1586e99445ef8e87bc1d30237bd5a1a/ml_dtypes-0.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8a9d46b4df5ae2135a8e8e72b465448ebbc1559997f4f9304a9ecc3413efb5b", size = 667609 }, + { url = "https://files.pythonhosted.org/packages/95/87/4a1c91ea325ec4708293ae270b375fbdc53917031a64be9d5e3908ac0185/ml_dtypes-0.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb2009ac98da274e893e03162f6269398b2b00d947e7057ee2469a921d58135", size = 4615607 }, + { url = "https://files.pythonhosted.org/packages/e7/6d/f53c438cbdf753164497958f4ff12a6844ae66a00edcabebcde1dc575ab7/ml_dtypes-0.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aefedc579ece2f8fb38f876aa7698204ee4c372d0e54f1c1ffa8ca580b54cc60", size = 4729131 }, + { url = "https://files.pythonhosted.org/packages/aa/10/03107a1ff3b03d6515d7365d9e356bc612b0750edbd5f4327a1db44404bc/ml_dtypes-0.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:8f2c028954f16ede77902b223a8da2d9cbb3892375b85809a5c3cfb1587960c4", size = 209426 }, +] + +[[package]] +name = "mlx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/3b/5160be3949dbed395c73646948f45d792a3de0dd6d8de213f26f2ea8d428/mlx-0.22.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:8082581e05b09d576cdf241184992fe0f96dcae1a857de38a562b2439d4e3333", size = 27110425 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/5d52b4144a946f2d66261df576b1e5f34845d781a2eba06d28ea0e554c51/mlx-0.22.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b45716a948df0727e4e3525ca22e3f590323842020b7505e74491120a3a06d47", size = 26853548 }, + { url = "https://files.pythonhosted.org/packages/b4/e7/2b4b551cff66370e0ce5d37dfb107122688d2b981eced38438e595ef98da/mlx-0.22.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:564ba1ca7711fc7ca5a1a17f43fc1859aef164229ee830050c66ef7453a9109d", size = 27110977 }, + { url = "https://files.pythonhosted.org/packages/69/33/16e3a1451f6b9d9d4252d48453470d907c20f13a39915fd3e18d7a049570/mlx-0.22.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:03326dde450b9a8d75933514d3d4dd86163d2296bf70ff5d37befed3d6ba4144", size = 26854060 }, + { url = "https://files.pythonhosted.org/packages/bc/33/dfab1ea36fc9b11e310922dceaeee210fc589efd277c409b0dc51bd38b79/mlx-0.22.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:56dec470a1a75010d1888a2e8f9cf24780980ff163eb6b03e9871bf440642a5b", size = 27107090 }, + { url = "https://files.pythonhosted.org/packages/32/2b/756b04394e41dacd04ccb6fde0fede837590ba752229d2c27e6c442929ce/mlx-0.22.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1dface82280483f14f2557da2afe8c965f7b09caa4bdae962cfa78897d0bed73", size = 26850177 }, + { url = "https://files.pythonhosted.org/packages/62/2b/427896261bc8d940eff561e6199d1aee9dbdc7caa117486654a44d7d793c/mlx-0.22.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:50d0d76826cfe939025791ce2c014e743ec7aff7aa67194ffaef40c40e574ef4", size = 27106970 }, + { url = "https://files.pythonhosted.org/packages/6e/5a/314451ba1600a57619643bfc61c65e8ce34fed0ad011488ad3bc0fe3e739/mlx-0.22.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bff0785c1be9ebc323d733c72f8a0683e757210221a9b8d53db3ac40ac7b75cf", size = 26850056 }, + { url = "https://files.pythonhosted.org/packages/57/51/19deaecfe6b34c6ca30b4e966c8224145664747330143463f76b4c218a55/mlx-0.22.0-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:121d23087ea706b216c9f43f684c8068a6803d4aec1ddce1e3f6a66e99e797fa", size = 27110566 }, + { url = "https://files.pythonhosted.org/packages/e4/88/56d767e752fea62e44704c9960fdf45a164b35866e6bc94f4a245122d8cc/mlx-0.22.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:d287de53d69af9129d8b59e13071c9c0711d729ddfc749f3187b0c894bf85d6b", size = 26853719 }, +] + +[[package]] +name = "mlx-lm" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "mlx" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "transformers", extra = ["sentencepiece"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/3a/125adb6e677795129ee932d189bac031253ebfcebaa1df559bdd4f0671b6/mlx_lm-0.21.1.tar.gz", hash = "sha256:701d4d1de92785cc24e651a28d46894df567187711cc994c88cb7247f3d1cde0", size = 106147 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/8f/41cd35d3ef6117a6831a02b27633b7a7dd8798c6d1a435a864a9db6ecb84/mlx_lm-0.21.1-py3-none-any.whl", hash = "sha256:fcc4a37c08cd3d6855f1fc5197cf4c8ebf790e1c778bce19f5c2c742096708c9", size = 140948 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, + { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, + { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, + { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, + { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, + { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, + { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, + { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, + { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, + { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, + { url = "https://files.pythonhosted.org/packages/f7/3b/544a5c5886042b80e1f4847a4757af3430f60d106d8d43bb7be72c9e9650/msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", size = 150713 }, + { url = "https://files.pythonhosted.org/packages/93/af/d63f25bcccd3d6f06fd518ba4a321f34a4370c67b579ca5c70b4a37721b4/msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", size = 84277 }, + { url = "https://files.pythonhosted.org/packages/92/9b/5c0dfb0009b9f96328664fecb9f8e4e9c8a1ae919e6d53986c1b813cb493/msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", size = 81357 }, + { url = "https://files.pythonhosted.org/packages/d1/7c/3a9ee6ec9fc3e47681ad39b4d344ee04ff20a776b594fba92d88d8b68356/msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", size = 371256 }, + { url = "https://files.pythonhosted.org/packages/f7/0a/8a213cecea7b731c540f25212ba5f9a818f358237ac51a44d448bd753690/msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", size = 377868 }, + { url = "https://files.pythonhosted.org/packages/1b/94/a82b0db0981e9586ed5af77d6cfb343da05d7437dceaae3b35d346498110/msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", size = 363370 }, + { url = "https://files.pythonhosted.org/packages/93/fc/6c7f0dcc1c913e14861e16eaf494c07fc1dde454ec726ff8cebcf348ae53/msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", size = 358970 }, + { url = "https://files.pythonhosted.org/packages/1f/c6/e4a04c0089deace870dabcdef5c9f12798f958e2e81d5012501edaff342f/msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", size = 366358 }, + { url = "https://files.pythonhosted.org/packages/b6/54/7d8317dac590cf16b3e08e3fb74d2081e5af44eb396f0effa13f17777f30/msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", size = 370336 }, + { url = "https://files.pythonhosted.org/packages/dc/6f/a5a1f43b6566831e9630e5bc5d86034a8884386297302be128402555dde1/msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", size = 68683 }, + { url = "https://files.pythonhosted.org/packages/5f/e8/2162621e18dbc36e2bc8492fd0e97b3975f5d89fe0472ae6d5f7fbdd8cf7/msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", size = 74787 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, + { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, + { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, + { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, + { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, + { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, + { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, + { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, + { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, + { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, + { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, + { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, + { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, + { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, + { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/e7/c9/9e153a6572b38ac5ff4434113af38acf8d5e9957897cdb1f513b3d6614ed/multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", size = 48550 }, + { url = "https://files.pythonhosted.org/packages/76/f5/79565ddb629eba6c7f704f09a09df085c8dc04643b12506f10f718cee37a/multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", size = 29298 }, + { url = "https://files.pythonhosted.org/packages/60/1b/9851878b704bc98e641a3e0bce49382ae9e05743dac6d97748feb5b7baba/multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", size = 29641 }, + { url = "https://files.pythonhosted.org/packages/89/87/d451d45aab9e422cb0fb2f7720c31a4c1d3012c740483c37f642eba568fb/multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", size = 126202 }, + { url = "https://files.pythonhosted.org/packages/fa/b4/27cbe9f3e2e469359887653f2e45470272eef7295139916cc21107c6b48c/multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", size = 133925 }, + { url = "https://files.pythonhosted.org/packages/4d/a3/afc841899face8adfd004235ce759a37619f6ec99eafd959650c5ce4df57/multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", size = 129039 }, + { url = "https://files.pythonhosted.org/packages/5e/41/0d0fb18c1ad574f807196f5f3d99164edf9de3e169a58c6dc2d6ed5742b9/multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", size = 124072 }, + { url = "https://files.pythonhosted.org/packages/00/22/defd7a2e71a44e6e5b9a5428f972e5b572e7fe28e404dfa6519bbf057c93/multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", size = 116532 }, + { url = "https://files.pythonhosted.org/packages/91/25/f7545102def0b1d456ab6449388eed2dfd822debba1d65af60194904a23a/multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", size = 128173 }, + { url = "https://files.pythonhosted.org/packages/45/79/3dbe8d35fc99f5ea610813a72ab55f426cb9cf482f860fa8496e5409be11/multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", size = 122654 }, + { url = "https://files.pythonhosted.org/packages/97/cb/209e735eeab96e1b160825b5d0b36c56d3862abff828fc43999bb957dcad/multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", size = 133197 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/a13808a7ada62808afccea67837a79d00ad6581440015ef00f726d064c2d/multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", size = 129754 }, + { url = "https://files.pythonhosted.org/packages/77/dd/8540e139eafb240079242da8f8ffdf9d3f4b4ad1aac5a786cd4050923783/multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", size = 126402 }, + { url = "https://files.pythonhosted.org/packages/86/99/e82e1a275d8b1ea16d3a251474262258dbbe41c05cce0c01bceda1fc8ea5/multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", size = 26421 }, + { url = "https://files.pythonhosted.org/packages/86/1c/9fa630272355af7e4446a2c7550c259f11ee422ab2d30ff90a0a71cf3d9e/multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", size = 28791 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "multiprocess" +version = "0.70.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980 }, + { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982 }, + { url = "https://files.pythonhosted.org/packages/d8/94/8638a89f93c80df329116e6781a060506c7e91e1f4370dc831e9d17a041d/multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41", size = 133497 }, + { url = "https://files.pythonhosted.org/packages/89/21/222066f6bb8d8af287923ae3bd26cf4699a9ce020228ac273caca1de8250/multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a", size = 133498 }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824 }, + { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519 }, + { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741 }, + { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628 }, + { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "networkx" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9.17' and python_full_version < '3.10'", + "python_full_version < '3.9.17'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772 }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + +[[package]] +name = "ninja" +version = "1.11.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/8f/21a2701f95b7d0d5137736561b3427ece0c4a1e085d4a223b92d16ab7d8b/ninja-1.11.1.3.tar.gz", hash = "sha256:edfa0d2e9d7ead1635b03e40a32ad56cc8f56798b6e2e9848d8300b174897076", size = 129532 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ba/0069cd4a83d68f7b0308be70e219b15d675e50c8ea28763a3f0373c45bfc/ninja-1.11.1.3-py3-none-macosx_10_9_universal2.whl", hash = "sha256:2b4879ea3f1169f3d855182c57dcc84d1b5048628c8b7be0d702b81882a37237", size = 279132 }, + { url = "https://files.pythonhosted.org/packages/72/6b/3805be87df8417a0c7b21078c8045f2a1e59b34f371bfe4cb4fb0d6df7f2/ninja-1.11.1.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bc3ebc8b2e47716149f3541742b5cd8e0b08f51013b825c05baca3e34854370d", size = 472101 }, + { url = "https://files.pythonhosted.org/packages/6b/35/a8e38d54768e67324e365e2a41162be298f51ec93e6bd4b18d237d7250d8/ninja-1.11.1.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a27e78ca71316c8654965ee94b286a98c83877bfebe2607db96897bbfe458af0", size = 422884 }, + { url = "https://files.pythonhosted.org/packages/2f/99/7996457319e139c02697fb2aa28e42fe32bb0752cef492edc69d56a3552e/ninja-1.11.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2883ea46b3c5079074f56820f9989c6261fcc6fd873d914ee49010ecf283c3b2", size = 157046 }, + { url = "https://files.pythonhosted.org/packages/6d/8b/93f38e5cddf76ccfdab70946515b554f25d2b4c95ef9b2f9cfbc43fa7cc1/ninja-1.11.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c4bdb9fd2d0c06501ae15abfd23407660e95659e384acd36e013b6dd7d8a8e4", size = 180014 }, + { url = "https://files.pythonhosted.org/packages/7d/1d/713884d0fa3c972164f69d552e0701d30e2bf25eba9ef160bfb3dc69926a/ninja-1.11.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:114ed5c61c8474df6a69ab89097a20749b769e2c219a452cb2fadc49b0d581b0", size = 157098 }, + { url = "https://files.pythonhosted.org/packages/c7/22/ecb0f70e77c9e22ee250aa717a608a142756833a34d43943d7d658ee0e56/ninja-1.11.1.3-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fa2247fce98f683bc712562d82b22b8a0a5c000738a13147ca2d1b68c122298", size = 130089 }, + { url = "https://files.pythonhosted.org/packages/ec/a6/3ee846c20ab6ad95b90c5c8703c76cb1f39cc8ce2d1ae468956e3b1b2581/ninja-1.11.1.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:a38c6c6c8032bed68b70c3b065d944c35e9f903342875d3a3218c1607987077c", size = 372508 }, + { url = "https://files.pythonhosted.org/packages/95/0d/aa44abe4141f29148ce671ac8c92045878906b18691c6f87a29711c2ff1c/ninja-1.11.1.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:56ada5d33b8741d298836644042faddebc83ee669782d661e21563034beb5aba", size = 419369 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/48bf5105568ac9bd2016b701777bdd5000cc09a14ac837fef9f15e8d634e/ninja-1.11.1.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:53409151da081f3c198bb0bfc220a7f4e821e022c5b7d29719adda892ddb31bb", size = 420304 }, + { url = "https://files.pythonhosted.org/packages/18/e5/69df63976cf971a03379899f8520a036c9dbab26330b37197512aed5b3df/ninja-1.11.1.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:1ad2112c2b0159ed7c4ae3731595191b1546ba62316fc40808edecd0306fefa3", size = 416056 }, + { url = "https://files.pythonhosted.org/packages/6f/4f/bdb401af7ed0e24a3fef058e13a149f2de1ce4b176699076993615d55610/ninja-1.11.1.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28aea3c1c280cba95b8608d50797169f3a34280e3e9a6379b6e340f0c9eaeeb0", size = 379725 }, + { url = "https://files.pythonhosted.org/packages/bd/68/05e7863bf13128c61652eeb3ec7096c3d3a602f32f31752dbfb034e3fa07/ninja-1.11.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b6966f83064a88a51693073eea3decd47e08c3965241e09578ef7aa3a7738329", size = 434881 }, + { url = "https://files.pythonhosted.org/packages/bd/ad/edc0d1efe77f29f45bbca2e1dab07ef597f61a88de6e4bccffc0aec2256c/ninja-1.11.1.3-py3-none-win32.whl", hash = "sha256:a4a3b71490557e18c010cbb26bd1ea9a0c32ee67e8f105e9731515b6e0af792e", size = 255988 }, + { url = "https://files.pythonhosted.org/packages/03/93/09a9f7672b4f97438aca6217ac54212a63273f1cd3b46b731d0bb22c53e7/ninja-1.11.1.3-py3-none-win_amd64.whl", hash = "sha256:04d48d14ea7ba11951c156599ab526bdda575450797ff57c6fdf99b2554d09c7", size = 296502 }, + { url = "https://files.pythonhosted.org/packages/d9/9d/0cc1e82849070ff3cbee69f326cb48a839407bcd15d8844443c30a5e7509/ninja-1.11.1.3-py3-none-win_arm64.whl", hash = "sha256:17978ad611d8ead578d83637f5ae80c2261b033db0b493a7ce94f88623f29e1b", size = 270571 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, + { url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", size = 20636301 }, + { url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", size = 13971216 }, + { url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", size = 14226281 }, + { url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", size = 18249516 }, + { url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", size = 13882132 }, + { url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", size = 18084181 }, + { url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", size = 5976360 }, + { url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", size = 15814633 }, + { url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", size = 20455961 }, + { url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", size = 18061071 }, + { url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", size = 15709730 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278 }, +] + +[[package]] +name = "nvidia-ml-py" +version = "12.560.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/10/5f34de4a71db8b2b7ec4269f4a33287f24c23e2857ea3187c977b7bc3604/nvidia-ml-py-12.560.30.tar.gz", hash = "sha256:f0254dc7400647680a072ee02509bfd46102b60bdfeca321576d4d4817e7fe97", size = 39194 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/f3/a69ce0b1a1e12fbf6b2ad9f4c14c9999fdbdf15f2478d210f0fd501ddc98/nvidia_ml_py-12.560.30-py3-none-any.whl", hash = "sha256:fea371c94d63e38a611c17bbb85fe400e9c8ddb9e8684a9cd0e47786a4bc3c73", size = 40526 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.20.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/2a/0a131f572aa09f741c30ccd45a8e56316e8be8dfc7bc19bf0ab7cfef7b19/nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:057f6bf9685f75215d0c53bf3ac4a10b3e6578351de307abad9e18a99182af56", size = 176249402 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.6.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138 }, +] + +[[package]] +name = "openai" +version = "1.59.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/c4/b4482784de63c7158f6c0afcb07fd66450ea6c912d6bddf9d7599f2eda25/openai-1.59.8.tar.gz", hash = "sha256:ac4bda5fa9819fdc6127e8ea8a63501f425c587244bc653c7c11a8ad84f953e1", size = 346775 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/cf/5b235e12ead3cd2098f9792776c966994c1bc558cba5799e12f3045227df/openai-1.59.8-py3-none-any.whl", hash = "sha256:a8b8ee35c4083b88e6da45406d883cf6bd91a98ab7dd79178b8bc24c8bfb09d9", size = 455567 }, +] + +[[package]] +name = "opt-einsum" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932 }, +] + +[[package]] +name = "outlines" +source = { editable = "." } +dependencies = [ + { name = "airportsdata" }, + { name = "cloudpickle" }, + { name = "diskcache" }, + { name = "genson" }, + { name = "interegular" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "lark" }, + { name = "nest-asyncio" }, + { name = "numpy" }, + { name = "outlines-core" }, + { name = "pre-commit" }, + { name = "pycountry" }, + { name = "pydantic" }, + { name = "referencing" }, + { name = "requests" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +exllamav2 = [ + { name = "exllamav2" }, +] +llamacpp = [ + { name = "datasets" }, + { name = "llama-cpp-python" }, + { name = "numpy" }, + { name = "transformers" }, +] +mlxlm = [ + { name = "datasets" }, + { name = "mlx-lm" }, +] +openai = [ + { name = "openai" }, +] +serve = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "uvicorn" }, + { name = "vllm" }, +] +test = [ + { name = "accelerate" }, + { name = "beartype" }, + { name = "coverage", extra = ["toml"] }, + { name = "datasets" }, + { name = "diff-cover", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9.17'" }, + { name = "diff-cover", version = "9.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9.17'" }, + { name = "exllamav2" }, + { name = "huggingface-hub" }, + { name = "jax", version = "0.4.30", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jax", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "llama-cpp-python" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "openai" }, + { name = "pillow" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "responses" }, + { name = "transformers" }, + { name = "vllm", marker = "sys_platform == 'linux'" }, +] +transformers = [ + { name = "accelerate" }, + { name = "datasets" }, + { name = "numpy" }, + { name = "transformers" }, +] +vllm = [ + { name = "numpy" }, + { name = "transformers" }, + { name = "vllm" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", marker = "extra == 'test'" }, + { name = "accelerate", marker = "extra == 'transformers'" }, + { name = "airportsdata" }, + { name = "beartype", marker = "extra == 'test'", specifier = "<0.16.0" }, + { name = "cloudpickle" }, + { name = "coverage", extras = ["toml"], marker = "extra == 'test'", specifier = ">=5.1" }, + { name = "datasets", marker = "extra == 'llamacpp'" }, + { name = "datasets", marker = "extra == 'mlxlm'" }, + { name = "datasets", marker = "extra == 'test'" }, + { name = "datasets", marker = "extra == 'transformers'" }, + { name = "diff-cover", marker = "extra == 'test'" }, + { name = "diskcache" }, + { name = "exllamav2", marker = "extra == 'exllamav2'" }, + { name = "exllamav2", marker = "extra == 'test'" }, + { name = "fastapi", marker = "extra == 'serve'" }, + { name = "genson" }, + { name = "huggingface-hub", marker = "extra == 'test'" }, + { name = "interegular" }, + { name = "jax", marker = "extra == 'test'" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "lark" }, + { name = "llama-cpp-python", marker = "extra == 'llamacpp'" }, + { name = "llama-cpp-python", marker = "extra == 'test'" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'test'", specifier = ">=0.19.2" }, + { name = "mlx-lm", marker = "extra == 'mlxlm'" }, + { name = "nest-asyncio" }, + { name = "numpy" }, + { name = "numpy", marker = "extra == 'llamacpp'", specifier = "<2" }, + { name = "numpy", marker = "extra == 'transformers'", specifier = "<2" }, + { name = "numpy", marker = "extra == 'vllm'", specifier = "<2" }, + { name = "openai", marker = "extra == 'openai'" }, + { name = "openai", marker = "extra == 'test'", specifier = ">=1.0.0" }, + { name = "outlines-core", specifier = "==0.1.26" }, + { name = "pillow", marker = "extra == 'test'" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pre-commit", marker = "extra == 'test'" }, + { name = "pycountry" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pydantic", marker = "extra == 'serve'", specifier = ">=2.0" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-benchmark", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pytest-mock", marker = "extra == 'test'" }, + { name = "referencing" }, + { name = "requests" }, + { name = "responses", marker = "extra == 'test'" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers", marker = "extra == 'llamacpp'" }, + { name = "transformers", marker = "extra == 'test'" }, + { name = "transformers", marker = "extra == 'transformers'" }, + { name = "transformers", marker = "extra == 'vllm'" }, + { name = "typing-extensions" }, + { name = "uvicorn", marker = "extra == 'serve'" }, + { name = "vllm", marker = "sys_platform == 'linux' and extra == 'test'" }, + { name = "vllm", marker = "extra == 'serve'", specifier = ">=0.3.0" }, + { name = "vllm", marker = "extra == 'vllm'" }, +] + +[[package]] +name = "outlines-core" +version = "0.1.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interegular" }, + { name = "jsonschema" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/f3/274d07f4702728b43581235a77e545ec602b25f9b0098b288a0f3052521d/outlines_core-0.1.26.tar.gz", hash = "sha256:481c4301341e77cc8f1832d616784adb4d461b4fec65878e7c0d2cba7163a189", size = 75139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/df/e9ff00f1dcf671cb8c4c20abcfd53406328b344cafa689a2832e8059c0b4/outlines_core-0.1.26-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6a962a7452e7ac170fa04d405342cadae2d28fafa5b1830cef7aa610257ed32f", size = 322602 }, + { url = "https://files.pythonhosted.org/packages/3c/f1/e9064f18c462a61f4abbe73b24f25e36d8abef19c593416fa69dce6a83c0/outlines_core-0.1.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15a3684fa29564da2db03934cf0097bef3e871f70d3af0ef2b52fdb886da2e09", size = 301929 }, + { url = "https://files.pythonhosted.org/packages/76/c3/6bc82db40b4818421e573237f43d4026c40a3305fa2558eb0aa1a7aa08f7/outlines_core-0.1.26-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64e01c0cfa9ba371634d7c3f6ea1862397cef98e4509fe98e3f57faa721a72d6", size = 321355 }, + { url = "https://files.pythonhosted.org/packages/c9/c2/1d85bfeaee3a83327e0d162bee4bdc7d7889bea5998e44fcc66c924dc1fd/outlines_core-0.1.26-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3c4196148e47f455f1ace78e329d5b97e531cbc406456d681592952adae7e17", size = 343552 }, + { url = "https://files.pythonhosted.org/packages/45/da/1e61d3d997ba1858fb8e71c3127f24a95c30575559da012ea5b45b147ad3/outlines_core-0.1.26-cp310-cp310-win32.whl", hash = "sha256:f38d290a7f6e5e12cbfcaee03269dfc0dbda49b360024b4279d1aba251fdc346", size = 234750 }, + { url = "https://files.pythonhosted.org/packages/1c/04/6d7968019a81df235ad6bc7405eefe32be8da4c4153792655e7490d06c8d/outlines_core-0.1.26-cp310-cp310-win_amd64.whl", hash = "sha256:11ff56af56cb54c563b7f25d86cd9ee77f3fed825f1d4dccd9449bb1e4e89538", size = 243713 }, + { url = "https://files.pythonhosted.org/packages/17/94/19d5c50c303ba71f3465c81620ca9b5af4db07fd8922dfe59ae5a9ae61d1/outlines_core-0.1.26-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b6787b07b7c673fc3087d2b537719ecac8e03b10a47d032dd1926985c32885b0", size = 322344 }, + { url = "https://files.pythonhosted.org/packages/f2/ea/f44beea7f610f2737ebb908c8dfa37d8324e92ca529468a56b00a77af199/outlines_core-0.1.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e0ea28a76da31d25b6f53242bf13e1b59a0241badf82353c88f55e1cf81b128", size = 301670 }, + { url = "https://files.pythonhosted.org/packages/6a/a6/ceac3760e1feb898b4047aeb54e0a3de975b59e87a17d6ba0a04dec5eaed/outlines_core-0.1.26-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8932044a3d9329be53a226118850638f85b4d7842f9b863d0a123f23de220cd", size = 321067 }, + { url = "https://files.pythonhosted.org/packages/92/f0/ad0074d6726fed86bb0bba1b9307cbbd67a2af5debd3540d66c69298a001/outlines_core-0.1.26-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84b7cd2fb6268bf990dd3d479ffb4fa0bace6f571cb85b15b6cdb44b84f5b69", size = 343264 }, + { url = "https://files.pythonhosted.org/packages/e6/bd/198c9a73d5f36e2ecad558a26359af3f0dbe4f5ba11c4629e46fccdfe2d6/outlines_core-0.1.26-cp311-cp311-win32.whl", hash = "sha256:f19765c151abfc970996368080aeea6d2a19e927817fe4e2af6726e639be3de4", size = 234529 }, + { url = "https://files.pythonhosted.org/packages/b9/27/354b484045e6368c92f688d954124064ec2ce961681e56711852904e1ec2/outlines_core-0.1.26-cp311-cp311-win_amd64.whl", hash = "sha256:3f59aeccea21ed6ff3cf52102fd163f26d279821c20e5127ddd18d4ea4d0c8d2", size = 243457 }, + { url = "https://files.pythonhosted.org/packages/c6/86/0fb40746e579db38d89f127122a3900d9e0350f76aae8cb61adeaff44cc2/outlines_core-0.1.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f54633bca50055d42ea4d94ae06dcbe52d3d76a9b621b75723b1177d0d952953", size = 321874 }, + { url = "https://files.pythonhosted.org/packages/ab/0c/b91f7bc03843796c1d643ee030b6cd8fd5a8ba2cd4856c855f140c878976/outlines_core-0.1.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9525321b48700dcaaabf60bcdc951e45f9357ba3fb3e1bfc81b662d7d4170e7c", size = 301995 }, + { url = "https://files.pythonhosted.org/packages/ad/db/fa91a2d54288b900de82d86eda3adb2417b3b5b2db6256854a5e8bc85c32/outlines_core-0.1.26-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f409f72c11f6ffadb57066950dd384d5388015028c1a1a615c9a64988dae3e", size = 321050 }, + { url = "https://files.pythonhosted.org/packages/e2/1d/a36292b6198986bd9c3ff8c24355deb82ed5475403379ee40b5b5473e2e3/outlines_core-0.1.26-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86a1bb46adc5cbf6dfd7a7fe4105e0e2a4c6e041732a053126b41c521a1f223", size = 343201 }, + { url = "https://files.pythonhosted.org/packages/08/63/5dd2b5a364412f674b6edcb59b0c21513bdb07cdcc7613b064c1a0660d01/outlines_core-0.1.26-cp312-cp312-win32.whl", hash = "sha256:19f462f6b00935708677ad27cb4df55e0e17f6ffe713ab750f5f2683b090f95d", size = 233970 }, + { url = "https://files.pythonhosted.org/packages/a5/56/8adf0b7446d1e975c2314454813c59eb7b195889908a2932ed34148c113c/outlines_core-0.1.26-cp312-cp312-win_amd64.whl", hash = "sha256:9b36bff12779e58883747116893a17b3551bbd10865878b951b03a44d112229a", size = 243578 }, + { url = "https://files.pythonhosted.org/packages/5b/b8/d38f13e417f2af82f6cd588df5a26ff8ffeb9a3a15b8a1a883d5922048ac/outlines_core-0.1.26-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7b7849cf40028319ebb9d8ba0fe4c590ef5888eebe524a81b3af30aaa06ea21c", size = 323047 }, + { url = "https://files.pythonhosted.org/packages/3b/59/43f5c7b8ad0e98a663db8f24cbff0421a58e946934e3f3751a17a06ae84d/outlines_core-0.1.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2f8641aab4a6bd84516907492ce82099503129da01b3c29c1dc9ad50320bae77", size = 302547 }, + { url = "https://files.pythonhosted.org/packages/23/30/d9eb8202cbc994536a1fefa60e87c582a714ad1b46367c43af299f3889ba/outlines_core-0.1.26-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba56604efdbc5932c7a8a88c2b8b0d0c740ab883b0012fb5464a9736796802b", size = 321817 }, + { url = "https://files.pythonhosted.org/packages/80/c7/bd718f6329f5c65b936edff532e40614b66032b153ab9ac89e6cb6d2fcff/outlines_core-0.1.26-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc8c87d89bd267356f8149c9066cbb98970425ec162997fbf195c3f1feb7009", size = 344102 }, + { url = "https://files.pythonhosted.org/packages/cb/3e/69ca7394fc89c98c763f2d475c08206fa56f2ffeec210833c1db0195ba51/outlines_core-0.1.26-cp39-cp39-win32.whl", hash = "sha256:9d792a43ed9d8a4e1b38f4d83fe99db442d57aad4404c2edf98b710892eda47e", size = 235081 }, + { url = "https://files.pythonhosted.org/packages/56/7f/38decd8d63f6b2d513c372186c68337455efba3fb45d0ff722ac268f05d2/outlines_core-0.1.26-cp39-cp39-win_amd64.whl", hash = "sha256:ad8564ecd7b64bcb840596c5049ff1c1a96346de494302ffcc0f2b188c15675e", size = 244007 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/8848a4c9b8fdf5a534fe2077af948bf53cd713d77ffbcd7bd15710348fd7/pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39", size = 12595535 }, + { url = "https://files.pythonhosted.org/packages/9c/b9/5cead4f63b6d31bdefeb21a679bc5a7f4aaf262ca7e07e2bc1c341b68470/pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30", size = 11319822 }, + { url = "https://files.pythonhosted.org/packages/31/af/89e35619fb573366fa68dc26dad6ad2c08c17b8004aad6d98f1a31ce4bb3/pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c", size = 15625439 }, + { url = "https://files.pythonhosted.org/packages/3d/dd/bed19c2974296661493d7acc4407b1d2db4e2a482197df100f8f965b6225/pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c", size = 13068928 }, + { url = "https://files.pythonhosted.org/packages/31/a3/18508e10a31ea108d746c848b5a05c0711e0278fa0d6f1c52a8ec52b80a5/pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea", size = 16783266 }, + { url = "https://files.pythonhosted.org/packages/c4/a5/3429bd13d82bebc78f4d78c3945efedef63a7cd0c15c17b2eeb838d1121f/pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761", size = 14450871 }, + { url = "https://files.pythonhosted.org/packages/2f/49/5c30646e96c684570925b772eac4eb0a8cb0ca590fa978f56c5d3ae73ea1/pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e", size = 11618011 }, +] + +[[package]] +name = "pillow" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 }, + { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 }, + { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 }, + { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 }, + { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 }, + { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 }, + { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 }, + { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 }, + { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 }, + { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 }, + { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, + { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, + { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, + { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, + { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, + { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, + { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, + { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, + { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, + { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, + { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, + { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, + { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, + { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, + { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, + { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, + { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, + { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, + { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, + { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, + { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, + { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, + { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, + { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, + { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, + { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, + { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, + { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, + { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, + { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, + { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, + { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, + { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, + { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, + { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, + { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, + { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, + { url = "https://files.pythonhosted.org/packages/9a/1f/9df5ac77491fddd2e36c352d16976dc11fbe6ab842f5df85fd7e31b847b9/pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6", size = 3229995 }, + { url = "https://files.pythonhosted.org/packages/a6/62/c7b359e924dca274173b04922ac06aa63614f7e934d132f2fe1d852509aa/pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e", size = 3101890 }, + { url = "https://files.pythonhosted.org/packages/7b/63/136f21340a434de895b62bcf2c386005a8aa24066c4facd619f5e0e9f283/pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc", size = 4310366 }, + { url = "https://files.pythonhosted.org/packages/f6/46/0bd0ca03d9d1164a7fa33d285ef6d1c438e963d0c8770e4c5b3737ef5abe/pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2", size = 4391582 }, + { url = "https://files.pythonhosted.org/packages/0c/55/f182db572b28bd833b8e806f933f782ceb2df64c40e4d8bd3d4226a46eca/pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade", size = 4350278 }, + { url = "https://files.pythonhosted.org/packages/75/fb/e330fdbbcbc4744214b5f53b84d9d8a9f4ffbebc2e9c2ac10475386e3296/pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884", size = 4471768 }, + { url = "https://files.pythonhosted.org/packages/eb/51/20ee6c4da4448d7a67ffb720a5fcdb965115a78e211a1f58f9845ae15f86/pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196", size = 4276549 }, + { url = "https://files.pythonhosted.org/packages/37/f2/a25c0bdaa6d6fd5cc3d4a6f65b5a7ea46e7af58bee00a98efe0a5af79c58/pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8", size = 4409350 }, + { url = "https://files.pythonhosted.org/packages/12/a7/06687947604cd3e47abeea1b78b65d34ffce7feab03cfe0dd985f115dca3/pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5", size = 2291271 }, + { url = "https://files.pythonhosted.org/packages/21/a6/f51d47675940b5c63b08ff0575b3518428b4acb891f88526fa4ee1edab6f/pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f", size = 2625783 }, + { url = "https://files.pythonhosted.org/packages/95/56/97750bd33e68648fa432dfadcb8ede7624bd905822d42262d34bcebdd9d7/pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a", size = 2375193 }, + { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 }, + { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 }, + { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 }, + { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 }, + { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 }, + { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 }, + { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "prometheus-client" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/01/e3ccb464ba9bf6c001ad85652e6fc7e63098c2685275a682cabc8e7871b8/prometheus_fastapi_instrumentator-7.0.2.tar.gz", hash = "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f", size = 19844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/f7/a67e804853d05b3f1f5def0ebd662b9f48dbcfe0b452f0aa5d4c183a6f86/prometheus_fastapi_instrumentator-7.0.2-py3-none-any.whl", hash = "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d", size = 18998 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, + { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, + { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, + { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, + { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, + { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, + { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, + { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, + { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, + { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, + { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, + { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, + { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, + { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, + { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, + { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/0a/08/6ab7f65240a16fa01023125e65258acf7e4884f483f267cdd6fcc48f37db/propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541", size = 80403 }, + { url = "https://files.pythonhosted.org/packages/34/fe/e7180285e21b4e6dff7d311fdf22490c9146a09a02834b5232d6248c6004/propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e", size = 46152 }, + { url = "https://files.pythonhosted.org/packages/9c/36/aa74d884af826030ba9cee2ac109b0664beb7e9449c315c9c44db99efbb3/propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4", size = 45674 }, + { url = "https://files.pythonhosted.org/packages/22/59/6fe80a3fe7720f715f2c0f6df250dacbd7cad42832410dbd84c719c52f78/propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097", size = 207792 }, + { url = "https://files.pythonhosted.org/packages/4a/68/584cd51dd8f4d0f5fff5b128ce0cdb257cde903898eecfb92156bbc2c780/propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd", size = 223280 }, + { url = "https://files.pythonhosted.org/packages/85/cb/4c3528460c41e61b06ec3f970c0f89f87fa21f63acac8642ed81a886c164/propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681", size = 221293 }, + { url = "https://files.pythonhosted.org/packages/69/c0/560e050aa6d31eeece3490d1174da508f05ab27536dfc8474af88b97160a/propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16", size = 208259 }, + { url = "https://files.pythonhosted.org/packages/0c/87/d6c86a77632eb1ba86a328e3313159f246e7564cb5951e05ed77555826a0/propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d", size = 198632 }, + { url = "https://files.pythonhosted.org/packages/3a/2b/3690ea7b662dc762ab7af5f3ef0e2d7513c823d193d7b2a1b4cda472c2be/propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae", size = 203516 }, + { url = "https://files.pythonhosted.org/packages/4d/b5/afe716c16c23c77657185c257a41918b83e03993b6ccdfa748e5e7d328e9/propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b", size = 199402 }, + { url = "https://files.pythonhosted.org/packages/a4/c0/2d2df3aa7f8660d0d4cc4f1e00490c48d5958da57082e70dea7af366f876/propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347", size = 200528 }, + { url = "https://files.pythonhosted.org/packages/21/c8/65ac9142f5e40c8497f7176e71d18826b09e06dd4eb401c9a4ee41aa9c74/propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf", size = 211254 }, + { url = "https://files.pythonhosted.org/packages/09/e4/edb70b447a1d8142df51ec7511e84aa64d7f6ce0a0fdf5eb55363cdd0935/propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04", size = 214589 }, + { url = "https://files.pythonhosted.org/packages/cb/02/817f309ec8d8883287781d6d9390f80b14db6e6de08bc659dfe798a825c2/propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587", size = 207283 }, + { url = "https://files.pythonhosted.org/packages/d7/fe/2d18612096ed2212cfef821b6fccdba5d52efc1d64511c206c5c16be28fd/propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb", size = 40866 }, + { url = "https://files.pythonhosted.org/packages/24/2e/b5134802e7b57c403c7b73c7a39374e7a6b7f128d1968b4a4b4c0b700250/propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1", size = 44975 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/85/a6/bf65a38f8be5ab8c3b575822acfd338702fdf7ac9abd8c81630cc7c9f4bd/protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7", size = 422676 }, + { url = "https://files.pythonhosted.org/packages/ac/e2/48d46adc86369ff092eaece3e537f76b3baaab45ca3dde257838cde831d2/protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da", size = 434593 }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, +] + +[[package]] +name = "psutil" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, +] + +[[package]] +name = "pyarrow" +version = "19.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/01/fe1fd04744c2aa038e5a11c7a4adb3d62bce09798695e54f7274b5977134/pyarrow-19.0.0.tar.gz", hash = "sha256:8d47c691765cf497aaeed4954d226568563f1b3b74ff61139f2d77876717084b", size = 1129096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/02/1ad80ffd3c558916858a49c83b6e494a9d93009bbebc603cf0cb8263bea7/pyarrow-19.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c318eda14f6627966997a7d8c374a87d084a94e4e38e9abbe97395c215830e0c", size = 30686262 }, + { url = "https://files.pythonhosted.org/packages/1b/f0/adab5f142eb8203db8bfbd3a816816e37a85423ae684567e7f3555658315/pyarrow-19.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:62ef8360ff256e960f57ce0299090fb86423afed5e46f18f1225f960e05aae3d", size = 32100005 }, + { url = "https://files.pythonhosted.org/packages/94/8b/e674083610e5efc48d2f205c568d842cdfdf683d12f9ff0d546e38757722/pyarrow-19.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2795064647add0f16563e57e3d294dbfc067b723f0fd82ecd80af56dad15f503", size = 41144815 }, + { url = "https://files.pythonhosted.org/packages/d5/fb/2726241a792b7f8a58789e5a63d1be9a5a4059206318fd0ff9485a578952/pyarrow-19.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a218670b26fb1bc74796458d97bcab072765f9b524f95b2fccad70158feb8b17", size = 42180380 }, + { url = "https://files.pythonhosted.org/packages/7d/09/7aef12446d8e7002dfc07bb7bc71f594c1d5844ca78b364a49f07efb65b1/pyarrow-19.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:66732e39eaa2247996a6b04c8aa33e3503d351831424cdf8d2e9a0582ac54b34", size = 40515021 }, + { url = "https://files.pythonhosted.org/packages/31/55/f05fc5608cc96060c2b24de505324d641888bd62d4eed2fa1dacd872a1e1/pyarrow-19.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e675a3ad4732b92d72e4d24009707e923cab76b0d088e5054914f11a797ebe44", size = 42067488 }, + { url = "https://files.pythonhosted.org/packages/f0/01/097653cec7a944c16313cb748a326771133c142034b252076bd84743b98d/pyarrow-19.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f094742275586cdd6b1a03655ccff3b24b2610c3af76f810356c4c71d24a2a6c", size = 25276726 }, + { url = "https://files.pythonhosted.org/packages/82/42/fba3a35bef5833bf88ed35e6a810dc1781236e1d4f808d2df824a7d21819/pyarrow-19.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8e3a839bf36ec03b4315dc924d36dcde5444a50066f1c10f8290293c0427b46a", size = 30711936 }, + { url = "https://files.pythonhosted.org/packages/88/7a/0da93a3eaaf251a30e32f3221e874263cdcd366c2cd6b7c05293aad91152/pyarrow-19.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ce42275097512d9e4e4a39aade58ef2b3798a93aa3026566b7892177c266f735", size = 32133182 }, + { url = "https://files.pythonhosted.org/packages/2f/df/fe43b1c50d3100d0de53f988344118bc20362d0de005f8a407454fa565f8/pyarrow-19.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9348a0137568c45601b031a8d118275069435f151cbb77e6a08a27e8125f59d4", size = 41145489 }, + { url = "https://files.pythonhosted.org/packages/45/bb/6f73b41b342a0342f2516a02db4aa97a4f9569cc35482a5c288090140cd4/pyarrow-19.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0144a712d990d60f7f42b7a31f0acaccf4c1e43e957f7b1ad58150d6f639c1", size = 42177823 }, + { url = "https://files.pythonhosted.org/packages/23/7b/f038a96f421e453a71bd7a0f78d62b1b2ae9bcac06ed51179ca532e6a0a2/pyarrow-19.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2a1a109dfda558eb011e5f6385837daffd920d54ca00669f7a11132d0b1e6042", size = 40530609 }, + { url = "https://files.pythonhosted.org/packages/b8/39/a2a6714b471c000e6dd6af4495dce00d7d1332351b8e3170dfb9f91dad1f/pyarrow-19.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:be686bf625aa7b9bada18defb3a3ea3981c1099697239788ff111d87f04cd263", size = 42081534 }, + { url = "https://files.pythonhosted.org/packages/6c/a3/8396fb06ca05d807e89980c177be26617aad15211ece3184e0caa730b8a6/pyarrow-19.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:239ca66d9a05844bdf5af128861af525e14df3c9591bcc05bac25918e650d3a2", size = 25281090 }, + { url = "https://files.pythonhosted.org/packages/bc/2e/152885f5ef421e80dae68b9c133ab261934f93a6d5e16b61d79c0ed597fb/pyarrow-19.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:a7bbe7109ab6198688b7079cbad5a8c22de4d47c4880d8e4847520a83b0d1b68", size = 30667964 }, + { url = "https://files.pythonhosted.org/packages/80/c2/08bbee9a8610a47c9a1466845f405baf53a639ddd947c5133d8ba13544b6/pyarrow-19.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:4624c89d6f777c580e8732c27bb8e77fd1433b89707f17c04af7635dd9638351", size = 32125039 }, + { url = "https://files.pythonhosted.org/packages/d2/56/06994df823212f5688d3c8bf4294928b12c9be36681872853655724d28c6/pyarrow-19.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b6d3ce4288793350dc2d08d1e184fd70631ea22a4ff9ea5c4ff182130249d9b", size = 41140729 }, + { url = "https://files.pythonhosted.org/packages/94/65/38ad577c98140a9db71e9e1e594b6adb58a7478a5afec6456a8ca2df7f70/pyarrow-19.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:450a7d27e840e4d9a384b5c77199d489b401529e75a3b7a3799d4cd7957f2f9c", size = 42202267 }, + { url = "https://files.pythonhosted.org/packages/b6/1f/966b722251a7354114ccbb71cf1a83922023e69efd8945ebf628a851ec4c/pyarrow-19.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a08e2a8a039a3f72afb67a6668180f09fddaa38fe0d21f13212b4aba4b5d2451", size = 40505858 }, + { url = "https://files.pythonhosted.org/packages/3b/5e/6bc81aa7fc9affc7d1c03b912fbcc984ca56c2a18513684da267715dab7b/pyarrow-19.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f43f5aef2a13d4d56adadae5720d1fed4c1356c993eda8b59dace4b5983843c1", size = 42084973 }, + { url = "https://files.pythonhosted.org/packages/53/c3/2f56da818b6a4758cbd514957c67bd0f078ebffa5390ee2e2bf0f9e8defc/pyarrow-19.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f672f5364b2d7829ef7c94be199bb88bf5661dd485e21d2d37de12ccb78a136", size = 25241976 }, + { url = "https://files.pythonhosted.org/packages/f5/b9/ba07ed3dd6b6e4f379b78e9c47c50c8886e07862ab7fa6339ac38622d755/pyarrow-19.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:cf3bf0ce511b833f7bc5f5bb3127ba731e97222023a444b7359f3a22e2a3b463", size = 30651291 }, + { url = "https://files.pythonhosted.org/packages/ad/10/0d304243c8277035298a68a70807efb76199c6c929bb3363c92ac9be6a0d/pyarrow-19.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:4d8b0c0de0a73df1f1bf439af1b60f273d719d70648e898bc077547649bb8352", size = 32100461 }, + { url = "https://files.pythonhosted.org/packages/8a/61/bcfc5182e11831bca3f849945b9b106e09fd10ded773dff466658e972a45/pyarrow-19.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92aff08e23d281c69835e4a47b80569242a504095ef6a6223c1f6bb8883431d", size = 41132491 }, + { url = "https://files.pythonhosted.org/packages/8e/87/2915a29049ec352dc69a967fbcbd76b0180319233de0daf8bd368df37099/pyarrow-19.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3b78eff5968a1889a0f3bc81ca57e1e19b75f664d9c61a42a604bf9d8402aae", size = 42192529 }, + { url = "https://files.pythonhosted.org/packages/48/18/44e5542b2707a8afaf78b5b88c608f261871ae77787eac07b7c679ca6f0f/pyarrow-19.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b34d3bde38eba66190b215bae441646330f8e9da05c29e4b5dd3e41bde701098", size = 40495363 }, + { url = "https://files.pythonhosted.org/packages/ba/d6/5096deb7599bbd20bc2768058fe23bc725b88eb41bee58303293583a2935/pyarrow-19.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5418d4d0fab3a0ed497bad21d17a7973aad336d66ad4932a3f5f7480d4ca0c04", size = 42074075 }, + { url = "https://files.pythonhosted.org/packages/2c/df/e3c839c04c284c9ec3d62b02a8c452b795d9b07b04079ab91ce33484d4c5/pyarrow-19.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e82c3d5e44e969c217827b780ed8faf7ac4c53f934ae9238872e749fa531f7c9", size = 25239803 }, + { url = "https://files.pythonhosted.org/packages/6a/d3/a6d4088e906c7b5d47792256212606d2ae679046dc750eee0ae167338e5c/pyarrow-19.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f208c3b58a6df3b239e0bb130e13bc7487ed14f39a9ff357b6415e3f6339b560", size = 30695401 }, + { url = "https://files.pythonhosted.org/packages/94/25/70040fd0e397dd1b937f459eaeeec942a76027357491dca0ada09d1322af/pyarrow-19.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:c751c1c93955b7a84c06794df46f1cec93e18610dcd5ab7d08e89a81df70a849", size = 32104680 }, + { url = "https://files.pythonhosted.org/packages/4e/f9/92783290cc0d80ca16d34b0c126305bfacca4b87dd889c8f16c6ef2a8fd7/pyarrow-19.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b903afaa5df66d50fc38672ad095806443b05f202c792694f3a604ead7c6ea6e", size = 41076754 }, + { url = "https://files.pythonhosted.org/packages/05/46/2c9870f50a495c72e2b8982ae29a9b1680707ea936edc0de444cec48f875/pyarrow-19.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22a4bc0937856263df8b94f2f2781b33dd7f876f787ed746608e06902d691a5", size = 42163133 }, + { url = "https://files.pythonhosted.org/packages/7b/2f/437922b902549228fb15814e8a26105bff2787ece466a8d886eb6699efad/pyarrow-19.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:5e8a28b918e2e878c918f6d89137386c06fe577cd08d73a6be8dafb317dc2d73", size = 40452210 }, + { url = "https://files.pythonhosted.org/packages/36/ef/1d7975053af9d106da973bac142d0d4da71b7550a3576cc3e0b3f444d21a/pyarrow-19.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:29cd86c8001a94f768f79440bf83fee23963af5e7bc68ce3a7e5f120e17edf89", size = 42077618 }, + { url = "https://files.pythonhosted.org/packages/59/13/e39417005ee632e131d0246cf5c1149618a55554ccdf2a4d887065e672a7/pyarrow-19.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:c0423393e4a07ff6fea08feb44153302dd261d0551cc3b538ea7a5dc853af43a", size = 30698254 }, + { url = "https://files.pythonhosted.org/packages/06/87/1f9d7df296dd5c065e52ae3e9070dfe611f6bd97e90f28b6a45c410dcb67/pyarrow-19.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:718947fb6d82409013a74b176bf93e0f49ef952d8a2ecd068fecd192a97885b7", size = 32114467 }, + { url = "https://files.pythonhosted.org/packages/b2/b1/9e7babf5d469bd35f1d062a04721ead72456101f6d851a2e8a43bb07a580/pyarrow-19.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1c162c4660e0978411a4761f91113dde8da3433683efa473501254563dcbe8", size = 41157781 }, + { url = "https://files.pythonhosted.org/packages/9f/25/fa8e882a6c06e6d8dd640d3acce3912d7c39358940eb0c7b3c8b962457d0/pyarrow-19.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c73268cf557e688efb60f1ccbc7376f7e18cd8e2acae9e663e98b194c40c1a2d", size = 42190773 }, + { url = "https://files.pythonhosted.org/packages/b8/d6/7fd60aa79cada815306d9804403386b06893ef63e73876174717a62002c4/pyarrow-19.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:edfe6d3916e915ada9acc4e48f6dafca7efdbad2e6283db6fd9385a1b23055f1", size = 40528310 }, + { url = "https://files.pythonhosted.org/packages/0e/f1/c1ec7620a5768b8c7f9083572fda7b05b77ea083c3400fbd9e4ae40e63bd/pyarrow-19.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:da410b70a7ab8eb524112f037a7a35da7128b33d484f7671a264a4c224ac131d", size = 42080774 }, + { url = "https://files.pythonhosted.org/packages/c4/28/c51c9af2703b5a592d1b66546611b24de8ca01e04c3f5da769c3318bca6c/pyarrow-19.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:597360ffc71fc8cceea1aec1fb60cb510571a744fffc87db33d551d5de919bec", size = 25464978 }, +] + +[[package]] +name = "pycountry" +version = "24.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/57/c389fa68c50590881a75b7883eeb3dc15e9e73a0fdc001cdd45c13290c92/pycountry-24.6.1.tar.gz", hash = "sha256:b61b3faccea67f87d10c1f2b0fc0be714409e8fcdcc1315613174f6466c10221", size = 6043910 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/ec/1fb891d8a2660716aadb2143235481d15ed1cbfe3ad669194690b0604492/pycountry-24.6.1-py3-none-any.whl", hash = "sha256:f1a4fb391cd7214f8eefd39556d740adcc233c778a27f8942c8dca351d6ce06f", size = 6335189 }, +] + +[[package]] +name = "pydantic" +version = "2.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, + { url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475 }, + { url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112 }, + { url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780 }, + { url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943 }, + { url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492 }, + { url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714 }, + { url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163 }, + { url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217 }, + { url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899 }, + { url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726 }, + { url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219 }, + { url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, + { url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733 }, + { url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375 }, + { url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307 }, + { url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971 }, + { url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616 }, + { url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943 }, + { url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654 }, + { url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292 }, + { url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-benchmark" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/d0/a8bd08d641b393db3be3819b03e2d9bb8760ca8479080a26a5f6e540e99c/pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105", size = 337810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/d6/b41653199ea09d5969d4e385df9bbfd9a100f28ca7e824ce7c0a016e3053/pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89", size = 44259 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "ray" +version = "2.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosignal" }, + { name = "click" }, + { name = "filelock" }, + { name = "frozenlist" }, + { name = "jsonschema" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "requests" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/42/492dc35c112c5adbcf258066d739f897f484b6e2aff29b28dd9ebc9832d4/ray-2.40.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:064af8bc52cc988c82470b8e76e5df417737fa7c1d87f597a892c69eb4ec3caa", size = 67061821 }, + { url = "https://files.pythonhosted.org/packages/07/c2/0847df9d81524ceafe0fafcddce9e4e4799501807687759d2c48f1b34f43/ray-2.40.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45beb4019cd20b6cb10572d8012c771bccd623f544a669da6797ccf993c4bb33", size = 64422550 }, + { url = "https://files.pythonhosted.org/packages/62/e7/cf468bfb109d904cc5b7650d890f7c4284842d72acf65b1ae05a5c02c2e5/ray-2.40.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:6cede5fbf7de4fae22cebe2c6977aaf3c85fde6f7de2aa10c46992cf24ea8bda", size = 65916634 }, + { url = "https://files.pythonhosted.org/packages/31/73/6763d7f87756816698fa5f841ba590a869e821a46c0a98d848055ea831bb/ray-2.40.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:f6eab11dc8490f88e78e06aa645905b259cde1fa03b15e8426155c4782ba0bbe", size = 66845129 }, + { url = "https://files.pythonhosted.org/packages/1f/cd/0107c782d8511ae100a49da95dc1951c1da67a467e204671ae730f6a5401/ray-2.40.0-cp310-cp310-win_amd64.whl", hash = "sha256:f83cda1ecceb7abe021cd377f0c503596f26d2d66cdff13c1089a06c8b780c23", size = 25295665 }, + { url = "https://files.pythonhosted.org/packages/a9/fc/811e88c982ea755c6351bd5f2c410cd8526695069a708d9aa3c9410a8dc3/ray-2.40.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:dac89bb2cb889c19549a4ac0383492e7550f3e63b78b629a3118e8b91e4e82f3", size = 67000351 }, + { url = "https://files.pythonhosted.org/packages/72/fb/f048a8580d97429ab3b60b13844500132e756ceaa5bb643087a0d46d9c9b/ray-2.40.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3e4efdf8aebff6e71391c2d5dd66bb45835f2d6d629ac03a3e21e2d4283e2311", size = 64359351 }, + { url = "https://files.pythonhosted.org/packages/99/34/e16767825432cf7553badba349c80344f4a86b227e5ff2ef4ae20ea40b6c/ray-2.40.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:c776f131e5d0a169a98ab8021c5796f52bf48fcfc6c44ffbd2a9d090fe10748a", size = 66058000 }, + { url = "https://files.pythonhosted.org/packages/55/4e/a46f514a574c33f699f729e26866475bd725287cf9ba3adf55883d6f809d/ray-2.40.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:71711cbf2c156213fd49b0f9cc93180a7ba424110070a34bdea3dc09527f31df", size = 66967160 }, + { url = "https://files.pythonhosted.org/packages/9b/fe/01adb0316658feab19ee48badb3dd232ae681633f47b2d6c191c4df14ebe/ray-2.40.0-cp311-cp311-win_amd64.whl", hash = "sha256:532321132618983366e39aeb4cc7867cf7241b0b1e49ee44b01d2aee9923e422", size = 25236232 }, + { url = "https://files.pythonhosted.org/packages/86/01/994daffa3516e582cd8eacbe958938d2128b79284f2387ff6ebc53b970e2/ray-2.40.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:6992922fe91a90b5cc97d9f05ca51b64d72cd644db7ad55caa936be9a6098cce", size = 66981507 }, + { url = "https://files.pythonhosted.org/packages/46/ce/bcf8416ce4137ce4bc7e0ebdedb2200a598f7beeca1a1004bc45478bb4b0/ray-2.40.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:28329e7a7471610a475d3bb09a4c1b31abcf3596cee25c4254f8d01ad161ba84", size = 64345984 }, + { url = "https://files.pythonhosted.org/packages/8a/96/dbcb31dd8ff74b81dbfe9332f49941c716daffaffdc3efb1c30ca5a1b4cd/ray-2.40.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:8ea05221fa48e32c652c29498d320e90134b3a012421006af98965097dd1cc3b", size = 66071071 }, + { url = "https://files.pythonhosted.org/packages/82/ad/2eaf308c64e39704629863720c59b82e1ceed814fca319b8585deb92bb74/ray-2.40.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:674755814f5692306c554cadbc24015af823dc0516e34bdef24ccac9d7a656e3", size = 67018887 }, + { url = "https://files.pythonhosted.org/packages/21/c4/23616495341e01c97d5b72d07555ab8908e87190cf2f53b4f029cb65bcf0/ray-2.40.0-cp312-cp312-win_amd64.whl", hash = "sha256:bbc01d773cbc43e3efa462ec28ee4c0cacc50f098078332fb45b1ab38eaf9b5d", size = 25223820 }, + { url = "https://files.pythonhosted.org/packages/44/6f/ecaf4363063c1a1f7e5d611083e459919c33815b32008c25eeb853b8da1f/ray-2.40.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:27292bf8921dd69757e7581644afcd3ccae13d6f10f3841f5523ae82b6612f4b", size = 67076882 }, + { url = "https://files.pythonhosted.org/packages/5c/7f/d6da83e7e2357770f482f9f521d15b46ea13116aeb0ba0569e3986d11658/ray-2.40.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b74ca43d0c4ccdcaefbf1e7d26aabb1c0d20f825688a9fd7134ba918bda8442", size = 64436021 }, + { url = "https://files.pythonhosted.org/packages/87/79/95634cfe3fd0457a662e24967391597a5074b5aecfab8c453221481c5dfa/ray-2.40.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5eb7a203f58defedff0dc53f78a4e1431d040b2b8458548704979c0113f3b892", size = 65938206 }, + { url = "https://files.pythonhosted.org/packages/1f/c9/09de884f8e62efe0251f8915380f322d92e49f7f7e471c2f15471c40b831/ray-2.40.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:a36a20a3b936b36d14fab031222f92e3c5e731d7db6bb183ca4fba6d0ce3f52a", size = 66864869 }, + { url = "https://files.pythonhosted.org/packages/ae/57/6742621756a7bce8ac99f9d30e2e7461e1d58716d5fd274a0a88657abd60/ray-2.40.0-cp39-cp39-win_amd64.whl", hash = "sha256:fbe9cd3e076dea676afd57caf19b2897a67ecdf14a542c03864800966cf2aec9", size = 25303127 }, +] + +[[package]] +name = "referencing" +version = "0.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/32/fd98246df7a0f309b58cae68b10b6b219ef2eb66747f00dfb34422687087/referencing-0.36.1.tar.gz", hash = "sha256:ca2e6492769e3602957e9b831b94211599d2aade9477f5d44110d2530cf9aade", size = 74661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/fa/9f193ef0c9074b659009f06d7cbacc6f25b072044815bcf799b76533dbb8/referencing-0.36.1-py3-none-any.whl", hash = "sha256:363d9c65f080d0d70bc41c721dce3c7f3e77fc09f269cd5c8813da18069a6794", size = 26777 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, + { url = "https://files.pythonhosted.org/packages/89/23/c4a86df398e57e26f93b13ae63acce58771e04bdde86092502496fa57f9c/regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", size = 482682 }, + { url = "https://files.pythonhosted.org/packages/3c/8b/45c24ab7a51a1658441b961b86209c43e6bb9d39caf1e63f46ce6ea03bc7/regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", size = 287679 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/598de10b17fdafc452d11f7dada11c3be4e379a8671393e4e3da3c4070df/regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", size = 284578 }, + { url = "https://files.pythonhosted.org/packages/49/70/c7eaa219efa67a215846766fde18d92d54cb590b6a04ffe43cef30057622/regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", size = 782012 }, + { url = "https://files.pythonhosted.org/packages/89/e5/ef52c7eb117dd20ff1697968219971d052138965a4d3d9b95e92e549f505/regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", size = 820580 }, + { url = "https://files.pythonhosted.org/packages/5f/3f/9f5da81aff1d4167ac52711acf789df13e789fe6ac9545552e49138e3282/regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", size = 809110 }, + { url = "https://files.pythonhosted.org/packages/86/44/2101cc0890c3621b90365c9ee8d7291a597c0722ad66eccd6ffa7f1bcc09/regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", size = 780919 }, + { url = "https://files.pythonhosted.org/packages/ce/2e/3e0668d8d1c7c3c0d397bf54d92fc182575b3a26939aed5000d3cc78760f/regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", size = 771515 }, + { url = "https://files.pythonhosted.org/packages/a6/49/1bc4584254355e3dba930a3a2fd7ad26ccba3ebbab7d9100db0aff2eedb0/regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", size = 696957 }, + { url = "https://files.pythonhosted.org/packages/c8/dd/42879c1fc8a37a887cd08e358af3d3ba9e23038cd77c7fe044a86d9450ba/regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", size = 768088 }, + { url = "https://files.pythonhosted.org/packages/89/96/c05a0fe173cd2acd29d5e13c1adad8b706bcaa71b169e1ee57dcf2e74584/regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", size = 774752 }, + { url = "https://files.pythonhosted.org/packages/b5/f3/a757748066255f97f14506483436c5f6aded7af9e37bca04ec30c90ca683/regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", size = 838862 }, + { url = "https://files.pythonhosted.org/packages/5c/93/c6d2092fd479dcaeea40fc8fa673822829181ded77d294a7f950f1dda6e2/regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", size = 842622 }, + { url = "https://files.pythonhosted.org/packages/ff/9c/daa99532c72f25051a90ef90e1413a8d54413a9e64614d9095b0c1c154d0/regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", size = 772713 }, + { url = "https://files.pythonhosted.org/packages/13/5d/61a533ccb8c231b474ac8e3a7d70155b00dfc61af6cafdccd1947df6d735/regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", size = 261756 }, + { url = "https://files.pythonhosted.org/packages/dc/7b/e59b7f7c91ae110d154370c24133f947262525b5d6406df65f23422acc17/regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", size = 274110 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "responses" +version = "0.25.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/63/759996eea0f17e8dc4c9ea9c60765292d28a7750bdbee073ad55d83caa57/responses-0.25.6.tar.gz", hash = "sha256:eae7ce61a9603004e76c05691e7c389e59652d91e94b419623c12bbfb8e331d8", size = 79145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c4/8d23584b3a3471ea6f5a18cfb035e11eeb9fa9b3112d901477c6ad10cc4e/responses-0.25.6-py3-none-any.whl", hash = "sha256:9cac8f21e1193bb150ec557875377e41ed56248aed94e4567ed644db564bacf1", size = 34730 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rpds-py" +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", size = 26771 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/2a/ead1d09e57449b99dcc190d8d2323e3a167421d8f8fdf0f217c6f6befe47/rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", size = 359514 }, + { url = "https://files.pythonhosted.org/packages/8f/7e/1254f406b7793b586c68e217a6a24ec79040f85e030fff7e9049069284f4/rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", size = 349031 }, + { url = "https://files.pythonhosted.org/packages/aa/da/17c6a2c73730d426df53675ff9cc6653ac7a60b6438d03c18e1c822a576a/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", size = 381485 }, + { url = "https://files.pythonhosted.org/packages/aa/13/2dbacd820466aa2a3c4b747afb18d71209523d353cf865bf8f4796c969ea/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", size = 386794 }, + { url = "https://files.pythonhosted.org/packages/6d/62/96905d0a35ad4e4bc3c098b2f34b2e7266e211d08635baa690643d2227be/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", size = 423523 }, + { url = "https://files.pythonhosted.org/packages/eb/1b/d12770f2b6a9fc2c3ec0d810d7d440f6d465ccd8b7f16ae5385952c28b89/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", size = 446695 }, + { url = "https://files.pythonhosted.org/packages/4d/cf/96f1fd75512a017f8e07408b6d5dbeb492d9ed46bfe0555544294f3681b3/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", size = 381959 }, + { url = "https://files.pythonhosted.org/packages/ab/f0/d1c5b501c8aea85aeb938b555bfdf7612110a2f8cdc21ae0482c93dd0c24/rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", size = 410420 }, + { url = "https://files.pythonhosted.org/packages/33/3b/45b6c58fb6aad5a569ae40fb890fc494c6b02203505a5008ee6dc68e65f7/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", size = 557620 }, + { url = "https://files.pythonhosted.org/packages/83/62/3fdd2d3d47bf0bb9b931c4c73036b4ab3ec77b25e016ae26fab0f02be2af/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", size = 584202 }, + { url = "https://files.pythonhosted.org/packages/04/f2/5dced98b64874b84ca824292f9cee2e3f30f3bcf231d15a903126684f74d/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", size = 552787 }, + { url = "https://files.pythonhosted.org/packages/67/13/2273dea1204eda0aea0ef55145da96a9aa28b3f88bb5c70e994f69eda7c3/rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", size = 220088 }, + { url = "https://files.pythonhosted.org/packages/4e/80/8c8176b67ad7f4a894967a7a4014ba039626d96f1d4874d53e409b58d69f/rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", size = 231737 }, + { url = "https://files.pythonhosted.org/packages/15/ad/8d1ddf78f2805a71253fcd388017e7b4a0615c22c762b6d35301fef20106/rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f", size = 359773 }, + { url = "https://files.pythonhosted.org/packages/c8/75/68c15732293a8485d79fe4ebe9045525502a067865fa4278f178851b2d87/rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a", size = 349214 }, + { url = "https://files.pythonhosted.org/packages/3c/4c/7ce50f3070083c2e1b2bbd0fb7046f3da55f510d19e283222f8f33d7d5f4/rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5", size = 380477 }, + { url = "https://files.pythonhosted.org/packages/9a/e9/835196a69cb229d5c31c13b8ae603bd2da9a6695f35fe4270d398e1db44c/rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb", size = 386171 }, + { url = "https://files.pythonhosted.org/packages/f9/8e/33fc4eba6683db71e91e6d594a2cf3a8fbceb5316629f0477f7ece5e3f75/rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2", size = 422676 }, + { url = "https://files.pythonhosted.org/packages/37/47/2e82d58f8046a98bb9497a8319604c92b827b94d558df30877c4b3c6ccb3/rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0", size = 446152 }, + { url = "https://files.pythonhosted.org/packages/e1/78/79c128c3e71abbc8e9739ac27af11dc0f91840a86fce67ff83c65d1ba195/rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1", size = 381300 }, + { url = "https://files.pythonhosted.org/packages/c9/5b/2e193be0e8b228c1207f31fa3ea79de64dadb4f6a4833111af8145a6bc33/rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d", size = 409636 }, + { url = "https://files.pythonhosted.org/packages/c2/3f/687c7100b762d62186a1c1100ffdf99825f6fa5ea94556844bbbd2d0f3a9/rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648", size = 556708 }, + { url = "https://files.pythonhosted.org/packages/8c/a2/c00cbc4b857e8b3d5e7f7fc4c81e23afd8c138b930f4f3ccf9a41a23e9e4/rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74", size = 583554 }, + { url = "https://files.pythonhosted.org/packages/d0/08/696c9872cf56effdad9ed617ac072f6774a898d46b8b8964eab39ec562d2/rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a", size = 552105 }, + { url = "https://files.pythonhosted.org/packages/18/1f/4df560be1e994f5adf56cabd6c117e02de7c88ee238bb4ce03ed50da9d56/rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64", size = 220199 }, + { url = "https://files.pythonhosted.org/packages/b8/1b/c29b570bc5db8237553002788dc734d6bd71443a2ceac2a58202ec06ef12/rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c", size = 231775 }, + { url = "https://files.pythonhosted.org/packages/75/47/3383ee3bd787a2a5e65a9b9edc37ccf8505c0a00170e3a5e6ea5fbcd97f7/rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", size = 352334 }, + { url = "https://files.pythonhosted.org/packages/40/14/aa6400fa8158b90a5a250a77f2077c0d0cd8a76fce31d9f2b289f04c6dec/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", size = 342111 }, + { url = "https://files.pythonhosted.org/packages/7d/06/395a13bfaa8a28b302fb433fb285a67ce0ea2004959a027aea8f9c52bad4/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", size = 384286 }, + { url = "https://files.pythonhosted.org/packages/43/52/d8eeaffab047e6b7b7ef7f00d5ead074a07973968ffa2d5820fa131d7852/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e", size = 391739 }, + { url = "https://files.pythonhosted.org/packages/83/31/52dc4bde85c60b63719610ed6f6d61877effdb5113a72007679b786377b8/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", size = 427306 }, + { url = "https://files.pythonhosted.org/packages/70/d5/1bab8e389c2261dba1764e9e793ed6830a63f830fdbec581a242c7c46bda/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", size = 442717 }, + { url = "https://files.pythonhosted.org/packages/82/a1/a45f3e30835b553379b3a56ea6c4eb622cf11e72008229af840e4596a8ea/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", size = 385721 }, + { url = "https://files.pythonhosted.org/packages/a6/27/780c942de3120bdd4d0e69583f9c96e179dfff082f6ecbb46b8d6488841f/rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", size = 415824 }, + { url = "https://files.pythonhosted.org/packages/94/0b/aa0542ca88ad20ea719b06520f925bae348ea5c1fdf201b7e7202d20871d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", size = 561227 }, + { url = "https://files.pythonhosted.org/packages/0d/92/3ed77d215f82c8f844d7f98929d56cc321bb0bcfaf8f166559b8ec56e5f1/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", size = 587424 }, + { url = "https://files.pythonhosted.org/packages/09/42/cacaeb047a22cab6241f107644f230e2935d4efecf6488859a7dd82fc47d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", size = 555953 }, + { url = "https://files.pythonhosted.org/packages/e6/52/c921dc6d5f5d45b212a456c1f5b17df1a471127e8037eb0972379e39dff4/rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", size = 221339 }, + { url = "https://files.pythonhosted.org/packages/f2/c7/f82b5be1e8456600395366f86104d1bd8d0faed3802ad511ef6d60c30d98/rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", size = 235786 }, + { url = "https://files.pythonhosted.org/packages/d0/bf/36d5cc1f2c609ae6e8bf0fc35949355ca9d8790eceb66e6385680c951e60/rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", size = 351657 }, + { url = "https://files.pythonhosted.org/packages/24/2a/f1e0fa124e300c26ea9382e59b2d582cba71cedd340f32d1447f4f29fa4e/rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", size = 341829 }, + { url = "https://files.pythonhosted.org/packages/cf/c2/0da1231dd16953845bed60d1a586fcd6b15ceaeb965f4d35cdc71f70f606/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", size = 384220 }, + { url = "https://files.pythonhosted.org/packages/c7/73/a4407f4e3a00a9d4b68c532bf2d873d6b562854a8eaff8faa6133b3588ec/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", size = 391009 }, + { url = "https://files.pythonhosted.org/packages/a9/c3/04b7353477ab360fe2563f5f0b176d2105982f97cd9ae80a9c5a18f1ae0f/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", size = 426989 }, + { url = "https://files.pythonhosted.org/packages/8d/e6/e4b85b722bcf11398e17d59c0f6049d19cd606d35363221951e6d625fcb0/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", size = 441544 }, + { url = "https://files.pythonhosted.org/packages/27/fc/403e65e56f65fff25f2973216974976d3f0a5c3f30e53758589b6dc9b79b/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", size = 385179 }, + { url = "https://files.pythonhosted.org/packages/57/9b/2be9ff9700d664d51fd96b33d6595791c496d2778cb0b2a634f048437a55/rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", size = 415103 }, + { url = "https://files.pythonhosted.org/packages/bb/a5/03c2ad8ca10994fcf22dd2150dd1d653bc974fa82d9a590494c84c10c641/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", size = 560916 }, + { url = "https://files.pythonhosted.org/packages/ba/2e/be4fdfc8b5b576e588782b56978c5b702c5a2307024120d8aeec1ab818f0/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", size = 587062 }, + { url = "https://files.pythonhosted.org/packages/67/e0/2034c221937709bf9c542603d25ad43a68b4b0a9a0c0b06a742f2756eb66/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", size = 555734 }, + { url = "https://files.pythonhosted.org/packages/ea/ce/240bae07b5401a22482b58e18cfbabaa392409b2797da60223cca10d7367/rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", size = 220663 }, + { url = "https://files.pythonhosted.org/packages/cb/f0/d330d08f51126330467edae2fa4efa5cec8923c87551a79299380fdea30d/rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", size = 235503 }, + { url = "https://files.pythonhosted.org/packages/f7/c4/dbe1cc03df013bf2feb5ad00615038050e7859f381e96fb5b7b4572cd814/rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", size = 347698 }, + { url = "https://files.pythonhosted.org/packages/a4/3a/684f66dd6b0f37499cad24cd1c0e523541fd768576fa5ce2d0a8799c3cba/rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", size = 337330 }, + { url = "https://files.pythonhosted.org/packages/82/eb/e022c08c2ce2e8f7683baa313476492c0e2c1ca97227fe8a75d9f0181e95/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", size = 380022 }, + { url = "https://files.pythonhosted.org/packages/e4/21/5a80e653e4c86aeb28eb4fea4add1f72e1787a3299687a9187105c3ee966/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", size = 390754 }, + { url = "https://files.pythonhosted.org/packages/37/a4/d320a04ae90f72d080b3d74597074e62be0a8ecad7d7321312dfe2dc5a6a/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", size = 423840 }, + { url = "https://files.pythonhosted.org/packages/87/70/674dc47d93db30a6624279284e5631be4c3a12a0340e8e4f349153546728/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", size = 438970 }, + { url = "https://files.pythonhosted.org/packages/3f/64/9500f4d66601d55cadd21e90784cfd5d5f4560e129d72e4339823129171c/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", size = 383146 }, + { url = "https://files.pythonhosted.org/packages/4d/45/630327addb1d17173adcf4af01336fd0ee030c04798027dfcb50106001e0/rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", size = 408294 }, + { url = "https://files.pythonhosted.org/packages/5f/ef/8efb3373cee54ea9d9980b772e5690a0c9e9214045a4e7fa35046e399fee/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", size = 556345 }, + { url = "https://files.pythonhosted.org/packages/54/01/151d3b9ef4925fc8f15bfb131086c12ec3c3d6dd4a4f7589c335bf8e85ba/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", size = 582292 }, + { url = "https://files.pythonhosted.org/packages/30/89/35fc7a6cdf3477d441c7aca5e9bbf5a14e0f25152aed7f63f4e0b141045d/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", size = 553855 }, + { url = "https://files.pythonhosted.org/packages/8f/e0/830c02b2457c4bd20a8c5bb394d31d81f57fbefce2dbdd2e31feff4f7003/rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", size = 219100 }, + { url = "https://files.pythonhosted.org/packages/f8/30/7ac943f69855c2db77407ae363484b915d861702dbba1aa82d68d57f42be/rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", size = 233794 }, + { url = "https://files.pythonhosted.org/packages/db/0f/a8ad17ddac7c880f48d5da50733dd25bfc35ba2be1bec9f23453e8c7a123/rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea", size = 359735 }, + { url = "https://files.pythonhosted.org/packages/0c/41/430903669397ea3ee76865e0b53ea236e8dc0ffbecde47b2c4c783ad6759/rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e", size = 348724 }, + { url = "https://files.pythonhosted.org/packages/c9/5c/3496f4f0ee818297544f2d5f641c49dde8ae156392e6834b79c0609ba006/rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d", size = 381782 }, + { url = "https://files.pythonhosted.org/packages/b6/dc/db0523ce0cd16ce579185cc9aa9141992de956d0a9c469ecfd1fb5d54ddc/rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3", size = 387036 }, + { url = "https://files.pythonhosted.org/packages/85/2a/9525c2427d2c257f877348918136a5d4e1b945c205a256e53bec61e54551/rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091", size = 424566 }, + { url = "https://files.pythonhosted.org/packages/b9/1c/f8c012a39794b84069635709f559c0309103d5d74b3f5013916e6ca4f174/rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e", size = 447203 }, + { url = "https://files.pythonhosted.org/packages/93/f5/c1c772364570d35b98ba64f36ec90c3c6d0b932bc4d8b9b4efef6dc64b07/rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543", size = 382283 }, + { url = "https://files.pythonhosted.org/packages/10/06/f94f61313f94fc75c3c3aa74563f80bbd990e5b25a7c1a38cee7d5d0309b/rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d", size = 410022 }, + { url = "https://files.pythonhosted.org/packages/3f/b0/37ab416a9528419920dfb64886c220f58fcbd66b978e0a91b66e9ee9a993/rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99", size = 557817 }, + { url = "https://files.pythonhosted.org/packages/2c/5d/9daa18adcd676dd3b2817c8a7cec3f3ebeeb0ce0d05a1b63bf994fc5114f/rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831", size = 585099 }, + { url = "https://files.pythonhosted.org/packages/41/3f/ad4e58035d3f848410aa3d59857b5f238bafab81c8b4a844281f80445d62/rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520", size = 552818 }, + { url = "https://files.pythonhosted.org/packages/b8/19/123acae8f4cab3c9463097c3ced3cc87c46f405056e249c874940e045309/rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9", size = 220246 }, + { url = "https://files.pythonhosted.org/packages/8b/8d/9db93e48d96ace1f6713c71ce72e2d94b71d82156c37b6a54e0930486f00/rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c", size = 231932 }, + { url = "https://files.pythonhosted.org/packages/8b/63/e29f8ee14fcf383574f73b6bbdcbec0fbc2e5fc36b4de44d1ac389b1de62/rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", size = 360786 }, + { url = "https://files.pythonhosted.org/packages/d3/e0/771ee28b02a24e81c8c0e645796a371350a2bb6672753144f36ae2d2afc9/rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", size = 350589 }, + { url = "https://files.pythonhosted.org/packages/cf/49/abad4c4a1e6f3adf04785a99c247bfabe55ed868133e2d1881200aa5d381/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", size = 381848 }, + { url = "https://files.pythonhosted.org/packages/3a/7d/f4bc6d6fbe6af7a0d2b5f2ee77079efef7c8528712745659ec0026888998/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", size = 387879 }, + { url = "https://files.pythonhosted.org/packages/13/b0/575c797377fdcd26cedbb00a3324232e4cb2c5d121f6e4b0dbf8468b12ef/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", size = 423916 }, + { url = "https://files.pythonhosted.org/packages/54/78/87157fa39d58f32a68d3326f8a81ad8fb99f49fe2aa7ad9a1b7d544f9478/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", size = 448410 }, + { url = "https://files.pythonhosted.org/packages/59/69/860f89996065a88be1b6ff2d60e96a02b920a262d8aadab99e7903986597/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", size = 382841 }, + { url = "https://files.pythonhosted.org/packages/bd/d7/bc144e10d27e3cb350f98df2492a319edd3caaf52ddfe1293f37a9afbfd7/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", size = 409662 }, + { url = "https://files.pythonhosted.org/packages/14/2a/6bed0b05233c291a94c7e89bc76ffa1c619d4e1979fbfe5d96024020c1fb/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", size = 558221 }, + { url = "https://files.pythonhosted.org/packages/11/23/cd8f566de444a137bc1ee5795e47069a947e60810ba4152886fe5308e1b7/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", size = 583780 }, + { url = "https://files.pythonhosted.org/packages/8d/63/79c3602afd14d501f751e615a74a59040328da5ef29ed5754ae80d236b84/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", size = 553619 }, + { url = "https://files.pythonhosted.org/packages/9f/2e/c5c1689e80298d4e94c75b70faada4c25445739d91b94c211244a3ed7ed1/rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", size = 233338 }, + { url = "https://files.pythonhosted.org/packages/bc/b7/d2c205723e3b4d75b03215694f0297a1b4b395bf834cb5896ad9bbb90f90/rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c", size = 360594 }, + { url = "https://files.pythonhosted.org/packages/d8/8f/c3515f5234cf6055046d4cfe9c80a3742a20acfa7d0b1b290f0d7f56a8db/rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055", size = 349594 }, + { url = "https://files.pythonhosted.org/packages/6b/98/5b487cb06afc484befe350c87fda37f4ce11333f04f3380aba43dcf5bce2/rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723", size = 381138 }, + { url = "https://files.pythonhosted.org/packages/5e/3a/12308d2c51b3fdfc173619943b7dc5ba41b4850c47112eeda38d9c54ed12/rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728", size = 387828 }, + { url = "https://files.pythonhosted.org/packages/17/b2/c242241ab5a2a206e093f24ccbfa519c4bbf10a762ac90bffe1766c225e0/rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b", size = 424634 }, + { url = "https://files.pythonhosted.org/packages/d5/c7/52a1b15012139f3ba740f291f1d03c6b632938ba61bc605f24c101952493/rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d", size = 447862 }, + { url = "https://files.pythonhosted.org/packages/55/3e/4d3ed8fd01bad77e8ed101116fe63b03f1011940d9596a8f4d82ac80cacd/rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11", size = 382506 }, + { url = "https://files.pythonhosted.org/packages/30/78/df59d6f92470a84369a3757abeae1cfd7f7239c8beb6d948949bf78317d2/rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f", size = 410534 }, + { url = "https://files.pythonhosted.org/packages/38/97/ea45d1edd9b753b20084b52dd5db6ee5e1ac3e036a27149972398a413858/rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca", size = 557453 }, + { url = "https://files.pythonhosted.org/packages/08/cd/3a1b35eb9da27ffbb981cfffd32a01c7655c4431ccb278cb3064f8887462/rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3", size = 584412 }, + { url = "https://files.pythonhosted.org/packages/87/91/31d1c5aeb1606f71188259e0ba6ed6f5c21a3c72f58b51db6a8bd0aa2b5d/rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7", size = 553446 }, + { url = "https://files.pythonhosted.org/packages/e7/ad/03b5ccd1ab492c9dece85b3bf1c96453ab8c47983936fae6880f688f60b3/rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6", size = 233013 }, +] + +[[package]] +name = "safetensors" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/4f/2ef9ef1766f8c194b01b67a63a444d2e557c8fe1d82faf3ebd85f370a917/safetensors-0.5.2.tar.gz", hash = "sha256:cb4a8d98ba12fa016f4241932b1fc5e702e5143f5374bba0bbcf7ddc1c4cf2b8", size = 66957 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/d1/017e31e75e274492a11a456a9e7c171f8f7911fe50735b4ec6ff37221220/safetensors-0.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:45b6092997ceb8aa3801693781a71a99909ab9cc776fbc3fa9322d29b1d3bef2", size = 427067 }, + { url = "https://files.pythonhosted.org/packages/24/84/e9d3ff57ae50dd0028f301c9ee064e5087fe8b00e55696677a0413c377a7/safetensors-0.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6d0d6a8ee2215a440e1296b843edf44fd377b055ba350eaba74655a2fe2c4bae", size = 408856 }, + { url = "https://files.pythonhosted.org/packages/f1/1d/fe95f5dd73db16757b11915e8a5106337663182d0381811c81993e0014a9/safetensors-0.5.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86016d40bcaa3bcc9a56cd74d97e654b5f4f4abe42b038c71e4f00a089c4526c", size = 450088 }, + { url = "https://files.pythonhosted.org/packages/cf/21/e527961b12d5ab528c6e47b92d5f57f33563c28a972750b238b871924e49/safetensors-0.5.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:990833f70a5f9c7d3fc82c94507f03179930ff7d00941c287f73b6fcbf67f19e", size = 458966 }, + { url = "https://files.pythonhosted.org/packages/a5/8b/1a037d7a57f86837c0b41905040369aea7d8ca1ec4b2a77592372b2ec380/safetensors-0.5.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dfa7c2f3fe55db34eba90c29df94bcdac4821043fc391cb5d082d9922013869", size = 509915 }, + { url = "https://files.pythonhosted.org/packages/61/3d/03dd5cfd33839df0ee3f4581a20bd09c40246d169c0e4518f20b21d5f077/safetensors-0.5.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46ff2116150ae70a4e9c490d2ab6b6e1b1b93f25e520e540abe1b81b48560c3a", size = 527664 }, + { url = "https://files.pythonhosted.org/packages/c5/dc/8952caafa9a10a3c0f40fa86bacf3190ae7f55fa5eef87415b97b29cb97f/safetensors-0.5.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab696dfdc060caffb61dbe4066b86419107a24c804a4e373ba59be699ebd8d5", size = 461978 }, + { url = "https://files.pythonhosted.org/packages/60/da/82de1fcf1194e3dbefd4faa92dc98b33c06bed5d67890e0962dd98e18287/safetensors-0.5.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03c937100f38c9ff4c1507abea9928a6a9b02c9c1c9c3609ed4fb2bf413d4975", size = 491253 }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d90e273c25f90c3ba1b0196a972003786f04c39e302fbd6649325b1272bb/safetensors-0.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a00e737948791b94dad83cf0eafc09a02c4d8c2171a239e8c8572fe04e25960e", size = 628644 }, + { url = "https://files.pythonhosted.org/packages/70/3c/acb23e05aa34b4f5edd2e7f393f8e6480fbccd10601ab42cd03a57d4ab5f/safetensors-0.5.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d3a06fae62418ec8e5c635b61a8086032c9e281f16c63c3af46a6efbab33156f", size = 721648 }, + { url = "https://files.pythonhosted.org/packages/71/45/eaa3dba5253a7c6931230dc961641455710ab231f8a89cb3c4c2af70f8c8/safetensors-0.5.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1506e4c2eda1431099cebe9abf6c76853e95d0b7a95addceaa74c6019c65d8cf", size = 659588 }, + { url = "https://files.pythonhosted.org/packages/b0/71/2f9851164f821064d43b481ddbea0149c2d676c4f4e077b178e7eeaa6660/safetensors-0.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c5b5d9da594f638a259fca766046f44c97244cc7ab8bef161b3e80d04becc76", size = 632533 }, + { url = "https://files.pythonhosted.org/packages/00/f1/5680e2ef61d9c61454fad82c344f0e40b8741a9dbd1e31484f0d31a9b1c3/safetensors-0.5.2-cp38-abi3-win32.whl", hash = "sha256:fe55c039d97090d1f85277d402954dd6ad27f63034fa81985a9cc59655ac3ee2", size = 291167 }, + { url = "https://files.pythonhosted.org/packages/86/ca/aa489392ec6fb59223ffce825461e1f811a3affd417121a2088be7a5758b/safetensors-0.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:78abdddd03a406646107f973c7843276e7b64e5e32623529dc17f3d94a20f589", size = 303756 }, +] + +[[package]] +name = "scipy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9.17' and python_full_version < '3.10'", + "python_full_version < '3.9.17'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076 }, + { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232 }, + { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202 }, + { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335 }, + { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728 }, + { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588 }, + { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805 }, + { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687 }, + { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638 }, + { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931 }, + { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145 }, + { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227 }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301 }, + { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348 }, + { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062 }, + { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311 }, + { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493 }, + { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955 }, + { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927 }, + { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538 }, + { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190 }, + { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244 }, + { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637 }, + { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440 }, +] + +[[package]] +name = "scipy" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/c6/8eb0654ba0c7d0bb1bf67bf8fbace101a8e4f250f7722371105e8b6f68fc/scipy-1.15.1.tar.gz", hash = "sha256:033a75ddad1463970c96a88063a1df87ccfddd526437136b6ee81ff0312ebdf6", size = 59407493 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/53/b204ce5a4433f1864001b9d16f103b9c25f5002a602ae83585d0ea5f9c4a/scipy-1.15.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c64ded12dcab08afff9e805a67ff4480f5e69993310e093434b10e85dc9d43e1", size = 41414518 }, + { url = "https://files.pythonhosted.org/packages/c7/fc/54ffa7a8847f7f303197a6ba65a66104724beba2e38f328135a78f0dc480/scipy-1.15.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5b190b935e7db569960b48840e5bef71dc513314cc4e79a1b7d14664f57fd4ff", size = 32519265 }, + { url = "https://files.pythonhosted.org/packages/f1/77/a98b8ba03d6f371dc31a38719affd53426d4665729dcffbed4afe296784a/scipy-1.15.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4b17d4220df99bacb63065c76b0d1126d82bbf00167d1730019d2a30d6ae01ea", size = 24792859 }, + { url = "https://files.pythonhosted.org/packages/a7/78/70bb9f0df7444b18b108580934bfef774822e28fd34a68e5c263c7d2828a/scipy-1.15.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:63b9b6cd0333d0eb1a49de6f834e8aeaefe438df8f6372352084535ad095219e", size = 27886506 }, + { url = "https://files.pythonhosted.org/packages/14/a7/f40f6033e06de4176ddd6cc8c3ae9f10a226c3bca5d6b4ab883bc9914a14/scipy-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f151e9fb60fbf8e52426132f473221a49362091ce7a5e72f8aa41f8e0da4f25", size = 38375041 }, + { url = "https://files.pythonhosted.org/packages/17/03/390a1c5c61fd76b0fa4b3c5aa3bdd7e60f6c46f712924f1a9df5705ec046/scipy-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e10b1dd56ce92fba3e786007322542361984f8463c6d37f6f25935a5a6ef52", size = 40597556 }, + { url = "https://files.pythonhosted.org/packages/4e/70/fa95b3ae026b97eeca58204a90868802e5155ac71b9d7bdee92b68115dd3/scipy-1.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5dff14e75cdbcf07cdaa1c7707db6017d130f0af9ac41f6ce443a93318d6c6e0", size = 42938505 }, + { url = "https://files.pythonhosted.org/packages/d6/07/427859116bdd71847c898180f01802691f203c3e2455a1eb496130ff07c5/scipy-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:f82fcf4e5b377f819542fbc8541f7b5fbcf1c0017d0df0bc22c781bf60abc4d8", size = 43909663 }, + { url = "https://files.pythonhosted.org/packages/8e/2e/7b71312da9c2dabff53e7c9a9d08231bc34d9d8fdabe88a6f1155b44591c/scipy-1.15.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:5bd8d27d44e2c13d0c1124e6a556454f52cd3f704742985f6b09e75e163d20d2", size = 41424362 }, + { url = "https://files.pythonhosted.org/packages/81/8c/ab85f1aa1cc200c796532a385b6ebf6a81089747adc1da7482a062acc46c/scipy-1.15.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:be3deeb32844c27599347faa077b359584ba96664c5c79d71a354b80a0ad0ce0", size = 32535910 }, + { url = "https://files.pythonhosted.org/packages/3b/9c/6f4b787058daa8d8da21ddff881b4320e28de4704a65ec147adb50cb2230/scipy-1.15.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5eb0ca35d4b08e95da99a9f9c400dc9f6c21c424298a0ba876fdc69c7afacedf", size = 24809398 }, + { url = "https://files.pythonhosted.org/packages/16/2b/949460a796df75fc7a1ee1becea202cf072edbe325ebe29f6d2029947aa7/scipy-1.15.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:74bb864ff7640dea310a1377d8567dc2cb7599c26a79ca852fc184cc851954ac", size = 27918045 }, + { url = "https://files.pythonhosted.org/packages/5f/36/67fe249dd7ccfcd2a38b25a640e3af7e59d9169c802478b6035ba91dfd6d/scipy-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:667f950bf8b7c3a23b4199db24cb9bf7512e27e86d0e3813f015b74ec2c6e3df", size = 38332074 }, + { url = "https://files.pythonhosted.org/packages/fc/da/452e1119e6f720df3feb588cce3c42c5e3d628d4bfd4aec097bd30b7de0c/scipy-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395be70220d1189756068b3173853029a013d8c8dd5fd3d1361d505b2aa58fa7", size = 40588469 }, + { url = "https://files.pythonhosted.org/packages/7f/71/5f94aceeac99a4941478af94fe9f459c6752d497035b6b0761a700f5f9ff/scipy-1.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce3a000cd28b4430426db2ca44d96636f701ed12e2b3ca1f2b1dd7abdd84b39a", size = 42965214 }, + { url = "https://files.pythonhosted.org/packages/af/25/caa430865749d504271757cafd24066d596217e83326155993980bc22f97/scipy-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fe1d95944f9cf6ba77aa28b82dd6bb2a5b52f2026beb39ecf05304b8392864b", size = 43896034 }, + { url = "https://files.pythonhosted.org/packages/d8/6e/a9c42d0d39e09ed7fd203d0ac17adfea759cba61ab457671fe66e523dbec/scipy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c09aa9d90f3500ea4c9b393ee96f96b0ccb27f2f350d09a47f533293c78ea776", size = 41478318 }, + { url = "https://files.pythonhosted.org/packages/04/ee/e3e535c81828618878a7433992fecc92fa4df79393f31a8fea1d05615091/scipy-1.15.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0ac102ce99934b162914b1e4a6b94ca7da0f4058b6d6fd65b0cef330c0f3346f", size = 32596696 }, + { url = "https://files.pythonhosted.org/packages/c4/5e/b1b0124be8e76f87115f16b8915003eec4b7060298117715baf13f51942c/scipy-1.15.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:09c52320c42d7f5c7748b69e9f0389266fd4f82cf34c38485c14ee976cb8cb04", size = 24870366 }, + { url = "https://files.pythonhosted.org/packages/14/36/c00cb73eefda85946172c27913ab995c6ad4eee00fa4f007572e8c50cd51/scipy-1.15.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cdde8414154054763b42b74fe8ce89d7f3d17a7ac5dd77204f0e142cdc9239e9", size = 28007461 }, + { url = "https://files.pythonhosted.org/packages/68/94/aff5c51b3799349a9d1e67a056772a0f8a47db371e83b498d43467806557/scipy-1.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c9d8fc81d6a3b6844235e6fd175ee1d4c060163905a2becce8e74cb0d7554ce", size = 38068174 }, + { url = "https://files.pythonhosted.org/packages/b0/3c/0de11ca154e24a57b579fb648151d901326d3102115bc4f9a7a86526ce54/scipy-1.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb57b30f0017d4afa5fe5f5b150b8f807618819287c21cbe51130de7ccdaed2", size = 40249869 }, + { url = "https://files.pythonhosted.org/packages/15/09/472e8d0a6b33199d1bb95e49bedcabc0976c3724edd9b0ef7602ccacf41e/scipy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491d57fe89927fa1aafbe260f4cfa5ffa20ab9f1435025045a5315006a91b8f5", size = 42629068 }, + { url = "https://files.pythonhosted.org/packages/ff/ba/31c7a8131152822b3a2cdeba76398ffb404d81d640de98287d236da90c49/scipy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:900f3fa3db87257510f011c292a5779eb627043dd89731b9c461cd16ef76ab3d", size = 43621992 }, + { url = "https://files.pythonhosted.org/packages/2b/bf/dd68965a4c5138a630eeed0baec9ae96e5d598887835bdde96cdd2fe4780/scipy-1.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:100193bb72fbff37dbd0bf14322314fc7cbe08b7ff3137f11a34d06dc0ee6b85", size = 41441136 }, + { url = "https://files.pythonhosted.org/packages/ef/5e/4928581312922d7e4d416d74c416a660addec4dd5ea185401df2269ba5a0/scipy-1.15.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:2114a08daec64980e4b4cbdf5bee90935af66d750146b1d2feb0d3ac30613692", size = 32533699 }, + { url = "https://files.pythonhosted.org/packages/32/90/03f99c43041852837686898c66767787cd41c5843d7a1509c39ffef683e9/scipy-1.15.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6b3e71893c6687fc5e29208d518900c24ea372a862854c9888368c0b267387ab", size = 24807289 }, + { url = "https://files.pythonhosted.org/packages/9d/52/bfe82b42ae112eaba1af2f3e556275b8727d55ac6e4932e7aef337a9d9d4/scipy-1.15.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:837299eec3d19b7e042923448d17d95a86e43941104d33f00da7e31a0f715d3c", size = 27929844 }, + { url = "https://files.pythonhosted.org/packages/f6/77/54ff610bad600462c313326acdb035783accc6a3d5f566d22757ad297564/scipy-1.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82add84e8a9fb12af5c2c1a3a3f1cb51849d27a580cb9e6bd66226195142be6e", size = 38031272 }, + { url = "https://files.pythonhosted.org/packages/f1/26/98585cbf04c7cf503d7eb0a1966df8a268154b5d923c5fe0c1ed13154c49/scipy-1.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070d10654f0cb6abd295bc96c12656f948e623ec5f9a4eab0ddb1466c000716e", size = 40210217 }, + { url = "https://files.pythonhosted.org/packages/fd/3f/3d2285eb6fece8bc5dbb2f9f94d61157d61d155e854fd5fea825b8218f12/scipy-1.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55cc79ce4085c702ac31e49b1e69b27ef41111f22beafb9b49fea67142b696c4", size = 42587785 }, + { url = "https://files.pythonhosted.org/packages/48/7d/5b5251984bf0160d6533695a74a5fddb1fa36edd6f26ffa8c871fbd4782a/scipy-1.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:c352c1b6d7cac452534517e022f8f7b8d139cd9f27e6fbd9f3cbd0bfd39f5bef", size = 43640439 }, + { url = "https://files.pythonhosted.org/packages/e7/b8/0e092f592d280496de52e152582030f8a270b194f87f890e1a97c5599b81/scipy-1.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0458839c9f873062db69a03de9a9765ae2e694352c76a16be44f93ea45c28d2b", size = 41619862 }, + { url = "https://files.pythonhosted.org/packages/f6/19/0b6e1173aba4db9e0b7aa27fe45019857fb90d6904038b83927cbe0a6c1d/scipy-1.15.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:af0b61c1de46d0565b4b39c6417373304c1d4f5220004058bdad3061c9fa8a95", size = 32610387 }, + { url = "https://files.pythonhosted.org/packages/e7/02/754aae3bd1fa0f2479ade3cfdf1732ecd6b05853f63eee6066a32684563a/scipy-1.15.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:71ba9a76c2390eca6e359be81a3e879614af3a71dfdabb96d1d7ab33da6f2364", size = 24883814 }, + { url = "https://files.pythonhosted.org/packages/1f/ac/d7906201604a2ea3b143bb0de51b3966f66441ba50b7dc182c4505b3edf9/scipy-1.15.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14eaa373c89eaf553be73c3affb11ec6c37493b7eaaf31cf9ac5dffae700c2e0", size = 27944865 }, + { url = "https://files.pythonhosted.org/packages/84/9d/8f539002b5e203723af6a6f513a45e0a7671e9dabeedb08f417ac17e4edc/scipy-1.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f735bc41bd1c792c96bc426dece66c8723283695f02df61dcc4d0a707a42fc54", size = 39883261 }, + { url = "https://files.pythonhosted.org/packages/97/c0/62fd3bab828bcccc9b864c5997645a3b86372a35941cdaf677565c25c98d/scipy-1.15.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2722a021a7929d21168830790202a75dbb20b468a8133c74a2c0230c72626b6c", size = 42093299 }, + { url = "https://files.pythonhosted.org/packages/e4/1f/5d46a8d94e9f6d2c913cbb109e57e7eed914de38ea99e2c4d69a9fc93140/scipy-1.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc7136626261ac1ed988dca56cfc4ab5180f75e0ee52e58f1e6aa74b5f3eacd5", size = 43181730 }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/d2/b9c7ca067c26d8ff085d252c89b5f69609ca93fb85a00ede95f4857865d4/sentencepiece-0.2.0.tar.gz", hash = "sha256:a52c19171daaf2e697dc6cbe67684e0fa341b1248966f6aebb541de654d15843", size = 2632106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/71/98648c3b64b23edb5403f74bcc906ad21766872a6e1ada26ea3f1eb941ab/sentencepiece-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:188779e1298a1c8b8253c7d3ad729cb0a9891e5cef5e5d07ce4592c54869e227", size = 2408979 }, + { url = "https://files.pythonhosted.org/packages/77/9f/7efbaa6d4c0c718a9affbecc536b03ca62f99f421bdffb531c16030e2d2b/sentencepiece-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bed9cf85b296fa2b76fc2547b9cbb691a523864cebaee86304c43a7b4cb1b452", size = 1238845 }, + { url = "https://files.pythonhosted.org/packages/1c/e4/c2541027a43ec6962ba9b601805d17ba3f86b38bdeae0e8ac65a2981e248/sentencepiece-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7b67e724bead13f18db6e1d10b6bbdc454af574d70efbb36f27d90387be1ca3", size = 1181472 }, + { url = "https://files.pythonhosted.org/packages/fd/46/316c1ba6c52b97de76aff7b9da678f7afbb52136afb2987c474d95630e65/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fde4b08cfe237be4484c6c7c2e2c75fb862cfeab6bd5449ce4caeafd97b767a", size = 1259151 }, + { url = "https://files.pythonhosted.org/packages/aa/5a/3c48738a0835d76dd06c62b6ac48d39c923cde78dd0f587353bdcbb99851/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c378492056202d1c48a4979650981635fd97875a00eabb1f00c6a236b013b5e", size = 1355931 }, + { url = "https://files.pythonhosted.org/packages/a6/27/33019685023221ca8ed98e8ceb7ae5e166032686fa3662c68f1f1edf334e/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1380ce6540a368de2ef6d7e6ba14ba8f3258df650d39ba7d833b79ee68a52040", size = 1301537 }, + { url = "https://files.pythonhosted.org/packages/ca/e4/55f97cef14293171fef5f96e96999919ab5b4d1ce95b53547ad653d7e3bf/sentencepiece-0.2.0-cp310-cp310-win32.whl", hash = "sha256:a1151d6a6dd4b43e552394aed0edfe9292820272f0194bd56c7c1660a0c06c3d", size = 936747 }, + { url = "https://files.pythonhosted.org/packages/85/f4/4ef1a6e0e9dbd8a60780a91df8b7452ada14cfaa0e17b3b8dfa42cecae18/sentencepiece-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:d490142b0521ef22bc1085f061d922a2a6666175bb6b42e588ff95c0db6819b2", size = 991525 }, + { url = "https://files.pythonhosted.org/packages/32/43/8f8885168a47a02eba1455bd3f4f169f50ad5b8cebd2402d0f5e20854d04/sentencepiece-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17982700c4f6dbb55fa3594f3d7e5dd1c8659a274af3738e33c987d2a27c9d5c", size = 2409036 }, + { url = "https://files.pythonhosted.org/packages/0f/35/e63ba28062af0a3d688a9f128e407a1a2608544b2f480cb49bf7f4b1cbb9/sentencepiece-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c867012c0e8bcd5bdad0f791609101cb5c66acb303ab3270218d6debc68a65e", size = 1238921 }, + { url = "https://files.pythonhosted.org/packages/de/42/ae30952c4a0bd773e90c9bf2579f5533037c886dfc8ec68133d5694f4dd2/sentencepiece-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd6071249c74f779c5b27183295b9202f8dedb68034e716784364443879eaa6", size = 1181477 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/2f2ab1d60bb2d795d054eebe5e3f24b164bc21b5a9b75fba7968b3b91b5a/sentencepiece-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f90c55a65013cbb8f4d7aab0599bf925cde4adc67ae43a0d323677b5a1c6cb", size = 1259182 }, + { url = "https://files.pythonhosted.org/packages/45/fb/14633c6ecf262c468759ffcdb55c3a7ee38fe4eda6a70d75ee7c7d63c58b/sentencepiece-0.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b293734059ef656dcd65be62ff771507bea8fed0a711b6733976e1ed3add4553", size = 1355537 }, + { url = "https://files.pythonhosted.org/packages/fb/12/2f5c8d4764b00033cf1c935b702d3bb878d10be9f0b87f0253495832d85f/sentencepiece-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e58b47f933aca74c6a60a79dcb21d5b9e47416256c795c2d58d55cec27f9551d", size = 1301464 }, + { url = "https://files.pythonhosted.org/packages/4e/b1/67afc0bde24f6dcb3acdea0dd8dcdf4b8b0db240f6bacd39378bd32d09f8/sentencepiece-0.2.0-cp311-cp311-win32.whl", hash = "sha256:c581258cf346b327c62c4f1cebd32691826306f6a41d8c4bec43b010dee08e75", size = 936749 }, + { url = "https://files.pythonhosted.org/packages/a2/f6/587c62fd21fc988555b85351f50bbde43a51524caafd63bc69240ded14fd/sentencepiece-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0993dbc665f4113017892f1b87c3904a44d0640eda510abcacdfb07f74286d36", size = 991520 }, + { url = "https://files.pythonhosted.org/packages/27/5a/141b227ed54293360a9ffbb7bf8252b4e5efc0400cdeac5809340e5d2b21/sentencepiece-0.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea5f536e32ea8ec96086ee00d7a4a131ce583a1b18d130711707c10e69601cb2", size = 2409370 }, + { url = "https://files.pythonhosted.org/packages/2e/08/a4c135ad6fc2ce26798d14ab72790d66e813efc9589fd30a5316a88ca8d5/sentencepiece-0.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0cb51f53b6aae3c36bafe41e86167c71af8370a039f542c43b0cce5ef24a68c", size = 1239288 }, + { url = "https://files.pythonhosted.org/packages/49/0a/2fe387f825ac5aad5a0bfe221904882106cac58e1b693ba7818785a882b6/sentencepiece-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3212121805afc58d8b00ab4e7dd1f8f76c203ddb9dc94aa4079618a31cf5da0f", size = 1181597 }, + { url = "https://files.pythonhosted.org/packages/cc/38/e4698ee2293fe4835dc033c49796a39b3eebd8752098f6bd0aa53a14af1f/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3149e3066c2a75e0d68a43eb632d7ae728c7925b517f4c05c40f6f7280ce08", size = 1259220 }, + { url = "https://files.pythonhosted.org/packages/12/24/fd7ef967c9dad2f6e6e5386d0cadaf65cda8b7be6e3861a9ab3121035139/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:632f3594d3e7ac8b367bca204cb3fd05a01d5b21455acd097ea4c0e30e2f63d7", size = 1355962 }, + { url = "https://files.pythonhosted.org/packages/4f/d2/18246f43ca730bb81918f87b7e886531eda32d835811ad9f4657c54eee35/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f295105c6bdbb05bd5e1b0cafbd78ff95036f5d3641e7949455a3f4e5e7c3109", size = 1301706 }, + { url = "https://files.pythonhosted.org/packages/8a/47/ca237b562f420044ab56ddb4c278672f7e8c866e183730a20e413b38a989/sentencepiece-0.2.0-cp312-cp312-win32.whl", hash = "sha256:fb89f811e5efd18bab141afc3fea3de141c3f69f3fe9e898f710ae7fe3aab251", size = 936941 }, + { url = "https://files.pythonhosted.org/packages/c6/97/d159c32642306ee2b70732077632895438867b3b6df282354bd550cf2a67/sentencepiece-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a673a72aab81fef5ebe755c6e0cc60087d1f3a4700835d40537183c1703a45f", size = 991994 }, + { url = "https://files.pythonhosted.org/packages/e9/18/eb620d94d63f62ca69cecccf4459529864ac3fbb35ec123190bd58dadb46/sentencepiece-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1e0f9c4d0a6b0af59b613175f019916e28ade076e21242fd5be24340d8a2f64a", size = 2409003 }, + { url = "https://files.pythonhosted.org/packages/6e/a6/df28bc0b6a2a86416232c0a5f0d69a9cb7244bb95cb5dcdfcbf01cced8a6/sentencepiece-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:298f21cc1366eb60311aedba3169d30f885c363ddbf44214b0a587d2908141ad", size = 1238898 }, + { url = "https://files.pythonhosted.org/packages/79/91/b54a528e0789cd7986341ed3909bec56365c3b672daef8b10aa4098238f0/sentencepiece-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f1ec95aa1e5dab11f37ac7eff190493fd87770f7a8b81ebc9dd768d1a3c8704", size = 1181534 }, + { url = "https://files.pythonhosted.org/packages/a3/69/e96ef68261fa5b82379fdedb325ceaf1d353c6e839ec346d8244e0da5f2f/sentencepiece-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b06b70af54daa4b4904cbb90b4eb6d35c9f3252fdc86c9c32d5afd4d30118d8", size = 1259161 }, + { url = "https://files.pythonhosted.org/packages/45/de/461d15856c29ba1ce778cf76e0462572661f647abc8a5373690c52e98a00/sentencepiece-0.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e37bac44dd6603388cb598c64ff7a76e41ca774646f21c23aadfbf5a2228ab", size = 1355945 }, + { url = "https://files.pythonhosted.org/packages/5f/01/c95e42eb86282b2c79305d3e0b0ca5a743f85a61262bb7130999c70b9374/sentencepiece-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0461324897735512a32d222e3d886e24ad6a499761952b6bda2a9ee6e4313ea5", size = 1301596 }, + { url = "https://files.pythonhosted.org/packages/be/47/e16f368fe6327e873e8029aa539115025e9f61a4e8ca8f0f8eaf8e6a4c1c/sentencepiece-0.2.0-cp39-cp39-win32.whl", hash = "sha256:38aed822fb76435fa1f12185f10465a94ab9e51d5e8a9159e9a540ce926f0ffd", size = 936757 }, + { url = "https://files.pythonhosted.org/packages/4b/36/497e6407700efd6b97f81bc160913a70d33b9b09227429f68fc86f387bbe/sentencepiece-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:d8cf876516548b5a1d6ac4745d8b554f5c07891d55da557925e5c13ff0b4e6ad", size = 991541 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.41.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, +] + +[[package]] +name = "sympy" +version = "1.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 }, +] + +[[package]] +name = "tbb" +version = "2021.13.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/8a/5062b00c378c051e26507e5eca8d3b5c91ed63f8a2139f6f0f422be84b02/tbb-2021.13.1-py3-none-win32.whl", hash = "sha256:00f5e5a70051650ddd0ab6247c0549521968339ec21002e475cd23b1cbf46d66", size = 248994 }, + { url = "https://files.pythonhosted.org/packages/9b/24/84ce997e8ae6296168a74d0d9c4dde572d90fb23fd7c0b219c30ff71e00e/tbb-2021.13.1-py3-none-win_amd64.whl", hash = "sha256:cbf024b2463fdab3ebe3fa6ff453026358e6b903839c80d647e08ad6d0796ee9", size = 286908 }, +] + +[[package]] +name = "tiktoken" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/02/576ff3a6639e755c4f70997b2d315f56d6d71e0d046f4fb64cb81a3fb099/tiktoken-0.8.0.tar.gz", hash = "sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2", size = 35107 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/ba/a35fad753bbca8ba0cc1b0f3402a70256a110ced7ac332cf84ba89fc87ab/tiktoken-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e", size = 1039905 }, + { url = "https://files.pythonhosted.org/packages/91/05/13dab8fd7460391c387b3e69e14bf1e51ff71fe0a202cd2933cc3ea93fb6/tiktoken-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21", size = 982417 }, + { url = "https://files.pythonhosted.org/packages/e9/98/18ec4a8351a6cf4537e40cd6e19a422c10cce1ef00a2fcb716e0a96af58b/tiktoken-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560", size = 1144915 }, + { url = "https://files.pythonhosted.org/packages/2e/28/cf3633018cbcc6deb7805b700ccd6085c9a5a7f72b38974ee0bffd56d311/tiktoken-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2", size = 1177221 }, + { url = "https://files.pythonhosted.org/packages/57/81/8a5be305cbd39d4e83a794f9e80c7f2c84b524587b7feb27c797b2046d51/tiktoken-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9", size = 1237398 }, + { url = "https://files.pythonhosted.org/packages/dc/da/8d1cc3089a83f5cf11c2e489332752981435280285231924557350523a59/tiktoken-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005", size = 884215 }, + { url = "https://files.pythonhosted.org/packages/f6/1e/ca48e7bfeeccaf76f3a501bd84db1fa28b3c22c9d1a1f41af9fb7579c5f6/tiktoken-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1", size = 1039700 }, + { url = "https://files.pythonhosted.org/packages/8c/f8/f0101d98d661b34534769c3818f5af631e59c36ac6d07268fbfc89e539ce/tiktoken-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a", size = 982413 }, + { url = "https://files.pythonhosted.org/packages/ac/3c/2b95391d9bd520a73830469f80a96e3790e6c0a5ac2444f80f20b4b31051/tiktoken-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d", size = 1144242 }, + { url = "https://files.pythonhosted.org/packages/01/c4/c4a4360de845217b6aa9709c15773484b50479f36bb50419c443204e5de9/tiktoken-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47", size = 1176588 }, + { url = "https://files.pythonhosted.org/packages/f8/a3/ef984e976822cd6c2227c854f74d2e60cf4cd6fbfca46251199914746f78/tiktoken-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419", size = 1237261 }, + { url = "https://files.pythonhosted.org/packages/1e/86/eea2309dc258fb86c7d9b10db536434fc16420feaa3b6113df18b23db7c2/tiktoken-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99", size = 884537 }, + { url = "https://files.pythonhosted.org/packages/c1/22/34b2e136a6f4af186b6640cbfd6f93400783c9ef6cd550d9eab80628d9de/tiktoken-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:881839cfeae051b3628d9823b2e56b5cc93a9e2efb435f4cf15f17dc45f21586", size = 1039357 }, + { url = "https://files.pythonhosted.org/packages/04/d2/c793cf49c20f5855fd6ce05d080c0537d7418f22c58e71f392d5e8c8dbf7/tiktoken-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe9399bdc3f29d428f16a2f86c3c8ec20be3eac5f53693ce4980371c3245729b", size = 982616 }, + { url = "https://files.pythonhosted.org/packages/b3/a1/79846e5ef911cd5d75c844de3fa496a10c91b4b5f550aad695c5df153d72/tiktoken-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a58deb7075d5b69237a3ff4bb51a726670419db6ea62bdcd8bd80c78497d7ab", size = 1144011 }, + { url = "https://files.pythonhosted.org/packages/26/32/e0e3a859136e95c85a572e4806dc58bf1ddf651108ae8b97d5f3ebe1a244/tiktoken-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2908c0d043a7d03ebd80347266b0e58440bdef5564f84f4d29fb235b5df3b04", size = 1175432 }, + { url = "https://files.pythonhosted.org/packages/c7/89/926b66e9025b97e9fbabeaa59048a736fe3c3e4530a204109571104f921c/tiktoken-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:294440d21a2a51e12d4238e68a5972095534fe9878be57d905c476017bff99fc", size = 1236576 }, + { url = "https://files.pythonhosted.org/packages/45/e2/39d4aa02a52bba73b2cd21ba4533c84425ff8786cc63c511d68c8897376e/tiktoken-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:d8f3192733ac4d77977432947d563d7e1b310b96497acd3c196c9bddb36ed9db", size = 883824 }, + { url = "https://files.pythonhosted.org/packages/e3/38/802e79ba0ee5fcbf240cd624143f57744e5d411d2e9d9ad2db70d8395986/tiktoken-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:02be1666096aff7da6cbd7cdaa8e7917bfed3467cd64b38b1f112e96d3b06a24", size = 1039648 }, + { url = "https://files.pythonhosted.org/packages/b1/da/24cdbfc302c98663fbea66f5866f7fa1048405c7564ab88483aea97c3b1a/tiktoken-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94ff53c5c74b535b2cbf431d907fc13c678bbd009ee633a2aca269a04389f9a", size = 982763 }, + { url = "https://files.pythonhosted.org/packages/e4/f0/0ecf79a279dfa41fc97d00adccf976ecc2556d3c08ef3e25e45eb31f665b/tiktoken-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b231f5e8982c245ee3065cd84a4712d64692348bc609d84467c57b4b72dcbc5", size = 1144417 }, + { url = "https://files.pythonhosted.org/packages/ab/d3/155d2d4514f3471a25dc1d6d20549ef254e2aa9bb5b1060809b1d3b03d3a/tiktoken-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4177faa809bd55f699e88c96d9bb4635d22e3f59d635ba6fd9ffedf7150b9953", size = 1175108 }, + { url = "https://files.pythonhosted.org/packages/19/eb/5989e16821ee8300ef8ee13c16effc20dfc26c777d05fbb6825e3c037b81/tiktoken-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5376b6f8dc4753cd81ead935c5f518fa0fbe7e133d9e25f648d8c4dabdd4bad7", size = 1236520 }, + { url = "https://files.pythonhosted.org/packages/40/59/14b20465f1d1cb89cfbc96ec27e5617b2d41c79da12b5e04e96d689be2a7/tiktoken-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:18228d624807d66c87acd8f25fc135665617cab220671eb65b50f5d70fa51f69", size = 883849 }, + { url = "https://files.pythonhosted.org/packages/08/f3/8a8ba9329e6b426d822c974d58fc6477f3f7b3b8deef651813d275cbe75f/tiktoken-0.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17807445f0cf1f25771c9d86496bd8b5c376f7419912519699f3cc4dc5c12e", size = 1040915 }, + { url = "https://files.pythonhosted.org/packages/42/7a/914bd98100449422778f9222d00b3a4ee654211c40784e57541fa46311ab/tiktoken-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:886f80bd339578bbdba6ed6d0567a0d5c6cfe198d9e587ba6c447654c65b8edc", size = 983753 }, + { url = "https://files.pythonhosted.org/packages/f7/01/1483856d84827c5fe541cb160f07914c6b063b8d961146e9c3557c4730c0/tiktoken-0.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6adc8323016d7758d6de7313527f755b0fc6c72985b7d9291be5d96d73ecd1e1", size = 1145913 }, + { url = "https://files.pythonhosted.org/packages/c2/e1/6c7a772e0200131e960e3381f1d7b26406bc5612c70677989c1498af2a60/tiktoken-0.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b591fb2b30d6a72121a80be24ec7a0e9eb51c5500ddc7e4c2496516dd5e3816b", size = 1178505 }, + { url = "https://files.pythonhosted.org/packages/3e/6b/3ae00f0bff5d0b6925bf6370cf0ff606f56daed76210c2b0a156017b78dc/tiktoken-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:845287b9798e476b4d762c3ebda5102be87ca26e5d2c9854002825d60cdb815d", size = 1239111 }, + { url = "https://files.pythonhosted.org/packages/d5/3b/7c8812952ca55e1bab08afc1dda3c5991804c71b550b9402e82a082ab795/tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02", size = 884803 }, +] + +[[package]] +name = "tokenizers" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/41/c2be10975ca37f6ec40d7abd7e98a5213bb04f284b869c1a24e6504fd94d/tokenizers-0.21.0.tar.gz", hash = "sha256:ee0894bf311b75b0c03079f33859ae4b2334d675d4e93f5a4132e1eae2834fe4", size = 343021 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/5c/8b09607b37e996dc47e70d6a7b6f4bdd4e4d5ab22fe49d7374565c7fefaf/tokenizers-0.21.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3c4c93eae637e7d2aaae3d376f06085164e1660f89304c0ab2b1d08a406636b2", size = 2647461 }, + { url = "https://files.pythonhosted.org/packages/22/7a/88e58bb297c22633ed1c9d16029316e5b5ac5ee44012164c2edede599a5e/tokenizers-0.21.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f53ea537c925422a2e0e92a24cce96f6bc5046bbef24a1652a5edc8ba975f62e", size = 2563639 }, + { url = "https://files.pythonhosted.org/packages/f7/14/83429177c19364df27d22bc096d4c2e431e0ba43e56c525434f1f9b0fd00/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b177fb54c4702ef611de0c069d9169f0004233890e0c4c5bd5508ae05abf193", size = 2903304 }, + { url = "https://files.pythonhosted.org/packages/7e/db/3433eab42347e0dc5452d8fcc8da03f638c9accffefe5a7c78146666964a/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b43779a269f4629bebb114e19c3fca0223296ae9fea8bb9a7a6c6fb0657ff8e", size = 2804378 }, + { url = "https://files.pythonhosted.org/packages/57/8b/7da5e6f89736c2ade02816b4733983fca1c226b0c42980b1ae9dc8fcf5cc/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aeb255802be90acfd363626753fda0064a8df06031012fe7d52fd9a905eb00e", size = 3095488 }, + { url = "https://files.pythonhosted.org/packages/4d/f6/5ed6711093dc2c04a4e03f6461798b12669bc5a17c8be7cce1240e0b5ce8/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b09dbeb7a8d73ee204a70f94fc06ea0f17dcf0844f16102b9f414f0b7463ba", size = 3121410 }, + { url = "https://files.pythonhosted.org/packages/81/42/07600892d48950c5e80505b81411044a2d969368cdc0d929b1c847bf6697/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:400832c0904f77ce87c40f1a8a27493071282f785724ae62144324f171377273", size = 3388821 }, + { url = "https://files.pythonhosted.org/packages/22/06/69d7ce374747edaf1695a4f61b83570d91cc8bbfc51ccfecf76f56ab4aac/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84ca973b3a96894d1707e189c14a774b701596d579ffc7e69debfc036a61a04", size = 3008868 }, + { url = "https://files.pythonhosted.org/packages/c8/69/54a0aee4d576045b49a0eb8bffdc495634309c823bf886042e6f46b80058/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb7202d231b273c34ec67767378cd04c767e967fda12d4a9e36208a34e2f137e", size = 8975831 }, + { url = "https://files.pythonhosted.org/packages/f7/f3/b776061e4f3ebf2905ba1a25d90380aafd10c02d406437a8ba22d1724d76/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:089d56db6782a73a27fd8abf3ba21779f5b85d4a9f35e3b493c7bbcbbf0d539b", size = 8920746 }, + { url = "https://files.pythonhosted.org/packages/d8/ee/ce83d5ec8b6844ad4c3ecfe3333d58ecc1adc61f0878b323a15355bcab24/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c87ca3dc48b9b1222d984b6b7490355a6fdb411a2d810f6f05977258400ddb74", size = 9161814 }, + { url = "https://files.pythonhosted.org/packages/18/07/3e88e65c0ed28fa93aa0c4d264988428eef3df2764c3126dc83e243cb36f/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4145505a973116f91bc3ac45988a92e618a6f83eb458f49ea0790df94ee243ff", size = 9357138 }, + { url = "https://files.pythonhosted.org/packages/15/b0/dc4572ca61555fc482ebc933f26cb407c6aceb3dc19c301c68184f8cad03/tokenizers-0.21.0-cp39-abi3-win32.whl", hash = "sha256:eb1702c2f27d25d9dd5b389cc1f2f51813e99f8ca30d9e25348db6585a97e24a", size = 2202266 }, + { url = "https://files.pythonhosted.org/packages/44/69/d21eb253fa91622da25585d362a874fa4710be600f0ea9446d8d0217cec1/tokenizers-0.21.0-cp39-abi3-win_amd64.whl", hash = "sha256:87841da5a25a3a5f70c102de371db120f41873b854ba65e52bccd57df5a3780c", size = 2389192 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "torch" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "mkl", marker = "sys_platform == 'win32'" }, + { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e5/2ddae60ae999b224aceb74490abeb885ee118227f866cb12046f0481d4c9/torch-2.3.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d8ea5a465dbfd8501f33c937d1f693176c9aef9d1c1b0ca1d44ed7b0a18c52ac", size = 779141888 }, + { url = "https://files.pythonhosted.org/packages/dc/82/7ab793eba3f4c3c84244e4c578ea838536d526db01c787448e8177404598/torch-2.3.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09c81c5859a5b819956c6925a405ef1cdda393c9d8a01ce3851453f699d3358c", size = 88506981 }, + { url = "https://files.pythonhosted.org/packages/2e/f7/503bab04f4e7a0a43f2ff05c3635cee9dfcf2e09656020d29502b87a94a3/torch-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:1bf023aa20902586f614f7682fedfa463e773e26c58820b74158a72470259459", size = 159799740 }, + { url = "https://files.pythonhosted.org/packages/01/c1/c6b42224122989ec95a820974aee92bdd4308380a7bb6ffa9a9d2429765d/torch-2.3.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:758ef938de87a2653bba74b91f703458c15569f1562bf4b6c63c62d9c5a0c1f5", size = 60993271 }, + { url = "https://files.pythonhosted.org/packages/35/3a/a39f354fa3119785be87e2f94ffa2620f8a270c8560f7356358ee62fb4c5/torch-2.3.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:493d54ee2f9df100b5ce1d18c96dbb8d14908721f76351e908c9d2622773a788", size = 779160265 }, + { url = "https://files.pythonhosted.org/packages/91/3c/98a9b901b40278b40a9ff5b9c6be387b20997269f929f2182d8845c94085/torch-2.3.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:bce43af735c3da16cc14c7de2be7ad038e2fbf75654c2e274e575c6c05772ace", size = 88536251 }, + { url = "https://files.pythonhosted.org/packages/2a/b7/a3cf5fd40334b9785cc83ee0c96b50603026eb3aa70210a33729018e7029/torch-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:729804e97b7cf19ae9ab4181f91f5e612af07956f35c8b2c8e9d9f3596a8e877", size = 159803952 }, + { url = "https://files.pythonhosted.org/packages/ad/08/c5e41eb22323db4a52260607598a207a2e1918916ae8201aa7a8ae005fcd/torch-2.3.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:d24e328226d8e2af7cf80fcb1d2f1d108e0de32777fab4aaa2b37b9765d8be73", size = 60998957 }, + { url = "https://files.pythonhosted.org/packages/51/03/1abad10990c76bee3703857b1617563b241f87d297ee466dbad922b0c308/torch-2.3.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:b0de2bdc0486ea7b14fc47ff805172df44e421a7318b7c4d92ef589a75d27410", size = 779062531 }, + { url = "https://files.pythonhosted.org/packages/f1/9d/dfe273e19b7165148208bd182fac87488c5a0f7a3318d1646f5f37af872f/torch-2.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a306c87a3eead1ed47457822c01dfbd459fe2920f2d38cbdf90de18f23f72542", size = 88437165 }, + { url = "https://files.pythonhosted.org/packages/37/04/a5cd83baccbf2d4329990ec06b8abf3a644e1559a7b1f764f42d2cb77d51/torch-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9b98bf1a3c8af2d4c41f0bf1433920900896c446d1ddc128290ff146d1eb4bd", size = 159749140 }, + { url = "https://files.pythonhosted.org/packages/55/51/4bdee83e6fa9cca8e3a6cdf81a2695ede9d3fd7148e4fd4188dff142d7b0/torch-2.3.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:dca986214267b34065a79000cee54232e62b41dff1ec2cab9abc3fc8b3dee0ad", size = 60968873 }, + { url = "https://files.pythonhosted.org/packages/da/af/abfe44a9ac7fd9705f145483e9e6c73a23d0533223082468eb2d978a494c/torch-2.3.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cd0dc498b961ab19cb3f8dbf0c6c50e244f2f37dbfa05754ab44ea057c944ef9", size = 779140324 }, + { url = "https://files.pythonhosted.org/packages/e6/b3/0ba7ab81889a964b027f9f46f7e1ddc49ac5532d5f7f09fc0730a9295bde/torch-2.3.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e05f836559251e4096f3786ee99f4a8cbe67bc7fbedba8ad5e799681e47c5e80", size = 88508656 }, + { url = "https://files.pythonhosted.org/packages/af/73/9fbe55b0db8e329b124df415bbdac760f4592e030d2cf20b05fd9aec68f5/torch-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:4fb27b35dbb32303c2927da86e27b54a92209ddfb7234afb1949ea2b3effffea", size = 159734927 }, + { url = "https://files.pythonhosted.org/packages/5b/e0/12a8ef2d7a00060ca5cbacef7b12f2b3e35fe3472973882e4a87165fea67/torch-2.3.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:760f8bedff506ce9e6e103498f9b1e9e15809e008368594c3a66bf74a8a51380", size = 60993508 }, +] + +[[package]] +name = "torchvision" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/e7/19e06d609f44f2c670db1056817deb928412ed329ff3f63cc2a27231b029/torchvision-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd61628a3d189c6852a12dc5ed4cd2eece66d2d67f35a866cb16f1dcb06c8c62", size = 1554228 }, + { url = "https://files.pythonhosted.org/packages/d4/7e/d41b771dbffa927b9cc37372b1e18c881348cd18a0e4ad73f2c6bdf56c0e/torchvision-0.18.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:493c45f9937dad37aa1b64b14da17c7a589c72b91adc4837d431009cfe29bd53", size = 6954946 }, + { url = "https://files.pythonhosted.org/packages/b2/dd/e5d39496413a5e5c2ca69d333bc241e7c8e8e412778c8309d54ce27cb9ec/torchvision-0.18.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5337f6acfa1fe959d5cb340d01a00614d6b31ce7a4824ccb95435a85c5273b95", size = 13995854 }, + { url = "https://files.pythonhosted.org/packages/f3/ef/f9559f3fc09362eafea5937521a2013a3ae67e38a7c6c3c9b51b3751c320/torchvision-0.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd8e6f3b5beb49965f15c461302488edfa3d8c2d01d3bb79b150d6fb62711e3a", size = 1174147 }, + { url = "https://files.pythonhosted.org/packages/b5/14/c05da13c98f528ba5fd99897320a7684df5dd136ec6faa6a5766f25e4a7e/torchvision-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6896a52168befe1105fb3c9335287390ed227e71d1e4ec4d68b62e8a3099fc09", size = 1554225 }, + { url = "https://files.pythonhosted.org/packages/6e/7d/bc67ec2d1011226e75cdcc45a2c85d97b8eaac32a7d648b71c432d584367/torchvision-0.18.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:3d7955398d4ceaad77c487c2c44f6f7813112402c9bab8cd906d346005891048", size = 6955003 }, + { url = "https://files.pythonhosted.org/packages/70/1d/107894816bf5ebecbc5a8556743fd89c0a1dd6da82b1c562d6becd5a108a/torchvision-0.18.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:e5a24d620cea14a4bb89f24aa2b506230c0a16a3ada57fc53ad80cfd256a2128", size = 13995855 }, + { url = "https://files.pythonhosted.org/packages/12/c2/7c89c62f2b0a606070aa7cdb8af8af0c905562942778ebdd77600642c3b9/torchvision-0.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:6ad70ddfa879bda5ed886b2518fe562640e0059787cbd65cb2bffa7674541410", size = 1174125 }, + { url = "https://files.pythonhosted.org/packages/7c/12/49d4fd4ae7a48c6d33babb01a523594aca38365d378518320a10f9a5baa1/torchvision-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb9d83c0e1dbb54ecb0fb04c87f786333e3a6fb8b9c400aca7c31081f9aa5707", size = 1554232 }, + { url = "https://files.pythonhosted.org/packages/6d/fe/729256fec03403b0bfdc229d4350936e29020d618f1740009c6a4f995b06/torchvision-0.18.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:b657d052d146f24cb3b2a78219bfc82ae70a9706671c50f632528907d10cccec", size = 6955047 }, + { url = "https://files.pythonhosted.org/packages/51/2d/30883e9c6734546f9e7e0c429b76bd2b651aa25f6ced087c9c11cbf2ef41/torchvision-0.18.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a964afbc7ddf50a46b941477f6c35729b416deedd139756befd488245e2e226d", size = 13996688 }, + { url = "https://files.pythonhosted.org/packages/53/8a/864c3969af219a95213a5065d453313a96598e7c744b859e99b6ac134e16/torchvision-0.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c770f0f748e0b17f57c0297508d7254f686cdf03fc2e2949f422b20574f4c0f", size = 1174123 }, + { url = "https://files.pythonhosted.org/packages/d1/dc/bace7633ce37bc165fb6509e741d3cd73535f36e86b3ee5a95b2b647af92/torchvision-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75e22ecf44a13b8f95b8ad421c0261282d859c61816badaca1959e073ccdd691", size = 1554270 }, + { url = "https://files.pythonhosted.org/packages/54/20/309df7711dd17c399b7cd233e78bcee0f6fcdfbef5f2ec0930910c1884df/torchvision-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4c334b3e719ba0a9ba6e15d4aff1178f5e6d029174f346163fed525f0ccfffd3", size = 6956173 }, + { url = "https://files.pythonhosted.org/packages/72/73/b50f27719a9d0d9decae9f4264967a5c54a84046d1986772316d2471b6a2/torchvision-0.18.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:36efd87001c6bee2383e043e46a025affb03179747c8f4777b9918527ffce756", size = 1626203 }, + { url = "https://files.pythonhosted.org/packages/db/4a/33a91dfcce5d62f826b15691a3bda2cdf62e4fbaf164f469af6a6756ebb4/torchvision-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:ccc292e093771d5baacf5535ac4416306b6b5f15676341cd4d010d8542eace25", size = 1174149 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "transformers" +version = "4.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/71/93a6331682d6f15adf7d646956db0c43e5f1759bbbd05f2ef53029bae107/transformers-4.48.0.tar.gz", hash = "sha256:03fdfcbfb8b0367fb6c9fbe9d1c9aa54dfd847618be9b52400b2811d22799cb1", size = 8372101 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/d6/a69764e89fc5c2c957aa473881527c8c35521108d553df703e9ba703daeb/transformers-4.48.0-py3-none-any.whl", hash = "sha256:6d3de6d71cb5f2a10f9775ccc17abce9620195caaf32ec96542bd2a6937f25b0", size = 9673380 }, +] + +[package.optional-dependencies] +sentencepiece = [ + { name = "protobuf" }, + { name = "sentencepiece" }, +] + +[[package]] +name = "triton" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.12'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/ee/8d50d44ed5b63677bb387f4ee67a7dbaaded0189b320ffe82685a6827728/triton-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ce4b8ff70c48e47274c66f269cce8861cf1dc347ceeb7a67414ca151b1822d8", size = 168055246 }, + { url = "https://files.pythonhosted.org/packages/3c/00/84e0006f2025260fa111ddfc66194bd1af731b3ee18e2fd611a00f290b5e/triton-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c3d9607f85103afdb279938fc1dd2a66e4f5999a58eb48a346bd42738f986dd", size = 168079300 }, + { url = "https://files.pythonhosted.org/packages/90/2f/3e8f0ea4ef7ac6d8720a48b9b9700fd32c9909ee83b2eb1f25209ace0767/triton-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:218d742e67480d9581bafb73ed598416cc8a56f6316152e5562ee65e33de01c0", size = 168091361 }, + { url = "https://files.pythonhosted.org/packages/8e/7b/8557e97f0f39992eaf390aacdf36c38a4de059caa9eda99321f7d3576d82/triton-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8f636e0341ac348899a47a057c3daea99ea7db31528a225a3ba4ded28ccc65", size = 168051036 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, + { url = "https://files.pythonhosted.org/packages/3c/a4/646a9d0edff7cde25fc1734695d3dfcee0501140dd0e723e4df3f0a50acb/uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", size = 1439646 }, + { url = "https://files.pythonhosted.org/packages/01/2e/e128c66106af9728f86ebfeeb52af27ecd3cb09336f3e2f3e06053707a15/uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", size = 800931 }, + { url = "https://files.pythonhosted.org/packages/2d/1a/9fbc2b1543d0df11f7aed1632f64bdf5ecc4053cf98cdc9edb91a65494f9/uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", size = 3829660 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/392e235e4100ae3b95b5c6dac77f82b529d2760942b1e7e0981e5d8e895d/uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", size = 3827185 }, + { url = "https://files.pythonhosted.org/packages/e1/24/a5da6aba58f99aed5255eca87d58d1760853e8302d390820cc29058408e3/uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", size = 3705833 }, + { url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, +] + +[[package]] +name = "vllm" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "cmake" }, + { name = "fastapi" }, + { name = "filelock" }, + { name = "lm-format-enforcer" }, + { name = "ninja" }, + { name = "numpy" }, + { name = "nvidia-ml-py" }, + { name = "openai" }, + { name = "outlines" }, + { name = "pillow" }, + { name = "prometheus-client" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "psutil" }, + { name = "py-cpuinfo" }, + { name = "pydantic" }, + { name = "ray" }, + { name = "requests" }, + { name = "sentencepiece" }, + { name = "tiktoken" }, + { name = "tokenizers" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "vllm-flash-attn" }, + { name = "xformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d2/38993991ee61bacaccd49182ce597bf4496bc4ac96e92a3b011fcde90bb7/vllm-0.5.1.tar.gz", hash = "sha256:c7b6d01ec0644dd251e00a52bd08f9a172da2185dce87538deb56c577120e0eb", size = 790562 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/43/a0fbf45a2ccf038ea05774fba715633c70f7a26c7b3270c337038f9f77c0/vllm-0.5.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:562e3dd1d54ad0afb299e64412322b372976faaebb70a7db8e4a6c4ef5ed67c4", size = 146906281 }, + { url = "https://files.pythonhosted.org/packages/13/79/e212b0eae62716be0d75994b71b7f97f4a45a7ffd8533408bcd3cde6a7e0/vllm-0.5.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:8b7483adcfc115dc7237663636a0976943fa65cde6f616a3839f0de69a419b5c", size = 146906182 }, + { url = "https://files.pythonhosted.org/packages/72/89/671a176610d4ffe04216dccde6bffd07a8c67f4c0c45ce8be60266d7f574/vllm-0.5.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e6cad87335713cfc6710bc44c2688a6158abc3184d3530731e89b948617b2c7", size = 146906268 }, +] + +[[package]] +name = "vllm-flash-attn" +version = "2.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/35/b00f367f3655bfed85a0df3c32fe6af34c0fd38b28ec7f0f9363991615f1/vllm_flash_attn-2.5.9-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:3e33af65880e99c47ff636f465d45c08e5e4c2eedbc06c9970599a6408768944", size = 37095920 }, + { url = "https://files.pythonhosted.org/packages/36/42/307d9afa8c884efd369f7e402e69a588dea22b23fb23ce540b9ef6a284b0/vllm_flash_attn-2.5.9-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:88a63365db204859131fd8b3a0e85ea3b8b726f4be5ddbf2c6211a94a2cf4258", size = 37112678 }, + { url = "https://files.pythonhosted.org/packages/f4/f2/155a33878f747b09250cf5a73e0449f52d23e723c770913952aa5340d66a/vllm_flash_attn-2.5.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:384845ae339946cb46e11cdfb5fadda1c0e519d2312b595e04da0df67a6d5383", size = 37092795 }, +] + +[[package]] +name = "watchfiles" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/02/22fcaed0396730b0d362bc8d1ffb3be2658fd473eecbb2ba84243e157f11/watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", size = 395212 }, + { url = "https://files.pythonhosted.org/packages/e9/3d/ec5a2369a46edf3ebe092c39d9ae48e8cb6dacbde51c4b4f98936c524269/watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", size = 384815 }, + { url = "https://files.pythonhosted.org/packages/df/b4/898991cececbe171e67142c31905510203649569d9817848f47c4177ee42/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", size = 450680 }, + { url = "https://files.pythonhosted.org/packages/58/f7/d4aa3000e812cfb5e5c2c6c0a3ec9d0a46a42489a8727edd160631c4e210/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", size = 455923 }, + { url = "https://files.pythonhosted.org/packages/dd/95/7e2e4c6aba1b02fb5c76d2f6a450b85215921ec5f8f7ad5efd075369563f/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", size = 482339 }, + { url = "https://files.pythonhosted.org/packages/bb/67/4265b0fabcc2ef2c9e3e8802ba7908cf718a357ebfb49c72e53787156a48/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", size = 519908 }, + { url = "https://files.pythonhosted.org/packages/0d/96/b57802d5f8164bdf070befb4fd3dec4edba5a364ec0670965a97eb8098ce/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", size = 501410 }, + { url = "https://files.pythonhosted.org/packages/8b/18/6db0de4e8911ba14e31853201b40c0fa9fea5ecf3feb86b0ad58f006dfc3/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", size = 452876 }, + { url = "https://files.pythonhosted.org/packages/df/df/092a961815edf723a38ba2638c49491365943919c3526cc9cf82c42786a6/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", size = 615353 }, + { url = "https://files.pythonhosted.org/packages/f3/cf/b85fe645de4ff82f3f436c5e9032379fce37c303f6396a18f9726cc34519/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", size = 613187 }, + { url = "https://files.pythonhosted.org/packages/f6/d4/a9fea27aef4dd69689bc3556718c1157a7accb72aa035ece87c1fa8483b5/watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", size = 270799 }, + { url = "https://files.pythonhosted.org/packages/df/02/dbe9d4439f15dd4ad0720b6e039bde9d66d1f830331f34c18eb70fa6608e/watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", size = 284145 }, + { url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 }, + { url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 }, + { url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 }, + { url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 }, + { url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 }, + { url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 }, + { url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 }, + { url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 }, + { url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 }, + { url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 }, + { url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 }, + { url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 }, + { url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 }, + { url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 }, + { url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 }, + { url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 }, + { url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 }, + { url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 }, + { url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 }, + { url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 }, + { url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 }, + { url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 }, + { url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 }, + { url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 }, + { url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 }, + { url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 }, + { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 }, + { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 }, + { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 }, + { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 }, + { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 }, + { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 }, + { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 }, + { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 }, + { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 }, + { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, + { url = "https://files.pythonhosted.org/packages/15/81/54484fc2fa715abe79694b975692af963f0878fb9d72b8251aa542bf3f10/watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21", size = 394967 }, + { url = "https://files.pythonhosted.org/packages/14/b3/557f0cd90add86586fe3deeebd11e8299db6bc3452b44a534f844c6ab831/watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0", size = 384707 }, + { url = "https://files.pythonhosted.org/packages/03/a3/34638e1bffcb85a405e7b005e30bb211fd9be2ab2cb1847f2ceb81bef27b/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff", size = 450442 }, + { url = "https://files.pythonhosted.org/packages/8f/9f/6a97460dd11a606003d634c7158d9fea8517e98daffc6f56d0f5fde2e86a/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a", size = 455959 }, + { url = "https://files.pythonhosted.org/packages/9d/bb/e0648c6364e4d37ec692bc3f0c77507d17d8bb8f75689148819142010bbf/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a", size = 483187 }, + { url = "https://files.pythonhosted.org/packages/dd/ad/d9290586a25288a81dfa8ad6329cf1de32aa1a9798ace45259eb95dcfb37/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8", size = 519733 }, + { url = "https://files.pythonhosted.org/packages/4e/a9/150c1666825cc9637093f8cae7fc6f53b3296311ab8bd65f1389acb717cb/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3", size = 502275 }, + { url = "https://files.pythonhosted.org/packages/44/dc/5bfd21e20a330aca1706ac44713bc322838061938edf4b53130f97a7b211/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf", size = 452907 }, + { url = "https://files.pythonhosted.org/packages/50/fe/8f4fc488f1699f564687b697456eb5c0cb8e2b0b8538150511c234c62094/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a", size = 615927 }, + { url = "https://files.pythonhosted.org/packages/ad/19/2e45f6f6eec89dd97a4d281635e3d73c17e5f692e7432063bdfdf9562c89/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b", size = 613435 }, + { url = "https://files.pythonhosted.org/packages/91/17/dc5ac62ca377827c24321d68050efc2eaee2ebaf3f21d055bbce2206d309/watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27", size = 270810 }, + { url = "https://files.pythonhosted.org/packages/82/2b/dad851342492d538e7ffe72a8c756f747dd147988abb039ac9d6577d2235/watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43", size = 284866 }, + { url = "https://files.pythonhosted.org/packages/6f/06/175d5ac6b838fb319008c0cd981d7bf289317c510154d411d3584ca2b67b/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", size = 396269 }, + { url = "https://files.pythonhosted.org/packages/86/ee/5db93b0b57dc0587abdbac4149296ee73275f615d790a82cb5598af0557f/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", size = 386010 }, + { url = "https://files.pythonhosted.org/packages/75/61/fe0dc5fedf152bfc085a53711f740701f6bdb8ab6b5c950402b681d4858b/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", size = 450913 }, + { url = "https://files.pythonhosted.org/packages/9f/dd/3c7731af3baf1a9957afc643d176f94480921a690ec3237c9f9d11301c08/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", size = 453474 }, + { url = "https://files.pythonhosted.org/packages/6b/b4/c3998f54c91a35cee60ee6d3a855a069c5dff2bae6865147a46e9090dccd/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3", size = 395565 }, + { url = "https://files.pythonhosted.org/packages/3f/05/ac1a4d235beb9ddfb8ac26ce93a00ba6bd1b1b43051ef12d7da957b4a9d1/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e", size = 385406 }, + { url = "https://files.pythonhosted.org/packages/4c/ea/36532e7d86525f4e52a10efed182abf33efb106a93d49f5fbc994b256bcd/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb", size = 450424 }, + { url = "https://files.pythonhosted.org/packages/7a/e9/3cbcf4d70cd0b6d3f30631deae1bf37cc0be39887ca327a44462fe546bf5/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42", size = 452488 }, +] + +[[package]] +name = "websockets" +version = "14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/fa/76607eb7dcec27b2d18d63f60a32e60e2b8629780f343bb83a4dbb9f4350/websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885", size = 163089 }, + { url = "https://files.pythonhosted.org/packages/9e/00/ad2246b5030575b79e7af0721810fdaecaf94c4b2625842ef7a756fa06dd/websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397", size = 160741 }, + { url = "https://files.pythonhosted.org/packages/72/f7/60f10924d333a28a1ff3fcdec85acf226281331bdabe9ad74947e1b7fc0a/websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610", size = 160996 }, + { url = "https://files.pythonhosted.org/packages/63/7c/c655789cf78648c01ac6ecbe2d6c18f91b75bdc263ffee4d08ce628d12f0/websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3", size = 169974 }, + { url = "https://files.pythonhosted.org/packages/fb/5b/013ed8b4611857ac92ac631079c08d9715b388bd1d88ec62e245f87a39df/websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980", size = 168985 }, + { url = "https://files.pythonhosted.org/packages/cd/33/aa3e32fd0df213a5a442310754fe3f89dd87a0b8e5b4e11e0991dd3bcc50/websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8", size = 169297 }, + { url = "https://files.pythonhosted.org/packages/93/17/dae0174883d6399f57853ac44abf5f228eaba86d98d160f390ffabc19b6e/websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7", size = 169677 }, + { url = "https://files.pythonhosted.org/packages/42/e2/0375af7ac00169b98647c804651c515054b34977b6c1354f1458e4116c1e/websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f", size = 169089 }, + { url = "https://files.pythonhosted.org/packages/73/8d/80f71d2a351a44b602859af65261d3dde3a0ce4e76cf9383738a949e0cc3/websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d", size = 169026 }, + { url = "https://files.pythonhosted.org/packages/48/97/173b1fa6052223e52bb4054a141433ad74931d94c575e04b654200b98ca4/websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d", size = 163967 }, + { url = "https://files.pythonhosted.org/packages/c0/5b/2fcf60f38252a4562b28b66077e0d2b48f91fef645d5f78874cd1dec807b/websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2", size = 164413 }, + { url = "https://files.pythonhosted.org/packages/15/b6/504695fb9a33df0ca56d157f5985660b5fc5b4bf8c78f121578d2d653392/websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166", size = 163088 }, + { url = "https://files.pythonhosted.org/packages/81/26/ebfb8f6abe963c795122439c6433c4ae1e061aaedfc7eff32d09394afbae/websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f", size = 160745 }, + { url = "https://files.pythonhosted.org/packages/a1/c6/1435ad6f6dcbff80bb95e8986704c3174da8866ddb751184046f5c139ef6/websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910", size = 160995 }, + { url = "https://files.pythonhosted.org/packages/96/63/900c27cfe8be1a1f2433fc77cd46771cf26ba57e6bdc7cf9e63644a61863/websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c", size = 170543 }, + { url = "https://files.pythonhosted.org/packages/00/8b/bec2bdba92af0762d42d4410593c1d7d28e9bfd952c97a3729df603dc6ea/websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473", size = 169546 }, + { url = "https://files.pythonhosted.org/packages/6b/a9/37531cb5b994f12a57dec3da2200ef7aadffef82d888a4c29a0d781568e4/websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473", size = 169911 }, + { url = "https://files.pythonhosted.org/packages/60/d5/a6eadba2ed9f7e65d677fec539ab14a9b83de2b484ab5fe15d3d6d208c28/websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56", size = 170183 }, + { url = "https://files.pythonhosted.org/packages/76/57/a338ccb00d1df881c1d1ee1f2a20c9c1b5b29b51e9e0191ee515d254fea6/websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142", size = 169623 }, + { url = "https://files.pythonhosted.org/packages/64/22/e5f7c33db0cb2c1d03b79fd60d189a1da044e2661f5fd01d629451e1db89/websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d", size = 169583 }, + { url = "https://files.pythonhosted.org/packages/aa/2e/2b4662237060063a22e5fc40d46300a07142afe30302b634b4eebd717c07/websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a", size = 163969 }, + { url = "https://files.pythonhosted.org/packages/94/a5/0cda64e1851e73fc1ecdae6f42487babb06e55cb2f0dc8904b81d8ef6857/websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b", size = 164408 }, + { url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 }, + { url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 }, + { url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 }, + { url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 }, + { url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 }, + { url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 }, + { url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 }, + { url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 }, + { url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 }, + { url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 }, + { url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 }, + { url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 }, + { url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 }, + { url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 }, + { url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 }, + { url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 }, + { url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 }, + { url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 }, + { url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 }, + { url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 }, + { url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 }, + { url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 }, + { url = "https://files.pythonhosted.org/packages/6f/eb/367e0ed7b8a960b4fc12c7c6bf3ebddf06875037de641637994849560d47/websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe", size = 163087 }, + { url = "https://files.pythonhosted.org/packages/96/f7/1f18d028ec4a2c14598dfec6a73381a915c27464b693873198c1de872095/websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12", size = 160740 }, + { url = "https://files.pythonhosted.org/packages/5c/db/b4b353fb9c3f0eaa8138ea4c76e6fa555b6d2821ed2d51d0ac3c320bc57e/websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7", size = 160992 }, + { url = "https://files.pythonhosted.org/packages/b9/b1/9149e420c61f375e432654d5c1545e563b90ac1f829ee1a8d1dccaf0869d/websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5", size = 169757 }, + { url = "https://files.pythonhosted.org/packages/2b/33/0bb58204191e113212360f1392b6b1e9f85f62c7ca5b3b15f52f2f835516/websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0", size = 168762 }, + { url = "https://files.pythonhosted.org/packages/be/3d/c3c192f16210d7b7535fbf4ee9a299612f4dccff665587617b13fa0a6aa3/websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258", size = 169060 }, + { url = "https://files.pythonhosted.org/packages/a6/73/75efa8d9e4b1b257818a7b7a0b9ac84a07c91120b52148941370ef2c8f16/websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0", size = 169457 }, + { url = "https://files.pythonhosted.org/packages/a4/11/300cf36cfd6990ffb218394862f0513be8c21917c9ff5e362f94599caedd/websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4", size = 168860 }, + { url = "https://files.pythonhosted.org/packages/c0/3d/5fd82500714ab7c09f003bde671dad1a3a131ac77b6b11ada72e466de4f6/websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc", size = 168825 }, + { url = "https://files.pythonhosted.org/packages/88/16/715580eb6caaacc232f303e9619103a42dcd354b0854baa5ed26aacaf828/websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661", size = 163960 }, + { url = "https://files.pythonhosted.org/packages/63/a7/a1035cb198eaa12eaa9621aaaa3ec021b0e3bac96e1df9ceb6bfe5e53e5f/websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef", size = 164424 }, + { url = "https://files.pythonhosted.org/packages/10/3d/91d3d2bb1325cd83e8e2c02d0262c7d4426dc8fa0831ef1aa4d6bf2041af/websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29", size = 160773 }, + { url = "https://files.pythonhosted.org/packages/33/7c/cdedadfef7381939577858b1b5718a4ab073adbb584e429dd9d9dc9bfe16/websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c", size = 161007 }, + { url = "https://files.pythonhosted.org/packages/ca/35/7a20a3c450b27c04e50fbbfc3dfb161ed8e827b2a26ae31c4b59b018b8c6/websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2", size = 162264 }, + { url = "https://files.pythonhosted.org/packages/e8/9c/e3f9600564b0c813f2448375cf28b47dc42c514344faed3a05d71fb527f9/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c", size = 161873 }, + { url = "https://files.pythonhosted.org/packages/3f/37/260f189b16b2b8290d6ae80c9f96d8b34692cf1bb3475df54c38d3deb57d/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a", size = 161818 }, + { url = "https://files.pythonhosted.org/packages/ff/1e/e47dedac8bf7140e59aa6a679e850c4df9610ae844d71b6015263ddea37b/websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3", size = 164465 }, + { url = "https://files.pythonhosted.org/packages/f7/c0/8e9325c4987dcf66d4a0d63ec380d4aefe8dcc1e521af71ad17adf2c1ae2/websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f", size = 160773 }, + { url = "https://files.pythonhosted.org/packages/5a/6e/c9a7f2edd4afddc4f8cccfc4e12468b7f6ec40f28d1b1e966a8d0298b875/websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42", size = 161006 }, + { url = "https://files.pythonhosted.org/packages/f3/10/b90ece894828c954e674a81cb0db250e6c324c54db30a8b19e96431f928f/websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f", size = 162260 }, + { url = "https://files.pythonhosted.org/packages/52/93/1147b6b5464a5fb6e8987da3ec7991dcc44f9090f67d9c841d7382fed429/websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574", size = 161868 }, + { url = "https://files.pythonhosted.org/packages/32/ab/f7d80b4049bff0aa617507330db3a27389d0e70df54e29f7a3d76bbd2086/websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270", size = 161813 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/adc9fb85f031b8df8e9f3d96cc004df25d2643e503953af5223c5b6825b7/websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365", size = 164457 }, + { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, +] + +[[package]] +name = "xformers" +version = "0.0.26.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d3/5eab1cb3171fc9d4e5613025e965b72727f6d12ae820d2d7ab9c37320eb6/xformers-0.0.26.post1.tar.gz", hash = "sha256:1d14b5f999ede649198379b0470ebdd25007ba224ae336ef958124158a6de8b1", size = 4087142 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/e7/27003645ef99e7571fb6964cd2f39da3f1b3f3011aa00bb2d3ac9b790757/xformers-0.0.26.post1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1fe0f9a3dddb7f6175c2e34de2f318d6742eec28c068b8334b093016b2c9c2c8", size = 222745400 }, + { url = "https://files.pythonhosted.org/packages/7a/53/f4a36d24f6c2d06fccc356986bb45c2cb2fd06609986d44a925d57f38070/xformers-0.0.26.post1-cp310-cp310-win_amd64.whl", hash = "sha256:e34b8dd6982077bee0c8eb2db8bc1513177201bfe0af890a4db42d8d31c966a5", size = 208730032 }, + { url = "https://files.pythonhosted.org/packages/ce/72/58e13d633efada336f65e23290dfc0bc9497c60fd9b8fdaab59b555b5f28/xformers-0.0.26.post1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0d35615870b9237077aec51802a5a821f9e9d2708730c8914d5cff301fb29558", size = 222780107 }, + { url = "https://files.pythonhosted.org/packages/bb/6b/ac99df9545d965c11172ec3613b4652d6f0f9098328551cfe0bbbfa634fe/xformers-0.0.26.post1-cp311-cp311-win_amd64.whl", hash = "sha256:d05c547b4ba603fc8e21fad03a342138eaaece35fe0a1692e6ee0d061ddd21ac", size = 208727774 }, + { url = "https://files.pythonhosted.org/packages/1d/42/7728b1d16425d3f5505eeb70749e15a71257e126a90ab44184d231d00341/xformers-0.0.26.post1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:16dcfa801450a03f148d6d5f7ba2b5540a27c52517946570fe0e1f75238d73a1", size = 222743742 }, + { url = "https://files.pythonhosted.org/packages/28/f7/452f15a1a69ef553f05119e03f4c26fa683793491520c88d0055ca90c9ec/xformers-0.0.26.post1-cp39-cp39-win_amd64.whl", hash = "sha256:cf267bac8f4454db1056e4e6ff9e4df1e02b2b31d8116d50913af15fa6ae7132", size = 208757765 }, +] + +[[package]] +name = "xxhash" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8a/0e9feca390d512d293afd844d31670e25608c4a901e10202aa98785eab09/xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212", size = 31970 }, + { url = "https://files.pythonhosted.org/packages/16/e6/be5aa49580cd064a18200ab78e29b88b1127e1a8c7955eb8ecf81f2626eb/xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520", size = 30801 }, + { url = "https://files.pythonhosted.org/packages/20/ee/b8a99ebbc6d1113b3a3f09e747fa318c3cde5b04bd9c197688fadf0eeae8/xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680", size = 220927 }, + { url = "https://files.pythonhosted.org/packages/58/62/15d10582ef159283a5c2b47f6d799fc3303fe3911d5bb0bcc820e1ef7ff4/xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da", size = 200360 }, + { url = "https://files.pythonhosted.org/packages/23/41/61202663ea9b1bd8e53673b8ec9e2619989353dba8cfb68e59a9cbd9ffe3/xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23", size = 428528 }, + { url = "https://files.pythonhosted.org/packages/f2/07/d9a3059f702dec5b3b703737afb6dda32f304f6e9da181a229dafd052c29/xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196", size = 194149 }, + { url = "https://files.pythonhosted.org/packages/eb/58/27caadf78226ecf1d62dbd0c01d152ed381c14c1ee4ad01f0d460fc40eac/xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c", size = 207703 }, + { url = "https://files.pythonhosted.org/packages/b1/08/32d558ce23e1e068453c39aed7b3c1cdc690c177873ec0ca3a90d5808765/xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482", size = 216255 }, + { url = "https://files.pythonhosted.org/packages/3f/d4/2b971e2d2b0a61045f842b622ef11e94096cf1f12cd448b6fd426e80e0e2/xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296", size = 202744 }, + { url = "https://files.pythonhosted.org/packages/19/ae/6a6438864a8c4c39915d7b65effd85392ebe22710412902487e51769146d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415", size = 210115 }, + { url = "https://files.pythonhosted.org/packages/48/7d/b3c27c27d1fc868094d02fe4498ccce8cec9fcc591825c01d6bcb0b4fc49/xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198", size = 414247 }, + { url = "https://files.pythonhosted.org/packages/a1/05/918f9e7d2fbbd334b829997045d341d6239b563c44e683b9a7ef8fe50f5d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442", size = 191419 }, + { url = "https://files.pythonhosted.org/packages/08/29/dfe393805b2f86bfc47c290b275f0b7c189dc2f4e136fd4754f32eb18a8d/xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da", size = 30114 }, + { url = "https://files.pythonhosted.org/packages/7b/d7/aa0b22c4ebb7c3ccb993d4c565132abc641cd11164f8952d89eb6a501909/xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9", size = 30003 }, + { url = "https://files.pythonhosted.org/packages/69/12/f969b81541ee91b55f1ce469d7ab55079593c80d04fd01691b550e535000/xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6", size = 26773 }, + { url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8", size = 30800 }, + { url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166", size = 221566 }, + { url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7", size = 201214 }, + { url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623", size = 429433 }, + { url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a", size = 194822 }, + { url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88", size = 208538 }, + { url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c", size = 216953 }, + { url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2", size = 203594 }, + { url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084", size = 210971 }, + { url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d", size = 415050 }, + { url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839", size = 192216 }, + { url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da", size = 30120 }, + { url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58", size = 30003 }, + { url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3", size = 26777 }, + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787 }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959 }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006 }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326 }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380 }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934 }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301 }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351 }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294 }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674 }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022 }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170 }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040 }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796 }, + { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795 }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792 }, + { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950 }, + { url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980 }, + { url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324 }, + { url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370 }, + { url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911 }, + { url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352 }, + { url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410 }, + { url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322 }, + { url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725 }, + { url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070 }, + { url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172 }, + { url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041 }, + { url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801 }, + { url = "https://files.pythonhosted.org/packages/d4/f6/531dd6858adf8877675270b9d6989b6dacfd1c2d7135b17584fc29866df3/xxhash-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc8cdd7f33d57f0468b0614ae634cc38ab9202c6957a60e31d285a71ebe0301", size = 31971 }, + { url = "https://files.pythonhosted.org/packages/7c/a8/b2a42b6c9ae46e233f474f3d307c2e7bca8d9817650babeca048d2ad01d6/xxhash-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0c48b6300cd0b0106bf49169c3e0536408dfbeb1ccb53180068a18b03c662ab", size = 30801 }, + { url = "https://files.pythonhosted.org/packages/b4/92/9ac297e3487818f429bcf369c1c6a097edf5b56ed6fc1feff4c1882e87ef/xxhash-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1a92cfbaa0a1253e339ccec42dbe6db262615e52df591b68726ab10338003f", size = 220644 }, + { url = "https://files.pythonhosted.org/packages/86/48/c1426dd3c86fc4a52f983301867463472f6a9013fb32d15991e60c9919b6/xxhash-3.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33513d6cc3ed3b559134fb307aae9bdd94d7e7c02907b37896a6c45ff9ce51bd", size = 200021 }, + { url = "https://files.pythonhosted.org/packages/f3/de/0ab8c79993765c94fc0d0c1a22b454483c58a0161e1b562f58b654f47660/xxhash-3.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eefc37f6138f522e771ac6db71a6d4838ec7933939676f3753eafd7d3f4c40bc", size = 428217 }, + { url = "https://files.pythonhosted.org/packages/b4/b4/332647451ed7d2c021294b7c1e9c144dbb5586b1fb214ad4f5a404642835/xxhash-3.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a606c8070ada8aa2a88e181773fa1ef17ba65ce5dd168b9d08038e2a61b33754", size = 193868 }, + { url = "https://files.pythonhosted.org/packages/f4/1c/a42c0a6cac752f84f7b44a90d1a9fa9047cf70bdba5198a304fde7cc471f/xxhash-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42eca420c8fa072cc1dd62597635d140e78e384a79bb4944f825fbef8bfeeef6", size = 207403 }, + { url = "https://files.pythonhosted.org/packages/c4/d7/04e1b0daae9dc9b02c73c1664cc8aa527498c3f66ccbc586eeb25bbe9f14/xxhash-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:604253b2143e13218ff1ef0b59ce67f18b8bd1c4205d2ffda22b09b426386898", size = 215978 }, + { url = "https://files.pythonhosted.org/packages/c4/f4/05e15e67505228fc19ee98a79e427b3a0b9695f5567cd66ced5d66389883/xxhash-3.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6e93a5ad22f434d7876665444a97e713a8f60b5b1a3521e8df11b98309bff833", size = 202416 }, + { url = "https://files.pythonhosted.org/packages/94/fb/e9028d3645bba5412a09de13ee36df276a567e60bdb31d499dafa46d76ae/xxhash-3.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7a46e1d6d2817ba8024de44c4fd79913a90e5f7265434cef97026215b7d30df6", size = 209853 }, + { url = "https://files.pythonhosted.org/packages/02/2c/18c6a622429368274739372d2f86c8125413ec169025c7d8ffb051784bba/xxhash-3.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:30eb2efe6503c379b7ab99c81ba4a779748e3830241f032ab46bd182bf5873af", size = 413926 }, + { url = "https://files.pythonhosted.org/packages/72/bb/5b55c391084a0321c3809632a018b9b657e59d5966289664f85a645942ac/xxhash-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c8aa771ff2c13dd9cda8166d685d7333d389fae30a4d2bb39d63ab5775de8606", size = 191156 }, + { url = "https://files.pythonhosted.org/packages/86/2b/915049db13401792fec159f57e4f4a5ca7a9768e83ef71d6645b9d0cd749/xxhash-3.5.0-cp39-cp39-win32.whl", hash = "sha256:5ed9ebc46f24cf91034544b26b131241b699edbfc99ec5e7f8f3d02d6eb7fba4", size = 30122 }, + { url = "https://files.pythonhosted.org/packages/d5/87/382ef7b24917d7cf4c540ee30f29b283bc87ac5893d2f89b23ea3cdf7d77/xxhash-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:220f3f896c6b8d0316f63f16c077d52c412619e475f9372333474ee15133a558", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/e2/47/d06b24e2d9c3dcabccfd734d11b5bbebfdf59ceac2c61509d8205dd20ac6/xxhash-3.5.0-cp39-cp39-win_arm64.whl", hash = "sha256:a7b1d8315d9b5e9f89eb2933b73afae6ec9597a258d52190944437158b49d38e", size = 26780 }, + { url = "https://files.pythonhosted.org/packages/ab/9a/233606bada5bd6f50b2b72c45de3d9868ad551e83893d2ac86dc7bb8553a/xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c", size = 29732 }, + { url = "https://files.pythonhosted.org/packages/0c/67/f75276ca39e2c6604e3bee6c84e9db8a56a4973fde9bf35989787cf6e8aa/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986", size = 36214 }, + { url = "https://files.pythonhosted.org/packages/0f/f8/f6c61fd794229cc3848d144f73754a0c107854372d7261419dcbbd286299/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6", size = 32020 }, + { url = "https://files.pythonhosted.org/packages/79/d3/c029c99801526f859e6b38d34ab87c08993bf3dcea34b11275775001638a/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b", size = 40515 }, + { url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da", size = 30064 }, + { url = "https://files.pythonhosted.org/packages/c2/56/30d3df421814947f9d782b20c9b7e5e957f3791cbd89874578011daafcbd/xxhash-3.5.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:531af8845aaadcadf951b7e0c1345c6b9c68a990eeb74ff9acd8501a0ad6a1c9", size = 29734 }, + { url = "https://files.pythonhosted.org/packages/82/dd/3c42a1f022ad0d82c852d3cb65493ebac03dcfa8c994465a5fb052b00e3c/xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce379bcaa9fcc00f19affa7773084dd09f5b59947b3fb47a1ceb0179f91aaa1", size = 36216 }, + { url = "https://files.pythonhosted.org/packages/b2/40/8f902ab3bebda228a9b4de69eba988280285a7f7f167b942bc20bb562df9/xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd1b2281d01723f076df3c8188f43f2472248a6b63118b036e641243656b1b0f", size = 32042 }, + { url = "https://files.pythonhosted.org/packages/db/87/bd06beb8ccaa0e9e577c9b909a49cfa5c5cd2ca46034342d72dd9ce5bc56/xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c770750cc80e8694492244bca7251385188bc5597b6a39d98a9f30e8da984e0", size = 40516 }, + { url = "https://files.pythonhosted.org/packages/bb/f8/505385e2fbd753ddcaafd5550eabe86f6232cbebabad3b2508d411b19153/xxhash-3.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b150b8467852e1bd844387459aa6fbe11d7f38b56e901f9f3b3e6aba0d660240", size = 30108 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, + { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, + { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, + { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, + { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, + { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, + { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, + { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, + { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, + { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, + { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, + { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, + { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, + { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, + { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/6a/3b/fec4b08f5e88f68e56ee698a59284a73704df2e0e0b5bdf6536c86e76c76/yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04", size = 142780 }, + { url = "https://files.pythonhosted.org/packages/ed/85/796b0d6a22d536ec8e14bdbb86519250bad980cec450b6e299b1c2a9079e/yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719", size = 94981 }, + { url = "https://files.pythonhosted.org/packages/ee/0e/a830fd2238f7a29050f6dd0de748b3d6f33a7dbb67dbbc081a970b2bbbeb/yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e", size = 92789 }, + { url = "https://files.pythonhosted.org/packages/0f/4f/438c9fd668954779e48f08c0688ee25e0673380a21bb1e8ccc56de5b55d7/yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee", size = 317327 }, + { url = "https://files.pythonhosted.org/packages/bd/79/a78066f06179b4ed4581186c136c12fcfb928c475cbeb23743e71a991935/yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789", size = 336999 }, + { url = "https://files.pythonhosted.org/packages/55/02/527963cf65f34a06aed1e766ff9a3b3e7d0eaa1c90736b2948a62e528e1d/yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8", size = 331693 }, + { url = "https://files.pythonhosted.org/packages/a2/2a/167447ae39252ba624b98b8c13c0ba35994d40d9110e8a724c83dbbb5822/yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c", size = 321473 }, + { url = "https://files.pythonhosted.org/packages/55/03/07955fabb20082373be311c91fd78abe458bc7ff9069d34385e8bddad20e/yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5", size = 313571 }, + { url = "https://files.pythonhosted.org/packages/95/e2/67c8d3ec58a8cd8ddb1d63bd06eb7e7b91c9f148707a3eeb5a7ed87df0ef/yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1", size = 325004 }, + { url = "https://files.pythonhosted.org/packages/06/43/51ceb3e427368fe6ccd9eccd162be227fd082523e02bad1fd3063daf68da/yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24", size = 322677 }, + { url = "https://files.pythonhosted.org/packages/e4/0e/7ef286bfb23267739a703f7b967a858e2128c10bea898de8fa027e962521/yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318", size = 332806 }, + { url = "https://files.pythonhosted.org/packages/c8/94/2d1f060f4bfa47c8bd0bcb652bfe71fba881564bcac06ebb6d8ced9ac3bc/yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985", size = 339919 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/73b5f9a6ab69acddf1ca1d5e7bc92f50b69124512e6c26b36844531d7f23/yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910", size = 340960 }, + { url = "https://files.pythonhosted.org/packages/41/13/ce6bc32be4476b60f4f8694831f49590884b2c975afcffc8d533bf2be7ec/yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1", size = 336592 }, + { url = "https://files.pythonhosted.org/packages/81/d5/6e0460292d6299ac3919945f912b16b104f4e81ab20bf53e0872a1296daf/yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5", size = 84833 }, + { url = "https://files.pythonhosted.org/packages/b2/fc/a8aef69156ad5508165d8ae956736d55c3a68890610834bd985540966008/yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9", size = 90968 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] From 9fc6df9fcd6d2d7675f02b23ccfd96e5a0f6985e Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 19 Feb 2025 07:57:53 -0800 Subject: [PATCH 163/505] Fix documentation error #1426 --- docs/reference/models/openai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/models/openai.md b/docs/reference/models/openai.md index 638107568..0777ad8b7 100644 --- a/docs/reference/models/openai.md +++ b/docs/reference/models/openai.md @@ -131,7 +131,7 @@ class Person(BaseModel): last_name: str age: int -generate.json(model, Person) +generator = generate.json(model, Person) generator("current indian prime minister on january 1st 2023") # Person(first_name='Narendra', last_name='Modi', age=72) From 7085885d975d0bb0d7b29927df41260ad5cfa48d Mon Sep 17 00:00:00 2001 From: Pedro Larroy Date: Tue, 18 Feb 2025 16:45:29 -0800 Subject: [PATCH 164/505] Make cache dir more robust inside docker containers and try to use XDG_CACHE_HOME if available. --- outlines/caching.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/outlines/caching.py b/outlines/caching.py index 6fdda6214..04c655943 100644 --- a/outlines/caching.py +++ b/outlines/caching.py @@ -2,6 +2,7 @@ import contextlib import functools import os +import tempfile from typing import Callable, Optional import cloudpickle @@ -50,8 +51,17 @@ def get_cache(): """ from outlines._version import __version__ as outlines_version # type: ignore - home_dir = os.path.expanduser("~") - cache_dir = os.environ.get("OUTLINES_CACHE_DIR", f"{home_dir}/.cache/outlines") + xdg_cache_home = os.environ.get("XDG_CACHE_HOME") + if xdg_cache_home: + cache_dir = os.path.join(xdg_cache_home, ".cache", "outlines") + else: + home_dir = os.path.normpath(os.path.expanduser("~")) + if home_dir != "/": + cache_dir = os.environ.get("OUTLINES_CACHE_DIR", f"{home_dir}/.cache/outlines") + else: + tempdir = tempfile.gettempdir() + cache_dir = os.path.join(tempdir, ".cache", "outlines") + memory = Cache( cache_dir, eviction_policy="none", From d8ccb844e55b653089458f22bc15474e0e5546c2 Mon Sep 17 00:00:00 2001 From: Pedro Larroy Date: Tue, 18 Feb 2025 16:50:25 -0800 Subject: [PATCH 165/505] Make cache dir more robust inside docker containers and try to use XDG_CACHE_HOME if available. --- outlines/caching.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/outlines/caching.py b/outlines/caching.py index 04c655943..0831c40bb 100644 --- a/outlines/caching.py +++ b/outlines/caching.py @@ -51,16 +51,20 @@ def get_cache(): """ from outlines._version import __version__ as outlines_version # type: ignore + outlines_cache_dir = os.environ.get('OUTLINES_CACHE_DIR') xdg_cache_home = os.environ.get("XDG_CACHE_HOME") - if xdg_cache_home: + home_dir = os.path.normpath(os.path.expanduser("~")) + if outlines_cache_dir: + # OUTLINES_CACHE_DIR takes precendence + cache_dir = outlines_cache_dir + elif xdg_cache_home: cache_dir = os.path.join(xdg_cache_home, ".cache", "outlines") + elif home_dir != "/": + cache_dir = os.path.join(home_dir, ".cache", "outlines") else: - home_dir = os.path.normpath(os.path.expanduser("~")) - if home_dir != "/": - cache_dir = os.environ.get("OUTLINES_CACHE_DIR", f"{home_dir}/.cache/outlines") - else: - tempdir = tempfile.gettempdir() - cache_dir = os.path.join(tempdir, ".cache", "outlines") + # home_dir may be / inside a docker container without existing user + tempdir = tempfile.gettempdir() + cache_dir = os.path.join(tempdir, ".cache", "outlines") memory = Cache( cache_dir, From da1b0db5658260f92d8b5529c9a600933e36f4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sun, 26 Jan 2025 19:47:29 +0100 Subject: [PATCH 166/505] Add regex DSL and re-organize custom types --- docs/reference/functions.md | 1 - docs/reference/regex_dsl.md | 316 ++++++++++++++++++++++++ mkdocs.yml | 1 + outlines/fsm/types.py | 40 ++- outlines/generate/format.py | 1 + outlines/generate/regex.py | 11 +- outlines/types/__init__.py | 43 +++- outlines/types/airports.py | 2 - outlines/types/dsl.py | 395 ++++++++++++++++++++++++++++++ outlines/types/email.py | 11 - outlines/types/isbn.py | 12 - outlines/types/locale/__init__.py | 1 + outlines/types/locale/us.py | 4 + outlines/types/locales.py | 21 -- outlines/types/phone_numbers.py | 16 -- outlines/types/zip_codes.py | 13 - pyproject.toml | 1 + tests/fsm/test_types.py | 31 +-- tests/test_types.py | 103 -------- tests/types/test_custom_types.py | 111 +++++++++ tests/types/test_dsl.py | 256 +++++++++++++++++++ tests/types/test_to_regex.py | 71 ++++++ 22 files changed, 1233 insertions(+), 228 deletions(-) delete mode 100644 docs/reference/functions.md create mode 100644 docs/reference/regex_dsl.md create mode 100644 outlines/types/dsl.py delete mode 100644 outlines/types/email.py delete mode 100644 outlines/types/isbn.py create mode 100644 outlines/types/locale/__init__.py create mode 100644 outlines/types/locale/us.py delete mode 100644 outlines/types/locales.py delete mode 100644 outlines/types/phone_numbers.py delete mode 100644 outlines/types/zip_codes.py delete mode 100644 tests/test_types.py create mode 100644 tests/types/test_custom_types.py create mode 100644 tests/types/test_dsl.py create mode 100644 tests/types/test_to_regex.py diff --git a/docs/reference/functions.md b/docs/reference/functions.md deleted file mode 100644 index d29213a89..000000000 --- a/docs/reference/functions.md +++ /dev/null @@ -1 +0,0 @@ -# Outlines functions diff --git a/docs/reference/regex_dsl.md b/docs/reference/regex_dsl.md new file mode 100644 index 000000000..aa04303cf --- /dev/null +++ b/docs/reference/regex_dsl.md @@ -0,0 +1,316 @@ +# DSL to express constraints + +This library provides a Domain-Specific Language (DSL) to construct regular expressions in a more intuitive and modular way. It allows you to create complex regexes using simple building blocks that represent literal strings, patterns, and various quantifiers. Additionally, these custom regex types can be used directly as types in [Pydantic](https://pydantic-docs.helpmanual.io/) schemas to enforce pattern constraints during text generation. + +--- + +## Why Use This DSL? + +1. **Modularity & Readability**: Instead of writing cryptic regular expression strings, you compose a regex as a tree of objects. +2. **Enhanced Debugging**: Each expression can be visualized as an ASCII tree, making it easier to understand and debug complex regexes. +3. **Pydantic Integration**: Use your DSL-defined regex as types in Pydantic models. The DSL seamlessly converts to JSON Schema with proper pattern constraints. +4. **Extensibility**: Easily add or modify quantifiers and other regex components by extending the provided classes. + +--- + +## Building Blocks + + +Every regex component in this DSL is a **Term**. Here are two primary types: + +- **`String`**: Represents a literal string. It escapes the characters that have a special meaning in regular expressions. +- **`Regex`**: Represents an existing regex pattern string. + +```python +from outlines.types import String, Regex + +# A literal string "hello" +literal = String("hello") # Internally represents "hello" + +# A regex pattern to match one or more digits +digit = Regex(r"[0-9]+") # Internally represents the pattern [0-9]+ + +# Converting to standard regex strings: +from outlines.types.dsl import to_regex + +print(to_regex(literal)) # Output: hello +print(to_regex(digit)) # Output: [0-9]+ +``` + +--- + +## Early Introduction to Quantifiers & Operators + +The DSL supports common regex quantifiers as methods on every `Term`. These methods allow you to specify how many times a pattern should be matched. They include: + +- **`times(count)`**: Matches the term exactly `count` times. +- **`optional()`**: Matches the term zero or one time. +- **`one_or_more()`**: Matches the term one or more times (Kleene Plus). +- **`zero_or_more()`**: Matches the term zero or more times (Kleene Star). +- **`repeat(min_count, max_count)`**: Matches the term between `min_count` and `max_count` times (or open-ended if one value is omitted). + +Let’s see these quantifiers side by side with examples. + +### Quantifiers in Action + +#### `times(count)` + +This method restricts the term to appear exactly `count` times. + +```python +# Example: exactly 5 digits +five_digits = Regex(r"\d").times(5) +print(to_regex(five_digits)) # Output: (\d){5} +``` + +You can also use the `times` function: + +```python +from outlines.types import times + +# Example: exactly 5 digits +five_digits = times(Regex(r"\d"), 5) +print(to_regex(five_digits)) # Output: (\d){5} +``` + +#### `optional()` + +The `optional()` method makes a term optional, meaning it may occur zero or one time. + +```python +# Example: an optional "s" at the end of a word +maybe_s = String("s").optional() +print(to_regex(maybe_s)) # Output: (s)? +``` + +You can also use the `optional` function, the string will automatically be converted to a `String` object: + +```python +from outlines.types import optional + +# Example: an optional "s" at the end of a word +maybe_s = optional("s") +print(to_regex(maybe_s)) # Output: (s)? +``` + +#### `one_or_more()` + +This method indicates that the term must appear at least once. + +```python +# Example: one or more alphabetic characters +letters = Regex(r"[A-Za-z]").one_or_more() +print(to_regex(letters)) # Output: ([A-Za-z])+ +``` + +You can also use the `one_or_more` function: + +```python +from outlines.types import one_or_more + +# Example: one or more alphabetic characters +letters = one_or_more(Regex(r"[A-Za-z]")) +print(to_regex(letters)) # Output: ([A-Za-z])+ + +``` + +#### `zero_or_more()` + +This method means that the term can occur zero or more times. + +```python +# Example: zero or more spaces +spaces = String(" ").zero_or_more() +print(to_regex(spaces)) # Output: ( )* +``` + +You can also use the `zero_or_more` function, the string will automatically be converted to a `String` instance: + +```python +from outlines.types import zero_or_more + +# Example: zero or more spaces +spaces = zero_or_more(" ") +print(to_regex(spaces)) # Output: ( )* +``` + +#### `repeat(min_count, max_count)` + +The `repeat` method provides flexibility to set a lower and/or upper bound on the number of occurrences. + +```python +# Example: Between 2 and 4 word characters +word_chars = Regex(r"\w").repeat(2, 4) +print(to_regex(word_chars)) # Output: (\w){2,4} + +# Example: At least 3 digits (min specified, max left open) +at_least_three = Regex(r"\d").repeat(3, None) +print(to_regex(at_least_three)) # Output: (\d){3,} + +# Example: Up to 2 punctuation marks (max specified, min omitted) +up_to_two = Regex(r"[,.]").repeat(None, 2) +print(to_regex(up_to_two)) # Output: ([,.]){,2} +``` + +You can also use the `repeat` function: + +```python +from outlines import repeat + +# Example: Between 2 and 4 word characters +word_chars = repeat(Regex(r"\w"), 2, 4) +print(to_regex(word_chars)) # Output: (\w){2,4} + +# Example: At least 3 digits (min specified, max left open) +at_least_three = repeat(Regex(r"\d"), 3, None) +print(to_regex(at_least_three)) # Output: (\d){3,} + +# Example: Up to 2 punctuation marks (max specified, min omitted) +up_to_two = repeat(Regex(r"[,.]"), None, 2) +print(to_regex(up_to_two)) # Output: ([,.]){,2} +``` + +--- + +## Combining Terms + +The DSL allows you to combine basic terms into more complex patterns using concatenation and alternation. + +### Concatenation (`+`) + +The `+` operator (and its reflected variant) concatenates terms, meaning that the terms are matched in sequence. + +```python +# Example: Match "hello world" +pattern = String("hello") + " " + Regex(r"\w+") +print(to_regex(pattern)) # Output: hello\ (\w+) +``` + +### Alternation (`|`) + +The `|` operator creates alternatives, allowing a match for one of several patterns. + +```python +# Example: Match either "cat" or "dog" +animal = String("cat") | "dog" +print(to_regex(animal)) # Output: (cat|dog) +``` + +*Note:* When using operators with plain strings (such as `"dog"`), the DSL automatically wraps them in a `String` object and escapes the characters that have a special meaning in regular expressions. + +--- + +## Custom types + +The DSL comes "batteries included" with types that represent common text constructs: + +- `integer` represents an integer number as recognized by `int` +- `boolean` represents a boolean, "True" or "False" as recognized by `bool` +- `number` represents a floating-point number recognize by Python's `float` +- `date` represents a date as understood by `datetime.date` +- `time` represents a time as undestood by `datetime.time` +- `datetime` represents a time as understoof by `datetime.datetime` +- `digit` represents a single digit +- `char` represents a single character +- `newline` represents a new line character +- `whitespace` represents a white space +- `sentence` represents a sentence +- `paragraph` reprensents a pagraph (one or more sentences separated by one or more ilne breaks) + + +For instance you can describe the answers in the GSM8K dataset using the following pattern: + +```python +from outlines.types import sentence, digit + +answer = "A: " + sentence.repeat(2,4) + " So the answer is: " + digit.repeat(1,4) +``` + +--- + +## Practical Examples + +### Example 1: Matching a Custom ID Format + +Suppose you want to create a regex that matches an ID format like "ID-12345", where: +- The literal "ID-" must be at the start. +- Followed by exactly 5 digits. + +```python +id_pattern = "ID-" + Regex(r"\d").times(5) +print(to_regex(id_pattern)) # Output: ID-(\d){5} +``` + +### Example 2: Email Validation with Pydantic + +You can define a regex for email validation and use it as a type in a Pydantic model. + +```python +from pydantic import BaseModel, ValidationError + +# Define an email regex term (this is a simplified version) +email_regex = Regex(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") + +class User(BaseModel): + name: str + email: email_regex # Use our DSL regex as a field type + +# Valid input +user = User(name="Alice", email="alice@example.com") +print(user) + +# Invalid input (raises a ValidationError) +try: + User(name="Bob", email="not-an-email") +except ValidationError as e: + print(e) +``` + +When used in a Pydantic model, the email field is automatically validated against the regex pattern and its JSON Schema includes the `pattern` constraint. + +### Example 3: Building a Complex Pattern + +Consider a pattern to match a simple date format: `YYYY-MM-DD`. + +```python +year = Regex(r"\d").times(4) # Four digits for the year +month = Regex(r"\d").times(2) # Two digits for the month +day = Regex(r"\d").times(2) # Two digits for the day + +# Combine with literal hyphens +date_pattern = year + "-" + month + "-" + day +print(to_regex(date_pattern)) +# Output: (\d){4}\-(\d){2}\-(\d){2} +``` + +--- + +## Visualizing Your Pattern + +One of the unique features of this DSL is that each term can print its underlying structure as an ASCII tree. This visualization can be particularly helpful when dealing with complex expressions. + +```python +# A composite pattern using concatenation and quantifiers +pattern = "a" + String("b").one_or_more() + "c" +print(pattern) +``` + +*Expected Output:* + +``` +└── Sequence + ├── String('a') + ├── KleenePlus(+) + │ └── String('b') + └── String('c') +``` + +This tree representation makes it easy to see the hierarchy and order of operations in your regular expression. + +--- + +## Final Words + +This DSL is designed to simplify the creation and management of regular expressions—whether you're validating inputs in a web API, constraining the output of an LLM, or just experimenting with regex patterns. With intuitive methods for common quantifiers and operators, clear visual feedback, and built-in integration with Pydantic, you can build robust and maintainable regex-based validations with ease. + +Feel free to explore the library further and adapt the examples to your use cases. Happy regexing! diff --git a/mkdocs.yml b/mkdocs.yml index 8d051d1af..2c31494da 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -133,6 +133,7 @@ nav: - Generation: - Overview: reference/generation/generation.md - Chat templating: reference/chat_templating.md + - Regex DSL: reference/regex_dsl.md - Text: reference/text.md - Samplers: reference/samplers.md - Structured generation: diff --git a/outlines/fsm/types.py b/outlines/fsm/types.py index cdb1d2115..e5a1f8f47 100644 --- a/outlines/fsm/types.py +++ b/outlines/fsm/types.py @@ -2,14 +2,9 @@ from enum import EnumMeta from typing import Any, Protocol, Tuple, Type -from typing_extensions import _AnnotatedAlias, get_args - -INTEGER = r"[+-]?(0|[1-9][0-9]*)" -BOOLEAN = "(True|False)" -FLOAT = rf"{INTEGER}(\.[0-9]+)?([eE][+-][0-9]+)?" -DATE = r"(\d{4})-(0[1-9]|1[0-2])-([0-2][0-9]|3[0-1])" -TIME = r"([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])" -DATETIME = rf"({DATE})(\s)({TIME})" +from outlines.types import Regex, boolean as boolean_regex, date as date_regex +from outlines.types import datetime as datetime_regex +from outlines.types import integer as integer_regex, number as number_regex, time as time_regex class FormatFunction(Protocol): @@ -17,18 +12,15 @@ def __call__(self, sequence: str) -> Any: ... -def python_types_to_regex(python_type: Type) -> Tuple[str, FormatFunction]: +def python_types_to_regex(python_type: Type) -> Tuple[Regex, FormatFunction]: # If it is a custom type - if isinstance(python_type, _AnnotatedAlias): - json_schema = get_args(python_type)[1].json_schema - type_class = get_args(python_type)[0] - - custom_regex_str = json_schema["pattern"] + if isinstance(python_type, Regex): + custom_regex_str = python_type.pattern - def custom_format_fn(sequence: str) -> Any: - return type_class(sequence) + def custom_format_fn(sequence: str) -> str: + return str(sequence) - return custom_regex_str, custom_format_fn + return Regex(custom_regex_str), custom_format_fn if isinstance(python_type, EnumMeta): values = python_type.__members__.keys() @@ -37,44 +29,44 @@ def custom_format_fn(sequence: str) -> Any: def enum_format_fn(sequence: str) -> str: return str(sequence) - return enum_regex_str, enum_format_fn + return Regex(enum_regex_str), enum_format_fn if python_type is float: def float_format_fn(sequence: str) -> float: return float(sequence) - return FLOAT, float_format_fn + return number_regex, float_format_fn elif python_type is int: def int_format_fn(sequence: str) -> int: return int(sequence) - return INTEGER, int_format_fn + return integer_regex, int_format_fn elif python_type is bool: def bool_format_fn(sequence: str) -> bool: return bool(sequence) - return BOOLEAN, bool_format_fn + return boolean_regex, bool_format_fn elif python_type == datetime.date: def date_format_fn(sequence: str) -> datetime.date: return datetime.datetime.strptime(sequence, "%Y-%m-%d").date() - return DATE, date_format_fn + return date_regex, date_format_fn elif python_type == datetime.time: def time_format_fn(sequence: str) -> datetime.time: return datetime.datetime.strptime(sequence, "%H:%M:%S").time() - return TIME, time_format_fn + return time_regex, time_format_fn elif python_type == datetime.datetime: def datetime_format_fn(sequence: str) -> datetime.datetime: return datetime.datetime.strptime(sequence, "%Y-%m-%d %H:%M:%S") - return DATETIME, datetime_format_fn + return datetime_regex, datetime_format_fn else: raise NotImplementedError( f"The Python type {python_type} is not supported. Please open an issue." diff --git a/outlines/generate/format.py b/outlines/generate/format.py index 88acec75f..56ed10ac1 100644 --- a/outlines/generate/format.py +++ b/outlines/generate/format.py @@ -33,6 +33,7 @@ def format( """ regex_str, format_fn = python_types_to_regex(python_type) + regex_str = regex_str.pattern generator = regex(model, regex_str, sampler) generator.format_sequence = format_fn diff --git a/outlines/generate/regex.py b/outlines/generate/regex.py index 673880e49..326701f4b 100644 --- a/outlines/generate/regex.py +++ b/outlines/generate/regex.py @@ -6,10 +6,11 @@ ) from outlines.models import OpenAI, TransformersVision from outlines.samplers import Sampler, multinomial +from outlines.types import Regex @singledispatch -def regex(model, regex_str: str, sampler: Sampler = multinomial()): +def regex(model, regex_str: str | Regex, sampler: Sampler = multinomial()): """Generate structured text in the language of a regular expression. Parameters @@ -31,6 +32,9 @@ def regex(model, regex_str: str, sampler: Sampler = multinomial()): """ from outlines.processors import RegexLogitsProcessor + if isinstance(regex_str, Regex): + regex_str = regex_str.pattern + logits_processor = RegexLogitsProcessor(regex_str, tokenizer=model.tokenizer) return SequenceGeneratorAdapter(model, logits_processor, sampler) @@ -38,11 +42,14 @@ def regex(model, regex_str: str, sampler: Sampler = multinomial()): @regex.register(TransformersVision) def regex_vision( model, - regex_str: str, + regex_str: str | Regex, sampler: Sampler = multinomial(), ): from outlines.processors import RegexLogitsProcessor + if isinstance(regex_str, Regex): + regex_str = regex_str.pattern + logits_processor = RegexLogitsProcessor(regex_str, tokenizer=model.tokenizer) return VisionSequenceGeneratorAdapter(model, logits_processor, sampler) diff --git a/outlines/types/__init__.py b/outlines/types/__init__.py index f4d2b8cd3..9511720af 100644 --- a/outlines/types/__init__.py +++ b/outlines/types/__init__.py @@ -1,4 +1,39 @@ -from . import airports, countries -from .email import Email -from .isbn import ISBN -from .locales import locale +from enum import Enum + +from . import airports, countries, locale +from outlines.types.dsl import Regex, json_schema, one_or_more, optional, regex, repeat, zero_or_more, times + +# Python types +integer = Regex(r"[+-]?(0|[1-9][0-9]*)") +boolean = Regex("(True|False)") +number = Regex(rf"{integer.pattern}(\.[0-9]+)?([eE][+-][0-9]+)?") +date = Regex(r"(\d{4})-(0[1-9]|1[0-2])-([0-2][0-9]|3[0-1])") +time = Regex(r"([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])") +datetime = Regex(rf"({date.pattern})(\s)({time.pattern})") + +# Basic regex types +digit = Regex(r"\d") +char = Regex(r"\w") +newline = Regex(r"(\r\n|\r|\n)") # Matched new lines on Linux, Windows & MacOS +whitespace = Regex(r"\s") + +# Document-specific types +sentence = Regex(r"[A-Z].*\s*[.!?]") +paragraph = Regex(rf"{sentence.pattern}(?:\s+{sentence.pattern})*\n+") + + +# The following regex is FRC 5322 compliant and was found at: +# https://emailregex.com/ +email = Regex( + r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""" +) + +# Matches any ISBN number. Note that this is not completely correct as not all +# 10 or 13 digits numbers are valid ISBNs. See https://en.wikipedia.org/wiki/ISBN +# Taken from O'Reilly's Regular Expression Cookbook: +# https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s13.html +# +# TODO: The check digit can only be computed by calling a function to compute it dynamically +isbn = Regex( + r"(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]" +) diff --git a/outlines/types/airports.py b/outlines/types/airports.py index 934ae1844..ec0ef72bd 100644 --- a/outlines/types/airports.py +++ b/outlines/types/airports.py @@ -6,6 +6,4 @@ AIRPORT_IATA_LIST = [ (v["iata"], v["iata"]) for v in airportsdata.load().values() if v["iata"] ] - - IATA = Enum("Airport", AIRPORT_IATA_LIST) # type:ignore diff --git a/outlines/types/dsl.py b/outlines/types/dsl.py new file mode 100644 index 000000000..86feabca0 --- /dev/null +++ b/outlines/types/dsl.py @@ -0,0 +1,395 @@ +import json as json +import re +from dataclasses import dataclass +from typing import Any, List, Union + +from pydantic import BaseModel, GetCoreSchemaHandler, GetJsonSchemaHandler +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import core_schema as cs +from outlines_core.fsm.json_schema import build_regex_from_schema + + +class Term: + """Represents types defined with a regular expression. + + `Regex` instances can be used as a type in a Pydantic model definittion. + They will be translated to JSON Schema as a "string" field with the + "pattern" keyword set to the regular expression this class represents. The + class also handles validation. + + Examples + -------- + + >>> from outlines.types import Regex + >>> from pydantic import BaseModel + >>> + >>> age_type = Regex("[0-9]+") + >>> + >>> class User(BaseModel): + >>> name: str + >>> age: age_type + + """ + + def __add__(self: "Term", other: Union[str, "Term"]) -> "Sequence": + if isinstance(other, str): + other = String(other) + + return Sequence([self, other]) + + def __radd__(self: "Term", other: Union[str, "Term"]) -> "Sequence": + if isinstance(other, str): + other = String(other) + + return Sequence([other, self]) + + def __or__(self: "Term", other: Union[str, "Term"]) -> "Alternatives": + if isinstance(other, str): + other = String(other) + + return Alternatives([self, other]) + + def __ror__(self: "Term", other: Union[str, "Term"]) -> "Alternatives": + if isinstance(other, str): + other = String(other) + + return Alternatives([other, self]) + + def __get_validator__(self, _core_schema): + def validate(input_value): + return self.validate(input_value) + + return validate + + def __get_pydantic_core_schema__( + self, source_type: Any, handler: GetCoreSchemaHandler + ) -> cs.CoreSchema: + return cs.no_info_plain_validator_function(lambda value: self.validate(value)) + + def __get_pydantic_json_schema__( + self, core_schema: cs.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + + return {"type": "string", "pattern": to_regex(self)} + + def validate(self, value: str) -> str: + pattern = to_regex(self) + compiled = re.compile(pattern) + if not compiled.fullmatch(str(value)): + raise ValueError( + f"Input should be in the language of the regular expression {pattern}" + ) + return value + + def matches(self, value: str) -> bool: + """Check that a given value is in the language defined by the Term. + + We make the assumption that the language defined by the term can + be defined with a regular expression. + + """ + pattern = to_regex(self) + compiled = re.compile(pattern) + if compiled.fullmatch(str(value)): + return True + return False + + def display_ascii_tree(self, indent="", is_last=True) -> str: + """Display the regex tree in ASCII format.""" + branch = "└── " if is_last else "├── " + result = indent + branch + self._display_node() + "\n" + + # Calculate the new indent for children + new_indent = indent + (" " if is_last else "│ ") + + # Let each subclass handle its children + result += self._display_children(new_indent) + return result + + def _display_node(self): + raise NotImplementedError + + def _display_children(self, indent: str) -> str: + """Display the children of this node. Override in subclasses with children.""" + return "" + + def __str__(self): + return self.display_ascii_tree() + + +@dataclass +class String(Term): + value: str + + def _display_node(self) -> str: + return f"String('{self.value}')" + + def __repr__(self): + return f"String(value='{self.value}')" + + +@dataclass +class Regex(Term): + pattern: str + + def _display_node(self) -> str: + return f"Regex('{self.pattern}')" + + def __repr__(self): + return f"Regex(pattern='{self.pattern}')" + + +class JsonSchema(Term): + def __init__(self, schema: Union[dict, str, type[BaseModel]]): + if isinstance(schema, dict): + schema_str = json.dumps(schema) + elif isinstance(schema, str): + schema_str = schema + elif issubclass(schema, BaseModel): + schema_str = json.dumps(schema.model_json_schema()) + else: + raise ValueError( + f"Cannot parse schema {json_schema}. The schema must be either " + + "a Pydantic class, a dictionary or a string that contains the JSON " + + "schema specification" + ) + + self.schema = schema_str + + def _display_node(self) -> str: + return f"JsonSchema('{self.schema}')" + + def __repr__(self): + return f"JsonSchema(schema='{self.schema}')" + + +@dataclass +class KleeneStar(Term): + term: Term + + def _display_node(self) -> str: + return "KleeneStar(*)" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return f"KleeneStar(term={repr(self.term)})" + + +@dataclass +class KleenePlus(Term): + term: Term + + def _display_node(self) -> str: + return "KleenePlus(+)" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return f"KleenePlus(term={repr(self.term)})" + + +@dataclass +class Optional(Term): + term: Term + + def _display_node(self) -> str: + return "Optional(?)" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return f"Optional(term={repr(self.term)})" + + +@dataclass +class Alternatives(Term): + terms: List[Term] + + def _display_node(self) -> str: + return "Alternatives(|)" + + def _display_children(self, indent: str) -> str: + return "".join( + term.display_ascii_tree(indent, i == len(self.terms) - 1) + for i, term in enumerate(self.terms) + ) + + def __repr__(self): + return f"Alternatives(terms={repr(self.terms)})" + + +@dataclass +class Sequence(Term): + terms: List[Term] + + def _display_node(self) -> str: + return "Sequence" + + def _display_children(self, indent: str) -> str: + return "".join( + term.display_ascii_tree(indent, i == len(self.terms) - 1) + for i, term in enumerate(self.terms) + ) + + def __repr__(self): + return f"Sequence(terms={repr(self.terms)})" + + +@dataclass +class QuantifyExact(Term): + term: Term + count: int + + def _display_node(self) -> str: + return f"Quantify({{{self.count}}})" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return f"QuantifyExact(term={repr(self.term)}, count={repr(self.count)})" + + +@dataclass +class QuantifyMinimum(Term): + term: Term + min_count: int + + def _display_node(self) -> str: + return f"Quantify({{{self.min_count},}})" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return ( + f"QuantifyMinimum(term={repr(self.term)}, min_count={repr(self.min_count)})" + ) + + +@dataclass +class QuantifyMaximum(Term): + term: Term + max_count: int + + def _display_node(self) -> str: + return f"Quantify({{,{self.max_count}}})" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return ( + f"QuantifyMaximum(term={repr(self.term)}, max_count={repr(self.max_count)})" + ) + + +@dataclass +class QuantifyBetween(Term): + term: Term + min_count: int + max_count: int + + def __post_init__(self): + if self.min_count > self.max_count: + raise ValueError( + "QuantifyBetween: `max_count` must be greater than `min_count`." + ) + + def _display_node(self) -> str: + return f"Quantify({{{self.min_count},{self.max_count}}})" + + def _display_children(self, indent: str) -> str: + return self.term.display_ascii_tree(indent, True) + + def __repr__(self): + return f"QuantifyBetween(term={repr(self.term)}, min_count={repr(self.min_count)}, max_count={repr(self.max_count)})" + + +def optional(self: Term) -> Optional: + return Optional(self) + + +def one_or_more(self: Term) -> KleenePlus: + return KleenePlus(self) + + +def repeat(self: Term, min_count: int, max_count: int) -> QuantifyBetween: + match (min_count, max_count): + case (None, None): + raise ValueError( + "repeat: you must provide a value for at least `min_count` or `max_count`" + ) + case (_, None): + return QuantifyMinimum(self, min_count) + case (None, _): + return QuantifyMaximum(self, max_count) + case _: + return QuantifyBetween(self, min_count, max_count) + + +def times(self: Term, count: int = 0) -> QuantifyExact: + return QuantifyExact(self, count) + + +def zero_or_more(self: Term) -> KleeneStar: + return KleeneStar(self) + + +Term.one_or_more = one_or_more # type: ignore +Term.optional = optional # type: ignore +Term.repeat = repeat # type: ignore +Term.times = times # type: ignore +Term.zero_or_more = zero_or_more # type: ignore + + +def regex(pattern: str): + return Regex(pattern) + + +def json_schema(schema: Union[str, dict, type[BaseModel]]): + return JsonSchema(schema) + + +def to_regex(term: Term) -> str: + """Convert a term to a regular expression. + + We only consider self-contained terms that do not refer to another rule. + + """ + match term: + case String(): + return re.escape(term.value) + case Regex(): + return f"({term.pattern})" + case JsonSchema(): + regex_str = build_regex_from_schema(term.schema) + return f"({regex_str})" + case KleeneStar(): + return f"({to_regex(term.term)})*" + case KleenePlus(): + return f"({to_regex(term.term)})+" + case Optional(): + return f"({to_regex(term.term)})?" + case Alternatives(): + regexes = [to_regex(subterm) for subterm in term.terms] + return f"({'|'.join(regexes)})" + case Sequence(): + regexes = [to_regex(subterm) for subterm in term.terms] + return f"{''.join(regexes)}" + case QuantifyExact(): + return f"({to_regex(term.term)}){{{term.count}}}" + case QuantifyMinimum(): + return f"({to_regex(term.term)}){{{term.min_count},}}" + case QuantifyMaximum(): + return f"({to_regex(term.term)}){{,{term.max_count}}}" + case QuantifyBetween(): + return f"({to_regex(term.term)}){{{term.min_count},{term.max_count}}}" + case _: + raise TypeError( + f"Cannot convert object {repr(term)} to a regular expression." + ) diff --git a/outlines/types/email.py b/outlines/types/email.py deleted file mode 100644 index 45f8c4b2c..000000000 --- a/outlines/types/email.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Email Address types.""" -from pydantic import WithJsonSchema -from typing_extensions import Annotated - -# Taken from StackOverflow -# https://stackoverflow.com/a/201378/14773537 -EMAIL_REGEX = r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""" -Email = Annotated[ - str, - WithJsonSchema({"type": "string", "pattern": EMAIL_REGEX}), -] diff --git a/outlines/types/isbn.py b/outlines/types/isbn.py deleted file mode 100644 index 5aebb067e..000000000 --- a/outlines/types/isbn.py +++ /dev/null @@ -1,12 +0,0 @@ -"""ISBN type""" -from pydantic import WithJsonSchema -from typing_extensions import Annotated - -# Matches any ISBN number. Note that this is not completely correct as not all -# 10 or 13 digits numbers are valid ISBNs. See https://en.wikipedia.org/wiki/ISBN -# Taken from O'Reilly's Regular Expression Cookbook: -# https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s13.html -# TODO: Can this be represented by a grammar or do we need semantic checks? -ISBN_REGEX = r"(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]" - -ISBN = Annotated[str, WithJsonSchema({"type": "string", "pattern": ISBN_REGEX})] diff --git a/outlines/types/locale/__init__.py b/outlines/types/locale/__init__.py new file mode 100644 index 000000000..511631d84 --- /dev/null +++ b/outlines/types/locale/__init__.py @@ -0,0 +1 @@ +from . import us diff --git a/outlines/types/locale/us.py b/outlines/types/locale/us.py new file mode 100644 index 000000000..0bda82d44 --- /dev/null +++ b/outlines/types/locale/us.py @@ -0,0 +1,4 @@ +from outlines.types.dsl import Regex + +zip_code = Regex(r"\d{5}(?:-\d{4})?") +phone_number = Regex(r"(\([0-9]{3}\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}") diff --git a/outlines/types/locales.py b/outlines/types/locales.py deleted file mode 100644 index c5d251bae..000000000 --- a/outlines/types/locales.py +++ /dev/null @@ -1,21 +0,0 @@ -from dataclasses import dataclass - -from outlines.types.phone_numbers import USPhoneNumber -from outlines.types.zip_codes import USZipCode - - -@dataclass -class US: - ZipCode = USZipCode - PhoneNumber = USPhoneNumber - - -def locale(locale_str: str): - locales = {"us": US} - - if locale_str not in locales: - raise NotImplementedError( - f"The locale {locale_str} is not supported yet. Please don't hesitate to create custom types for you locale and open a Pull Request." - ) - - return locales[locale_str] diff --git a/outlines/types/phone_numbers.py b/outlines/types/phone_numbers.py deleted file mode 100644 index 618687e75..000000000 --- a/outlines/types/phone_numbers.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Phone number types. - -We currently only support US phone numbers. We can however imagine having custom types -for each country, for instance leveraging the `phonenumbers` library. - -""" -from pydantic import WithJsonSchema -from typing_extensions import Annotated - -US_PHONE_NUMBER = r"(\([0-9]{3}\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}" - - -USPhoneNumber = Annotated[ - str, - WithJsonSchema({"type": "string", "pattern": US_PHONE_NUMBER}), -] diff --git a/outlines/types/zip_codes.py b/outlines/types/zip_codes.py deleted file mode 100644 index 67d994d5c..000000000 --- a/outlines/types/zip_codes.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Zip code types. - -We currently only support US Zip Codes. - -""" -from pydantic import WithJsonSchema -from typing_extensions import Annotated - -# This matches Zip and Zip+4 codes -US_ZIP_CODE = r"\d{5}(?:-\d{4})?" - - -USZipCode = Annotated[str, WithJsonSchema({"type": "string", "pattern": US_ZIP_CODE})] diff --git a/pyproject.toml b/pyproject.toml index f1ff57323..2762db439 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,6 +132,7 @@ module = [ "cloudpickle.*", "diskcache.*", "pydantic.*", + "pydantic_core.*", "pytest", "referencing.*", "torch.*", diff --git a/tests/fsm/test_types.py b/tests/fsm/test_types.py index d5450434c..4017a817c 100644 --- a/tests/fsm/test_types.py +++ b/tests/fsm/test_types.py @@ -1,29 +1,22 @@ -import datetime +import datetime as pydatetime import pytest -from outlines.fsm.types import ( - BOOLEAN, - DATE, - DATETIME, - FLOAT, - INTEGER, - TIME, - python_types_to_regex, -) +from outlines.fsm.types import python_types_to_regex +from outlines import types @pytest.mark.parametrize( - "python_type,regex", + "python_type,custom_type", [ - (int, INTEGER), - (float, FLOAT), - (bool, BOOLEAN), - (datetime.date, DATE), - (datetime.time, TIME), - (datetime.datetime, DATETIME), + (int, types.integer), + (float, types.number), + (bool, types.boolean), + (pydatetime.date, types.date), + (pydatetime.time, types.time), + (pydatetime.datetime, types.datetime), ], ) -def test_python_types(python_type, regex): +def test_python_types(python_type, custom_type): test_regex, _ = python_types_to_regex(python_type) - assert regex == test_regex + assert custom_type.pattern == test_regex.pattern diff --git a/tests/test_types.py b/tests/test_types.py deleted file mode 100644 index 5e60348b2..000000000 --- a/tests/test_types.py +++ /dev/null @@ -1,103 +0,0 @@ -import re - -import pytest -from pydantic import BaseModel - -from outlines import types -from outlines.fsm.types import python_types_to_regex - - -@pytest.mark.parametrize( - "custom_type,test_string,should_match", - [ - (types.phone_numbers.USPhoneNumber, "12", False), - (types.phone_numbers.USPhoneNumber, "(123) 123-1234", True), - (types.phone_numbers.USPhoneNumber, "123-123-1234", True), - (types.zip_codes.USZipCode, "12", False), - (types.zip_codes.USZipCode, "12345", True), - (types.zip_codes.USZipCode, "12345-1234", True), - (types.ISBN, "ISBN 0-1-2-3-4-5", False), - (types.ISBN, "ISBN 978-0-596-52068-7", True), - # (types.ISBN, "ISBN 978-0-596-52068-1", True), wrong check digit - (types.ISBN, "ISBN-13: 978-0-596-52068-7", True), - (types.ISBN, "978 0 596 52068 7", True), - (types.ISBN, "9780596520687", True), - (types.ISBN, "ISBN-10: 0-596-52068-9", True), - (types.ISBN, "0-596-52068-9", True), - (types.Email, "eitan@gmail.com", True), - (types.Email, "99@yahoo.com", True), - (types.Email, "eitan@.gmail.com", False), - (types.Email, "myemail", False), - (types.Email, "eitan@gmail", False), - (types.Email, "eitan@my.custom.domain", True), - ], -) -def test_type_regex(custom_type, test_string, should_match): - class Model(BaseModel): - attr: custom_type - - schema = Model.model_json_schema() - assert schema["properties"]["attr"]["type"] == "string" - regex_str = schema["properties"]["attr"]["pattern"] - does_match = re.match(regex_str, test_string) is not None - assert does_match is should_match - - regex_str, format_fn = python_types_to_regex(custom_type) - assert isinstance(format_fn(1), str) - does_match = re.match(regex_str, test_string) is not None - assert does_match is should_match - - -def test_locale_not_implemented(): - with pytest.raises(NotImplementedError): - types.locale("fr") - - -@pytest.mark.parametrize( - "locale_str,base_types,locale_types", - [ - ( - "us", - ["ZipCode", "PhoneNumber"], - [types.zip_codes.USZipCode, types.phone_numbers.USPhoneNumber], - ) - ], -) -def test_locale(locale_str, base_types, locale_types): - for base_type, locale_type in zip(base_types, locale_types): - type = getattr(types.locale(locale_str), base_type) - assert type == locale_type - - -@pytest.mark.parametrize( - "custom_type,test_string,should_match", - [ - (types.airports.IATA, "CDG", True), - (types.airports.IATA, "XXX", False), - (types.countries.Alpha2, "FR", True), - (types.countries.Alpha2, "XX", False), - (types.countries.Alpha3, "UKR", True), - (types.countries.Alpha3, "XXX", False), - (types.countries.Numeric, "004", True), - (types.countries.Numeric, "900", False), - (types.countries.Name, "Ukraine", True), - (types.countries.Name, "Wonderland", False), - (types.countries.Flag, "🇿🇼", True), - (types.countries.Flag, "🤗", False), - ], -) -def test_type_enum(custom_type, test_string, should_match): - type_name = custom_type.__name__ - - class Model(BaseModel): - attr: custom_type - - schema = Model.model_json_schema() - assert isinstance(schema["$defs"][type_name]["enum"], list) - does_match = test_string in schema["$defs"][type_name]["enum"] - assert does_match is should_match - - regex_str, format_fn = python_types_to_regex(custom_type) - assert isinstance(format_fn(1), str) - does_match = re.match(regex_str, test_string) is not None - assert does_match is should_match diff --git a/tests/types/test_custom_types.py b/tests/types/test_custom_types.py new file mode 100644 index 000000000..46df9ba4a --- /dev/null +++ b/tests/types/test_custom_types.py @@ -0,0 +1,111 @@ +import re + +import pytest +from pydantic import BaseModel + +from outlines import types +from outlines.fsm.types import python_types_to_regex +from outlines.types.dsl import to_regex + + +@pytest.mark.parametrize( + "custom_type,test_string,should_match", + [ + (types.locale.us.phone_number, "12", False), + (types.locale.us.phone_number, "(123) 123-1234", True), + (types.locale.us.phone_number, "123-123-1234", True), + (types.locale.us.zip_code, "12", False), + (types.locale.us.zip_code, "12345", True), + (types.locale.us.zip_code, "12345-1234", True), + (types.isbn, "ISBN 0-1-2-3-4-5", False), + (types.isbn, "ISBN 978-0-596-52068-7", True), + (types.isbn, "ISBN-13: 978-0-596-52068-7", True), + (types.isbn, "978 0 596 52068 7", True), + (types.isbn, "9780596520687", True), + (types.isbn, "ISBN-10: 0-596-52068-9", True), + (types.isbn, "0-596-52068-9", True), + (types.email, "eitan@gmail.com", True), + (types.email, "99@yahoo.com", True), + (types.email, "eitan@.gmail.com", False), + (types.email, "myemail", False), + (types.email, "eitan@gmail", False), + (types.email, "eitan@my.custom.domain", True), + (types.integer, "-19", True), + (types.integer, "19", True), + (types.integer, "019", False), + (types.integer, "1.9", False), + (types.integer, "a", False), + (types.boolean, "True", True), + (types.boolean, "False", True), + (types.boolean, "true", False), + (types.number, "10", True), + (types.number, "10.9", True), + (types.number, "10.9e+3", True), + (types.number, "10.9e-3", True), + (types.number, "a", False), + (types.date, "2022-03-23", True), + (types.date, "2022-03-32", False), + (types.date, "2022-13-23", False), + (types.date, "32-03-2022", False), + (types.time, "01:23:59", True), + (types.time, "01:23:61", False), + (types.time, "01:61:59", False), + (types.time, "24:23:59", False), + (types.sentence, "The temperature is 23.5 degrees !", True), + (types.sentence, "Did you earn $1,234.56 last month ?", True), + (types.sentence, "The #1 player scored 100 points .", True), + (types.sentence, "Hello @world, this is a test!", True), + (types.sentence, "invalid sentence.", False), + (types.sentence, "Invalid sentence", False), + (types.paragraph, "This is a paragraph!\n", True), + (types.paragraph, "Line1\nLine2", False), + (types.paragraph, "One sentence. Two sentences.\n\n", True), + (types.paragraph, "One sentence. invalid sentence.", False), + (types.paragraph, "One sentence. Invalid sentence\n", False), + ], +) +def test_type_regex(custom_type, test_string, should_match): + class Model(BaseModel): + attr: custom_type + + schema = Model.model_json_schema() + assert schema["properties"]["attr"]["type"] == "string" + regex_str = schema["properties"]["attr"]["pattern"] + does_match = re.fullmatch(regex_str, test_string) is not None + assert does_match is should_match + + regex_str = to_regex(custom_type) + does_match = re.fullmatch(regex_str, test_string) is not None + assert does_match is should_match + + +@pytest.mark.parametrize( + "custom_type,test_string,should_match", + [ + (types.airports.IATA, "CDG", True), + (types.airports.IATA, "XXX", False), + (types.countries.Alpha2, "FR", True), + (types.countries.Alpha2, "XX", False), + (types.countries.Alpha3, "UKR", True), + (types.countries.Alpha3, "XXX", False), + (types.countries.Numeric, "004", True), + (types.countries.Numeric, "900", False), + (types.countries.Name, "Ukraine", True), + (types.countries.Name, "Wonderland", False), + (types.countries.Flag, "🇿🇼", True), + (types.countries.Flag, "🤗", False), + ], +) +def test_type_enum(custom_type, test_string, should_match): + type_name = custom_type.__name__ + + class Model(BaseModel): + attr: custom_type + + schema = Model.model_json_schema() + assert isinstance(schema["$defs"][type_name]["enum"], list) + does_match = test_string in schema["$defs"][type_name]["enum"] + assert does_match is should_match + + does_match = test_string in custom_type.__members__ + assert does_match is should_match diff --git a/tests/types/test_dsl.py b/tests/types/test_dsl.py new file mode 100644 index 000000000..dc2779ef1 --- /dev/null +++ b/tests/types/test_dsl.py @@ -0,0 +1,256 @@ +import pytest +from pydantic import BaseModel + +from outlines.types.dsl import ( + Alternatives, + JsonSchema, + KleenePlus, + KleeneStar, + Optional, + QuantifyBetween, + QuantifyExact, + QuantifyMaximum, + QuantifyMinimum, + Regex, + Sequence, + String, + Term, + one_or_more, + optional, + repeat, + times, + regex, + json_schema, + zero_or_more, +) + + +def test_dsl_init(): + string = String("test") + assert string.value == "test" + assert repr(string) == "String(value='test')" + + regex = Regex("[0-9]") + assert regex.pattern == "[0-9]" + assert repr(regex) == "Regex(pattern='[0-9]')" + + schema = JsonSchema('{ "type": "string" }') + assert schema.schema == '{ "type": "string" }' + assert repr(schema) == 'JsonSchema(schema=\'{ "type": "string" }\')' + + kleene_star = KleeneStar(string) + assert kleene_star.term == string + assert repr(kleene_star) == "KleeneStar(term=String(value='test'))" + + kleene_plus = KleenePlus(string) + assert kleene_plus.term == string + assert repr(kleene_plus) == "KleenePlus(term=String(value='test'))" + + optional = Optional(string) + assert optional.term == string + assert repr(optional) == "Optional(term=String(value='test'))" + + alternatives = Alternatives([string, regex]) + assert alternatives.terms[0] == string + assert alternatives.terms[1] == regex + assert ( + repr(alternatives) + == "Alternatives(terms=[String(value='test'), Regex(pattern='[0-9]')])" + ) + + sequence = Sequence([string, regex]) + assert sequence.terms[0] == string + assert sequence.terms[1] == regex + assert ( + repr(sequence) + == "Sequence(terms=[String(value='test'), Regex(pattern='[0-9]')])" + ) + + exact = QuantifyExact(string, 3) + assert exact.term == string + assert exact.count == 3 + assert repr(exact) == "QuantifyExact(term=String(value='test'), count=3)" + + minimum = QuantifyMinimum(string, 3) + assert minimum.term == string + assert minimum.min_count == 3 + assert repr(minimum) == "QuantifyMinimum(term=String(value='test'), min_count=3)" + + maximum = QuantifyMaximum(string, 3) + assert maximum.term == string + assert maximum.max_count == 3 + assert repr(maximum) == "QuantifyMaximum(term=String(value='test'), max_count=3)" + + between = QuantifyBetween(string, 1, 3) + assert between.term == string + assert between.min_count == 1 + assert between.max_count == 3 + assert ( + repr(between) + == "QuantifyBetween(term=String(value='test'), min_count=1, max_count=3)" + ) + + with pytest.raises( + ValueError, match="`max_count` must be greater than `min_count`" + ): + QuantifyBetween(string, 3, 1) + + +def test_dsl_operations(): + a = String("a") + b = String("b") + assert isinstance(a + b, Sequence) + assert (a + b).terms[0] == a + assert (a + b).terms[1] == b + + assert isinstance(a | b, Alternatives) + assert (a | b).terms[0] == a + assert (a | b).terms[1] == b + + +def test_dsl_operations_string_conversion(): + b = String("b") + sequence = "a" + b + assert isinstance(sequence, Sequence) + assert isinstance(sequence.terms[0], String) + assert sequence.terms[0].value == "a" + assert sequence.terms[1].value == "b" + + sequence = b + "a" + assert isinstance(sequence, Sequence) + assert isinstance(sequence.terms[0], String) + assert sequence.terms[0].value == "b" + assert sequence.terms[1].value == "a" + + alternative = "a" | b + assert isinstance(alternative, Alternatives) + assert isinstance(alternative.terms[0], String) + assert alternative.terms[0].value == "a" + assert alternative.terms[1].value == "b" + + alternative = b | "a" + assert isinstance(alternative, Alternatives) + assert isinstance(alternative.terms[0], String) + assert alternative.terms[0].value == "b" + assert alternative.terms[1].value == "a" + + +def test_dsl_aliases(): + test = regex("[0-9]") + assert isinstance(test, Regex) + + test = json_schema('{"type": "string"}') + assert isinstance(test, JsonSchema) + + test = String("test") + + assert isinstance(test.times(3), QuantifyExact) + assert test.times(3).count == 3 + assert test.times(3).term == test + + assert isinstance(times(test, 3), QuantifyExact) + assert times(test, 3).count == 3 + assert times(test, 3).term == test + + assert isinstance(test.one_or_more(), KleenePlus) + assert test.one_or_more().term == test + + assert isinstance(one_or_more(test), KleenePlus) + assert one_or_more(test).term == test + + assert isinstance(test.zero_or_more(), KleeneStar) + assert test.zero_or_more().term == test + + assert isinstance(zero_or_more(test), KleeneStar) + assert zero_or_more(test).term == test + + assert isinstance(test.optional(), Optional) + assert test.optional().term == test + + assert isinstance(optional(test), Optional) + assert optional(test).term == test + + rep_min = test.repeat(2, None) + assert isinstance(rep_min, QuantifyMinimum) + assert rep_min.min_count == 2 + + rep_min = repeat(test, 2, None) + assert isinstance(rep_min, QuantifyMinimum) + assert rep_min.min_count == 2 + + rep_max = test.repeat(None, 2) + assert isinstance(rep_max, QuantifyMaximum) + assert rep_max.max_count == 2 + + rep_max = repeat(test, None, 2) + assert isinstance(rep_max, QuantifyMaximum) + assert rep_max.max_count == 2 + + rep_between = test.repeat(1, 2) + assert isinstance(rep_between, QuantifyBetween) + assert rep_between.min_count == 1 + assert rep_between.max_count == 2 + + rep_between = repeat(test, 1, 2) + assert isinstance(rep_between, QuantifyBetween) + assert rep_between.min_count == 1 + assert rep_between.max_count == 2 + + with pytest.raises(ValueError, match="QuantifyBetween: `max_count` must be"): + test.repeat(2, 1) + + with pytest.raises(ValueError, match="QuantifyBetween: `max_count` must be"): + repeat(test, 2, 1) + + with pytest.raises(ValueError, match="repeat: you must provide"): + test.repeat(None, None) + + with pytest.raises(ValueError, match="repeat: you must provide"): + repeat(test, None, None) + + +def test_dsl_term_pydantic_simple(): + a = String("a") + + class Model(BaseModel): + field: a + + schema = Model.model_json_schema() + assert schema == { + "properties": {"field": {"pattern": "a", "title": "Field", "type": "string"}}, + "required": ["field"], + "title": "Model", + "type": "object", + } + + +def test_dsl_term_pydantic_combination(): + a = String("a") + b = String("b") + c = String("c") + + class Model(BaseModel): + field: (a + b) | c + + schema = Model.model_json_schema() + assert schema == { + "properties": { + "field": {"pattern": "(ab|c)", "title": "Field", "type": "string"} + }, + "required": ["field"], + "title": "Model", + "type": "object", + } + + +def test_dsl_display(): + a = String("a") + b = String("b") + c = Regex("[0-9]") + d = KleeneStar(a | b) + c + + tree = str(d) + assert ( + tree + == "└── Sequence\n ├── KleeneStar(*)\n │ └── Alternatives(|)\n │ ├── String('a')\n │ └── String('b')\n └── Regex('[0-9]')\n" + ) diff --git a/tests/types/test_to_regex.py b/tests/types/test_to_regex.py new file mode 100644 index 000000000..6cb566fc5 --- /dev/null +++ b/tests/types/test_to_regex.py @@ -0,0 +1,71 @@ +import pytest + + +from outlines.types.dsl import String, Regex, JsonSchema, KleeneStar, KleenePlus, QuantifyBetween, QuantifyExact, QuantifyMaximum, QuantifyMinimum, Sequence, Alternatives, Optional, Term, to_regex + + +def test_to_regex_simple(): + a = String("a") + assert to_regex(a) == "a" + assert a.matches("a") is True + + a = Regex("[0-9]") + assert to_regex(a) == "([0-9])" + assert a.matches(0) is True + assert a.matches(10) is False + assert a.matches("a") is False + + a = JsonSchema({"type": "integer"}) + assert to_regex(a) == r"((-)?(0|[1-9][0-9]*))" + assert a.matches(1) is True + assert a.matches("1") is True + assert a.matches("a") is False + + a = Optional(String("a")) + assert to_regex(a) == "(a)?" + assert a.matches("") is True + assert a.matches("a") is True + + a = KleeneStar(String("a")) + assert to_regex(a) == "(a)*" + assert a.matches("") is True + assert a.matches("a") is True + assert a.matches("aaaaa") is True + + a = KleenePlus(String("a")) + assert to_regex(a) == "(a)+" + assert a.matches("") is False + assert a.matches("a") is True + assert a.matches("aaaaa") is True + + a = QuantifyExact(String("a"), 2) + assert to_regex(a) == "(a){2}" + assert a.matches("a") is False + assert a.matches("aa") is True + assert a.matches("aaa") is False + + a = QuantifyMinimum(String("a"), 2) + assert to_regex(a) == "(a){2,}" + assert a.matches("a") is False + assert a.matches("aa") is True + assert a.matches("aaa") is True + + a = QuantifyMaximum(String("a"), 2) + assert to_regex(a) == "(a){,2}" + assert a.matches("aa") is True + assert a.matches("aaa") is False + + a = QuantifyBetween(String("a"), 1, 2) + assert to_regex(a) == "(a){1,2}" + assert a.matches("") is False + assert a.matches("a") is True + assert a.matches("aa") is True + assert a.matches("aaa") is False + + with pytest.raises(TypeError, match="Cannot convert"): + to_regex(Term()) + + +def test_to_regex_combinations(): + a = Sequence([Regex("dog|cat"), String("fish")]) + assert to_regex(a) == "(dog|cat)fish" From 9c27efb9be33bb86862cbef341ce2066ece8fe5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Thu, 13 Feb 2025 18:54:43 +0100 Subject: [PATCH 167/505] Use `uv` in CI --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f54a888fa..46d3d5ff6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,9 @@ jobs: - name: Set up test environment run: | python -m pip install --upgrade pip - pip install -e .[test] + pip install uv + uv venv + uv pip install -e .[test] - name: Create matrix id id: matrix-id env: @@ -44,7 +46,7 @@ jobs: echo "::set-output name=id::$MATRIX_ID" - name: Run tests run: | - pytest -x --cov=outlines + uv run pytest -x --cov=outlines env: COVERAGE_FILE: .coverage.${{ steps.matrix-id.outputs.id }} - name: Upload coverage data From 116ebff8a5dbd26825075c847a8790b710c43da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 17 Feb 2025 22:58:42 +0100 Subject: [PATCH 168/505] Remove vLLM test dependency vLLM can only currently be run on GPU (unless you want to go to extreme lengths to make it work on CPU), and we thus cannot run the tests in CI. We thus separate the test dependencies in "with GPU" and "without GPU" (a subset of "with GPU") that the user has to pick manually. --- docs/community/contribute.md | 44 ++++++++++++++++++++++++------------ pyproject.toml | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/community/contribute.md b/docs/community/contribute.md index 6867263b4..21dc4581e 100644 --- a/docs/community/contribute.md +++ b/docs/community/contribute.md @@ -18,7 +18,7 @@ Note that the [issue tracker][issues] is only intended for actionable items. In First, [fork the repository on GitHub](https://github.com/dottxt-ai/outlines/fork) and clone the fork locally: -```bash +```shell git clone git@github.com/YourUserName/outlines.git cd outlines ``` @@ -27,7 +27,7 @@ Create a new virtual environment: *If you are using `uv`*: -```bash +```shell uv venv source .venv/bin/activate alias pip="uv pip" # ... or just remember to prepend any pip command with uv in the rest of this guide @@ -35,27 +35,41 @@ alias pip="uv pip" # ... or just remember to prepend any pip command with uv in *If you are using `venv`*: -```bash +```shell python -m venv .venv source .venv/bin/activate ``` *If you are using `conda`*: -```bash +```shell conda env create -f environment.yml ``` Then install the dependencies in editable mode, and install the `pre-commit` hooks: -```bash +```shell +python -m venv .venv +source .venv/bin/activate +``` + +Then install the dependencies in editable mode, and install the pre-commit hooks: + +```shell pip install -e ".[test]" pre-commit install ``` +If you own a GPU and want to run the vLLM tests you will have to run: + +```shell +pip install -e ".[test-gpu]" +``` + +instead. Outlines provides optional dependencies for different supported backends, which you can install with -```bash +```shell pip install ".[vllm]" ``` @@ -85,13 +99,13 @@ You will not have access to a GPU, but you'll be able to make basic contribution Run the tests: -```bash +```shell pytest ``` And run the code style checks: -```bash +```shell pre-commit run --all-files ``` @@ -101,7 +115,7 @@ Outlines uses [asv](https://asv.readthedocs.io) for automated benchmark testing. You can run the benchmark test suite locally with the following command: -```bash +```shell asv run --config benchmarks/asv.conf.json ``` @@ -112,19 +126,19 @@ Caveats: #### Run a specific test: -```bash +```shell asv run --config benchmarks/asv.conf.json -b bench_json_schema.JsonSchemaBenchmark.time_json_schema_to_fsm ``` #### Profile a specific test: -```bash +```shell asv run --config benchmarks/asv.conf.json --profile -b bench_json_schema.JsonSchemaBenchmark.time_json_schema_to_fsm ``` #### Compare to `origin/main` -```bash +```shell get fetch origin asv continuous origin/main HEAD --config benchmarks/asv.conf.json ``` @@ -140,13 +154,13 @@ asv continuous origin/main HEAD --config benchmarks/asv.conf.json To work on the *documentation* you will need to install the related dependencies: -```bash +```shell pip install -r requirements-doc.txt ``` To build the documentation and serve it locally, run the following command in the repository's root folder: -```bash +```shell mkdocs serve ``` @@ -157,7 +171,7 @@ It will be updated every time you make a change. Create a new branch on your fork, commit and push the changes: -```bash +```shell git checkout -b new-branch git add . git commit -m "Changes I made" diff --git a/pyproject.toml b/pyproject.toml index 2762db439..969290b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,12 +69,12 @@ test = [ "huggingface_hub", "openai>=1.0.0", "datasets", - "vllm; sys_platform == 'linux'", "transformers", "pillow", "exllamav2", "jax", ] +test-gpu=["outlines[test]", "vllm; sys_platform == 'linux'"] serve = [ "vllm>=0.3.0", "uvicorn", From f9e3af00ce96f13a26e2b955be7d8cd54b364489 Mon Sep 17 00:00:00 2001 From: Edoardo Abati <29585319+EdAbati@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:09:38 +0100 Subject: [PATCH 169/505] Various docs fixes (#1429) Follow up of #1343 In this PR I added: - all models to the API reference (we had only openai and transformers before) - fixes to the docstrings that were not following the numpy style - fixes to docstrings wrongly rendered by mkdocstring Thanks :) --- docs/api/models.md | 4 +--- docs/community/contribute.md | 7 ------- outlines/models/exllamav2.py | 8 +++++--- outlines/models/llamacpp.py | 12 ++++++------ outlines/models/mlxlm.py | 25 +++++++++++++++---------- outlines/models/openai.py | 5 ++--- outlines/models/transformers.py | 6 +++--- outlines/models/transformers_vision.py | 4 ++-- outlines/models/vllm.py | 6 +++--- outlines/prompts.py | 14 ++++++++++---- 10 files changed, 47 insertions(+), 44 deletions(-) diff --git a/docs/api/models.md b/docs/api/models.md index 27ad297fc..cb9497be1 100644 --- a/docs/api/models.md +++ b/docs/api/models.md @@ -1,3 +1 @@ -::: outlines.models.transformers - -::: outlines.models.openai +::: outlines.models diff --git a/docs/community/contribute.md b/docs/community/contribute.md index 21dc4581e..78c713b18 100644 --- a/docs/community/contribute.md +++ b/docs/community/contribute.md @@ -48,13 +48,6 @@ conda env create -f environment.yml Then install the dependencies in editable mode, and install the `pre-commit` hooks: -```shell -python -m venv .venv -source .venv/bin/activate -``` - -Then install the dependencies in editable mode, and install the pre-commit hooks: - ```shell pip install -e ".[test]" pre-commit install diff --git a/outlines/models/exllamav2.py b/outlines/models/exllamav2.py index d2aa84b0d..efd70f96a 100644 --- a/outlines/models/exllamav2.py +++ b/outlines/models/exllamav2.py @@ -118,9 +118,11 @@ def reformat_output( self, output: Union[str, List[str]], sampling_parameters: SamplingParameters ): """ - The purpose of this function is to reformat the output from exllamav2's output format to outline's output format - For exllamav2, it mainly accepts only a list or a string(they also do cfg sampling with tuples but we will ignore this for now) - The exllamav2's logic is + The purpose of this function is to reformat the output from exllamav2's output format to outline's output format. + + For exllamav2, it mainly accepts only a list or a string(they also do cfg sampling with tuples but we will ignore this for now). + The exllamav2's logic is: + 1. If the prompt is a string, return a string. This is the same as outlines 2. If a prompt is a list, return a list. This is not the same as outlines output in that if the list is only one element, the string is expected to be outputted. 3. There is no such thing as num_samples, so the prompts had to be duplicated by num_samples times. Then, we had the function output a list of lists diff --git a/outlines/models/llamacpp.py b/outlines/models/llamacpp.py index 904b193c4..7c7ad64df 100644 --- a/outlines/models/llamacpp.py +++ b/outlines/models/llamacpp.py @@ -248,8 +248,8 @@ def generate( ) -> str: """Generate text using `llama-cpp-python`. - Arguments - --------- + Parameters + ---------- prompts A prompt or list of prompts. generation_parameters @@ -302,8 +302,8 @@ def stream( ) -> Iterator[str]: """Stream text using `llama-cpp-python`. - Arguments - --------- + Parameters + ---------- prompts A prompt or list of prompts. generation_parameters @@ -372,8 +372,8 @@ def llamacpp( a path to the downloaded model. One can still load a local model by initializing `llama_cpp.Llama` directly. - Arguments - --------- + Parameters + ---------- repo_id The name of the model repository. filename: diff --git a/outlines/models/mlxlm.py b/outlines/models/mlxlm.py index d8b7e032c..843107d66 100644 --- a/outlines/models/mlxlm.py +++ b/outlines/models/mlxlm.py @@ -49,8 +49,8 @@ def stream( ) -> Iterator[str]: """Generate text using `mlx_lm`. - Arguments - --------- + Parameters + ---------- prompts A prompt or list of prompts. generation_parameters @@ -63,6 +63,7 @@ def stream( An instance of `SamplingParameters`, a dataclass that contains the name of the sampler to use and related parameters as available in Outlines. + Returns ------- The generated text. @@ -135,14 +136,18 @@ def generate_step( A generator producing token ids based on the given prompt from the model. - Args: - prompt (mx.array): The input prompt. - temp (float): The temperature for sampling, if 0 the argmax is used. - Default: ``0``. - top_p (float, optional): Nulceus sampling, higher means model considers - more less likely words. - sampler (str): The sampler string defined by SequenceGeneratorAdapter - logits_processor (OutlinesLogitsProcessor): Augment logits before sampling. + Parameters + ---------- + prompt + The input prompt. + temp + The temperature for sampling, if 0 the argmax is used. + top_p + Nulceus sampling, higher means model considers more less likely words. + sampler + The sampler string defined by SequenceGeneratorAdapter + logits_processor + Augment logits before sampling. """ import mlx.core as mx import mlx_lm diff --git a/outlines/models/openai.py b/outlines/models/openai.py index 89c26f217..40ade1c25 100644 --- a/outlines/models/openai.py +++ b/outlines/models/openai.py @@ -20,11 +20,11 @@ class OpenAIConfig: properties that are specific to the OpenAI API. Not all these properties are supported by Outlines. - Properties + Parameters ---------- model The name of the model. Available models can be found on OpenAI's website. - frequence_penalty + frequency_penalty Number between 2.0 and -2.0. Positive values penalize new tokens based on their existing frequency in the text, logit_bias @@ -49,7 +49,6 @@ class OpenAIConfig: Number between 0 and 1. Parameter for nucleus sampling. user A unique identifier for the end-user. - """ model: str = "" diff --git a/outlines/models/transformers.py b/outlines/models/transformers.py index 444492500..6b204ec6e 100644 --- a/outlines/models/transformers.py +++ b/outlines/models/transformers.py @@ -203,8 +203,8 @@ def generate( ) -> Union[str, List[str], List[List[str]]]: """Generate text using `transformers`. - Arguments - --------- + Parameters + ---------- prompts A prompt or list of prompts. generation_parameters @@ -304,7 +304,7 @@ def _get_generation_kwargs( sampling_parameters: SamplingParameters, ) -> dict: """ - Conert outlines generation parameters into model.generate kwargs + Convert outlines generation parameters into model.generate kwargs """ from transformers import GenerationConfig, LogitsProcessorList, set_seed diff --git a/outlines/models/transformers_vision.py b/outlines/models/transformers_vision.py index 772645b80..c8a86536e 100644 --- a/outlines/models/transformers_vision.py +++ b/outlines/models/transformers_vision.py @@ -22,8 +22,8 @@ def generate( # type: ignore ) -> Union[str, List[str], List[List[str]]]: """Generate text using `transformers`. - Arguments - --------- + Parameters + ---------- prompts A prompt or list of prompts. media diff --git a/outlines/models/vllm.py b/outlines/models/vllm.py index 778c27c6f..b9b035d1f 100644 --- a/outlines/models/vllm.py +++ b/outlines/models/vllm.py @@ -52,8 +52,8 @@ def generate( ): """Generate text using vLLM. - Arguments - --------- + Parameters + ---------- prompts A prompt or list of prompts. generation_parameters @@ -171,7 +171,7 @@ def load_lora(self, adapter_path: Optional[str]): def vllm(model_name: str, **vllm_model_params): """Load a vLLM model. - Arguments + Parameters --------- model_name The name of the model to load from the HuggingFace hub. diff --git a/outlines/prompts.py b/outlines/prompts.py index 1cc264226..b04f91856 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -125,6 +125,7 @@ def prompt( manipulation by providing some degree of encapsulation. It uses the `render` function internally to render templates. + ```pycon >>> import outlines >>> >>> @outlines.prompt @@ -132,35 +133,40 @@ def prompt( ... "I have a ${question}" ... >>> prompt = build_prompt("How are you?") + ``` This API can also be helpful in an "agent" context where parts of the prompt are set when the agent is initialized and never modified later. In this situation we can partially apply the prompt function at initialization. + ```pycon >>> import outlines >>> import functools as ft ... >>> @outlines.prompt ... def solve_task(name: str, objective: str, task: str): - ... '''Your name is {{name}}. - .. Your overall objective is to {{objective}}. + ... \"""Your name is {{name}}. + ... Your overall objective is to {{objective}}. ... Please solve the following task: {{task}} - ... ''' + ... \""" ... >>> hal = ft.partial(solve_task, "HAL", "Travel to Jupiter") + ``` Additional Jinja2 filters can be provided as keyword arguments to the decorator. + ```pycon >>> def reverse(s: str) -> str: ... return s[::-1] ... >>> @outlines.prompt(filters={ 'reverse': reverse }) ... def reverse_prompt(text): - ... '''{{ text | reverse }}''' + ... \"""{{ text | reverse }}\""" ... >>> prompt = reverse_prompt("Hello") >>> print(prompt) ... "olleH" + ``` Returns ------- From 9c98de767c06fe27f0a0d544eb0ea0261fcef8fe Mon Sep 17 00:00:00 2001 From: deepybee Date: Fri, 21 Feb 2025 16:27:49 +0100 Subject: [PATCH 170/505] Add Bluesky social media profile link to docs This commit adds a link in the docs to the social media profile of dottxt on the Bluesky social network alongside the existing link to the profile on X. --- docs/overrides/home.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 6525ab062..de5b2258a 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -135,8 +135,11 @@

From f25d3ab336fa1036af75d7ecc678475735bf1df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sun, 23 Feb 2025 23:24:46 +0100 Subject: [PATCH 171/505] Misc format fixes --- outlines/caching.py | 2 +- outlines/fsm/types.py | 9 ++++++--- outlines/models/openai.py | 9 ++++++++- outlines/processors/structured.py | 1 + outlines/samplers.py | 3 +-- outlines/serve/serve.py | 3 ++- outlines/types/__init__.py | 11 ++++++++++- outlines/types/airports.py | 1 + outlines/types/countries.py | 1 + outlines/types/dsl.py | 1 - tests/types/test_to_regex.py | 17 ++++++++++++++++- 11 files changed, 47 insertions(+), 11 deletions(-) diff --git a/outlines/caching.py b/outlines/caching.py index 0831c40bb..6882bac6b 100644 --- a/outlines/caching.py +++ b/outlines/caching.py @@ -51,7 +51,7 @@ def get_cache(): """ from outlines._version import __version__ as outlines_version # type: ignore - outlines_cache_dir = os.environ.get('OUTLINES_CACHE_DIR') + outlines_cache_dir = os.environ.get("OUTLINES_CACHE_DIR") xdg_cache_home = os.environ.get("XDG_CACHE_HOME") home_dir = os.path.normpath(os.path.expanduser("~")) if outlines_cache_dir: diff --git a/outlines/fsm/types.py b/outlines/fsm/types.py index e5a1f8f47..f6409aa66 100644 --- a/outlines/fsm/types.py +++ b/outlines/fsm/types.py @@ -4,12 +4,15 @@ from outlines.types import Regex, boolean as boolean_regex, date as date_regex from outlines.types import datetime as datetime_regex -from outlines.types import integer as integer_regex, number as number_regex, time as time_regex +from outlines.types import ( + integer as integer_regex, + number as number_regex, + time as time_regex, +) class FormatFunction(Protocol): - def __call__(self, sequence: str) -> Any: - ... + def __call__(self, sequence: str) -> Any: ... def python_types_to_regex(python_type: Type) -> Tuple[Regex, FormatFunction]: diff --git a/outlines/models/openai.py b/outlines/models/openai.py index 40ade1c25..e46107bd4 100644 --- a/outlines/models/openai.py +++ b/outlines/models/openai.py @@ -1,4 +1,5 @@ """Integration with OpenAI's API.""" + import copy import functools from dataclasses import asdict, dataclass, field, replace @@ -139,7 +140,13 @@ def __call__( if samples is None: samples = self.config.n - config = replace(self.config, max_tokens=max_tokens, temperature=temperature, n=samples, stop=stop_at) # type: ignore + config = replace( + self.config, + max_tokens=max_tokens, + temperature=temperature, + n=samples, + stop=stop_at, + ) # type: ignore response, prompt_tokens, completion_tokens = generate_chat( prompt, system_prompt, self.client, config diff --git a/outlines/processors/structured.py b/outlines/processors/structured.py index 583fcc98f..8ce69c32a 100644 --- a/outlines/processors/structured.py +++ b/outlines/processors/structured.py @@ -23,6 +23,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import math from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union diff --git a/outlines/samplers.py b/outlines/samplers.py index 3ab1728fc..3fef673b1 100644 --- a/outlines/samplers.py +++ b/outlines/samplers.py @@ -14,8 +14,7 @@ def __call__( next_token_logits: "torch.DoubleTensor", sequence_weights: "torch.DoubleTensor", rng: "torch.Generator", - ) -> "torch.DoubleTensor": - ... + ) -> "torch.DoubleTensor": ... @dataclass(frozen=True) diff --git a/outlines/serve/serve.py b/outlines/serve/serve.py index 998fbc459..61d3ed7af 100644 --- a/outlines/serve/serve.py +++ b/outlines/serve/serve.py @@ -78,7 +78,8 @@ async def generate(request: Request) -> Response: logits_processors = [] sampling_params = SamplingParams( - **request_dict, logits_processors=logits_processors # type: ignore + **request_dict, + logits_processors=logits_processors, # type: ignore ) request_id = random_uuid() diff --git a/outlines/types/__init__.py b/outlines/types/__init__.py index 9511720af..dbd8f507b 100644 --- a/outlines/types/__init__.py +++ b/outlines/types/__init__.py @@ -1,7 +1,16 @@ from enum import Enum from . import airports, countries, locale -from outlines.types.dsl import Regex, json_schema, one_or_more, optional, regex, repeat, zero_or_more, times +from outlines.types.dsl import ( + Regex, + json_schema, + one_or_more, + optional, + regex, + repeat, + zero_or_more, + times, +) # Python types integer = Regex(r"[+-]?(0|[1-9][0-9]*)") diff --git a/outlines/types/airports.py b/outlines/types/airports.py index ec0ef72bd..6e3d011b4 100644 --- a/outlines/types/airports.py +++ b/outlines/types/airports.py @@ -1,4 +1,5 @@ """Generate valid airport codes.""" + from enum import Enum import airportsdata diff --git a/outlines/types/countries.py b/outlines/types/countries.py index c612640b4..96be735d3 100644 --- a/outlines/types/countries.py +++ b/outlines/types/countries.py @@ -1,4 +1,5 @@ """Generate valid country codes and names.""" + from enum import Enum from iso3166 import countries diff --git a/outlines/types/dsl.py b/outlines/types/dsl.py index 86feabca0..94b4d26d9 100644 --- a/outlines/types/dsl.py +++ b/outlines/types/dsl.py @@ -69,7 +69,6 @@ def __get_pydantic_core_schema__( def __get_pydantic_json_schema__( self, core_schema: cs.CoreSchema, handler: GetJsonSchemaHandler ) -> JsonSchemaValue: - return {"type": "string", "pattern": to_regex(self)} def validate(self, value: str) -> str: diff --git a/tests/types/test_to_regex.py b/tests/types/test_to_regex.py index 6cb566fc5..4b0403ac6 100644 --- a/tests/types/test_to_regex.py +++ b/tests/types/test_to_regex.py @@ -1,7 +1,22 @@ import pytest -from outlines.types.dsl import String, Regex, JsonSchema, KleeneStar, KleenePlus, QuantifyBetween, QuantifyExact, QuantifyMaximum, QuantifyMinimum, Sequence, Alternatives, Optional, Term, to_regex +from outlines.types.dsl import ( + String, + Regex, + JsonSchema, + KleeneStar, + KleenePlus, + QuantifyBetween, + QuantifyExact, + QuantifyMaximum, + QuantifyMinimum, + Sequence, + Alternatives, + Optional, + Term, + to_regex, +) def test_to_regex_simple(): From 41eacc5db717e985f9d276468c74cbb005b150c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sun, 23 Feb 2025 23:25:08 +0100 Subject: [PATCH 172/505] Refactor the `prompts.py` file I replace a couple `classmethod`s with simple function, and added the default filter to the Jinja environment built when reading the template from a file. --- outlines/prompts.py | 108 +++++++++++++++++++++++++----------------- tests/test_prompts.py | 6 +-- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/outlines/prompts.py b/outlines/prompts.py index b04f91856..257dc3007 100644 --- a/outlines/prompts.py +++ b/outlines/prompts.py @@ -14,10 +14,10 @@ @dataclass class Prompt: - """Represents a prompt function. + """Represents a prompt template. We return a `Prompt` class instead of a simple function so the - template defined in prompt functions can be accessed. + template can be accessed by callers. """ @@ -41,8 +41,7 @@ def __call__(self, *args, **kwargs) -> str: @classmethod def from_str(cls, content: str, filters: Dict[str, Callable] = {}): - """ - Create an instance of the class from a string. + """Create a `Prompt` instance from a string containing a Jinja template. Parameters ---------- @@ -53,12 +52,11 @@ def from_str(cls, content: str, filters: Dict[str, Callable] = {}): ------- An instance of the class with the provided content as a template. """ - return cls(cls._template_from_str(content, filters), None) + return cls(build_template_from_str(content, filters), None) @classmethod def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): - """ - Create a Prompt instance from a file containing a Jinja template. + """Create a `Prompt` instance from a file containing a Jinja template. Note: This method does not allow to include and inheritance to reference files that are outside the folder or subfolders of the file given to `from_file`. @@ -75,44 +73,38 @@ def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): """ # We don't use a `Signature` here because it seems not feasible to infer one from a Jinja2 environment that is # split across multiple files (since e.g. we support features like Jinja2 includes and template inheritance) - return cls(cls._template_from_file(path, filters), None) + return cls(build_template_from_file(path, filters), None) - @classmethod - def _template_from_str( - _, content: str, filters: Dict[str, Callable] = {} - ) -> jinja2.Template: - # Dedent, and remove extra linebreak - cleaned_template = inspect.cleandoc(content) - - # Add linebreak if there were any extra linebreaks that - # `cleandoc` would have removed - ends_with_linebreak = content.replace(" ", "").endswith("\n\n") - if ends_with_linebreak: - cleaned_template += "\n" - - # Remove extra whitespaces, except those that immediately follow a newline symbol. - # This is necessary to avoid introducing whitespaces after backslash `\` characters - # used to continue to the next line without linebreak. - cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template) - - env = create_jinja_env(None, filters) - env.filters["name"] = get_fn_name - env.filters["description"] = get_fn_description - env.filters["source"] = get_fn_source - env.filters["signature"] = get_fn_signature - env.filters["schema"] = get_schema - env.filters["args"] = get_fn_args - - return env.from_string(cleaned_template) - @classmethod - def _template_from_file( - _, path: Path, filters: Dict[str, Callable] = {} - ) -> jinja2.Template: - file_directory = os.path.dirname(os.path.abspath(path)) - env = create_jinja_env(jinja2.FileSystemLoader(file_directory), filters) +def build_template_from_str( + content: str, filters: Dict[str, Callable] = {} +) -> jinja2.Template: + # Dedent, and remove extra linebreak + cleaned_template = inspect.cleandoc(content) + + # Add linebreak if there were any extra linebreaks that + # `cleandoc` would have removed + ends_with_linebreak = content.replace(" ", "").endswith("\n\n") + if ends_with_linebreak: + cleaned_template += "\n" + + # Remove extra whitespaces, except those that immediately follow a newline symbol. + # This is necessary to avoid introducing whitespaces after backslash `\` characters + # used to continue to the next line without linebreak. + cleaned_template = re.sub(r"(?![\r\n])(\b\s+)", " ", cleaned_template) + + env = create_jinja_env(None, filters) - return env.get_template(os.path.basename(path)) + return env.from_string(cleaned_template) + + +def build_template_from_file( + path: Path, filters: Dict[str, Callable] = {} +) -> jinja2.Template: + file_directory = os.path.dirname(os.path.abspath(path)) + env = create_jinja_env(jinja2.FileSystemLoader(file_directory), filters) + + return env.get_template(os.path.basename(path)) def prompt( @@ -184,7 +176,7 @@ def prompt( if docstring is None: raise TypeError("Could not find a template in the function's docstring.") - template = Prompt._template_from_str(cast(str, docstring), filters) + template = build_template_from_str(cast(str, docstring), filters) return Prompt(template, signature) @@ -192,6 +184,27 @@ def prompt( def create_jinja_env( loader: Optional[jinja2.BaseLoader], filters: Dict[str, Callable] ) -> jinja2.Environment: + """Create a new Jinja environment. + + The Jinja environment is loaded with a set of pre-defined filters: + - `name`: get the name of a function + - `description`: get a function's docstring + - `source`: get a function's source code + - `signature`: get a function's signature + - `args`: get a function's arguments + - `schema`: isplay a JSON Schema + + Users may pass additional filters, and/or override existing ones. + + Arguments + --------- + loader + An optional `BaseLoader` instance + filters + A dictionary of filters, map between the filter's name and the + corresponding function. + + """ env = jinja2.Environment( loader=loader, trim_blocks=True, @@ -200,6 +213,15 @@ def create_jinja_env( undefined=jinja2.StrictUndefined, ) + env.filters["name"] = get_fn_name + env.filters["description"] = get_fn_description + env.filters["source"] = get_fn_source + env.filters["signature"] = get_fn_signature + env.filters["schema"] = get_schema + env.filters["args"] = get_fn_args + + # The filters passed by the user may override the + # pre-defined filters. for name, filter_fn in filters.items(): env.filters[name] = filter_fn diff --git a/tests/test_prompts.py b/tests/test_prompts.py index f59c04ac0..c29348b8e 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -6,11 +6,11 @@ from pydantic import BaseModel, Field import outlines -from outlines.prompts import Prompt +from outlines.prompts import Prompt, build_template_from_str def render(content: str, **kwargs): - template = Prompt._template_from_str(content) + template = build_template_from_str(content) return template.render(kwargs) @@ -418,5 +418,5 @@ def test_template_from_str_with_extra_linebreaks(): """ - template = Prompt._template_from_str(content) + template = build_template_from_str(content) assert template.render(name="World") == "Hello, World!\n" From f35b57956094b08e52bf55d52d345a034f62a6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sun, 23 Feb 2025 23:50:04 +0100 Subject: [PATCH 173/505] Rename `Prompt` to `Template` The `Prompt` class really represents a prompt *template* and not a prompt. I thus renamed it to `Template` to make it less confusing to the user. --- README.md | 21 ++++++++++++++++++++ docs/api/prompts.md | 1 - docs/api/templates.md | 1 + outlines/__init__.py | 2 +- outlines/function.py | 4 ++-- outlines/{prompts.py => templates.py} | 16 +++++++-------- tests/{test_prompts.py => test_templates.py} | 6 +++--- 7 files changed, 36 insertions(+), 15 deletions(-) delete mode 100644 docs/api/prompts.md create mode 100644 docs/api/templates.md rename outlines/{prompts.py => templates.py} (95%) rename tests/{test_prompts.py => test_templates.py} (98%) diff --git a/README.md b/README.md index b3cde1dd1..83740fcec 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,27 @@ prompt = labelling("Just awesome", examples) answer = outlines.generate.text(model)(prompt, max_tokens=100) ``` +You can also load a template from a text file. Assume you have the following prompt template defined in `prompt.txt`: + +``` text +You are a sentiment-labelling assistant. + +{% for example in examples %} +{{ example[0] }} // {{ example[1] }} +{% endfor %} +{{ to_label }} // +``` + +You can load it with: + +``` python +import outlines + +labelling = outlines.Template.from_file("prompt.txt") +prompt = labelling("Just awesome", examples) + +``` + ## Join us - 💡 **Have an idea?** Come chat with us on [Discord][discord] diff --git a/docs/api/prompts.md b/docs/api/prompts.md deleted file mode 100644 index 9d28f838c..000000000 --- a/docs/api/prompts.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.prompts diff --git a/docs/api/templates.md b/docs/api/templates.md new file mode 100644 index 000000000..6150512d4 --- /dev/null +++ b/docs/api/templates.md @@ -0,0 +1 @@ +::: outlines.templates diff --git a/outlines/__init__.py b/outlines/__init__.py index eeba78dc7..628c88f0e 100644 --- a/outlines/__init__.py +++ b/outlines/__init__.py @@ -8,7 +8,7 @@ from outlines.base import vectorize from outlines.caching import clear_cache, disable_cache, get_cache from outlines.function import Function -from outlines.prompts import Prompt, prompt +from outlines.templates import Template, prompt __all__ = [ "clear_cache", diff --git a/outlines/function.py b/outlines/function.py index 48577be8f..aff21d68f 100644 --- a/outlines/function.py +++ b/outlines/function.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from outlines.generate.api import SequenceGenerator - from outlines.prompts import Prompt + from outlines.templates import Template @dataclass @@ -22,7 +22,7 @@ class Function: """ - prompt_template: "Prompt" + prompt_template: "Template" schema: Union[str, Callable, object] model_name: str generator: Optional["SequenceGenerator"] = None diff --git a/outlines/prompts.py b/outlines/templates.py similarity index 95% rename from outlines/prompts.py rename to outlines/templates.py index 257dc3007..56c4bc7c0 100644 --- a/outlines/prompts.py +++ b/outlines/templates.py @@ -13,10 +13,10 @@ @dataclass -class Prompt: +class Template: """Represents a prompt template. - We return a `Prompt` class instead of a simple function so the + We return a `Template` class instead of a simple function so the template can be accessed by callers. """ @@ -41,7 +41,7 @@ def __call__(self, *args, **kwargs) -> str: @classmethod def from_str(cls, content: str, filters: Dict[str, Callable] = {}): - """Create a `Prompt` instance from a string containing a Jinja template. + """Create a `Template` instance from a string containing a Jinja template. Parameters ---------- @@ -56,7 +56,7 @@ def from_str(cls, content: str, filters: Dict[str, Callable] = {}): @classmethod def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): - """Create a `Prompt` instance from a file containing a Jinja template. + """Create a `Template` instance from a file containing a Jinja template. Note: This method does not allow to include and inheritance to reference files that are outside the folder or subfolders of the file given to `from_file`. @@ -68,8 +68,8 @@ def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): Returns ------- - Prompt - An instance of the Prompt class with the template loaded from the file. + Template + An instance of the Template class with the template loaded from the file. """ # We don't use a `Signature` here because it seems not feasible to infer one from a Jinja2 environment that is # split across multiple files (since e.g. we support features like Jinja2 includes and template inheritance) @@ -162,7 +162,7 @@ def prompt( Returns ------- - A `Prompt` callable class which will render the template when called. + A `Template` callable class which will render the template when called. """ if fn is None: @@ -178,7 +178,7 @@ def prompt( template = build_template_from_str(cast(str, docstring), filters) - return Prompt(template, signature) + return Template(template, signature) def create_jinja_env( diff --git a/tests/test_prompts.py b/tests/test_templates.py similarity index 98% rename from tests/test_prompts.py rename to tests/test_templates.py index c29348b8e..1b16b0157 100644 --- a/tests/test_prompts.py +++ b/tests/test_templates.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field import outlines -from outlines.prompts import Prompt, build_template_from_str +from outlines.templates import Template, build_template_from_str def render(content: str, **kwargs): @@ -380,7 +380,7 @@ def temp_prompt_file(): def test_prompt_from_file(temp_prompt_file): - prompt = Prompt.from_file(temp_prompt_file) + prompt = Template.from_file(temp_prompt_file) assert prompt.signature is None examples = [ {"question": "What is the capital of France?", "answer": "Paris"}, @@ -407,7 +407,7 @@ def test_prompt_from_str(): content = """ Hello, {{ name }}! """ - prompt = Prompt.from_str(content) + prompt = Template.from_str(content) assert prompt.signature is None assert prompt(name="World") == "Hello, World!" From 2c8b40db64c77f542de99fbe24190beb1cc57824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sun, 23 Feb 2025 23:58:57 +0100 Subject: [PATCH 174/505] Add deprecation warning for the `prompt` decorator --- README.md | 55 ++++------ outlines/templates.py | 11 ++ tests/test_function.py | 21 ++-- tests/test_templates.py | 236 +++++++++++++++++++++------------------- 4 files changed, 166 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 83740fcec..cb57be0ae 100644 --- a/README.md +++ b/README.md @@ -379,40 +379,12 @@ print(result) ## Prompting Building prompts can get messy. **Outlines** makes it easier to write and manage -prompts by encapsulating templates inside "template functions". +prompts by encapsulating templates inside "template functions". Template +functions use the Jinja2 templating engine to help build complex prompts in a +concise manner. -These functions make it possible to neatly separate the prompt logic from the -general program logic; they can be imported from other modules and libraries. - -Template functions require no superfluous abstraction, they use the Jinja2 -templating engine to help build complex prompts in a concise manner: - -``` python -import outlines - -examples = [ - ("The food was disgusting", "Negative"), - ("We had a fantastic night", "Positive"), - ("Recommended", "Positive"), - ("The waiter was rude", "Negative") -] - -@outlines.prompt -def labelling(to_label, examples): - """You are a sentiment-labelling assistant. - - {% for example in examples %} - {{ example[0] }} // {{ example[1] }} - {% endfor %} - {{ to_label }} // - """ - -model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") -prompt = labelling("Just awesome", examples) -answer = outlines.generate.text(model)(prompt, max_tokens=100) -``` - -You can also load a template from a text file. Assume you have the following prompt template defined in `prompt.txt`: +Template functions are created by loading a Jinja2 template from a text file. +Assume you have the following prompt template defined in `prompt.txt`: ``` text You are a sentiment-labelling assistant. @@ -423,16 +395,29 @@ You are a sentiment-labelling assistant. {{ to_label }} // ``` -You can load it with: +You can then load it and call it with: ``` python import outlines +examples = [ + ("The food was disgusting", "Negative"), + ("We had a fantastic night", "Positive"), + ("Recommended", "Positive"), + ("The waiter was rude", "Negative") +] + labelling = outlines.Template.from_file("prompt.txt") prompt = labelling("Just awesome", examples) - ``` +This helps: + +- Keep content separate from the code +- Design "white space perfect" prompts + +It is more maintainable and means prompts can be versioned separately from the code. + ## Join us - 💡 **Have an idea?** Come chat with us on [Discord][discord] diff --git a/outlines/templates.py b/outlines/templates.py index 56c4bc7c0..a1fbc1932 100644 --- a/outlines/templates.py +++ b/outlines/templates.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Dict, Optional, Type, cast +import warnings import jinja2 import pydantic @@ -165,6 +166,16 @@ def prompt( A `Template` callable class which will render the template when called. """ + warnings.warn( + "The @prompt decorator is deprecated and will be removed in outlines 1.1.0. " + "Instead of using docstring templates, please use Template.from_file() to " + "load your prompts from separate template files, or a simple Python function " + "that returns text. This helps keep prompt content separate from code and is " + "more maintainable.", + DeprecationWarning, + stacklevel=2, + ) + if fn is None: return lambda fn: prompt(fn, cast(Dict[str, Callable], filters)) diff --git a/tests/test_function.py b/tests/test_function.py index 62f7ea29f..d66f3911c 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -8,9 +8,10 @@ def test_function_basic(): - @outlines.prompt - def test_template(text: str): - """{{ text }}""" + with pytest.deprecated_call(match="The @prompt decorator"): + @outlines.prompt + def test_template(text: str): + """{{ text }}""" class Foo(BaseModel): id: int @@ -102,10 +103,11 @@ class User(BaseModel): ) """ - fn = extract_function_from_file(content, "function") - assert ( - str(type(fn)) == "" - ) # because imported via `exec` + with pytest.deprecated_call(match="The @prompt decorator"): + fn = extract_function_from_file(content, "function") + assert ( + str(type(fn)) == "" + ) # because imported via `exec` def test_extract_function_from_file_no_function(): @@ -129,5 +131,6 @@ class User(BaseModel): ) """ - with pytest.raises(AttributeError, match="Could not find"): - extract_function_from_file(content, "function") + with pytest.deprecated_call(match="The @prompt decorator"): + with pytest.raises(AttributeError, match="Could not find"): + extract_function_from_file(content, "function") diff --git a/tests/test_templates.py b/tests/test_templates.py index 1b16b0157..06727eab1 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -113,60 +113,63 @@ def test_render_jinja(): def test_prompt_basic(): - @outlines.prompt - def test_tpl(variable): - """{{variable}} test""" + with pytest.deprecated_call(): + @outlines.prompt + def test_tpl(variable): + """{{variable}} test""" - assert list(test_tpl.signature.parameters) == ["variable"] + assert list(test_tpl.signature.parameters) == ["variable"] - with pytest.raises(TypeError): - test_tpl(v="test") + with pytest.raises(TypeError): + test_tpl(v="test") - p = test_tpl("test") - assert p == "test test" + p = test_tpl("test") + assert p == "test test" - p = test_tpl(variable="test") - assert p == "test test" + p = test_tpl(variable="test") + assert p == "test test" - @outlines.prompt - def test_single_quote_tpl(variable): - "${variable} test" + @outlines.prompt + def test_single_quote_tpl(variable): + "${variable} test" - assert list(test_single_quote_tpl.signature.parameters) == ["variable"] + assert list(test_single_quote_tpl.signature.parameters) == ["variable"] - p = test_tpl("test") - assert p == "test test" + p = test_tpl("test") + assert p == "test test" def test_prompt_kwargs(): - @outlines.prompt - def test_kwarg_tpl(var, other_var="other"): - """{{var}} and {{other_var}}""" + with pytest.deprecated_call(): + @outlines.prompt + def test_kwarg_tpl(var, other_var="other"): + """{{var}} and {{other_var}}""" - assert list(test_kwarg_tpl.signature.parameters) == ["var", "other_var"] + assert list(test_kwarg_tpl.signature.parameters) == ["var", "other_var"] - p = test_kwarg_tpl("test") - assert p == "test and other" + p = test_kwarg_tpl("test") + assert p == "test and other" - p = test_kwarg_tpl("test", other_var="kwarg") - assert p == "test and kwarg" + p = test_kwarg_tpl("test", other_var="kwarg") + assert p == "test and kwarg" - p = test_kwarg_tpl("test", "test") - assert p == "test and test" + p = test_kwarg_tpl("test", "test") + assert p == "test and test" def test_no_prompt(): - with pytest.raises(TypeError, match="template"): + with pytest.deprecated_call(): + with pytest.raises(TypeError, match="template"): - @outlines.prompt - def test_empty(variable): - pass + @outlines.prompt + def test_empty(variable): + pass - with pytest.raises(TypeError, match="template"): + with pytest.raises(TypeError, match="template"): - @outlines.prompt - def test_only_code(variable): - return variable + @outlines.prompt + def test_only_code(variable): + return variable def test_prompt_function(): @@ -180,41 +183,44 @@ def with_description(): """ pass - @outlines.prompt - def name_description_ppt(fn): - """ - {{fn|name}}: {{fn|description}} - """ + with pytest.deprecated_call(): + @outlines.prompt + def name_description_ppt(fn): + """ + {{fn|name}}: {{fn|description}} + """ - rendered = name_description_ppt(empty_fn) - assert rendered == "empty_fn: " + rendered = name_description_ppt(empty_fn) + assert rendered == "empty_fn: " - rendered = name_description_ppt(with_description) - assert rendered == "with_description: A description." + rendered = name_description_ppt(with_description) + assert rendered == "with_description: A description." def with_signature(one: int, two: List[str], three: float = 1.0): pass - @outlines.prompt - def name_signature_ppt(fn): - """ - {{fn|name}}: {{fn|signature}} - """ + with pytest.deprecated_call(): + @outlines.prompt + def name_signature_ppt(fn): + """ + {{fn|name}}: {{fn|signature}} + """ - rendered = name_signature_ppt(with_signature) - assert rendered == "with_signature: one: int, two: List[str], three: float = 1.0" + rendered = name_signature_ppt(with_signature) + assert rendered == "with_signature: one: int, two: List[str], three: float = 1.0" def test_function_call(one, two=2): return one + two - @outlines.prompt - def source_ppt(fn): - """ - {{fn|source}} - """ + with pytest.deprecated_call(): + @outlines.prompt + def source_ppt(fn): + """ + {{fn|source}} + """ - rendered = source_ppt(test_function_call) - assert rendered == "def test_function_call(one, two=2):\n return one + two\n" + rendered = source_ppt(test_function_call) + assert rendered == "def test_function_call(one, two=2):\n return one + two\n" def test_prompt_pydantic_response(): @@ -222,43 +228,45 @@ class SimpleResponse(BaseModel): one: str = Field(description="a description") two: str - @outlines.prompt - def source_ppt(model): - "{{model | schema }}" + with pytest.deprecated_call(): + @outlines.prompt + def source_ppt(model): + "{{model | schema }}" - prompt = source_ppt(SimpleResponse) - assert prompt == '{\n "one": "a description",\n "two": ""\n}' + prompt = source_ppt(SimpleResponse) + assert prompt == '{\n "one": "a description",\n "two": ""\n}' - class NestedResponse(BaseModel): - answer: str - thought: SimpleResponse + class NestedResponse(BaseModel): + answer: str + thought: SimpleResponse - prompt = source_ppt(NestedResponse) - assert ( - prompt - == '{\n "answer": "",\n "thought": {\n "one": "a description",\n "two": ""\n }\n}' - ) + prompt = source_ppt(NestedResponse) + assert ( + prompt + == '{\n "answer": "",\n "thought": {\n "one": "a description",\n "two": ""\n }\n}' + ) - class ConvolutedResponse(BaseModel): - part_one: NestedResponse - part_two: SimpleResponse + class ConvolutedResponse(BaseModel): + part_one: NestedResponse + part_two: SimpleResponse - prompt = source_ppt(ConvolutedResponse) - assert ( - prompt - == '{\n "part_one": {\n "answer": "",\n "thought": {\n "one": "a description",\n "two": ""\n }\n },\n "part_two": {\n "one": "a description",\n "two": ""\n }\n}' - ) + prompt = source_ppt(ConvolutedResponse) + assert ( + prompt + == '{\n "part_one": {\n "answer": "",\n "thought": {\n "one": "a description",\n "two": ""\n }\n },\n "part_two": {\n "one": "a description",\n "two": ""\n }\n}' + ) def test_prompt_dict_response(): response = {"one": "a description", "two": ""} - @outlines.prompt - def source_ppt(model): - "{{model | schema }}" + with pytest.deprecated_call(): + @outlines.prompt + def source_ppt(model): + "{{model | schema }}" - prompt = source_ppt(response) - assert prompt == '{\n "one": "a description",\n "two": ""\n}' + prompt = source_ppt(response) + assert prompt == '{\n "one": "a description",\n "two": ""\n}' def test_prompt_args(): @@ -297,45 +305,47 @@ def with_all( ): pass - @outlines.prompt - def args_prompt(fn): - """args: {{ fn | args }}""" - - assert args_prompt(no_args) == "args: " - assert args_prompt(with_args) == "args: x, y, z" - assert ( - args_prompt(with_annotations) - == "args: x: bool, y: str, z: Dict[int, List[str]]" - ) - assert ( - args_prompt(with_defaults) - == "args: x=True, y='Hi', z={4: ['I', 'love', 'outlines']}" - ) - assert ( - args_prompt(with_annotations_and_defaults) - == "args: x: bool = True, y: str = 'Hi', z: Dict[int, List[str]] = {4: ['I', 'love', 'outlines']}" - ) - assert ( - args_prompt(with_all) - == "args: x1, y1, z1, x2: bool, y2: str, z2: Dict[int, List[str]], x3=True, y3='Hi', z3={4: ['I', 'love', 'outlines']}, x4: bool = True, y4: str = 'Hi', z4: Dict[int, List[str]] = {4: ['I', 'love', 'outlines']}" - ) + with pytest.deprecated_call(): + @outlines.prompt + def args_prompt(fn): + """args: {{ fn | args }}""" + + assert args_prompt(no_args) == "args: " + assert args_prompt(with_args) == "args: x, y, z" + assert ( + args_prompt(with_annotations) + == "args: x: bool, y: str, z: Dict[int, List[str]]" + ) + assert ( + args_prompt(with_defaults) + == "args: x=True, y='Hi', z={4: ['I', 'love', 'outlines']}" + ) + assert ( + args_prompt(with_annotations_and_defaults) + == "args: x: bool = True, y: str = 'Hi', z: Dict[int, List[str]] = {4: ['I', 'love', 'outlines']}" + ) + assert ( + args_prompt(with_all) + == "args: x1, y1, z1, x2: bool, y2: str, z2: Dict[int, List[str]], x3=True, y3='Hi', z3={4: ['I', 'love', 'outlines']}, x4: bool = True, y4: str = 'Hi', z4: Dict[int, List[str]] = {4: ['I', 'love', 'outlines']}" + ) def test_prompt_with_additional_filters(): def reverse(s: str) -> str: return s[::-1] - @outlines.prompt(filters=dict(reverse=reverse)) - def test_tpl(variable): - """{{ variable | reverse }} test""" + with pytest.deprecated_call(): + @outlines.prompt(filters=dict(reverse=reverse)) + def test_tpl(variable): + """{{ variable | reverse }} test""" - assert list(test_tpl.signature.parameters) == ["variable"] + assert list(test_tpl.signature.parameters) == ["variable"] - p = test_tpl("test") - assert p == "tset test" + p = test_tpl("test") + assert p == "tset test" - p = test_tpl(variable="example") - assert p == "elpmaxe test" + p = test_tpl(variable="example") + assert p == "elpmaxe test" @pytest.fixture From ce27432021b502a4db1ec8740caac0b5c3c8e278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 24 Feb 2025 09:42:11 +0100 Subject: [PATCH 175/505] Improve the README --- README.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cb57be0ae..01d7b9c98 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,12 @@ First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/la ## Features -- [x] 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/latest/installation): OpenAI, transformers, llama.cpp, exllama2, mamba -- [x] 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/) -- [x] 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping -- [x] ⚡ Fast [regex-structured generation](#efficient-regex-structured-generation) -- [x] 🔥 Fast [JSON generation](#efficient-json-generation-following-a-pydantic-model) following a JSON schema or a Pydantic model -- [x] 📝 [Grammar-structured generation](#using-context-free-grammars-to-guide-generation) -- [x] 🐍 Interleave completions with loops, conditionals, and custom Python functions -- [x] 💾 Caching of generations -- [x] 🗂️ Batch inference -- [x] 🎲 Sample with the greedy, multinomial and beam search algorithms (and more to come!) -- [x] 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/latest/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! +- 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/latest/installation): OpenAI, transformers, llama.cpp, exllama2, mamba +- 🔥 Fast [JSON generation](#efficient-json-generation-following-a-pydantic-model) following a JSON schema or a Pydantic model +- 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping +- 📝 Generate text that follows a [regex](#efficient-regex-structured-generation) or a [context-free grammar](#using-context-free-grammars-to-guide-generation) +- 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/) +- 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/latest/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! Outlines has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date! From eced2ceb275f2edfffc8ace64b70b5d590d38fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Mon, 24 Feb 2025 12:38:44 +0100 Subject: [PATCH 176/505] Fix documentation ToC --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2c31494da..b31fc1f54 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -167,7 +167,7 @@ nav: - API Reference: - api/index.md - api/models.md - - api/prompts.md + - api/templates.md - api/json_schema.md - api/guide.md - api/parsing.md From 5c8ff7e1b3f501364ce4cb5b262efdf4f0c64433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Tue, 25 Feb 2025 12:28:13 +0100 Subject: [PATCH 177/505] s/from_str/from_string --- outlines/templates.py | 8 ++++---- tests/test_templates.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/outlines/templates.py b/outlines/templates.py index a1fbc1932..16588e813 100644 --- a/outlines/templates.py +++ b/outlines/templates.py @@ -41,7 +41,7 @@ def __call__(self, *args, **kwargs) -> str: return self.template.render(**kwargs) @classmethod - def from_str(cls, content: str, filters: Dict[str, Callable] = {}): + def from_string(cls, content: str, filters: Dict[str, Callable] = {}): """Create a `Template` instance from a string containing a Jinja template. Parameters @@ -53,7 +53,7 @@ def from_str(cls, content: str, filters: Dict[str, Callable] = {}): ------- An instance of the class with the provided content as a template. """ - return cls(build_template_from_str(content, filters), None) + return cls(build_template_from_string(content, filters), None) @classmethod def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): @@ -77,7 +77,7 @@ def from_file(cls, path: Path, filters: Dict[str, Callable] = {}): return cls(build_template_from_file(path, filters), None) -def build_template_from_str( +def build_template_from_string( content: str, filters: Dict[str, Callable] = {} ) -> jinja2.Template: # Dedent, and remove extra linebreak @@ -187,7 +187,7 @@ def prompt( if docstring is None: raise TypeError("Could not find a template in the function's docstring.") - template = build_template_from_str(cast(str, docstring), filters) + template = build_template_from_string(cast(str, docstring), filters) return Template(template, signature) diff --git a/tests/test_templates.py b/tests/test_templates.py index 06727eab1..aa9939278 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -6,11 +6,11 @@ from pydantic import BaseModel, Field import outlines -from outlines.templates import Template, build_template_from_str +from outlines.templates import Template, build_template_from_string def render(content: str, **kwargs): - template = build_template_from_str(content) + template = build_template_from_string(content) return template.render(kwargs) @@ -417,7 +417,7 @@ def test_prompt_from_str(): content = """ Hello, {{ name }}! """ - prompt = Template.from_str(content) + prompt = Template.from_string(content) assert prompt.signature is None assert prompt(name="World") == "Hello, World!" @@ -428,5 +428,5 @@ def test_template_from_str_with_extra_linebreaks(): """ - template = build_template_from_str(content) + template = build_template_from_string(content) assert template.render(name="World") == "Hello, World!\n" From 4abe628a2961c87c437b52e7bf0da87f5ed924d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Tue, 25 Feb 2025 12:26:39 +0100 Subject: [PATCH 178/505] Update documentation and examples following `outlines.prompt` deprecation --- docs/cookbook/chain_of_density.md | 7 +- docs/cookbook/classification.md | 7 +- docs/cookbook/dating_profiles.md | 5 +- docs/cookbook/extraction.md | 7 +- docs/cookbook/simtom.md | 14 +-- docs/quickstart.md | 10 +- docs/reference/prompting.md | 182 ++++++++++++++---------------- examples/babyagi.py | 64 +---------- examples/dating_profile.py | 20 +--- examples/math_generate_code.py | 6 +- examples/meta_prompting.py | 32 +++--- examples/pick_odd_one_out.py | 5 +- examples/react.py | 39 +++---- examples/self_consistency.py | 6 +- 14 files changed, 169 insertions(+), 235 deletions(-) diff --git a/docs/cookbook/chain_of_density.md b/docs/cookbook/chain_of_density.md index 2a6b4eb39..10bda9771 100644 --- a/docs/cookbook/chain_of_density.md +++ b/docs/cookbook/chain_of_density.md @@ -29,10 +29,10 @@ The prompt also asks the model to return a list of JSON objects that contain the We can now implement the prompt provided in the paper: ```python -import outlines +from outlines import Template -@outlines.prompt -def chain_of_density(article): + +chain_of_density = Template.from_string( """Article: {{ article }} You will generate increasingly concise, entity-dense summaries of the above Article. @@ -61,6 +61,7 @@ def chain_of_density(article): Answer in JSON. The JSON should be a a dictionary with key "summaries" that contains a list (length 5) of dictionaries whose keys are "Missing_Entities" and "Denser_Summary". """ +) ``` ??? Note diff --git a/docs/cookbook/classification.md b/docs/cookbook/classification.md index c56096318..023e4543a 100644 --- a/docs/cookbook/classification.md +++ b/docs/cookbook/classification.md @@ -16,8 +16,10 @@ model = outlines.models.transformers("TheBloke/Mistral-7B-OpenOrca-AWQ", device= We will use the following prompt template: ```python -@outlines.prompt -def customer_support(request): +from outlines import Template + + +customer_support = Template.from_string( """You are an experienced customer success manager. Given a request from a client, you need to determine when the @@ -36,6 +38,7 @@ def customer_support(request): Request: {{ request }} Label: """ +) ``` ## Choosing between multiple choices diff --git a/docs/cookbook/dating_profiles.md b/docs/cookbook/dating_profiles.md index d0fb9b576..1cbea556c 100644 --- a/docs/cookbook/dating_profiles.md +++ b/docs/cookbook/dating_profiles.md @@ -57,9 +57,9 @@ class Example: We will use Outlines' prompt templating abilities to generate the prompt for us. This help clearly separate the general prompting logic from what is specific to an example. ```python +from outlines import Template -@outlines.prompt -def dating_profile_prompt(description: str, examples: list[Example]): +dating_profile_prompt = Template.from_string( """ You are a world-renowned matchmaker who understands the modern dating market. Your job is to generate dating app profiles for male clients @@ -79,6 +79,7 @@ def dating_profile_prompt(description: str, examples: list[Example]): Description: {{ description }} Profile: """ +) ``` We will provide the model with several few-shot examples: diff --git a/docs/cookbook/extraction.md b/docs/cookbook/extraction.md index 28317b6b0..dfcca0cfd 100644 --- a/docs/cookbook/extraction.md +++ b/docs/cookbook/extraction.md @@ -15,8 +15,10 @@ model = outlines.models.transformers("TheBloke/Mistral-7B-OpenOrca-AWQ", device= And we will be using the following prompt template: ```python -@outlines.prompt -def take_order(order): +from outlines import Template + + +take_order = Template.from_string( """You are the owner of a pizza parlor. Customers \ send you orders from which you need to extract: @@ -42,6 +44,7 @@ def take_order(order): ORDER: {{ order }} RESULT: """ +) ``` We now define our data model using Pydantic: diff --git a/docs/cookbook/simtom.md b/docs/cookbook/simtom.md index 4ad78846b..a80ebc24f 100644 --- a/docs/cookbook/simtom.md +++ b/docs/cookbook/simtom.md @@ -23,18 +23,15 @@ To implement SimToM with Outlines, we will need to: Let's dive into it! -### Using Prompt Functions - -With Outlines, you can write your prompts as Python functions by adding the `@outlines.prompt` decorator. The prompt template is contained in their docstring, and their arguments correspond to variables used in the prompt. +### Using Prompt Templates The authors have shared their code, prompts and data in [this GitHub repository](https://github.com/shawnsihyunlee/simulatedtom). Below, we define in Outlines the prompts they used for the ToMI dataset: ```python -import outlines +from outlines import Template -@outlines.prompt -def perspective_taking(story: str, character: str) -> None: +perspective_taking = Template.from_string( """[INST] The following is a sequence of events about some characters, that takes place in multiple locations. Your job is to output only the events that the specified character, {{character}}, knows about. @@ -45,9 +42,9 @@ def perspective_taking(story: str, character: str) -> None: Story: {{story}} What events does {{character}} know about? Only output the events according to the above rules, do not provide an explanation. [/INST]""" # noqa +) -@outlines.prompt -def simulation(events: list, name: str, question: str) -> None: +simulation = Template.from_string( """[INST] {% for event in events %} {{event}} {% endfor %} @@ -55,6 +52,7 @@ def simulation(events: list, name: str, question: str) -> None: Based on the above information, answer the following question: {{question}} You must choose one of the above choices, do not say there is not enough information. Answer with a single word, do not output anything else. [/INST]""" # noqa +) ``` ### JSON Structured Generation diff --git a/docs/quickstart.md b/docs/quickstart.md index 81a067ad6..ec445dcba 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -115,10 +115,9 @@ Or use the [requests][requests]{:target="_blank"} library from another python pr Prompting can lead to messy code. Outlines' prompt functions are python functions that contain a template for the prompt in their docstring. We use a powerful templating language to allow you to loop over lists, dictionaries, add conditionals, etc. directly from the prompt. When called, a prompt function returns the rendered template: ```python -import outlines +from outlines import Template -@outlines.prompt -def few_shots(instructions, examples, question): +few_shots = Template.from_string( """{{ instructions }} Examples @@ -135,6 +134,7 @@ def few_shots(instructions, examples, question): Q: {{ question }} A: """ +) instructions = "Please answer the following question following the examples" examples = [ @@ -175,9 +175,9 @@ Once you are done experimenting with a prompt and an output structure, it is use import outlines - @outlines.prompt - def tell_a_joke(topic): + tell_a_joke = outlines.Template.from_string( """Tell me a joke about {{ topic }}.""" + ) class Joke(BaseModel): setup: str diff --git a/docs/reference/prompting.md b/docs/reference/prompting.md index 8ea9c0243..986ca82fc 100644 --- a/docs/reference/prompting.md +++ b/docs/reference/prompting.md @@ -33,15 +33,13 @@ will pass to the prompt function. === "Code" - ```python - import outlines + ```python title="greetings.py" + from outlines import Template - @outlines.prompt - def greetings(name, question): - """Hello, {{ name }}! - {{ question }} - """ + prompt = """Hello, {{ name }}! + {{ question }}""" + greetings = Template.from_string(prompt) prompt = greetings("user", "How are you?") print(prompt) ``` @@ -58,12 +56,10 @@ If a variable is missing in the function's arguments, Jinja2 will throw an `Unde === "Code" ```python - import outlines - - @outlines.prompt - def greetings(name): - """Hello, {{ surname }}!""" + from outlines import Template + prompt = """Hello, {{ surname }}!""" + greetings = Template.from_string(prompt) prompt = greetings("user") ``` @@ -72,9 +68,9 @@ If a variable is missing in the function's arguments, Jinja2 will throw an `Unde ```text Traceback (most recent call last): File "", line 9, in - File "/home/remi/projects/normal/outlines/outlines/prompts.py", line 38, in __call__ + File "/home/remi/projects/normal/outlines/outlines/templates.py", line 38, in __call__ return render(self.template, **bound_arguments.arguments) - File "/home/remi/projects/normal/outlines/outlines/prompts.py", line 213, in render + File "/home/remi/projects/normal/outlines/outlines/templates.py", line 213, in render return jinja_template.render(**values) File "/home/remi/micromamba/envs/outlines/lib/python3.9/site-packages/jinja2/environment.py", line 1301, in render self.environment.handle_exception() @@ -84,26 +80,23 @@ If a variable is missing in the function's arguments, Jinja2 will throw an `Unde jinja2.exceptions.UndefinedError: 'surname' is undefined ``` -## Importing prompt functions +## Importing prompts from files -Prompt functions are functions, and thus can be imported from other modules: - -=== "prompts.py" - ```python - import outlines +Outlines allows you to read a prompt template from a text file. This way you can build "white space perfect" prompts, and version them independently from your code. We have found ourselves gravitating around this pattern a lot since Outlines came out: - @outlines.prompt - def greetings(name, question): - """Hello, {{ name }}! - {{ question }} - """ +=== "prompt.txt" + ```text + """Hello, {{ name }}! + {{ question }} + """ ``` === "generate.py" ```python - from .prompts import greetings + from outlines import Template + greetings = Template.from_file("prompt.txt") prompt = greetings("John Doe", "How are you today?") ``` @@ -123,27 +116,26 @@ keys `question` and `answer` to the prompt function: === "Code" - ```python - import outlines + ```text title="prompt.txt" + {{ instructions }} - @outlines.prompt - def few_shots(instructions, examples, question): - """{{ instructions }} + Examples + -------- - Examples - -------- + {% for example in examples %} + Q: {{ example.question }} + A: {{ example.answer }} - {% for example in examples %} - Q: {{ example.question }} - A: {{ example.answer }} + {% endfor %} + Question + -------- - {% endfor %} - Question - -------- + Q: {{ question }} + A: + ``` - Q: {{ question }} - A: - """ + ```python title="render.py" + from outlines import Template instructions = "Please answer the following question following the examples" examples = [ @@ -152,6 +144,7 @@ keys `question` and `answer` to the prompt function: ] question = "4+4 = ?" + few_shots = Template.from_file("prompt.txt") prompt = few_shots(instructions, examples, question) print(prompt) ``` @@ -194,7 +187,7 @@ Several projects (e.g.[Toolformer](https://arxiv.org/abs/2302.04761), [ViperGPT] === "Code" ```python - import outlines + from outlines import Template def my_tool(arg1: str, arg2: int): """Tool description. @@ -203,16 +196,15 @@ Several projects (e.g.[Toolformer](https://arxiv.org/abs/2302.04761), [ViperGPT] """ pass - @outlines.prompt - def tool_prompt(question, tool): - """{{ question }} + prompt = """{{ question }} - COMMANDS - 1. {{ tool | name }}: {{ tool | description }}, args: {{ tool | args }} + COMMANDS + 1. {{ tool | name }}: {{ tool | description }}, args: {{ tool | args }} - {{ tool | source }} - """ + {{ tool | source }} + """ + tool_prompt = Template.from_string(prompt) prompt = tool_prompt("Can you do something?", my_tool) print(prompt) ``` @@ -250,16 +242,14 @@ pretty print a dictionary from within an Outlines prompt function ```python from pydantic import BaseModel, Field - import outlines + from outlines import Template class MyResponse(BaseModel): field1: int = Field(description="an int") field2: str - @outlines.prompt - def my_prompt(response_model): - """{{ response_model | schema }}""" + my_prompt = Template.from_string("""{{ response_model | schema }}""") prompt = my_prompt(MyResponse) print(prompt) # { @@ -285,8 +275,9 @@ pretty print a dictionary from within an Outlines prompt function ## Formatting conventions -Prompt functions are opinionated when it comes to rendering, and these opinions -are meant to avoid prompting mistakes and help with formatting. +Prompt templates are opinionated when it comes to rendering a template read from +a string, and these opinions are meant to avoid prompting mistakes and help with +formatting. ### Whitespaces @@ -300,18 +291,19 @@ below does not matter for formatting: === "Code" ```python - import outlines + from outlines import Template - @outlines.prompt - def prompt1(): - """My prompt - """ - @outlines.prompt - def prompt2(): - """ - My prompt - """ + prompt1 = Template.from_string( + """My prompt + """ + ) + + prompt2 = Template.from_string( + """ + My prompt + """ + ) print(prompt1()) print(prompt2()) @@ -329,27 +321,27 @@ Indentation is relative to the second line of the docstring, and leading spaces === "Code" ```python - import outlines + from outlines import Template + + example1 = Template.from_string( + """First line + Second line + """ + ) - @outlines.prompt - def example1(): - """First line + example2 = Template.from_string( + """ Second line - """ + Third line + """ + ) - @outlines.prompt - def example2(): - """ - Second line + example3 = Template.from_string( + """ + Second line Third line - """ - - @outlines.prompt - def example3(): - """ - Second line - Third line - """ + """ + ) print(example1()) print(example2()) @@ -378,18 +370,18 @@ You can use the backslash `\` to break a long line of text. It will render as a === "Code" ```python - import outlines - - @outlines.prompt - def example(): - """ - Break in \ - several lines \ - But respect the indentation - on line breaks. - And after everything \ - Goes back to normal - """ + from outlines import Template + + example = Template.from_string( + """ + Break in \ + several lines \ + But respect the indentation + on line breaks. + And after everything \ + Goes back to normal + """ + ) print(example()) ``` diff --git a/examples/babyagi.py b/examples/babyagi.py index 0a7a0b13b..314f9dbca 100644 --- a/examples/babyagi.py +++ b/examples/babyagi.py @@ -9,48 +9,16 @@ import outlines import outlines.models as models +from outlines import Template + model = models.openai("gpt-4o-mini") complete = outlines.generate.text(model) - -################# -# Perform tasks # -################# - - -@outlines.prompt -def perform_task_ppt(objective: str, task: str): - """You are an AI who performs one task based on the following objective: {{objective}}. - - Your task: {{task.task_name}} - - Response: - """ - - -##################### -# Create a new task # -##################### - - -@outlines.prompt -def create_tasks_ppt( - objective: str, previous_task: str, result: str, task_list: List[str] -): - """You are an task creation AI that uses the result of an execution agent to \ - create new tasks with the following objective: {{objective}}. - - The last completed task has the result: {{result}}. - - This result was based on this task description: {{previous_task}}. These are \ - incomplete tasks: {{task_list | join(task_list)}}. - - Based on the result, create new tasks to be completed by the AI system that \ - do not overlap with incomplete tasks. - - Return the tasks as an array. - """ +## Load the prompts +perform_task_ppt = Template.from_file("prompts/babyagi_perform_task.txt") +create_tasks_ppt = Template.from_file("prompts/babyagi_create_task.txt") +prioritize_tasks_ppt = Template.from_file("prompts/babyagi_prioritize_task.txt") def create_tasks_fmt(result: str) -> List[str]: @@ -65,26 +33,6 @@ def create_tasks_fmt(result: str) -> List[str]: return task_list -######################## -# Prioritize new tasks # -######################## - - -@outlines.prompt -def prioritize_tasks_ppt(objective: str, task_names: List[str], next_task_id: int): - """You are a task prioritization AI tasked with cleaning the formatting of \ - and reprioritizing the following tasks: {{task_names}}. - - Consider the ultimate objective of your team: {{objective}}. - - Do not remove any tasks. Return the result as a numbered list, like: - #. First task - #. Second task - - Start the tasks list with the number {{next_task_id}}. - """ - - def prioritize_tasks_fmt(result: str): new_tasks = result.split("\n") diff --git a/examples/dating_profile.py b/examples/dating_profile.py index 504ec943d..d26984223 100644 --- a/examples/dating_profile.py +++ b/examples/dating_profile.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, conlist import outlines -from outlines import models +from outlines import models, Template class QuestionChoice(str, Enum): @@ -41,23 +41,6 @@ class Example: profile: DatingProfile -@outlines.prompt -def dating_profile_prompt(description: str, examples: list[Example]): - """ - You are a world-renowned matchmaker who understands the modern dating market. Your job is to generate dating app profiles for male clients interested in women based on a provided description. The profiles should be authentic, show off their strengths, and maximize their likelihood of getting matches on dating apps. - Here are some examples of past clients that you have successfully created profiles for: - {% for example in examples %} - Description: - {{ example.description }} - Profile: - {{ example.profile }} - {% endfor %} - Here is the new client who you need to create a profile for: - Description: {{ description }} - Profile: - """ - - samples: list[Example] = [ Example( description="I'm an author and former professional soccer player living in Seattle who publishes popular fiction books. A typical day for me starts by hanging out with my cat, drinking a coffee, and reading as much as I can in a few hours. Then, I'll prepare a quick smoothie before starting to write for a few hours, take a break with soccer or running a few miles, and finally meet friends for dinner at a new, hip restaurant in the evening. Sometimes we go axe-throwing afterwards, or play poker, or watch a comedy show, or visit a dive bar. On my vacations, I travel extensively to countries South America, Europe, and Asia, with the goal of visiting them all!", @@ -120,6 +103,7 @@ def dating_profile_prompt(description: str, examples: list[Example]): new_description = "I'm a laid-back lawyer who spends a lot of his free-time gaming. I work in a corporate office, but ended up here after the start-up I cofounded got acquired, so still play ping pong with my cool coworkers every day. I have a bar at home where I make cocktails, which is great for entertaining friends. I secretly like to wear suits and get a new one tailored every few months. I also like weddings because I get to wear those suits, and it's a good excuse for a date. I watch the latest series because I'm paying, with my hard-earned money, for every streaming service." +dating_profile_prompt = Template.from_file("prompts/dating_profile.txt") prompt = dating_profile_prompt(description=new_description, examples=samples) profile = outlines.generate.json(model, DatingProfile)(prompt) # type: ignore print(profile) diff --git a/examples/math_generate_code.py b/examples/math_generate_code.py index 7eb1651a7..6d2ba8ae6 100644 --- a/examples/math_generate_code.py +++ b/examples/math_generate_code.py @@ -1,6 +1,7 @@ """Example from https://dust.tt/spolu/a/d12ac33169""" import outlines import outlines.models as models +from outlines import Template examples = [ {"question": "What is 37593 * 67?", "code": "37593 * 67"}, @@ -16,9 +17,7 @@ question = "Carla is downloading a 200 GB file. She can download 2 GB/minute, but 40% of the way through the download, the download fails. Then Carla has to restart the download from the beginning. How load did it take her to download the file in minutes?" - -@outlines.prompt -def answer_with_code_prompt(question, examples): +answer_with_code_prompt = Template.from_string( """ {% for example in examples %} QUESTION: {{example.question}} @@ -27,6 +26,7 @@ def answer_with_code_prompt(question, examples): {% endfor %} QUESTION: {{question}} CODE:""" +) def execute_code(code): diff --git a/examples/meta_prompting.py b/examples/meta_prompting.py index cba18b5fe..85f512bf6 100644 --- a/examples/meta_prompting.py +++ b/examples/meta_prompting.py @@ -13,14 +13,16 @@ import outlines import outlines.models as models +from outlines import Template def split_into_steps(question, model_name: str): - @outlines.prompt - def solve(question): + + solve = Template.from_string( """{{question}} Rephrase : : as a true or false statement, identify an Object, relationship and subject """ + ) model = models.openai(model_name) generator = outlines.generate.text(model) @@ -38,16 +40,17 @@ def solve(question): def fill_in_the_blanks(question, model_name: str): - @outlines.prompt - def determine_goal(question): + + determine_goal = Template.from_string( """{{question}} In order to solve this problem, we will analyze each of the options and determine """ + ) - @outlines.prompt - def solve(memory): + solve = Template.from_string( """{{memory}}. Let's begin.""" + ) model = models.openai(model_name) generator = outlines.generate.text(model) @@ -62,8 +65,8 @@ def solve(memory): def ask_an_expert(question, model_name: str): - @outlines.prompt - def find_expert(question): + + find_expert = Template.from_string( """ {{question}} I entered my question into the Expert Generator \ @@ -79,15 +82,16 @@ def find_expert(question): found the most qualified expert. The name displayed \ on the screen: " """ + ) - @outlines.prompt - def get_answer(question, expert, memory): + get_answer = Template.from_string( """ {{memory}}". I am ready to ask my question. "{{expert}}" I say, {{question}} """ + ) model = models.openai(model_name) generator = outlines.generate.text(model) @@ -102,20 +106,20 @@ def get_answer(question, expert, memory): def ask_an_expert_simple(question, model_name: str): - @outlines.prompt - def find_expert(question): + find_expert = Template.from_string( """ Q: {{question}} A: A good person to answer this question would be """ + ) - @outlines.prompt - def get_answer(expert, memory): + get_answer = Template.from_string( """ {{memory}}. For instance, {{expert}} would answer """ + ) model = models.openai(model_name) generator = outlines.generate.text(model) diff --git a/examples/pick_odd_one_out.py b/examples/pick_odd_one_out.py index 6cd9f1daf..834c40824 100644 --- a/examples/pick_odd_one_out.py +++ b/examples/pick_odd_one_out.py @@ -13,8 +13,7 @@ import outlines.models as models -@outlines.prompt -def build_ooo_prompt(options): +build_ooo_prompt = outlines.Template.from_string( """ Pick the odd word out: skirt, dress, pen, jacket. skirt is clothing, dress is clothing, pen is an object, jacket is clothing. @@ -27,7 +26,7 @@ def build_ooo_prompt(options): Pick the odd word out: {{ options | join(", ") }}. """ - +) options = ["sea", "mountains", "plains", "sock"] diff --git a/examples/react.py b/examples/react.py index 34b3c6eb2..9b876eb49 100644 --- a/examples/react.py +++ b/examples/react.py @@ -13,29 +13,30 @@ import requests # type: ignore import outlines +from outlines import Template import outlines.generate as generate import outlines.models as models -@outlines.prompt -def build_reAct_prompt(question): - """What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? - Tho 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado ... - Act 2: Search 'Colorado orogeny' - Obs 2: The Colorado orogeny was an episode of mountain building (an orogeny) ... - Tho 3: It does not mention the eastern sector. So I need to look up eastern sector. - ... - Tho 4: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. - Act 5: Finish '1,800 to 7,000 ft' - {{ question }} - """ - - -@outlines.prompt -def add_mode(i, mode, result, prompt): - """{{ prompt }} - {{ mode }} {{ i }}: {{ result }} - """ +build_reAct_prompt = Template.from_string( +"""What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? +Tho 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado ... +Act 2: Search 'Colorado orogeny' +Obs 2: The Colorado orogeny was an episode of mountain building (an orogeny) ... +Tho 3: It does not mention the eastern sector. So I need to look up eastern sector. +... +Tho 4: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. +Act 5: Finish '1,800 to 7,000 ft' +{{ question }} +""" +) + + +add_mode = Template.from_string( +"""{{ prompt }} +{{ mode }} {{ i }}: {{ result }} +""" +) def search_wikipedia(query: str): diff --git a/examples/self_consistency.py b/examples/self_consistency.py index f1bbe2a18..50f12acf1 100644 --- a/examples/self_consistency.py +++ b/examples/self_consistency.py @@ -4,6 +4,7 @@ import outlines import outlines.models as models +from outlines import Template examples = [ { @@ -43,8 +44,7 @@ question = "When I was 6 my sister was half my age. Now I’m 70 how old is my sister?" -@outlines.prompt -def few_shots(question, examples): +few_shots = Template.from_string( """ {% for example in examples %} Q: {{ example.question }} @@ -53,7 +53,7 @@ def few_shots(question, examples): Q: {{ question }} A: """ - +) model = models.openai("gpt-4o-mini") generator = outlines.generate.text(model) From 8a06251aa91b6923116a6253d1c902a9d472a05c Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Fri, 28 Feb 2025 19:39:10 +0100 Subject: [PATCH 179/505] Format files and fix numerous `mypy` warnings, thanks to `ruff` Left to fix: outlines/models/exllamav2.py:356: error: Argument 3 to "ExLlamaV2Model" has incompatible type "int | None"; expected "int" [arg-type] outlines/generate/format.py:36: error: Incompatible types in assignment (expression has type "str", variable has type "Regex") [assignment] --- examples/babyagi.py | 1 + examples/math_generate_code.py | 1 + examples/meta_prompting.py | 8 ++------ examples/parsing.py | 1 + examples/pick_odd_one_out.py | 1 + examples/react.py | 5 +++-- examples/sampling.ipynb | 25 ++++++++++++++++------- examples/self_consistency.py | 2 +- examples/simulation_based_inference.ipynb | 10 ++++++--- tests/test_function.py | 1 + tests/test_templates.py | 13 +++++++++++- 11 files changed, 48 insertions(+), 20 deletions(-) diff --git a/examples/babyagi.py b/examples/babyagi.py index 314f9dbca..a8f1de8ba 100644 --- a/examples/babyagi.py +++ b/examples/babyagi.py @@ -4,6 +4,7 @@ The original repo can be found at https://github.com/yoheinakajima/babyagi """ + from collections import deque from typing import Deque, List diff --git a/examples/math_generate_code.py b/examples/math_generate_code.py index 6d2ba8ae6..c65537ddb 100644 --- a/examples/math_generate_code.py +++ b/examples/math_generate_code.py @@ -1,4 +1,5 @@ """Example from https://dust.tt/spolu/a/d12ac33169""" + import outlines import outlines.models as models from outlines import Template diff --git a/examples/meta_prompting.py b/examples/meta_prompting.py index 85f512bf6..18f1ad41a 100644 --- a/examples/meta_prompting.py +++ b/examples/meta_prompting.py @@ -9,6 +9,7 @@ https://arxiv.org/abs/2102.07350. """ + import argparse import outlines @@ -17,7 +18,6 @@ def split_into_steps(question, model_name: str): - solve = Template.from_string( """{{question}} Rephrase : : as a true or false statement, identify an Object, relationship and subject @@ -40,7 +40,6 @@ def split_into_steps(question, model_name: str): def fill_in_the_blanks(question, model_name: str): - determine_goal = Template.from_string( """{{question}} @@ -48,9 +47,7 @@ def fill_in_the_blanks(question, model_name: str): """ ) - solve = Template.from_string( - """{{memory}}. Let's begin.""" - ) + solve = Template.from_string("""{{memory}}. Let's begin.""") model = models.openai(model_name) generator = outlines.generate.text(model) @@ -65,7 +62,6 @@ def fill_in_the_blanks(question, model_name: str): def ask_an_expert(question, model_name: str): - find_expert = Template.from_string( """ {{question}} diff --git a/examples/parsing.py b/examples/parsing.py index a10da4ebe..9e08eae14 100644 --- a/examples/parsing.py +++ b/examples/parsing.py @@ -1,4 +1,5 @@ """An example illustrating parser-based masking.""" + import math import time from copy import copy diff --git a/examples/pick_odd_one_out.py b/examples/pick_odd_one_out.py index 834c40824..f1a992411 100644 --- a/examples/pick_odd_one_out.py +++ b/examples/pick_odd_one_out.py @@ -9,6 +9,7 @@ arXiv preprint arXiv:2212.06094. """ + import outlines import outlines.models as models diff --git a/examples/react.py b/examples/react.py index 9b876eb49..9c1954e88 100644 --- a/examples/react.py +++ b/examples/react.py @@ -10,6 +10,7 @@ .. [2] Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2022). React: Synergizing reasoning and acting in language models. arXiv preprint arXiv:2210.03629. """ + import requests # type: ignore import outlines @@ -19,7 +20,7 @@ build_reAct_prompt = Template.from_string( -"""What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? + """What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? Tho 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado ... Act 2: Search 'Colorado orogeny' Obs 2: The Colorado orogeny was an episode of mountain building (an orogeny) ... @@ -33,7 +34,7 @@ add_mode = Template.from_string( -"""{{ prompt }} + """{{ prompt }} {{ mode }} {{ i }}: {{ result }} """ ) diff --git a/examples/sampling.ipynb b/examples/sampling.ipynb index bcbcca1e2..d6b50e33d 100644 --- a/examples/sampling.ipynb +++ b/examples/sampling.ipynb @@ -85,6 +85,7 @@ " },\n", "]\n", "\n", + "\n", "@text.prompt\n", "def few_shot_prompt(question, examples):\n", " \"\"\"\n", @@ -96,6 +97,7 @@ " A:\n", " \"\"\"\n", "\n", + "\n", "# Prompt functions can be partially evaluated like any other function\n", "gsm8k_prompt = ft.partial(few_shot_prompt, examples=examples)" ] @@ -155,31 +157,40 @@ " digits.append(digit)\n", " except AttributeError:\n", " print(f\"Could not parse the completion: '{answer}'\")\n", - " \n", + "\n", " unique_digits, counts = np.unique(digits, return_counts=True)\n", " return {d: c for d, c in zip(unique_digits, counts)}\n", "\n", + "\n", "def plot_counts(counts):\n", - " fig = plt.figure(figsize=(12,8))\n", + " fig = plt.figure(figsize=(12, 8))\n", " ax = fig.add_subplot(111)\n", - " \n", + "\n", " bar = ax.bar(counts.keys(), counts.values())\n", " ax.spines[[\"right\", \"top\", \"left\"]].set_visible(False)\n", " ax.get_yaxis().set_visible(False)\n", " ax.get_yaxis().set_visible(False)\n", - " \n", + "\n", " for rect in bar:\n", " height = rect.get_height()\n", - " plt.text(rect.get_x() + rect.get_width() / 2.0, height, f'{height:.0f}', ha='center', va='bottom', fontsize=20)\n", - " \n", + " plt.text(\n", + " rect.get_x() + rect.get_width() / 2.0,\n", + " height,\n", + " f\"{height:.0f}\",\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=20,\n", + " )\n", + "\n", " ax.set_xticks(list(counts.keys()))\n", " ax.set_xlabel(\"Answer\")\n", "\n", + "\n", "def entropy(counts):\n", " counts = np.array(list(counts.values()))\n", " probs = counts / np.sum(counts)\n", " log_probs = np.log(probs)\n", - " return - np.sum(probs * log_probs)" + " return -np.sum(probs * log_probs)" ] }, { diff --git a/examples/self_consistency.py b/examples/self_consistency.py index 50f12acf1..4acba9d58 100644 --- a/examples/self_consistency.py +++ b/examples/self_consistency.py @@ -78,5 +78,5 @@ answer_value = [key for key, value in results.items() if value == max_count][0] total_count = sum(results.values()) print( - f"The most likely answer is {answer_value} ({max_count/total_count*100}% consensus)" + f"The most likely answer is {answer_value} ({max_count / total_count * 100}% consensus)" ) diff --git a/examples/simulation_based_inference.ipynb b/examples/simulation_based_inference.ipynb index e6b999582..6ad52e58e 100644 --- a/examples/simulation_based_inference.ipynb +++ b/examples/simulation_based_inference.ipynb @@ -61,7 +61,9 @@ "metadata": {}, "outputs": [], "source": [ - "result = requests.get(\"https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/train.jsonl\")\n", + "result = requests.get(\n", + " \"https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/train.jsonl\"\n", + ")\n", "lines = result.iter_lines()" ] }, @@ -121,8 +123,10 @@ " A:\n", " \"\"\"\n", "\n", + "\n", "model = models.text_completion.openai(\"text-davinci-003\", max_tokens=128)\n", "\n", + "\n", "# TODO: This could largely benefit from vectorization in #52\n", "def one_train_example(problem, example_set):\n", " example_ids = random.choices(range(0, len(example_set)), k=5)\n", @@ -216,7 +220,7 @@ "\n", "example_ids, counts = np.unique(samples, return_counts=True)\n", "\n", - "fig = plt.figure(figsize=(12,8))\n", + "fig = plt.figure(figsize=(12, 8))\n", "ax = fig.add_subplot(111)\n", "ax.bar(example_ids, counts)\n", "\n", @@ -224,7 +228,7 @@ "\n", "ax.set_xticks(range(10))\n", "ax.set_xlabel(\"Example #\")\n", - "ax.set_ylabel(\"Counts\")\n" + "ax.set_ylabel(\"Counts\")" ] }, { diff --git a/tests/test_function.py b/tests/test_function.py index d66f3911c..8bb12976e 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -9,6 +9,7 @@ def test_function_basic(): with pytest.deprecated_call(match="The @prompt decorator"): + @outlines.prompt def test_template(text: str): """{{ text }}""" diff --git a/tests/test_templates.py b/tests/test_templates.py index aa9939278..2fda3caa3 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -114,6 +114,7 @@ def test_render_jinja(): def test_prompt_basic(): with pytest.deprecated_call(): + @outlines.prompt def test_tpl(variable): """{{variable}} test""" @@ -141,6 +142,7 @@ def test_single_quote_tpl(variable): def test_prompt_kwargs(): with pytest.deprecated_call(): + @outlines.prompt def test_kwarg_tpl(var, other_var="other"): """{{var}} and {{other_var}}""" @@ -184,6 +186,7 @@ def with_description(): pass with pytest.deprecated_call(): + @outlines.prompt def name_description_ppt(fn): """ @@ -200,6 +203,7 @@ def with_signature(one: int, two: List[str], three: float = 1.0): pass with pytest.deprecated_call(): + @outlines.prompt def name_signature_ppt(fn): """ @@ -207,12 +211,15 @@ def name_signature_ppt(fn): """ rendered = name_signature_ppt(with_signature) - assert rendered == "with_signature: one: int, two: List[str], three: float = 1.0" + assert ( + rendered == "with_signature: one: int, two: List[str], three: float = 1.0" + ) def test_function_call(one, two=2): return one + two with pytest.deprecated_call(): + @outlines.prompt def source_ppt(fn): """ @@ -229,6 +236,7 @@ class SimpleResponse(BaseModel): two: str with pytest.deprecated_call(): + @outlines.prompt def source_ppt(model): "{{model | schema }}" @@ -261,6 +269,7 @@ def test_prompt_dict_response(): response = {"one": "a description", "two": ""} with pytest.deprecated_call(): + @outlines.prompt def source_ppt(model): "{{model | schema }}" @@ -306,6 +315,7 @@ def with_all( pass with pytest.deprecated_call(): + @outlines.prompt def args_prompt(fn): """args: {{ fn | args }}""" @@ -335,6 +345,7 @@ def reverse(s: str) -> str: return s[::-1] with pytest.deprecated_call(): + @outlines.prompt(filters=dict(reverse=reverse)) def test_tpl(variable): """{{ variable | reverse }} test""" From e91a64c40bef28da70bd183736458c717d58a050 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Mon, 3 Mar 2025 15:01:00 +0100 Subject: [PATCH 180/505] Remove `pre-commit` runtime dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 969290b07..dfed39973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ dependencies = [ "torch", "outlines_core==0.1.26", "genson", - "pre-commit>=4.0.1", ] dynamic = ["version"] From 4701ecd6b94d48e431fb022e1fdc6d7b81eaebb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Fri, 28 Feb 2025 15:02:13 +0100 Subject: [PATCH 181/505] Remove `|` and us `either` factory function instead `|` is not defined for strings, so using it has the potential to lead to confusing errors. We thus replace the operator overloading with a more explicit `either` factory function which converts string arguments to `String` objects automatically. --- outlines/types/dsl.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/outlines/types/dsl.py b/outlines/types/dsl.py index 94b4d26d9..e5163b0ce 100644 --- a/outlines/types/dsl.py +++ b/outlines/types/dsl.py @@ -43,18 +43,6 @@ def __radd__(self: "Term", other: Union[str, "Term"]) -> "Sequence": return Sequence([other, self]) - def __or__(self: "Term", other: Union[str, "Term"]) -> "Alternatives": - if isinstance(other, str): - other = String(other) - - return Alternatives([self, other]) - - def __ror__(self: "Term", other: Union[str, "Term"]) -> "Alternatives": - if isinstance(other, str): - other = String(other) - - return Alternatives([other, self]) - def __get_validator__(self, _core_schema): def validate(input_value): return self.validate(input_value) @@ -310,6 +298,16 @@ def __repr__(self): def optional(self: Term) -> Optional: +def either(*args: Union[str, Term]): + """Represents an alternative between different terms or strings. + + This factory function automatically translates string arguments + into `String` objects. + """ + args = [String(arg) if arg is str else arg for arg in args] + return Alternatives(args) + + return Optional(self) From cf2eaf7c3272b0806a4b1c7b3f4ae4bec1d52a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Fri, 28 Feb 2025 15:04:59 +0100 Subject: [PATCH 182/505] Convert string inputs into `String` objects automatically --- outlines/types/dsl.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/outlines/types/dsl.py b/outlines/types/dsl.py index e5163b0ce..0246b28ef 100644 --- a/outlines/types/dsl.py +++ b/outlines/types/dsl.py @@ -297,7 +297,6 @@ def __repr__(self): return f"QuantifyBetween(term={repr(self.term)}, min_count={repr(self.min_count)}, max_count={repr(self.max_count)})" -def optional(self: Term) -> Optional: def either(*args: Union[str, Term]): """Represents an alternative between different terms or strings. @@ -308,14 +307,18 @@ def either(*args: Union[str, Term]): return Alternatives(args) +def optional(self: Union[Term, str]) -> Optional: + self = String(self) if self is str else self return Optional(self) def one_or_more(self: Term) -> KleenePlus: + self = String(self) if self is str else self return KleenePlus(self) -def repeat(self: Term, min_count: int, max_count: int) -> QuantifyBetween: +def between(self: Term, min_count: int, max_count: int) -> QuantifyBetween: + self = String(self) if self is str else self match (min_count, max_count): case (None, None): raise ValueError( @@ -330,10 +333,12 @@ def repeat(self: Term, min_count: int, max_count: int) -> QuantifyBetween: def times(self: Term, count: int = 0) -> QuantifyExact: + self = String(self) if self is str else self return QuantifyExact(self, count) def zero_or_more(self: Term) -> KleeneStar: + self = String(self) if self is str else self return KleeneStar(self) From 3ca64b325171e99a85c9ef98af53d8666397195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Fri, 28 Feb 2025 15:15:20 +0100 Subject: [PATCH 183/505] Update the quantifiers factory functions --- outlines/types/__init__.py | 11 +- outlines/types/dsl.py | 95 ++++++++++------- tests/types/test_dsl.py | 205 ++++++++++++++++++++++--------------- 3 files changed, 187 insertions(+), 124 deletions(-) diff --git a/outlines/types/__init__.py b/outlines/types/__init__.py index dbd8f507b..d7f40309a 100644 --- a/outlines/types/__init__.py +++ b/outlines/types/__init__.py @@ -4,12 +4,15 @@ from outlines.types.dsl import ( Regex, json_schema, - one_or_more, - optional, regex, - repeat, + either, + optional, + exactly, + at_least, + at_most, + between, + one_or_more, zero_or_more, - times, ) # Python types diff --git a/outlines/types/dsl.py b/outlines/types/dsl.py index 0246b28ef..73de3876c 100644 --- a/outlines/types/dsl.py +++ b/outlines/types/dsl.py @@ -103,6 +103,27 @@ def _display_children(self, indent: str) -> str: def __str__(self): return self.display_ascii_tree() + def optional(self) -> "Optional": + return optional(self) + + def exactly(self, count: int) -> "QuantifyExact": + return exactly(count, self) + + def at_least(self, count: int) -> "QuantifyMinimum": + return at_least(count, self) + + def at_most(self, count: int) -> "QuantifyMaximum": + return at_most(count, self) + + def between(self, min_count: int, max_count: int) -> "QuantifyBetween": + return between(min_count, max_count, self) + + def one_or_more(self) -> "KleenePlus": + return one_or_more(self) + + def zero_or_more(self) -> "KleeneStar": + return zero_or_more(self) + @dataclass class String(Term): @@ -297,64 +318,60 @@ def __repr__(self): return f"QuantifyBetween(term={repr(self.term)}, min_count={repr(self.min_count)}, max_count={repr(self.max_count)})" -def either(*args: Union[str, Term]): +def regex(pattern: str): + return Regex(pattern) + + +def json_schema(schema: Union[str, dict, type[BaseModel]]): + return JsonSchema(schema) + + +def either(*terms: Union[str, Term]): """Represents an alternative between different terms or strings. This factory function automatically translates string arguments into `String` objects. """ - args = [String(arg) if arg is str else arg for arg in args] - return Alternatives(args) + terms = [String(arg) if isinstance(arg, str) else arg for arg in terms] + return Alternatives(terms) -def optional(self: Union[Term, str]) -> Optional: - self = String(self) if self is str else self - return Optional(self) +def optional(term: Union[Term, str]) -> Optional: + term = String(term) if isinstance(term, str) else term + return Optional(term) -def one_or_more(self: Term) -> KleenePlus: - self = String(self) if self is str else self - return KleenePlus(self) - - -def between(self: Term, min_count: int, max_count: int) -> QuantifyBetween: - self = String(self) if self is str else self - match (min_count, max_count): - case (None, None): - raise ValueError( - "repeat: you must provide a value for at least `min_count` or `max_count`" - ) - case (_, None): - return QuantifyMinimum(self, min_count) - case (None, _): - return QuantifyMaximum(self, max_count) - case _: - return QuantifyBetween(self, min_count, max_count) +def exactly(count: int, term: Union[Term, str]) -> QuantifyExact: + """Repeat the term exactly `count` times.""" + term = String(term) if isinstance(term, str) else term + return QuantifyExact(term, count) -def times(self: Term, count: int = 0) -> QuantifyExact: - self = String(self) if self is str else self - return QuantifyExact(self, count) +def at_least(count: int, term: Union[Term, str]) -> QuantifyMinimum: + """Repeat the term at least `count` times.""" + term = String(term) if isinstance(term, str) else term + return QuantifyMinimum(term, count) -def zero_or_more(self: Term) -> KleeneStar: - self = String(self) if self is str else self - return KleeneStar(self) +def at_most(count: int, term: Union[Term, str]) -> QuantifyMaximum: + """Repeat the term exactly `count` times.""" + term = String(term) if isinstance(term, str) else term + return QuantifyMaximum(term, count) -Term.one_or_more = one_or_more # type: ignore -Term.optional = optional # type: ignore -Term.repeat = repeat # type: ignore -Term.times = times # type: ignore -Term.zero_or_more = zero_or_more # type: ignore +def between(min_count: int, max_count: int, term: Union[Term, str]) -> QuantifyBetween: + term = String(term) if isinstance(term, str) else term + return QuantifyBetween(term, min_count, max_count) -def regex(pattern: str): - return Regex(pattern) +def zero_or_more(term: Union[Term, str]) -> KleeneStar: + term = String(term) if isinstance(term, str) else term + return KleeneStar(term) -def json_schema(schema: Union[str, dict, type[BaseModel]]): - return JsonSchema(schema) +def one_or_more(term: Union[Term, str]) -> KleenePlus: + term = String(term) if isinstance(term, str) else term + return KleenePlus(term) def to_regex(term: Term) -> str: diff --git a/tests/types/test_dsl.py b/tests/types/test_dsl.py index dc2779ef1..8351ef319 100644 --- a/tests/types/test_dsl.py +++ b/tests/types/test_dsl.py @@ -15,13 +15,16 @@ Sequence, String, Term, + either, one_or_more, + zero_or_more, optional, - repeat, - times, + between, + at_most, + at_least, + exactly, regex, json_schema, - zero_or_more, ) @@ -96,117 +99,157 @@ def test_dsl_init(): QuantifyBetween(string, 3, 1) -def test_dsl_operations(): +def test_dsl_sequence(): a = String("a") b = String("b") - assert isinstance(a + b, Sequence) - assert (a + b).terms[0] == a - assert (a + b).terms[1] == b - - assert isinstance(a | b, Alternatives) - assert (a | b).terms[0] == a - assert (a | b).terms[1] == b + sequence = a + b + assert isinstance(sequence, Sequence) + assert sequence.terms[0] == a + assert sequence.terms[1] == b -def test_dsl_operations_string_conversion(): - b = String("b") sequence = "a" + b assert isinstance(sequence, Sequence) assert isinstance(sequence.terms[0], String) assert sequence.terms[0].value == "a" assert sequence.terms[1].value == "b" - sequence = b + "a" + sequence = a + "b" assert isinstance(sequence, Sequence) - assert isinstance(sequence.terms[0], String) - assert sequence.terms[0].value == "b" - assert sequence.terms[1].value == "a" + assert isinstance(sequence.terms[1], String) + assert sequence.terms[0].value == "a" + assert sequence.terms[1].value == "b" - alternative = "a" | b - assert isinstance(alternative, Alternatives) - assert isinstance(alternative.terms[0], String) - assert alternative.terms[0].value == "a" - assert alternative.terms[1].value == "b" - alternative = b | "a" - assert isinstance(alternative, Alternatives) - assert isinstance(alternative.terms[0], String) - assert alternative.terms[0].value == "b" - assert alternative.terms[1].value == "a" +def test_dsl_alternatives(): + a = String("a") + b = String("b") + alt = either(a, b) + assert isinstance(alt, Alternatives) + assert isinstance(alt.terms[0], String) + assert isinstance(alt.terms[1], String) -def test_dsl_aliases(): - test = regex("[0-9]") - assert isinstance(test, Regex) + alt = either("a", "b") + assert isinstance(alt, Alternatives) + assert isinstance(alt.terms[0], String) + assert isinstance(alt.terms[1], String) - test = json_schema('{"type": "string"}') - assert isinstance(test, JsonSchema) + alt = either("a", b) + assert isinstance(alt, Alternatives) + assert isinstance(alt.terms[0], String) + assert isinstance(alt.terms[1], String) + + +def test_dsl_optional(): + a = String("a") + + opt = optional(a) + assert isinstance(opt, Optional) + + opt = optional("a") + assert isinstance(opt, Optional) + assert isinstance(opt.term, String) + + opt = a.optional() + assert isinstance(opt, Optional) + + +def test_dsl_exactly(): + a = String("a") + + rep = exactly(2, a) + assert isinstance(rep, QuantifyExact) + assert rep.count == 2 + + rep = exactly(2, "a") + assert isinstance(rep, QuantifyExact) + assert isinstance(rep.term, String) + + rep = a.exactly(2) + assert isinstance(rep, QuantifyExact) + + +def test_dsl_at_least(): + a = String("a") + + rep = at_least(2, a) + assert isinstance(rep, QuantifyMinimum) + assert rep.min_count == 2 - test = String("test") + rep = at_least(2, "a") + assert isinstance(rep, QuantifyMinimum) + assert isinstance(rep.term, String) - assert isinstance(test.times(3), QuantifyExact) - assert test.times(3).count == 3 - assert test.times(3).term == test + rep = a.at_least(2) + assert isinstance(rep, QuantifyMinimum) - assert isinstance(times(test, 3), QuantifyExact) - assert times(test, 3).count == 3 - assert times(test, 3).term == test - assert isinstance(test.one_or_more(), KleenePlus) - assert test.one_or_more().term == test +def test_dsl_at_most(): + a = String("a") - assert isinstance(one_or_more(test), KleenePlus) - assert one_or_more(test).term == test + rep = at_most(2, a) + assert isinstance(rep, QuantifyMaximum) + assert rep.max_count == 2 - assert isinstance(test.zero_or_more(), KleeneStar) - assert test.zero_or_more().term == test + rep = at_most(2, "a") + assert isinstance(rep, QuantifyMaximum) + assert isinstance(rep.term, String) - assert isinstance(zero_or_more(test), KleeneStar) - assert zero_or_more(test).term == test + rep = a.at_most(2) + assert isinstance(rep, QuantifyMaximum) - assert isinstance(test.optional(), Optional) - assert test.optional().term == test - assert isinstance(optional(test), Optional) - assert optional(test).term == test +def test_between(): + a = String("a") - rep_min = test.repeat(2, None) - assert isinstance(rep_min, QuantifyMinimum) - assert rep_min.min_count == 2 + rep = between(1, 2, a) + assert isinstance(rep, QuantifyBetween) + assert rep.min_count == 1 + assert rep.max_count == 2 - rep_min = repeat(test, 2, None) - assert isinstance(rep_min, QuantifyMinimum) - assert rep_min.min_count == 2 + rep = between(1, 2, "a") + assert isinstance(rep, QuantifyBetween) + assert isinstance(rep.term, String) - rep_max = test.repeat(None, 2) - assert isinstance(rep_max, QuantifyMaximum) - assert rep_max.max_count == 2 + rep = a.between(1, 2) + assert isinstance(rep, QuantifyBetween) - rep_max = repeat(test, None, 2) - assert isinstance(rep_max, QuantifyMaximum) - assert rep_max.max_count == 2 - rep_between = test.repeat(1, 2) - assert isinstance(rep_between, QuantifyBetween) - assert rep_between.min_count == 1 - assert rep_between.max_count == 2 +def test_dsl_zero_or_more(): + a = String("a") - rep_between = repeat(test, 1, 2) - assert isinstance(rep_between, QuantifyBetween) - assert rep_between.min_count == 1 - assert rep_between.max_count == 2 + rep = zero_or_more(a) + assert isinstance(rep, KleeneStar) - with pytest.raises(ValueError, match="QuantifyBetween: `max_count` must be"): - test.repeat(2, 1) + rep = zero_or_more("a") + assert isinstance(rep, KleeneStar) + assert isinstance(rep.term, String) - with pytest.raises(ValueError, match="QuantifyBetween: `max_count` must be"): - repeat(test, 2, 1) + rep = a.zero_or_more() + assert isinstance(rep, KleeneStar) - with pytest.raises(ValueError, match="repeat: you must provide"): - test.repeat(None, None) - with pytest.raises(ValueError, match="repeat: you must provide"): - repeat(test, None, None) +def test_dsl_one_or_more(): + a = String("a") + + rep = one_or_more(a) + assert isinstance(rep, KleenePlus) + + rep = one_or_more("a") + assert isinstance(rep, KleenePlus) + assert isinstance(rep.term, String) + + rep = a.zero_or_more() + assert isinstance(rep, KleeneStar) + + +def test_dsl_aliases(): + test = regex("[0-9]") + assert isinstance(test, Regex) + + test = json_schema('{"type": "string"}') + assert isinstance(test, JsonSchema) def test_dsl_term_pydantic_simple(): @@ -230,7 +273,7 @@ def test_dsl_term_pydantic_combination(): c = String("c") class Model(BaseModel): - field: (a + b) | c + field: either((a + b), c) schema = Model.model_json_schema() assert schema == { @@ -247,7 +290,7 @@ def test_dsl_display(): a = String("a") b = String("b") c = Regex("[0-9]") - d = KleeneStar(a | b) + c + d = Sequence([KleeneStar(Alternatives([a, b])), c]) tree = str(d) assert ( From fde48c850bf5a7fa39053c033e6e36e2993edc15 Mon Sep 17 00:00:00 2001 From: Robin Picard Date: Tue, 4 Mar 2025 15:20:34 +0100 Subject: [PATCH 184/505] Update the documentation of the regex dsl --- docs/reference/regex_dsl.md | 114 ++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/docs/reference/regex_dsl.md b/docs/reference/regex_dsl.md index aa04303cf..f42b6dd34 100644 --- a/docs/reference/regex_dsl.md +++ b/docs/reference/regex_dsl.md @@ -39,43 +39,47 @@ print(to_regex(digit)) # Output: [0-9]+ --- -## Early Introduction to Quantifiers & Operators +## Early Introduction to Quantifiers & Combining Terms The DSL supports common regex quantifiers as methods on every `Term`. These methods allow you to specify how many times a pattern should be matched. They include: -- **`times(count)`**: Matches the term exactly `count` times. +- **`exactly(count)`**: Matches the term exactly `count` times. - **`optional()`**: Matches the term zero or one time. - **`one_or_more()`**: Matches the term one or more times (Kleene Plus). - **`zero_or_more()`**: Matches the term zero or more times (Kleene Star). -- **`repeat(min_count, max_count)`**: Matches the term between `min_count` and `max_count` times (or open-ended if one value is omitted). +- **`between(min_count, max_count)`**: Matches the term between `min_count` and `max_count` times (inclusive). +- **`at_least(count)`**: Matches the term at least `count` times. +- **`at_most(count)`**: Matches the term up to `count` times. -Let’s see these quantifiers side by side with examples. +These quantifiers can also be used as functions that take the `Term` as an argument. If the term is a plain string, it will be automatically converted to a `String` object. Thus `String("foo").optional()` is equivalent to `optional("foo")`. + +Let's see these quantifiers side by side with examples. ### Quantifiers in Action -#### `times(count)` +#### `exactly(count)` This method restricts the term to appear exactly `count` times. ```python # Example: exactly 5 digits -five_digits = Regex(r"\d").times(5) +five_digits = Regex(r"\d").exactly(5) print(to_regex(five_digits)) # Output: (\d){5} ``` -You can also use the `times` function: +You can also use the `exactly` function: ```python -from outlines.types import times +from outlines.types import exactly # Example: exactly 5 digits -five_digits = times(Regex(r"\d"), 5) +five_digits = exactly(Regex(r"\d"), 5) print(to_regex(five_digits)) # Output: (\d){5} ``` #### `optional()` -The `optional()` method makes a term optional, meaning it may occur zero or one time. +This method makes a term optional, meaning it may occur zero or one time. ```python # Example: an optional "s" at the end of a word @@ -83,7 +87,7 @@ maybe_s = String("s").optional() print(to_regex(maybe_s)) # Output: (s)? ``` -You can also use the `optional` function, the string will automatically be converted to a `String` object: +You can also use the `optional` function: ```python from outlines.types import optional @@ -116,7 +120,7 @@ print(to_regex(letters)) # Output: ([A-Za-z])+ #### `zero_or_more()` -This method means that the term can occur zero or more times. +This method indicates that the term can occur zero or more times. ```python # Example: zero or more spaces @@ -124,7 +128,7 @@ spaces = String(" ").zero_or_more() print(to_regex(spaces)) # Output: ( )* ``` -You can also use the `zero_or_more` function, the string will automatically be converted to a `String` instance: +You can also use the `zero_or_more` function: ```python from outlines.types import zero_or_more @@ -134,40 +138,64 @@ spaces = zero_or_more(" ") print(to_regex(spaces)) # Output: ( )* ``` -#### `repeat(min_count, max_count)` +#### `between(min_count, max_count)` -The `repeat` method provides flexibility to set a lower and/or upper bound on the number of occurrences. +This method indicates that the term can appear any number of times between `min_count` and `max_count` (inclusive). ```python # Example: Between 2 and 4 word characters -word_chars = Regex(r"\w").repeat(2, 4) +word_chars = Regex(r"\w").between(2, 4) print(to_regex(word_chars)) # Output: (\w){2,4} - -# Example: At least 3 digits (min specified, max left open) -at_least_three = Regex(r"\d").repeat(3, None) -print(to_regex(at_least_three)) # Output: (\d){3,} - -# Example: Up to 2 punctuation marks (max specified, min omitted) -up_to_two = Regex(r"[,.]").repeat(None, 2) -print(to_regex(up_to_two)) # Output: ([,.]){,2} ``` -You can also use the `repeat` function: +You can also use the `between` function: ```python -from outlines import repeat +from outlines.types import between # Example: Between 2 and 4 word characters -word_chars = repeat(Regex(r"\w"), 2, 4) +word_chars = between(Regex(r"\w"), 2, 4) print(to_regex(word_chars)) # Output: (\w){2,4} +``` + +#### `at_least(count)` -# Example: At least 3 digits (min specified, max left open) -at_least_three = repeat(Regex(r"\d"), 3, None) +This method indicates that the term must appear at least `count` times. + +```python +# Example: At least 3 digits +at_least_three = Regex(r"\d").at_least(3) print(to_regex(at_least_three)) # Output: (\d){3,} +``` + +You can also use the `at_least` function: + +```python +from outlines.types import at_least + +# Example: At least 3 digits +at_least_three = at_least(Regex(r"\d"), 3) +print(to_regex(at_least_three)) # Output: (\d){3,} +``` + +#### `at_most(count)` + +This method indicates that the term can appear at most `count` times. + +```python +# Example: At most 3 digits +up_to_three = Regex(r"\d").at_most(3) +print(to_regex(up_to_three)) # Output: (\d){0,3} +``` + +You can also use the `at_most` function: + +```python +from outlines.types import at_most -# Example: Up to 2 punctuation marks (max specified, min omitted) -up_to_two = repeat(Regex(r"[,.]"), None, 2) -print(to_regex(up_to_two)) # Output: ([,.]){,2} +# Example: At most 3 digits +up_to_three = at_most(Regex(r"\d"), 3) +print(to_regex(up_to_three)) # Output: (\d){0,3} ``` --- @@ -186,17 +214,17 @@ pattern = String("hello") + " " + Regex(r"\w+") print(to_regex(pattern)) # Output: hello\ (\w+) ``` -### Alternation (`|`) +### Alternation (`either()`) -The `|` operator creates alternatives, allowing a match for one of several patterns. +The `either()` function creates alternatives, allowing a match for one of several patterns. You can provide as many terms as you want. ```python -# Example: Match either "cat" or "dog" -animal = String("cat") | "dog" -print(to_regex(animal)) # Output: (cat|dog) +# Example: Match either "cat" or "dog" or "mouse" +animal = either(String("cat"), "dog", "mouse") +print(to_regex(animal)) # Output: (cat|dog|mouse) ``` -*Note:* When using operators with plain strings (such as `"dog"`), the DSL automatically wraps them in a `String` object and escapes the characters that have a special meaning in regular expressions. +*Note:* When using `either()` with plain strings (such as `"dog"`), the DSL automatically wraps them in a `String` object that escapes the characters that have a special meaning in regular expressions, just like with quantifier functions. --- @@ -223,7 +251,7 @@ For instance you can describe the answers in the GSM8K dataset using the followi ```python from outlines.types import sentence, digit -answer = "A: " + sentence.repeat(2,4) + " So the answer is: " + digit.repeat(1,4) +answer = "A: " + sentence.between(2,4) + " So the answer is: " + digit.between(1,4) ``` --- @@ -237,7 +265,7 @@ Suppose you want to create a regex that matches an ID format like "ID-12345", wh - Followed by exactly 5 digits. ```python -id_pattern = "ID-" + Regex(r"\d").times(5) +id_pattern = "ID-" + Regex(r"\d").exactly(5) print(to_regex(id_pattern)) # Output: ID-(\d){5} ``` @@ -273,9 +301,9 @@ When used in a Pydantic model, the email field is automatically validated agains Consider a pattern to match a simple date format: `YYYY-MM-DD`. ```python -year = Regex(r"\d").times(4) # Four digits for the year -month = Regex(r"\d").times(2) # Two digits for the month -day = Regex(r"\d").times(2) # Two digits for the day +year = Regex(r"\d").exactly(4) # Four digits for the year +month = Regex(r"\d").exactly(2) # Two digits for the month +day = Regex(r"\d").exactly(2) # Two digits for the day # Combine with literal hyphens date_pattern = year + "-" + month + "-" + day From 9dd927d5fdb3b39560092d2a9881a7bdc7dc374f Mon Sep 17 00:00:00 2001 From: Frederik Fix Date: Tue, 4 Mar 2025 17:08:35 +0100 Subject: [PATCH 185/505] fix schema schema issue --- outlines/generate/json.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/outlines/generate/json.py b/outlines/generate/json.py index 008662206..ab05df1f4 100644 --- a/outlines/generate/json.py +++ b/outlines/generate/json.py @@ -92,7 +92,9 @@ def json_openai( ) if isinstance(schema_object, type(BaseModel)): - schema = pyjson.dumps(schema_object.model_json_schema()) + schema = schema_object.model_json_schema() + schema["additionalProperties"] = False + schema = pyjson.dumps(schema) format_sequence = lambda x: schema_object.parse_raw(x) elif isinstance(schema_object, str): schema = schema_object From d1a9adaddc2e4bf254fed695408e9c362d705fbf Mon Sep 17 00:00:00 2001 From: Nicholas Chammas Date: Wed, 5 Mar 2025 12:08:43 -0500 Subject: [PATCH 186/505] Tweak version commands in bug_report.yml The way these commands show up when a user fills out a new bug report is misleading, as they all get mashed into one line. Copy-pasting this results in only the first command running, which is presumably not what the template author intended. --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index af90ed988..cbc67f624 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -63,9 +63,9 @@ body: label: "Outlines/Python version information:" description: | Please run the following code and paste the output here. - python -c "from outlines import _version; print(_version.__version__)" - python -c "import sys; print('Python', sys.version)" - pip freeze + python -c "from outlines import _version; print(_version.__version__)"; + python -c "import sys; print('Python', sys.version)"; + pip freeze; value: | Version information
From 433581ace44a045faa47c2fea3ecb05d8f1063f8 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Mon, 10 Mar 2025 11:31:32 -0700 Subject: [PATCH 187/505] Fix capitalization in installation.md --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 5f8bb2d13..f88bc7125 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,5 +1,5 @@ --- -title: installation +title: Installation --- # Installation From fc6192f5d6ec67c4cb0ce9b3f5b96e04aa5f8931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Sat, 15 Mar 2025 11:37:52 +0100 Subject: [PATCH 188/505] Remove all issue templates except bug reports --- .github/ISSUE_TEMPLATE/doc_request.md | 21 -------------- .github/ISSUE_TEMPLATE/feature_request.md | 27 ------------------ .github/ISSUE_TEMPLATE/improvement.yml | 34 ----------------------- 3 files changed, 82 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/doc_request.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/ISSUE_TEMPLATE/improvement.yml diff --git a/.github/ISSUE_TEMPLATE/doc_request.md b/.github/ISSUE_TEMPLATE/doc_request.md deleted file mode 100644 index f9dd65f92..000000000 --- a/.github/ISSUE_TEMPLATE/doc_request.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: 📚 Documentation -about: Help with documentation improvements. -title: '' -labels: '' -assignees: '' ---- - -### Documentation request - -Documentation is a high priority for outlines. If you would like to see something added to the documentation, please help us out by: - -- Checking to see if the documentation does not already exist. -- Providing a detailed explanation of what you'd like to understand better. -- Highlighting any specific difficulties you ran into. - -### Are you willing to open a PR? - -Tell us whether you are willing to submit a PR for this yourself, we'd greatly appreciate it! - -Thanks for contributing! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 702a41bd1..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: 🚀 New feature -about: Request a new feature. -title: '' -labels: '' -assignees: '' ---- - -Before suggesting a new feature, please make sure this hasn't already been suggested by searching through the past issues and the PR tracker. - -### Presentation of the new feature - -Assume that we know nothing about this feature. Please give us as much information as possible so we can judge it fairly. That includes (but is not limited to): -- Academic article -- Blog posts -- Implementations -- Personnal experience with the feature - -### Where does it fit in Outlines? - -Please explain to us why you are suggesting this feature for integration in Outlines. - -### Are you willing to open a PR? - -Tell us whether you are willing to add the feature yourself, and if so if you can share a design plan. **You may be challenged.** - -Thanks for contributing! diff --git a/.github/ISSUE_TEMPLATE/improvement.yml b/.github/ISSUE_TEMPLATE/improvement.yml deleted file mode 100644 index 64212eedd..000000000 --- a/.github/ISSUE_TEMPLATE/improvement.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: ✨ Improvement -description: Propose an improvement to Outlines. -title: "" -labels: "enhancement" - -body: - - type: markdown - attributes: - value: >- - Before suggesting an improvement, please make sure this hasn't already been suggested by searching through the past issues and the PR tracker. - - ## Current behavior - - - type: textarea - attributes: - label: "What behavior of the library made you think about the improvement?" - placeholder: | - Sample code with the current behavior - - - type: markdown - attributes: - value: >- - ## Desired behavior - - - type: textarea - attributes: - label: "How would you like it to behave?" - placeholder: | - Sample code with the desired behavior - - - type: markdown - attributes: - value: >- - **Be aware that your proposal may be challenged.** Outlines has strong design principles that we are committed to stick to. If there is an alternative way to achieve what you would like, we will let you know. From 05d775fe628ec9929e65ee150bc5322b17dd384f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Louf?= Date: Thu, 3 Apr 2025 12:01:49 +0200 Subject: [PATCH 189/505] Update the logo --- README.md | 8 +- docs/assets/images/dottxt-dark.svg | 1 + docs/assets/images/dottxt-light.svg | 32 ++++ docs/assets/images/logo-dark-mode.png | Bin 0 -> 7425 bytes docs/assets/images/logo-dark-mode.svg | 207 ++++++++++++++++++++++ docs/assets/images/logo-light-mode.png | Bin 0 -> 9174 bytes docs/assets/images/logo-light-mode.svg | 1 + docs/assets/images/logo-simple.png | Bin 2196 -> 3323 bytes docs/assets/images/logo-square.png | Bin 2233 -> 3323 bytes docs/assets/images/logo-square.svg | 229 +++++++++++++------------ docs/assets/images/logo.png | Bin 6386 -> 0 bytes docs/assets/images/logo.svg | 134 --------------- docs/overrides/home.html | 3 +- 13 files changed, 367 insertions(+), 248 deletions(-) create mode 100644 docs/assets/images/dottxt-dark.svg create mode 100644 docs/assets/images/dottxt-light.svg create mode 100644 docs/assets/images/logo-dark-mode.png create mode 100644 docs/assets/images/logo-dark-mode.svg create mode 100644 docs/assets/images/logo-light-mode.png create mode 100644 docs/assets/images/logo-light-mode.svg delete mode 100644 docs/assets/images/logo.png delete mode 100644 docs/assets/images/logo.svg diff --git a/README.md b/README.md index 01d7b9c98..419282561 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@
-Outlines Logo +Outlines Logo +Outlines Logo 🗒️ *Make LLMs speak the language of every application.* 🗒️ @@ -48,10 +49,11 @@ Outlines has new releases and features coming every week. Make sure to ⭐ star ## .txt company
-Outlines Logo +dottxt logo +dottxt logo
-We started a company to keep pushing the boundaries of structured generation. Learn more about [.txt](https://twitter.com/dottxtai), and [give our .json API a try](https://h1xbpbfsf0w.typeform.com/to/ZgBCvJHF) if you need a hosted solution ✨ +We started a company to keep pushing the boundaries of structured generation. Learn more about [.txt](https://dottxt.ai), and [license our commercial product](https://h1xbpbfsf0w.typeform.com/to/ZgBCvJHF) if you need SOTA performance and quality ✨ ## Structured generation diff --git a/docs/assets/images/dottxt-dark.svg b/docs/assets/images/dottxt-dark.svg new file mode 100644 index 000000000..a95ff7d32 --- /dev/null +++ b/docs/assets/images/dottxt-dark.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/images/dottxt-light.svg b/docs/assets/images/dottxt-light.svg new file mode 100644 index 000000000..567e239ee --- /dev/null +++ b/docs/assets/images/dottxt-light.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/logo-dark-mode.png b/docs/assets/images/logo-dark-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..24c5f3bc36cd271902376dcaebc7c667f9e909a4 GIT binary patch literal 7425 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 tDX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/logo-light-mode.png b/docs/assets/images/logo-light-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..ac04c20f52f01fcbdca2913c22df1e0e9f63d13a GIT binary patch literal 9174 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 diff --git a/docs/assets/images/logo-simple.png b/docs/assets/images/logo-simple.png index 2c14b4d9f9d8e38431c44c004594d83ad6a111e8..9bbb39243415b826a71ba65121b20ada5a7b9503 100644 GIT binary patch literal 3323 zcmc&%X;4#H8V#{is_w1(z5AW- zob%m#;|n+E9b5Ib!eFo+E@w`C34>`IP>;WAZc@(>$KYu7XG_c(-*^}dfq6YNt}w}f zdU8*KlTU&NCM*GaG42u!i^ZCL6^)7yxfpZF6cZO-JZ-NBgKfX=a_YF}`+eAUmyrKmLb{CB12HfHM?{V9U{^z0`Gzi9{!uN0hf+E4jogvp<;o;har zjV9&Jt#3=6Dv(xJzd$v3?(?Mh&?eoi>9-4aoIvl|I|$jt8=8GLfDA3NUvJq8MIp^@ zH!sblC}Mi?a_lUtVH6+eVx3oOKZ+Zn4GI@T7{?a*Lu2PKYj1Bu3J8Bzagzypk6!$+*)wf+>a${62jmYG-F0oDm@c0nE8F_Q03lg4EMPry zGFZNupY9vDJPWRVr;k3p%m>N9-2XiM^&$7ont-@wBcW zI;fYzmLXlS)wvS+!fb^usNLFFu&Fg|(0Fav7w2yyi8GCdAxYG}v^O%-=1@v_(_Flf|*18#z8^RqGpQ6lG#p=|hsg*ok1-eVRd=0mxe?VftmFI_r`O$aX zc4xGW3s|BjhyYJUtjOW=S5z-;gQKyc418*DrzjA?QNAp24#RQz298ZD>G_2{s=O3; zn%v*Wk|Im9Ymh%==dc29Y#@U;?h`M_jx?HKu%LWM(6=Oa^f0)JymK^RrnX-s-zQaE zg0w8>&YjWR@X7m~=S_mc?jTtCT+TMtakX({Y&JvJGawG3q+ckD;7ubetf*WqjG^6} zO@=dsD(3bbLF45t!5;7Z?I1XaNKlslFq%Gj5OI(21K69xiqf}>3!=$y-OcbTMI;16 zD|h#RKN;=YdFq2c7*kLOS`%xR-DFC7l|oQS8(1CdyJ76uGAJzZbx=atVENv{NuZqU zi|^`YMb*oS)FL6~uo{?)!*h>}39+FQXZQO60AnPOD=o$CNb2Jur2WUU{Bt3lYmwBW z%Y9XMzl|~sO2$uxw0`!tA#rggb9L)@{?2zT`JKw&bM*ljI;|W({0Ij>^Hhgg*{_Q2 z9r#kktE4C&}-%#mld%()uhT7S~$`lNqG$QE9FI+bQGGxYeh8jNHx8ogVwzaY}s1&RRp z+6wg`@JA3Fg;#kI-5Hz2|4IVJik>j$0R^YZBVz|tN93-R@>wGGn&75`FUU5-@h`gbyv<3 zXIp@lwKqQ|oJ>KzF8EUvAt_lh+wUPYKdkmIup@&p#PGmq*!kQ4l6Ut1kT<}u2HEb6 zjpAyI8>4;vSRELdjD|`*q_7tIkFIa?S};7xTrmVj8m;G1mr(2$RZT}X>n_GY76tji zi!_p?{n92?+d(0F^pJ%m`Tj*+_){dn&eNies%Bi&&cg<{xeqcP+{!yp>p;Ls*%}~d zoyT9I4}`;cSrsdI_1OqMs(g`ynC+0WkJx ooF=m)A%l%qHm_eBt7g+qwHY~_RyAv@A1*K#C%02ICoWw57oI%f82|tP literal 2196 zcmb_edsNa_9Ea7cY-(l8=XAB1bt<(qEfs4EmmU{A2rTey}xi|Fy zY_Ea7xjqO4GVu0-9s+^D2Ef?8R1a9m7jLWvre%>{M`J*sjTtioEMB`E_yc4X>v<#= zPQb**1xFtT#l^+h{SbkVITjpw+>Q{9&3@``4gxJ*>ka+paQt;;uP~j8HEUFlkTB=W zS=G1G?0H=;ojO{)hnx&$mUosur#Kr2O{b$MXu>kpmR6V6%;D~2a` z`B^BrxWbCsP+f?FI*Uq=J;5SpP#JtUh$*7`$YoF~-JQqmea6g){|hUXsXR8)6Z z;`E?&a_D{pwU%C?HtNJEP0D(Ga#}G#8bf({t!yNATYthQZ_aurh@;&Vwv*`eB`LWu z8%LI;m?0@^YDx7CsLBf0BShdW$hn&HjEb11?+MEOGOCXtKmtn|S+-);yNY^!&_I3- zq_Yp@Ub0m@lu0Q~=`&y@+PLEA(_1Hqiia(avsns%raN8Rm7}z?_Qn{>yN$T*9vlWW z0xYN@V#mkCK9)oI&|^3qTh@IwkD>^qDmPoBC9oQX6ibvSH+c-EGSc!6)^ZaNF9>q- z&1oj1eVaMkc0Qix)_!?R*`!kS@}J}AItR+Igv1x6OkSY~r&L2_!m+@M;bQ|BN$M}O zgVHH0BPy=X;!;WuJBW(D^Qd90P3-2k-;zv6+A>3FE#(1%D!jIc`eHLuC2oZas0ru$ zTe6x>n2JA&v=0SUw712c3*wCnoQ>q^R{K6v{uvh6i7YoNk-uu2HtexZbVl~_yWq~WLdFvMWxOu^ zn1!J9*E8*XksbDjCF4cf9hU&@=SMc{^sf_%hcwc5g{{l!O!VE3$-sQsqm)ohGE&1v zSc!^!aW9sl9;H(9LXO@I}U8K#! zL3lMMR6{A!CV9V@5QP!OpNa97*l_wmR$?JTSP-yiy=bC~lqFhNi`!hnehlyGecu_Y zfC8HB-yahHN1mhI88%A77Aw;f*X=WGN8QrFiOse%q|Rm(Tm^=kq5!V(W(HE@cZBYo z+W8PO;-uO^JS8U9t!^k~DFr$}IHvup_P>8Otf72#p7}a2gqsV1o=ghn631VO(Uy%1 zQ*^J34=Iw-ijh2cF*x&ya=gg5k_O-ego;E!+&{7-yoeaSZZPCwA;X#Z)Cbd0>R*FZ zwQ1?Xq570tyLMadjGsd6R(ZeYR1fyHVOnLEx3 zk$+)(o;kcT%E{x|K<4x<#s<)m=Sm_F#I#RqNGy(;O)Wr9<&-TPPXoK`aGT5*Q{+2= P7X`@M69#2}8+7g;FTJ7w diff --git a/docs/assets/images/logo-square.png b/docs/assets/images/logo-square.png index 0592b8fa9a2d43e08f7367e187514beabc685e2f..9bbb39243415b826a71ba65121b20ada5a7b9503 100644 GIT binary patch literal 3323 zcmc&%X;4#H8V#{is_w1(z5AW- zob%m#;|n+E9b5Ib!eFo+E@w`C34>`IP>;WAZc@(>$KYu7XG_c(-*^}dfq6YNt}w}f zdU8*KlTU&NCM*GaG42u!i^ZCL6^)7yxfpZF6cZO-JZ-NBgKfX=a_YF}`+eAUmyrKmLb{CB12HfHM?{V9U{^z0`Gzi9{!uN0hf+E4jogvp<;o;har zjV9&Jt#3=6Dv(xJzd$v3?(?Mh&?eoi>9-4aoIvl|I|$jt8=8GLfDA3NUvJq8MIp^@ zH!sblC}Mi?a_lUtVH6+eVx3oOKZ+Zn4GI@T7{?a*Lu2PKYj1Bu3J8Bzagzypk6!$+*)wf+>a${62jmYG-F0oDm@c0nE8F_Q03lg4EMPry zGFZNupY9vDJPWRVr;k3p%m>N9-2XiM^&$7ont-@wBcW zI;fYzmLXlS)wvS+!fb^usNLFFu&Fg|(0Fav7w2yyi8GCdAxYG}v^O%-=1@v_(_Flf|*18#z8^RqGpQ6lG#p=|hsg*ok1-eVRd=0mxe?VftmFI_r`O$aX zc4xGW3s|BjhyYJUtjOW=S5z-;gQKyc418*DrzjA?QNAp24#RQz298ZD>G_2{s=O3; zn%v*Wk|Im9Ymh%==dc29Y#@U;?h`M_jx?HKu%LWM(6=Oa^f0)JymK^RrnX-s-zQaE zg0w8>&YjWR@X7m~=S_mc?jTtCT+TMtakX({Y&JvJGawG3q+ckD;7ubetf*WqjG^6} zO@=dsD(3bbLF45t!5;7Z?I1XaNKlslFq%Gj5OI(21K69xiqf}>3!=$y-OcbTMI;16 zD|h#RKN;=YdFq2c7*kLOS`%xR-DFC7l|oQS8(1CdyJ76uGAJzZbx=atVENv{NuZqU zi|^`YMb*oS)FL6~uo{?)!*h>}39+FQXZQO60AnPOD=o$CNb2Jur2WUU{Bt3lYmwBW z%Y9XMzl|~sO2$uxw0`!tA#rggb9L)@{?2zT`JKw&bM*ljI;|W({0Ij>^Hhgg*{_Q2 z9r#kktE4C&}-%#mld%()uhT7S~$`lNqG$QE9FI+bQGGxYeh8jNHx8ogVwzaY}s1&RRp z+6wg`@JA3Fg;#kI-5Hz2|4IVJik>j$0R^YZBVz|tN93-R@>wGGn&75`FUU5-@h`gbyv<3 zXIp@lwKqQ|oJ>KzF8EUvAt_lh+wUPYKdkmIup@&p#PGmq*!kQ4l6Ut1kT<}u2HEb6 zjpAyI8>4;vSRELdjD|`*q_7tIkFIa?S};7xTrmVj8m;G1mr(2$RZT}X>n_GY76tji zi!_p?{n92?+d(0F^pJ%m`Tj*+_){dn&eNies%Bi&&cg<{xeqcP+{!yp>p;Ls*%}~d zoyT9I4}`;cSrsdI_1OqMs(g`ynC+0WkJx ooF=m)A%l%qHm_eBt7g+qwHY~_RyAv@A1*K#C%02ICoWw57oI%f82|tP literal 2233 zcmcgue>9VO7=PXM8_DUO${Zw>>J%v?8>;KK?oKtHGj*_)CTBK|HTY6i|Ax(k?SPtS#=>2rO*I~#^P5WV zA)Cu(9Wx4vj4h&aEp&t(a+*7(d-|9$q^n*c;}WT3M&~0Hb%Wi3G2y(meZqdWHTwd5Zm2oM=_UUkaW0Md)lyT?dJ-ZhXs_zU#$`uDrM_s{?tvv zGt+|TxTQn4zEsY<_cqUtI&{blN_3_tO!^0x8EMXn>q9(Z42b87XW+Ex8Sfr7?a9Gj zt90X$+RFEnc;$vBZ$^|fA>maY>~%$3jwclB)rS>4?re`7c_p+)U%WF$40zO_nxp2l z-fdrVpNUwz%P~N4>nCv!*eQy|Nu*R?udxy4EkgE_+H94BGM^z_JH`s7-PaZ;d{9_x zG36Qt=9p)=t@s$W_%Yp9je|BBT1uX5Y1Yj`;fIa!@tp zI4e-uyAwOtNy?MH4}BR1GaiA|qTtkC6i>H)V?Tbi{slMc2c&s=%SFS?2x)s{-H%+e zOi@{;6l`&I4#l~X%vD!^}KE_ANRuooB3nrg5{d-;LMgy|y0{N~=1(8LJ>R&%09O((XAoD5 zy`;7YD&ex^G?LZNElMRkC%djkt!&xhJ#8q9G;(_OxE z@|4iG`$LS~$XuBp(nhJ<9QVSdSjL~&`9dJJOvVOglH{??i#IGa7NYgUCOf3JwIBTL O032)()+L8d{rWfXa;-}M diff --git a/docs/assets/images/logo-square.svg b/docs/assets/images/logo-square.svg index 29b1c20f2..efc68a642 100644 --- a/docs/assets/images/logo-square.svg +++ b/docs/assets/images/logo-square.svg @@ -1,23 +1,22 @@ + + inkscape:zoom="0.27368421" + inkscape:cx="171.73077" + inkscape:cy="45.673077" + inkscape:window-width="1416" + inkscape:window-height="820" + inkscape:window-x="30" + inkscape:window-y="38" + inkscape:window-maximized="0" + inkscape:current-layer="g39" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png deleted file mode 100644 index 005b7c6d2195cf0af295c15fd2e167f3f30b1fad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6386 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 e{|6U^O+Qpov+E8&+y~4KI(+B|tn`;tzy1^S?} -outlines diff --git a/docs/overrides/home.html b/docs/overrides/home.html index de5b2258a..9e7fdaf0b 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -119,7 +119,8 @@
- Outlines Logo + Outlines Logo + Outlines Logo

From 837c814497fbdcf9018d229227b4f0aea6b9c427 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 24 Apr 2025 11:13:05 -0700 Subject: [PATCH 190/505] Add commerical/contact info for .txt --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 419282561..b20fc821c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ Made with ❤👷️ by the team at [.txt](https://dottxt.co). pip install outlines ``` -First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/latest/welcome/) +First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/latest/welcome/)/ + +Need a high-performance commercial solution for structured outputs? Email us at [contact@dottxt.co](mailto:contact@dottxt.co), or [schedule a call](https://cal.com/team/dottxt/sales) with us. ## Features From 2fd02438846d84fcdaf969c88a851627ecdc9c3b Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 30 Apr 2025 10:54:56 -0700 Subject: [PATCH 191/505] Make docs "blog" link to blog.dottxt.co --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b31fc1f54..2e77a0440 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -180,4 +180,4 @@ nav: - How to contribute 🏗️: community/contribute.md - Your projects 👏: community/examples.md - Versioning Guide 📌: community/versioning.md - - Blog: blog/index.md + - Blog: https://blog.dottxt.co From 7fc5f1cf031c1e31bbc69cf89dfc572625e107cf Mon Sep 17 00:00:00 2001 From: Robin Picard <83579270+RobinPicard@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:07:24 +0200 Subject: [PATCH 192/505] Merge v1.0 into main (#1619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rémi Louf Co-authored-by: Rémi Louf Co-authored-by: Victoria Terenina Co-authored-by: Robin Picard Co-authored-by: Robin Picard Co-authored-by: Cameron Pfiffer --- .github/workflows/tests.yml | 29 +- .github/workflows/tests_nightly.yml | 60 ++ Dockerfile | 23 - README.md | 802 +++++++++++------- benchmarks/bench_cfg_guide.py | 2 +- benchmarks/bench_json_schema.py | 2 +- benchmarks/bench_regex_guide.py | 2 +- docs/api/guide.md | 1 - docs/api/json_schema.md | 1 - docs/api/models.md | 1 - docs/api/parsing.md | 1 - docs/api/regex.md | 1 - docs/api/samplers.md | 1 - docs/api/templates.md | 1 - docs/{api => api_reference}/index.md | 0 docs/assets/images/install.png | Bin 0 -> 12669 bytes docs/assets/images/logo-square.png | Bin 3323 -> 13864 bytes docs/assets/images/logo-square.svg | 192 +++-- docs/assets/images/readme-dark.png | Bin 0 -> 38912 bytes docs/assets/images/readme-light.png | Bin 0 -> 38943 bytes docs/assets/images/use_philosophy.png | Bin 0 -> 16463 bytes docs/blog/.authors.yml | 5 - docs/blog/assets/4000_stars.png | Bin 57307 -> 0 bytes docs/blog/posts/roadmap-2024.md | 91 -- docs/community/belonging.png | Bin 142520 -> 0 bytes docs/community/index.md | 3 - docs/community/versioning.md | 4 + docs/cookbook/classification.md | 106 --- docs/cookbook/extract_event_details.py | 46 - docs/core_concepts.md | 13 + docs/{cookbook => examples}/atomic_caption.md | 53 +- .../chain_of_density.md | 51 +- .../chain_of_thought.md | 87 +- docs/examples/classification.md | 85 ++ .../{cookbook => examples}/dating_profiles.md | 30 +- .../deploy-using-bentoml.md | 22 +- .../deploy-using-cerebrium.md | 11 +- .../deploy-using-modal.md | 16 +- .../earnings-reports.md | 24 +- .../extract_event_details.md | 2 +- docs/examples/extract_event_details.py | 54 ++ docs/{cookbook => examples}/extraction.md | 16 +- .../images/chain_of_density.png | Bin .../images/coding_structure_diagram.png | Bin .../images/knowledge-graph-extraction.png | Bin .../images/nvidia-income.png | Bin docs/{cookbook => examples}/images/simtom.png | Bin .../images/trader-joes-receipt.jpg | Bin docs/{cookbook => examples}/index.md | 14 +- .../knowledge_graph_extraction.md | 102 ++- .../models_playing_chess.md | 28 +- .../prompt_templates/chain_of_density.txt | 27 + .../prompt_templates/classification.txt | 18 + .../examples/prompt_templates/react_agent.txt | 27 + .../simtom_prospective_taking.txt | 10 + .../prompt_templates/simtom_simulation.txt | 7 + .../qa-with-citations.md | 125 +-- docs/{cookbook => examples}/react_agent.md | 90 +- docs/{cookbook => examples}/read-pdfs.md | 79 +- .../receipt-digitization.md | 99 +-- docs/{cookbook => examples}/simtom.md | 52 +- .../structured_generation_workflow.md | 62 +- docs/features/advanced/logits_processors.md | 97 +++ docs/features/core/generator.md | 122 +++ docs/features/core/output_types.md | 205 +++++ docs/features/index.md | 19 + docs/features/models/anthropic.md | 98 +++ docs/features/models/dottxt.md | 74 ++ docs/features/models/gemini.md | 148 ++++ docs/features/models/index.md | 107 +++ docs/features/models/llamacpp.md | 168 ++++ docs/features/models/mlxlm.md | 189 +++++ docs/features/models/ollama.md | 103 +++ docs/features/models/openai.md | 134 +++ docs/features/models/openai_compatible.md | 87 ++ docs/features/models/sglang.md | 171 ++++ docs/features/models/tgi.md | 137 +++ docs/features/models/transformers.md | 207 +++++ .../models/transformers_multimodal.md | 89 ++ docs/features/models/vllm.md | 186 ++++ docs/features/models/vllm_offline.md | 176 ++++ docs/features/utility/application.md | 65 ++ .../utility}/regex_dsl.md | 6 +- docs/features/utility/template.md | 85 ++ docs/{reference => guide}/chat_templating.md | 0 docs/guide/core_concepts.md | 13 + docs/guide/getting_started.md | 363 ++++++++ docs/guide/installation.md | 73 ++ docs/guide/migration.md | 246 ++++++ docs/guide/selecting_an_inference_backend.md | 5 + docs/index.md | 162 +++- docs/installation.md | 49 -- docs/logos/cohere.png | Bin 0 -> 113536 bytes docs/logos/vllm.png | Bin 0 -> 89976 bytes docs/quickstart.md | 228 ----- docs/reference/generation/cfg.md | 154 ---- docs/reference/generation/choices.md | 16 - .../reference/generation/creating_grammars.md | 99 --- docs/reference/generation/custom_fsm_ops.md | 37 - docs/reference/generation/format.md | 23 - docs/reference/generation/generation.md | 216 ----- docs/reference/generation/json.md | 178 ---- docs/reference/generation/regex.md | 27 - .../structured_generation_explanation.md | 54 -- docs/reference/generation/types.md | 83 -- docs/reference/index.md | 15 - docs/reference/models/exllamav2.md | 15 - docs/reference/models/llamacpp.md | 230 ----- docs/reference/models/mlxlm.md | 93 -- docs/reference/models/models.md | 56 -- docs/reference/models/openai.md | 204 ----- docs/reference/models/tgi.md | 3 - docs/reference/models/transformers.md | 148 ---- docs/reference/models/transformers_vision.md | 115 --- docs/reference/models/vllm.md | 235 ----- docs/reference/prompting.md | 395 --------- docs/reference/samplers.md | 178 ---- docs/reference/serve/lmstudio.md | 89 -- docs/reference/serve/vllm.md | 73 -- docs/reference/text.md | 86 -- docs/stylesheets/extra.css | 161 +--- docs/welcome.md | 183 ---- examples/babyagi.py | 18 +- examples/beam-cloud/app.py | 11 +- examples/bentoml/service.py | 16 +- examples/cerebrium/main.py | 16 +- examples/cfg.py | 22 +- examples/dating_profile.py | 21 +- examples/llamacpp_example.py | 5 +- examples/llamacpp_processor.py | 4 +- examples/math_generate_code.py | 10 +- examples/meta_prompting.py | 48 +- examples/modal_example.py | 23 +- examples/parsing.py | 1 - examples/pick_odd_one_out.py | 47 +- examples/prompts/babyagi_create_task.txt | 9 + examples/prompts/babyagi_perform_task.txt | 5 + examples/prompts/babyagi_prioritize_task.txt | 7 + examples/prompts/dating_profile.txt | 11 + examples/prompts/pick_odd_one_out.txt | 9 + examples/prompts/self_consistency.txt | 6 + examples/react.py | 56 +- examples/sampling.ipynb | 48 +- examples/self_consistency.py | 23 +- examples/simulation_based_inference.ipynb | 18 +- examples/transformers_integration.py | 25 - ...gration.py => vllm_offline_integration.py} | 7 +- mkdocs.yml | 366 ++++---- outlines/__init__.py | 77 +- outlines/applications.py | 107 +++ outlines/caching.py | 10 +- outlines/fsm/__init__.py | 1 + outlines/fsm/json_schema.py | 83 -- outlines/fsm/parsing.py | 2 + outlines/fsm/types.py | 76 -- outlines/generate/api.py | 609 ------------- outlines/generate/cfg.py | 54 -- outlines/generate/choice.py | 59 -- outlines/generate/format.py | 48 -- outlines/generate/fsm.py | 31 - outlines/generate/generator.py | 312 ------- outlines/generate/json.py | 123 --- outlines/generate/regex.py | 66 -- outlines/generate/text.py | 50 -- outlines/generator.py | 347 ++++++++ outlines/grammars.py | 23 +- outlines/models/__init__.py | 43 +- outlines/models/anthropic.py | 227 +++++ outlines/models/base.py | 377 ++++++++ outlines/models/dottxt.py | 215 +++++ outlines/models/gemini.py | 296 +++++++ outlines/models/llamacpp.py | 393 ++++----- outlines/models/mlxlm.py | 340 +++----- outlines/models/ollama.py | 213 +++++ outlines/models/openai.py | 483 +++++++++-- outlines/models/sglang.py | 372 ++++++++ outlines/models/tgi.py | 340 ++++++++ outlines/models/tokenizer.py | 12 +- outlines/models/transformers.py | 555 ++++++------ outlines/models/transformers_vision.py | 138 --- outlines/models/vllm.py | 483 +++++++---- outlines/models/vllm_offline.py | 206 +++++ outlines/processors/__init__.py | 2 + outlines/processors/base_logits_processor.py | 246 +++--- outlines/{fsm => processors}/guide.py | 236 +++++- outlines/processors/structured.py | 218 +++-- .../processors/tensor_adapters/__init__.py | 26 + outlines/processors/tensor_adapters/base.py | 257 ++++++ outlines/processors/tensor_adapters/jax.py | 50 ++ outlines/processors/tensor_adapters/mlx.py | 60 ++ outlines/processors/tensor_adapters/numpy.py | 50 ++ .../processors/tensor_adapters/tensorflow.py | 50 ++ outlines/processors/tensor_adapters/torch.py | 48 ++ outlines/release_note.md | 392 +++++++++ outlines/samplers.py | 353 -------- outlines/serve/__init__.py | 0 outlines/serve/serve.py | 140 --- outlines/templates.py | 85 +- outlines/types/__init__.py | 27 +- outlines/types/dsl.py | 567 ++++++++++++- outlines/types/locale/__init__.py | 2 + outlines/types/locale/us.py | 2 + outlines/types/utils.py | 206 +++++ outlines/{ => v0_legacy}/base.py | 141 ++- outlines/{ => v0_legacy}/function.py | 75 +- outlines/{ => v0_legacy}/generate/__init__.py | 11 +- outlines/v0_legacy/generate/api.py | 175 ++++ outlines/v0_legacy/generate/cfg.py | 80 ++ outlines/v0_legacy/generate/choice.py | 126 +++ outlines/v0_legacy/generate/format.py | 70 ++ outlines/v0_legacy/generate/fsm.py | 77 ++ outlines/v0_legacy/generate/json.py | 145 ++++ outlines/v0_legacy/generate/regex.py | 67 ++ outlines/v0_legacy/generate/text.py | 64 ++ outlines/v0_legacy/models/__init__.py | 21 + outlines/{ => v0_legacy}/models/exllamav2.py | 141 +-- outlines/v0_legacy/models/llamacpp.py | 147 ++++ outlines/v0_legacy/models/mlxlm.py | 153 ++++ outlines/v0_legacy/models/openai.py | 64 ++ outlines/v0_legacy/models/transformers.py | 196 +++++ .../v0_legacy/models/transformers_vision.py | 107 +++ outlines/v0_legacy/models/vllm_offline.py | 102 +++ outlines/v0_legacy/samplers.py | 118 +++ pyproject.toml | 77 +- requirements-doc.txt | 3 + scripts/gen_ref_pages.py | 53 ++ tests/fsm/test_json_schema.py | 78 -- tests/fsm/test_types.py | 22 - tests/generate/__init__.py | 0 tests/generate/conftest.py | 66 -- tests/generate/test_api.py | 33 - tests/generate/test_generate.py | 439 ---------- tests/generate/test_generator.py | 497 ----------- tests/generate/test_integration_exllamav2.py | 363 -------- tests/generate/test_integration_llamacpp.py | 374 -------- .../generate/test_integration_transformers.py | 569 ------------- .../test_integration_transformers_vision.py | 116 --- tests/generate/test_integration_vllm.py | 243 ------ tests/models/test_anthropic.py | 106 +++ tests/models/test_dottxt.py | 131 +++ tests/models/test_dottxt_type_adapter.py | 137 +++ tests/models/test_gemini.py | 221 +++++ tests/models/test_gemini_type_adapter.py | 204 +++++ tests/models/test_llamacpp.py | 134 +++ tests/models/test_llamacpp_tokenizer.py | 133 +++ tests/models/test_llamacpp_type_adapter.py | 41 + tests/models/test_mlxlm.py | 133 +-- tests/models/test_ollama.py | 101 +++ tests/models/test_ollama_type_adapter.py | 131 +++ tests/models/test_openai.py | 292 ++++--- tests/models/test_openai_type_adapter.py | 195 +++++ tests/models/test_sglang.py | 272 ++++++ tests/models/test_sglang_type_adapter.py | 145 ++++ tests/models/test_tgi.py | 180 ++++ tests/models/test_tgi_model_adapter.py | 93 ++ tests/models/test_transformers.py | 283 +++--- tests/models/test_transformers_multimodal.py | 194 +++++ ...st_transformers_multimodal_type_adapter.py | 48 ++ tests/models/test_transformers_tokenizer.py | 142 ++++ .../models/test_transformers_type_adapter.py | 50 ++ tests/models/test_vllm.py | 254 ++++++ tests/models/test_vllm_offline.py | 117 +++ .../models/test_vllm_offline_type_adapter.py | 84 ++ tests/models/test_vllm_type_adapter.py | 121 +++ tests/processors/test_base_processor.py | 148 ++-- tests/{fsm => processors}/test_cfg_guide.py | 15 +- tests/{fsm => processors}/test_guide.py | 82 +- tests/processors/test_structured.py | 135 +++ tests/processors/test_tensor_adapters.py | 276 ++++++ tests/test_applications.py | 93 ++ tests/test_cache.py | 31 + tests/test_generator.py | 255 ++++++ tests/test_grammars.py | 2 +- tests/test_samplers.py | 277 ------ tests/test_templates.py | 122 ++- tests/test_utils/mock_openai_client.py | 117 +++ tests/test_utils/mock_tgi_client.py | 63 ++ tests/test_utils/utils.py | 23 + tests/types/test_custom_types.py | 1 - tests/types/test_dsl.py | 508 ++++++++++- tests/types/test_types_utils.py | 409 +++++++++ tests/v0_legacy/generate/test_api_legacy.py | 186 ++++ tests/v0_legacy/generate/test_cfg_legacy.py | 111 +++ .../v0_legacy/generate/test_choice_legacy.py | 97 +++ .../v0_legacy/generate/test_format_legacy.py | 93 ++ tests/v0_legacy/generate/test_fsm_legacy.py | 104 +++ tests/v0_legacy/generate/test_json_legacy.py | 200 +++++ tests/v0_legacy/generate/test_regex_legacy.py | 90 ++ tests/v0_legacy/generate/test_text_legacy.py | 83 ++ .../v0_legacy/models/test_exllamav2_legacy.py | 76 ++ .../v0_legacy/models/test_llamacpp_legacy.py | 169 ++++ tests/v0_legacy/models/test_mlxlm_legacy.py | 193 +++++ tests/v0_legacy/models/test_openai_legacy.py | 159 ++++ .../models/test_transformers_legacy.py | 172 ++++ .../models/test_transformers_vision_legacy.py | 110 +++ .../models/test_vllm_offline_legacy.py | 181 ++++ .../test_base_legacy.py} | 2 +- .../test_function_legacy.py} | 85 +- tests/v0_legacy/test_samplers_legacy.py | 60 ++ uv.lock | 28 - 300 files changed, 21850 insertions(+), 12037 deletions(-) create mode 100644 .github/workflows/tests_nightly.yml delete mode 100644 Dockerfile delete mode 100644 docs/api/guide.md delete mode 100644 docs/api/json_schema.md delete mode 100644 docs/api/models.md delete mode 100644 docs/api/parsing.md delete mode 100644 docs/api/regex.md delete mode 100644 docs/api/samplers.md delete mode 100644 docs/api/templates.md rename docs/{api => api_reference}/index.md (100%) create mode 100644 docs/assets/images/install.png create mode 100644 docs/assets/images/readme-dark.png create mode 100644 docs/assets/images/readme-light.png create mode 100644 docs/assets/images/use_philosophy.png delete mode 100644 docs/blog/.authors.yml delete mode 100644 docs/blog/assets/4000_stars.png delete mode 100644 docs/blog/posts/roadmap-2024.md delete mode 100644 docs/community/belonging.png delete mode 100644 docs/cookbook/classification.md delete mode 100644 docs/cookbook/extract_event_details.py create mode 100644 docs/core_concepts.md rename docs/{cookbook => examples}/atomic_caption.md (75%) rename docs/{cookbook => examples}/chain_of_density.md (74%) rename docs/{cookbook => examples}/chain_of_thought.md (63%) create mode 100644 docs/examples/classification.md rename docs/{cookbook => examples}/dating_profiles.md (92%) rename docs/{cookbook => examples}/deploy-using-bentoml.md (91%) rename docs/{cookbook => examples}/deploy-using-cerebrium.md (90%) rename docs/{cookbook => examples}/deploy-using-modal.md (93%) rename docs/{cookbook => examples}/earnings-reports.md (94%) rename docs/{cookbook => examples}/extract_event_details.md (94%) create mode 100644 docs/examples/extract_event_details.py rename docs/{cookbook => examples}/extraction.md (85%) rename docs/{cookbook => examples}/images/chain_of_density.png (100%) rename docs/{cookbook => examples}/images/coding_structure_diagram.png (100%) rename docs/{cookbook => examples}/images/knowledge-graph-extraction.png (100%) rename docs/{cookbook => examples}/images/nvidia-income.png (100%) rename docs/{cookbook => examples}/images/simtom.png (100%) rename docs/{cookbook => examples}/images/trader-joes-receipt.jpg (100%) rename docs/{cookbook => examples}/index.md (82%) rename docs/{cookbook => examples}/knowledge_graph_extraction.md (60%) rename docs/{cookbook => examples}/models_playing_chess.md (77%) create mode 100644 docs/examples/prompt_templates/chain_of_density.txt create mode 100644 docs/examples/prompt_templates/classification.txt create mode 100644 docs/examples/prompt_templates/react_agent.txt create mode 100644 docs/examples/prompt_templates/simtom_prospective_taking.txt create mode 100644 docs/examples/prompt_templates/simtom_simulation.txt rename docs/{cookbook => examples}/qa-with-citations.md (68%) rename docs/{cookbook => examples}/react_agent.md (71%) rename docs/{cookbook => examples}/read-pdfs.md (79%) rename docs/{cookbook => examples}/receipt-digitization.md (71%) rename docs/{cookbook => examples}/simtom.md (68%) rename docs/{cookbook => examples}/structured_generation_workflow.md (79%) create mode 100644 docs/features/advanced/logits_processors.md create mode 100644 docs/features/core/generator.md create mode 100644 docs/features/core/output_types.md create mode 100644 docs/features/index.md create mode 100644 docs/features/models/anthropic.md create mode 100644 docs/features/models/dottxt.md create mode 100644 docs/features/models/gemini.md create mode 100644 docs/features/models/index.md create mode 100644 docs/features/models/llamacpp.md create mode 100644 docs/features/models/mlxlm.md create mode 100644 docs/features/models/ollama.md create mode 100644 docs/features/models/openai.md create mode 100644 docs/features/models/openai_compatible.md create mode 100644 docs/features/models/sglang.md create mode 100644 docs/features/models/tgi.md create mode 100644 docs/features/models/transformers.md create mode 100644 docs/features/models/transformers_multimodal.md create mode 100644 docs/features/models/vllm.md create mode 100644 docs/features/models/vllm_offline.md create mode 100644 docs/features/utility/application.md rename docs/{reference => features/utility}/regex_dsl.md (99%) create mode 100644 docs/features/utility/template.md rename docs/{reference => guide}/chat_templating.md (100%) create mode 100644 docs/guide/core_concepts.md create mode 100644 docs/guide/getting_started.md create mode 100644 docs/guide/installation.md create mode 100644 docs/guide/migration.md create mode 100644 docs/guide/selecting_an_inference_backend.md delete mode 100644 docs/installation.md create mode 100644 docs/logos/cohere.png create mode 100644 docs/logos/vllm.png delete mode 100644 docs/quickstart.md delete mode 100644 docs/reference/generation/cfg.md delete mode 100644 docs/reference/generation/choices.md delete mode 100644 docs/reference/generation/creating_grammars.md delete mode 100644 docs/reference/generation/custom_fsm_ops.md delete mode 100644 docs/reference/generation/format.md delete mode 100644 docs/reference/generation/generation.md delete mode 100644 docs/reference/generation/json.md delete mode 100644 docs/reference/generation/regex.md delete mode 100644 docs/reference/generation/structured_generation_explanation.md delete mode 100644 docs/reference/generation/types.md delete mode 100644 docs/reference/index.md delete mode 100644 docs/reference/models/exllamav2.md delete mode 100644 docs/reference/models/llamacpp.md delete mode 100644 docs/reference/models/mlxlm.md delete mode 100644 docs/reference/models/models.md delete mode 100644 docs/reference/models/openai.md delete mode 100644 docs/reference/models/tgi.md delete mode 100644 docs/reference/models/transformers.md delete mode 100644 docs/reference/models/transformers_vision.md delete mode 100644 docs/reference/models/vllm.md delete mode 100644 docs/reference/prompting.md delete mode 100644 docs/reference/samplers.md delete mode 100644 docs/reference/serve/lmstudio.md delete mode 100644 docs/reference/serve/vllm.md delete mode 100644 docs/reference/text.md delete mode 100644 docs/welcome.md create mode 100644 examples/prompts/babyagi_create_task.txt create mode 100644 examples/prompts/babyagi_perform_task.txt create mode 100644 examples/prompts/babyagi_prioritize_task.txt create mode 100644 examples/prompts/dating_profile.txt create mode 100644 examples/prompts/pick_odd_one_out.txt create mode 100644 examples/prompts/self_consistency.txt delete mode 100644 examples/transformers_integration.py rename examples/{vllm_integration.py => vllm_offline_integration.py} (80%) create mode 100644 outlines/applications.py delete mode 100644 outlines/fsm/json_schema.py delete mode 100644 outlines/fsm/types.py delete mode 100644 outlines/generate/api.py delete mode 100644 outlines/generate/cfg.py delete mode 100644 outlines/generate/choice.py delete mode 100644 outlines/generate/format.py delete mode 100644 outlines/generate/fsm.py delete mode 100644 outlines/generate/generator.py delete mode 100644 outlines/generate/json.py delete mode 100644 outlines/generate/regex.py delete mode 100644 outlines/generate/text.py create mode 100644 outlines/generator.py create mode 100644 outlines/models/anthropic.py create mode 100644 outlines/models/base.py create mode 100644 outlines/models/dottxt.py create mode 100644 outlines/models/gemini.py create mode 100644 outlines/models/ollama.py create mode 100644 outlines/models/sglang.py create mode 100644 outlines/models/tgi.py delete mode 100644 outlines/models/transformers_vision.py create mode 100644 outlines/models/vllm_offline.py rename outlines/{fsm => processors}/guide.py (58%) create mode 100644 outlines/processors/tensor_adapters/__init__.py create mode 100644 outlines/processors/tensor_adapters/base.py create mode 100644 outlines/processors/tensor_adapters/jax.py create mode 100644 outlines/processors/tensor_adapters/mlx.py create mode 100644 outlines/processors/tensor_adapters/numpy.py create mode 100644 outlines/processors/tensor_adapters/tensorflow.py create mode 100644 outlines/processors/tensor_adapters/torch.py create mode 100644 outlines/release_note.md delete mode 100644 outlines/samplers.py delete mode 100644 outlines/serve/__init__.py delete mode 100644 outlines/serve/serve.py create mode 100644 outlines/types/utils.py rename outlines/{ => v0_legacy}/base.py (67%) rename outlines/{ => v0_legacy}/function.py (58%) rename outlines/{ => v0_legacy}/generate/__init__.py (62%) create mode 100644 outlines/v0_legacy/generate/api.py create mode 100644 outlines/v0_legacy/generate/cfg.py create mode 100644 outlines/v0_legacy/generate/choice.py create mode 100644 outlines/v0_legacy/generate/format.py create mode 100644 outlines/v0_legacy/generate/fsm.py create mode 100644 outlines/v0_legacy/generate/json.py create mode 100644 outlines/v0_legacy/generate/regex.py create mode 100644 outlines/v0_legacy/generate/text.py create mode 100644 outlines/v0_legacy/models/__init__.py rename outlines/{ => v0_legacy}/models/exllamav2.py (79%) create mode 100644 outlines/v0_legacy/models/llamacpp.py create mode 100644 outlines/v0_legacy/models/mlxlm.py create mode 100644 outlines/v0_legacy/models/openai.py create mode 100644 outlines/v0_legacy/models/transformers.py create mode 100644 outlines/v0_legacy/models/transformers_vision.py create mode 100644 outlines/v0_legacy/models/vllm_offline.py create mode 100644 outlines/v0_legacy/samplers.py create mode 100644 scripts/gen_ref_pages.py delete mode 100644 tests/fsm/test_json_schema.py delete mode 100644 tests/fsm/test_types.py delete mode 100644 tests/generate/__init__.py delete mode 100644 tests/generate/conftest.py delete mode 100644 tests/generate/test_api.py delete mode 100644 tests/generate/test_generate.py delete mode 100644 tests/generate/test_generator.py delete mode 100644 tests/generate/test_integration_exllamav2.py delete mode 100644 tests/generate/test_integration_llamacpp.py delete mode 100644 tests/generate/test_integration_transformers.py delete mode 100644 tests/generate/test_integration_transformers_vision.py delete mode 100644 tests/generate/test_integration_vllm.py create mode 100644 tests/models/test_anthropic.py create mode 100644 tests/models/test_dottxt.py create mode 100644 tests/models/test_dottxt_type_adapter.py create mode 100644 tests/models/test_gemini.py create mode 100644 tests/models/test_gemini_type_adapter.py create mode 100644 tests/models/test_llamacpp.py create mode 100644 tests/models/test_llamacpp_tokenizer.py create mode 100644 tests/models/test_llamacpp_type_adapter.py create mode 100644 tests/models/test_ollama.py create mode 100644 tests/models/test_ollama_type_adapter.py create mode 100644 tests/models/test_openai_type_adapter.py create mode 100644 tests/models/test_sglang.py create mode 100644 tests/models/test_sglang_type_adapter.py create mode 100644 tests/models/test_tgi.py create mode 100644 tests/models/test_tgi_model_adapter.py create mode 100644 tests/models/test_transformers_multimodal.py create mode 100644 tests/models/test_transformers_multimodal_type_adapter.py create mode 100644 tests/models/test_transformers_tokenizer.py create mode 100644 tests/models/test_transformers_type_adapter.py create mode 100644 tests/models/test_vllm.py create mode 100644 tests/models/test_vllm_offline.py create mode 100644 tests/models/test_vllm_offline_type_adapter.py create mode 100644 tests/models/test_vllm_type_adapter.py rename tests/{fsm => processors}/test_cfg_guide.py (97%) rename tests/{fsm => processors}/test_guide.py (85%) create mode 100644 tests/processors/test_structured.py create mode 100644 tests/processors/test_tensor_adapters.py create mode 100644 tests/test_applications.py create mode 100644 tests/test_generator.py delete mode 100644 tests/test_samplers.py create mode 100644 tests/test_utils/mock_openai_client.py create mode 100644 tests/test_utils/mock_tgi_client.py create mode 100644 tests/test_utils/utils.py create mode 100644 tests/types/test_types_utils.py create mode 100644 tests/v0_legacy/generate/test_api_legacy.py create mode 100644 tests/v0_legacy/generate/test_cfg_legacy.py create mode 100644 tests/v0_legacy/generate/test_choice_legacy.py create mode 100644 tests/v0_legacy/generate/test_format_legacy.py create mode 100644 tests/v0_legacy/generate/test_fsm_legacy.py create mode 100644 tests/v0_legacy/generate/test_json_legacy.py create mode 100644 tests/v0_legacy/generate/test_regex_legacy.py create mode 100644 tests/v0_legacy/generate/test_text_legacy.py create mode 100644 tests/v0_legacy/models/test_exllamav2_legacy.py create mode 100644 tests/v0_legacy/models/test_llamacpp_legacy.py create mode 100644 tests/v0_legacy/models/test_mlxlm_legacy.py create mode 100644 tests/v0_legacy/models/test_openai_legacy.py create mode 100644 tests/v0_legacy/models/test_transformers_legacy.py create mode 100644 tests/v0_legacy/models/test_transformers_vision_legacy.py create mode 100644 tests/v0_legacy/models/test_vllm_offline_legacy.py rename tests/{test_base.py => v0_legacy/test_base_legacy.py} (99%) rename tests/{test_function.py => v0_legacy/test_function_legacy.py} (59%) create mode 100644 tests/v0_legacy/test_samplers_legacy.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 46d3d5ff6..c6e37d684 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Tests on: pull_request: - branches: [main] + branches: [main,v1.0] push: branches: [main] @@ -29,6 +29,11 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Install Ollama + run: | + curl -fsSL https://ollama.com/install.sh | sh + ollama --version + ollama pull tinyllama - name: Set up test environment run: | python -m pip install --upgrade pip @@ -46,7 +51,7 @@ jobs: echo "::set-output name=id::$MATRIX_ID" - name: Run tests run: | - uv run pytest -x --cov=outlines + uv run pytest -x --cov=outlines -m 'not api_call' env: COVERAGE_FILE: .coverage.${{ steps.matrix-id.outputs.id }} - name: Upload coverage data @@ -82,16 +87,30 @@ jobs: with: name: coverage-data - - name: Fetch main for coverage diff + - name: Determine base branch for comparison + id: base-branch + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + # For PRs, use the remote tracking branch + COMPARE_BRANCH="origin/${{ github.base_ref }}" + echo "COMPARE_BRANCH=$COMPARE_BRANCH" >> $GITHUB_ENV + else + # For push events, compare against the parent commit + COMPARE_BRANCH="${{ github.event.before }}" + echo "COMPARE_BRANCH=$COMPARE_BRANCH" >> $GITHUB_ENV + fi + echo "Using $COMPARE_BRANCH for coverage comparison" + + - name: Fetch base branch for coverage diff run: | - git fetch --no-tags --prune origin main + git fetch --no-tags --prune origin ${COMPARE_BRANCH#origin/} - name: Combine coverage & fail if it's <100%. run: | python -m coverage combine python -m coverage html --skip-covered --skip-empty python -m coverage xml - diff-cover coverage.xml --markdown-report=coverage.md --fail-under=100 || (cat coverage.md >> $GITHUB_STEP_SUMMARY && exit 1) + diff-cover coverage.xml --compare-branch=$COMPARE_BRANCH --markdown-report=coverage.md --fail-under=100 || (cat coverage.md >> $GITHUB_STEP_SUMMARY && exit 1) - name: Upload HTML report if check failed. uses: actions/upload-artifact@v4 diff --git a/.github/workflows/tests_nightly.yml b/.github/workflows/tests_nightly.yml new file mode 100644 index 000000000..c34a707fa --- /dev/null +++ b/.github/workflows/tests_nightly.yml @@ -0,0 +1,60 @@ +name: Nightly Tests + +# Run every day at 6:00 AM UTC +on: + schedule: + - cron: "0 6 * * *" + +jobs: + tests: + name: Run the tests + runs-on: ubuntu-latest + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + DOTTXT_API_KEY: ${{ secrets.DOTTXT_API_KEY }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Ollama + run: | + curl -fsSL https://ollama.com/install.sh | sh + ollama --version + ollama pull tinyllama + - name: Set up test environment + run: | + python -m pip install --upgrade pip + pip install uv + uv venv + uv pip install -e .[test] + - name: Create matrix id + id: matrix-id + env: + MATRIX_CONTEXT: ${{ toJson(matrix) }} + run: | + echo $MATRIX_CONTEXT + export MATRIX_ID=`echo $MATRIX_CONTEXT | md5sum | cut -c 1-32` + echo $MATRIX_ID + echo "::set-output name=id::$MATRIX_ID" + - name: Run tests + run: | + uv run pytest -x --cov=outlines + env: + COVERAGE_FILE: .coverage.${{ steps.matrix-id.outputs.id }} + - name: Slack notification on failure + if: failure() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: commit,author,ref + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TESTS_ERRORS_WEBHOOK_URL }} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7e6f2e68a..000000000 --- a/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -### Build stage -FROM python:3.10 AS builder - -WORKDIR /outlines - -RUN pip install --upgrade pip - -# Copy necessary build components -COPY pyproject.toml . -COPY outlines ./outlines - -# Install outlines and outlines[serve] -# .git required by setuptools-scm -RUN --mount=source=.git,target=.git,type=bind \ - pip install --no-cache-dir .[serve] - -### Runtime stage -FROM python:3.10 -WORKDIR /outlines -COPY --from=builder /outlines /outlines - -# https://dottxt-ai.github.io/outlines/reference/vllm/ -ENTRYPOINT ["python3", "-m", "outlines.serve.serve"] diff --git a/README.md b/README.md index b20fc821c..0d85aebf1 100644 --- a/README.md +++ b/README.md @@ -1,449 +1,669 @@
-Outlines Logo -Outlines Logo +Outlines Logo +Outlines Logo - 🗒️ *Make LLMs speak the language of every application.* 🗒️ + 🗒️ *Structured outputs for LLMs* 🗒️ -Made with ❤👷️ by the team at [.txt](https://dottxt.co). +Made with ❤👷️ by the team at [.txt](https://dottxt.co) +
Trusted by NVIDIA, Cohere, HuggingFace, vLLM, etc. -[![Documentation][documentation-badge]][documentation] -[![Contributors][contributors-badge]][contributors] + +[![PyPI Version][pypi-version-badge]][pypi] [![Downloads][downloads-badge]][pypistats] -[![Discord][discord-badge]][discord] - -[Youtube channel][youtube-dottxt] | [.txt blog][blog-dottxt] | [Twitter][dottxt-twitter] +[![Stars][stars-badge]][stars] + +[![Discord][discord-badge]][discord] +[![Blog][dottxt-blog-badge]][dottxt-blog] +[![Twitter][twitter-badge]][twitter]
+Need a high-performance commercial solution for structured outputs? Email us at [contact@dottxt.co](mailto:contact@dottxt.co), or [schedule a call](https://cal.com/team/dottxt/sales). -``` bash -pip install outlines -``` +## Table of Contents -First time here? Go to our [setup guide](https://dottxt-ai.github.io/outlines/latest/welcome/)/ +- [Why Outlines?](#why-outlines) +- [Quickstart](#quickstart) +- [Real-World Examples](#real-world-examples) + - [🙋‍♂️ Customer Support Triage](#customer-support-triage) + - [📦 E-commerce Product Categorization](#e-commerce-product-categorization) + - [💊 Pull Medication Data from Unstructured Notes](#pull-medication-data-from-unstructured-notes) + - [📊 Parse Event Details with Incomplete Data](#parse-event-details-with-incomplete-data) + - [🗂️ Categorize Documents into Predefined Types](#categorize-documents-into-predefined-types) + - [📅 Schedule a Meeting with Function Calling](#schedule-a-meeting-with-function-calling) + - [📝 Dynamically Generate Prompts with Re-usable Templates](#dynamically-generate-prompts-with-re-usable-templates) +- [They Use Outlines](#they-use-outlines) +- [Model Integrations](#model-integrations) +- [Core Features](#core-features) +- [Other Features](#other-features) +- [About .txt](#about-txt) +- [Community](#community) -Need a high-performance commercial solution for structured outputs? Email us at [contact@dottxt.co](mailto:contact@dottxt.co), or [schedule a call](https://cal.com/team/dottxt/sales) with us. +
-## Features +## Why Outlines? -- 🤖 [Multiple model integrations](https://dottxt-ai.github.io/outlines/latest/installation): OpenAI, transformers, llama.cpp, exllama2, mamba -- 🔥 Fast [JSON generation](#efficient-json-generation-following-a-pydantic-model) following a JSON schema or a Pydantic model -- 🚄 [Multiple choices](#multiple-choices), [type constraints](#type-constraint) and dynamic stopping -- 📝 Generate text that follows a [regex](#efficient-regex-structured-generation) or a [context-free grammar](#using-context-free-grammars-to-guide-generation) -- 🖍️ Simple and powerful prompting primitives based on the [Jinja templating engine](https://jinja.palletsprojects.com/) -- 🚀 [Serve with vLLM](https://dottxt-ai.github.io/outlines/latest/reference/serve/vllm), with official Docker image, [`outlinesdev/outlines`](https://hub.docker.com/r/outlinesdev/outlines)! +LLMs are powerful but their outputs are unpredictable. Most solutions attempt to fix bad outputs after generation using parsing, regex, or fragile code that breaks easily. +Outlines guarantees structured outputs during generation — directly from any LLM. -Outlines has new releases and features coming every week. Make sure to ⭐ star and 👀 watch this repository, follow [@dottxtai][dottxt-twitter] to stay up to date! +- **Works with any model** - Same code runs across OpenAI, Ollama, vLLM, and more +- **Simple integration** - Just pass your desired output type: `model(prompt, output_type)` +- **Guaranteed valid structure** - No more parsing headaches or broken JSON +- **Provider independence** - Switch models without changing code -## Why should I use structured generation? -* It doesn't add any overhead during inference (cost-free) -* It allows Open Source models to beat closed source models ([Mistral](https://x.com/dottxtai/status/1797692104023363765), [GPT-4](https://x.com/dottxtai/status/1798443290913853770)) -* [It speeds up inference](http://blog.dottxt.co/coalescence.html) -* [It improves the performance of base models (GSM8K)](http://blog.dottxt.co/performance-gsm8k.html) -* [It improves the performance of finetuned models (CoNNL)](https://predibase.com/blog/lorax-outlines-better-json-extraction-with-structured-generation-and-lora) -* [It improves model efficiency (less examples needed)](https://huggingface.co/blog/evaluation-structured-outputs) +### The Outlines Philosophy -## .txt company +
-
-dottxt logo -dottxt logo -
- -We started a company to keep pushing the boundaries of structured generation. Learn more about [.txt](https://dottxt.ai), and [license our commercial product](https://h1xbpbfsf0w.typeform.com/to/ZgBCvJHF) if you need SOTA performance and quality ✨ - -## Structured generation - -The first step towards reliability of systems that include large language models -is to ensure that there is a well-defined interface between their output and -user-defined code. **Outlines** provides ways to control the generation of -language models to make their output more predictable. +Outlines follows a simple pattern that mirrors Python's own type system. Simply specify the desired output type, and Outlines will ensure your data matches that structure exactly: -The following methods of structured generation are supported: +- For a yes/no response, use `Literal["Yes", "No"]` +- For numerical values, use `int` +- For complex objects, define a structure with a [Pydantic model](https://docs.pydantic.dev/latest/) -- [Multiple choices](#multiple-choices) -- [Type constraints](#type-constraint) -- [Efficient regex-structured generation](#efficient-regex-structured-generation) -- [Efficient JSON generation following a Pydantic model](#efficient-json-generation-following-a-pydantic-model) -- [Using context-free grammars to guide generation](#using-context-free-grammars-to-guide-generation) -- [Open functions](#open-functions) +## Quickstart -### Chat template tokens +Getting started with outlines is simple: -Outlines does not manage chat templating tokens when using instruct models. You must apply the chat template tokens to the prompt yourself. Chat template tokens are not needed for base models. +### 1. Install outlines -Please see [the documentation](https://dottxt-ai.github.io/outlines/latest/reference/chat_templating) on chat templating for more. - -### Multiple choices +``` shell +pip install outlines +``` -You can reduce the completion to a choice between multiple possibilities: +### 2. Connect to your preferred model ``` python import outlines +from transformers import AutoTokenizer, AutoModelForCausalLM -model_name = "HuggingFaceTB/SmolLM2-360M-Instruct" -model = outlines.models.transformers(model_name) -# You must apply the chat template tokens to the prompt! -# See below for an example. -prompt = """ -<|im_start|>system -You extract information from text. -<|im_end|> +MODEL_NAME = "microsoft/Phi-3-mini-4k-instruct" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) +) +``` -<|im_start|>user -What food does the following text describe? +### 3. Start with simple structured outputs + +``` python +from typing import Literal +from pydantic import BaseModel -Text: I really really really want pizza. -<|im_end|> -<|im_start|>assistant -""" -generator = outlines.generate.choice(model, ["Pizza", "Pasta", "Salad", "Dessert"]) -answer = generator(prompt) +# Simple classification +sentiment = model( + "Analyze: 'This product completely changed my life!'", + Literal["Positive", "Negative", "Neutral"] +) +print(sentiment) # "Positive" -# Likely answer: Pizza +# Extract specific types +temperature = model("What's the boiling point of water in Celsius?", int) +print(temperature) # 100 ``` -You can also pass in choices with an `Enum`: +### 4. Create complex structures -````python +``` python +from pydantic import BaseModel from enum import Enum -class Food(str, Enum): - pizza = "Pizza" - pasta = "Pasta" - salad = "Salad" - dessert = "Dessert" +class Rating(Enum): + poor = 1 + fair = 2 + good = 3 + excellent = 4 + +class ProductReview(BaseModel): + rating: Rating + pros: list[str] + cons: list[str] + summary: str + +review = model( + "Review: The XPS 13 has great battery life and a stunning display, but it runs hot and the webcam is poor quality.", + ProductReview, + max_new_tokens=200, +) -generator = outlines.generate.choice(model, Food) -answer = generator(prompt) -# Likely answer: Pizza -```` +review = ProductReview.model_validate_json(review) +print(f"Rating: {review.rating.name}") # "Rating: good" +print(f"Pros: {review.pros}") # "Pros: ['great battery life', 'stunning display']" +print(f"Summary: {review.summary}") # "Summary: Good laptop with great display but thermal issues"t(result) +``` -### Type constraints +## Real-world examples -You can instruct the model to only return integers or floats: +Here are production-ready examples showing how Outlines solves common problems: +
🙋‍♂️ Customer Support Triage +
This example shows how to convert a free-form customer email into a structured service ticket. By parsing attributes like priority, category, and escalation flags, the code enables automated routing and handling of support issues. +
``` python import outlines +from enum import Enum +from pydantic import BaseModel +from transformers import AutoTokenizer, AutoModelForCausalLM +from typing import List -model = outlines.models.transformers("WizardLM/WizardMath-7B-V1.1") -prompt = "result of 9 + 9 = 18result of 1 + 2 = " -answer = outlines.generate.format(model, int)(prompt) -print(answer) -# 3 +MODEL_NAME = "microsoft/Phi-3-mini-4k-instruct" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) +) -prompt = "sqrt(2)=" -generator = outlines.generate.format(model, float) -answer = generator(prompt, max_tokens=10) -print(answer) -# 1.41421356 -``` -### Efficient regex-structured generation +def alert_manager(ticket): + print("Alert!", ticket) -Outlines also comes with fast regex-structured generation. In fact, the `choice` and -`format` functions above all use regex-structured generation under the -hood: -``` python -import outlines +class TicketPriority(str, Enum): + low = "low" + medium = "medium" + high = "high" + urgent = "urgent" -model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") +class ServiceTicket(BaseModel): + priority: TicketPriority + category: str + requires_manager: bool + summary: str + action_items: List[str] -prompt = """ -<|im_start|>system You are a helpful assistant. -<|im_end|> +customer_email = """ +Subject: URGENT - Cannot access my account after payment + +I paid for the premium plan 3 hours ago and still can't access any features. +I've tried logging out and back in multiple times. This is unacceptable as I +have a client presentation in an hour and need the analytics dashboard. +Please fix this immediately or refund my payment. +""" + +prompt = f""" <|im_start|>user -What is an IP address of the Google DNS servers? +Analyze this customer email: + +{customer_email} <|im_end|> <|im_start|>assistant -The IP address of a Google DNS server is - """ -generator = outlines.generate.text(model) -unstructured = generator(prompt, max_tokens=30) +ticket = model( + prompt, + ServiceTicket, + max_new_tokens=500 +) + +# Use structured data to route the ticket +ticket = ServiceTicket.model_validate_json(ticket) +if ticket.priority == "urgent" or ticket.requires_manager: + alert_manager(ticket) +``` +
+ +
📦 E-commerce product categorization +
This use case demonstrates how outlines can transform product descriptions into structured categorization data (e.g., main category, sub-category, and attributes) to streamline tasks such as inventory management. Each product description is processed automatically, reducing manual categorization overhead. +
+ +```python +import outlines +from pydantic import BaseModel +from transformers import AutoTokenizer, AutoModelForCausalLM +from typing import List, Optional -generator = outlines.generate.regex( - model, - r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)", - sampler=outlines.samplers.greedy(), + +MODEL_NAME = "microsoft/Phi-3-mini-4k-instruct" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) ) -structured = generator(prompt, max_tokens=30) -print(unstructured) -# 8.8.8.8 -# -# <|im_end|> -print(structured) -# 8.8.8.8 -``` +def update_inventory(product, category, sub_category): + print(f"Updated {product.split(',')[0]} in category {category}/{sub_category}") -Unlike other libraries, regex-structured generation in Outlines is almost as fast -as non-structured generation. -### Efficient JSON generation following a Pydantic model +class ProductCategory(BaseModel): + main_category: str + sub_category: str + attributes: List[str] + brand_match: Optional[str] -Outlines users can guide the generation process so the output is *guaranteed* to follow a [JSON schema](https://json-schema.org/) or [Pydantic model](https://docs.pydantic.dev/latest/): +# Process product descriptions in batches +product_descriptions = [ + "Apple iPhone 15 Pro Max 256GB Titanium, 6.7-inch Super Retina XDR display with ProMotion", + "Organic Cotton T-Shirt, Men's Medium, Navy Blue, 100% Sustainable Materials", + "KitchenAid Stand Mixer, 5 Quart, Red, 10-Speed Settings with Dough Hook Attachment" +] + +template = outlines.Template.from_string(""" +<|im_start|>user +Categorize this product: + +{{ description }} +<|im_end|> +<|im_start|>assistant +""") + +# Get structured categorization for all products +categories = model( + [template(description=desc) for desc in product_descriptions], + ProductCategory, + max_new_tokens=200 +) + +# Use categorization for inventory management +categories = [ + ProductCategory.model_validate_json(category) for category in categories +] +for product, category in zip(product_descriptions, categories): + update_inventory(product, category.main_category, category.sub_category) +``` +
+ +
📊 Parse event details with incomplete data +
This example uses outlines to parse event descriptions into structured information (like event name, date, location, type, and topics), even handling cases where the data is incomplete. It leverages union types to return either structured event data or a fallback “I don’t know” answer, ensuring robust extraction in varying scenarios. +
```python +import outlines +from typing import Union, List, Literal +from pydantic import BaseModel from enum import Enum -from pydantic import BaseModel, constr +from transformers import AutoTokenizer, AutoModelForCausalLM -import outlines -class Weapon(str, Enum): - sword = "sword" - axe = "axe" - mace = "mace" - spear = "spear" - bow = "bow" - crossbow = "crossbow" +MODEL_NAME = "microsoft/Phi-3-mini-4k-instruct" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) +) +class EventType(str, Enum): + conference = "conference" + webinar = "webinar" + workshop = "workshop" + meetup = "meetup" + other = "other" + + +class EventInfo(BaseModel): + """Structured information about a tech event""" + name: str + date: str + location: str + event_type: EventType + topics: List[str] + registration_required: bool + +# Create a union type that can either be a structured EventInfo or "I don't know" +EventResponse = Union[EventInfo, Literal["I don't know"]] + +# Sample event descriptions +event_descriptions = [ + # Complete information + """ + Join us for DevCon 2023, the premier developer conference happening on November 15-17, 2023 + at the San Francisco Convention Center. Topics include AI/ML, cloud infrastructure, and web3. + Registration is required. + """, + + # Insufficient information + """ + Tech event next week. More details coming soon! + """ +] -class Armor(str, Enum): - leather = "leather" - chainmail = "chainmail" - plate = "plate" +# Process events +results = [] +for description in event_descriptions: + prompt = f""" +<|im_start>system +You are a helpful assistant +<|im_end|> +<|im_start>user +Extract structured information about this tech event: +{description} -class Character(BaseModel): - name: constr(max_length=10) - age: int - armor: Armor - weapon: Weapon - strength: int +If there is enough information, return a JSON object with the following fields: +- name: The name of the event +- date: The date where the event is taking place +- location: Where the event is taking place +- event_type: either 'conference', 'webinar', 'workshop', 'meetup' or 'other' +- topics: a list of topics of the conference +- registration_required: a boolean that indicates whether registration is required -model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") +If the information available does not allow you to fill this JSON, and only then, answer 'I don't know'. +<|im_end|> +<|im_start|>assistant +""" + # Union type allows the model to return structured data or "I don't know" + result = model(prompt, EventResponse, max_new_tokens=200) + results.append(result) + +# Display results +for i, result in enumerate(results): + print(f"Event {i+1}:") + if isinstance(result, str): + print(f" {result}") + else: + # It's an EventInfo object + print(f" Name: {result.name}") + print(f" Type: {result.event_type}") + print(f" Date: {result.date}") + print(f" Topics: {', '.join(result.topics)}") + print() + +# Use structured data in downstream processing +structured_count = sum(1 for r in results if isinstance(r, EventInfo)) +print(f"Successfully extracted data for {structured_count} of {len(results)} events") +``` +
-# Construct structured sequence generator -generator = outlines.generate.json(model, Character) +
🗂️ Categorize documents into predefined types +
In this case, outlines classifies documents into predefined categories (e.g., “Financial Report,” “Legal Contract”) using a literal type specification. The resulting classifications are displayed in both a table format and through a category distribution summary, illustrating how structured outputs can simplify content management. +
-# Draw a sample -seed = 789001 +```python +import outlines +from typing import Literal, List +import pandas as pd +from transformers import AutoTokenizer, AutoModelForCausalLM -character = generator("Give me a character description", seed=seed) -print(repr(character)) -# Character(name='Anderson', age=28, armor=, weapon=, strength=8) +MODEL_NAME = "microsoft/Phi-3-mini-4k-instruct" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) +) -character = generator("Give me an interesting character description") -print(repr(character)) -# Character(name='Vivian Thr', age=44, armor=, weapon=, strength=125) -``` +# Define classification categories using Literal +DocumentCategory = Literal[ + "Financial Report", + "Legal Contract", + "Technical Documentation", + "Marketing Material", + "Personal Correspondence" +] -The method works with union types, optional types, arrays, nested schemas, etc. Some field constraints are [not supported yet](https://github.com/dottxt-ai/outlines/issues/215), but everything else should work. +# Sample documents to classify +documents = [ + "Q3 Financial Summary: Revenue increased by 15% year-over-year to $12.4M. EBITDA margin improved to 23% compared to 19% in Q3 last year. Operating expenses...", -### Efficient JSON generation following a JSON Schema + "This agreement is made between Party A and Party B, hereinafter referred to as 'the Parties', on this day of...", -Sometimes you just want to be able to pass a JSON Schema instead of a Pydantic model. We've got you covered: + "The API accepts POST requests with JSON payloads. Required parameters include 'user_id' and 'transaction_type'. The endpoint returns a 200 status code on success." +] -``` python -import outlines +template = outlines.Template.from_string(""" +<|im_start|>user +Classify the following document into exactly one category among the following categories: +- Financial Report +- Legal Contract +- Technical Documentation +- Marketing Material +- Personal Correspondence + +Document: +{{ document }} +<|im_end|> +<|im_start|>assistant +""") -schema = '''{ - "title": "Character", - "type": "object", - "properties": { - "name": { - "title": "Name", - "maxLength": 10, - "type": "string" - }, - "age": { - "title": "Age", - "type": "integer" - }, - "armor": {"$ref": "#/definitions/Armor"}, - "weapon": {"$ref": "#/definitions/Weapon"}, - "strength": { - "title": "Strength", - "type": "integer" - } - }, - "required": ["name", "age", "armor", "weapon", "strength"], - "definitions": { - "Armor": { - "title": "Armor", - "description": "An enumeration.", - "enum": ["leather", "chainmail", "plate"], - "type": "string" - }, - "Weapon": { - "title": "Weapon", - "description": "An enumeration.", - "enum": ["sword", "axe", "mace", "spear", "bow", "crossbow"], - "type": "string" - } - } -}''' +# Classify documents +def classify_documents(texts: List[str]) -> List[DocumentCategory]: + results = [] -model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct") -generator = outlines.generate.json(model, schema) -character = generator("Give me a character description") -``` + for text in texts: + prompt = template(document=text) + # The model must return one of the predefined categories + category = model(prompt, DocumentCategory, max_new_tokens=200) + results.append(category) -### Using context-free grammars to guide generation + return results -Formal grammars rule the world, and Outlines makes them rule LLMs too. You can pass any context-free grammar in the EBNF format and Outlines will generate an output that is valid to this grammar: +# Perform classification +classifications = classify_documents(documents) -``` python +# Create a simple results table +results_df = pd.DataFrame({ + "Document": [doc[:50] + "..." for doc in documents], + "Classification": classifications +}) + +print(results_df) + +# Count documents by category +category_counts = pd.Series(classifications).value_counts() +print("\nCategory Distribution:") +print(category_counts) +``` +
+ +
+📅 Schedule a meeting from requests with Function Calling +
This example demonstrates how outlines can interpret a natural language meeting request and translate it into a structured format matching a predefined function’s parameters. Once the meeting details are extracted (e.g., title, date, duration, attendees), they are used to automatically schedule the meeting. +
+ +```python import outlines +import json +from typing import List, Optional +from datetime import date +from transformers import AutoTokenizer, AutoModelForCausalLM -arithmetic_grammar = """ - ?start: expression - ?expression: term (("+" | "-") term)* +MODEL_NAME = "microsoft/phi-4" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) +) - ?term: factor (("*" | "/") factor)* - ?factor: NUMBER - | "-" factor - | "(" expression ")" +# Define a function with typed parameters +def schedule_meeting( + title: str, + date: date, + duration_minutes: int, + attendees: List[str], + location: Optional[str] = None, + agenda_items: Optional[List[str]] = None +): + """Schedule a meeting with the specified details""" + # In a real app, this would create the meeting + meeting = { + "title": title, + "date": date, + "duration_minutes": duration_minutes, + "attendees": attendees, + "location": location, + "agenda_items": agenda_items + } + return f"Meeting '{title}' scheduled for {date} with {len(attendees)} attendees" - %import common.NUMBER +# Natural language request +user_request = """ +I need to set up a product roadmap review with the engineering team for next +Tuesday at 2pm. It should last 90 minutes. Please invite john@example.com, +sarah@example.com, and the product team at product@example.com. """ -model = outlines.models.transformers("WizardLM/WizardMath-7B-V1.1") -generator = outlines.generate.cfg(model, arithmetic_grammar) -sequence = generator("Alice had 4 apples and Bob ate 2. Write an expression for Alice's apples:") +# Outlines automatically infers the required structure from the function signature +prompt = f""" +<|im_start|>user +Extract the meeting details from this request: -print(sequence) -# (8-2) -``` +{user_request} +<|im_end|> +<|im_start|>assistant +""" +meeting_params = model(prompt, schedule_meeting, max_new_tokens=200) -This was a very simple grammar, and you can use `outlines.generate.cfg` to generate syntactically valid Python, SQL, and much more than this. Any kind of structured text, really. All you have to do is search for "X EBNF grammar" on the web, and take a look at the [Outlines `grammars` module](https://github.com/dottxt-ai/outlines/tree/main/outlines/grammars). +# The result is a dictionary matching the function parameters +meeting_params = json.loads(meeting_params) +print(meeting_params) -### Open functions +# Call the function with the extracted parameters +result = schedule_meeting(**meeting_params) +print(result) +# "Meeting 'Product Roadmap Review' scheduled for 2023-10-17 with 3 attendees" +``` +
-Outlines can infer the structure of the output from the signature of a function. The result is a dictionary, and can be passed directly to the function using the usual dictionary expansion syntax `**`: +
+📝 Dynamically generate prompts with re-usable templates +
Using Jinja-based templates, this example shows how to generate dynamic prompts for tasks like sentiment analysis. It illustrates how to easily re-use and customize prompts—including few-shot learning strategies—for different content types while ensuring the outputs remain structured. +
```python import outlines +from typing import List, Literal +from transformers import AutoTokenizer, AutoModelForCausalLM -def add(a: int, b: int): - return a + b +MODEL_NAME = "microsoft/phi-4" +model = outlines.from_transformers( + AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"), + AutoTokenizer.from_pretrained(MODEL_NAME) +) -model = outlines.models.transformers("WizardLM/WizardMath-7B-V1.1") -generator = outlines.generate.json(model, add) -result = generator("Return json with two integers named a and b respectively. a is odd and b even.") -print(add(**result)) -# 3 -``` +# 1. Create a reusable template with Jinja syntax +sentiment_template = outlines.Template.from_string(""" +<|im_start>user +Analyze the sentiment of the following {{ content_type }}: -A great advantage of passing functions directly to specify the structure is that the structure of the LLM will change with the function's definition. No need to change the code at several places! +{{ text }} -You can also embed various functions into an enum to generate params: +Provide your analysis as either "Positive", "Negative", or "Neutral". +<|im_end> +<|im_start>assistant +""" -```python -from enum import Enum -from functools import partial +# 2. Generate prompts with different parameters +review = "This restaurant exceeded all my expectations. Fantastic service!" +prompt = sentiment_template(content_type="review", text=review) -import outlines +# 3. Use the templated prompt with structured generation +result = model(prompt, Literal["Positive", "Negative", "Neutral"]) +print(result) # "Positive" +# Templates can also be loaded from files +example_template = outlines.Template.from_file("templates/few_shot.txt") -def add(a: int, b: int) -> int: - return a + b +# Use with examples for few-shot learning +examples = [ + ("The food was cold", "Negative"), + ("The staff was friendly", "Positive") +] +few_shot_prompt = example_template(examples=examples, query="Service was slow") +print(few_shot_prompt) +``` +
-def mul(c: float, d: float) -> float: - return c * d +## They use outlines -class Operation(Enum): - add = partial(add) - mul = partial(mul) +
+Users Logo +Users Logo +
-model = outlines.models.transformers("WizardLM/WizardMath-7B-V1.1") -generator = outlines.generate.json(model, Operation) -result = generator("Return json with two float named c and d respectively. c is negative and d greater than 1.0.") +## Model Integrations -print(result) -# {'c': -3.14, 'd': 1.5} -``` +| Model type | Description | Documentation | +|---------|-------------|:-------------:| +| **Server Support** | vLLM and Ollama | [Server Integrations →](https://dottxt-ai.github.io/outlines/latest/installation) | +| **Local Model Support** | transformers and llama.cpp | [Model Integrations →](https://dottxt-ai.github.io/outlines/latest/installation) | +| **API Support** | OpenAI and Gemini | [API Integrations →](https://dottxt-ai.github.io/outlines/latest/installation) | -## Prompting +## Core Features -Building prompts can get messy. **Outlines** makes it easier to write and manage -prompts by encapsulating templates inside "template functions". Template -functions use the Jinja2 templating engine to help build complex prompts in a -concise manner. +| Feature | Description | Documentation | +|---------|-------------|:-------------:| +| **Multiple Choices** | Constrain outputs to predefined options | [Multiple Choices →](https://dottxt-ai.github.io/outlines/latest/guides/choice_generation) | +| **️Function Calls** | Infer structure from function signatures | [Function Guide →](https://dottxt-ai.github.io/outlines/latest/guides/function_generation) | +| **JSON/Pydantic** | Generate outputs matching JSON schemas | [JSON Guide →](https://dottxt-ai.github.io/outlines/latest/guides/json_generation) | +| **Regular Expressions** | Generate text following a regex pattern | [Regex Guide →](https://dottxt-ai.github.io/outlines/latest/guides/regex_generation) | +| **Grammars** | Enforce complex output structures | [Grammar Guide →](https://dottxt-ai.github.io/outlines/latest/guides/cfg_generation) | -Template functions are created by loading a Jinja2 template from a text file. -Assume you have the following prompt template defined in `prompt.txt`: +## Other Features -``` text -You are a sentiment-labelling assistant. +| Feature | Description | Documentation | +|---------|-------------|:-------------:| +| **Prompt templates** | Separate complex prompts from code | | +| **Custome types** | Intuitive interface to build complex types | | +| **Applications** | Encapsulate templates and types into functions | | -{% for example in examples %} -{{ example[0] }} // {{ example[1] }} -{% endfor %} -{{ to_label }} // -``` +## About .txt -You can then load it and call it with: +
+dottxt logo +dottxt logo +
-``` python -import outlines +Outlines is developed and maintained by [.txt](https://dottxt.co), a company dedicated to making LLMs more reliable for production applications. -examples = [ - ("The food was disgusting", "Negative"), - ("We had a fantastic night", "Positive"), - ("Recommended", "Positive"), - ("The waiter was rude", "Negative") -] +Our focus is on advancing structured generation technology through: -labelling = outlines.Template.from_file("prompt.txt") -prompt = labelling("Just awesome", examples) -``` +- 🧪 **Cutting-edge Research**: We publish our findings on [structured generation](http://blog.dottxt.co/performance-gsm8k.html) +- 🚀 **Enterprise-grade solutions**: You can license [our enterprise-grade libraries](https://docs.dottxt.co). +- 🧩 **Open Source Collaboration**: We believe in building in public and contributing to the community + +Follow us on [Twitter](https://twitter.com/dottxtai) or check out our [blog](https://blog.dottxt.co/) to stay updated on our latest work in making LLMs more reliable. -This helps: +## Community -- Keep content separate from the code -- Design "white space perfect" prompts +
-It is more maintainable and means prompts can be versioned separately from the code. +[![Contributors][contributors-badge]][contributors] +[![Stars][stars-badge]][stars] +[![Downloads][downloads-badge]][pypistats] +[![Discord badge][discord-badge]][discord] -## Join us +
- 💡 **Have an idea?** Come chat with us on [Discord][discord] -- 🔨 **Want to contribute?** Consult our [contribution guide](https://dottxt-ai.github.io/outlines/latest/community/contribute/). - 🐞 **Found a bug?** Open an [issue](https://github.com/dottxt-ai/outlines/issues) +- 🧩 **Want to contribute?** Consult our [contribution guide](https://dottxt-ai.github.io/outlines/latest/community/contribute/). ## Cite Outlines ``` @article{willard2023efficient, - title={Efficient Guided Generation for LLMs}, + title={Efficient Guided Generation for Large Language Models}, author={Willard, Brandon T and Louf, R{\'e}mi}, journal={arXiv preprint arXiv:2307.09702}, year={2023} } ``` -[documentation]: https://dottxt-ai.github.io/outlines/latest/welcome/ -[documentation-badge]: https://img.shields.io/readthedocs/outlines [contributors]: https://github.com/dottxt-ai/outlines/graphs/contributors [contributors-badge]: https://img.shields.io/github/contributors/dottxt-ai/outlines?style=flat-square&logo=github&logoColor=white&color=ECEFF4 +[dottxt-blog]: https://blog.dottxt.co/ +[dottxt-blog-badge]: https://img.shields.io/badge/dottxt%20blog-a6b4a3 [dottxt-twitter]: https://twitter.com/dottxtai +[dottxt-twitter-badge]: https://img.shields.io/twitter/follow/dottxtai?style=social [discord]: https://discord.gg/R9DSu34mGd -[discord-badge]: https://img.shields.io/discord/1182316225284554793?color=81A1C1&logo=discord&logoColor=white&style=flat-square -[downloads-badge]: https://img.shields.io/pypi/dm/outlines?color=89AC6B&logo=python&logoColor=white&style=flat-square +[discord-badge]: https://img.shields.io/discord/1182316225284554793?color=ddb8ca&logo=discord&logoColor=white&style=flat-square +[downloads-badge]: https://img.shields.io/pypi/dm/outlines?color=A6B4A3&logo=python&logoColor=white&style=flat-square [pypistats]: https://pypistats.org/packages/outlines -[dottxt-twitter-badge]: https://img.shields.io/twitter/follow/dottxtai?style=social -[youtube-dottxt]: https://www.youtube.com/@dottxt-ai -[blog-dottxt]: https://blog.dottxt.co/ +[pypi-version-badge]: https://img.shields.io/pypi/v/outlines?style=flat-square&logoColor=white&color=ddb8ca +[pypi]: https://pypi.org/project/outlines/ +[stars]: https://github.com/dottxt-ai/outlines/stargazers +[stars-badge]: https://img.shields.io/github/stars/dottxt-ai/outlines?style=flat-square&logo=github&color=BD932F&logoColor=white +[twitter-badge]: https://img.shields.io/twitter/follow/dottxtai?style=flat-square&logo=x&logoColor=white&color=bd932f +[twitter]: https://x.com/dottxtai diff --git a/benchmarks/bench_cfg_guide.py b/benchmarks/bench_cfg_guide.py index 477104435..a4d288700 100644 --- a/benchmarks/bench_cfg_guide.py +++ b/benchmarks/bench_cfg_guide.py @@ -4,7 +4,7 @@ import outlines.grammars from outlines.caching import cache_disabled -from outlines.fsm.guide import CFGGuide +from outlines.processors.guide import CFGGuide from outlines.models.transformers import TransformerTokenizer random.seed(42) diff --git a/benchmarks/bench_json_schema.py b/benchmarks/bench_json_schema.py index 3a1f72cb6..6ed223ef4 100644 --- a/benchmarks/bench_json_schema.py +++ b/benchmarks/bench_json_schema.py @@ -1,7 +1,7 @@ from outlines_core.fsm.json_schema import build_regex_from_schema from outlines.caching import cache_disabled -from outlines.fsm.guide import RegexGuide +from outlines.processors.guide import RegexGuide from .common import setup_tokenizer # noqa: E402 diff --git a/benchmarks/bench_regex_guide.py b/benchmarks/bench_regex_guide.py index fa23a724f..14b15da89 100644 --- a/benchmarks/bench_regex_guide.py +++ b/benchmarks/bench_regex_guide.py @@ -1,5 +1,5 @@ from outlines.caching import cache_disabled -from outlines.fsm.guide import RegexGuide +from outlines.processors.guide import RegexGuide from .common import setup_tokenizer diff --git a/docs/api/guide.md b/docs/api/guide.md deleted file mode 100644 index 1c3160c8c..000000000 --- a/docs/api/guide.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.fsm.guide diff --git a/docs/api/json_schema.md b/docs/api/json_schema.md deleted file mode 100644 index 471cb3a80..000000000 --- a/docs/api/json_schema.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.fsm.json_schema diff --git a/docs/api/models.md b/docs/api/models.md deleted file mode 100644 index cb9497be1..000000000 --- a/docs/api/models.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.models diff --git a/docs/api/parsing.md b/docs/api/parsing.md deleted file mode 100644 index e9662999c..000000000 --- a/docs/api/parsing.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.fsm.parsing diff --git a/docs/api/regex.md b/docs/api/regex.md deleted file mode 100644 index 5ef91db46..000000000 --- a/docs/api/regex.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.generate.regex diff --git a/docs/api/samplers.md b/docs/api/samplers.md deleted file mode 100644 index 2b9b34234..000000000 --- a/docs/api/samplers.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.samplers diff --git a/docs/api/templates.md b/docs/api/templates.md deleted file mode 100644 index 6150512d4..000000000 --- a/docs/api/templates.md +++ /dev/null @@ -1 +0,0 @@ -::: outlines.templates diff --git a/docs/api/index.md b/docs/api_reference/index.md similarity index 100% rename from docs/api/index.md rename to docs/api_reference/index.md diff --git a/docs/assets/images/install.png b/docs/assets/images/install.png new file mode 100644 index 0000000000000000000000000000000000000000..2b89b72680c6dff599fc96dc5a2cbfe014a48903 GIT binary patch literal 12669 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 literal 3323 zcmc&%X;4#H8V#{is_w1(z5AW- zob%m#;|n+E9b5Ib!eFo+E@w`C34>`IP>;WAZc@(>$KYu7XG_c(-*^}dfq6YNt}w}f zdU8*KlTU&NCM*GaG42u!i^ZCL6^)7yxfpZF6cZO-JZ-NBgKfX=a_YF}`+eAUmyrKmLb{CB12HfHM?{V9U{^z0`Gzi9{!uN0hf+E4jogvp<;o;har zjV9&Jt#3=6Dv(xJzd$v3?(?Mh&?eoi>9-4aoIvl|I|$jt8=8GLfDA3NUvJq8MIp^@ zH!sblC}Mi?a_lUtVH6+eVx3oOKZ+Zn4GI@T7{?a*Lu2PKYj1Bu3J8Bzagzypk6!$+*)wf+>a${62jmYG-F0oDm@c0nE8F_Q03lg4EMPry zGFZNupY9vDJPWRVr;k3p%m>N9-2XiM^&$7ont-@wBcW zI;fYzmLXlS)wvS+!fb^usNLFFu&Fg|(0Fav7w2yyi8GCdAxYG}v^O%-=1@v_(_Flf|*18#z8^RqGpQ6lG#p=|hsg*ok1-eVRd=0mxe?VftmFI_r`O$aX zc4xGW3s|BjhyYJUtjOW=S5z-;gQKyc418*DrzjA?QNAp24#RQz298ZD>G_2{s=O3; zn%v*Wk|Im9Ymh%==dc29Y#@U;?h`M_jx?HKu%LWM(6=Oa^f0)JymK^RrnX-s-zQaE zg0w8>&YjWR@X7m~=S_mc?jTtCT+TMtakX({Y&JvJGawG3q+ckD;7ubetf*WqjG^6} zO@=dsD(3bbLF45t!5;7Z?I1XaNKlslFq%Gj5OI(21K69xiqf}>3!=$y-OcbTMI;16 zD|h#RKN;=YdFq2c7*kLOS`%xR-DFC7l|oQS8(1CdyJ76uGAJzZbx=atVENv{NuZqU zi|^`YMb*oS)FL6~uo{?)!*h>}39+FQXZQO60AnPOD=o$CNb2Jur2WUU{Bt3lYmwBW z%Y9XMzl|~sO2$uxw0`!tA#rggb9L)@{?2zT`JKw&bM*ljI;|W({0Ij>^Hhgg*{_Q2 z9r#kktE4C&}-%#mld%()uhT7S~$`lNqG$QE9FI+bQGGxYeh8jNHx8ogVwzaY}s1&RRp z+6wg`@JA3Fg;#kI-5Hz2|4IVJik>j$0R^YZBVz|tN93-R@>wGGn&75`FUU5-@h`gbyv<3 zXIp@lwKqQ|oJ>KzF8EUvAt_lh+wUPYKdkmIup@&p#PGmq*!kQ4l6Ut1kT<}u2HEb6 zjpAyI8>4;vSRELdjD|`*q_7tIkFIa?S};7xTrmVj8m;G1mr(2$RZT}X>n_GY76tji zi!_p?{n92?+d(0F^pJ%m`Tj*+_){dn&eNies%Bi&&cg<{xeqcP+{!yp>p;Ls*%}~d zoyT9I4}`;cSrsdI_1OqMs(g`ynC+0WkJx ooF=m)A%l%qHm_eBt7g+qwHY~_RyAv@A1*K#C%02ICoWw57oI%f82|tP diff --git a/docs/assets/images/logo-square.svg b/docs/assets/images/logo-square.svg index efc68a642..2c1b9321e 100644 --- a/docs/assets/images/logo-square.svg +++ b/docs/assets/images/logo-square.svg @@ -1,16 +1,16 @@ + inkscape:current-layer="g39" + inkscape:pageshadow="2" + showgrid="false" /> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/readme-dark.png b/docs/assets/images/readme-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d4526256d462dde768cbcbde886afe07bea2769a GIT binary patch literal 38912 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 UqCg!_2!QK_vc_{1*dqLY09x|HegFUf literal 0 HcmV?d00001 diff --git a/docs/assets/images/readme-light.png b/docs/assets/images/readme-light.png new file mode 100644 index 0000000000000000000000000000000000000000..f59fe8843787c25f5cd994e7170616561a481df2 GIT binary patch literal 38943 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 aq%0s;ckOXwX!@4bkEN>M;Sq<1MvAdt`rih_VN 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%0AKxTIss6DQ2fQ77sr$ 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